@sakuraapi/auth-native-authority
TypeScript icon, indicating that this package has built-in type declarations

0.6.2 • Public • Published

Status

Branch Status
Develop Build Status
Master Build Status

@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 password
  • create handles field mapping of custom fields. For example, the body on user creation is expected to contain a firstName field and it will be mapped to the user's record as fn.

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 expired
  • fields are additional fields from the user's document that should be included in the JWT token
  • audiences 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": "george_washington@whitehouse.gov",
  "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":"hi@sakuraapi.com",
	"password": "SomethingSuperSecure"
}

Create User

POST /api/auth/native/

{
	"email":"george_washington@whitehouse.gov",
	"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":"george.washington@whitehouse.gov"
}

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":"george.washington@whitehouse.gov",
	"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

CLA assistant

See: CONTRIBUTING for details.

Package Sidebar

Install

npm i @sakuraapi/auth-native-authority

Weekly Downloads

0

Version

0.6.2

License

BSD-3-Clause

Unpacked Size

63.6 kB

Total Files

7

Last publish

Collaborators

  • jpoveda
  • sakuraapi