Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
morphatic committed Apr 14, 2019
1 parent 1932139 commit 76a9f0b
Show file tree
Hide file tree
Showing 16 changed files with 4,220 additions and 39 deletions.
25 changes: 25 additions & 0 deletions .eslintrc.json
@@ -0,0 +1,25 @@
{
"env": {
"es6": true,
"node": true,
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2017
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
]
}
}
128 changes: 90 additions & 38 deletions .gitignore
@@ -1,61 +1,113 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm
# Dependency directory
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
# Users Environment Variables
.lock-wscript

# next.js build output
.next
# Certificates
*.pem
*.key
*.crt

# IDEs and editors (shamelessly copied from @angular/cli's .gitignore)
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

### Linux ###
*~

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

### OSX ###
*.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msm
*.msp

# Windows shortcuts
*.lnk
10 changes: 10 additions & 0 deletions .npmignore
@@ -0,0 +1,10 @@
.editorconfig
.jshintrc
.travis.yml
.istanbul.yml
.babelrc
.idea/
.vscode/
test/
coverage/
.github/
11 changes: 11 additions & 0 deletions .travis.yml
@@ -0,0 +1,11 @@
sudo: false
language: node_js
node_js:
- 'node'
- '8'
before_install:
- npm i -g nyc coveralls
script:
- npm test
after_success:
- npm run coverage
7 changes: 6 additions & 1 deletion README.md
@@ -1,2 +1,7 @@
# feathers-auth0-authorize-hook
# FeathersJS Auth0 Authorization Hook

