Auth handler and sign in resolvers for oidc provider

Signed-off-by: Hasan Ozdemir <21654050+nodify-at@users.noreply.github.com>
This commit is contained in:
Hasan Ozdemir
2021-12-01 23:54:12 +01:00
parent 4d35309170
commit 36fa32216f
6 changed files with 152 additions and 49 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Added signIn and authHandler resolver for oAuth2 provider
+10
View File
@@ -14,7 +14,9 @@ import { Logger as Logger_2 } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { Profile } from 'passport';
import { TokenSet } from 'openid-client';
import { UserEntity } from '@backstage/catalog-model';
import { UserinfoResponse } from 'openid-client';
// Warning: (ae-missing-release-tag) "AtlassianAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -245,6 +247,14 @@ export const createOAuth2Provider: (
options?: OAuth2ProviderOptions | undefined,
) => AuthProviderFactory;
// Warning: (ae-forgotten-export) The symbol "OidcProviderOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "createOidcProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const createOidcProvider: (
options?: OidcProviderOptions | undefined,
) => AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createOktaProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -19,6 +19,7 @@ export * from './gitlab';
export * from './google';
export * from './microsoft';
export * from './oauth2';
export * from './oidc';
export * from './okta';
export * from './bitbucket';
export * from './atlassian';
@@ -15,4 +15,3 @@
*/
export { createOidcProvider } from './provider';
export type { OidcProviderOptions } from './provider';
@@ -24,7 +24,8 @@ import { setupServer } from 'msw/node';
import { ClientMetadata, IssuerMetadata } from 'openid-client';
import { OAuthAdapter } from '../../lib/oauth';
import { AuthProviderFactoryOptions } from '../types';
import { createOidcProvider, OidcAuthProvider } from './provider';
import { createOidcProvider, OidcAuthProvider, Options } from './provider';
import { getVoidLogger } from '@backstage/backend-common';
const issuerMetadata = {
issuer: 'https://oidc.test',
@@ -42,7 +43,23 @@ const issuerMetadata = {
request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'],
};
const clientMetadata = {
const catalogIdentityClient = {
findUser: jest.fn(),
};
const tokenIssuer = {
issueToken: jest.fn(),
listPublicKeys: jest.fn(),
};
const clientMetadata: Options = {
authHandler: async input => ({
profile: {
displayName: input.userinfo.email,
},
}),
catalogIdentityClient: catalogIdentityClient as unknown as any,
logger: getVoidLogger(),
tokenIssuer: tokenIssuer as unknown as any,
callbackUrl: 'https://oidc.test/callback',
clientId: 'testclientid',
clientSecret: 'testclientsecret',
@@ -160,7 +177,7 @@ describe('OidcAuthProvider', () => {
...clientMetadata,
metadataUrl: 'https://oidc.test/.well-known/openid-configuration',
},
});
} as any);
const options = {
globalConfig: {
appUrl: 'https://oidc.test',
@@ -16,28 +16,37 @@
import express from 'express';
import {
Issuer,
Client,
Issuer,
Strategy as OidcStrategy,
TokenSet,
UserinfoResponse,
} from 'openid-client';
import {
OAuthAdapter,
OAuthProviderOptions,
OAuthHandlers,
OAuthResponse,
OAuthEnvironmentHandler,
OAuthStartRequest,
encodeState,
OAuthAdapter,
OAuthEnvironmentHandler,
OAuthHandlers,
OAuthProviderOptions,
OAuthRefreshRequest,
OAuthResponse,
OAuthStartRequest,
} from '../../lib/oauth';
import {
executeFrameHandlerStrategy,
executeRedirectStrategy,
makeProfileInfo,
PassportDoneCallback,
} from '../../lib/passport';
import { RedirectInfo, AuthProviderFactory } from '../types';
import {
AuthHandler,
AuthProviderFactory,
RedirectInfo,
SignInResolver,
} from '../types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { TokenIssuer } from '../../identity';
import { Logger } from 'winston';
type PrivateInfo = {
refreshToken?: string;
@@ -58,6 +67,11 @@ export type Options = OAuthProviderOptions & {
scope?: string;
prompt?: string;
tokenSignedResponseAlg?: string;
signInResolver?: SignInResolver<AuthResult>;
authHandler: AuthHandler<AuthResult>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
};
export class OidcAuthProvider implements OAuthHandlers {
@@ -65,10 +79,21 @@ export class OidcAuthProvider implements OAuthHandlers {
private readonly scope?: string;
private readonly prompt?: string;
private readonly signInResolver?: SignInResolver<AuthResult>;
private readonly authHandler: AuthHandler<AuthResult>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
constructor(options: Options) {
this.implementation = this.setupStrategy(options);
this.scope = options.scope;
this.prompt = options.prompt;
this.signInResolver = options.signInResolver;
this.authHandler = options.authHandler;
this.tokenIssuer = options.tokenIssuer;
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
@@ -97,19 +122,8 @@ export class OidcAuthProvider implements OAuthHandlers {
result: { userinfo, tokenset },
privateInfo,
} = strategyResponse;
const identityResponse = await this.populateIdentity({
profile: {
displayName: userinfo.name,
email: userinfo.email,
picture: userinfo.picture,
},
providerInfo: {
idToken: tokenset.id_token,
accessToken: tokenset.access_token || '',
scope: tokenset.scope || '',
expiresInSeconds: tokenset.expires_in,
},
});
const identityResponse = await this.handleResult({ tokenset, userinfo });
return {
response: identityResponse,
refreshToken: privateInfo.refreshToken,
@@ -123,17 +137,7 @@ export class OidcAuthProvider implements OAuthHandlers {
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,
});
return this.handleResult({ tokenset, userinfo: profile });
}
private async setupStrategy(options: Options): Promise<OidcImpl> {
@@ -177,26 +181,70 @@ export class OidcAuthProvider implements OAuthHandlers {
// 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;
if (!profile.email) {
throw new Error('Profile does not contain an email');
private async handleResult(result: AuthResult): Promise<OAuthResponse> {
const { profile } = await this.authHandler(result);
const response: OAuthResponse = {
providerInfo: {
idToken: result.tokenset.id_token,
accessToken: result.tokenset.access_token!,
refreshToken: result.tokenset.refresh_token,
scope: result.tokenset.scope!,
expiresInSeconds: result.tokenset.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 } };
return response;
}
}
export type OidcProviderOptions = {};
export const oAuth2DefaultSignInResolver: SignInResolver<AuthResult> = async (
info,
ctx,
) => {
const { profile } = info;
if (!profile.email) {
throw new Error('Profile contained no email');
}
const userId = profile.email.split('@')[0];
const token = await ctx.tokenIssuer.issueToken({
claims: { sub: userId, ent: [`user:default/${userId}`] },
});
return { id: userId, token };
};
export type OidcProviderOptions = {
authHandler?: AuthHandler<AuthResult>;
signIn?: {
resolver?: SignInResolver<AuthResult>;
};
};
export const createOidcProvider = (
_options?: OidcProviderOptions,
options?: OidcProviderOptions,
): 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');
@@ -207,6 +255,24 @@ export const createOidcProvider = (
);
const scope = envConfig.getOptionalString('scope');
const prompt = envConfig.getOptionalString('prompt');
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
const authHandler: AuthHandler<AuthResult> = options?.authHandler
? options.authHandler
: async ({ userinfo, tokenset }) => ({
profile: makeProfileInfo({ ...userinfo } as any, tokenset.id_token), // todo some required values does not exist in oidc response
});
const signInResolverFn =
options?.signIn?.resolver ?? oAuth2DefaultSignInResolver;
const signInResolver: SignInResolver<AuthResult> = info =>
signInResolverFn(info, {
catalogIdentityClient,
tokenIssuer,
logger,
});
const provider = new OidcAuthProvider({
clientId,
@@ -216,6 +282,11 @@ export const createOidcProvider = (
metadataUrl,
scope,
prompt,
signInResolver,
authHandler,
logger,
tokenIssuer,
catalogIdentityClient,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {