@financial-times/okta-express-middleware
v1.1.0
Published
FT okta express middleware to verify jwt
Downloads
3,003
Maintainers
Keywords
Readme
Okta Express Middleware
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 thecookie session
configuration
app.use(session({
...
path: '/path-for-router-module',
}));
- Configure the
express router
to use theOktaMiddleware
andcookie-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 theOktaMiddleware
configuration
app.use('/path-for-router-module', router);
Config Options
Required
client_id
- See Prerequsitesclient_secret
- See Prerequsitesissuer
- Check the Okta documentation on Tech Hub for issuer URLappBaseUrl
- Your app's base url (e.g.https://foo.ft.com
)scope
- See Scopes
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 ashttps://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 forroutes.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
- Create a branch in this repository, clone it, and make your changes.
- In your application or our handy test application, run
npm install git+ssh://[email protected]/Financial-Times/okta-express-middleware.git#{INSERT_BRANCH_NAME_HERE}
. - Check that your changes do what you want them to do.
- Add tests to test/app.test.js
- 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:
- Copy
.env.example
to.env
file and update it with values ofNode example app
inft-dev.oktapreview.com
- Run
npm install
- Run
npm start
- Open Incognito Mode in your browser and go to
http://localhost:3000
- Log in using Okta/GSuite
- If you see
Authenticated as...
on the page then the initial login is successful - Refresh the page after the initial access token expires as indicated on the page
- If you see
New access token was obtained using refresh token
then refresh token flow is successful too