diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/providers/OAuthProvider.ts index d02f7da110..81f5ed25a6 100644 --- a/plugins/auth-backend/src/providers/OAuthProvider.ts +++ b/plugins/auth-backend/src/providers/OAuthProvider.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import express, { CookieOptions } from 'express'; import crypto from 'crypto'; import { AuthProviderRouteHandlers, OAuthProviderHandlers } from './types'; @@ -7,6 +23,69 @@ import { postMessageResponse, ensuresXRequestedWith } from './utils'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; +export const verifyNonce = (req: express.Request, provider: string) => { + const cookieNonce = req.cookies[`${provider}-nonce`]; + const stateNonce = req.query.state; + + if (!cookieNonce || !stateNonce) { + throw new Error('Missing nonce'); + } + + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; + +export const setNonceCookie = (res: express.Response, provider: string) => { + const nonce = crypto.randomBytes(16).toString('base64'); + + const options: CookieOptions = { + maxAge: TEN_MINUTES_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}/handler`, + httpOnly: true, + }; + + res.cookie(`${provider}-nonce`, nonce, options); + + return nonce; +}; + +export const setRefreshTokenCookie = ( + res: express.Response, + provider: string, + refreshToken: string, +) => { + const options: CookieOptions = { + maxAge: THOUSAND_DAYS_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, refreshToken, options); +}; + +export const removeRefreshTokenCookie = ( + res: express.Response, + provider: string, +) => { + const options: CookieOptions = { + maxAge: 0, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, '', options); +}; + export class OAuthProvider implements AuthProviderRouteHandlers { private readonly provider: string; private readonly providerHandlers: OAuthProviderHandlers; @@ -106,72 +185,9 @@ export class OAuthProvider implements AuthProviderRouteHandlers { refreshToken, scope, ); - res.send(refreshInfo); + return res.send(refreshInfo); } catch (error) { - res.status(401).send(`${error.message}`); + return res.status(401).send(`${error.message}`); } } } - -export const verifyNonce = (req: express.Request, provider: string) => { - const cookieNonce = req.cookies[`${provider}-nonce`]; - const stateNonce = req.query.state; - - if (!cookieNonce || !stateNonce) { - throw new Error('Missing nonce'); - } - - if (cookieNonce !== stateNonce) { - throw new Error('Invalid nonce'); - } -}; - -export const setNonceCookie = (res: express.Response, provider: string) => { - const nonce = crypto.randomBytes(16).toString('base64'); - - const options: CookieOptions = { - maxAge: TEN_MINUTES_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}/handler`, - httpOnly: true, - }; - - res.cookie(`${provider}-nonce`, nonce, options); - - return nonce; -}; - -export const setRefreshTokenCookie = ( - res: express.Response, - provider: string, - refreshToken: string, -) => { - const options: CookieOptions = { - maxAge: THOUSAND_DAYS_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}`, - httpOnly: true, - }; - - res.cookie(`${provider}-refresh-token`, refreshToken, options); -}; - -export const removeRefreshTokenCookie = ( - res: express.Response, - provider: string, -) => { - const options: CookieOptions = { - maxAge: 0, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}`, - httpOnly: true, - }; - - res.cookie(`${provider}-refresh-token`, '', options); -}; diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts index 515a91f25c..3ec8539330 100644 --- a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts @@ -1,6 +1,22 @@ -import express, { CookieOptions } from 'express'; +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; import passport from 'passport'; -import { RedirectInfo, AuthInfoBase } from './types'; +import { RedirectInfo, RefreshTokenResponse } from './types'; export const executeRedirectStrategy = async ( req: express.Request, @@ -28,7 +44,7 @@ export const executeFrameHandlerStrategy = async ( }; strategy.fail = ( info: { type: 'success' | 'error'; message?: string }, - _status?: number, + // _status: number, ) => { reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); }; @@ -47,7 +63,7 @@ export const executeRefreshTokenStrategy = async ( providerstrategy: passport.Strategy, refreshToken: string, scope: string, -): Promise => { +): Promise => { return new Promise((resolve, reject) => { const anyStrategy = providerstrategy as any; const OAuth2 = anyStrategy._oauth2.constructor; @@ -84,9 +100,7 @@ export const executeRefreshTokenStrategy = async ( } resolve({ accessToken, - idToken: params.id_token, - expiresInSeconds: 10, - scope: params.scope, + params, }); }, ); diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index 5a94d88ada..0ec98bef89 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -1 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + export { GoogleAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index bfa6ac7a81..90d33652a5 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import express from 'express'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { @@ -14,12 +30,10 @@ import { } from '../types'; export class GoogleAuthProvider implements OAuthProviderHandlers { - private readonly provider: string; private readonly providerConfig: AuthProviderConfig; private readonly _strategy: GoogleStrategy; constructor(providerConfig: AuthProviderConfig) { - this.provider = providerConfig.provider; this.providerConfig = providerConfig; // TODO: throw error if env variables not set? this._strategy = new GoogleStrategy( @@ -38,7 +52,7 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { idToken: params.id_token, accessToken, scope: params.scope, - expiresInSeconds: 10, + expiresInSeconds: params.expires_in, }, { refreshToken, @@ -59,10 +73,17 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { } async refresh(refreshToken: string, scope: string): Promise { - return await executeRefreshTokenStrategy( + const { accessToken, params } = await executeRefreshTokenStrategy( this._strategy, refreshToken, scope, ); + + return { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }; } } diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 6c4bf82481..dcbe6ad1de 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -73,3 +73,8 @@ export type RedirectInfo = { url: string; status?: number; }; + +export type RefreshTokenResponse = { + accessToken: string; + params: any; +}; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 03d197f50b..49487d551a 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -36,6 +36,7 @@ export async function createRouter( // configure all the providers for (const providerConfig of providers) { const { providerId, providerRouter } = makeProvider(providerConfig); + logger.info(`Configuring provider, ${providerId}`); router.use(`/${providerId}`, providerRouter); }