From 95f9bac2548cb0fd93f66122b63ade0e8c3c2d42 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 5 May 2017 15:42:34 -0600 Subject: [PATCH] Initial migration to JWT --- app.js | 37 +----- middleware/authentication.js | 19 +++ package.json | 21 ++- .../server/router.js | 2 +- routes/api/auth/index.js | 14 +- services/passport.js | 122 ++++++++++++------ services/session.js | 36 ------ services/subscriptions.js | 10 +- views/admin.ejs | 1 - views/auth-callback.ejs | 5 +- views/embed/stream.ejs | 1 - yarn.lock | 82 ++---------- 12 files changed, 138 insertions(+), 212 deletions(-) create mode 100644 middleware/authentication.js delete mode 100644 services/session.js diff --git a/app.js b/app.js index a0a5b7789..f5243f81a 100644 --- a/app.js +++ b/app.js @@ -3,12 +3,11 @@ const bodyParser = require('body-parser'); const morgan = require('morgan'); const path = require('path'); const helmet = require('helmet'); +const authentication = require('./middleware/authentication'); const {passport} = require('./services/passport'); const plugins = require('./services/plugins'); const enabled = require('debug').enabled; -const csrf = require('csurf'); const errors = require('./errors'); -const session = require('./services/session'); const {createGraphOptions} = require('./graph'); const apollo = require('graphql-server-express'); @@ -37,12 +36,6 @@ app.use('/public', express.static(path.join(__dirname, 'public'))); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); -//============================================================================== -// SESSION MIDDLEWARE -//============================================================================== - -app.use(session); - //============================================================================== // PASSPORT MIDDLEWARE //============================================================================== @@ -60,7 +53,10 @@ plugins.get('server', 'passport').forEach((plugin) => { // Setup the PassportJS Middleware. app.use(passport.initialize()); -app.use(passport.session()); + +// Attach the authentication middleware, this will be responsible for decoding +// (if present) the JWT on the request. +app.use('/api', authentication); //============================================================================== // GraphQL Router @@ -84,29 +80,6 @@ if (app.get('env') !== 'production') { } -//============================================================================== -// CSRF MIDDLEWARE -//============================================================================== - -if (process.env.TEST_MODE === 'unit') { - - // Add this fake test token in the event we are in unit test mode, and don't - // include the CSRF protection. - app.locals.csrfToken = 'UNIT_TESTS'; - -} else { - - // Setup route middlewares for CSRF protection. - // Default ignore methods are GET, HEAD, OPTIONS - app.use(csrf({})); - app.use((req, res, next) => { - res.locals.csrfToken = req.csrfToken(); - - next(); - }); - -} - //============================================================================== // ROUTES //============================================================================== diff --git a/middleware/authentication.js b/middleware/authentication.js new file mode 100644 index 000000000..ca6949a5c --- /dev/null +++ b/middleware/authentication.js @@ -0,0 +1,19 @@ +const {passport} = require('../services/passport'); + +const authentication = (req, res, next) => passport.authenticate('jwt', { + session: false +}, (err, user) => { + if (err) { + return next(err); + } + + if (user) { + + // Attach the user to the request object, now that we know it exists. + req.user = user; + } + + next(); +})(req, res, next); + +module.exports = authentication; diff --git a/package.json b/package.json index bfca21643..f5ab201ef 100644 --- a/package.json +++ b/package.json @@ -58,14 +58,12 @@ "commander": "^2.9.0", "connect-redis": "^3.1.0", "cross-spawn": "^5.1.0", - "csurf": "^1.9.0", "dataloader": "^1.3.0", "debug": "^2.6.3", "dotenv": "^4.0.0", "ejs": "^2.5.6", "env-rewrite": "^1.0.2", "express": "^4.15.2", - "express-session": "^1.15.1", "form-data": "^2.1.2", "gql-merge": "^0.0.4", "graphql": "^0.9.1", @@ -77,11 +75,10 @@ "helmet": "^3.5.0", "inquirer": "^3.0.6", "joi": "^10.4.1", - "jsonwebtoken": "^7.3.0", + "jsonwebtoken": "^7.4.0", "kue": "^0.11.5", "linkify-it": "^2.0.3", "lodash": "^4.16.6", - "marked": "^0.3.6", "metascraper": "^1.0.6", "minimist": "^1.2.0", "mongoose": "^4.9.1", @@ -92,18 +89,12 @@ "nodemailer": "^2.6.4", "parse-duration": "^0.1.1", "passport": "^0.3.2", + "passport-jwt": "^2.2.1", "passport-local": "^1.0.0", - "prop-types": "^15.5.8", - "react-apollo": "^1.1.0", - "react-recaptcha": "^2.2.6", - "recompose": "^0.23.1", "redis": "^2.7.1", - "uuid": "^3.0.1", - "simplemde": "^1.11.2", - "subscriptions-transport-ws": "^0.5.5-alpha.0", "resolve": "^1.3.2", "semver": "^5.3.0", - "simplemde": "^1.11.2", + "subscriptions-transport-ws": "^0.5.5-alpha.0", "uuid": "^3.0.1" }, "devDependencies": { @@ -157,6 +148,7 @@ "keymaster": "^1.6.2", "license-webpack-plugin": "^0.4.2", "material-design-lite": "^1.2.1", + "marked": "^0.3.6", "mocha": "^3.1.2", "mocha-junit-reporter": "^1.12.1", "nightwatch": "^0.9.11", @@ -178,11 +170,16 @@ "react-redux": "^4.4.5", "react-router": "^3.0.0", "react-tagsinput": "^3.14.0", + "prop-types": "^15.5.8", + "react-apollo": "^1.1.0", + "react-recaptcha": "^2.2.6", + "recompose": "^0.23.1", "redux": "^3.6.0", "redux-mock-store": "^1.2.1", "redux-thunk": "^2.1.0", "regenerator": "^0.8.46", "selenium-standalone": "^5.11.2", + "simplemde": "^1.11.2", "style-loader": "^0.16.0", "subscriptions-transport-ws": "^0.5.5-alpha.0", "supertest": "^2.0.1", diff --git a/plugins/coral-plugin-facebook-auth/server/router.js b/plugins/coral-plugin-facebook-auth/server/router.js index 7ef7b0ed2..56fcd2ab2 100644 --- a/plugins/coral-plugin-facebook-auth/server/router.js +++ b/plugins/coral-plugin-facebook-auth/server/router.js @@ -15,6 +15,6 @@ module.exports = (router) => { router.get('/api/v1/auth/facebook/callback', (req, res, next) => { // Perform the facebook login flow and pass the data back through the opener. - passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next); + passport.authenticate('facebook', {session: false}, HandleAuthPopupCallback(req, res, next))(req, res, next); }); }; diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index 4926192de..54932ac03 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -1,6 +1,5 @@ const express = require('express'); -const {passport, HandleAuthCallback} = require('../../../services/passport'); -const authorization = require('../../../middleware/authorization'); +const {passport, HandleGenerateCredentials} = require('../../../services/passport'); const router = express.Router(); @@ -20,15 +19,6 @@ router.get('/', (req, res, next) => { res.json({user: req.user}); }); -/** - * This destroys the session of a user, if they have one. - */ -router.delete('/', authorization.needed(), (req, res) => { - delete req.session.passport; - - res.status(204).end(); -}); - //============================================================================== // PASSPORT ROUTES //============================================================================== @@ -39,7 +29,7 @@ router.delete('/', authorization.needed(), (req, res) => { router.post('/local', (req, res, next) => { // Perform the local authentication. - passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next); + passport.authenticate('local', {session: false}, HandleGenerateCredentials(req, res, next))(req, res, next); }); module.exports = router; diff --git a/services/passport.js b/services/passport.js index 682d86ce1..a455fa745 100644 --- a/services/passport.js +++ b/services/passport.js @@ -3,33 +3,43 @@ const UsersService = require('./users'); const SettingsService = require('./settings'); const fetch = require('node-fetch'); const FormData = require('form-data'); +const JWT = require('jsonwebtoken'); const LocalStrategy = require('passport-local').Strategy; const errors = require('../errors'); +const uuid = require('uuid'); const debug = require('debug')('talk:passport'); -//============================================================================== -// SESSION SERIALIZATION -//============================================================================== +// JWT_SECRET is the secret used to sign and verify tokens issued by this +// application. +const JWT_SECRET = process.env.JWT_SECRET; -passport.serializeUser((user, done) => { - done(null, user.id); +// JWT_EXPIRY is the time for which a given token is valid for. +const JWT_EXPIRY = process.env.JWT_EXPIRY || '1 day'; + +// 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. +const JWT_ISSUER = process.env.JWT_ISSUER || process.env.TALK_ROOT_URL || undefined; + +// 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`. +const JWT_AUDIENCE = process.env.JWT_AUDIENCE || 'talk'; + +// GenerateToken will sign a token to include all the authorization information +// needed for the front end. +const GenerateToken = (user) => JWT.sign({}, JWT_SECRET, { + jwtid: uuid.v4(), + expiresIn: JWT_EXPIRY, + issuer: JWT_ISSUER, + subject: user.id, + audience: JWT_AUDIENCE }); -passport.deserializeUser((id, done) => { - UsersService - .findById(id) - .then((user) => { - done(null, user); - }) - .catch((err) => { - done(err); - }); -}); - -/** - * This sends back the user data as JSON. - */ -const HandleAuthCallback = (req, res, next) => (err, user) => { +// HandleGenerateCredentials validates that an authentication scheme did indeed +// return a user, if it did, then sign and return the user and token to be used +// by the frontend to display and update the UI. +const HandleGenerateCredentials = (req, res, next) => (err, user) => { if (err) { return next(err); } @@ -38,15 +48,11 @@ const HandleAuthCallback = (req, res, next) => (err, user) => { return next(errors.ErrNotAuthorized); } - // Perform the login of the user! - req.logIn(user, (err) => { - if (err) { - return next(err); - } + // Generate the token to re-issue to the frontend. + const token = GenerateToken(user); - // We logged in the user! Let's send back the user data and the CSRF token. - res.json({user}); - }); + // Send back the details! + res.json({user, token}); }; /** @@ -54,22 +60,18 @@ const HandleAuthCallback = (req, res, next) => (err, user) => { */ const HandleAuthPopupCallback = (req, res, next) => (err, user) => { if (err) { - return res.render('auth-callback', {err: JSON.stringify(err), data: null}); + return res.render('auth-callback', {auth: JSON.stringify({err, data: null})}); } if (!user) { - return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null}); + return res.render('auth-callback', {auth: JSON.stringify({err, data: null})}); } - // Perform the login of the user! - req.logIn(user, (err) => { - if (err) { - return res.render('auth-callback', {err: JSON.stringify(err), data: null}); - } + // Generate the token to re-issue to the frontend. + const token = GenerateToken(user); - // We logged in the user! Let's send back the user data. - res.render('auth-callback', {err: null, data: JSON.stringify(user)}); - }); + // We logged in the user! Let's send back the user data. + res.render('auth-callback', {auth: JSON.stringify({err: null, data: {user, token}})}); }; /** @@ -119,7 +121,45 @@ function ValidateUserLogin(loginProfile, user, done) { } //============================================================================== -// STRATEGIES +// JWT STRATEGY +//============================================================================== + +const JwtStrategy = require('passport-jwt').Strategy; +const ExtractJwt = require('passport-jwt').ExtractJwt; + +// Extract the JWT from the 'Authorization' header with the 'Bearer' scheme. +passport.use(new JwtStrategy({ + + // Prepare the extractor from the header. + jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'), + + // Use the secret passed in which is loaded from the environment. This can be + // a certificate (loaded) or a HMAC key. + secretOrKey: JWT_SECRET, + + // Verify the issuer. + issuer: JWT_ISSUER, + + // Verify the audience. + audience: JWT_AUDIENCE, + + // Enable only the HS256 algorithm. + algorithms: ['HS256'] +}, async (jwt, done) => { + + // Load the user from the environment, because we just got a user from the + // header. + try { + let user = await UsersService.findById(jwt.sub); + + return done(null, user); + } catch(e) { + return done(e); + } +})); + +//============================================================================== +// LOCAL STRATEGY //============================================================================== /** @@ -356,6 +396,6 @@ module.exports = { passport, ValidateUserLogin, HandleFailedAttempt, - HandleAuthCallback, - HandleAuthPopupCallback + HandleAuthPopupCallback, + HandleGenerateCredentials }; diff --git a/services/session.js b/services/session.js deleted file mode 100644 index 505dfdf4a..000000000 --- a/services/session.js +++ /dev/null @@ -1,36 +0,0 @@ -const session = require('express-session'); -const RedisStore = require('connect-redis')(session); -const redis = require('./redis'); - -//============================================================================== -// SESSION MIDDLEWARE -//============================================================================== - -const session_opts = { - secret: process.env.TALK_SESSION_SECRET, - httpOnly: true, - rolling: true, - saveUninitialized: true, - resave: true, - unset: 'destroy', - name: 'talk.sid', - cookie: { - secure: false, - maxAge: 8.64e+7, // 24 hours for session token expiry - }, - store: new RedisStore({ - client: redis.createClient(), - }) -}; - -if (process.env.NODE_ENV === 'production') { - - // Enable the secure cookie when we are in production mode. - session_opts.cookie.secure = true; -} else if (process.env.NODE_ENV === 'test') { - - // Add in the secret during tests. - session_opts.secret = 'keyboard cat'; -} - -module.exports = session(session_opts); diff --git a/services/subscriptions.js b/services/subscriptions.js index 8c34507f2..c4cd2378a 100644 --- a/services/subscriptions.js +++ b/services/subscriptions.js @@ -1,12 +1,18 @@ -const session = require('./session'); const passport = require('./passport'); +const authentication = require('../middleware/authentication'); // Session data does not automatically attach to websocket req objects. // This middleware code looks for a user in the session and, if it exists, // attaches it to the graph req. const deserializeUser = (req) => { return new Promise((resolve, reject) => { - session(req, {}, () => { + + // This uses the authentication connect middleware to establish the session + // user details from the headers. + authentication(req, null, (err) => { + if (err) { + return reject(err); + } if ('session' in req && 'passport' in req.session && 'user' in req.session.passport) { passport.deserializeUser(req.session.passport.user, (err, user) => { diff --git a/views/admin.ejs b/views/admin.ejs index 947f15425..946284622 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -3,7 +3,6 @@ - Talk - Coral Admin diff --git a/views/auth-callback.ejs b/views/auth-callback.ejs index d38d759ee..a97884ae2 100644 --- a/views/auth-callback.ejs +++ b/views/auth-callback.ejs @@ -2,7 +2,10 @@ diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs index d99d4bce1..03897d85f 100644 --- a/views/embed/stream.ejs +++ b/views/embed/stream.ejs @@ -3,7 +3,6 @@ - diff --git a/yarn.lock b/yarn.lock index 99703bdf3..4663f70fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1957,10 +1957,6 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: os-homedir "^1.0.1" require-from-string "^1.1.0" -crc@3.4.4: - version "3.4.4" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.4.tgz#9da1e980e3bd44fc5c93bf5ab3da3378d85e466b" - create-ecdh@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" @@ -2024,14 +2020,6 @@ crypto-browserify@^3.11.0: public-encrypt "^4.0.0" randombytes "^2.0.0" -csrf@~3.0.3: - version "3.0.6" - resolved "https://registry.yarnpkg.com/csrf/-/csrf-3.0.6.tgz#b61120ddceeafc91e76ed5313bb5c0b2667b710a" - dependencies: - rndm "1.2.0" - tsscmp "1.0.5" - uid-safe "2.1.4" - css-color-function@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.0.tgz#72c767baf978f01b8a8a94f42f17ba5d22a776fc" @@ -2164,15 +2152,6 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0": dependencies: cssom "0.3.x" -csurf@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/csurf/-/csurf-1.9.0.tgz#49d2c6925ffcec7b7de559597c153fa533364133" - dependencies: - cookie "0.3.1" - cookie-signature "1.0.6" - csrf "~3.0.3" - http-errors "~1.5.0" - cz-conventional-changelog@1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-1.1.5.tgz#0a4d1550c4e2fb6a3aed8f6cd858c21760e119b8" @@ -2256,12 +2235,6 @@ debug@2.6.1: dependencies: ms "0.7.2" -debug@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" - dependencies: - ms "0.7.2" - debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" @@ -2919,20 +2892,6 @@ exports-loader@^0.6.4: loader-utils "^1.0.2" source-map "0.5.x" -express-session@^1.15.1: - version "1.15.2" - resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.15.2.tgz#d98516443a4ccb8688e1725ae584c02daa4093d4" - dependencies: - cookie "0.3.1" - cookie-signature "1.0.6" - crc "3.4.4" - debug "2.6.3" - depd "~1.1.0" - on-headers "~1.0.1" - parseurl "~1.3.1" - uid-safe "~2.1.4" - utils-merge "1.0.0" - express@^4.12.2, express@^4.15.2: version "4.15.2" resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" @@ -3807,14 +3766,6 @@ htmlparser2@~3.8.1: entities "1.0" readable-stream "1.1" -http-errors@~1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" - dependencies: - inherits "2.0.3" - setprototypeof "1.0.2" - statuses ">= 1.3.1 < 2" - http-errors@~1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" @@ -4547,7 +4498,7 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" -jsonwebtoken@^7.3.0: +jsonwebtoken@^7.0.0, jsonwebtoken@^7.4.0: version "7.4.0" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.0.tgz#515bf2bba070ec615bad97fd2e945027eb476946" dependencies: @@ -5817,13 +5768,20 @@ parseurl@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" +passport-jwt@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-2.2.1.tgz#0e004c94071319d673d9d9bcfd1574a868011527" + dependencies: + jsonwebtoken "^7.0.0" + passport-strategy "^1.0.0" + passport-local@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" dependencies: passport-strategy "1.x.x" -passport-strategy@1.x.x: +passport-strategy@1.x.x, passport-strategy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" @@ -6678,10 +6636,6 @@ ramda@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.23.0.tgz#ccd13fff73497a93974e3e86327bfd87bd6e8e2b" -random-bytes@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" - randomatic@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" @@ -7247,10 +7201,6 @@ ripemd160@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" -rndm@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/rndm/-/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c" - run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -7364,10 +7314,6 @@ setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" -setprototypeof@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" - setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" @@ -7987,10 +7933,6 @@ tryor@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" -tsscmp@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97" - tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -8063,12 +8005,6 @@ uid-number@~0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" -uid-safe@2.1.4, uid-safe@~2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81" - dependencies: - random-bytes "~1.0.0" - ultron@1.0.x: version "1.0.2" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"