[![Build Status](https://travis-ci.org/morphatic/feathers-auth0-authorize-hook.svg?branch=master)](https://travis-ci.org/morphatic/feathers-auth0-authorize-hook)
[![Coverage Status](https://coveralls.io/repos/github/morphatic/feathers-auth0-authorize-hook/badge.svg?branch=master)](https://coveralls.io/github/morphatic/feathers-auth0-authorize-hook?branch=master)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/morphatic/astrologyjs/master/LICENSE)

A "before" hook for FeathersJS to authorize requests accompanied by an Auth0-issued JWT.
16 changes: 16 additions & 0 deletions config/default.json
@@ -0,0 +1,16 @@
{
"host": "localhost",
"port": 3030,
"jwksUri": "https://example.auth0.com/.well-known/jwks.json",
"jwtOptions": {
"algorithms": [
"RS256"
],
"audience": [
"https://example.auth0.com/api/v2/",
"https://example.auth0.com/userinfo"
],
"ignoreExpiration": false,
"issuer": "https://example.auth0.com/"
}
}
161 changes: 161 additions & 0 deletions lib/authorize.js
@@ -0,0 +1,161 @@
/**
* Checks the `Authorization` header for a JWT and verifies
* that it is legitimate and valid before allowing the
* request to proceed.
*/

const errors = require('@feathersjs/errors')
const jwt = require('jsonwebtoken')
const rp = require('request-promise')

module.exports = ({
// These two parameters allow users to customize which models/services
// they want to use with this hook
userService = 'users',
keysService = 'keys',
// the actual hook
authorize = async (context) => {
// Throw if the hook is being called from an unexpected location.
if (context.type !== 'before')
throw new errors.NotAuthenticated('`authorize()` can only be used as a `before` hook.', context)

// get the Authorization header
const header = (context.params.headers || {}).authorization || null

// throw an error if the Authorization header is not set
if (!header) throw new errors.NotAuthenticated('`Authorization` header not set.', context)

// extract the raw token from the header
const currentToken = header.replace('Bearer ', '').trim()

// decode it
let token = jwt.decode(currentToken, { complete: true })

// throw an error if the token was malformed or missing
if (!token) throw new errors.NotAuthenticated('The token was malformed or missing.')

// get the user ID from the token payload
const user_id = token.payload.sub

// check to see if we have a member with this ID in the database
let member
try {
member = await context.app.service(userService).find({
paginate: false,
query: { user_id, $limit: 1 }
}).then(results => {
if (results[0]) return results[0]
throw 'member does not exist'
})
} catch (err) {
// throw an error if no such member exists
throw new errors.NotAuthenticated('No member with this ID exists.', context)
}

// if the member already has a valid, current token, stop here
if (member.currentToken && member.currentToken === currentToken) return context

// otherwise, get the kid from the token header
const kid = token.header.kid

// create a JWKS retrieval client
const client = getJWKS(context.app.get('jwksUri'))

// get the signing key from the JWKS endpoint at Auth0
const key = await getKey(kid, context.app.service(keysService), client)

// verify the raw JWT
try {
jwt.verify(currentToken, key, context.app.get('jwtOptions'))
} catch (err) {
throw new errors.NotAuthenticated('Token could not be verified.', err.message)
}

// OK! The JWT is valid, store it in the member profile
// (It's okay if this fails)
context.app.service(userService).patch(
null,
{ currentToken },
{ query: { user_id: member.user_id } }
)

// If we made it this far, we're all good!
return context
},
/**
* Takes a JWKS endpoint URI and returns a function that can retrieve an
* array of JWKs, i.e. a JWKS. The resulting function may throw any of
* [the errors described here]{@link https://github.com/request/promise-core/blob/master/lib/errors.js}
*
* @param {string} uri The URI of the JWKS endpoint
* @returns {function} A function that can retrieve a JWKS from the endpoint
*/
getJWKS = uri => () => rp({ uri, json: true }),
/**
* Takes a JWK object and returns a valid key in PEM format. Throws
* a GeneralError if there are no x5c items stored on the JWK.
*
* @param {string} jwk The JWK to be parsed
* @returns {string} The key in PEM format from the first x5c entry
* @throws {GeneralError} Throws a GeneralError if there are no x5c items
*/
x5cToPEM = jwk => {
if (!jwk.x5c.length > 0) throw new errors.GeneralError('Stored JWK has no x5c property.')
const lines = jwk.x5c[0].match(/.{1,64}/g).join('\n')
return `-----BEGIN CERTIFICATE-----\n${lines}\n-----END CERTIFICATE-----\n`
},
/**
* Takes a `kid`, a reference to an in-memory Feathers service (`svc`)
* for storing JWKs, and a `client` for retrieving signing keys from a
* JWKS endpoint. Returns a valid signing key in PEM format or throws
* a `SigningKeyNotFoundError`. If a key is successfully retrieved from
* the endpoint, it tries to store this value using the `svc`.
*
* @async
* @param {string} kid The `kid` for the JWK to be retrieved
* @param {object} svc The Feathers service used to store JWKs in memory
* @param {function} jwksClient A function that takes a `kid` and returns a key
* @returns {string} The retrieved signing key in PEM format
* @throws {GeneralError} Thrown by the `client` if `kid` is not found
*/
getKey = async (kid, svc, jwksClient) => {
try {
// get the signing key from the in-memory service, if it exists
const storedKey = await svc.get(kid)

// if the storedKey exists, return it
if (storedKey) return x5cToPEM(storedKey)
} catch (err) {
// nothing to see here. please move along...
}

// otherwise, we need to get it from our JWKS endpoint
let jwk
try {
const jwks = await jwksClient()
jwk = jwks.keys.find(k => k.kid === kid)
} catch (err) {
// throw an error if we still don't have a signing key
throw new errors.GeneralError('Could not retrieve JWKS', err)
}

// throw an error if there were no JWKs that contained our kid
if (!jwk) throw new errors.GeneralError('Could not find a JWK matching given kid')

// get the signing key from the retrieved JWK
const key = x5cToPEM(jwk)

// store the jwk in our in-memory service
try { svc.create(jwk) } catch (e) { /* no problem if this fails */ }

// and return the key
return key
}
} = {}) => ({
userService,
keysService,
authorize,
getJWKS,
x5cToPEM,
getKey
})
3 changes: 3 additions & 0 deletions lib/index.js
@@ -0,0 +1,3 @@
const authorize = require('./authorize')

module.exports = authorize

0 comments on commit 76a9f0b

Please sign in to comment.