From ca7b66d0a1011fb7952a13992d47c63c8028778d Mon Sep 17 00:00:00 2001 From: Ryan Manny Date: Wed, 10 Feb 2021 10:29:15 -0800 Subject: [PATCH] Don't pass default as a scope to OIDC providers --- packages/core/src/api-wrappers/defaultApis.ts | 445 +++++++++--------- .../src/providers/oidc/provider.ts | 402 ++++++++-------- 2 files changed, 426 insertions(+), 421 deletions(-) diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index d044b50b7a..ee3406688e 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -1,220 +1,225 @@ -/* - * 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 { - alertApiRef, - errorApiRef, - AlertApiForwarder, - ErrorApiForwarder, - ErrorAlerter, - discoveryApiRef, - GoogleAuth, - GithubAuth, - OAuth2, - OktaAuth, - GitlabAuth, - Auth0Auth, - MicrosoftAuth, - oauthRequestApiRef, - OAuthRequestManager, - googleAuthApiRef, - githubAuthApiRef, - oauth2ApiRef, - oktaAuthApiRef, - gitlabAuthApiRef, - auth0AuthApiRef, - microsoftAuthApiRef, - storageApiRef, - WebStorage, - createApiFactory, - configApiRef, - UrlPatternDiscovery, - samlAuthApiRef, - SamlAuth, - oneloginAuthApiRef, - OneLoginAuth, - oidcAuthApiRef, -} from '@backstage/core-api'; - -import OAuth2Icon from '@material-ui/icons/AcUnit'; - -export const defaultApis = [ - createApiFactory({ - api: discoveryApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => - UrlPatternDiscovery.compile( - `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, - ), - }), - createApiFactory(alertApiRef, new AlertApiForwarder()), - createApiFactory({ - api: errorApiRef, - deps: { alertApi: alertApiRef }, - factory: ({ alertApi }) => - new ErrorAlerter(alertApi, new ErrorApiForwarder()), - }), - createApiFactory({ - api: storageApiRef, - deps: { errorApi: errorApiRef }, - factory: ({ errorApi }) => WebStorage.create({ errorApi }), - }), - createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), - createApiFactory({ - api: googleAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GoogleAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: microsoftAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - MicrosoftAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: githubAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GithubAuth.create({ - discoveryApi, - oauthRequestApi, - defaultScopes: ['read:user'], - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oktaAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OktaAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: gitlabAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GitlabAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oneloginAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OneLoginAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: OAuth2Icon, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), -]; +/* + * 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 { + alertApiRef, + errorApiRef, + AlertApiForwarder, + ErrorApiForwarder, + ErrorAlerter, + discoveryApiRef, + GoogleAuth, + GithubAuth, + OAuth2, + OktaAuth, + GitlabAuth, + Auth0Auth, + MicrosoftAuth, + oauthRequestApiRef, + OAuthRequestManager, + googleAuthApiRef, + githubAuthApiRef, + oauth2ApiRef, + oktaAuthApiRef, + gitlabAuthApiRef, + auth0AuthApiRef, + microsoftAuthApiRef, + storageApiRef, + WebStorage, + createApiFactory, + configApiRef, + UrlPatternDiscovery, + samlAuthApiRef, + SamlAuth, + oneloginAuthApiRef, + OneLoginAuth, + oidcAuthApiRef, +} from '@backstage/core-api'; + +import OAuth2Icon from '@material-ui/icons/AcUnit'; + +export const defaultApis = [ + createApiFactory({ + api: discoveryApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + UrlPatternDiscovery.compile( + `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, + ), + }), + createApiFactory(alertApiRef, new AlertApiForwarder()), + createApiFactory({ + api: errorApiRef, + deps: { alertApi: alertApiRef }, + factory: ({ alertApi }) => + new ErrorAlerter(alertApi, new ErrorApiForwarder()), + }), + createApiFactory({ + api: storageApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => WebStorage.create({ errorApi }), + }), + createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), + createApiFactory({ + api: googleAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GoogleAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: microsoftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + MicrosoftAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: githubAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oktaAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OktaAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: gitlabAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GitlabAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: auth0AuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + Auth0Auth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oauth2ApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: samlAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, configApi }) => + SamlAuth.create({ + discoveryApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oneloginAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OneLoginAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oidcAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: OAuth2Icon, + }, + defaultScopes: [ + 'openid', + 'offline_access', + 'email', + ], + environment: configApi.getOptionalString('auth.environment'), + }), + }), +]; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 9e44a82c37..87ac7739f0 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -1,201 +1,201 @@ -/* - * 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 { - Issuer, - Client, - Strategy as OidcStrategy, - TokenSet, - UserinfoResponse, -} from 'openid-client'; -import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthResponse, - OAuthEnvironmentHandler, - OAuthStartRequest, - encodeState, - OAuthRefreshRequest, -} from '../../lib/oauth'; -import { - executeFrameHandlerStrategy, - executeRedirectStrategy, - PassportDoneCallback, -} from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory, ProfileInfo } from '../types'; - -type PrivateInfo = { - refreshToken: string; -}; - -type OidcImpl = { - strategy: OidcStrategy; - client: Client; -}; - -export type OidcAuthProviderOptions = OAuthProviderOptions & { - metadataUrl: string; - tokenSignedResponseAlg?: string; -}; - -export class OidcAuthProvider implements OAuthHandlers { - private readonly implementation: Promise; - - constructor(options: OidcAuthProviderOptions) { - this.implementation = this.setupStrategy(options); - } - - async start(req: OAuthStartRequest): Promise { - const { strategy } = await this.implementation; - return await executeRedirectStrategy(req, strategy, { - accessType: 'offline', - prompt: 'none', - scope: `${req.scope} default`, - state: encodeState(req.state), - }); - } - - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { strategy } = await this.implementation; - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, - PrivateInfo - >(req, strategy); - - return { - response: await this.populateIdentity(response), - refreshToken: privateInfo.refreshToken, - }; - } - - async refresh(req: OAuthRefreshRequest): Promise { - const { client } = await this.implementation; - const tokenset = await client.refresh(req.refreshToken); - if (!tokenset.access_token) { - throw new Error('Refresh failed'); - } - const profile = await client.userinfo(tokenset.access_token); - - return this.populateIdentity({ - providerInfo: { - accessToken: tokenset.access_token, - refreshToken: tokenset.refresh_token, - expiresInSeconds: tokenset.expires_in, - idToken: tokenset.id_token, - scope: tokenset.scope || '', - }, - profile, - }); - } - - private async setupStrategy( - options: OidcAuthProviderOptions, - ): Promise { - const issuer = await Issuer.discover(options.metadataUrl); - const client = new issuer.Client({ - client_id: options.clientId, - client_secret: options.clientSecret, - redirect_uris: [options.callbackUrl], - response_types: ['code'], - id_token_signed_response_alg: options.tokenSignedResponseAlg || 'RS256', - }); - - const strategy = new OidcStrategy( - { - client, - passReqToCallback: false as true, - }, - ( - tokenset: TokenSet, - userinfo: UserinfoResponse, - done: PassportDoneCallback, - ) => { - const profile: ProfileInfo = { - displayName: userinfo.name, - email: userinfo.email, - picture: userinfo.picture, - }; - - done( - undefined, - { - providerInfo: { - idToken: tokenset.id_token || '', - accessToken: tokenset.access_token || '', - scope: tokenset.scope || '', - expiresInSeconds: tokenset.expires_in, - }, - profile, - }, - { - refreshToken: tokenset.refresh_token || '', - }, - ); - }, - ); - strategy.error = console.error; - return { strategy, client }; - } - - // Use this function to grab the user profile info from the token - // Then populate the profile with it - private async populateIdentity( - response: OAuthResponse, - ): Promise { - const { profile } = response; - - if (!profile.email) { - throw new Error('Profile does not contain an email'); - } - const id = profile.email.split('@')[0]; - - return { ...response, backstageIdentity: { id } }; - } -} - -export const createOidcProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const metadataUrl = envConfig.getString('metadataUrl'); - const tokenSignedResponseAlg = envConfig.getString( - 'tokenSignedResponseAlg', - ); - - const provider = new OidcAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenSignedResponseAlg, - metadataUrl, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - }); - }); +/* + * 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 { + Issuer, + Client, + Strategy as OidcStrategy, + TokenSet, + UserinfoResponse, +} from 'openid-client'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, + OAuthStartRequest, + encodeState, + OAuthRefreshRequest, +} from '../../lib/oauth'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + PassportDoneCallback, +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory, ProfileInfo } from '../types'; + +type PrivateInfo = { + refreshToken: string; +}; + +type OidcImpl = { + strategy: OidcStrategy; + client: Client; +}; + +export type OidcAuthProviderOptions = OAuthProviderOptions & { + metadataUrl: string; + tokenSignedResponseAlg?: string; +}; + +export class OidcAuthProvider implements OAuthHandlers { + private readonly implementation: Promise; + + constructor(options: OidcAuthProviderOptions) { + this.implementation = this.setupStrategy(options); + } + + async start(req: OAuthStartRequest): Promise { + const { strategy } = await this.implementation; + return await executeRedirectStrategy(req, strategy, { + accessType: 'offline', + prompt: 'none', + scope: req.scope, + state: encodeState(req.state), + }); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { strategy } = await this.implementation; + const { response, privateInfo } = await executeFrameHandlerStrategy< + OAuthResponse, + PrivateInfo + >(req, strategy); + + return { + response: await this.populateIdentity(response), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(req: OAuthRefreshRequest): Promise { + const { client } = await this.implementation; + const tokenset = await client.refresh(req.refreshToken); + if (!tokenset.access_token) { + throw new Error('Refresh failed'); + } + const profile = await client.userinfo(tokenset.access_token); + + return this.populateIdentity({ + providerInfo: { + accessToken: tokenset.access_token, + refreshToken: tokenset.refresh_token, + expiresInSeconds: tokenset.expires_in, + idToken: tokenset.id_token, + scope: tokenset.scope || '', + }, + profile, + }); + } + + private async setupStrategy( + options: OidcAuthProviderOptions, + ): Promise { + const issuer = await Issuer.discover(options.metadataUrl); + const client = new issuer.Client({ + client_id: options.clientId, + client_secret: options.clientSecret, + redirect_uris: [options.callbackUrl], + response_types: ['code'], + id_token_signed_response_alg: options.tokenSignedResponseAlg || 'RS256', + }); + + const strategy = new OidcStrategy( + { + client, + passReqToCallback: false as true, + }, + ( + tokenset: TokenSet, + userinfo: UserinfoResponse, + done: PassportDoneCallback, + ) => { + const profile: ProfileInfo = { + displayName: userinfo.name, + email: userinfo.email, + picture: userinfo.picture, + }; + + done( + undefined, + { + providerInfo: { + idToken: tokenset.id_token || '', + accessToken: tokenset.access_token || '', + scope: tokenset.scope || '', + expiresInSeconds: tokenset.expires_in, + }, + profile, + }, + { + refreshToken: tokenset.refresh_token || '', + }, + ); + }, + ); + strategy.error = console.error; + return { strategy, client }; + } + + // Use this function to grab the user profile info from the token + // Then populate the profile with it + private async populateIdentity( + response: OAuthResponse, + ): Promise { + const { profile } = response; + + if (!profile.email) { + throw new Error('Profile does not contain an email'); + } + const id = profile.email.split('@')[0]; + + return { ...response, backstageIdentity: { id } }; + } +} + +export const createOidcProvider: AuthProviderFactory = ({ + providerId, + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const metadataUrl = envConfig.getString('metadataUrl'); + const tokenSignedResponseAlg = envConfig.getString( + 'tokenSignedResponseAlg', + ); + + const provider = new OidcAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenSignedResponseAlg, + metadataUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); + });