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/bin/cli-assets b/bin/cli-assets
index 166152baf..97b5b043c 100755
--- a/bin/cli-assets
+++ b/bin/cli-assets
@@ -5,7 +5,7 @@
*/
const program = require('./commander');
-const parseDuration = require('parse-duration');
+const parseDuration = require('ms');
const Table = require('cli-table');
const AssetModel = require('../models/asset');
const mongoose = require('../services/mongoose');
diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js
index 3c94d5f81..264b1b2aa 100644
--- a/client/coral-embed-stream/src/components/Embed.js
+++ b/client/coral-embed-stream/src/components/Embed.js
@@ -41,10 +41,10 @@ export default class Embed extends React.Component {
return (
-
-
- {t('framework.my_profile')}
- {t('framework.configure_stream')}
+
+
+ {t('framework.my_profile')}
+ {t('framework.configure_stream')}
{commentId &&
(
+export default ({children, tabId, active, onTabClick, cStyle = 'base', ...props}) => (
onTabClick(tabId)}
>
{children}
);
-
diff --git a/client/coral-ui/components/TabBar.js b/client/coral-ui/components/TabBar.js
index d03a5d5b1..d04ccc30d 100644
--- a/client/coral-ui/components/TabBar.js
+++ b/client/coral-ui/components/TabBar.js
@@ -17,7 +17,7 @@ class TabBar extends React.Component {
const {children, activeTab, cStyle = 'base'} = this.props;
return (
-
+
{React.Children.toArray(children)
.filter((child) => !child.props.restricted)
.map((child, tabId) =>
diff --git a/package.json b/package.json
index fe87f17f5..14893de54 100644
--- a/package.json
+++ b/package.json
@@ -92,11 +92,11 @@
"minimist": "^1.2.0",
"mongoose": "^4.9.8",
"morgan": "^1.8.1",
+ "ms": "^2.0.0",
"natural": "^0.5.0",
"node-emoji": "^1.5.1",
"node-fetch": "^1.6.3",
"nodemailer": "^2.6.4",
- "parse-duration": "^0.1.1",
"passport": "^0.3.2",
"passport-jwt": "^2.2.1",
"passport-local": "^1.0.0",
diff --git a/services/passport.js b/services/passport.js
index 4f5a8fb8d..03602cdb6 100644
--- a/services/passport.js
+++ b/services/passport.js
@@ -10,6 +10,7 @@ const uuid = require('uuid');
const debug = require('debug')('talk:passport');
const {createClient} = require('./redis');
const bowser = require('bowser');
+const ms = require('ms');
// Create a redis client to use for authentication.
const client = createClient();
@@ -39,7 +40,7 @@ const SetTokenForSafari = (req, res, token) => {
if (browser.ios || browser.safari) {
res.cookie('authorization', token, {
httpOnly: true,
- expires: new Date(Date.now() + 900000)
+ expires: new Date(Date.now() + ms(JWT_EXPIRY))
});
}
};
@@ -172,6 +173,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 +187,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 +222,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 +234,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;
+};
diff --git a/yarn.lock b/yarn.lock
index 9855d91e8..9e3b3084d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5365,6 +5365,10 @@ ms@0.7.3, ms@^0.7.1:
version "0.7.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff"
+ms@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
muri@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/muri/-/muri-1.2.1.tgz#ec7ea5ce6ca6a523eb1ab35bacda5fa816c9aa3c"
@@ -5861,10 +5865,6 @@ parse-asn1@^5.0.0:
evp_bytestokey "^1.0.0"
pbkdf2 "^3.0.3"
-parse-duration@^0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-0.1.1.tgz#13114ddc9891c1ecd280036244554de43647a226"
-
parse-glob@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"