Merge pull request #8658 from backstage/rugvip/sir

auth-backend: migrate onelogin and auth0 to sign-in resolver
This commit is contained in:
Patrik Oldsberg
2021-12-28 20:19:08 +01:00
committed by GitHub
5 changed files with 261 additions and 71 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Update `auth0` and `onelogin` providers to allow for `authHandler` and `signIn.resolver` configuration.
+26
View File
@@ -47,6 +47,14 @@ export type AtlassianProviderOptions = {
};
};
// @public (undocumented)
export type Auth0ProviderOptions = {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver<OAuthResult>;
};
};
// @public
export type AuthHandler<AuthResult> = (
input: AuthResult,
@@ -219,6 +227,11 @@ export const createAtlassianProvider: (
options?: AtlassianProviderOptions | undefined,
) => AuthProviderFactory;
// @public (undocumented)
export const createAuth0Provider: (
options?: Auth0ProviderOptions | undefined,
) => AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -282,6 +295,11 @@ export const createOktaProvider: (
_options?: OktaProviderOptions | undefined,
) => AuthProviderFactory;
// @public (undocumented)
export const createOneLoginProvider: (
options?: OneLoginProviderOptions | undefined,
) => AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -572,6 +590,14 @@ export type OktaProviderOptions = {
};
};
// @public (undocumented)
export type OneLoginProviderOptions = {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver<OAuthResult>;
};
};
// Warning: (ae-missing-release-tag) "postMessageResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -36,7 +36,15 @@ import {
makeProfileInfo,
PassportDoneCallback,
} from '../../lib/passport';
import { RedirectInfo, AuthProviderFactory } from '../types';
import {
RedirectInfo,
AuthProviderFactory,
AuthHandler,
SignInResolver,
} from '../types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { TokenIssuer } from '../../identity';
import { Logger } from 'winston';
type PrivateInfo = {
refreshToken: string;
@@ -44,12 +52,27 @@ type PrivateInfo = {
export type Auth0AuthProviderOptions = OAuthProviderOptions & {
domain: string;
signInResolver?: SignInResolver<OAuthResult>;
authHandler: AuthHandler<OAuthResult>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
};
export class Auth0AuthProvider implements OAuthHandlers {
private readonly _strategy: Auth0Strategy;
private readonly signInResolver?: SignInResolver<OAuthResult>;
private readonly authHandler: AuthHandler<OAuthResult>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
constructor(options: Auth0AuthProviderOptions) {
this.signInResolver = options.signInResolver;
this.authHandler = options.authHandler;
this.tokenIssuer = options.tokenIssuer;
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
this._strategy = new Auth0Strategy(
{
clientID: options.clientId,
@@ -98,18 +121,8 @@ export class Auth0AuthProvider implements OAuthHandlers {
PrivateInfo
>(req, this._strategy);
const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
return {
response: await this.populateIdentity({
profile,
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
}),
response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
@@ -125,53 +138,118 @@ export class Auth0AuthProvider implements OAuthHandlers {
this._strategy,
accessToken,
);
const profile = makeProfileInfo(fullProfile, params.id_token);
return this.populateIdentity({
providerInfo: {
accessToken,
idToken: params.id_token,
expiresInSeconds: params.expires_in,
scope: params.scope,
},
profile,
return this.handleResult({
fullProfile,
params,
accessToken,
refreshToken: req.refreshToken,
});
}
// Use this function to grab the user profile info from the token
// Then populate the profile with it
private async populateIdentity(
response: OAuthResponse,
): Promise<OAuthResponse> {
const { profile } = response;
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
if (!profile.email) {
throw new Error('Profile does not contain an email');
const response: OAuthResponse = {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
profile,
};
if (this.signInResolver) {
response.backstageIdentity = await this.signInResolver(
{
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
);
}
const id = profile.email.split('@')[0];
return { ...response, backstageIdentity: { id, token: '' } };
return response;
}
}
export type Auth0ProviderOptions = {};
const defaultSignInResolver: SignInResolver<OAuthResult> = async info => {
const { profile } = info;
if (!profile.email) {
throw new Error('Profile does not contain an email');
}
const id = profile.email.split('@')[0];
return { id, token: '' };
};
/** @public */
export type Auth0ProviderOptions = {
/**
* The profile transformation function used to verify and convert the auth response
* into the profile that will be presented to the user.
*/
authHandler?: AuthHandler<OAuthResult>;
/**
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
*/
signIn?: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<OAuthResult>;
};
};
/** @public */
export const createAuth0Provider = (
_options?: Auth0ProviderOptions,
options?: Auth0ProviderOptions,
): AuthProviderFactory => {
return ({ providerId, globalConfig, config, tokenIssuer }) =>
return ({
providerId,
globalConfig,
config,
tokenIssuer,
catalogApi,
logger,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const domain = envConfig.getString('domain');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
? options.authHandler
: async ({ fullProfile, params }) => ({
profile: makeProfileInfo(fullProfile, params.id_token),
});
const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver;
const provider = new Auth0AuthProvider({
clientId,
clientSecret,
callbackUrl,
domain,
authHandler,
signInResolver,
tokenIssuer,
catalogIdentityClient,
logger,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {
+5 -3
View File
@@ -14,6 +14,10 @@
* limitations under the License.
*/
export * from './atlassian';
export * from './auth0';
export * from './aws-alb';
export * from './bitbucket';
export * from './github';
export * from './gitlab';
export * from './google';
@@ -21,9 +25,7 @@ export * from './microsoft';
export * from './oauth2';
export * from './oidc';
export * from './okta';
export * from './bitbucket';
export * from './atlassian';
export * from './aws-alb';
export * from './onelogin';
export * from './saml';
export { factories as defaultAuthProviderFactories } from './factories';
@@ -36,7 +36,15 @@ import {
executeFetchUserProfileStrategy,
PassportDoneCallback,
} from '../../lib/passport';
import { RedirectInfo, AuthProviderFactory } from '../types';
import {
RedirectInfo,
AuthProviderFactory,
AuthHandler,
SignInResolver,
} from '../types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
type PrivateInfo = {
refreshToken: string;
@@ -44,12 +52,27 @@ type PrivateInfo = {
export type Options = OAuthProviderOptions & {
issuer: string;
signInResolver?: SignInResolver<OAuthResult>;
authHandler: AuthHandler<OAuthResult>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
};
export class OneLoginProvider implements OAuthHandlers {
private readonly _strategy: any;
private readonly signInResolver?: SignInResolver<OAuthResult>;
private readonly authHandler: AuthHandler<OAuthResult>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
constructor(options: Options) {
this.signInResolver = options.signInResolver;
this.authHandler = options.authHandler;
this.tokenIssuer = options.tokenIssuer;
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
this._strategy = new OneLoginStrategy(
{
issuer: options.issuer,
@@ -97,18 +120,8 @@ export class OneLoginProvider implements OAuthHandlers {
PrivateInfo
>(req, this._strategy);
const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
return {
response: await this.populateIdentity({
profile,
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
}),
response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
@@ -124,51 +137,117 @@ export class OneLoginProvider implements OAuthHandlers {
this._strategy,
accessToken,
);
const profile = makeProfileInfo(fullProfile, params.id_token);
return this.populateIdentity({
providerInfo: {
accessToken,
idToken: params.id_token,
expiresInSeconds: params.expires_in,
scope: params.scope,
},
profile,
return this.handleResult({
fullProfile,
params,
accessToken,
});
}
private async populateIdentity(
response: OAuthResponse,
): Promise<OAuthResponse> {
const { profile } = response;
private async handleResult(result: OAuthResult) {
const { profile } = await this.authHandler(result);
if (!profile.email) {
throw new Error('OIDC profile contained no email');
const response: OAuthResponse = {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
profile,
};
if (this.signInResolver) {
response.backstageIdentity = await this.signInResolver(
{
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
);
}
const id = profile.email.split('@')[0];
return { ...response, backstageIdentity: { id, token: '' } };
return response;
}
}
export type OneLoginProviderOptions = {};
const defaultSignInResolver: SignInResolver<OAuthResult> = async info => {
const { profile } = info;
if (!profile.email) {
throw new Error('OIDC profile contained no email');
}
const id = profile.email.split('@')[0];
return { id, token: '' };
};
/** @public */
export type OneLoginProviderOptions = {
/**
* The profile transformation function used to verify and convert the auth response
* into the profile that will be presented to the user.
*/
authHandler?: AuthHandler<OAuthResult>;
/**
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
*/
signIn?: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<OAuthResult>;
};
};
/** @public */
export const createOneLoginProvider = (
_options?: OneLoginProviderOptions,
options?: OneLoginProviderOptions,
): AuthProviderFactory => {
return ({ providerId, globalConfig, config, tokenIssuer }) =>
return ({
providerId,
globalConfig,
config,
tokenIssuer,
catalogApi,
logger,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const issuer = envConfig.getString('issuer');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
? options.authHandler
: async ({ fullProfile, params }) => ({
profile: makeProfileInfo(fullProfile, params.id_token),
});
const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver;
const provider = new OneLoginProvider({
clientId,
clientSecret,
callbackUrl,
issuer,
authHandler,
signInResolver,
tokenIssuer,
catalogIdentityClient,
logger,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {