From 8cdf289b13b70ae85ac24811d21f716c3f9e5aaf Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 4 Aug 2017 15:49:37 +1000 Subject: [PATCH] exposed more controls on jwt and added docs --- config.js | 21 +++++++++++++++++++++ docs/_docs/02-01-configuration.md | 21 +++++++++++++++++---- services/passport.js | 22 ++++++++++++++-------- services/tokens.js | 7 +++++-- 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/config.js b/config.js index 16353caaa..282b05ea9 100644 --- a/config.js +++ b/config.js @@ -35,11 +35,22 @@ const CONFIG = { // cleared when the user is logged out. JWT_CLEAR_COOKIE_LOGOUT: process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT ? process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT !== 'FALSE' : true, + // JWT_DISABLE_AUDIENCE when TRUE will disable the issuer claim (iss) from tokens. + JWT_DISABLE_AUDIENCE: process.env.TALK_JWT_DISABLE_AUDIENCE === 'TRUE', + // JWT_AUDIENCE is the value for the audience claim for the tokens that will be // verified when decoding. If `JWT_AUDIENCE` is not in the environment, then it // will default to `talk`. JWT_AUDIENCE: process.env.TALK_JWT_AUDIENCE || 'talk', + // JWT_DISABLE_ISSUER when TRUE will disable the issuer claim (iss) from tokens. + JWT_DISABLE_ISSUER: process.env.TALK_JWT_DISABLE_ISSUER === 'TRUE', + + // JWT_USER_ID_CLAIM is the claim which stores the user's id. This may be a deep + // object delimited using dot notation. Example `user.id` would store it like: + // {user: {id}} on the claims object. (Default `sub`) + JWT_USER_ID_CLAIM: process.env.TALK_JWT_USER_ID_CLAIM || 'sub', + // JWT_ISSUER is the value for the issuer for the tokens that will be verified // when decoding. If `JWT_ISSUER` is not in the environment, then it will try // `TALK_ROOT_URL`, otherwise, it will be undefined. @@ -144,6 +155,16 @@ if (CONFIG.JWT_SECRETS) { } } +// Disable the audience claim if requested. +if (CONFIG.JWT_DISABLE_AUDIENCE) { + CONFIG.JWT_AUDIENCE = undefined; +} + +// Disable the issuer claim if requested. +if (CONFIG.JWT_DISABLE_ISSUER) { + CONFIG.JWT_ISSUER = undefined; +} + //------------------------------------------------------------------------------ // External database url's //------------------------------------------------------------------------------ diff --git a/docs/_docs/02-01-configuration.md b/docs/_docs/02-01-configuration.md index d2e990375..475823b4c 100644 --- a/docs/_docs/02-01-configuration.md +++ b/docs/_docs/02-01-configuration.md @@ -77,15 +77,28 @@ The following are configuration shared with every type of secret used. tokens. (Default `process.env.TALK_ROOT_URL`) - `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens. (Default `talk`) + +**You must also specify secrets as either the `TALK_JWT_SECRET` or the `TALK_JWT_SECRETS` +variable. Refer to the [Secrets Documentation]({{ "/docs/running/secrets/" | absolute_url }}) +on the contents of those variables.** + +#### Advanced + +These are advanced settings for fine tuning the auth integration, and +is not needed in most situations. + - `TALK_JWT_COOKIE_NAME` (_optional_) - the name of the cookie to extract the JWT from (Default `authorization`) - `TALK_JWT_CLEAR_COOKIE_LOGOUT` (_optional_) - when `FALSE`, Talk will not clear the cookie with name `TALK_JWT_COOKIE_NAME` when logging out (Default `TRUE`) - -**You must also specify secrets as either the `TALK_JWT_SECRET` or the `TALK_JWT_SECRETS` -variable. Refer to the [Secrets Documentation]({{ "/docs/running/secrets/" | absolute_url }}) -on the contents of those variables.** +- `TALK_JWT_DISABLE_AUDIENCE` (_optional_) - when `TRUE`, Talk will not verify or sign JWT's + with an audience (`aud`) claim, even if the `TALK_JWT_AUDIENCE` config is set. (Default `FALSE`) +- `TALK_JWT_DISABLE_ISSUER` (_optional_) - when `TRUE`, Talk will not verify or sign JWT's + with an issuer (`iss`) claim, even if the `TALK_JWT_ISSUER` config is set. (Default `FALSE`) +- `TALK_JWT_USER_ID_CLAIM` (_optional_) - specify the claim using dot notation for where the + user id should be stored/read to/from. Example `user.id` would store it like: `{user: {id}}` + on the claims object. (Default `sub`) ### Email diff --git a/services/passport.js b/services/passport.js index 8b5b3879b..409bb2279 100644 --- a/services/passport.js +++ b/services/passport.js @@ -1,4 +1,5 @@ const passport = require('passport'); +const {set, get} = require('lodash'); const UsersService = require('./users'); const SettingsService = require('./settings'); const TokensService = require('./tokens'); @@ -23,21 +24,26 @@ const { RECAPTCHA_SECRET, RECAPTCHA_ENABLED, JWT_COOKIE_NAME, - JWT_CLEAR_COOKIE_LOGOUT + JWT_CLEAR_COOKIE_LOGOUT, + JWT_USER_ID_CLAIM, } = require('../config'); const { - jwt: JWT_SECRET + jwt } = require('../secrets'); // GenerateToken will sign a token to include all the authorization information // needed for the front end. const GenerateToken = (user) => { - return JWT_SECRET.sign({}, { + const claims = {}; + + // Set the user id. + set(claims, JWT_USER_ID_CLAIM, user.id); + + return jwt.sign(claims, { jwtid: uuid.v4(), expiresIn: JWT_EXPIRY, issuer: JWT_ISSUER, - subject: user.id, audience: JWT_AUDIENCE, algorithm: JWT_ALG }); @@ -191,7 +197,7 @@ const CheckBlacklisted = async (jwt) => { // Check to see if this is a PAT. if (jwt.pat) { - return TokensService.validate(jwt.sub, jwt.jti); + return TokensService.validate(get(jwt, JWT_USER_ID_CLAIM), jwt.jti); } // It wasn't a PAT! Check to see if it is valid anyways. @@ -216,7 +222,7 @@ let cookieExtractor = function(req) { // Override the JwtVerifier method on the JwtStrategy so we can pack the // original token into the payload. JwtStrategy.JwtVerifier = (token, secretOrKey, options, callback) => { - return JWT_SECRET.verify(token, options, (err, jwt) => { + return jwt.verify(token, options, (err, jwt) => { if (err) { return callback(err); } @@ -238,7 +244,7 @@ passport.use(new JwtStrategy({ // Use the secret passed in which is loaded from the environment. This can be // a certificate (loaded) or a HMAC key. - secretOrKey: JWT_SECRET, + secretOrKey: jwt, // Verify the issuer. issuer: JWT_ISSUER, @@ -265,7 +271,7 @@ passport.use(new JwtStrategy({ // Try to get the user from the database or crack it from the token and // plugin integrations. - user = await UsersService.findOrCreateByIDToken(jwt.sub, {token, jwt}); + user = await UsersService.findOrCreateByIDToken(get(jwt, JWT_USER_ID_CLAIM), {token, jwt}); } // Attach the JWT to the request. diff --git a/services/tokens.js b/services/tokens.js index 9c9af34a4..bedd5b272 100644 --- a/services/tokens.js +++ b/services/tokens.js @@ -1,10 +1,12 @@ const errors = require('../errors'); const UserModel = require('../models/user'); const uuid = require('uuid'); +const {set, get} = require('lodash'); const { JWT_ISSUER, - JWT_AUDIENCE + JWT_AUDIENCE, + JWT_USER_ID_CLAIM, } = require('../config'); const { @@ -30,10 +32,11 @@ module.exports = class TokenService { jti: uuid.v4(), iss: JWT_ISSUER, aud: JWT_AUDIENCE, - sub: userID, pat: true }; + set(payload, JWT_USER_ID_CLAIM, userID); + // Sign the payload. const jwt = JWT_SECRET.sign(payload, {});