diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 7af6e631a8..5b1fc54330 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -58,9 +58,13 @@ function createEnv(plugin: string): PluginEnvironment { async function main() { const app = express(); + const corsOptions: cors.CorsOptions = { + origin: 'http://localhost:3000', + credentials: true, + }; app.use(helmet()); - app.use(cors()); + app.use(cors(corsOptions)); app.use(compression()); app.use(express.json()); app.use(requestLoggingHandler()); diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts index ccf99772fc..dbcf53dbe0 100644 --- a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts @@ -40,7 +40,7 @@ type CreateOptions = { export type GoogleAuthResponse = { accessToken: string; idToken: string; - scopes: string; + scope: string; expiresInSeconds: number; }; @@ -70,7 +70,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi { return { idToken: res.idToken, accessToken: res.accessToken, - scopes: GoogleAuth.normalizeScopes(res.scopes), + scopes: GoogleAuth.normalizeScopes(res.scope), expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000), }; }, diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts index 161015b1a8..ca8a9cd2ae 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts @@ -99,7 +99,7 @@ export class DefaultAuthConnector } async refreshSession(): Promise { - const res = await fetch(this.buildUrl('/token', { optional: true }), { + const res = await fetch(this.buildUrl('/refresh', { optional: true }), { headers: { 'x-requested-with': 'XMLHttpRequest', }, diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 6b85bdfe1c..fe82e544e7 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -25,10 +25,16 @@ "morgan": "^1.10.0", "winston": "^3.2.1", "yn": "^4.0.0", - "passport": "0.4.1", - "passport-google-oauth20": "2.0.0", - "@types/passport": "1.0.3", - "@types/passport-google-oauth20": "2.0.3" + "passport": "^0.4.1", + "passport-google-oauth20": "^2.0.0", + "passport-oauth2-refresh": "^2.0.0", + "passport-oauth2": "^1.5.0", + "cookie-parser": "^1.4.5", + "@types/passport-oauth2-refresh": "^1.1.1", + "@types/passport": "^1.0.3", + "@types/passport-google-oauth20": "^2.0.3", + "@types/cookie-parser": "^1.4.2", + "@types/passport-oauth2": "^1.4.9" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index 845c46a4da..b692a35216 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { GoogleAuthProvider } from './provider'; +import { GoogleAuthProvider, THOUSAND_DAYS_MS } from './provider'; import passport from 'passport'; import express from 'express'; import * as utils from './../utils'; +import refresh from 'passport-oauth2-refresh'; const googleAuthProviderConfig = { provider: 'google', @@ -51,7 +52,7 @@ describe('GoogleAuthProvider', () => { }); describe('start authentication handler', () => { - const mockResponse: any = ({} as unknown) as express.Response; + const mockResponse = ({} as unknown) as express.Response; const mockNext: express.NextFunction = jest.fn(); it('should initiate authenticate request with provided scopes', () => { @@ -97,6 +98,7 @@ describe('GoogleAuthProvider', () => { it('should perform logout and respond with 200', () => { const mockResponse: any = ({ send: jest.fn(), + cookie: jest.fn(), } as unknown) as express.Response; const googleAuthProvider = new GoogleAuthProvider( @@ -110,23 +112,68 @@ describe('GoogleAuthProvider', () => { googleAuthProvider.logout(mockRequest, mockResponse); expect(spyResponse).toBeCalledTimes(1); expect(spyResponse).toBeCalledWith('logout!'); + expect(mockResponse.cookie).toBeCalledTimes(1); + expect(mockResponse.cookie).toBeCalledWith( + 'google-refresh-token', + '', + expect.objectContaining({ maxAge: 0 }), + ); }); }); describe('redirect frame handler', () => { const mockRequest = ({} as unknown) as express.Request; const mockResponse: any = ({ - send: jest.fn(), + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + cookie: jest.fn().mockReturnThis(), } as unknown) as express.Response; const mockNext: express.NextFunction = jest.fn(); - it('should call authenticate and post a response ', () => { + it('should call authenticate and post a response', () => { const spyPostMessage = jest .spyOn(utils, 'postMessageResponse') .mockImplementation(() => jest.fn()); const spyPassport = jest .spyOn(passport, 'authenticate') + .mockImplementation((_x, callbackFunc) => { + const cb = callbackFunc as Function; + cb(null, { refreshToken: 'REFRESH_TOKEN' }); + return jest.fn(); + }); + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); + expect(spyPassport).toBeCalledTimes(1); + expect(spyPostMessage).toBeCalledTimes(1); + expect(mockResponse.cookie).toBeCalledTimes(1); + expect(mockResponse.cookie).toBeCalledWith( + 'google-refresh-token', + 'REFRESH_TOKEN', + expect.objectContaining({ + path: '/auth/google', + sameSite: 'none', + httpOnly: true, + maxAge: THOUSAND_DAYS_MS, + }), + ); + }); + + it('should respond with a error message if no refresh token returned', () => { + const spyPassport = jest + .spyOn(passport, 'authenticate') + .mockImplementation((_x, callbackFunc) => { + const cb = callbackFunc as Function; + cb(null, {}); + return jest.fn(); + }); + + const spyPostMessage = jest + .spyOn(utils, 'postMessageResponse') .mockImplementation(() => jest.fn()); const googleAuthProvider = new GoogleAuthProvider( @@ -134,10 +181,38 @@ describe('GoogleAuthProvider', () => { ); googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - const callbackFunc = spyPassport.mock.calls[0][1] as Function; - callbackFunc(); expect(spyPassport).toBeCalledTimes(1); expect(spyPostMessage).toBeCalledTimes(1); + expect(spyPostMessage).toBeCalledWith(mockResponse, { + type: 'auth-result', + error: new Error('Missing refresh token'), + }); + }); + + it('should respond with a error message if auth failed', () => { + const spyPassport = jest + .spyOn(passport, 'authenticate') + .mockImplementation((_x, callbackFunc) => { + const cb = callbackFunc as Function; + cb(new Error('TokenError'), null); + return jest.fn(); + }); + + const spyPostMessage = jest + .spyOn(utils, 'postMessageResponse') + .mockImplementation(() => jest.fn()); + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); + expect(spyPassport).toBeCalledTimes(1); + expect(spyPostMessage).toBeCalledTimes(1); + expect(spyPostMessage).toBeCalledWith(mockResponse, { + type: 'auth-result', + error: new Error('Google auth failed, Error: TokenError'), + }); }); }); @@ -159,4 +234,159 @@ describe('GoogleAuthProvider', () => { }).toThrow(); }); }); + + describe('refresh token handler', () => { + const mockResponse = ({ + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + } as unknown) as express.Response; + + describe('no refresh token cookie', () => { + it('should respond with a 401', () => { + const mockRequest = ({ + cookies: jest.fn(), + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith('Missing session cookie'); + + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + }); + }); + + describe('refresh token cookie, no scope', () => { + const mockRequest = ({ + cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, + query: {}, + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + it('should request for a new access token and fail if no access token returned', () => { + const spyRefresh = jest + .spyOn(refresh, 'requestNewAccessToken') + .mockImplementation((_x, _y, _z, callbackFunc) => { + const cb = callbackFunc as Function; + cb(undefined, undefined, undefined, {}); + }); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(spyRefresh).toBeCalledTimes(1); + expect(spyRefresh).toBeCalledWith( + 'google', + 'REFRESH_TOKEN', + {}, + expect.any(Function), + ); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith( + 'Failed to refresh access token', + ); + }); + + it('should request for a new access token and return 401 if any error', () => { + const spyRefresh = jest + .spyOn(refresh, 'requestNewAccessToken') + .mockImplementation((_x, _y, _z, callbackFunc) => { + const cb = callbackFunc as Function; + cb({ error: 'ERROR' }, undefined, undefined, {}); + }); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(spyRefresh).toBeCalledTimes(1); + expect(spyRefresh).toBeCalledWith( + 'google', + 'REFRESH_TOKEN', + {}, + expect.any(Function), + ); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith( + 'Failed to refresh access token', + ); + }); + + it('should fetch and return a new access token', () => { + const spyRefresh = jest + .spyOn(refresh, 'requestNewAccessToken') + .mockImplementation((_x, _y, _z, callbackFunc) => { + const cb = callbackFunc as Function; + cb(undefined, 'ACCESS_TOKEN', undefined, { + expires_in: 'EXPIRES_IN', + id_token: 'ID_TOKEN', + }); + }); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(spyRefresh).toBeCalledTimes(1); + expect(spyRefresh).toBeCalledWith( + 'google', + 'REFRESH_TOKEN', + {}, + expect.any(Function), + ); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith({ + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 'EXPIRES_IN', + scope: undefined, + }); + }); + }); + + describe('refresh token cookie and scope', () => { + const mockRequest = ({ + cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, + query: { + scope: 'a,b', + }, + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + it('should fetch and return a new access token with scopes', () => { + const spyRefresh = jest + .spyOn(refresh, 'requestNewAccessToken') + .mockImplementation((_x, _y, _z, callbackFunc) => { + const cb = callbackFunc as Function; + cb(undefined, 'ACCESS_TOKEN', undefined, { + expires_in: 'EXPIRES_IN', + id_token: 'ID_TOKEN', + scope: 'a,b', + }); + }); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(spyRefresh).toBeCalledTimes(1); + expect(spyRefresh).toBeCalledWith( + 'google', + 'REFRESH_TOKEN', + { scope: 'a,b' }, + expect.any(Function), + ); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith({ + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 'EXPIRES_IN', + scope: 'a,b', + }); + }); + }); + }); }); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 0ef7e19d3e..096260e044 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -15,8 +15,9 @@ */ import passport from 'passport'; -import express from 'express'; +import express, { CookieOptions } from 'express'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import refresh from 'passport-oauth2-refresh'; import { AuthProvider, AuthProviderRouteHandlers, @@ -25,6 +26,7 @@ import { import { postMessageResponse } from './../utils'; import { InputError } from '@backstage/backend-common'; +export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export class GoogleAuthProvider implements AuthProvider, AuthProviderRouteHandlers { private readonly providerConfig: AuthProviderConfig; @@ -53,8 +55,40 @@ export class GoogleAuthProvider res: express.Response, next: express.NextFunction, ) { - return passport.authenticate('google', (_, user) => { - postMessageResponse(res, { + return passport.authenticate('google', (err, user) => { + if (err) { + return postMessageResponse(res, { + type: 'auth-result', + error: new Error(`Google auth failed, ${err}`), + }); + } + + const { refreshToken } = user; + + if (!refreshToken) { + return postMessageResponse(res, { + type: 'auth-result', + error: new Error('Missing refresh token'), + }); + } + + delete user.refreshToken; + + const options: CookieOptions = { + maxAge: THOUSAND_DAYS_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${this.providerConfig.provider}`, + httpOnly: true, + }; + + res.cookie( + `${this.providerConfig.provider}-refresh-token`, + refreshToken, + options, + ); + return postMessageResponse(res, { type: 'auth-result', payload: user, }); @@ -62,7 +96,46 @@ export class GoogleAuthProvider } async logout(_req: express.Request, res: express.Response) { - res.send('logout!'); + const options: CookieOptions = { + maxAge: 0, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${this.providerConfig.provider}`, + httpOnly: true, + }; + + res.cookie(`${this.providerConfig.provider}-refresh-token`, '', options); + return res.send('logout!'); + } + + async refresh(req: express.Request, res: express.Response) { + const refreshToken = + req.cookies[`${this.providerConfig.provider}-refresh-token`]; + + if (!refreshToken) { + return res.status(401).send('Missing session cookie'); + } + + const scope = req.query.scope?.toString() ?? ''; + const refreshTokenRequestParams = scope ? { scope } : {}; + + return refresh.requestNewAccessToken( + this.providerConfig.provider, + refreshToken, + refreshTokenRequestParams, + (err, accessToken, _refreshToken, params) => { + if (err || !accessToken) { + return res.status(401).send('Failed to refresh access token'); + } + return res.send({ + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }); + }, + ); } strategy(): passport.Strategy { @@ -81,6 +154,8 @@ export class GoogleAuthProvider idToken: params.id_token, accessToken, refreshToken, + scope: params.scope, + expiresInSeconds: params.expires_in, }); }, ); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 6658299033..68fe61c7a6 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -24,7 +24,7 @@ export const defaultRouter = (provider: AuthProviderRouteHandlers) => { router.get('/handler/frame', provider.frameHandler); router.get('/logout', provider.logout); if (provider.refresh) { - router.get('/refreshToken', provider.refresh); + router.get('/refresh', provider.refresh); } return router; }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 4350f36200..36941c6850 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -58,16 +58,25 @@ export type AuthProviderFactory = { new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers; }; -export type AuthInfo = { - profile: passport.Profile; +export type AuthInfoBase = { accessToken: string; + idToken?: string; expiresInSeconds?: number; + scope: string; +}; + +export type AuthInfoWithProfile = AuthInfoBase & { + profile: passport.Profile; +}; + +export type AuthInfoPrivate = AuthInfoWithProfile & { + refreshToken: string; }; export type AuthResponse = | { type: 'auth-result'; - payload: AuthInfo; + payload: AuthInfoBase | AuthInfoWithProfile; } | { type: 'auth-result'; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index cdde95d9d0..a2e3b3e1db 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -17,6 +17,9 @@ import express from 'express'; import Router from 'express-promise-router'; import passport from 'passport'; +import cookieParser from 'cookie-parser'; +import refresh from 'passport-oauth2-refresh'; +import OAuth2Strategy from 'passport-oauth2'; import { Logger } from 'winston'; import { providers } from './../providers/config'; import { makeProvider } from '../providers'; @@ -39,6 +42,9 @@ export async function createRouter( ); logger.info(`Configuring provider: ${providerId}`); passport.use(strategy); + if (strategy instanceof OAuth2Strategy) { + refresh.use(strategy); + } providerRouters[providerId] = providerRouter; } @@ -52,6 +58,7 @@ export async function createRouter( router.use(passport.initialize()); router.use(passport.session()); + router.use(cookieParser()); for (const providerId in providerRouters) { if (providerRouters.hasOwnProperty(providerId)) { diff --git a/yarn.lock b/yarn.lock index 3115a4ccfe..c23e7a42a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3861,6 +3861,13 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.2": + version "1.4.2" + resolved "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5" + integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg== + dependencies: + "@types/express" "*" + "@types/cookiejar@*": version "2.1.1" resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" @@ -4181,7 +4188,7 @@ resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/passport-google-oauth20@2.0.3": +"@types/passport-google-oauth20@^2.0.3": version "2.0.3" resolved "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.3.tgz#f554ff6d39f395acff3f1d762e54462194dac8da" integrity sha512-6EUEGzEg4acwowvgR/yVZIj8S2Kkwc6JmlY2/wnM1wJHNz20o7s1TIGrxnah8ymLgJasYDpy95P3TMMqlmetPw== @@ -4190,7 +4197,15 @@ "@types/passport" "*" "@types/passport-oauth2" "*" -"@types/passport-oauth2@*": +"@types/passport-oauth2-refresh@^1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@types/passport-oauth2-refresh/-/passport-oauth2-refresh-1.1.1.tgz#cbe466d4fcac36182fd75bf55279c0b1e953c382" + integrity sha512-Tw0JvfDPv9asgFPACd9oOGCaD/0/Uyi+QF7fmrJC74cJKC6I8N8wwhJJHyfd1N2E/qaLgTh431lhOa9jicpNdg== + dependencies: + "@types/oauth" "*" + "@types/passport-oauth2" "*" + +"@types/passport-oauth2@*", "@types/passport-oauth2@^1.4.9": version "1.4.9" resolved "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.9.tgz#134007c4b505a82548c9cb19094c5baeb2205c92" integrity sha512-QP0q+NVQOaIu2r0e10QWkiUA0Ya5mOBHRJN0UrI+LolMLOP1/VN4EVIpJ3xVwFo+xqNFRoFvFwJhBvKnk7kpUA== @@ -4199,7 +4214,7 @@ "@types/oauth" "*" "@types/passport" "*" -"@types/passport@*", "@types/passport@1.0.3": +"@types/passport@*", "@types/passport@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.3.tgz#e459ed6c262bf0686684d1b05901be0d0b192a9c" integrity sha512-nyztuxtDPQv9utCzU0qW7Gl8BY2Dn8BKlYAFFyxKipFxjaVd96celbkLCV/tRqqBUZ+JB8If3UfgV8347DTo3Q== @@ -7241,6 +7256,14 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, dependencies: safe-buffer "~5.1.1" +cookie-parser@^1.4.5: + version "1.4.5" + resolved "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49" + integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -15987,14 +16010,19 @@ pascalcase@^0.1.1: resolved "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= -passport-google-oauth20@2.0.0: +passport-google-oauth20@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" integrity sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ== dependencies: passport-oauth2 "1.x.x" -passport-oauth2@1.x.x: +passport-oauth2-refresh@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/passport-oauth2-refresh/-/passport-oauth2-refresh-2.0.0.tgz#7b19c77ff3cc000819c69f6ad9e318450f57b85e" + integrity sha512-yXvCB6nem/O+WThhiyI3TlPXpzSGY+9+hy9OTx9QF8e9GInplyRHxHaaOhFylKvnof9UmWHAufQFZk8cO1Fb2g== + +passport-oauth2@1.x.x, passport-oauth2@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== @@ -16010,7 +16038,7 @@ passport-strategy@1.x.x: resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= -passport@0.4.1: +passport@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270" integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==