Contents
- Acknowledgement
- Prerequisites
- Get your frontend running
- Create the GitHub OAuth App
- Save GitHub OAuth App credentials somewhere secure
- Create your OAuth Lambdas
- Triggering Lambdas
- Write some OAuth code
- From local to remote
- Test your OAuth Lambdas
- Start up your local frontend
- Login into your local CMS backend
Acknowledgement
Before starting this post, I need to give a big shout out to Mark Steele 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.
Prerequisites
- GitHub Account
- AWS Account
- AWS 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 page and choose a starter template. For the purposes of this post, we're going to use the One Click Hugo 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.
Create the GitHub OAuth App
Our next step is to create a GitHub OAuth Application. Alternatively, you can follow along with on the GitHub website 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.
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.
After you login use the "Find Services" search bar and search for "Secrets Manager" and click on the resulting search.
On the next page you need to click the "Store a new secret" button in the top right corner.
Fill in the form adding two new "Secret key/value" pairs as shown in the image below and click "Next" at the bottom right.
Fill in the next form as well and click "Next" at the bottom right of the page.
Leave this next page on its default settings and click "Next".
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 and click on Create Function in the top right corner.
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.
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:
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:
{
"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 on the dropdown arrow for the policy already created in the table and choose the "Edit policy" button.
Click on the "(+) Add addutuibak permissions" button on the right hand side of this next page.
Click on "Service" and search for "Secrets Manager" and choose the only option available.
Clck on "Actions", "Access level", and finally choose the "GetSecretValue" check box.
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.
Go back to your SecretsManager, find stored secret and copy its ARN and paste it into the input opened from the "Add ARN" link.
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.
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.
On the next page just follow along with the image:
- API Gateway
- Create an API
- HTTP API
- Security: Open
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.
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.
mkdir OAuthLambdas
cd OAuthLambdas
Once inside, we need to initialize this directory as a Node project and install the node-fetch
package using npm
:
npm init -y
npm i node-fetch
Last, we need to create some new files and directories with the following commands:
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:
OAuthLambdas/
---- handlers/
---- redirect.js
---- callback.js
---- node_modules/
---- utils/
---- authenticateGitHubUser.js
---- callbackHtmlPage.js
---- package.json
- Open redirect.js and place the following code inside
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
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
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
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 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.
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
{
"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.
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:
{
"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".
{
"queryStringParameters": {
"code": "abc123"
}
}
Go ahead and click "Test" and if everything was set up correctly you should receive the following success message.
{
"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
ornpm i
- In terminal run
yarn start
ornpm 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.
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.
| 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.
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.
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.
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.
Top comments (2)
wait a second, I don't see were are you saying your frontend how to contact or use your lambda function in aws.
In case anyone else finds this - in your /static/ folder of a typical hugo install, put the netlify CMS config and index file. (config.yml and index.html)
Get the lambda API address.
In the netlify config:
base_url: Everything in the Lambda API address up to .com
auth_endpoint: every after the .com (/default/name of lambda)