diff --git a/app-config.yaml b/app-config.yaml index f578fb3fca..e65df93969 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -28,72 +28,74 @@ sentry: auth: providers: - google: - development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_GOOGLE_CLIENT_ID - clientSecret: - $secret: - env: AUTH_GOOGLE_CLIENT_SECRET - github: - development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_GITHUB_CLIENT_ID - clientSecret: - $secret: - env: AUTH_GITHUB_CLIENT_SECRET - enterpriseInstanceUrl: - $secret: - env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL - gitlab: - development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_GITLAB_CLIENT_ID - clientSecret: - $secret: - env: AUTH_GITLAB_CLIENT_SECRET - audience: - $secret: - env: GITLAB_BASE_URL - # saml: + # google: # development: - # entryPoint: "http://localhost:7001/" - # issuer: "passport-saml" - okta: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_GOOGLE_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_GOOGLE_CLIENT_SECRET + # github: + # development: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_GITHUB_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_GITHUB_CLIENT_SECRET + # enterpriseInstanceUrl: + # $secret: + # env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + # gitlab: + # development: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_GITLAB_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_GITLAB_CLIENT_SECRET + # audience: + # $secret: + # env: GITLAB_BASE_URL + saml: development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_OKTA_CLIENT_ID - clientSecret: - $secret: - env: AUTH_OKTA_CLIENT_SECRET - audience: - $secret: - env: AUTH_OKTA_AUDIENCE - oauth2: - development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_OAUTH2_CLIENT_ID - clientSecret: - $secret: - env: AUTH_OAUTH2_CLIENT_SECRET - authorizationURL: - $secret: - env: AUTH_OAUTH2_AUTH_URL - tokenURL: - $secret: - env: AUTH_OAUTH2_TOKEN_URL + entryPoint: 'https://sso.jumpcloud.com/saml2/backstage-test' + issuer: 'passport-saml' + # entryPoint: 'http://localhost:7001/' + # issuer: 'passport-saml' + # okta + # development: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_OKTA_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_OKTA_CLIENT_SECRET + # audience: + # $secret: + # env: AUTH_OKTA_AUDIENCE + # oauth2: + # development: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_OAUTH2_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_OAUTH2_CLIENT_SECRET + # authorizationURL: + # $secret: + # env: AUTH_OAUTH2_AUTH_URL + # tokenURL: + # $secret: + # env: AUTH_OAUTH2_TOKEN_URL diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9a9fdfe5c4..29e371b469 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,7 +33,10 @@ const app = createApp({ components: { SignInPage: props => { return ( - + ); }, }, diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index cac3d6e171..0c097450bc 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -38,6 +38,8 @@ import { gitlabAuthApiRef, storageApiRef, WebStorage, + SamlAuth, + samlAuthApiRef, } from '@backstage/core'; import { @@ -139,6 +141,14 @@ export const apis = (config: ConfigApi) => { }), ); + builder.add( + samlAuthApiRef, + SamlAuth.create({ + apiOrigin: backendUrl, + basePath: '/auth/', + }), + ); + builder.add( techRadarApiRef, new TechRadar({ diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index e80b1d6fea..e229bb7671 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -19,6 +19,7 @@ import { gitlabAuthApiRef, oktaAuthApiRef, githubAuthApiRef, + samlAuthApiRef, } from '@backstage/core'; export const providers = [ @@ -46,4 +47,10 @@ export const providers = [ message: 'Sign In using Okta', apiRef: oktaAuthApiRef, }, + { + id: 'saml-auth-provider', + title: 'SAML', + message: 'Sign In using SAML', + apiRef: samlAuthApiRef, + }, ]; diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index ec7aba81c4..6dfe9595b0 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -97,6 +97,21 @@ export type OAuthApi = { logout(): Promise; }; +/** + * This API provides access to SAML 2 credentials. Verify user access with identity provider. + */ +export type SamlApi = { + // Not sure what Promise call back should have. + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; + + getProfile(options?: AuthRequestOptions): Promise; + + // Not sure if this is needed. + logout(): Promise; +}; + /** * This API provides access to OpenID Connect credentials. It lets you request ID tokens, * which can be passed to backend services to prove the user's identity. @@ -273,3 +288,11 @@ export const oauth2ApiRef = createApiRef< id: 'core.auth.oauth2', description: 'Example of how to use oauth2 custom provider', }); + +/** + * Provides authentication for saml based identity providers + */ +export const samlAuthApiRef = createApiRef({ + id: 'core.auth.saml', + description: 'provides authentication towards saml based provider', +}); diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 786e9fa771..0906e34149 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -19,3 +19,4 @@ export * from './gitlab'; export * from './google'; export * from './oauth2'; export * from './okta'; +export * from './saml'; diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts new file mode 100644 index 0000000000..75aefda7f5 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -0,0 +1,126 @@ +/* + * 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 SamlIcon from '@material-ui/icons/AcUnit'; +import { SamlAuthConnector } from '../../../../lib/AuthConnector'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { Observable } from '../../../../types'; +import { + ProfileInfo, + BackstageIdentity, + SessionState, + AuthRequestOptions, + SamlApi, +} from '../../../definitions/auth'; +import { AuthProvider } from '../../../definitions'; +import { SamlSession } from './types'; +import { + SamlAuthSessionManager, + SamlAuthSessionStore, +} from '../../../../lib/AuthSessionManager'; + +type CreateOptions = { + apiOrigin: string; + basePath: string; + + // oauthRequestApi?: OAuthRequestApi; + + environment?: string; + provider?: AuthProvider & { id: string }; +}; + +export type SamlAuthResponse = { + userId: string; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +const DEFAULT_PROVIDER = { + id: 'saml', + title: 'SAML', + icon: SamlIcon, +}; + +class SamlAuth implements SamlApi { + static create({ + apiOrigin, + basePath, + environment = 'development', + provider = DEFAULT_PROVIDER, + }: CreateOptions) { + // eslint-disable-next-line no-console + console.log('this is from SamlAuth'); + + const connector = new SamlAuthConnector({ + apiOrigin, + basePath, + environment, + provider, + }); + + const sessionManager = new SamlAuthSessionManager({ + connector, + }); + + const authSessionStore = new SamlAuthSessionStore({ + manager: sessionManager, + storageKey: 'samlSession', + }); + + // return new SamlAuth(authSessionStore); + return new SamlAuth(authSessionStore); + } + + sessionState$(): Observable { + return this.sessionManager.sessionState$(); + } + // constructor(private readonly sessionManager: any) {} + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(private readonly sessionManager: SessionManager) { + // eslint-disable-next-line no-console + console.log('this is the constructor'); + } + + async getBackstageIdentity( + options: AuthRequestOptions, + ): Promise { + // eslint-disable-next-line no-console + console.log('===> Saml getBackstageIdentity()'); + const session = await this.sessionManager.getSession(options); + // eslint-disable-next-line no-console + console.log('this this thish lkkdfkkfkfji'); + // eslint-disable-next-line no-console + console.log(session); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + // eslint-disable-next-line no-console + console.log('==> samlauth getprofile()'); + const session = await this.sessionManager.getSession(options); + // eslint-disable-next-line no-console + console.log('+++ this is the session from getProfile()'); + // eslint-disable-next-line no-console + console.log(session); + return session?.profile; + } + + async logout() { + await this.sessionManager.removeSession(); + } +} + +export default SamlAuth; diff --git a/packages/core-api/src/apis/implementations/auth/saml/index.ts b/packages/core-api/src/apis/implementations/auth/saml/index.ts new file mode 100644 index 0000000000..c2436ab435 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/saml/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { default as SamlAuth } from './SamlAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/saml/types.ts b/packages/core-api/src/apis/implementations/auth/saml/types.ts new file mode 100644 index 0000000000..296f70b0ea --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/saml/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { ProfileInfo, BackstageIdentity } from '../../../definitions'; + +export type SamlSession = { + userId: string; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts index 69ee5d28ac..1a6cdac3e6 100644 --- a/packages/core-api/src/app/AppIdentity.ts +++ b/packages/core-api/src/app/AppIdentity.ts @@ -67,6 +67,10 @@ export class AppIdentity implements IdentityApi { // This is indirectly called by the sign-in page to continue into the app. setSignInResult(result: SignInResult) { + // eslint-disable-next-line no-console + console.log('this is from setSignInResult'); + // eslint-disable-next-line no-console + console.log(result); if (this.hasIdentity) { return; } diff --git a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts new file mode 100644 index 0000000000..9dc0fca0a0 --- /dev/null +++ b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts @@ -0,0 +1,105 @@ +/* + * 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 { + AuthProvider, + ProfileInfo, + BackstageIdentity, +} from '../../apis/definitions'; +import { AuthConnector } from './types'; +import { showLoginPopup } from '../loginPopup'; + +const DEFAULT_BASE_PATH = '/api/auth'; + +type Options = { + apiOrigin?: string; + basePath?: string; + environment?: string; + provider: AuthProvider & { id: string }; +}; + +export type SamlResponse = { + userId: string; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +export class SamlAuthConnector implements AuthConnector { + private readonly apiOrigin: string; + private readonly basePath: string; + private readonly environment: string | undefined; + private readonly provider: AuthProvider & { id: string }; + + constructor(options: Options) { + const { + apiOrigin = window.location.origin, + basePath = DEFAULT_BASE_PATH, + environment, + provider, + } = options; + + this.apiOrigin = apiOrigin; + this.basePath = basePath; + this.environment = environment; + this.provider = provider; + } + + async createSession(): Promise { + // eslint-disable-next-line no-console + console.log('==> from SamlAuthConnector createSession'); + + const payload = await showLoginPopup({ + url: 'http://localhost:7000/auth/saml/start', // FIXME: this should be from app.config or somewhere + name: 'SAML Login', // FIXME: change this to provider name? and not hardcode the name + origin: this.apiOrigin, + width: 450, + height: 730, + }); + + // eslint-disable-next-line no-console + console.log(payload); + + return { + ...payload, + id: payload.profile.email, + }; + } + + // TODO: do we need this for SAML? + async refreshSession(): Promise { + // eslint-disable-next-line no-console + console.log('==> this is refresh session'); + } + + async removeSession(): Promise { + // eslint-disable-next-line no-console + console.log('this removes the session'); + const res = await fetch('http://localhost:7000/auth/saml/logout', { + method: 'POST', + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + credentials: 'include', + }).catch(error => { + throw new Error(`Logout request failed, ${error}`); + }); + + if (!res.ok) { + const error: any = new Error(`Logout request failed, ${res.statusText}`); + error.status = res.status; + throw error; + } + } +} diff --git a/packages/core-api/src/lib/AuthConnector/index.ts b/packages/core-api/src/lib/AuthConnector/index.ts index db5c582328..84f5067443 100644 --- a/packages/core-api/src/lib/AuthConnector/index.ts +++ b/packages/core-api/src/lib/AuthConnector/index.ts @@ -15,4 +15,5 @@ */ export { DefaultAuthConnector } from './DefaultAuthConnector'; +export { SamlAuthConnector } from './SamlAuthConnector'; export * from './types'; diff --git a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts new file mode 100644 index 0000000000..9439684cf4 --- /dev/null +++ b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts @@ -0,0 +1,64 @@ +/* + * 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 { SessionManager, GetSessionOptions } from './types'; +import { SamlAuthConnector, SamlResponse } from '../AuthConnector/SamlAuthConnector'; +import { SessionStateTracker } from './SessionStateTracker'; + + +type Options = { + connector: SamlAuthConnector; +}; + +export class SamlAuthSessionManager implements SessionManager { + private readonly connector: SamlAuthConnector; + private readonly stateTracker = new SessionStateTracker(); + + private currentSession: any | undefined; // FIXME: proper typing here + + constructor(options: Options) { + const { connector } = options; + + this.connector = connector; + // this.helper = new SessionScopeHelper + } + + async getSession(options: GetSessionOptions): Promise { + // eslint-disable-next-line no-console + console.log('==> this is from SamlAuthSessionManager getSession()'); + if (this.currentSession) { + return this.currentSession; + } + + if (options.optional) { + return undefined; + } + + this.currentSession = await this.connector.createSession(); + this.stateTracker.setIsSignedIn(true); + return this.currentSession; + } + + async removeSession() { + this.currentSession = undefined; + await this.connector.removeSession(); + this.stateTracker.setIsSignedIn(false); + } + + sessionState$() { + return this.stateTracker.sessionState$(); + } +} diff --git a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts new file mode 100644 index 0000000000..53fba09b54 --- /dev/null +++ b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts @@ -0,0 +1,96 @@ +/* + * 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 { SamlAuthSessionManager, GetSessionOptions } from './types'; + +type Options = { + manager: SamlAuthSessionManager; + storageKey: string; +}; + +export class SamlAuthSessionStore implements SamlAuthSessionManager { + private readonly manager: SamlAuthSessionManager; + private readonly storageKey: string; + + constructor(options: Options) { + const { manager, storageKey } = options; + + this.manager = manager; + this.storageKey = storageKey; + } + + async getSession(options: GetSessionOptions): Promise { + // eslint-disable-next-line no-console + console.log('==>> this is from SamlAuthSessionStore getSession()'); + const session = this.loadSession(); + + if (session) { + return session!; + } + + const newSession = await this.manager.getSession(options); + this.saveSession(newSession); + return newSession; + } + + async removeSession() { + localStorage.removeItem(this.storageKey); + await this.manager.removeSession(); + } + + sessionState$() { + return this.manager.sessionState$(); + } + + private saveSession(session: T | undefined) { + if (session === undefined) { + localStorage.removeItem(this.storageKey); + } else { + localStorage.setItem( + this.storageKey, + JSON.stringify(session, (_key, value) => { + if (value instanceof Set) { + return { + __type: 'Set', + __value: Array.from(value), + }; + } + return value; + }), + ); + } + } + + private loadSession(): T | undefined { + try { + const sessionJson = localStorage.getItem(this.storageKey); + if (sessionJson) { + const session = JSON.parse(sessionJson, (_key, value) => { + if (value?.__type === 'Set') { + return new Set(value.__value); + } + return value; + }); + return session; + } + + return undefined; + } catch (error) { + localStorage.removeItem(this.storageKey); + return undefined; + } + } +} diff --git a/packages/core-api/src/lib/AuthSessionManager/index.ts b/packages/core-api/src/lib/AuthSessionManager/index.ts index 5f4dde8662..a452ac0055 100644 --- a/packages/core-api/src/lib/AuthSessionManager/index.ts +++ b/packages/core-api/src/lib/AuthSessionManager/index.ts @@ -16,5 +16,7 @@ export { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; export { StaticAuthSessionManager } from './StaticAuthSessionManager'; +export { SamlAuthSessionManager } from './SamlAuthSessionManager'; export { AuthSessionStore } from './AuthSessionStore'; +export { SamlAuthSessionStore } from './SamlAuthSessionStore'; export * from './types'; diff --git a/packages/core-api/src/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts index 804c7121e1..1be439a840 100644 --- a/packages/core-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-api/src/lib/AuthSessionManager/types.ts @@ -36,6 +36,13 @@ export type SessionManager = { sessionState$(): Observable; }; +// FIXME do we need this? +export type SamlAuthSessionManager = { + getSession(options: GetSessionOptions): Promise; + removeSession(): Promise; + sessionState$(): Observable; +}; + /** * A function called to determine the scopes of a session. */ diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index e69d63186d..b08a3b82d8 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -49,6 +49,12 @@ export function SidebarUserSettings() { if (!sidebarOpen && open) setOpen(false); }, [open, sidebarOpen]); + // FIXME: Change this and remove the session storage stuff + const handleLogout = () => { + identityApi.logout(); + window.sessionStorage.clear(); + }; + return ( <> @@ -91,7 +97,7 @@ export function SidebarUserSettings() { identityApi.logout()} + onClick={handleLogout} /> diff --git a/packages/core/src/layout/SignInPage/commonProvider.tsx b/packages/core/src/layout/SignInPage/commonProvider.tsx index f9611342a2..88a53ec358 100644 --- a/packages/core/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core/src/layout/SignInPage/commonProvider.tsx @@ -36,6 +36,11 @@ const Component: ProviderComponent = ({ config, onResult }) => { instantPopup: true, }); + // eslint-disable-next-line no-console + console.log('====++++++++++++++++++++++++++> handleLogin for commonProvider'); + // eslint-disable-next-line no-console + console.log(identity); + const profile = await authApi.getProfile(); onResult({ userId: identity!.id, diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index ff60eba228..436078e49c 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -26,6 +26,7 @@ import { SignInConfig, IdentityProviders, SignInProvider } from './types'; import { commonProvider } from './commonProvider'; import { guestProvider } from './guestProvider'; import { customProvider } from './customProvider'; +import { samlProvider } from './samlProvider'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; @@ -41,6 +42,7 @@ const signInProviders: { [key: string]: SignInProvider } = { guest: guestProvider, custom: customProvider, common: commonProvider, + saml: samlProvider, }; function validateIDs(id: string, providers: SignInProviderType): void { diff --git a/packages/core/src/layout/SignInPage/samlProvider.tsx b/packages/core/src/layout/SignInPage/samlProvider.tsx new file mode 100644 index 0000000000..e9673bca8c --- /dev/null +++ b/packages/core/src/layout/SignInPage/samlProvider.tsx @@ -0,0 +1,92 @@ +/* + * 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 React from 'react'; +import { Grid, Typography, Button } from '@material-ui/core'; +import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { useApi, errorApiRef /* samlAuthApiRef */ } from '@backstage/core-api'; +import { InfoCard } from '../InfoCard/InfoCard'; + +const Component: ProviderComponent = ({ onResult }) => { + // const samlApi = useApi(samlAuthApiRef); + const errorApi = useApi(errorApiRef); + + const handleSSORedirect = () => { + // FIXME: this shoudl be from identity API or something. + window.open('http://localhost:7000/auth/saml/start'); + }; + + const receiveMessage = (event: any) => { + // FIXME: Should use the backstage identity API and not create this session storage. + window.sessionStorage.setItem( + 'helixSession', + JSON.stringify(event.data.response), + ); + + onResult({ + userId: event.data.response.backstageIdentity, + profile: { + email: event.data.response.profile.email, + displayName: event.data.response.profile.displayName, + }, + }); + }; + + const handleLogin = async () => { + try { + // FIXME: this should be handle through backstage API or something. + handleSSORedirect(); + window.addEventListener('message', receiveMessage, false); + } catch (error) { + errorApi.post(error); + } + }; + + return ( + + + Sign In + + } + > + Sign In using SAML lolololol + + + ); +}; + +const loader: ProviderLoader = async () => { + // FIXME: should get the profile from identiy API not from storage session. + const sessionRaw = window.sessionStorage.getItem('helixSession'); + + if (!sessionRaw) { + return undefined; + } + + const session = JSON.parse(sessionRaw); + + return { + userId: session.backstageIdentity, + profile: { + email: session.profile.email, + displayName: session.profile.displayName, + }, + }; +}; + +export const samlProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/types.ts b/packages/core/src/layout/SignInPage/types.ts index 9c501e8095..74463f93b4 100644 --- a/packages/core/src/layout/SignInPage/types.ts +++ b/packages/core/src/layout/SignInPage/types.ts @@ -24,18 +24,19 @@ import { ProfileInfoApi, BackstageIdentityApi, SessionStateApi, + SamlApi, } from '@backstage/core-api'; export type SignInConfig = { id: string; title: string; message: string; - apiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi - >; + apiRef: + | ApiRef + | ApiRef; }; -export type IdentityProviders = ('guest' | 'custom' | SignInConfig)[]; +export type IdentityProviders = ('guest' | 'custom' | 'saml' | SignInConfig)[]; export type ProviderComponent = ComponentType< SignInPageProps & { config: SignInConfig } diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 701eef81af..3311bfc836 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -62,6 +62,7 @@ export const createAuthProviderRouter = ( for (const env of envs) { const envConfig = providerConfig.getConfig(env); + console.log(envConfig); const provider = factory(globalConfig, env, envConfig, logger, issuer); if (provider) { envProviders[env] = provider; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 1ccee1a9cb..fe6bb87b6d 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -55,8 +55,21 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { // for non-oauth auth flows. // TODO: This flow doesn't issue an identity token that can be used to validate // the identity of the user in other backends, which we need in some form. + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===> SamlAuthProvider constructor'); + console.log(profile); done(undefined, { - userId: profile.ID!, + userId: profile.nameID!, profile: { email: profile.email!, displayName: profile.displayName as string, @@ -76,9 +89,9 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { ): Promise { try { const { - response: { userId, profile }, + response: { userId, profile }, } = await executeFrameHandlerStrategy(req, this.strategy); - + const id = userId; const idToken = await this.tokenIssuer.issueToken({ claims: { sub: id }, @@ -107,8 +120,14 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { res.send('noop'); } - identifyEnv(): string | undefined { - return undefined; + identifyEnv(req: express.Request): string | undefined { + const reqEnv = req.query.env?.toString(); + if (reqEnv) { + return reqEnv; + } + + // FIXME : do we want to always to return 'development' ? + return 'development'; } } diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 0224dc7f1d..2b446c9cd6 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -116,14 +116,14 @@ export class HigherOrderOperations implements HigherOrderOperation { * Entities are read from their respective sources, are parsed and validated * according to the entity policy, and get inserted or updated in the catalog. * Entities that have disappeared from their location are left orphaned, - * without changes. + * without changes.i */ async refreshAllLocations(): Promise { const startTimestamp = new Date().valueOf(); - this.logger.info('Beginning locations refresh'); + // this.logger.info('Beginning locations refresh'); const locations = await this.locationsCatalog.locations(); - this.logger.info(`Visiting ${locations.length} locations`); + // this.logger.info(`Visiting ${locations.length} locations`); for (const { data: location } of locations) { this.logger.debug( diff --git a/yarn.lock b/yarn.lock index 0354b1844f..f873b2cdaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9848,9 +9848,8 @@ file-loader@^4.2.0: loader-utils "^1.2.3" schema-utils "^2.5.0" -"file-saver@github:eligrey/FileSaver.js#1.3.8": +file-saver@eligrey/FileSaver.js#1.3.8: version "1.3.8" - uid e865e37af9f9947ddcced76b549e27dc45c1cb2e resolved "https://codeload.github.com/eligrey/FileSaver.js/tar.gz/e865e37af9f9947ddcced76b549e27dc45c1cb2e" file-system-cache@^1.0.5: @@ -12977,7 +12976,7 @@ jspdf@1.5.3: integrity sha512-J9X76xnncMw+wIqb15HeWfPMqPwYxSpPY8yWPJ7rAZN/ZDzFkjCSZObryCyUe8zbrVRNiuCnIeQteCzMn7GnWw== dependencies: canvg "1.5.3" - file-saver "github:eligrey/FileSaver.js#1.3.8" + file-saver eligrey/FileSaver.js#1.3.8 html2canvas "1.0.0-alpha.12" omggif "1.0.7" promise-polyfill "8.1.0"