These days you can do a lot of stuff without ever needing a backend. But there are always those moments where you need a really simple backend: authenticating users, sharing information across users and devices… Personally, being mostly a front-end developer, I can deal with coding a simple Node.js backend, but I find it very cumbersome having to deal with deployments, databases, and servers. That's why serverless can be an interesting option to add that bit of functionality to your mostly front-end based projects. Yeah, there's a server somewhere but if you don't have to deal with it, does it matter?
To use a real-life example, recently I had to build a web application that uses the GitHub API. For this, I need a token, and GitHub does not allow client-side authentication. At first, I considered using Gatekeeper, which can be easily deployed to Heroku, but I felt this could be a good opportunity to learn serverless. So, in this post, we will learn how to handle login to a GitHub OAuth application to get our token!
The way the whole process works is this:
- First, we create an OAuth app on GitHub. For the URLs, for now, you can use http://localhost:8080/ or wherever you are running your application.
- In your front-end application, you add a link pointing to
https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirect_uri}
. Let's talk about this two variables:-
clientId
: you get it after creating the OAuth app -
redirect_uri
: is the authorization callback URL you set in the app settings. It's basically a route in your front-end application that will handle the token exchange.
-
- When the user clicks this link, they will be redirected to GitHub to accept the application.
- After the user accepts to log in to your application, GitHub will redirect them back to the
redirect_uri
, with acode
query parameter that you can use to exchange for a token. This is the step that cannot be done client-side, so we will create a serverless function to exchange the code for our token.
Phew! That seems quite a lot of work to a simple login, but fear not, together we can do this!
First, a few concepts:
AWS
We'll be using Amazon Web Services. Why? I tried it and I liked it so I haven't checked any other providers. So you'll need to create an AWS account. You might be tempted to start checking out the documentation and services available but I suggest you don't (at least for now). The first time I logged into the AWS console I was so overwhelmed by the amount of information that I thought I would never be able to make anything work with that.
Me closing the console tab right after opening it
Luckily I found that you don't have to check it too often, as in the next section we will see a clearer way to access that information.
AWS offers a lot of services but we'll focus on these:
Lambda
As you may have guessed, AWS Lambda are just functions. JavaScript functions! (Actually you can write them in a few languages but we're front-end developers so we'll stick to what we know). And you can pretty much do anything you want there: make requests to an API, get data from a database… keeping in mind that it must run within the limitations.
API Gateway
But how to run these functions? Since we'll probably want to run them from your front-end application, we'll create an API (actually we won't have to do anything, the Serverless framework will), and each endpoint will trigger a Lambda.
Parameter Store
You might need to store some environment variables that you do not want to commit to the repository, because of security reasons or just because it depends on the environment, like the OAuth id and secret. There are a few ways to do that (you can add environment variables manually to each Lambda, there's also AWS Secret Manager), but I find the Parameter Store the easiest to deal with.
After this quick overview, let's get started!
The Serverless framework
Setting up all these services might look like a lot of work and the whole point of this post is to use serverless to avoid complexity, and that's where the Serverless framework comes into action! As its name suggests, it's a framework that helps us set up serverless services easily. We will just fill in our desired configuration on a yml
file, and once we deploy we will be able to monitor everything easily on the dashboard. The framework itself is open source, the dashboard service can be used with the free plan.
So, let's get started! Create an account and follow the steps to install and login via CLI. After that it's really easy to create a base project:
serverless create --template aws-nodejs
You will see that it's just two files:
-
serverless.yml
: the configuration of your service. You will see it's full of useful comments, and you can check all the available properties in the documentation. -
handler.js
: your actual code
For now, we will update the serverless.yml
, add app name and organization.
We will also edit the hello world function (that's what will become our Lambda) so it becomes a login
function! The events
bit under the function handler creates an API Gateway endpoint to trigger our function (we set it to use the POST
method and enable cors
). It should look something like this:
service: github-login
app: github-login
org: your-organization
provider:
name: aws
runtime: nodejs10.x
memorySize: 1024
region: us-east-1
functions:
login:
handler: login.handler
events:
- http:
path: login
method: post
cors: true
Tip: Keep in mind that handlers must be named exports, it doesn't work with default exports. If you can't come up with a name for the export, the convention is to name the export handler
.
Here is the code of our login.js
file. We will be using axios to make the request to GitHub so make sure to npm init
to create your package.json
file, and npm i axios
"use strict";
const qs = require("querystring");
const axios = require("axios");
function authenticate(code) {
const data = qs.stringify({
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
code
});
return axios
.post("https://github.com/login/oauth/access_token", data, {
headers: {
"content-length": data.length
}
})
.then(({ data }) => {
return {
token: qs.parse(data).access_token
};
})
.catch(e => ({
error: e.message
}));
}
module.exports.handler = async event => {
const body = JSON.parse(event.body);
const result = {
error: null,
token: null
};
if (body) {
const { error, token } = await authenticate(body.code);
if (error || !token) {
result.error = error || "bad_code";
} else {
result.token = token;
}
} else {
result.error = "no_code";
}
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true
},
body: JSON.stringify(result, null, 2)
};
};
But, as we can see we need two environment variables: CLIENT_ID
and CLIENT_SECRET
(remember that we can get those when creating an OAuth app under developer settings). We know that committing environment variables into a repo is a big NO, so how can we handle this? Read on!
Handling environment variables
This is where the parameter store will come in handy since most probably the environment variables will be different depending on the stage (OAuth apps require you to set an authorization callback URL, and this will likely be different in development and production so you will need to create two apps).
You can create new variables on the AWS console: https://console.aws.amazon.com/systems-manager/parameters/create. Create the dev
and prod
parameters for CLIENT_ID
and CLIENT_SECRET
following this structure: /app-name/stage/parameter-name
(so /github-login/dev/clientId
and so on). You can set variables to be encrypted by selecting the SecureString
type.
In your serverless.yml
file you will set the environment
variables to be retrieved from the custom
section, which will point to correct the parameter store variable. If you chose SecureString
type, make sure to add ~true
when referencing it:
provider:
name: aws
runtime: nodejs10.x
stage: ${opt:stage,'dev'}
...
environment:
CLIENT_ID: ${self:custom.clientId.${self:provider.stage}}
CLIENT_SECRET: ${self:custom.clientSecret.${self:provider.stage}}
custom:
clientId:
dev: ${ssm:/github-login/dev/clientId}
prod: ${ssm:/github-login/prod/clientId}
clientSecret:
dev: ${ssm:/github-login/dev/clientSecret~true}
prod: ${ssm:/github-login/prod/clientSecret~true}
Keep in mind that even though the default stage is dev
, the self:provider.stage
bit will not get the stage correctly unless you add the stage: ${opt:stage,'dev'}
(I got stuck a pretty long time trying to debug that!)
This blogpost covers how to manage environment variables per stage in a bit more detail.
Now we have all the code for our application and we are ready to deploy! If you got stuck at any part, the full code is on GitHub.
Deploying
Now our login function is ready to be deployed! You can choose where to deploy using the stage
option (if you don't pass any stage, the default will be dev
):
serverless deploy
serverless deploy --stage prod
Easy huh. After the command ends you will see all the service information, including the endpoint you will need to use from your frontend application. It should look something like https://r4nd0m1d.execute-api.us-east-1.amazonaws.com/dev/login
. Feel free to test it using cURL or something like Postman. Even if you don't send a valid code
in the body, you can see how it handles the error messages.
You can check the dashboard to see the function invocations and check if there are errors (the UI/UX is way more friendly than having to check the AWS console!):
Next steps!
I hope this was a helpful introduction to serverless! Now feel free to go back to the comfort of front-end development and use the token to start working with the GitHub API. Maybe create some data visualization with your commits per repo, or the most popular languages of your followers?
Or, if you haven't got enough of serverless, and you want to keep learning, I recommend checking out this guide, and learn by doing! Maybe try creating a REST API with DynamoDB, or maybe try GraphQL (we love it, if you need an introduction to GraphQL I recommend Anna's post). And if Node or AWS are not your cup of tea, there are tons of other examples!
But of course, the best thing you can do now is to relax for a while, you've earned it!
Never forget old Yoda
Cover photo by chuttersnap