@financial-times/okta-express-middleware

1.0.7 • Public • Published

Okta Express Middleware

CircleCI

This middleware builds on top of Okta's middleware and JWT verifier in order to provide a fully capable but simple to integrate OIDC authentication to Express based apps. It will verify the access token and ID token (including expiry). If either of these are not valid it will either start the refresh token flow or the authorization code flow.

You should be using cookie-session or express-session. Our recommendation is to use cookie-session unless the application is already utilising express-session.

Why use this instead of Okta's library

Okta's library never checks the expiration of the tokens inside the session. Access tokens are valid for an hour and refresh tokens are valid for 12 hours so the cookie that underpins the cookie-session has a TTL of 12 hours. When you have a cookie with an expired access token and valid refresh token, Okta's library lets you in without initiating refresh token flow flow. You could also exploit cookie-session by modifying the cookie TTL in browser. In this exploit scenario, Okta's library lets you in with an expired access token and expired refresh token. In express-session, the TTL of the session could be increased with each request.

We built this library on top of Okta's library and added functionality to verify tokens in the session on every request.

Prerequisites

The application will require a pair of client id and client secret. Both are obtained when adding the app configuration to Okta. Please follow the Integration Guide to add your application as an OAuth 2.0 app, also referred to as web_oidc within the config.

How to use

Install the library

npm install @financial-times/okta-express-middleware

Import the class

const OktaMiddleware = require('@financial-times/okta-express-middleware');

Set up cookie-session or express-session.

The syntax for specifying cookie properties (e.g. maxAge, httpOnly etc) is different for express-session and cookie-session. Please ensure you are using the right syntax.

In the examples below, SuperSecretValueHere must be a randomly generated, securely stored key e.g. generate a UUID and store in vault with your other environment variables.

Cookie Session

cookie-session is a simple session store for keeping small amounts of session data.

const session = require('cookie-session');

app.use(session({
  secret: 'SuperSecretValueHere',
  maxAge: 12 * 3600 * 1000, // 12 hours
  httpOnly: true,
  secure: true
}));

If you are using Heroku or your server is not directly serving requests over a TLS connection (e.g. an EC2 instance serving http traffic and fronted by an ELB which terminates TLS), add the below so the cookie's secure flag is set correctly. Ref:Express JS Behind Proxies

app.enable('trust proxy');

Express Session

express-session uses a data store like Redis for storing larger sessions. It might be more complex to set up.

const session = require('express-session');

app.use(session({
  secret: 'SuperSecretValueHere',
  cookie: {
    maxAge: 12 * 3600 * 1000, // 12 hours
    httpOnly: true,
    secure: true
  }
}));

Instantiate the class

const okta = new OktaMiddleware({
  client_id: 'dummyClientId',
  client_secret: 'dummyClientSecret',
  issuer: 'https://foo.okta.com/dummy-issuer',
  appBaseUrl: 'https://foo.ft.com',
  scope: 'openid name offline_access',
});

Use the router and the two middleware functions:

app.use(okta.router);
app.use(okta.ensureAuthenticated());
app.use(okta.verifyJwts());

Routes declared before this block are not protected by Okta and routes declared after the block are protected by Okta.

// This route is not protected by Okta
app.get('/__gtg', (req, res) => {
  res.send('Good to go 👍');
});

app.use(okta.router);
app.use(okta.ensureAuthenticated());
app.use(okta.verifyJwts());

// This route is protected by Okta
app.use('/', landing);

If static assets used in your application do not contain secure information, we recommend you declare static assets before calling the middleware block. This minimizes Okta login attempts during refresh token flow.

// Static assets are not protected by Okta
app.use(express.static('public'));

app.use(okta.router);
app.use(okta.ensureAuthenticated());
app.use(okta.verifyJwts());

Wait for express-middleware to initialise

The middleware must retrieve some information about your client before starting the server. You must wait until it is ready before starting your server.

The onReady() method in the OktaMiddleware class has been introduced for this purpose.

okta.onReady(() => {
  app.listen(port, () => console.log(`The app has started!`));
});

