authHandler/signInResolver backwards compatibility
Signed-off-by: Jamie Klassen <jamie.klassen@broadcom.com>
This commit is contained in:
@@ -7,10 +7,10 @@ import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { BaseClient } from 'openid-client';
|
||||
import { OAuthAuthenticator } from '@backstage/plugin-auth-node';
|
||||
import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node';
|
||||
import { PassportOAuthResult } from '@backstage/plugin-auth-node';
|
||||
import { PassportProfile } from '@backstage/plugin-auth-node';
|
||||
import { SignInResolverFactory } from '@backstage/plugin-auth-node';
|
||||
import { Strategy } from 'openid-client';
|
||||
import { TokenSet } from 'openid-client';
|
||||
import { UserinfoResponse } from 'openid-client';
|
||||
|
||||
// @public (undocumented)
|
||||
const authModuleOidcProvider: () => BackendFeature;
|
||||
@@ -18,16 +18,24 @@ export default authModuleOidcProvider;
|
||||
|
||||
// @public (undocumented)
|
||||
export const oidcAuthenticator: OAuthAuthenticator<
|
||||
Promise<{
|
||||
helper: PassportOAuthAuthenticatorHelper;
|
||||
client: BaseClient;
|
||||
{
|
||||
initializedScope: string | undefined;
|
||||
initializedPrompt: string | undefined;
|
||||
strategy: Strategy<PassportOAuthResult, BaseClient>;
|
||||
}>,
|
||||
PassportProfile
|
||||
promise: Promise<{
|
||||
helper: PassportOAuthAuthenticatorHelper;
|
||||
client: BaseClient;
|
||||
strategy: Strategy<OidcAuthResult, BaseClient>;
|
||||
}>;
|
||||
},
|
||||
OidcAuthResult
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type OidcAuthResult = {
|
||||
tokenset: TokenSet;
|
||||
userinfo: UserinfoResponse;
|
||||
};
|
||||
|
||||
// @public
|
||||
export namespace oidcSignInResolvers {
|
||||
const emailLocalPartMatchingUserEntityName: SignInResolverFactory<
|
||||
|
||||
@@ -281,81 +281,75 @@ describe('oidcAuthenticator', () => {
|
||||
});
|
||||
|
||||
it('exchanges authorization code for access token', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
const authenticatorResult = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const accessToken = handlerResponse.session.accessToken;
|
||||
const accessToken = authenticatorResult.session.accessToken;
|
||||
|
||||
expect(accessToken).toEqual('accessToken');
|
||||
});
|
||||
|
||||
it('exchanges authorization code for refresh token', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
const authenticatorResult = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const refreshToken = handlerResponse.session.refreshToken;
|
||||
const refreshToken = authenticatorResult.session.refreshToken;
|
||||
|
||||
expect(refreshToken).toEqual('refreshToken');
|
||||
});
|
||||
|
||||
it('returns granted scope', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
const authenticatorResult = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const responseScope = handlerResponse.session.scope;
|
||||
const responseScope = authenticatorResult.session.scope;
|
||||
|
||||
expect(responseScope).toEqual('testScope');
|
||||
});
|
||||
|
||||
it('returns a default session.tokentype field', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
const authenticatorResult = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const tokenType = handlerResponse.session.tokenType;
|
||||
const tokenType = authenticatorResult.session.tokenType;
|
||||
|
||||
expect(tokenType).toEqual('bearer');
|
||||
});
|
||||
|
||||
it('returns defined fullProfile with picture and email', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
it('returns picture and email', async () => {
|
||||
const authenticatorResult = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const displayName = handlerResponse.fullProfile.displayName;
|
||||
const email = handlerResponse.fullProfile.emails![0].value;
|
||||
const picture = handlerResponse.fullProfile.photos![0].value;
|
||||
|
||||
expect(displayName).toEqual('Alice Adams');
|
||||
expect(email).toEqual('alice@test.com');
|
||||
expect(picture).toEqual('http://testPictureUrl/photo.jpg');
|
||||
expect(authenticatorResult).toMatchObject({
|
||||
fullProfile: {
|
||||
userinfo: {
|
||||
email: 'alice@test.com',
|
||||
picture: 'http://testPictureUrl/photo.jpg',
|
||||
name: 'Alice Adams',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns defined response with an idToken', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
it('returns idToken', async () => {
|
||||
const authenticatorResult = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
|
||||
expect(handlerResponse).toMatchObject({
|
||||
fullProfile: {
|
||||
displayName: 'Alice Adams',
|
||||
id: 'test',
|
||||
provider: 'oidc',
|
||||
},
|
||||
expect(authenticatorResult).toMatchObject({
|
||||
session: {
|
||||
accessToken: 'accessToken',
|
||||
idToken,
|
||||
refreshToken: 'refreshToken',
|
||||
scope: 'testScope',
|
||||
tokenType: 'bearer',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
Math.abs(handlerResponse.session.expiresInSeconds! - 3600),
|
||||
Math.abs(authenticatorResult.session.expiresInSeconds! - 3600),
|
||||
).toBeLessThan(5);
|
||||
});
|
||||
|
||||
|
||||
@@ -23,15 +23,35 @@ import {
|
||||
} from 'openid-client';
|
||||
import {
|
||||
createOAuthAuthenticator,
|
||||
OAuthAuthenticatorResult,
|
||||
PassportDoneCallback,
|
||||
PassportHelpers,
|
||||
PassportOAuthAuthenticatorHelper,
|
||||
PassportOAuthDoneCallback,
|
||||
PassportOAuthPrivateInfo,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
/**
|
||||
* authentication result for the OIDC which includes the token set and user
|
||||
* profile response
|
||||
* @public
|
||||
*/
|
||||
export type OidcAuthResult = {
|
||||
tokenset: TokenSet;
|
||||
userinfo: UserinfoResponse;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
async initialize({ callbackUrl, config }) {
|
||||
defaultProfileTransform: async (
|
||||
input: OAuthAuthenticatorResult<OidcAuthResult>,
|
||||
) => ({
|
||||
profile: {
|
||||
email: input.fullProfile.userinfo.email,
|
||||
picture: input.fullProfile.userinfo.picture,
|
||||
displayName: input.fullProfile.userinfo.name,
|
||||
},
|
||||
}),
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const metadataUrl = config.getString('metadataUrl');
|
||||
@@ -45,81 +65,53 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
const initializedScope = config.getOptionalString('scope');
|
||||
const initializedPrompt = config.getOptionalString('prompt');
|
||||
|
||||
const issuer = await Issuer.discover(metadataUrl);
|
||||
const client = new issuer.Client({
|
||||
access_type: 'offline', // this option must be passed to provider to receive a refresh token
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uris: [customCallbackUrl || callbackUrl],
|
||||
response_types: ['code'],
|
||||
token_endpoint_auth_method:
|
||||
tokenEndpointAuthMethod || 'client_secret_basic',
|
||||
id_token_signed_response_alg: tokenSignedResponseAlg || 'RS256',
|
||||
scope: initializedScope || '',
|
||||
const promise = Issuer.discover(metadataUrl).then(issuer => {
|
||||
const client = new issuer.Client({
|
||||
access_type: 'offline', // this option must be passed to provider to receive a refresh token
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uris: [customCallbackUrl || callbackUrl],
|
||||
response_types: ['code'],
|
||||
token_endpoint_auth_method:
|
||||
tokenEndpointAuthMethod || 'client_secret_basic',
|
||||
id_token_signed_response_alg: tokenSignedResponseAlg || 'RS256',
|
||||
scope: initializedScope || '',
|
||||
});
|
||||
|
||||
const strategy = new OidcStrategy(
|
||||
{
|
||||
client,
|
||||
passReqToCallback: false,
|
||||
},
|
||||
(
|
||||
tokenset: TokenSet,
|
||||
userinfo: UserinfoResponse,
|
||||
done: PassportDoneCallback<OidcAuthResult, PassportOAuthPrivateInfo>,
|
||||
) => {
|
||||
if (typeof done !== 'function') {
|
||||
throw new Error(
|
||||
'OIDC IdP must provide a userinfo_endpoint in the metadata response',
|
||||
);
|
||||
}
|
||||
|
||||
done(
|
||||
undefined,
|
||||
{ tokenset, userinfo },
|
||||
{ refreshToken: tokenset.refresh_token },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const helper = PassportOAuthAuthenticatorHelper.from(strategy);
|
||||
return { helper, client, strategy };
|
||||
});
|
||||
|
||||
const strategy = new OidcStrategy(
|
||||
{
|
||||
client,
|
||||
passReqToCallback: false,
|
||||
},
|
||||
(
|
||||
tokenset: TokenSet,
|
||||
userinfo: UserinfoResponse,
|
||||
done: PassportOAuthDoneCallback,
|
||||
) => {
|
||||
if (typeof done !== 'function') {
|
||||
throw new Error(
|
||||
'OIDC IdP must provide a userinfo_endpoint in the metadata response',
|
||||
);
|
||||
}
|
||||
|
||||
const emails = userinfo.email ? [{ value: userinfo.email }] : undefined;
|
||||
const photos = userinfo.picture
|
||||
? [{ value: userinfo.picture }]
|
||||
: undefined;
|
||||
const name =
|
||||
userinfo.family_name && userinfo.given_name
|
||||
? {
|
||||
familyName: userinfo.family_name,
|
||||
givenName: userinfo.given_name,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
done(
|
||||
undefined,
|
||||
{
|
||||
fullProfile: {
|
||||
provider: 'oidc',
|
||||
id: userinfo.sub,
|
||||
displayName: userinfo.name!,
|
||||
username: userinfo.preferred_username,
|
||||
name,
|
||||
emails,
|
||||
photos,
|
||||
},
|
||||
accessToken: tokenset.access_token!,
|
||||
params: {
|
||||
id_token: tokenset.id_token,
|
||||
scope: tokenset.scope!,
|
||||
expires_in: tokenset.expires_in!,
|
||||
token_type: tokenset.token_type,
|
||||
},
|
||||
},
|
||||
{
|
||||
refreshToken: tokenset.refresh_token,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const helper = PassportOAuthAuthenticatorHelper.from(strategy);
|
||||
|
||||
return { helper, client, initializedScope, initializedPrompt, strategy };
|
||||
return { initializedScope, initializedPrompt, promise };
|
||||
},
|
||||
|
||||
async start(input, ctx) {
|
||||
const { initializedScope, initializedPrompt, helper, strategy } = await ctx;
|
||||
const { initializedScope, initializedPrompt, promise } = ctx;
|
||||
const { helper, strategy } = await promise;
|
||||
const options: Record<string, string> = {
|
||||
scope: input.scope || initializedScope || 'openid profile email',
|
||||
state: input.state,
|
||||
@@ -140,12 +132,32 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
});
|
||||
},
|
||||
|
||||
async authenticate(input, ctx) {
|
||||
return (await ctx).helper.authenticate(input);
|
||||
async authenticate(
|
||||
input,
|
||||
ctx,
|
||||
): Promise<OAuthAuthenticatorResult<OidcAuthResult>> {
|
||||
const { strategy } = await ctx.promise;
|
||||
const { result, privateInfo } =
|
||||
await PassportHelpers.executeFrameHandlerStrategy<
|
||||
OidcAuthResult,
|
||||
PassportOAuthPrivateInfo
|
||||
>(input.req, strategy);
|
||||
|
||||
return {
|
||||
fullProfile: result,
|
||||
session: {
|
||||
accessToken: result.tokenset.access_token!,
|
||||
tokenType: result.tokenset.token_type ?? 'bearer',
|
||||
scope: result.tokenset.scope!,
|
||||
expiresInSeconds: result.tokenset.expires_in,
|
||||
idToken: result.tokenset.id_token,
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async refresh(input, ctx) {
|
||||
const { client } = await ctx;
|
||||
const { client } = await ctx.promise;
|
||||
const tokenset = await client.refresh(input.refreshToken);
|
||||
if (!tokenset.access_token) {
|
||||
throw new Error('Refresh failed');
|
||||
@@ -160,11 +172,7 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
reject(new Error('Refresh Failed'));
|
||||
}
|
||||
resolve({
|
||||
fullProfile: {
|
||||
provider: 'oidc',
|
||||
id: userinfo.sub,
|
||||
displayName: userinfo.name!,
|
||||
},
|
||||
fullProfile: { userinfo, tokenset },
|
||||
session: {
|
||||
accessToken: tokenset.access_token!,
|
||||
tokenType: tokenset.token_type ?? 'bearer',
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
*/
|
||||
|
||||
export { oidcAuthenticator } from './authenticator';
|
||||
export type { OidcAuthResult } from './authenticator';
|
||||
export { authModuleOidcProvider as default } from './module';
|
||||
export { oidcSignInResolvers } from './resolvers';
|
||||
|
||||
@@ -25,6 +25,7 @@ import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { OAuth2ProxyResult as OAuth2ProxyResult_2 } from '@backstage/plugin-auth-backend-module-oauth2-proxy-provider';
|
||||
import { OAuthEnvironmentHandler as OAuthEnvironmentHandler_2 } from '@backstage/plugin-auth-node';
|
||||
import { OAuthState as OAuthState_2 } from '@backstage/plugin-auth-node';
|
||||
import { OidcAuthResult } from '@backstage/plugin-auth-backend-module-oidc-provider';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { prepareBackstageIdentityResponse as prepareBackstageIdentityResponse_2 } from '@backstage/plugin-auth-node';
|
||||
@@ -34,9 +35,7 @@ import { SignInInfo as SignInInfo_2 } from '@backstage/plugin-auth-node';
|
||||
import { SignInResolver as SignInResolver_2 } from '@backstage/plugin-auth-node';
|
||||
import { TokenManager } from '@backstage/backend-common';
|
||||
import { TokenParams as TokenParams_2 } from '@backstage/plugin-auth-node';
|
||||
import { TokenSet } from 'openid-client';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { UserinfoResponse } from 'openid-client';
|
||||
import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-auth-node';
|
||||
|
||||
// @public @deprecated
|
||||
@@ -340,12 +339,6 @@ export type OAuthStartResponse = {
|
||||
// @public @deprecated (undocumented)
|
||||
export type OAuthState = OAuthState_2;
|
||||
|
||||
// @public @deprecated
|
||||
export type OidcAuthResult = {
|
||||
tokenset: TokenSet;
|
||||
userinfo: UserinfoResponse;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const postMessageResponse: (
|
||||
res: express.Response,
|
||||
@@ -564,10 +557,10 @@ export const providers: Readonly<{
|
||||
create: (
|
||||
options?:
|
||||
| {
|
||||
authHandler?: AuthHandler<OAuthResult> | undefined;
|
||||
authHandler?: AuthHandler<OidcAuthResult> | undefined;
|
||||
signIn?:
|
||||
| {
|
||||
resolver: SignInResolver<OAuthResult>;
|
||||
resolver: SignInResolver<OidcAuthResult>;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"passport-auth0": "^1.4.3",
|
||||
"passport-bitbucket-oauth2": "^0.1.2",
|
||||
"passport-github2": "^0.1.12",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-microsoft": "^1.0.0",
|
||||
"passport-oauth2": "^1.6.1",
|
||||
|
||||
@@ -29,7 +29,6 @@ export type {
|
||||
} from './cloudflare-access';
|
||||
export type { GithubOAuthResult } from './github';
|
||||
export type { OAuth2ProxyResult } from './oauth2-proxy';
|
||||
export type { OidcAuthResult } from './oidc';
|
||||
export type { SamlAuthResult } from './saml';
|
||||
export type { GcpIapResult, GcpIapTokenInfo } from './gcp-iap';
|
||||
|
||||
|
||||
@@ -15,4 +15,3 @@
|
||||
*/
|
||||
|
||||
export { oidc } from './provider';
|
||||
export type { OidcAuthResult } from './provider';
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
AuthResolverContext,
|
||||
CookieConfigurer,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import express from 'express';
|
||||
import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { oidc } from './provider';
|
||||
|
||||
describe('oidc.create', () => {
|
||||
const userinfo = {
|
||||
sub: 'test',
|
||||
iss: 'https://oidc.test',
|
||||
aud: 'clientId',
|
||||
nonce: 'foo',
|
||||
};
|
||||
const server = setupServer();
|
||||
setupRequestMockHandlers(server);
|
||||
|
||||
let publicKey: JWK;
|
||||
let tokenset: object;
|
||||
let providerFactoryOptions: {
|
||||
providerId: string;
|
||||
globalConfig: AuthProviderConfig;
|
||||
config: Config;
|
||||
logger: LoggerService;
|
||||
resolverContext: AuthResolverContext;
|
||||
baseUrl: string;
|
||||
appUrl: string;
|
||||
isOriginAllowed: (origin: string) => boolean;
|
||||
cookieConfigurer?: CookieConfigurer;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const keyPair = await generateKeyPair('RS256');
|
||||
const privateKey = await exportJWK(keyPair.privateKey);
|
||||
publicKey = await exportJWK(keyPair.publicKey);
|
||||
publicKey.alg = privateKey.alg = 'RS256';
|
||||
|
||||
tokenset = {
|
||||
id_token: await new SignJWT({
|
||||
iat: Date.now(),
|
||||
exp: Date.now() + 10000,
|
||||
...userinfo,
|
||||
})
|
||||
.setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid })
|
||||
.sign(keyPair.privateKey),
|
||||
access_token: 'accessToken',
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(
|
||||
'https://oidc.test/.well-known/openid-configuration',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
issuer: 'https://oidc.test',
|
||||
token_endpoint: 'https://oidc.test/oauth2/token',
|
||||
userinfo_endpoint: 'https://oidc.test/idp/userinfo.openid',
|
||||
jwks_uri: 'https://oidc.test/jwks.json',
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.post('https://oidc.test/oauth2/token', (_req, res, ctx) =>
|
||||
res(ctx.json(tokenset)),
|
||||
),
|
||||
rest.get('https://oidc.test/jwks.json', async (_req, res, ctx) =>
|
||||
res(ctx.json({ keys: [{ ...publicKey }] })),
|
||||
),
|
||||
rest.get(
|
||||
'https://oidc.test/idp/userinfo.openid',
|
||||
async (_req, res, ctx) => res(ctx.json(userinfo)),
|
||||
),
|
||||
);
|
||||
providerFactoryOptions = {
|
||||
providerId: 'myoidc',
|
||||
baseUrl: 'http://backstage.test/api/auth',
|
||||
appUrl: 'http://backstage.test',
|
||||
isOriginAllowed: _ => true,
|
||||
globalConfig: {
|
||||
baseUrl: 'http://backstage.test/api/auth',
|
||||
appUrl: 'http://backstage.test',
|
||||
isOriginAllowed: _ => true,
|
||||
},
|
||||
config: new ConfigReader({
|
||||
development: {
|
||||
metadataUrl: 'https://oidc.test/.well-known/openid-configuration',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
}),
|
||||
logger: getVoidLogger(),
|
||||
resolverContext: {
|
||||
issueToken: jest.fn(),
|
||||
findCatalogUser: jest.fn(),
|
||||
signInWithCatalogUser: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('invokes authHandler with tokenset and userinfo response', async () => {
|
||||
const authHandler = jest.fn();
|
||||
const provider = oidc.create({ authHandler })(providerFactoryOptions);
|
||||
const state = Buffer.from('nonce=foo&env=development').toString('hex');
|
||||
|
||||
await provider.frameHandler(
|
||||
{
|
||||
method: 'GET',
|
||||
url: `http://backstage.test/api/auth/myoidc/handler/frame?code=blahblah&state=${state}`,
|
||||
query: { state },
|
||||
cookies: { 'myoidc-nonce': 'foo' },
|
||||
session: { 'oidc:oidc.test': { state, nonce: 'foo' } },
|
||||
} as unknown as express.Request,
|
||||
{ setHeader: jest.fn(), end: jest.fn() } as unknown as express.Response,
|
||||
);
|
||||
|
||||
expect(authHandler).toHaveBeenCalledWith(
|
||||
{ tokenset, userinfo },
|
||||
providerFactoryOptions.resolverContext,
|
||||
);
|
||||
});
|
||||
|
||||
it('invokes sign-in resolver with tokenset and userinfo response', async () => {
|
||||
const resolver = jest.fn();
|
||||
const provider = oidc.create({ signIn: { resolver } })(
|
||||
providerFactoryOptions,
|
||||
);
|
||||
const state = Buffer.from('nonce=foo&env=development').toString('hex');
|
||||
|
||||
await provider.frameHandler(
|
||||
{
|
||||
method: 'GET',
|
||||
url: `http://backstage.test/api/auth/myoidc/handler/frame?code=blahblah&state=${state}`,
|
||||
query: { state },
|
||||
cookies: { 'myoidc-nonce': 'foo' },
|
||||
session: { 'oidc:oidc.test': { state, nonce: 'foo' } },
|
||||
} as unknown as express.Request,
|
||||
{ setHeader: jest.fn(), end: jest.fn() } as unknown as express.Response,
|
||||
);
|
||||
|
||||
expect(resolver).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ result: { tokenset, userinfo } }),
|
||||
providerFactoryOptions.resolverContext,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -15,30 +15,23 @@
|
||||
*/
|
||||
|
||||
import { AuthHandler, SignInResolver } from '../types';
|
||||
import { OAuthResult } from '../../lib/oauth';
|
||||
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
import { createOAuthProviderFactory } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
adaptLegacyOAuthHandler,
|
||||
adaptLegacyOAuthSignInResolver,
|
||||
} from '../../lib/legacy';
|
||||
import { oidcAuthenticator } from '@backstage/plugin-auth-backend-module-oidc-provider';
|
||||
import { TokenSet, UserinfoResponse } from 'openid-client';
|
||||
createOAuthProviderFactory,
|
||||
AuthResolverContext,
|
||||
BackstageSignInResult,
|
||||
OAuthAuthenticatorResult,
|
||||
SignInInfo,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
oidcAuthenticator,
|
||||
OidcAuthResult,
|
||||
} from '@backstage/plugin-auth-backend-module-oidc-provider';
|
||||
import {
|
||||
commonByEmailLocalPartResolver,
|
||||
commonByEmailResolver,
|
||||
} from '../resolvers';
|
||||
|
||||
/**
|
||||
* authentication result for the OIDC which includes the token set and user information (a profile response sent by OIDC server)
|
||||
* @public
|
||||
* @deprecated No longer used
|
||||
*/
|
||||
export type OidcAuthResult = {
|
||||
tokenset: TokenSet;
|
||||
userinfo: UserinfoResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* Auth provider integration for generic OpenID Connect auth
|
||||
*
|
||||
@@ -50,19 +43,39 @@ export const oidc = createAuthProviderIntegration({
|
||||
* 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>;
|
||||
authHandler?: AuthHandler<OidcAuthResult>;
|
||||
|
||||
/**
|
||||
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
|
||||
* Configure sign-in for this provider; convert user profile respones into
|
||||
* Backstage identities.
|
||||
*/
|
||||
signIn?: {
|
||||
resolver: SignInResolver<OAuthResult>;
|
||||
resolver: SignInResolver<OidcAuthResult>;
|
||||
};
|
||||
}) {
|
||||
const authHandler = options?.authHandler;
|
||||
const signInResolver = options?.signIn?.resolver;
|
||||
return createOAuthProviderFactory({
|
||||
authenticator: oidcAuthenticator,
|
||||
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
||||
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver),
|
||||
profileTransform:
|
||||
authHandler &&
|
||||
((
|
||||
result: OAuthAuthenticatorResult<OidcAuthResult>,
|
||||
context: AuthResolverContext,
|
||||
) => authHandler(result.fullProfile, context)),
|
||||
signInResolver:
|
||||
signInResolver &&
|
||||
((
|
||||
info: SignInInfo<OAuthAuthenticatorResult<OidcAuthResult>>,
|
||||
context: AuthResolverContext,
|
||||
): Promise<BackstageSignInResult> =>
|
||||
signInResolver(
|
||||
{
|
||||
result: info.result.fullProfile,
|
||||
profile: info.profile,
|
||||
},
|
||||
context,
|
||||
)),
|
||||
});
|
||||
},
|
||||
resolvers: {
|
||||
|
||||
Reference in New Issue
Block a user