-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
102 changed files
with
3,505 additions
and
3,869 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
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 |
---|---|---|
@@ -1,219 +1,17 @@ | ||
# SafePlaces Authentication Service | ||
# SafePlaces Information Security | ||
|
||
[![Build Status](https://github.com/Path-Check/safeplaces-auth/workflows/Node.js%20CI/badge.svg)](https://github.com/Path-Check/safeplaces-auth/actions?query=workflow%3A%22Node.js+CI%22) | ||
[![Coverage Status](https://coveralls.io/repos/github/Path-Check/safeplaces-auth/badge.svg?branch=master)](https://coveralls.io/github/Path-Check/safeplaces-auth?branch=master) | ||
|
||
The modular authentication service for the SafePlaces backend. | ||
The modular information security service for the SafePlaces backend. | ||
|
||
```shell script | ||
# Install using NPM | ||
npm install @pathcheck/safeplaces-auth@^1.0.0-alpha.5 | ||
npm install @pathcheck/safeplaces-auth@^2.0.0-alpha.1 | ||
|
||
# Install using Yarn | ||
yarn add @pathcheck/safeplaces-auth@^1.0.0-alpha.5 | ||
yarn add @pathcheck/safeplaces-auth@^2.0.0-alpha.1 | ||
``` | ||
|
||
# Table of Contents | ||
* [Examples](#examples) | ||
* [Securing API endpoints](#securing-api-endpoints) | ||
* [Handling login requests](#handling-login-requests) | ||
* [Handling logout requests](#handling-logout-requests) | ||
* [Strategies](#strategies) | ||
* [Auth0](#auth0) | ||
* [Symmetric JWT](#symmetric-jwt) | ||
* [Dynamic strategy selection](#dynamic-strategy-selection) | ||
* [Debugging](#debugging) | ||
## API Specification | ||
|
||
# Examples | ||
|
||
## Securing API endpoints | ||
|
||
The authentication service can be instantiated and attached to an | ||
existing Express application. The point or order at which you attach | ||
the service relative to other middleware affects the point at which | ||
it is invoked. | ||
|
||
```javascript | ||
const auth = require('@pathcheck/safeplaces-auth'); | ||
|
||
// Instantiate a public key retrieval client. | ||
const pkClient = new auth.JWKSClient( | ||
// Pass the URL for the JWKS. | ||
`${process.env.AUTH0_BASE_URL}/.well-known/jwks.json`, | ||
); | ||
|
||
// Instantiate a request verification strategy. | ||
const auth0Strategy = new auth.strategies.Auth0({ | ||
jwksClient: pkClient, | ||
// The API audience is checked against the user's access token. | ||
apiAudience: process.env.AUTH0_API_AUDIENCE, | ||
}); | ||
|
||
// Instantiate a strategy enforcer. | ||
const enforcer = new auth.Enforcer({ | ||
strategy: auth0Strategy, | ||
// Pass in an asynchronous database lookup function | ||
// that takes the user ID as the only argument. | ||
userGetter: id => User.findOne(id), | ||
}); | ||
|
||
// `app` is your Express application. | ||
// A middleware is attached to the app, and it intercepts every | ||
// request for verification. The argument to `enforcer.secure` | ||
// affects which routes are secured. | ||
enforcer.secure(app); | ||
``` | ||
|
||
For greater flexibility, you access the built-in `enforcer.handleRequest(req, res)` | ||
function to selectively handle requests under your own logic. Note that `enforcer` | ||
will continue to end the request with `403 Forbidden` if the request is unauthorized. | ||
|
||
```javascript | ||
app.use((req, res, next) => { | ||
if (req.headers['X-Bypass-Login']) { | ||
return next(); | ||
} else { | ||
return enforcer | ||
// Enforcer ends the request with a `403 Forbidden` if it is unauthorized, | ||
// meaning `next` will not be called unless the request is authorized. | ||
.handleRequest(req, res, next) | ||
.catch(err => next(err)); | ||
} | ||
}); | ||
``` | ||
|
||
For the most flexibility, you can use `enforcer.processRequest(req)` to only validate a request, | ||
allowing you to decide how to handle the validation result yourself, whether that be ending | ||
the request or ignoring the error. | ||
|
||
```javascript | ||
app.use((req, res, next) => { | ||
enforcer | ||
.processRequest(req) | ||
// `.then` is called only if `processRequest` has | ||
// determined the request to be authorized. | ||
.then(note => { | ||
// `note` is an error encountered by `processRequest` that may be indicated in server logs | ||
// or in a request header. It was not enough to halt the request, but the server | ||
// may encounter an unexpected error if logic continues. | ||
console.log(note); | ||
next(); | ||
}) | ||
// Otherwise, an error describing the validation error | ||
// will be thrown, and you can decide what to do with it. | ||
.catch(err => res.status(403).send('Forbidden')); | ||
}); | ||
``` | ||
|
||
## Handling login requests | ||
|
||
```javascript | ||
// Instantiate a login handler. | ||
const loginHandler = new auth.handlers.Login({ | ||
auth0: { | ||
baseUrl: process.env.AUTH0_BASE_URL, | ||
apiAudience: process.env.AUTH0_API_AUDIENCE, | ||
clientId: process.env.AUTH0_CLIENT_ID, | ||
clientSecret: process.env.AUTH0_CLIENT_SECRET, | ||
realm: process.env.AUTH0_REALM, | ||
}, | ||
cookie: { | ||
// Enable/disable cookie attributes depending on environment. | ||
secure: process.env.NODE_ENV !== 'development', | ||
sameSite: process.env.BYPASS_SAME_SITE !== 'true', | ||
}, | ||
}); | ||
|
||
// Handle all requests to the login endpoint. | ||
// Since we are passing around the `handle` function, make sure | ||
// to bind the handle function to its object. | ||
app.post('/auth/login', loginHandler.handle.bind(loginHandler)); | ||
``` | ||
|
||
## Handling logout requests | ||
|
||
```javascript | ||
// Instantiate a logout handler. | ||
const logoutHandler = new auth.handlers.Logout({ | ||
redirect: 'https://example.com/logout-success/', | ||
cookie: { | ||
// Enable/disable cookie attributes depending on environment. | ||
secure: process.env.NODE_ENV !== 'development', | ||
sameSite: process.env.BYPASS_SAME_SITE !== 'true', | ||
}, | ||
}); | ||
|
||
// Handle all requests to the logout endpoint. | ||
// Since we are passing around the `handle` function, make sure | ||
// to bind the handle function to its object. | ||
app.get('/auth/logout', logoutHandler.handle.bind(logoutHandler)); | ||
``` | ||
|
||
# Strategies | ||
|
||
**Supported strategies:** | ||
- Auth0 asymmetric JWT | ||
- Symmetric JWT | ||
|
||
## Auth0 | ||
|
||
Validate the JSON Web Token by checking the signature with | ||
the retrieved public key, and validate the API audience. | ||
|
||
```javascript | ||
const auth = require('@pathcheck/safeplaces-auth'); | ||
|
||
const pkClient = new auth.JWKSClient( | ||
`${process.env.AUTH0_BASE_URL}/.well-known/jwks.json`, | ||
); | ||
const auth0Strategy = new auth.strategies.Auth0({ | ||
jwksClient: pkClient, | ||
apiAudience: process.env.AUTH0_API_AUDIENCE, | ||
}); | ||
``` | ||
|
||
## Symmetric JWT | ||
|
||
Validate the JSON Web Token by checking the signature with | ||
a fixed private key. | ||
|
||
```javascript | ||
const auth = require('@pathcheck/safeplaces-auth'); | ||
|
||
const symJWTStrategy = new auth.strategies.SymJWT({ | ||
algorithm: 'HS256', | ||
privateKey: process.env.JWT_SECRET, | ||
}); | ||
``` | ||
|
||
## Dynamic strategy selection | ||
|
||
You can also pass a function into the strategy parameter | ||
to dynamically select the strategy based on the request | ||
or some other variables. | ||
|
||
The function should accept the request as the only argument | ||
and return the desired strategy or a promise resolving the | ||
desired strategy. | ||
|
||
```javascript | ||
const enforcer = new auth.Enforcer({ | ||
strategy: req => { | ||
console.log(req); | ||
// Check something in the request. | ||
// ... | ||
// Example conditional: | ||
if (process.env.NODE_ENV === 'development') { | ||
return symJWTStrategy; | ||
} else { | ||
return auth0Strategy; | ||
} | ||
}, | ||
}); | ||
``` | ||
|
||
# Debugging | ||
|
||
To debug why the `enforcer` is rejecting a request, set the | ||
`AUTH_LOGGING` environment variable to `verbose`, and the request | ||
validation error will be logged every time. | ||
See [SafePlaces Information Security API](docs/api/README.md) for more information. |
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,14 @@ | ||
# SafePlaces Information Security API | ||
|
||
The SafePlaces Information Security API serves 3 primary goals: | ||
|
||
1. Determining and verifying the identity of a user | ||
2. Granting users access to resources based on their authorization level | ||
3. Managing the user pool and allowing users to perform self-service tasks | ||
|
||
## Table of Contents | ||
|
||
- [API Reference](reference.md) | ||
- [Multi-factor Authentication API](mfa/README.md) | ||
- [User Management API](users/README.md) | ||
- [Multi-factor Authentication Flow](mfa-flow.md) |
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 @@ | ||
# Log in | ||
|
||
Logs in and obtains an access token for a registered user. | ||
|
||
```http request | ||
POST /auth/login | ||
``` | ||
|
||
### Examples | ||
|
||
```json | ||
{ | ||
"username": "[email protected]", | ||
"password": "xyz2020" | ||
} | ||
``` | ||
|
||
## Default Response | ||
|
||
The access token was sent in the `Set-Cookie` response header. The cookie cannot be accessed by JavaScript. | ||
|
||
The user ID was returned in the response body, and it can be used to call other endpoints | ||
to act on the created user. | ||
|
||
```http request | ||
Status: 200 OK | ||
``` | ||
|
||
```json | ||
{ | ||
"id": "a0f853d6-4c28-4384-9853-bec18293bfa9" | ||
} | ||
``` | ||
|
||
## Error Response | ||
|
||
The `username` and `password` combination is wrong. | ||
|
||
```http request | ||
Status: 401 Unauthorized | ||
``` | ||
|
||
```json | ||
{ | ||
"error": "InvalidCredentials", | ||
"message": "Wrong username or password" | ||
} | ||
``` | ||
|
||
<br/> | ||
|
||
Multi-factor authentication is required. | ||
|
||
```http request | ||
Status: 401 Unauthorized | ||
``` | ||
|
||
```json | ||
{ | ||
"error": "MFARequired", | ||
"message": "Multifactor authentication required", | ||
"mfa_token": "Fe26.2*82dcca*be8149..." | ||
} | ||
``` | ||
|
||
<br/> | ||
|
||
The user was found not in the database. | ||
|
||
```http request | ||
Status: 500 Internal Server Error | ||
``` | ||
|
||
```json | ||
{ | ||
"error": "DBError", | ||
"message": "Unable to find user in DB" | ||
} | ||
``` | ||
|
||
<br/> | ||
|
||
The access token returned by the identity provider is malformed. | ||
|
||
```http request | ||
Status: 500 Internal Server Error | ||
``` | ||
|
||
```json | ||
{ | ||
"error": "InternalServerError", | ||
"message": "Unable to decode access token" | ||
} | ||
``` |
Oops, something went wrong.