authenticator alongside passing unit tests
Signed-off-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
committed by
Jamie Klassen
parent
55c639839a
commit
a3c911e636
+4
-1
@@ -25,8 +25,11 @@ export interface Config {
|
||||
* @visibility secret
|
||||
*/
|
||||
clientSecret: string;
|
||||
audience?: string;
|
||||
metadataUrl: string;
|
||||
callbackUrl?: string;
|
||||
tokenSignedResponseAlg?: string;
|
||||
scope?: string;
|
||||
prompt?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
OAuthAuthenticatorAuthenticateInput,
|
||||
OAuthAuthenticatorRefreshInput,
|
||||
OAuthAuthenticatorStartInput,
|
||||
OAuthState,
|
||||
decodeOAuthState,
|
||||
encodeOAuthState,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { oidcAuthenticator } from './authenticator';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose';
|
||||
import { rest } from 'msw';
|
||||
import express from 'express';
|
||||
|
||||
describe('oidcAuthenticator', () => {
|
||||
let implementation: any;
|
||||
let oauthState: OAuthState;
|
||||
let idToken: string;
|
||||
let publicKey: JWK;
|
||||
|
||||
const mswServer = setupServer();
|
||||
setupRequestMockHandlers(mswServer);
|
||||
|
||||
const issuerMetadata = {
|
||||
issuer: 'https://oidc.test',
|
||||
authorization_endpoint: 'https://oidc.test/oauth2/authorize',
|
||||
token_endpoint: 'https://oidc.test/oauth2/token',
|
||||
revocation_endpoint: 'https://oidc.test/oauth2/revoke_token',
|
||||
userinfo_endpoint: 'https://oidc.test/idp/userinfo.openid',
|
||||
introspection_endpoint: 'https://oidc.test/introspect.oauth2',
|
||||
jwks_uri: 'https://oidc.test/jwks.json',
|
||||
scopes_supported: [
|
||||
'openid',
|
||||
'offline_access',
|
||||
'oidc:request-audience',
|
||||
'username',
|
||||
'groups',
|
||||
],
|
||||
claims_supported: ['email', 'username', 'groups', 'additionalClaims'],
|
||||
response_types_supported: ['code'],
|
||||
id_token_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'],
|
||||
token_endpoint_auth_signing_alg_values_supported: [
|
||||
'RS256',
|
||||
'RS512',
|
||||
'HS256',
|
||||
],
|
||||
request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'],
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const keyPair = await generateKeyPair('RS256');
|
||||
const privateKey = await exportJWK(keyPair.privateKey);
|
||||
publicKey = await exportJWK(keyPair.publicKey);
|
||||
publicKey.alg = privateKey.alg = 'RS256';
|
||||
|
||||
idToken = await new SignJWT({
|
||||
sub: 'test',
|
||||
iss: 'https://oidc.test',
|
||||
iat: Date.now(),
|
||||
aud: 'clientId',
|
||||
exp: Date.now() + 10000,
|
||||
})
|
||||
.setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid })
|
||||
.sign(keyPair.privateKey);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mswServer.use(
|
||||
rest.get(
|
||||
'https://oidc.test/.well-known/openid-configuration',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(issuerMetadata),
|
||||
),
|
||||
),
|
||||
rest.get('https://oidc.test/jwks.json', async (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })),
|
||||
),
|
||||
rest.post('https://oidc.test/oauth2/token', async (req, res, ctx) => {
|
||||
return res(
|
||||
req.headers.get('Authorization')
|
||||
? ctx.json({
|
||||
access_token: 'accessToken',
|
||||
id_token: idToken,
|
||||
refresh_token: 'refreshToken',
|
||||
scope: 'testScope',
|
||||
token_type: '',
|
||||
})
|
||||
: ctx.status(401),
|
||||
);
|
||||
}),
|
||||
rest.get(
|
||||
'https://oidc.test/idp/userinfo.openid',
|
||||
async (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
sub: 'test',
|
||||
name: 'Alice Adams',
|
||||
given_name: 'Alice',
|
||||
family_name: 'Adams',
|
||||
email: 'alice@test.com',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
implementation = oidcAuthenticator.initialize({
|
||||
callbackUrl: 'https://backstage.test/callback',
|
||||
config: new ConfigReader({
|
||||
metadataUrl: 'https://oidc.test',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
}),
|
||||
});
|
||||
|
||||
oauthState = {
|
||||
nonce: 'nonce',
|
||||
env: 'env',
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
let fakeSession: Record<string, any>;
|
||||
let startRequest: OAuthAuthenticatorStartInput;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeSession = {};
|
||||
startRequest = {
|
||||
state: encodeOAuthState(oauthState),
|
||||
req: {
|
||||
method: 'GET',
|
||||
url: 'test',
|
||||
session: fakeSession,
|
||||
},
|
||||
} as unknown as OAuthAuthenticatorStartInput;
|
||||
});
|
||||
|
||||
it('redirects to authorization endpoint returned from OIDC metadata endpoint', async () => {
|
||||
const startResponse = await oidcAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const url = new URL(startResponse.url);
|
||||
|
||||
expect(url.protocol).toBe('https:');
|
||||
expect(url.hostname).toBe('oidc.test');
|
||||
expect(url.pathname).toBe('/oauth2/authorize');
|
||||
});
|
||||
|
||||
it('initiates authorization code grant', async () => {
|
||||
const startResponse = await oidcAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('response_type')).toBe('code');
|
||||
});
|
||||
|
||||
it('passes client ID from config', async () => {
|
||||
const startResponse = await oidcAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('client_id')).toBe('clientId');
|
||||
});
|
||||
|
||||
it('passes callback URL from config', async () => {
|
||||
const startResponse = await oidcAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('redirect_uri')).toBe(
|
||||
'https://backstage.test/callback',
|
||||
);
|
||||
});
|
||||
|
||||
it('generates PKCE challenge', async () => {
|
||||
const startResponse = await oidcAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('code_challenge_method')).toBe('S256');
|
||||
expect(searchParams.get('code_challenge')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('stores PKCE verifier in session', async () => {
|
||||
await oidcAuthenticator.start(startRequest, implementation);
|
||||
expect(fakeSession['oidc:oidc.test'].code_verifier).toBeDefined();
|
||||
});
|
||||
|
||||
// without the offline_access scope refresh should not work should we add in the test that is a required scope to test for? curently dont see that by defualt in the provider implementation.
|
||||
it('requests default scopes if none are provided in config', async () => {
|
||||
const startResponse = await oidcAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
const scopes = searchParams.get('scope')?.split(' ') ?? [];
|
||||
|
||||
expect(scopes).toEqual(
|
||||
expect.arrayContaining(['openid', 'profile', 'email']),
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes OAuth state in query param', async () => {
|
||||
const startResponse = await oidcAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
const stateParam = searchParams.get('state');
|
||||
const decodedState = decodeOAuthState(stateParam!);
|
||||
|
||||
expect(decodedState).toMatchObject(oauthState);
|
||||
});
|
||||
|
||||
it('fails when request has no session', async () => {
|
||||
return expect(
|
||||
oidcAuthenticator.start(
|
||||
{
|
||||
state: encodeOAuthState(oauthState),
|
||||
req: {
|
||||
method: 'GET',
|
||||
url: 'test',
|
||||
},
|
||||
} as unknown as OAuthAuthenticatorStartInput,
|
||||
implementation,
|
||||
),
|
||||
).rejects.toThrow('authentication requires session support');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#authenticate', () => {
|
||||
let handlerRequest: OAuthAuthenticatorAuthenticateInput;
|
||||
|
||||
beforeEach(() => {
|
||||
handlerRequest = {
|
||||
req: {
|
||||
method: 'GET',
|
||||
url: `https://test?code=authorization_code&state=${encodeOAuthState(
|
||||
oauthState,
|
||||
)}`,
|
||||
session: {
|
||||
'oidc:oidc.test': {
|
||||
state: encodeOAuthState(oauthState),
|
||||
},
|
||||
},
|
||||
} as unknown as express.Request,
|
||||
};
|
||||
});
|
||||
|
||||
it('exchanges authorization code for access token', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const accessToken = handlerResponse.session.accessToken;
|
||||
|
||||
expect(accessToken).toEqual('accessToken');
|
||||
});
|
||||
|
||||
it('exchanges authorization code for refresh token', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const refreshToken = handlerResponse.session.refreshToken;
|
||||
|
||||
expect(refreshToken).toEqual('refreshToken');
|
||||
});
|
||||
|
||||
it('returns granted scope', async () => {
|
||||
const handlerResponse = await oidcAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const responseScope = handlerResponse.session.scope;
|
||||
|
||||
expect(responseScope).toEqual('testScope');
|
||||
});
|
||||
|
||||
it('fails without authorization code', async () => {
|
||||
handlerRequest.req.url = 'https://test.com';
|
||||
return expect(
|
||||
oidcAuthenticator.authenticate(handlerRequest, implementation),
|
||||
).rejects.toThrow('Unexpected redirect');
|
||||
});
|
||||
|
||||
it('fails without oauth state', async () => {
|
||||
return expect(
|
||||
oidcAuthenticator.authenticate(
|
||||
{
|
||||
req: {
|
||||
method: 'GET',
|
||||
url: `https://test?code=authorization_code}`,
|
||||
session: {
|
||||
['oidc:pinniped.test']: {
|
||||
state: { handle: 'sessionid', code_verifier: 'foo' },
|
||||
},
|
||||
},
|
||||
} as unknown as express.Request,
|
||||
},
|
||||
implementation,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Authentication failed, did not find expected authorization request details in session, req.session["oidc:oidc.test"] is undefined',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when request has no session', async () => {
|
||||
return expect(
|
||||
oidcAuthenticator.authenticate(
|
||||
{
|
||||
req: {
|
||||
method: 'GET',
|
||||
url: 'https://test.com',
|
||||
} as unknown as express.Request,
|
||||
},
|
||||
implementation,
|
||||
),
|
||||
).rejects.toThrow('authentication requires session support');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#refresh', () => {
|
||||
let refreshRequest: OAuthAuthenticatorRefreshInput;
|
||||
|
||||
beforeEach(() => {
|
||||
refreshRequest = {
|
||||
scope: '',
|
||||
refreshToken: 'otherRefreshToken',
|
||||
req: {} as express.Request,
|
||||
};
|
||||
});
|
||||
|
||||
it('gets new refresh token', async () => {
|
||||
const refreshResponse = await oidcAuthenticator.refresh(
|
||||
refreshRequest,
|
||||
implementation,
|
||||
);
|
||||
|
||||
expect(refreshResponse.session.refreshToken).toBe('refreshToken');
|
||||
});
|
||||
|
||||
it('gets access token', async () => {
|
||||
const refreshResponse = await oidcAuthenticator.refresh(
|
||||
refreshRequest,
|
||||
implementation,
|
||||
);
|
||||
|
||||
expect(refreshResponse.session.accessToken).toBe('accessToken');
|
||||
});
|
||||
|
||||
it('gets id token', async () => {
|
||||
const refreshResponse = await oidcAuthenticator.refresh(
|
||||
refreshRequest,
|
||||
implementation,
|
||||
);
|
||||
|
||||
expect(refreshResponse.session.idToken).toBe(idToken);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,15 +23,10 @@ import {
|
||||
} from 'openid-client';
|
||||
import {
|
||||
createOAuthAuthenticator,
|
||||
decodeOAuthState,
|
||||
encodeOAuthState,
|
||||
PassportDoneCallback,
|
||||
PassportOAuthAuthenticatorHelper,
|
||||
PassportOAuthDoneCallback,
|
||||
PassportOAuthPrivateInfo,
|
||||
PassportProfile,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { OidcAuthResult } from '@backstage/plugin-auth-backend';
|
||||
// import { BACKSTAGE_SESSION_EXPIRATION } from '@backstage/plugin-auth-backend/src/lib/session';
|
||||
|
||||
/** @public */
|
||||
export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
@@ -40,9 +35,9 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
async initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const metadataUrl = config.getString('metadataUrl');
|
||||
const customCallbackUrl = config.getOptionalString('callbackUrl');
|
||||
const callbackUrl2 = customCallbackUrl || callbackUrl;
|
||||
const metadataUrl = config.getString('metadataUrl');
|
||||
const tokenEndpointAuthMethod = config.getOptionalString(
|
||||
'tokenEndpointAuthMethod',
|
||||
) as ClientAuthMethod;
|
||||
@@ -66,39 +61,54 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
scope: initializedScope || '',
|
||||
});
|
||||
|
||||
const helper = PassportOAuthAuthenticatorHelper.from(
|
||||
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 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 expiresInSeconds = !tokenset.expires_in
|
||||
? 3600
|
||||
: Math.min(tokenset.expires_in!, 3600);
|
||||
|
||||
done(
|
||||
undefined,
|
||||
{
|
||||
fullProfile: {
|
||||
provider: 'oidc',
|
||||
id: userinfo.sub,
|
||||
displayName: userinfo.name!,
|
||||
},
|
||||
accessToken: tokenset.access_token!,
|
||||
params: {
|
||||
scope: tokenset.scope!,
|
||||
expires_in: expiresInSeconds,
|
||||
},
|
||||
},
|
||||
{
|
||||
refreshToken: tokenset.refresh_token,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return { helper, client, initializedScope, initializedPrompt };
|
||||
const helper = PassportOAuthAuthenticatorHelper.from(strategy);
|
||||
|
||||
return { helper, client, initializedScope, initializedPrompt, strategy };
|
||||
},
|
||||
|
||||
async start(input, implementation) {
|
||||
const { initializedScope, initializedPrompt, helper } =
|
||||
await implementation;
|
||||
async start(input, ctx) {
|
||||
const { initializedScope, initializedPrompt, helper, strategy } = await ctx;
|
||||
const options: Record<string, string> = {
|
||||
scope: input.scope || initializedScope || 'openid profile email',
|
||||
state: input.state,
|
||||
@@ -108,16 +118,48 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
options.prompt = prompt;
|
||||
}
|
||||
|
||||
return helper.start(input, {
|
||||
...options,
|
||||
return new Promise((resolve, reject) => {
|
||||
strategy.error = reject;
|
||||
|
||||
return helper
|
||||
.start(input, {
|
||||
...options,
|
||||
})
|
||||
.then(resolve);
|
||||
});
|
||||
},
|
||||
|
||||
async authenticate(input, implementation) {
|
||||
return (await implementation).helper.authenticate(input);
|
||||
async authenticate(input, ctx) {
|
||||
return (await ctx).helper.authenticate(input);
|
||||
},
|
||||
|
||||
async refresh(input, implementation) {
|
||||
return (await implementation).helper.refresh(input);
|
||||
async refresh(input, ctx) {
|
||||
const { client } = await ctx;
|
||||
const tokenset = await client.refresh(input.refreshToken);
|
||||
if (!tokenset.access_token) {
|
||||
throw new Error('Refresh failed');
|
||||
}
|
||||
const userinfo = await client.userinfo(tokenset.access_token);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!tokenset.access_token) {
|
||||
reject(new Error('Refresh Failed'));
|
||||
}
|
||||
resolve({
|
||||
fullProfile: {
|
||||
provider: 'oidc',
|
||||
id: userinfo.sub,
|
||||
displayName: userinfo.name!,
|
||||
},
|
||||
session: {
|
||||
accessToken: tokenset.access_token!,
|
||||
tokenType: tokenset.token_type ?? 'bearer',
|
||||
scope: tokenset.scope!,
|
||||
expiresInSeconds: tokenset.expires_in,
|
||||
idToken: tokenset.id_token,
|
||||
refreshToken: tokenset.refresh_token,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,10 +67,10 @@ describe('authModuleOidcProvider', () => {
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const keyPair = await generateKeyPair('ES256');
|
||||
const keyPair = await generateKeyPair('RS256');
|
||||
const privateKey = await exportJWK(keyPair.privateKey);
|
||||
publicKey = await exportJWK(keyPair.publicKey);
|
||||
publicKey.alg = privateKey.alg = 'ES256';
|
||||
publicKey.alg = privateKey.alg = 'RS256';
|
||||
|
||||
idToken = await new SignJWT({
|
||||
sub: 'test',
|
||||
@@ -109,6 +109,36 @@ describe('authModuleOidcProvider', () => {
|
||||
ctx.set('Location', callbackUrl.toString()),
|
||||
);
|
||||
}),
|
||||
rest.get('https://oidc.test/jwks.json', async (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })),
|
||||
),
|
||||
rest.post('https://oidc.test/oauth2/token', async (req, res, ctx) => {
|
||||
return res(
|
||||
req.headers.get('Authorization')
|
||||
? ctx.json({
|
||||
access_token: 'accessToken',
|
||||
id_token: idToken,
|
||||
refresh_token: 'refreshToken',
|
||||
scope: 'testScope',
|
||||
token_type: '',
|
||||
})
|
||||
: ctx.status(401),
|
||||
);
|
||||
}),
|
||||
rest.get(
|
||||
'https://oidc.test/idp/userinfo.openid',
|
||||
async (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
sub: 'test',
|
||||
name: 'Alice Adams',
|
||||
given_name: 'Alice',
|
||||
family_name: 'Adams',
|
||||
email: 'alice@test.com',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const secret = 'secret';
|
||||
@@ -212,5 +242,38 @@ describe('authModuleOidcProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: This seems to be the place for integration testing, so far only have test that hit metadata endpoints along with /start but might be missing responses for handler?
|
||||
it('#authenticate exchanges authorization code for a access_token', async () => {
|
||||
const agent = request.agent('');
|
||||
|
||||
// make /start request with audience parameter
|
||||
const startResponse = await agent.get(
|
||||
`${appUrl}/api/auth/oidc/start?env=development`,
|
||||
);
|
||||
// follow redirect to authorization endpoint
|
||||
const authorizationResponse = await agent.get(
|
||||
startResponse.header.location,
|
||||
);
|
||||
// follow redirect to token_endpoint
|
||||
const handlerResponse = await agent.get(
|
||||
authorizationResponse.header.location,
|
||||
);
|
||||
|
||||
expect(handlerResponse.text).toContain(
|
||||
encodeURIComponent(
|
||||
JSON.stringify({
|
||||
type: 'authorization_response',
|
||||
response: {
|
||||
profile: {
|
||||
displayName: 'Alice Adams',
|
||||
},
|
||||
providerInfo: {
|
||||
accessToken: 'accessToken',
|
||||
scope: 'testScope',
|
||||
expiresInSeconds: 3600,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,12 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createSignInResolverFactory,
|
||||
OAuthAuthenticatorResult,
|
||||
PassportProfile,
|
||||
SignInInfo,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { commonSignInResolvers } from '@backstage/plugin-auth-node';
|
||||
|
||||
/**
|
||||
* Available sign-in resolvers for the Oidc auth provider.
|
||||
@@ -28,44 +23,16 @@ import {
|
||||
*/
|
||||
export namespace oidcSignInResolvers {
|
||||
/**
|
||||
* Looks up the user by matching their Oidc username to the entity name.
|
||||
* A oidc resolver that looks up the user using the local part of
|
||||
* their email address as the entity name.
|
||||
*/
|
||||
export const usernameMatchingUserEntityName = createSignInResolverFactory({
|
||||
create() {
|
||||
return async (
|
||||
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
ctx,
|
||||
) => {
|
||||
const { result } = info;
|
||||
|
||||
const id = result.fullProfile.username;
|
||||
if (!id) {
|
||||
throw new Error(`Oidc user profile does not contain a username`);
|
||||
}
|
||||
|
||||
return ctx.signInWithCatalogUser({ entityRef: { name: id } });
|
||||
};
|
||||
},
|
||||
});
|
||||
export const emailLocalPartMatchingUserEntityName =
|
||||
commonSignInResolvers.emailLocalPartMatchingUserEntityName;
|
||||
|
||||
/**
|
||||
* Looks up the user by matching their email to the `google.com/email` annotation. Still working out this resolver.....
|
||||
* A oidc resolver that looks up the user using their email address
|
||||
* as email of the entity.
|
||||
*/
|
||||
export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
|
||||
create() {
|
||||
return async (info: SignInInfo<GcpIapResult>, ctx) => {
|
||||
const email = info.result.iapToken.email;
|
||||
|
||||
if (!email) {
|
||||
throw new Error('Google IAP sign-in result is missing email');
|
||||
}
|
||||
|
||||
return ctx.signInWithCatalogUser({
|
||||
annotations: {
|
||||
'google.com/email': email,
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
export const emailMatchingUserEntityProfileEmail =
|
||||
commonSignInResolvers.emailMatchingUserEntityProfileEmail;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"@backstage/plugin-auth-backend-module-google-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-oidc-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-okta-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 { Config, ConfigReader } from '@backstage/config';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import express from 'express';
|
||||
import { Session } from 'express-session';
|
||||
import { UnsecuredJWT } from 'jose';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ClientMetadata, IssuerMetadata } from 'openid-client';
|
||||
import { OAuthAdapter } from '../../lib/oauth';
|
||||
import { oidc, OidcAuthProvider, Options } from './provider';
|
||||
import { AuthResolverContext } from '../types';
|
||||
|
||||
const issuerMetadata = {
|
||||
issuer: 'https://oidc.test',
|
||||
authorization_endpoint: 'https://oidc.test/as/authorization.oauth2',
|
||||
token_endpoint: 'https://oidc.test/as/token.oauth2',
|
||||
revocation_endpoint: 'https://oidc.test/as/revoke_token.oauth2',
|
||||
userinfo_endpoint: 'https://oidc.test/idp/userinfo.openid',
|
||||
introspection_endpoint: 'https://oidc.test/as/introspect.oauth2',
|
||||
jwks_uri: 'https://oidc.test/pf/JWKS',
|
||||
scopes_supported: ['openid'],
|
||||
claims_supported: ['email'],
|
||||
response_types_supported: ['code'],
|
||||
id_token_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'],
|
||||
token_endpoint_auth_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'],
|
||||
request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'],
|
||||
};
|
||||
|
||||
const clientMetadata: Options = {
|
||||
authHandler: async input => ({
|
||||
profile: {
|
||||
displayName: input.userinfo.email,
|
||||
},
|
||||
}),
|
||||
resolverContext: {} as AuthResolverContext,
|
||||
callbackUrl: 'https://oidc.test/callback',
|
||||
clientId: 'testclientid',
|
||||
clientSecret: 'testclientsecret',
|
||||
metadataUrl: 'https://oidc.test/.well-known/openid-configuration',
|
||||
tokenEndpointAuthMethod: 'none',
|
||||
tokenSignedResponseAlg: 'none',
|
||||
};
|
||||
|
||||
describe('OidcAuthProvider', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('hit the metadata url', async () => {
|
||||
const handler = jest.fn((_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(issuerMetadata),
|
||||
);
|
||||
});
|
||||
worker.use(
|
||||
rest.get('https://oidc.test/.well-known/openid-configuration', handler),
|
||||
);
|
||||
const provider = new OidcAuthProvider(clientMetadata);
|
||||
const { strategy } = (await (provider as any).implementation) as any as {
|
||||
strategy: {
|
||||
_client: ClientMetadata;
|
||||
_issuer: IssuerMetadata;
|
||||
};
|
||||
};
|
||||
// Assert that the expected request to the metadaurl was made.
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
const { _client, _issuer } = strategy;
|
||||
expect(_client.client_id).toBe(clientMetadata.clientId);
|
||||
expect(_issuer.token_endpoint).toBe(issuerMetadata.token_endpoint);
|
||||
});
|
||||
|
||||
it('OidcAuthProvider#handler successfully invokes the oidc endpoints', async () => {
|
||||
const sub = 'alice';
|
||||
const iss = 'https://oidc.test';
|
||||
const iat = Date.now();
|
||||
const aud = clientMetadata.clientId;
|
||||
const exp = Date.now() + 10000;
|
||||
const jwt = await new UnsecuredJWT({ iss, sub, aud, iat, exp })
|
||||
.setIssuer(iss)
|
||||
.setAudience(aud)
|
||||
.setSubject(sub)
|
||||
.setIssuedAt(iat)
|
||||
.setExpirationTime(exp)
|
||||
.encode();
|
||||
const requestSequence: Array<string> = [];
|
||||
|
||||
// The array of expected requests executed by the provider handler
|
||||
const requests: Array<{
|
||||
method: 'get' | 'post';
|
||||
url: string;
|
||||
payload: object;
|
||||
}> = [
|
||||
{
|
||||
method: 'get',
|
||||
url: 'https://oidc.test/.well-known/openid-configuration',
|
||||
payload: issuerMetadata,
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
url: 'https://oidc.test/as/token.oauth2',
|
||||
payload: {
|
||||
id_token: jwt,
|
||||
access_token: 'test',
|
||||
authorization_signed_response_alg: 'HS256',
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
url: 'https://oidc.test/idp/userinfo.openid',
|
||||
payload: {
|
||||
sub: 'alice',
|
||||
email: 'alice@oidc.test',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
...requests.map(r => {
|
||||
return rest[r.method](r.url, (_req, res, ctx) => {
|
||||
requestSequence.push(r.url);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(r.payload),
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
const provider = new OidcAuthProvider(clientMetadata);
|
||||
const req = {
|
||||
method: 'GET',
|
||||
url: 'https://oidc.test/?code=test2',
|
||||
session: { 'oidc:oidc.test': 'test' } as any as Session,
|
||||
} as express.Request;
|
||||
await provider.handler(req);
|
||||
expect(requestSequence).toEqual([0, 1, 2].map(i => requests[i].url));
|
||||
});
|
||||
|
||||
it('oidc.create', async () => {
|
||||
const handler = jest.fn((_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(issuerMetadata),
|
||||
);
|
||||
});
|
||||
worker.use(
|
||||
rest.get('https://oidc.test/.well-known/openid-configuration', handler),
|
||||
);
|
||||
const config: Config = new ConfigReader({
|
||||
testEnv: {
|
||||
...clientMetadata,
|
||||
metadataUrl: 'https://oidc.test/.well-known/openid-configuration',
|
||||
},
|
||||
} as any);
|
||||
const provider = oidc.create()({
|
||||
globalConfig: {
|
||||
appUrl: 'https://oidc.test',
|
||||
baseUrl: 'https://oidc.test',
|
||||
},
|
||||
config,
|
||||
} as any) as OAuthAdapter;
|
||||
expect(provider.start).toBeDefined();
|
||||
// Cast provider as any here to be able to inspect private members
|
||||
await (provider as any).handlers.get('testEnv').handlers.implementation;
|
||||
// Assert that the expected request to the metadaurl was made.
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -14,278 +14,282 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import {
|
||||
Client,
|
||||
ClientAuthMethod,
|
||||
Issuer,
|
||||
Strategy as OidcStrategy,
|
||||
TokenSet,
|
||||
UserinfoResponse,
|
||||
} from 'openid-client';
|
||||
import {
|
||||
encodeState,
|
||||
OAuthAdapter,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthHandlers,
|
||||
OAuthProviderOptions,
|
||||
OAuthRefreshRequest,
|
||||
OAuthResponse,
|
||||
OAuthStartRequest,
|
||||
} from '../../lib/oauth';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import {
|
||||
AuthHandler,
|
||||
AuthResolverContext,
|
||||
OAuthStartResponse,
|
||||
SignInResolver,
|
||||
} from '../types';
|
||||
import { AuthHandler, SignInResolver } from '../types';
|
||||
import { OAuthResult } from '../../lib/oauth';
|
||||
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
import { createOAuthProviderFactory } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
commonByEmailLocalPartResolver,
|
||||
commonByEmailResolver,
|
||||
} from '../resolvers';
|
||||
import { BACKSTAGE_SESSION_EXPIRATION } from '../../lib/session';
|
||||
adaptLegacyOAuthHandler,
|
||||
adaptLegacyOAuthSignInResolver,
|
||||
} from '../../lib/legacy';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken?: string;
|
||||
};
|
||||
import { oidcAuthenticator } from '@backstage/plugin-auth-backend-module-oidc-provider';
|
||||
|
||||
type OidcImpl = {
|
||||
strategy: OidcStrategy<UserinfoResponse, Client>;
|
||||
client: Client;
|
||||
};
|
||||
// type PrivateInfo = {
|
||||
// refreshToken?: string;
|
||||
// };
|
||||
|
||||
/**
|
||||
* authentication result for the OIDC which includes the token set and user information (a profile response sent by OIDC server)
|
||||
* @public
|
||||
*/
|
||||
export type OidcAuthResult = {
|
||||
tokenset: TokenSet;
|
||||
userinfo: UserinfoResponse;
|
||||
};
|
||||
// type OidcImpl = {
|
||||
// strategy: OidcStrategy<UserinfoResponse, Client>;
|
||||
// client: Client;
|
||||
// };
|
||||
|
||||
export type Options = OAuthProviderOptions & {
|
||||
metadataUrl: string;
|
||||
scope?: string;
|
||||
prompt?: string;
|
||||
tokenEndpointAuthMethod?: ClientAuthMethod;
|
||||
tokenSignedResponseAlg?: string;
|
||||
signInResolver?: SignInResolver<OidcAuthResult>;
|
||||
authHandler: AuthHandler<OidcAuthResult>;
|
||||
resolverContext: AuthResolverContext;
|
||||
};
|
||||
// /**
|
||||
// * authentication result for the OIDC which includes the token set and user information (a profile response sent by OIDC server)
|
||||
// * @public
|
||||
// */
|
||||
// export type OidcAuthResult = {
|
||||
// tokenset: TokenSet;
|
||||
// userinfo: UserinfoResponse;
|
||||
// };
|
||||
|
||||
export class OidcAuthProvider implements OAuthHandlers {
|
||||
private readonly implementation: Promise<OidcImpl>;
|
||||
private readonly scope?: string;
|
||||
private readonly prompt?: string;
|
||||
// export type Options = OAuthProviderOptions & {
|
||||
// metadataUrl: string;
|
||||
// scope?: string;
|
||||
// prompt?: string;
|
||||
// tokenEndpointAuthMethod?: ClientAuthMethod;
|
||||
// tokenSignedResponseAlg?: string;
|
||||
// signInResolver?: SignInResolver<OidcAuthResult>;
|
||||
// authHandler: AuthHandler<OidcAuthResult>;
|
||||
// resolverContext: AuthResolverContext;
|
||||
// };
|
||||
|
||||
private readonly signInResolver?: SignInResolver<OidcAuthResult>;
|
||||
private readonly authHandler: AuthHandler<OidcAuthResult>;
|
||||
private readonly resolverContext: AuthResolverContext;
|
||||
// export class OidcAuthProvider implements OAuthHandlers {
|
||||
// private readonly implementation: Promise<OidcImpl>;
|
||||
// private readonly scope?: string;
|
||||
// private readonly prompt?: string;
|
||||
|
||||
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.resolverContext = options.resolverContext;
|
||||
}
|
||||
// private readonly signInResolver?: SignInResolver<OidcAuthResult>;
|
||||
// private readonly authHandler: AuthHandler<OidcAuthResult>;
|
||||
// private readonly resolverContext: AuthResolverContext;
|
||||
|
||||
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
|
||||
const { strategy } = await this.implementation;
|
||||
const options: Record<string, string> = {
|
||||
scope: req.scope || this.scope || 'openid profile email',
|
||||
state: encodeState(req.state),
|
||||
};
|
||||
const prompt = this.prompt || 'none';
|
||||
if (prompt !== 'auto') {
|
||||
options.prompt = prompt;
|
||||
}
|
||||
return await executeRedirectStrategy(req, strategy, options);
|
||||
}
|
||||
// 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.resolverContext = options.resolverContext;
|
||||
// }
|
||||
|
||||
async handler(req: express.Request) {
|
||||
const { strategy } = await this.implementation;
|
||||
const { result, privateInfo } = await executeFrameHandlerStrategy<
|
||||
OidcAuthResult,
|
||||
PrivateInfo
|
||||
>(req, strategy);
|
||||
// async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
|
||||
// const { strategy } = await this.implementation;
|
||||
// const options: Record<string, string> = {
|
||||
// scope: req.scope || this.scope || 'openid profile email',
|
||||
// state: encodeState(req.state),
|
||||
// };
|
||||
// const prompt = this.prompt || 'none';
|
||||
// if (prompt !== 'auto') {
|
||||
// options.prompt = prompt;
|
||||
// }
|
||||
// return await executeRedirectStrategy(req, strategy, options);
|
||||
// }
|
||||
|
||||
return {
|
||||
response: await this.handleResult(result),
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
};
|
||||
}
|
||||
// async handler(req: express.Request) {
|
||||
// const { strategy } = await this.implementation;
|
||||
// const { result, privateInfo } = await executeFrameHandlerStrategy<
|
||||
// OidcAuthResult,
|
||||
// PrivateInfo
|
||||
// >(req, strategy);
|
||||
|
||||
async refresh(req: OAuthRefreshRequest) {
|
||||
const { client } = await this.implementation;
|
||||
const tokenset = await client.refresh(req.refreshToken);
|
||||
if (!tokenset.access_token) {
|
||||
throw new Error('Refresh failed');
|
||||
}
|
||||
if (!tokenset.scope) {
|
||||
tokenset.scope = req.scope;
|
||||
}
|
||||
const userinfo = await client.userinfo(tokenset.access_token);
|
||||
// async refresh(req: OAuthRefreshRequest) {
|
||||
// const { client } = await this.implementation;
|
||||
// const tokenset = await client.refresh(req.refreshToken);
|
||||
// if (!tokenset.access_token) {
|
||||
// throw new Error('Refresh failed');
|
||||
// }
|
||||
// if (!tokenset.scope) {
|
||||
// tokenset.scope = req.scope;
|
||||
// }
|
||||
// const userinfo = await client.userinfo(tokenset.access_token);
|
||||
// return {
|
||||
// response: await this.handleResult(result),
|
||||
// refreshToken: privateInfo.refreshToken,
|
||||
// };
|
||||
// }
|
||||
|
||||
return {
|
||||
response: await this.handleResult({ tokenset, userinfo }),
|
||||
refreshToken: tokenset.refresh_token,
|
||||
};
|
||||
}
|
||||
// async refresh(req: OAuthRefreshRequest) {
|
||||
// const { client } = await this.implementation;
|
||||
// const tokenset = await client.refresh(req.refreshToken);
|
||||
// if (!tokenset.access_token) {
|
||||
// throw new Error('Refresh failed');
|
||||
// }
|
||||
// const userinfo = await client.userinfo(tokenset.access_token);
|
||||
|
||||
private async setupStrategy(options: Options): Promise<OidcImpl> {
|
||||
const issuer = await Issuer.discover(options.metadataUrl);
|
||||
const client = new issuer.Client({
|
||||
access_type: 'offline', // this option must be passed to provider to receive a refresh token
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret,
|
||||
redirect_uris: [options.callbackUrl],
|
||||
response_types: ['code'],
|
||||
token_endpoint_auth_method:
|
||||
options.tokenEndpointAuthMethod || 'client_secret_basic',
|
||||
id_token_signed_response_alg: options.tokenSignedResponseAlg || 'RS256',
|
||||
scope: options.scope || '',
|
||||
});
|
||||
// return {
|
||||
// response: await this.handleResult({ tokenset, userinfo }),
|
||||
// refreshToken: tokenset.refresh_token,
|
||||
// };
|
||||
// }
|
||||
|
||||
const strategy = new OidcStrategy(
|
||||
{
|
||||
client,
|
||||
passReqToCallback: false,
|
||||
},
|
||||
(
|
||||
tokenset: TokenSet,
|
||||
userinfo: UserinfoResponse,
|
||||
done: PassportDoneCallback<OidcAuthResult, PrivateInfo>,
|
||||
) => {
|
||||
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,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
strategy.error = console.error;
|
||||
return { strategy, client };
|
||||
}
|
||||
// private async setupStrategy(options: Options): Promise<OidcImpl> {
|
||||
// const issuer = await Issuer.discover(options.metadataUrl);
|
||||
// const client = new issuer.Client({
|
||||
// access_type: 'offline', // this option must be passed to provider to receive a refresh token
|
||||
// client_id: options.clientId,
|
||||
// client_secret: options.clientSecret,
|
||||
// redirect_uris: [options.callbackUrl],
|
||||
// response_types: ['code'],
|
||||
// token_endpoint_auth_method:
|
||||
// options.tokenEndpointAuthMethod || 'client_secret_basic',
|
||||
// id_token_signed_response_alg: options.tokenSignedResponseAlg || 'RS256',
|
||||
// scope: options.scope || '',
|
||||
// });
|
||||
|
||||
// Use this function to grab the user profile info from the token
|
||||
// Then populate the profile with it
|
||||
private async handleResult(result: OidcAuthResult): Promise<OAuthResponse> {
|
||||
const { profile } = await this.authHandler(result, this.resolverContext);
|
||||
// const strategy = new OidcStrategy(
|
||||
// {
|
||||
// client,
|
||||
// passReqToCallback: false,
|
||||
// },
|
||||
// (
|
||||
// tokenset: TokenSet,
|
||||
// userinfo: UserinfoResponse,
|
||||
// done: PassportDoneCallback<OidcAuthResult, PrivateInfo>,
|
||||
// ) => {
|
||||
// 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,
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// strategy.error = console.error;
|
||||
// return { strategy, client };
|
||||
// }
|
||||
|
||||
const expiresInSeconds =
|
||||
result.tokenset.expires_in === undefined
|
||||
? BACKSTAGE_SESSION_EXPIRATION
|
||||
: Math.min(result.tokenset.expires_in, BACKSTAGE_SESSION_EXPIRATION);
|
||||
// Use this function to grab the user profile info from the token
|
||||
// Then populate the profile with it
|
||||
// private async handleResult(result: OidcAuthResult): Promise<OAuthResponse> {
|
||||
// const { profile } = await this.authHandler(result, this.resolverContext);
|
||||
|
||||
let backstageIdentity = undefined;
|
||||
if (this.signInResolver) {
|
||||
backstageIdentity = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
},
|
||||
this.resolverContext,
|
||||
);
|
||||
}
|
||||
// const expiresInSeconds =
|
||||
// result.tokenset.expires_in === undefined
|
||||
// ? BACKSTAGE_SESSION_EXPIRATION
|
||||
// : Math.min(result.tokenset.expires_in, BACKSTAGE_SESSION_EXPIRATION);
|
||||
|
||||
return {
|
||||
backstageIdentity,
|
||||
providerInfo: {
|
||||
idToken: result.tokenset.id_token,
|
||||
accessToken: result.tokenset.access_token!,
|
||||
scope: result.tokenset.scope!,
|
||||
expiresInSeconds,
|
||||
},
|
||||
profile,
|
||||
};
|
||||
}
|
||||
}
|
||||
// let backstageIdentity = undefined;
|
||||
// if (this.signInResolver) {
|
||||
// backstageIdentity = await this.signInResolver(
|
||||
// {
|
||||
// result,
|
||||
// profile,
|
||||
// },
|
||||
// this.resolverContext,
|
||||
// );
|
||||
// }
|
||||
|
||||
// return {
|
||||
// backstageIdentity,
|
||||
// providerInfo: {
|
||||
// idToken: result.tokenset.id_token,
|
||||
// accessToken: result.tokenset.access_token!,
|
||||
// scope: result.tokenset.scope!,
|
||||
// expiresInSeconds,
|
||||
// },
|
||||
// profile,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Auth provider integration for generic OpenID Connect auth
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
// export const oidc = createAuthProviderIntegration({
|
||||
// create(options?: {
|
||||
// authHandler?: AuthHandler<OidcAuthResult>;
|
||||
|
||||
// signIn?: {
|
||||
// resolver: SignInResolver<OidcAuthResult>;
|
||||
// };
|
||||
// }) {
|
||||
// return ({ providerId, globalConfig, config, resolverContext }) =>
|
||||
// OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
// const clientId = envConfig.getString('clientId');
|
||||
// const clientSecret = envConfig.getString('clientSecret');
|
||||
// const customCallbackUrl = envConfig.getOptionalString('callbackUrl');
|
||||
// const callbackUrl =
|
||||
// customCallbackUrl ||
|
||||
// `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
// const metadataUrl = envConfig.getString('metadataUrl');
|
||||
// const tokenEndpointAuthMethod = envConfig.getOptionalString(
|
||||
// 'tokenEndpointAuthMethod',
|
||||
// ) as ClientAuthMethod;
|
||||
// const tokenSignedResponseAlg = envConfig.getOptionalString(
|
||||
// 'tokenSignedResponseAlg',
|
||||
// );
|
||||
// const scope = envConfig.getOptionalString('scope');
|
||||
// const prompt = envConfig.getOptionalString('prompt');
|
||||
|
||||
// const authHandler: AuthHandler<OidcAuthResult> = options?.authHandler
|
||||
// ? options.authHandler
|
||||
// : async ({ userinfo }) => ({
|
||||
// profile: {
|
||||
// displayName: userinfo.name,
|
||||
// email: userinfo.email,
|
||||
// picture: userinfo.picture,
|
||||
// },
|
||||
// });
|
||||
|
||||
// const provider = new OidcAuthProvider({
|
||||
// clientId,
|
||||
// clientSecret,
|
||||
// callbackUrl,
|
||||
// tokenEndpointAuthMethod,
|
||||
// tokenSignedResponseAlg,
|
||||
// metadataUrl,
|
||||
// scope,
|
||||
// prompt,
|
||||
// signInResolver: options?.signIn?.resolver,
|
||||
// authHandler,
|
||||
// resolverContext,
|
||||
// });
|
||||
|
||||
// return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
// providerId,
|
||||
// callbackUrl,
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
// resolvers: {
|
||||
// /**
|
||||
// * Looks up the user by matching their email local part to the entity name.
|
||||
// */
|
||||
// emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver,
|
||||
// /**
|
||||
// * Looks up the user by matching their email to the entity email.
|
||||
// */
|
||||
// emailMatchingUserEntityProfileEmail: () => commonByEmailResolver,
|
||||
// },
|
||||
// });
|
||||
|
||||
export const oidc = createAuthProviderIntegration({
|
||||
create(options?: {
|
||||
authHandler?: AuthHandler<OidcAuthResult>;
|
||||
/**
|
||||
* 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?: {
|
||||
resolver: SignInResolver<OidcAuthResult>;
|
||||
resolver: SignInResolver<OAuthResult>;
|
||||
};
|
||||
}) {
|
||||
return ({ providerId, globalConfig, config, resolverContext }) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const customCallbackUrl = envConfig.getOptionalString('callbackUrl');
|
||||
const callbackUrl =
|
||||
customCallbackUrl ||
|
||||
`${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
const metadataUrl = envConfig.getString('metadataUrl');
|
||||
const tokenEndpointAuthMethod = envConfig.getOptionalString(
|
||||
'tokenEndpointAuthMethod',
|
||||
) as ClientAuthMethod;
|
||||
const tokenSignedResponseAlg = envConfig.getOptionalString(
|
||||
'tokenSignedResponseAlg',
|
||||
);
|
||||
const scope = envConfig.getOptionalString('scope');
|
||||
const prompt = envConfig.getOptionalString('prompt');
|
||||
|
||||
const authHandler: AuthHandler<OidcAuthResult> = options?.authHandler
|
||||
? options.authHandler
|
||||
: async ({ userinfo }) => ({
|
||||
profile: {
|
||||
displayName: userinfo.name,
|
||||
email: userinfo.email,
|
||||
picture: userinfo.picture,
|
||||
},
|
||||
});
|
||||
|
||||
const provider = new OidcAuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
tokenEndpointAuthMethod,
|
||||
tokenSignedResponseAlg,
|
||||
metadataUrl,
|
||||
scope,
|
||||
prompt,
|
||||
signInResolver: options?.signIn?.resolver,
|
||||
authHandler,
|
||||
resolverContext,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
providerId,
|
||||
callbackUrl,
|
||||
});
|
||||
});
|
||||
},
|
||||
resolvers: {
|
||||
/**
|
||||
* Looks up the user by matching their email local part to the entity name.
|
||||
*/
|
||||
emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver,
|
||||
/**
|
||||
* Looks up the user by matching their email to the entity email.
|
||||
*/
|
||||
emailMatchingUserEntityProfileEmail: () => commonByEmailResolver,
|
||||
return createOAuthProviderFactory({
|
||||
authenticator: oidcAuthenticator,
|
||||
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
||||
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4672,7 +4672,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-auth-backend-module-oidc-provider@workspace:plugins/auth-backend-module-oidc-provider":
|
||||
"@backstage/plugin-auth-backend-module-oidc-provider@workspace:^, @backstage/plugin-auth-backend-module-oidc-provider@workspace:plugins/auth-backend-module-oidc-provider":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-auth-backend-module-oidc-provider@workspace:plugins/auth-backend-module-oidc-provider"
|
||||
dependencies:
|
||||
@@ -4777,6 +4777,7 @@ __metadata:
|
||||
"@backstage/plugin-auth-backend-module-google-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-oidc-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-okta-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@backstage/plugin-catalog-node": "workspace:^"
|
||||
@@ -31276,13 +31277,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jose@npm:^4.14.6, jose@npm:^4.15.4, jose@npm:^4.6.0":
|
||||
"jose@npm:^4.14.6, jose@npm:^4.15.1":
|
||||
version: 4.15.4
|
||||
resolution: "jose@npm:4.15.4"
|
||||
checksum: dccad91cb3357f36423774a0b89ad830dd84b31090de65cd139b85488439f16a00f8c59c0773825e8a1adb0dd9d13ad725ad66e6ea33880ecb3959bb99e1ea5b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jose@npm:^4.6.0":
|
||||
version: 4.15.2
|
||||
resolution: "jose@npm:4.15.2"
|
||||
checksum: 8f0cab1eef31243abe14a935b2b330cd95f10f9b69808fd642088ae5000e50e566664934537d2c6413ab2f6b54acd8265a5033da05157aa1260c5f1d7e57fab0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"joycon@npm:^3.0.1":
|
||||
version: 3.1.0
|
||||
resolution: "joycon@npm:3.1.0"
|
||||
@@ -35655,19 +35663,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openid-client@npm:^5.2.1, openid-client@npm:^5.3.0, openid-client@npm:^5.4.3":
|
||||
version: 5.6.4
|
||||
resolution: "openid-client@npm:5.6.4"
|
||||
dependencies:
|
||||
jose: ^4.15.4
|
||||
lru-cache: ^6.0.0
|
||||
object-hash: ^2.2.0
|
||||
oidc-token-hash: ^5.0.3
|
||||
checksum: 69843f078dacbbc6bad6d65ca6689414ac73f095dfe2f8e606822e6cfc9d9cd7d0dfaf2649352eda604653806f0ea65326ad2d6266da897e4740ec93d26d21f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openid-client@npm:^5.5.0":
|
||||
"openid-client@npm:^5.2.1, openid-client@npm:^5.3.0, openid-client@npm:^5.5.0":
|
||||
version: 5.5.0
|
||||
resolution: "openid-client@npm:5.5.0"
|
||||
dependencies:
|
||||
@@ -35679,6 +35675,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openid-client@npm:^5.4.3":
|
||||
version: 5.6.1
|
||||
resolution: "openid-client@npm:5.6.1"
|
||||
dependencies:
|
||||
jose: ^4.15.1
|
||||
lru-cache: ^6.0.0
|
||||
object-hash: ^2.2.0
|
||||
oidc-token-hash: ^5.0.3
|
||||
checksum: 9d939cec57540e6dd3f67e9a248ec5ecec3b439b7ab5bd2e9fb4481bd03e8d030deedd87a447348194be7f3e93e84085841b0414033caf86479870f526cdbc2f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oppa@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "oppa@npm:0.4.0"
|
||||
|
||||
Reference in New Issue
Block a user