Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React authenticate #22

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ node_modules
.tmp

#SERVERLESS
admin.env
.env
env.yml
env.*.yml

#Ignore _meta folder
_meta
Expand Down
69 changes: 47 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Serverless Authentication

Note by Jeremy Cummins: This fork is meant to work with the `react-authenticate` branch of https://github.com/99xt/serverless-react-boilerplate/.
It will authorize an API endpoint that includes a user ID so that the client cannot modify a different user's data by replacing the user ID in the request. See **redirectProxyCallback** method in **authentication/lib/helpers.js**.

[![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com)

This project is aimed to be a generic authentication boilerplate for the [Serverless framework](http://www.serverless.com).
Expand All @@ -14,13 +17,13 @@ If you are using Serverless framework v.0.5, see branch https://github.com/laard

Installation will create one DynamoDB table for OAuth state and refresh tokens.

1. Run `serverless install --url https://github.com/laardee/serverless-authentication-boilerplate`, clone or download the repository
2. Rename _example.env_ in _authentication_ to _.env_ and set [environmental variables](#env-vars).
3. Change directory to `authentication` and run `npm install`.
4. Run `serverless deploy` on the authentication folder to deploy authentication service to AWS. Notice the arn of the authorize function.
5. (optional) Change directory to test-token and insert the arn of the authorizer function to authorizer/arn in serverless.yml. Then run `serverless deploy` to deploy test-token service.
1. Clone or download the repository https://github.com/jcummins54/serverless-authentication-boilerplate/.
2. Switch to the branch `git checkout -b react-authenticate` and get the latest `git pull origin react-authenticate`.
3. In the _authentication_ folder, copy _example-env.yml_ to _env.yml_ and set [environmental variables](#env-vars).
4. Change directory to `authentication` and run `npm install`.
5. Run `serverless deploy` on the authentication folder to deploy authentication service to AWS. Notice the arn of the authorize function, this will be used in the React client.

If you wish to change the cache db name, change `CACHE_DB_NAME ` in _.env_ file and `TableName` in _serverless.yml_ in Dynamo resource.
If you wish to change the cache db name, change `CACHE_DB_NAME ` in _env.yml_ file and `TableName` in _serverless.yml_ in Dynamo resource.

## Set up Authentication Provider Application Settings

Expand Down Expand Up @@ -61,24 +64,45 @@ Functions:

## <a id="env-vars"></a>Environmental Variables

Open authentication/.env, fill in what you use and other ones can be deleted.
Open authentication/env.yml and fill in your details.

```
STAGE = dev
REDIRECT_CLIENT_URI = http://url-to-frontend-webapp/
TOKEN_SECRET = secret-for-json-web-token
PROVIDER_FACEBOOK_ID = facebook-app-id
PROVIDER_FACEBOOK_SECRET = facebook-app-secret
PROVIDER_GOOGLE_ID = google-app-id
PROVIDER_GOOGLE_SECRET = google-app-secret
PROVIDER_MICROSOFT_ID = microsoft-app-id
PROVIDER_MICROSOFT_SECRET = microsoft-app-secret
PROVIDER_CUSTOM_GOOGLE_ID = google-app-id
PROVIDER_CUSTOM_GOOGLE_SECRET = google-app-secret
COGNITO_IDENTITY_POOL_ID = cognito-pool-id
COGNITO_REGION = eu-west-1
COGNITO_PROVIDER_NAME = your-service-name

SERVICE_NAME: serverless-authentication

STAGE: dev

# local db for running tests - check the IP of your Docker container created by specs-docker.sh
LOCAL_DDB_ENDPOINT: http://localhost:8000/

# database table names
CACHE_DB_NAME: ${self:custom.writeEnvVars.STAGE}-serverless-authentication-cache
USERS_DB_NAME: ${self:custom.writeEnvVars.STAGE}-serverless-authentication-users

# AWS info
REGION: us-east-1
AWS_ACCOUNT_ID: aws-account-id
COGNITO_IDENTITY_POOL_ID: cognito-pool-id
COGNITO_REGION: us-east-1
COGNITO_PROVIDER_NAME: serverless-authentication-boilerplate

# Your application's client URI
REDIRECT_CLIENT_URI: http://localhost:8080/

# The API endpoint to be authorized, such as the test-token API endpoint
API_AUTH_ENDPOINT: https://API-ID.execute-api.us-east-1.amazonaws.com/${self:custom.writeEnvVars.STAGE}/todos/{userId}/register

# Change this
TOKEN_SECRET: token-secret-123

# Providers you aren't using can be deleted
PROVIDER_FACEBOOK_ID: fb-mock-id
PROVIDER_FACEBOOK_SECRET: fb-mock-secret
PROVIDER_GOOGLE_ID: g-mock-id
PROVIDER_GOOGLE_SECRET: g-mock-secret
PROVIDER_MICROSOFT_ID: ms-mock-id
PROVIDER_MICROSOFT_SECRET: ms-mock-secret
PROVIDER_CUSTOM_GOOGLE_ID: cg-mock-id
PROVIDER_CUSTOM_GOOGLE_SECRET: cg-mock-secret
```

## Example Provider Packages
Expand All @@ -97,3 +121,4 @@ Package contains example [/authentication/lib/custom-google.js](https://github.c
* Install Docker and Docker Compose
* Run `npm install` in project root directory
* Run ./specs-docker.sh
* If the Setup specs test fails starting DynamoDB, check the IP of the created Docker container and make sure the value of `LOCAL_DDB_ENDPOINT` in _authentication/env.yml_ matches.
6 changes: 2 additions & 4 deletions authentication/event.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{
"key3": "value3",
"key2": "value2",
"key1": "value1"
}
"methodArn": "arn:aws:lambda:us-east-1:AWS-ACCOUNT-ID:function:aws-react-auth-boilerplate-dev-items/dev/*/todos/1/*"
}
36 changes: 36 additions & 0 deletions authentication/example-env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
SERVICE_NAME: serverless-authentication

STAGE: dev

# local db for running tests - check the IP of your Docker container created by specs-docker.sh
LOCAL_DDB_ENDPOINT: http://localhost:8000/

# database table names
CACHE_DB_NAME: ${self:custom.writeEnvVars.STAGE}-serverless-authentication-cache
USERS_DB_NAME: ${self:custom.writeEnvVars.STAGE}-serverless-authentication-users

# AWS info
REGION: us-east-1
AWS_ACCOUNT_ID: aws-account-id
COGNITO_IDENTITY_POOL_ID: cognito-pool-id
COGNITO_REGION: us-east-1
COGNITO_PROVIDER_NAME: serverless-authentication-boilerplate

# Your application's client URI
REDIRECT_CLIENT_URI: http://localhost:8080/

# The API endpoint to be authorized, such as the test-token API endpoint
API_AUTH_ENDPOINT: https://API-ID.execute-api.us-east-1.amazonaws.com/${self:custom.writeEnvVars.STAGE}/todos/{userId}/register

# Change this
TOKEN_SECRET: token-secret-123

# Providers you aren't using can be deleted
PROVIDER_FACEBOOK_ID: fb-mock-id
PROVIDER_FACEBOOK_SECRET: fb-mock-secret
PROVIDER_GOOGLE_ID: g-mock-id
PROVIDER_GOOGLE_SECRET: g-mock-secret
PROVIDER_MICROSOFT_ID: ms-mock-id
PROVIDER_MICROSOFT_SECRET: ms-mock-secret
PROVIDER_CUSTOM_GOOGLE_ID: cg-mock-id
PROVIDER_CUSTOM_GOOGLE_SECRET: cg-mock-secret
15 changes: 0 additions & 15 deletions authentication/example.env

This file was deleted.

15 changes: 13 additions & 2 deletions authentication/lib/handlers/authorizeHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,20 @@ const slsAuth = require('serverless-authentication');
const config = slsAuth.config;
const utils = slsAuth.utils;


// Authorize
//
// event.methodArn: "arn:aws:execute-api:<regionId>:<accountId>:<apiId>/<stage>/<method>/<resourcePath>/<userId>/<function>"
// ...will be parsed as...
// const resource = "arn:aws:execute-api:<regionId>:<accountId>:<apiId>/<stage>/*/<resourcePath>/<userId>/*"

const authorize = (event, callback) => {
const stage = event.methodArn.split('/')[1] || 'dev'; // @todo better implementation
const resourceParts = event.methodArn.split('/');
const stage = resourceParts[1];
let resource = `${resourceParts[0]}/${stage}/*/${resourceParts[3]}/${resourceParts[4]}`;
if (resourceParts.length > 5) {
resource += '/*';
}
let error = null;
let policy;
const authorizationToken = event.authorizationToken;
Expand All @@ -17,7 +28,7 @@ const authorize = (event, callback) => {
// this example uses simple expiration time validation
const providerConfig = config({ provider: '', stage });
const data = utils.readToken(authorizationToken, providerConfig.token_secret);
policy = utils.generatePolicy(data.id, 'Allow', event.methodArn);
policy = utils.generatePolicy(data.id, 'Allow', resource);
} catch (err) {
error = 'Unauthorized';
}
Expand Down
22 changes: 18 additions & 4 deletions authentication/lib/handlers/callbackHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,26 @@ function callbackHandler(proxyEvent, context) {
} else {
cache.revokeState(state)
.then(() => {
const id = createUserId(`${profile.provider}-${profile.id}`, providerConfig.token_secret);
const data = createResponseData(id, providerConfig);
const uid = createUserId(`${profile.provider}-${profile.id}`,
providerConfig.token_secret);
const data = createResponseData(uid, providerConfig);

// Return the user's ID as a paramter in the callback URI.
// The user ID becomes part of the API path authorized, ensuring that this user cannot
// modify another user's data.
// e.g. https://<API ID>.execute-api.us-east-1.amazonaws.com/dev/*/todos/<USER ID>/*
Object.assign(data, { id: uid });

// map data for db
Object.assign(profile, { userName: profile.name });
// remove DynamoDB keyword
Object.assign(profile, { name: null });
Object.assign(profile, { providerId: profile.id });
Object.assign(profile, { id: uid });

Promise.all([
cache.saveRefreshToken(id),
users.saveUser(Object.assign(profile, { userId: id }))
cache.saveRefreshToken(uid),
users.saveUser(profile)
])
.then((results) => tokenResponse(Object.assign(data, { refreshToken: results[0] })))
.catch((_error) => errorResponse({ error: _error }));
Expand Down
38 changes: 35 additions & 3 deletions authentication/lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,42 @@ const createResponseData = (id) => {
};

const redirectProxyCallback = (context, data) => {
context.succeed({
statusCode: 302,
// TODO: Better if utils.tokenResponse returned authorizationToken as a seperate parameter
// so we don't have to parse it out of the url.
const url = require('url');
const query = url.parse(data.url, true).query;

if (!query.authorization_token) {
context.succeed({
statusCode: 302,
headers: {
Location: data.url
}
});
return;
}

// Authorize before returning auth token to ensure client cannot authorize a different user ID.
// The user ID becomes part of the API path authorized, ensuring that this user cannot
// modify another user's data.
const authUrl = process.env.API_AUTH_ENDPOINT.replace(/{userId}/, query.id);
const request = require('request');
const options = {
url: authUrl,
headers: {
Location: data.url
Authorization: query.authorization_token
}
};
request(options, (error, response) => {
if (!error && response.statusCode === 200) {
context.succeed({
statusCode: 302,
headers: {
Location: data.url
}
});
} else {
console.log(error);
}
});
};
Expand Down
8 changes: 3 additions & 5 deletions authentication/lib/storage/cacheStorage.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
'use strict';

require('dotenv').config();
// const table = `${process.env.SERVERLESS_STAGE}-${process.env.SERVERLESS_PROJECT}-cache`;
const table = process.env.CACHE_DB_NAME.replace(/{stage}/, process.env.STAGE);
const config = { region: process.env.SERVERLESS_REGION };
const table = process.env.CACHE_DB_NAME;
const config = { region: process.env.REGION };

if (process.env.LOCAL_DDB_ENDPOINT) config.endpoint = process.env.LOCAL_DDB_ENDPOINT;
if (process.env.TEST_LOCAL) config.endpoint = process.env.LOCAL_DDB_ENDPOINT;

// Common
const AWS = require('aws-sdk');
Expand Down
21 changes: 18 additions & 3 deletions authentication/lib/storage/usersStorage.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
'use strict';

const table = process.env.USERS_DB_NAME;
const config = { region: process.env.REGION };

// Common
const AWS = require('aws-sdk');
// const config = { region: process.env.SERVERLESS_REGION };
// const dynamodb = new AWS.DynamoDB.DocumentClient(config);
const dynamodb = new AWS.DynamoDB.DocumentClient(config);
const Promise = require('bluebird');
const cognitoidentity = new AWS.CognitoIdentity({ region: process.env.COGNITO_REGION });

const saveDatabase = (profile) => new Promise((resolve, reject) => {
if (profile) {
resolve(null);
// resolve(null);

const params = {
TableName: table,
Item: profile
};

dynamodb.put(params, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
} else {
reject('Invalid profile');
}
Expand Down
5 changes: 3 additions & 2 deletions authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
"private": true,
"dependencies": {
"bluebird": "^3.4.0",
"decamelize": "^1.2.0",
"dotenv": "^2.0.0",
"request": "^2.76.0",
"serverless-authentication": "^0.4.4",
"serverless-authentication-facebook": "^0.3.2",
"serverless-authentication-google": "^0.3.2",
"serverless-authentication-microsoft": "^0.3.2"
"serverless-authentication-microsoft": "^0.3.2",
"serverless-plugin-write-env-vars": "^1.0.1"
}
}
Loading