Merge pull request #1512 from spotify/freben/oauth2-custom-identity-provider

feat(auth): implement generic oauth2 provider
This commit is contained in:
Nikita Dudnik
2020-07-03 10:21:06 +02:00
committed by GitHub
14 changed files with 520 additions and 28 deletions
+12 -1
View File
@@ -26,12 +26,14 @@ import {
FeatureFlags,
GoogleAuth,
GithubAuth,
OAuth2,
OktaAuth,
GitlabAuth,
oauthRequestApiRef,
OAuthRequestManager,
googleAuthApiRef,
githubAuthApiRef,
oauth2ApiRef,
oktaAuthApiRef,
gitlabAuthApiRef,
storageApiRef,
@@ -109,7 +111,16 @@ export const apis = (config: ConfigApi) => {
builder.add(
gitlabAuthApiRef,
GitlabAuth.create({
apiOrigin: 'http://localhost:7000',
apiOrigin: backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
oauth2ApiRef,
OAuth2.create({
apiOrigin: backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
+7 -1
View File
@@ -25,7 +25,13 @@ You should only need to do this once.
After that, go to the `packages/backend` directory and run
```bash
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x SENTRY_TOKEN=x LOG_LEVEL=debug yarn start
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \
AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \
AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \
AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \
SENTRY_TOKEN=x \
LOG_LEVEL=debug \
yarn start
```
Substitute `x` for actual values, or leave them as
@@ -263,3 +263,13 @@ export const gitlabAuthApiRef = createApiRef<
id: 'core.auth.gitlab',
description: 'Provides authentication towards Gitlab APIs',
});
/**
* Provides authentication for custom identity providers.
*/
export const oauth2ApiRef = createApiRef<
OAuthApi & OpenIdConnectApi & ProfileInfoApi & SessionStateApi
>({
id: 'core.auth.oauth2',
description: 'Example of how to use oauth2 custom provider',
});
@@ -14,7 +14,8 @@
* limitations under the License.
*/
export * from './google';
export * from './github';
export * from './gitlab';
export * from './google';
export * from './oauth2';
export * from './okta';
@@ -0,0 +1,164 @@
/*
* 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 OAuth2Icon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { Observable } from '../../../../types';
import { AuthProvider, OAuthRequestApi } from '../../../definitions';
import {
AuthRequestOptions,
BackstageIdentity,
OAuthApi,
OpenIdConnectApi,
ProfileInfo,
ProfileInfoApi,
SessionState,
SessionStateApi,
} from '../../../definitions/auth';
import { OAuth2Session } from './types';
type CreateOptions = {
apiOrigin: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
environment?: string;
provider?: AuthProvider & { id: string };
};
export type OAuth2Response = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'oauth2',
title: 'Your Identity Provider',
icon: OAuth2Icon,
};
const SCOPE_PREFIX = '';
class OAuth2
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
static create({
apiOrigin,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
apiOrigin,
basePath,
environment,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform(res: OAuth2Response): OAuth2Session {
return {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: OAuth2.normalizeScopes(res.providerInfo.scope),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
},
});
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set([
'openid',
`${SCOPE_PREFIX}userinfo.email`,
`${SCOPE_PREFIX}userinfo.profile`,
]),
sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes,
sessionShouldRefresh: (session: OAuth2Session) => {
const expiresInSec =
(session.providerInfo.expiresAt.getTime() - Date.now()) / 1000;
return expiresInSec < 60 * 5;
},
});
return new OAuth2(sessionManager);
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<OAuth2Session>) {}
async getAccessToken(
scope?: string | string[],
options?: AuthRequestOptions,
) {
const normalizedScopes = OAuth2.normalizeScopes(scope);
const session = await this.sessionManager.getSession({
...options,
scopes: normalizedScopes,
});
return session?.providerInfo.accessToken ?? '';
}
async getIdToken(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.providerInfo.idToken ?? '';
}
async logout() {
await this.sessionManager.removeSession();
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
static normalizeScopes(scopes?: string | string[]): Set<string> {
if (!scopes) {
return new Set();
}
const scopeList = Array.isArray(scopes)
? scopes
: scopes.split(/[\s]/).filter(Boolean);
return new Set(scopeList);
}
}
export default OAuth2;
@@ -0,0 +1,18 @@
/*
* 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 OAuth2 } from './OAuth2';
export * from './types';
@@ -0,0 +1,28 @@
/*
* 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 OAuth2Session = {
providerInfo: {
idToken: string;
accessToken: string;
scopes: Set<string>;
expiresAt: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
@@ -14,25 +14,26 @@
* limitations under the License.
*/
import React, { useContext, useEffect } from 'react';
import Collapse from '@material-ui/core/Collapse';
import Star from '@material-ui/icons/Star';
import SignOutIcon from '@material-ui/icons/MeetingRoom';
import { SidebarContext } from './config';
import {
googleAuthApiRef,
githubAuthApiRef,
gitlabAuthApiRef,
googleAuthApiRef,
identityApiRef,
oauth2ApiRef,
oktaAuthApiRef,
useApi,
} from '@backstage/core-api';
import Collapse from '@material-ui/core/Collapse';
import SignOutIcon from '@material-ui/icons/MeetingRoom';
import Star from '@material-ui/icons/Star';
import React, { useContext, useEffect } from 'react';
import { SidebarContext } from './config';
import { SidebarItem } from './Items';
import {
OAuthProviderSettings,
OIDCProviderSettings,
UserProfile as SidebarUserProfile,
} from './Settings';
import { SidebarItem } from './Items';
export function SidebarUserSettings() {
const { isOpen: sidebarOpen } = useContext(SidebarContext);
@@ -68,6 +69,11 @@ export function SidebarUserSettings() {
apiRef={oktaAuthApiRef}
icon={Star}
/>
<OIDCProviderSettings
title="YourOrg"
apiRef={oauth2ApiRef}
icon={Star}
/>
<SidebarItem
icon={SignOutIcon}
text="Sign Out"
+23 -12
View File
@@ -1,21 +1,23 @@
import {
ApiRegistry,
AlertApiForwarder,
alertApiRef,
ApiRegistry,
ErrorAlerter,
ErrorApiForwarder,
errorApiRef,
GithubAuth,
githubAuthApiRef,
GitlabAuth,
gitlabAuthApiRef,
GoogleAuth,
googleAuthApiRef,
identityApiRef,
OAuth2,
oauth2ApiRef,
oauthRequestApiRef,
OAuthRequestManager,
googleAuthApiRef,
githubAuthApiRef,
gitlabAuthApiRef,
oktaAuthApiRef,
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
GoogleAuth,
GithubAuth,
GitlabAuth,
OktaAuth,
identityApiRef,
oktaAuthApiRef,
} from '@backstage/core';
const builder = ApiRegistry.builder();
@@ -72,4 +74,13 @@ builder.add(
}),
);
builder.add(
oauth2ApiRef,
OAuth2.create({
apiOrigin: 'http://localhost:7000',
basePath: '/auth/',
oauthRequestApi,
}),
);
export const apis = builder.build();