There is also an optional onError() method for error handling. Errors could happen during the startup process or during logout.

okta.onError((err) => {
  // An error has happened
});

Express Router

Using the express router with the OktaMiddleware requires the following additional configuration:

  • Add a path declaration to the cookie session configuration
app.use(session({
	...
	path: '/path-for-router-module',
}));
  • Configure the express router to use the OktaMiddleware and cookie-session
app.use(okta.router); // keep 'okta.router' as 'app.use'
router.use(cookieParser());
router.use(okta.ensureAuthenticated());
router.use(okta.verifyJwts());
  • Finally load the express router module into the app after the OktaMiddleware configuration
app.use('/path-for-router-module', router);

Config Options

Required

Optional

  • routes.loginCallback.path - Where Okta will callback to after the user has authenticated. Default: /authorization-code/callback

    if appBaseUrl is https://foo.ft.com, Okta express middleware constructs the redirect URL as https://foo.ft.com/authorization-code/callback. This exact URL needs to be defined as the redirect URI(s) in your Okta Application. If you instead prefer the redirect URL to be a different value e.g. https://foo.ft.com/callback, you have to override the login callback route by providing /callback as the value for routes.loginCallback.path; and update the redirect URI in your Okta application.

  • routes.login.path - Where in your application the middleware will redirect to if the user is unauthenticated. Default: /login

Scopes

Scopes allow you to request additional information about the user (See Okta's blog post)

These are the scopes our Okta servers currently support:

  • (REQUIRED) openid - Returns an ID Token and Access Token from Okta.
  • (RECOMMENDED) offline_access - Returns a Refresh Token from Okta which will be automatically used by the middleware. See Okta's blog post
  • name - Returns the user's first and last name in req.userContext.
  • email - Returns the user's email address in req.userContext.
  • groups - This scope returns the AD and Okta groups the user is a member of in req.userContext. This only returns groups that have been assigned to the application.

Examples

app.use(okta.verifyJwts());

Or

app.get('/behindOkta', okta.verifyJwts(), (req, res) => {
  res.send('Authenticated');
});

If you do not want the user to be redirected to Okta when they are not logged in, you can do...

app.use(okta.verifyJwts({redirect: false}));

Or

app.get('/behindOkta', okta.verifyJwts({redirect: false}), (req, res) => {
  res.send('Authenticated');
});

To obtain user info, you can extract it from req.userContext For example, to obtain the user email (only when using email scope, see above):

app.get('/behindOkta', okta.verifyJwts(), (req, res) => {
  res.send('user email is: ' + req.userContext.userinfo.email);
});

Also see an example at test/app.js

Development

  1. Create a branch in this repository, clone it, and make your changes.
  2. In your application or our handy test application, run npm install git+ssh://git@github.com/Financial-Times/okta-express-middleware.git#{INSERT_BRANCH_NAME_HERE}.
  3. Check that your changes do what you want them to do.
  4. Add tests to test/app.test.js
  5. Raise a PR.

We also have some tests that can be run with npm run test or npm run coverage.

Manual regression testing

End to end regression testing is useful to test external changes such as dependency version updates or backend (Okta) changes.

These are the steps to test the functionality end to end:

  1. Copy .env.example to .env file and update it with values of Node example app in ft-dev.oktapreview.com
  2. Run npm install
  3. Run npm start
  4. Open Incognito Mode in your browser and go to http://localhost:3000
  5. Log in using Okta/GSuite
  6. If you see Authenticated as... on the page then the initial login is successful
  7. Refresh the page after the initial access token expires as indicated on the page
  8. If you see New access token was obtained using refresh token then refresh token flow is successful too

Readme

Keywords

none

Package Sidebar

Install

npm i @financial-times/okta-express-middleware

Weekly Downloads

1,614

Version

1.0.7

License

UNLICENSED

Unpacked Size

42.1 kB

Total Files

17

Last publish

Collaborators

  • robertboulton
  • seraph2000
  • hamza.samih
  • notlee
  • emmalewis
  • aendra
  • the-ft
  • rowanmanning
  • chee
  • alexwilson