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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Added signIn and authHandler resolver for oAuth2 provider
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user