-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial authentication/authorization module (#1)
* Create an auth module that can be register with Koop. * Module's authentication functions are intended to be applied to a provider Model's prototype
- Loading branch information
Showing
11 changed files
with
261 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,4 +8,5 @@ test/test.log.** | |
.flowconfig | ||
.vscode | ||
.idea | ||
dist | ||
dist | ||
**/user-store.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
language: node_js | ||
node_js: | ||
- "8" | ||
sudo: false | ||
cache: | ||
- node_modules | ||
script: | ||
- npm test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Koop-Auth-Direct-File | ||
## A authentication module for implementing direct authentication from client to Koop server with a file-based user-store | ||
|
||
## Authentication pattern | ||
|
||
The authentication module implemented here uses a *direct authentication* pattern; it receives user credentials (username/password) from a client and authenticates those credentials against an identity/user-store. Requests with valid credentials are issued an access-token (a string of encoded-data); The access token is encoded with the use of a secret known only to the Koop server. The access-token expires and becomes invalid after a certain period (default of 60 minutes). | ||
|
||
 | ||
|
||
The issued access-token should be attached to all subsequent service requests by the client. When the server receives a request, it will check for the presence of an access-token and reject any requests that are missing such token. If the token is present, the server attempts to decode it with its stored secret. Failure to decode results in a request rejection. Once decoded, the server checks the token's expiration-date and rejects any token with a date that is out of range. If the token is not expired, the request for the desired resource proceeds. | ||
|
||
 | ||
|
||
## Example of Koop authentication implementation | ||
|
||
The [server.js](./server.js) file provides an example of securing a provider's resources. Start by requiring the authentication module. Pass it a secret and the file path of your user-store. | ||
|
||
let auth = require('./koop-auth-direct/src')('pass-in-your-secret', `${__dirname}/user-store.json`) | ||
koop.register(auth) | ||
|
||
Then require and register your providers. | ||
|
||
const provider = require('./') | ||
koop.register(provider) | ||
|
||
The authentication module will configure and add its `authorize`, `authenticate`, and `authenticationSpecification` functions to the provider's model prototype. Output services will leverage these functions to secure the service endpoints and properly route requests to authenticate. | ||
|
||
Finally, create a JSON file store. This should be an array of objects with properties `username` and `password`. Set an environment variable `USER_STORE` with the path of the file relative to the root of the repository (e.g, `USER_STORE=./user-store.json`) | ||
|
||
## Authentication API | ||
|
||
### (secret, options) ⇒ <code>Object</code> | ||
* configure the authentication module with secret use for token encoding/decoding | ||
|
||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| secret | <code>string</code> | secret for encoding/decoding tokens | | ||
| userStoreFilePath | <code>string</code> | path to the JSON file containing the array of username/password objects | | ||
| options | <code>object</code> | options object | | ||
| options.tokenExpirationMinutes | <code>integer</code> | minutes until token expires (default 60) | |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
const fs = require('fs') | ||
const jwt = require('jsonwebtoken') | ||
const validateCredentials = require('./validate-credentials') | ||
const TOKEN_EXPIRATION_MINTUES = 60 | ||
let _tokenExpirationMinutes | ||
let _secret | ||
let _userStoreFilePath | ||
|
||
/** | ||
* configure auth functions | ||
* @param {string} secret - secret for generating tokens | ||
* @param {string} userStoreFilePath - file path of user store JSON file | ||
* @param {object} options | ||
* @param {integer} options.tokenExpirationMinutes - number of minutes until token expires | ||
*/ | ||
function auth (secret, userStoreFilePath, options = {}) { | ||
// Throw error if user-store file does not exist | ||
fs.stat(userStoreFilePath, function (err, stats) { | ||
if (err) throw err | ||
}) | ||
|
||
_secret = secret | ||
_userStoreFilePath = userStoreFilePath | ||
_tokenExpirationMinutes = options.tokenExpirationMinutes || TOKEN_EXPIRATION_MINTUES | ||
|
||
// Ensure token expiration is an integer greater than 5 | ||
if (!Number.isInteger(_tokenExpirationMinutes) || _tokenExpirationMinutes < 5) throw new Error(`"tokenExpirationMinutes" must be an integer >= 5`) | ||
|
||
return { | ||
type: 'auth', | ||
getAuthenticationSpecification, | ||
authenticate, | ||
authorize | ||
} | ||
} | ||
|
||
/** | ||
* Parameterize a "authenticationSpecification" function with the name of a provider | ||
* @param {string} providerNamespace | ||
*/ | ||
function getAuthenticationSpecification (providerNamespace) { | ||
return function authenticationSpecification () { | ||
return { | ||
provider: providerNamespace, | ||
secured: true | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Authenticate a user's submitted credentials | ||
* @param {string} username | ||
* @param {strting} password | ||
* @returns {Promise} | ||
*/ | ||
function authenticate (username, password) { | ||
return new Promise((resolve, reject) => { | ||
// Validate user's credentials | ||
validateCredentials(username, password, _userStoreFilePath) | ||
.then(valid => { | ||
// If credentials were not valid, reject | ||
if (!valid) { | ||
let err = new Error('Invalid credentials.') | ||
err.code = 401 | ||
reject(err)// Create access token | ||
} | ||
let expires = Date.now() + (_tokenExpirationMinutes * 60 * 1000) | ||
let json = { | ||
token: jwt.sign({exp: Math.floor(expires / 1000), sub: username}, _secret), | ||
expires | ||
} | ||
resolve(json) | ||
}) | ||
.catch(err => { | ||
reject(err) | ||
}) | ||
}) | ||
} | ||
|
||
function authorize (token) { | ||
return new Promise((resolve, reject) => { | ||
// Verify token with async decoded function | ||
jwt.verify(token, _secret, function (err, decoded) { | ||
// If token invalid, reject | ||
if (err) { | ||
err.code = 401 | ||
reject(err) | ||
} | ||
resolve(decoded) | ||
}) | ||
}) | ||
} | ||
|
||
module.exports = auth |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
const fs = require('fs') | ||
|
||
/** | ||
* Validate username and password. | ||
* @param {string} username | ||
* @param {string} password | ||
* @param {string} userStoreFilePath path to user-store file | ||
* @returns {Promise} | ||
*/ | ||
function validateFileBasedCredentials (username, password, userStoreFilePath) { | ||
const promise = new Promise((resolve, reject) => { | ||
fs.readFile(userStoreFilePath, function (err, dataBuffer) { | ||
if (err) return reject(err) | ||
|
||
let userStore = JSON.parse(dataBuffer.toString()) | ||
|
||
const user = userStore.find(user => { | ||
return user.username === username | ||
}) | ||
|
||
if (!user || user.password !== password) { | ||
resolve(false) | ||
} | ||
resolve(true) | ||
}) | ||
}) | ||
return promise | ||
} | ||
|
||
module.exports = validateFileBasedCredentials |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
const test = require('tape') | ||
const path = require('path') | ||
const jwt = require('jsonwebtoken') | ||
const providerMock = { | ||
name: 'test-provider', | ||
Model: function () {} | ||
} | ||
const secret = 'secret' | ||
const auth = require('../src')(secret, path.join(__dirname, '/fixtures/user-store.json')) | ||
|
||
test('authorize success', async function (t) { | ||
t.plan(1) | ||
// Mock token | ||
const token = jwt.sign({exp: Math.floor(Date.now() / 1000) + 120, iss: providerMock.name, sub: 'username'}, secret) | ||
let decoded = await auth.authorize(token) | ||
t.equals(decoded.iss, providerMock.name) | ||
}) | ||
|
||
test('authorize failure - no token', async function (t) { | ||
t.plan(1) | ||
try { | ||
await auth.authorize(undefined) | ||
} catch (err) { | ||
t.equals(err.code, 401) | ||
} | ||
}) | ||
|
||
test('authorize failure - expired token', async function (t) { | ||
t.plan(1) | ||
// Mock token | ||
const token = jwt.sign({exp: Math.floor(Date.now() / 1000) - 120, iss: providerMock.name, sub: 'username'}, secret) | ||
try { | ||
await auth.authorize(token) | ||
} catch (err) { | ||
t.equals(err.code, 401) | ||
} | ||
}) | ||
|
||
test('authenticate success', async function (t) { | ||
t.plan(2) | ||
let result = await auth.authenticate('jerry', 'garcia') | ||
t.equals(typeof result.token, 'string') | ||
t.equals(typeof result.expires, 'number') | ||
}) | ||
|
||
test('authenticate failure', async function (t) { | ||
t.plan(2) | ||
try { | ||
await auth.authenticate('lou', 'reed') | ||
} catch (err) { | ||
t.equals(err.code, 401) | ||
t.equals(err.message, 'Invalid credentials.') | ||
} | ||
}) | ||
|
||
test('authenticationSpecifiction', t => { | ||
t.plan(2) | ||
let authenticationSpecification = auth.getAuthenticationSpecification(providerMock.name) | ||
let result = authenticationSpecification() | ||
t.equals(result.secured, true) | ||
t.equals(result.provider, providerMock.name) | ||
}) | ||
|
||
test('tokenExpirationMinutes - invalid setting', t => { | ||
t.plan(1) | ||
t.throws(function () { | ||
require('../src')(secret, path.join(__dirname, '/fixtures/user-store.json'), {tokenExpirationMinutes: -1}) | ||
}, /"tokenExpirationMinutes" must be an integer >= 5/) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[ | ||
{ | ||
"username": "jerry", | ||
"password": "garcia" | ||
|
||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[{ | ||
"username": "rich", "password": "rich" | ||
}] |