Create your own Serverless OAuth Portal for Netlify CMS
Tags: javascript, aws, lambda, netlify
Published: 2020-04-09 00:49:53

# Contents
- [Acknowledgement](#acknowledgement)
- [Prerequisites](#prerequisites)
- [Get your frontend running](#get-your-frontend-running)
- [Create the GitHub OAuth App](#create-the-github-oauth-app)
- [Save GitHub OAuth App credentials somewhere secure](#save-github-oauth-app-credentials-somewhere-secure)
- [Create your OAuth Lambdas](#create-your-oauth-lambdas)
- [Triggering Lambdas](#triggering-lambdas)
- [Write some OAuth code](#write-some-oauth-code)
- [From local to remote](#from-local-to-remote)
- [Test your OAuth Lambdas](#test-your-oauth-lambdas)
- [Start up your local frontend](#start-up-your-local-frontend)
- [Login into your local CMS backend](#login-into-your-local-cms-backend)


# Acknowledgement
Before starting this post, I need to give a big shout out to [Mark Steele](https://github.com/marksteele) whos Serverless solution is actually the basis of this post and you'll even be using some of the code from his repository, [Netlify Serverless OAuth2 Backend](https://github.com/marksteele/netlify-serverless-oauth2-backend).


# Prerequisites
- [GitHub Account](https://github.com/)
- [AWS Account](https://aws.amazon.com/)
- [AWS CLI](https://aws.amazon.com/cli/)
- Basic knowledge of AWS is helpful but not necessary


# Get your frontend running
Before we can worry about authenticating users to allow them to create content for our site, we first need a site to begin with. Head on over to the [Netlify CMS one-click solutions](https://www.netlifycms.org/docs/start-with-a-template/) page and choose a starter template. For the purposes of this post, we're going to use the [One Click Hugo CMS](https://app.netlify.com/start/deploy?repository=https://github.com/netlify-templates/one-click-hugo-cms&stack=cms) template for no other reason than that is the template I am most familiar with. Choose your template and follow the instructions. In just a moment, you should land on your new websites' dashboard page.

Congrats, in just a few simple clicks you now have a website that you can use to start creating blog posts, pages, etc.

![Dashboard for your new website](https://dev-to-uploads.s3.amazonaws.com/i/rplrdxgb4uvitj1cyh8r.PNG)


# Create the GitHub OAuth App
Our next step is to create a [GitHub OAuth Application](https://developer.github.com/apps/building-oauth-apps/). Alternatively, [you can follow along with on the GitHub website](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) or you can follow the next bit of instructions.

On GitHub, click on your profile picture in the top right corner of GitHub and at the bottom of the dropdown click on "Settings". On this page go ahead and click on "Developer Settings" on the bottom left of the navigation menu on the left hand side of the page. On the next page choose "OAuth Apps" and then click on the "New OAuth App" button on the top right of the page. Go ahead and fill in the form and click on the "Register application" button on the bottom left.

![GitHub Register OAuth App](https://dev-to-uploads.s3.amazonaws.com/i/7mir1pqma5750d5a8mhp.png)


# Save GitHub OAuth App credentials somewhere secure
Now that we have our OAuth application we need to store the sensitive information that was generated with it, **Client ID** and **Client Secret**. You need to treat these values as if they were your very own credentials into your bank account meaning **do not share these with anyone**.

Leave this browser tab open as we'll need those values in just a moment. Open a new tab and navigate to `https://aws.amazon.com/` and click on the "Sign In to the Console" button at the top right of the page.

![Sign into AWS console](https://dev-to-uploads.s3.amazonaws.com/i/cmhhx8gy98ugydbu26tq.png)

After you login use the "Find Services" search bar and search for "Secrets Manager" and click on the resulting search.

![AWS Search SecretsManager](https://dev-to-uploads.s3.amazonaws.com/i/zd3v9iab4q0na06rsv1l.png)

On the next page you need to click the "Store a new secret" button in the top right corner.

![AWS store new secret](https://dev-to-uploads.s3.amazonaws.com/i/yuz385vzlovg8rfwseqj.png)

Fill in the form adding two new "Secret key/value" pairs as shown in the image below and click "Next" at the bottom right.

![AWS add secret](https://dev-to-uploads.s3.amazonaws.com/i/xmxhrd6mn3b0ia5dli52.png)

Fill in the next form as well and click "Next" at the bottom right of the page.

![AWS store secrets](https://dev-to-uploads.s3.amazonaws.com/i/ckzzzpgxgtvar52ugnks.png)

Leave this next page on its default settings and click "Next".

![AWS SecretsManager default settings](https://dev-to-uploads.s3.amazonaws.com/i/fbdppd5fazrhtmgr34vi.png)

Finally, just scroll to the very bottom and click the "Store" button on the bottom right.


# Create your OAuth Lambdas
This portion might sound daunting, especially if you've never had to handle anything cloud or authentication related but honestly, this part is pretty simple. There is a bit of confusing code but we'll go over it to get a better understanding of what's going on.

Head over to your [AWS Lambda page](https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions) and click on **Create Function** in the top right corner.

![AWS Lambda Create Function button in the top right corner of the page](https://dev-to-uploads.s3.amazonaws.com/i/gw3arxrx7slygo24bpvg.PNG)

On the next screen go ahead and fill in a few of the options just like mine:
- Author from Scratch
- Function Name: CreateYourOwnServerlessOauthPortalForNetlifyCms__redirect (feel free to rename this)
- Runtime: Node.js 12.x

There's no need to create a special role or give this role any special permissions. The default permissions that AWS attaches will be enough for this Lambda.

![Fill in the details for your Lambda](https://dev-to-uploads.s3.amazonaws.com/i/z9dyvghg4pmvb6b4sq96.PNG)

Now let's create a second Lambda with all the same parameters but this time replace `__redirect` with `__callback` and click on the "Choose or create an execution role" dropdown at the bottom left of the page, choose "Use an existing role" and select the role AWS created for the `__redirect` Lambda. If you followed my naming conventions it should be something along the lines of `service-role/CreateYourOwnServerlessOauthPortalForNetlifyCms__r-role-abc123`. We're reusing the same role because both Lambdas need permission to the same resource (Secrets Manager) so we can just reuse the same role and permissions. If needed in the future, you may change the roles or even add policy permissions to them as you see fit.

Great, you now have two Lambdas. From now on we'll refer to the first one as the `__redirect` Lambda and the second as the `__callback` Lambda.

Before we give our Lambdas permission I think it would be a good idea to see a common but easily fixed error. Open up your `__redirect` lambda and replace the code inside with the following:

```js
const AWS = require('aws-sdk')
const secretsManager = new AWS.SecretsManager({ region: 'us-east-1' })

exports.handler = async () => {
    const secrets = await secretsManager.getSecretValue({ SecretId: 'GH_TOKENS' }).promise()
    return {
        statusCode: 200,
        body: JSON.stringify(secrets)
    }
}
```

Hit the "Save" and then the "Test" button at the top and you should receive an error saying:

```json
{
  "errorType": "AccessDeniedException",
  "errorMessage": "User: arn:aws:sts::123123:assumed-role/CreateYourOwnServerlessOauthPortalForNetlifyCms__r-role-abc123/CreateYourOwnServerlessOauthPortalForNetlifyCms__redirect is not authorized to perform: secretsmanager:GetSecretValue on resource: arn:aws:secretsmanager:us-east-1:123123:secret:GH_TOKENS-abc123"
  ... More error message ....
}
```

This error is pretty self explanatory but can be confusing when you receive this in the middle of the stress that is learning AWS. Like I said, the fix is simple and the first step is to choose the "Permissions" tab right above your Lambda code.

![Click Lambda permissions tab](https://dev-to-uploads.s3.amazonaws.com/i/xowaiha85qwse9sz1e9t.png)

Click on the dropdown arrow for the policy already created in the table and choose the "Edit policy" button.

![Edit Lambda policy](https://dev-to-uploads.s3.amazonaws.com/i/lsc0ambx7d1v2q9cag2r.png)

Click on the "(+) Add addutuibak permissions" button on the right hand side of this next page.

![Add additional permissions](https://dev-to-uploads.s3.amazonaws.com/i/4lhosyaimfuxglypy4zf.png)

Click on "Service" and search for "Secrets Manager" and choose the only option available.

![Search for SecretManager service](https://dev-to-uploads.s3.amazonaws.com/i/8jpa75wz3nezxx915sd1.png)

Clck on "Actions", "Access level", and finally choose the "GetSecretValue" check box.

![Set access level to read and retrieve secret values](https://dev-to-uploads.s3.amazonaws.com/i/edcpc5ol3iy4glp3c4ii.png)

Next click on "Resources" and choose the "Specific" radial option then proceed to click "Add ARN" a little to the right of the radial options.

![Add resource-specific ARN](https://dev-to-uploads.s3.amazonaws.com/i/xpgt8lheejocq8cbag75.png)

Go back to your SecretsManager, find stored secret and copy its ARN and paste it into the input opened from the "Add ARN" link.

![Copy secret ARN](https://dev-to-uploads.s3.amazonaws.com/i/ycei6c2s0gg0q6vc94iz.png)

![Specific ARN for secret](https://dev-to-uploads.s3.amazonaws.com/i/8upt8unqmhrxlnjylnyc.png)

Now click "Review policy" and then "Save changes" and you should be good to go. You can always double check by going back to view the policy, click the policy dropdown arrow and making sure it has the "Secrets Manager" policy attached to it.

![SecretManager policy added](https://dev-to-uploads.s3.amazonaws.com/i/wfgthwebtyo1qk6nx6aa.png)

Go back to your `__redirect` Lambda and click the "Test" button and you should now be greeted with a green success card, statusCode 200 and some JSON as the body.


# Triggering Lambdas
Lambda functions are fun on their own but we'll need a way to trigger the code inside to run under certain conditions. For our use case, we just need an endpoint and have it run whenever someone hits that endpoint. Luckily enough, creating API Endpoints through the Lambda UI is really straight forward.

I'm going to explain how to do this for the `__redirect` Lambda but the steps are near identical for both. The only difference is the `__callback` URL will use the API Gateway created from the `__redirect` URL instead of creating a new API Gateway.

Navigate to your `__redirect` Lambda and click on the **"Add trigger"** button on the left side of the page.

![Click on the Add Trigger button on the left side of the page](https://dev-to-uploads.s3.amazonaws.com/i/yqdov5c40dpoqigthhki.PNG)

On the next page just follow along with the image:
- **API Gateway**
- **Create an API**
- **HTTP API**
- **Security: Open**

![Steps on creating an API Gateway](https://dev-to-uploads.s3.amazonaws.com/i/l5y83s8pejzb5p1r1z98.PNG)

Go ahead and navigate to your `__callback` Lambda and create a second trigger, this time choose your previously created API Gateway as the API choice in the second dropdown input.

![Steps to create your second Lambda trigger](https://dev-to-uploads.s3.amazonaws.com/i/en22q6yg5y5zq2tlyuxp.PNG)

You should now have two API endpoints that you can send data to or receive data from.


# Write some OAuth code
Open up your terminal and navigate to where you would like to store your CMS repo. From there I want you to clone your repo and navigate inside. In the root of the repo create a new directory named "OAuthLambdas" and go inside.

```nim
mkdir OAuthLambdas
cd OAuthLambdas
```

Once inside, we need to initialize this directory as a Node project and install the `node-fetch` package using `npm`:

```nim
npm init -y
npm i node-fetch
```

Last, we need to create some new files and directories with the following commands:

```nim
mkdir handlers utils
touch handlers/redirect.js handlers/callback.js utils/authenticateGitHubUser.js utils/callbackHtmlPage.js
```

If done correctly, your OAuthLambdas directory should have the following structure:

```nim
OAuthLambdas/
---- handlers/
    ---- redirect.js
    ---- callback.js
    
---- node_modules/

---- utils/
    ---- authenticateGitHubUser.js
    ---- callbackHtmlPage.js

---- package.json
```

- Open **redirect.js** and place the following code inside

```js
const AWS = require('aws-sdk')

/**
 * Redirects users to our NetlifyCms GitHub OAuth2.0 page
 */
exports.handler = async () => {
    const region = "us-east-1"  // the Region we saved OAuth App Client Id into the AWS SecretsManager
    const secretsManager = new AWS.SecretsManager({ region })  // SecretsManager API
    const SecretId = "GH_TOKENS"  // The Secret container we want to access (Not the values but this holds the values)
    const { SecretString } = await secretsManager.getSecretValue({ SecretId }).promise()  // This gives us all of the values from the Secrets Container
    const { CLIENT_ID } = JSON.parse(SecretString)  // SecretString stores our values as a string so we need to transform it into an object to make it easier to work with
    const Location = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&scope=repo%20user`  // Standard GitHub OAuth URL learn more here: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#1-request-a-users-github-identity
    return {
        statusCode: 302,  // "302" required for AWS Lambda to permit redirects
        headers: { Location }  // "Location" header sets redirect location
    }
}

```

- Open **callback.js** and place the following code inside

```js
const { authenticateGitHubUser } = require('../utils/authenticateGitHubUser')

exports.handler = async (e, _ctx, cb) => {
    try {
        return await authenticateGitHubUser(e.queryStringParameters.code, cb)
    }
    catch (e) {
        return {
            statusCode: 500,
            body: JSON.stringify(e.message)
        }
    }
}
```

- Open **authenticateGitHubUser.js** and place the following code inside

```js
const AWS = require('aws-sdk')
const fetch = require('node-fetch')

const { getScript } = require('./getScript')

async function authenticateGitHubUser(gitHubAuthCode, cb) {
    const region = "us-east-1"
    const client = new AWS.SecretsManager({ region })
    const SecretId = "GH_TOKENS"
    const { SecretString } = await client.getSecretValue({ SecretId }).promise()
    const { CLIENT_ID, CLIENT_SECRET } = JSON.parse(SecretString)
    const postOptions = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify({
            client_id: CLIENT_ID,
            client_secret: CLIENT_SECRET,
            code: gitHubAuthCode
        })
    }
    const data = await fetch('https://github.com/login/oauth/access_token', postOptions)
    const response = await data.json()

    cb(
        null,
        {
            statusCode: 200,
            headers: {
                'Content-Type': 'text/html',
            },
            body: getScript('success', {
                token: response.access_token,
                provider: 'github',
            }),
        },
    )
}

exports.authenticateGitHubUser = authenticateGitHubUser
```

- Open **callbackHtmlPage.js** and place the following code inside

```js
function getScript(mess, content) {
    return `<html><body><script>
    (function() {
        function receiveMessage(e) {
        console.log('authorization:github:${mess}:${JSON.stringify(content)}')
        window.opener.postMessage(
            'authorization:github:${mess}:${JSON.stringify(content)}',
            '*'
        )
        window.removeEventListener("message", receiveMessage, false);
        }
        window.addEventListener("message", receiveMessage, false)
        window.opener.postMessage("authorizing:github", "*")
        })()
    </script></body></html>`;
}

exports.getScript = getScript
```


# From local to remote
We have our Lambdas but only locally. We need an easy way to move that code from our machine to AWS Lambda so we can finally run this code. Finally, this is where the [AWS CLI](https://aws.amazon.com/cli/) comes in handy.

With your terminal open make sure you're in the OAuthLambdas directory. From there you need to run the following commands replacing the `--function-name` values with whatever you've named your Lambdas over on AWS.

```nim
user@group:~$ zip -r ../foo.zip .

zip -r ../OAuthLambdas.zip .

aws lambda update-function-code \
--function-name CreateYourOwnServerlessOauthPortalForNetlifyCms__redirect \
--zip-file fileb://$PWD/../OAuthLambdas.zip

aws lambda update-function-code \
--function-name CreateYourOwnServerlessOauthPortalForNetlifyCms__callback \
--zip-file fileb://$PWD/../OAuthLambdas.zip

rm -rf ../OAuthLambdas.zip
```

On a successful update you should receive some JSON in your terminal similar to the following

```json
{
    "FunctionName": "CreateYourOwnServerlessOauthPortalForNetlifyCms__callback",
    "FunctionArn": "arn:aws:lambda:us-east-1:abc123:function:CreateYourOwnServerlessOauthPortalForNetlifyCms__callback",
    "Runtime": "nodejs12.x",
    "Role": "arn:aws:iam::abc123:role/service-role/CreateYourOwnServerlessOauthPortalForNetlifyCms__c-role-0pttkkqs",
    "Handler": "index.handler",
    "CodeSize": 51768,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2020-04-01T00:36:58.395+0000",
    "CodeSha256": "abc123=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "abc123",
    "State": "Active",
    "LastUpdateStatus": "Successful"
}
```

Go to AWS Lambda in your browser and manually check that both Lambdas have been updated


# Test your OAuth Lambdas
- Open your `__redirect` Lambda
- Change "Handler" input found above the code on the right side to `handlers/redirect.handler`
- Click "Save" in top right corner
- Click "Test" button in top right corner
- Click "Configure test events" from dropdown
- Name the test "RedirectTest"
- Insert following:

Go back to your browser and navigate to your `__redirect` Lambda in AWS. First thing you need to do is change the **Handler** input to match your Lambda. For `__redirect` this value will be `handlers/redirect.handler`. Make sure to click "Save" at the top right of the page.

Before testing this Lambda we need to set the data that will be passed to it. This Lambda is pretty simple and isn't expecting any data. Click on the dropdown input left of the "Test" button and choose "Configure test events" and replace the data inside with an empty object.

![Configure redirect Lambda test](https://dev-to-uploads.s3.amazonaws.com/i/qzjozjdmfjnrtmrrd5zp.png)

Now we need to click "Test" at the top right corner of the page and you *should* be greeted with a nice success message similar to the following:

```json
{
  "statusCode": 302,
  "headers": {
    "Location": "https://github.com/login/oauth/authorize?client_id=abc123&scope=repo%20user"
  }
}
```

Now that we know our `__redirect` Lambda is working as expected lets open up our `__callback` Lambda. Again, we need to change the **Handler** input to match what we're exporting. This time, the value will be `handlers/callback.handler` and click "Save".

Just like in our `__redirect` Lambda, we need to set our test data. Follow the same steps as above only this time we need to pass data to our Lambda. Put the following JSON inside and click "Save".

```json
{
  "queryStringParameters": {
    "code": "abc123"
  }
}
```

Go ahead and click "Test" and if everything was set up correctly you should receive the following success message.

```json
{
  "statusCode": 200,
  "headers": {
    "Content-Type": "text/html"
  },
  "body": "<html><body><script>\n    (function() {\n      function receiveMessage(e) {\n        console.log('authorization:github:success:{\"provider\":\"github\"}')\n        window.opener.postMessage(\n          'authorization:github:success:{\"provider\":\"github\"}',\n          '*'\n        )\n        window.removeEventListener(\"message\", receiveMessage, false);\n      }\n      window.addEventListener(\"message\", receiveMessage, false)\n      window.opener.postMessage(\"authorizing:github\", \"*\")\n      })()\n    </script></body></html>"
}
```

This looks confusing but it means everything is working. If you look at the `body` property you'll notice that it's the same code in our `callbackHtmlPage.js` file.

# Start up your local frontend
- In terminal navigate to root of your project
- In terminal run command `yarn` or `npm i`
- In terminal run `yarn start` or `npm start`
- You will know your project is up and running if your terminal looks similar to the following

We're almost there! I can see the finish line. Last thing to do is to run our CMS locally and successfull authenticate.

Back to your terminal, make sure you're in the root of your project and run the following commands.

```nim
yarn
yarn start
```

Let your dependencies download and let **Hugo** and **Webpack** finish its tasks. When that's complete you should see the following in your terminal.

```nim
                   | EN  
-------------------+-----
  Pages            | 10  
  Paginator pages  |  0  
  Non-page files   |  0  
  Static files     | 43  
  Processed images |  0  
  Aliases          |  1  
  Sitemaps         |  1  
  Cleaned          |  0  

Watching for changes in ~/dev/one-click-hugo-cms-dev.to-post/site/{content,data,layouts,static}
Press Ctrl+C to stop
Watching for config changes in site/config.toml
ℹ ｢wds｣: Project is running at http://localhost:3000/
ℹ ｢wds｣: webpack output is served from /
ℹ ｢wds｣: Content not from webpack is served from ~/dev/one-click-hugo-cms-dev.to-post/dist
ℹ ｢wds｣: 404s will fallback to /index.html
ℹ ｢wdm｣: wait until bundle finished: /
ℹ ｢wdm｣: Hash: c80db40b3737e7b46070
Version: webpack 4.42.0
```

Good! From here just open your browser, navigate to `http://localhost:3000`, and make sure your coffee website loads.

![Website running locally at http://localhost:3000](https://dev-to-uploads.s3.amazonaws.com/i/8pip815i6j85qz0oxa0c.png)


# Login into your local CMS backend
The last step, I promise. Navigate to your CMS login page, `http://localhost:3000/admin/`, click on the "Login with GitHub" button.

![CMS Login page](https://dev-to-uploads.s3.amazonaws.com/i/eqe1unonuc28d7ikitmw.png)

This should open up a separate window asking you to give your GitHub OAuth app the required permissions.

Just follow the steps and after a few clicks the window should close and you are now authenticated into your CMS and ready to write some new content.

![CMS Admin page](https://dev-to-uploads.s3.amazonaws.com/i/9plffsfkh9yfh14bp253.png)


# Conclusion
Alright, you've done it! Grab a drink, sit back, and relax with confidence that you authentication system is working and secure, backed by GitHub.

I'm only human so if you see any mistakes *please* do not hesitate to leave a comment correcting me! I'd really appeciate the help.

If you run into any errors make sure to double check your work. If you can't figure it out then leave a comment with your situation and any relevant errors.