WIP: Got the SAML login working
TODO: Cleanup, remove hardcoded url.
This commit is contained in:
+69
-67
@@ -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
|
||||
|
||||
@@ -33,7 +33,10 @@ const app = createApp({
|
||||
components: {
|
||||
SignInPage: props => {
|
||||
return (
|
||||
<SignInPage {...props} providers={['guest', 'custom', ...providers]} />
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', 'custom', 'saml', ...providers]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -97,6 +97,21 @@ export type OAuthApi = {
|
||||
logout(): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<BackstageIdentity | undefined>;
|
||||
|
||||
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
|
||||
|
||||
// Not sure if this is needed.
|
||||
logout(): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<SamlApi>({
|
||||
id: 'core.auth.saml',
|
||||
description: 'provides authentication towards saml based provider',
|
||||
});
|
||||
|
||||
@@ -19,3 +19,4 @@ export * from './gitlab';
|
||||
export * from './google';
|
||||
export * from './oauth2';
|
||||
export * from './okta';
|
||||
export * from './saml';
|
||||
|
||||
@@ -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<SamlSession>({
|
||||
apiOrigin,
|
||||
basePath,
|
||||
environment,
|
||||
provider,
|
||||
});
|
||||
|
||||
const sessionManager = new SamlAuthSessionManager<SamlSession>({
|
||||
connector,
|
||||
});
|
||||
|
||||
const authSessionStore = new SamlAuthSessionStore<SamlSession>({
|
||||
manager: sessionManager,
|
||||
storageKey: 'samlSession',
|
||||
});
|
||||
|
||||
// return new SamlAuth(authSessionStore);
|
||||
return new SamlAuth(authSessionStore);
|
||||
}
|
||||
|
||||
sessionState$(): Observable<SessionState> {
|
||||
return this.sessionManager.sessionState$();
|
||||
}
|
||||
// constructor(private readonly sessionManager: any) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor(private readonly sessionManager: SessionManager<SamlSession>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('this is the constructor');
|
||||
}
|
||||
|
||||
async getBackstageIdentity(
|
||||
options: AuthRequestOptions,
|
||||
): Promise<BackstageIdentity | undefined> {
|
||||
// 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;
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<SamlResponse> implements AuthConnector<SamlResponse> {
|
||||
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<SamlResponse> {
|
||||
// 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<any> {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('==> this is refresh session');
|
||||
}
|
||||
|
||||
async removeSession(): Promise<void> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
export { DefaultAuthConnector } from './DefaultAuthConnector';
|
||||
export { SamlAuthConnector } from './SamlAuthConnector';
|
||||
export * from './types';
|
||||
|
||||
@@ -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<SamlResponse>;
|
||||
};
|
||||
|
||||
export class SamlAuthSessionManager<T> implements SessionManager<T> {
|
||||
private readonly connector: SamlAuthConnector<SamlResponse>;
|
||||
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<T | undefined> {
|
||||
// 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$();
|
||||
}
|
||||
}
|
||||
@@ -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<T> = {
|
||||
manager: SamlAuthSessionManager<T>;
|
||||
storageKey: string;
|
||||
};
|
||||
|
||||
export class SamlAuthSessionStore<T> implements SamlAuthSessionManager<T> {
|
||||
private readonly manager: SamlAuthSessionManager<T>;
|
||||
private readonly storageKey: string;
|
||||
|
||||
constructor(options: Options<T>) {
|
||||
const { manager, storageKey } = options;
|
||||
|
||||
this.manager = manager;
|
||||
this.storageKey = storageKey;
|
||||
}
|
||||
|
||||
async getSession(options: GetSessionOptions): Promise<T | undefined> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -36,6 +36,13 @@ export type SessionManager<T> = {
|
||||
sessionState$(): Observable<SessionState>;
|
||||
};
|
||||
|
||||
// FIXME do we need this?
|
||||
export type SamlAuthSessionManager<T> = {
|
||||
getSession(options: GetSessionOptions): Promise<T | undefined>;
|
||||
removeSession(): Promise<void>;
|
||||
sessionState$(): Observable<SessionState>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function called to determine the scopes of a session.
|
||||
*/
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<SidebarUserProfile open={open} setOpen={setOpen} />
|
||||
@@ -91,7 +97,7 @@ export function SidebarUserSettings() {
|
||||
<SidebarItem
|
||||
icon={SignOutIcon}
|
||||
text="Sign Out"
|
||||
onClick={() => identityApi.logout()}
|
||||
onClick={handleLogout}
|
||||
/>
|
||||
</Collapse>
|
||||
</>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<Grid item>
|
||||
<InfoCard
|
||||
title="SAML"
|
||||
actions={
|
||||
<Button color="primary" variant="outlined" onClick={handleLogin}>
|
||||
Sign In
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography variant="body1">Sign In using SAML lolololol</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
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 };
|
||||
@@ -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<OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi>
|
||||
| ApiRef<SamlApi>;
|
||||
};
|
||||
|
||||
export type IdentityProviders = ('guest' | 'custom' | SignInConfig)[];
|
||||
export type IdentityProviders = ('guest' | 'custom' | 'saml' | SignInConfig)[];
|
||||
|
||||
export type ProviderComponent = ComponentType<
|
||||
SignInPageProps & { config: SignInConfig }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void> {
|
||||
try {
|
||||
const {
|
||||
response: { userId, profile },
|
||||
response: { userId, profile },
|
||||
} = await executeFrameHandlerStrategy<SamlInfo>(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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user