From e8143aea14b384422d3c87fd6e00754536c7258e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 5 Jun 2017 15:52:32 -0600 Subject: [PATCH] Added new tokenUserNotFound plugin hook --- PLUGINS.md | 35 +++++++++++++++++++++++++++++++++ services/passport.js | 22 ++++++++++++++++++--- services/users.js | 46 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 0d17ccd55..cf1eade31 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -280,6 +280,41 @@ send data to the client. If the type in question contains args, clients may subs For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions). +#### Field: `tokenUserNotFound` + +```js +tokenUserNotFound: async ({jwt, token}) => { + let profile = await someExternalService(token); + if (!profile) { + return null; + } + + let user = await UserModel.findOneAndUpdate({ + id: profile.id + }, { + id: profile.id, + username: profile.username, + lowercaseUsername: profile.username.toLowerCase(), + roles: [], + profiles: [] + }, { + setDefaultsOnInsert: true, + new: true, + upsert: true + }); + + return user; +} +``` + +The `tokenUserNotFound` hook allows auth integrations to hook into the event +when a valid token is provided but a user can't be found in the database that +matches the provided id. + +The function is async, and should return the user object that was created in the +database, or null if the user wasn't found. The `jwt` paramenter of the object +is the unpacked token, while `token` is the original jwt token string. + #### Field: `router` ```js diff --git a/services/passport.js b/services/passport.js index 4f5a8fb8d..72d1d3c3e 100644 --- a/services/passport.js +++ b/services/passport.js @@ -172,6 +172,7 @@ const CheckBlacklisted = (jwt) => new Promise((resolve, reject) => { }); }); +const jwt = require('jsonwebtoken'); const JwtStrategy = require('passport-jwt').Strategy; const ExtractJwt = require('passport-jwt').ExtractJwt; @@ -185,6 +186,19 @@ let cookieExtractor = function(req) { return token; }; +// 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.verify(token, secretOrKey, options, (err, jwt) => { + if (err) { + return callback(err); + } + + // Attach the original token onto the payload. + return callback(false, {token, jwt}); + }); +}; + // Extract the JWT from the 'Authorization' header with the 'Bearer' scheme. passport.use(new JwtStrategy({ @@ -207,10 +221,10 @@ passport.use(new JwtStrategy({ // Enable only the HS256 algorithm. algorithms: ['HS256'], - // Pass the request objecto back to the callback so we can attach the JWT to + // Pass the request object back to the callback so we can attach the JWT to // it. passReqToCallback: true -}, async (req, jwt, done) => { +}, async (req, {token, jwt}, done) => { // Load the user from the environment, because we just got a user from the // header. @@ -219,7 +233,9 @@ passport.use(new JwtStrategy({ // Check to see if the token has been revoked await CheckBlacklisted(jwt); - let user = await UsersService.findById(jwt.sub); + // Try to get the user from the database or crack it from the token and + // plugin integrations. + let user = await UsersService.findOrCreateByIDToken(jwt.sub, {token, jwt}); // Attach the JWT to the request. req.jwt = jwt; diff --git a/services/users.js b/services/users.js index 09ddd8ce9..5c6440a0e 100644 --- a/services/users.js +++ b/services/users.js @@ -9,6 +9,7 @@ const { JWT_SECRET, ROOT_URL } = require('../config'); +const debug = require('debug')('talk:services:users'); const redis = require('./redis'); const redisClient = redis.createClient(); @@ -526,6 +527,29 @@ module.exports = class UsersService { return UserModel.findOne({id}); } + /** + * + * @param {String} id the id of the current user + * @param {Object} token a jwt token used to sign in the user + */ + static async findOrCreateByIDToken(id, token) { + + // Try to get the user. + let user = await UserModel.findOne({ + id + }); + + // If the user was not found, try to look it up. + if (user === null) { + + // If the user wasn't found, it will return null and the variable will be + // unchanged. + user = await lookupUserNotFound(token); + } + + return user; + } + /** * Finds users in an array of ids. * @param {Array} ids array of user identifiers (uuid) @@ -903,3 +927,25 @@ module.exports = class UsersService { }); } }; + +// Extract all the tokenUserNotFound plugins so we can integrate with other +// providers. +const tokenUserNotFoundHooks = require('./plugins') + .get('server', 'tokenUserNotFound') + .map(({plugin, tokenUserNotFound}) => { + debug(`added plugin '${plugin.name}' to tokenUserNotFound hooks`); + + return tokenUserNotFound; + }); + +// Provide a function that +const lookupUserNotFound = async (token) => { + for (let hook of tokenUserNotFoundHooks) { + let user = await hook(token); + if (user !== null && typeof user !== 'undefined') { + return user; + } + } + + return null; +};