Skip to content

Commit

Permalink
Added Client Credentials grant with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
yohei.kanehara committed May 11, 2017
1 parent 37e98e1 commit 754653e
Show file tree
Hide file tree
Showing 26 changed files with 2,304 additions and 71 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
lib
mochawesome-reports
.env
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CONFIG_PATH=[absolute path to local-properties.json]
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ COPY . /app

EXPOSE 3000

CMD ["npm", "run", "start:prod"]
CMD ["npm", "run", "start:production"]
16 changes: 15 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
/**
* This file is only executed locally
* Non locally, we will pre-compile src/ with Babel and build it to a lib/ directory and execute that instead
*
* See package.json for details of the scripts
*/

const assert = require('assert');

assert(process.env.NODE_ENV !== 'production');

// Read in environment variables from .env
require('dotenv').config();

// Babel hook
require('babel-core/register');

// Server start
// Server
require('./src');
7 changes: 7 additions & 0 deletions local-properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"mongo": {
"uri": "docker:37117/authserver",
"user": "test",
"pass": "password"
}
}
22 changes: 18 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
{
"name": "oauth2-server",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "NODE_ENV=dev nodemon index.js",
"start:prod": "npm run build && NODE_ENV=prod node lib/index.js",
"build": "babel src -d lib"
"start:production": "npm run build && NODE_ENV=production node lib/index.js",
"test": "NODE_ENV=test mocha --compilers js:babel-core/register 'test/**/*.spec.js' --reporter mochawesome --reporter-options inlineAssets=true",
"build": "babel src -d lib",
"db:create:client": "node src/db-scripts/create-client",
"clean": "rimraf mochawesome-reports && rimraf lib"
},
"dependencies": {
"body-parser": "~1.13.2",
"chai": "^3.5.0",
"express": "~4.14.1",
"express-oauth-server": "^2.0.0-b1",
"lodash": "^4.15.0",
"mocha": "^3.2.0",
"mongoose": "^4.9.1",
"morgan": "~1.6.1",
"oauth2-server": "^2.4.1",
"raven": "^1.2.0",
"sinon": "^2.1.0",
"underscore": "^1.8.3",
"winston": "^2.3.1"
},
"devDependencies": {
"babel-cli": "^6.24.0",
"babel-core": "^6.24.0",
"babel-preset-es2015-node": "^6.1.1",
"babel-preset-stage-2": "^6.22.0",
"nodemon": "^1.11.0"
"chai-http": "^3.0.0",
"dotenv": "^4.0.0",
"mochawesome": "^2.0.4",
"mockgoose": "^7.0.7",
"nodemon": "^1.11.0",
"rimraf": "^2.6.1"
},
"babel": {
"presets": [
Expand Down
32 changes: 18 additions & 14 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,35 @@ import morgan from "morgan";
import bodyParser from "body-parser";
import OAuth2Server from 'express-oauth-server';
import logger from './logger';
import model from './model';
import modelFactory from './oauth2-model-factory';
const model = modelFactory();

const app = express();
const oauth2 = new OAuth2Server({
debug: true,
model
});
const oauth2 = new OAuth2Server({ model });

app.use(morgan('combined', {stream: logger.stream}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Unprotected
app.get('/', (req, res) => {
res.send('hello world');
});

// Health check
/**
* Health check endpoint
*/
app.get('/healthz', (req, res) => {
res.send('Healthy as a horse');
});

// Protected route
app.get('/secret', oauth2.authenticate(), (req, res) => {
res.send('Ooh I hope nobody gets their hands on me Strawberry Smiggles');
/**
* Retrieve tokens through OAuth2 authentication
*/
app.post('/auth/token', oauth2.token(), (res, req) => {
res.send(res.locals.oauth);
});

/**
* Authenticate a request
*/
app.get('/auth/authenticate', oauth2.authenticate(), (req, res) => {
res.sendStatus(200);
});

export default app;
27 changes: 20 additions & 7 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
export default {
mongo: {
uri: 'localhost:32768',
user: process.env.MONGO_USER || '',
pass: process.env.MONGO_PASS || ''
}
}
/**
* This will read the file (expects JSON) defined at the path set in the CONFIG_PATH environment variable
* CONFIG_PATH will be configured differently per envrionment
*
* Locally, this is set in the .env file via dotenv: https://github.com/motdotla/dotenv
*/
import fs from 'fs';
import logger from './logger';

const pathToProperties = process.env.CONFIG_PATH;

let config = {};
try {
config = JSON.parse(fs.readFileSync(pathToProperties, 'utf8'));
} catch(err) {
logger.error(`Could not parse file as JSON at path ${pathToProperties}. This means our app will fail! Killing the app...`, err);
process.exit();
}

export default config;
73 changes: 73 additions & 0 deletions src/db-scripts/create-client/create-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* DB loading script that will load a client instance for client_credentials grants
* DB connection configuration must be valid in local-properties.json if running locally
*
* This creates a {@link Client} with a random 8 byte Client ID and random 16 byte Client secret
* associated with a {@link User} also with a random 8 byte username and 16 byte password
*
* Example:
* - npm run db:create:client <client-name>
* REQUIRED: `client-name` - A canonical name associated with client
*/
import connect from '../../db';
import logger from '../../logger';
import crypto from 'crypto';
import { User, Client } from '../../models';
import mongoose from 'mongoose';
const connection = mongoose.connection;

// The first two elements of process.argv are 'node' and the js file path being executed respectively
const [clientName] = process.argv.slice(2);

if (!clientName) {
logger.error("A client name must be supplied to this command as the first argument! Example: `npm run db:create:client ADD`");
process.exit();
}

connect();
connection.once('open', () => {
loadClient(clientName)
.then(() => {
logger.info("Successfully loaded new Client");
process.exit();
})
.catch(err => {
logger.error("Error loading new Client: ", err);
process.exit();
});
});
connection.on('error', (err) => {
logger.error('Error in Mongo connection:', err);
process.exit();
});

async function loadClient(clientName) {
const clientCredentials = {
name: clientName,
clientId: crypto.randomBytes(8).toString('hex'),
clientSecret: crypto.randomBytes(16).toString('hex')
};

const userCredentials = {
username: crypto.randomBytes(8).toString('hex'),
password: crypto.randomBytes(16).toString('hex')
};

const newUser = await User.create({
username: userCredentials.username,
password: userCredentials.password,
// TODO: update script to take in scopes as an arg once scopes are implemented
scope: null
});
logger.info('Finished loading new User', newUser);

const newClient = await
Client.create({
"name": clientCredentials.name,
"clientId": clientCredentials.clientId,
"clientSecret": clientCredentials.clientSecret,
"grants": ["client_credentials"],
"user": newUser._id
});
logger.info('Finished loading new client', newClient);
}
7 changes: 7 additions & 0 deletions src/db-scripts/create-client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require('babel-core/register');

// dotenv is only necessary when running this script locally
require('dotenv').config();

// Runs create-client.js script
require('./create-client.js');
15 changes: 14 additions & 1 deletion src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,22 @@ db.on('disconnected', () => {
});

export default function connect() {

// Set Promise interface for Mongoose to ES6 promises
mongoose.Promise = global.Promise;

if (!config.mongo) {
logger.error("Configuration json is missing 'mongo' key! Killing app...");
process.exit();
}
const { uri, user, pass } = config.mongo;
const options = {
user,
pass,
promiseLibrary: global.Promise
};

// Makes connection asynchronously. Mongoose will queue up database
// operations and release them when the connection is complete.
return mongoose.connect(uri, { user, pass });
return mongoose.connect(uri, options);
}
24 changes: 22 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
#!/usr/bin/env node

/**
* Module dependencies.
*/
import app from './app';
import http from 'http';
import logger from './logger';
import config from './config';
import connect from './db';
import mongoose from 'mongoose';

const Raven = require('raven');

// Initialize Mongo connection
connect();

mongoose.connection.once('open', () => {
if (process.env.NODE_ENV === 'production') {
configureRaven();
}

/**
* Get port from environment and store in Express.
*/
Expand All @@ -29,6 +35,20 @@ mongoose.connection.once('open', () => {
server.listen(port, () => logger.info('Express server listening on %d', port))
});

function configureRaven() {
if (!config.sentry) {
logger.error("Configuration json is missing 'sentry' key! Killing the app...");
process.exit();
} else if (!config.sentry.dsn) {
logger.error("Configuration json is missing 'sentry.dsn' key! Killing the app...");
process.exit();
}

Raven.config(config.sentry.dsn).install();
app.use(Raven.requestHandler());
app.use(Raven.errorHandler());
}

// log uncaught exceptions
process.on('uncaughtException', err => logger.error('uncaught exception:', err));
process.on('unhandledRejection', error => logger.error('unhandled rejection:', error));
2 changes: 1 addition & 1 deletion src/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import winston from 'winston';

const level =
process.env.NODE_ENV === 'test' ? 'error' :
(process.env.NODE_ENV === 'prod' ? 'info' : 'debug');
(process.env.NODE_ENV === 'production' ? 'info' : 'debug');

const logger = new winston.Logger({
transports: [
Expand Down
19 changes: 19 additions & 0 deletions src/models/Client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import mongoose from 'mongoose';
const Schema = mongoose.Schema;

/**
* An OAuth2 Client
*/
export const Client = mongoose.model('Client', new Schema({
name: String,
clientId: String,
clientSecret: String,
grants: [String],

/**
* If this Client supports client_credentials grant type,
* this will hold the Client's User instance.
* Otherwise, this will be undefined
*/
user: {type: Schema.Types.ObjectId, ref: 'User'}
}));
21 changes: 21 additions & 0 deletions src/models/Token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import mongoose from 'mongoose';
const Schema = mongoose.Schema;

/**
* An OAuth2 Token
*/
export const Token = mongoose.model('Token', new Schema({
accessToken: String,
accessTokenExpiresAt: Date,
scope: [String],

/**
* The Client associated with the Token
*/
client: {type: Schema.Types.ObjectId, ref: 'Client'},

/**
* The User associated with the Token
*/
user: {type: Schema.Types.ObjectId, ref: 'User'}
}));
11 changes: 11 additions & 0 deletions src/models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import mongoose from 'mongoose';
const Schema = mongoose.Schema;

/**
* An OAuth2 User
*/
export const User = mongoose.model('User', new Schema({
username: String,
password: String,
scope: [String]
}));
3 changes: 3 additions & 0 deletions src/models/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './Client';
export * from './Token';
export * from './User';
Loading

0 comments on commit 754653e

Please sign in to comment.