@sakuraapi/auth-native-authority
v0.6.2
Published
Middleware to support native authentication capabilities
Downloads
6
Readme
Status
|Branch |Status | |-----------|-----------| | Develop || | Master ||
@sakuraapi/auth-native-authority
Middleware that adds native (email/password) authentication authority capabilities to a SakuraApi server. An "authentication authority" (in SakuraApi land) is a micro-service / server that is responsible for authentication on behalf of itself and other micro-services. Other SakuraApi micro-services trusting this authority will implement a separate package for consuming the tokens generated by the authentication authority.
npm install @sakuraapi/auth-native-authority
Integration
Somewhere, early, in the bootstrapping of SakuraApi (this example assumes you're following the pattern of having a sakura-api.ts
file that boostraps SakuraAPI):
import {SakuraApi} from '@sakuraapi/api';
import {dbs} from './config/db';
import {addAuthenticationAuthority} from '@sakuraapi/auth-native-authority';
import bodyParser = require('body-parser');
import helmet = require('helmet');
export const sapi = new SakuraApi();
sapi.baseUri = '/api';
sapi.addMiddleware(helmet());
sapi.addMiddleware(bodyParser.json());
addAuthenticationAuthority(sapi, {
userDbConfig: dbs.user,
authDbConfig: dbs.authentication,
defaultDomain: 'default',
onJWTPayloadInject: onJWTPayloadInject,
onBeforeUserCreate: onBeforeUserCreate,
onUserCreated: onUserCreated,
onResendEmailConfirmation: onResendEmailConfirmation,
onForgotPasswordEmailRequest: onForgotPasswordEmailRequest,
onChangePasswordEmailRequest: onChangePasswordEmailRequest
});
function onJWTPayloadInject(payload: any, dbResult: any) {
// allows you to inject other stuff in to the JWT payload...For example, you might want to look up
// additional fields from other collections in your app that need to be in the JWT. This is the place to
// do that. The dbResult (the user's record) so that you can also grab stuff out of there.
// When you're done modifying the payload to your heart's content, return it as the result of the Promise.
payload.hi = 'Super custom JWT payload property';
return Promise.resolve(payload);
}
function onBeforeUserCreate(res, req, next) {
// called when a `POST /api/auth/native/` happens (see Create User below). Allows you to
// do whatever manipulation of the incoming request before proceeding. You may want to
// implement some kind of safeguard to prevent bots from peppering your server with bogus users.
//
// You can also implement some kind of garbage collector that deletes users after a certain amount
// of time if their email addresses remain unverified.
next();
}
function onUserCreated(user: any, token: string, req, res): Promise<any> {
// wire up your outbound email that sends the email verification token to the
// user... This should redirect to your servers's email verification endpoint that's added
// by @sakuraapi/auth-native-authority. For example:
// GET localhost:8001/api/auth/native/confirm/R5dtU3y302JZuITtSFrNqdl5Mv0QkOPh3VcfgOG86NVkzb385Q.s3RGM0FA0rSfFknjZTfROg.SClksqzZDersBoC2CfhXkQ
// When the user clicks on that link, their user record will be updated as having verified email address ownership.
if (req.query.mobile) {
res.locals.send(200, {welcome: `${user.email}, please check your email.`});
} else {
res.redirect('http://some-link-welcoming-the-user-and-giving-them-next-steps-like-checking-their-email');
}
return Promise.resolve();
}
function onResendEmailConfirmation(user: any, token: string, req, res): Promise<any> {
// add logic here to resend the link with the email verification token
return Promise.resolve();
}
function onForgotPasswordEmailRequest(user: any, token: string, req, res): Promise<any> {
// send an email with a forgot password token
// This email shoudl direct them to a link on your site or a deep link into your app that receives the token then puts it
// to your server with the token and {"password":"123"} as the body. For example:
// PUT localhost:8001/api/auth/native/reset-password/WTtK-g3k-NbikCYxbQ9n97jA.9cXEoJNoMBcxWAGMlB7x4g.x89KcHr0bp-tYkZobbZ10A
return Promise.resolve();
}
function onChangePasswordEmailRequest(user: any, req, res): Promise<any> {
// send an email after successful password change
}
Config
Sample:
"server": {
"address": "127.0.0.1",
"port": 8001
},
"authentication": {
"native": {
"bcryptHashRounds": 12,
"create": {
"acceptFields": {
"firstName": "fn",
"lastName": "ln",
"phone": "ph"
}
}
},
"jwt": {
"key": "%o9*rMlaU#nm*1m%x!8FSvnqil#$#wsk",
"issuer": "profile.someserver.org",
"exp": "48h",
"fields": {
"fn": "firstName",
"ln": "lastName",
"_id": "id"
},
"audiences": {
"donations.someserver.org": "pJ2@Ymf5#3v53%iKj7vY^G#Qdt&mEnBf",
"reports.someserver.org": "106s2h*29I@vUrhg&toDpLLCltyf0mYl"
}
}
}
}
The authentication.native
section defines how @sakuraapi/auth-native-authority
behaves.
bcryptHashRounds
is how many hash rounds bcrypt should go through when hashing the user's passwordcreate
handles field mapping of custom fields. For example, the body on user creation is expected to contain afirstName
field and it will be mapped to the user's record asfn
.
The jwt
section defines how the JWT token is generated. The key is the main private AES-256 key for the token used by your Authentication Authority (All keys should be 32 characters long, and highly complex with high entropy. Consider using LastPass to generate these keys).
issuer
is the JWT identification for the authentication authority server (i.e., the server that implements this plugin).exp
is how long the token should be good for before it is considered expiredfields
are additional fields from the user's document that should be included in the JWT tokenaudiences
are the target servers that should be included in the resulting token dictionary.
Supporting multiple audiences allows the authentication authority (the issuer) to serve multiple other micro-services without needing those micro-services to share secrets with each other. They only have to trust the issuer.
Token Dictionary
The following is returned upon successful login (based on the sample configuration above):
{
"token": {
"profile.someserver.org": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imdlb3JnZV93YXNoaW5ndG9uQHdoaXRlaG91c2UuZ292IiwiZG9tYWluIjoiZGVmYXVsdCIsImZpcnN0TmFtZSI6Ikdlb3JnZSIsImxhc3ROYW1lIjoiV2FzaGluZ3RvbiIsImlkIjoiNTkzMWIyNTE4NjVlNWI4Mjk4YmQ1ZmZmIiwiaXNzU2lnIjoiYzc1N2FlNTkwNjFlNjczN2YyYjIwMjVmOGQ3NThkNTVjNGY4ZjY1YjdhZjdkMjA4Y2EyZjlmZWZjZDMxYmJhOCIsImlhdCI6MTQ5NjQyOTE4OSwiZXhwIjoxNDk2NjAxOTg5LCJhdWQiOiJwcm9maWxlLnNvbWVzZXJ2ZXIub3JnIiwiaXNzIjoicHJvZmlsZS5zb21lc2VydmVyLm9yZyIsImp0aSI6IjA1ZDQ0MTc2LTM0MmUtNDdjNi05ZDNiLWMwZWZjNzIxZTc2OSJ9.zryHww9R8y68jiRqSbf1OOYq88CQ69UsuAlwKWjuftk",
"donations.someserver.org": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imdlb3JnZV93YXNoaW5ndG9uQHdoaXRlaG91c2UuZ292IiwiZG9tYWluIjoiZGVmYXVsdCIsImZpcnN0TmFtZSI6Ikdlb3JnZSIsImxhc3ROYW1lIjoiV2FzaGluZ3RvbiIsImlkIjoiNTkzMWIyNTE4NjVlNWI4Mjk4YmQ1ZmZmIiwiaXNzU2lnIjoiYzc1N2FlNTkwNjFlNjczN2YyYjIwMjVmOGQ3NThkNTVjNGY4ZjY1YjdhZjdkMjA4Y2EyZjlmZWZjZDMxYmJhOCIsImlhdCI6MTQ5NjQyOTE4OSwiZXhwIjoxNDk2NjAxOTg5LCJhdWQiOiJkb25hdGlvbnMuc29tZXNlcnZlci5vcmciLCJpc3MiOiJwcm9maWxlLnNvbWVzZXJ2ZXIub3JnIiwianRpIjoiMDVkNDQxNzYtMzQyZS00N2M2LTlkM2ItYzBlZmM3MjFlNzY5In0.weoMYNFXQ3skhVXCFzOYDFtFMXDyNWhuyVPtGZIlRfs",
"reports.someserver.org": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imdlb3JnZV93YXNoaW5ndG9uQHdoaXRlaG91c2UuZ292IiwiZG9tYWluIjoiZGVmYXVsdCIsImZpcnN0TmFtZSI6Ikdlb3JnZSIsImxhc3ROYW1lIjoiV2FzaGluZ3RvbiIsImlkIjoiNTkzMWIyNTE4NjVlNWI4Mjk4YmQ1ZmZmIiwiaXNzU2lnIjoiYzc1N2FlNTkwNjFlNjczN2YyYjIwMjVmOGQ3NThkNTVjNGY4ZjY1YjdhZjdkMjA4Y2EyZjlmZWZjZDMxYmJhOCIsImlhdCI6MTQ5NjQyOTE4OSwiZXhwIjoxNDk2NjAxOTg5LCJhdWQiOiJyZXBvcnRzLnNvbWVzZXJ2ZXIub3JnIiwiaXNzIjoicHJvZmlsZS5zb21lc2VydmVyLm9yZyIsImp0aSI6IjA1ZDQ0MTc2LTM0MmUtNDdjNi05ZDNiLWMwZWZjNzIxZTc2OSJ9.ZHf6oAIgt7IbGu4nQ5VputBl8lwvfCSjAdEdxLsD1KY"
}
}
Each audience (and the issuer itself) gets its own signed JWT. For example, the profile.someserver.org
token would be signed by the issuer key and would contain the following payload based on the sample configuration above:
{
"email": "[email protected]",
"domain": "default",
"firstName": "George",
"lastName": "Washington",
"id": "5931b251865e5b8298bd5fff",
"issSig": "c757ae59061e6737f2b2025f8d758d55c4f8f65b7af7d208ca2f9fefcd31bba8",
"iat": 1496429189,
"exp": 1496601989,
"aud": "profile.someserver.org",
"iss": "profile.someserver.org",
"jti": "05d44176-342e-47c6-9d3b-c0efc721e769"
}
The issSig
is the payload signed by the issuer so that if an audience server needs to send a token to the issuer (the authentication authority), the issuer can verify that the token hasn't been modified since being issued by the issuer since the audience server has its own private key and could modify the package and since the issue cannot trust that the audience server isn't compromised.
You can read more about JWT here: https://jwt.io/.
Remember, JWT is base64 encoded, so you can decode it in your client and grab whatever fields your need. If you
need to communicate encrypted data that the client doesn't have access to, use the onJWTPayloadInject
to modify
the payload fields appropriately (e.g., AES encrypt some value).
Endpoints
Assuming your have a sapi.baseUri = '/api';
Login
POST /api/auth/native/login Body:
{
"email":"[email protected]",
"password": "SomethingSuperSecure"
}
Create User
POST /api/auth/native/
{
"email":"[email protected]",
"domain": "default",
"password": "IuIwCmKyKVZdj&400IlSW&cyzd0EVZE2",
"firstName": "George",
"lastName": "Washington",
"phone": "(202) 456-1111"
}
Verify an email address
GET /api/auth/native/confirm/mqfKg-vXfl6jfHmrgN4CvzgiYZ-5QVbf_WEiHWQz-7mpGLvgdg.tcay86ro6kCH_PZsK3V1VQ.gomOs3y4PGdN1a9YKs3Igw
Assuming the token received was mqfKg-vXfl6jfHmrgN4CvzgiYZ-5QVbf_WEiHWQz-7mpGLvgdg.tcay86ro6kCH_PZsK3V1VQ.gomOs3y4PGdN1a9YKs3Igw
.
Forgot Password
PUT /api/auth/native/forgot-password Body:
{
"email":"[email protected]"
}
Reset Forgotten Password
PUT /api/auth/native/reset-password/WTtK-g3k-NbikCYxbQ9n97jAf17VVZJ98oz2V96AknCZ1cr1k3_tUAtwyDNftRoTT07e0AW-LAdj91Mb.9cXEoJNoMBcxWAGMlB7x4g.x89KcHr0bp-tYkZobbZ10A Body:
{
"password":"someSuperSecureNewPassword"
}
Assuming the "forgot password" token received was WTtK-g3k-NbikCYxbQ9n97jAf17VVZJ98oz2V96AknCZ1cr1k3_tUAtwyDNftRoTT07e0AW-LAdj91Mb.9cXEoJNoMBcxWAGMlB7x4g.x89KcHr0bp-tYkZobbZ10A
Change Passsword
PUT /api/auth/native/change-password/ Body:
{
"email":"[email protected]",
"currentPassword": "123",
"newPassword": "321"
}
Indexing
@sakuraapi/auth-native-authority
relies on two collections:
- A user collection (defined by
userDbConfig
in the example above) that stores documents representing users - An authentication collection (defined by
authDbConfig
in the example above) that stores all active tokens that have been issued.
Both collections are supplied by the integrator (you). As the integrator, you should ensure that proper indexing is applied to these collections. For example, if you turn on domain support, you should ensure a unique index for the user's email address and the domain. For the authentication collection, you should set the TTL index to automatically delete documents older than the TTL (or some period longer than that if you want to keep them around for a while for short-term auditing purposes).
Contributions
See: CONTRIBUTING for details.