auth-backend-module-*: update OAuth providers to use new scope handling
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-atlassian-provider': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `scope` and `scopes` config options have been removed and replaced by the standard `additionalScopes` config. In addition, the `offline_access`, `read:jira-work`, and `read:jira-user` scopes have been set to required and will always be present.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-bitbucket-provider': patch
|
||||
---
|
||||
|
||||
Added support for the new shared `additionalScopes` configuration. In addition, the `account` scope has been set to required and will always be present.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-github-provider': patch
|
||||
---
|
||||
|
||||
Added support for the new shared `additionalScopes` configuration. In addition, the `read:user` scope has been set to required and will always be present.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-gitlab-provider': patch
|
||||
---
|
||||
|
||||
Added support for the new shared `additionalScopes` configuration. In addition, the `read_user` scope has been set to required and will always be present.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-google-provider': patch
|
||||
---
|
||||
|
||||
Added support for the new shared `additionalScopes` configuration. In addition, the `openid`, `userinfo.email`, and `userinfo.profile` scopes have been set to required and will always be present.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-microsoft-provider': patch
|
||||
---
|
||||
|
||||
Added support for the new shared `additionalScopes` configuration.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-oauth2-provider': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `scope` config option have been removed and replaced by the standard `additionalScopes` config.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-oidc-provider': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `scope` config option have been removed and replaced by the standard `additionalScopes` config. In addition, `openid`, `profile`, and `email` scopes have been set to required and will always be present.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-okta-provider': patch
|
||||
---
|
||||
|
||||
Added support for the new shared `additionalScopes` configuration, which means it can now also be specified as an array. In addition, the `openid`, `email`, `profile`, and `offline_access` scopes have been set to required and will always be present.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-pinniped-provider': patch
|
||||
---
|
||||
|
||||
**BREAKING**: The `scope` config option have been removed and replaced by the standard `additionalScopes` config. In addition, the `openid`, `pinniped:request-audience`, `username`, and `offline_access` scopes have been set to required and will always be present.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `scope` config option have been removed and replaced by the standard `additionalScopes` config. In addition, `openid`, and `offline_access` scopes have been set to required and will always be present.
|
||||
@@ -27,7 +27,7 @@ export interface Config {
|
||||
clientSecret: string;
|
||||
audience?: string;
|
||||
callbackUrl?: string;
|
||||
scope?: string;
|
||||
additionalScopes?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,15 +26,20 @@ import { Strategy as AtlassianStrategy } from 'passport-atlassian-oauth2';
|
||||
export const atlassianAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
scopes: {
|
||||
required: ['offline_access', 'read:jira-work', 'read:jira-user'],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const scope =
|
||||
config.getOptionalString('scope') ??
|
||||
config.getOptionalString('scopes') ??
|
||||
'offline_access read:jira-work read:jira-user';
|
||||
const baseUrl = 'https://auth.atlassian.com';
|
||||
|
||||
if (config.has('scope') || config.has('scopes')) {
|
||||
throw new Error(
|
||||
'The atlassian provider no longer supports the "scope" or "scopes" configuration options. Please use the "additionalScopes" option instead.',
|
||||
);
|
||||
}
|
||||
|
||||
return PassportOAuthAuthenticatorHelper.from(
|
||||
new AtlassianStrategy(
|
||||
{
|
||||
@@ -45,7 +50,6 @@ export const atlassianAuthenticator = createOAuthAuthenticator({
|
||||
authorizationURL: `${baseUrl}/authorize`,
|
||||
tokenURL: `${baseUrl}/oauth/token`,
|
||||
profileURL: `${baseUrl}/api/v4/user`,
|
||||
scope,
|
||||
},
|
||||
(
|
||||
accessToken: string,
|
||||
|
||||
@@ -75,7 +75,8 @@ describe('authModuleAtlassianProvider', () => {
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
});
|
||||
});
|
||||
it('should start with and use custom scopes from scope config field', async () => {
|
||||
|
||||
it('should start with and use custom scopes from additionalScopes config field', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
import('@backstage/plugin-auth-backend'),
|
||||
@@ -91,7 +92,10 @@ describe('authModuleAtlassianProvider', () => {
|
||||
development: {
|
||||
clientId: 'my-client-id',
|
||||
clientSecret: 'my-client-secret',
|
||||
scope: 'offline_access read:filter:jira read:jira-work',
|
||||
additionalScopes: [
|
||||
'read:filter:jira',
|
||||
'read:jira-work', // already required
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,7 +127,7 @@ describe('authModuleAtlassianProvider', () => {
|
||||
client_id: 'my-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/atlassian/handler/frame`,
|
||||
state: expect.any(String),
|
||||
scope: 'offline_access read:filter:jira read:jira-work',
|
||||
scope: 'offline_access read:jira-work read:jira-user read:filter:jira',
|
||||
});
|
||||
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
@@ -131,60 +135,35 @@ describe('authModuleAtlassianProvider', () => {
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
});
|
||||
});
|
||||
it('should start with and use custom scopes from scopes config field for backward compatibility', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
import('@backstage/plugin-auth-backend'),
|
||||
authModuleAtlassianProvider,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
auth: {
|
||||
providers: {
|
||||
atlassian: {
|
||||
development: {
|
||||
clientId: 'my-client-id',
|
||||
clientSecret: 'my-client-secret',
|
||||
scopes: 'offline_access read:filter:jira read:jira-work',
|
||||
|
||||
it('should fail to start with scope or scopes config', async () => {
|
||||
await expect(
|
||||
startTestBackend({
|
||||
features: [
|
||||
import('@backstage/plugin-auth-backend'),
|
||||
authModuleAtlassianProvider,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
auth: {
|
||||
providers: {
|
||||
atlassian: {
|
||||
development: {
|
||||
clientId: 'my-client-id',
|
||||
clientSecret: 'my-client-secret',
|
||||
scope: 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const agent = request.agent(server);
|
||||
|
||||
const res = await agent.get('/api/auth/atlassian/start?env=development');
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
|
||||
const nonceCookie = agent.jar.getCookie('atlassian-nonce', {
|
||||
domain: 'localhost',
|
||||
path: '/api/auth/atlassian/handler',
|
||||
script: false,
|
||||
secure: false,
|
||||
});
|
||||
expect(nonceCookie).toBeDefined();
|
||||
|
||||
const startUrl = new URL(res.get('location'));
|
||||
expect(startUrl.origin).toBe('https://auth.atlassian.com');
|
||||
expect(startUrl.pathname).toBe('/authorize');
|
||||
expect(Object.fromEntries(startUrl.searchParams)).toEqual({
|
||||
response_type: 'code',
|
||||
client_id: 'my-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/atlassian/handler/frame`,
|
||||
state: expect.any(String),
|
||||
scope: 'offline_access read:filter:jira read:jira-work',
|
||||
});
|
||||
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
env: 'development',
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
});
|
||||
}),
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/atlassian provider no longer supports the "scope" or "scopes" configuration options/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Config {
|
||||
* @visibility secret
|
||||
*/
|
||||
clientSecret: string;
|
||||
additionalScopes?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,6 +26,9 @@ import {
|
||||
export const bitbucketAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
scopes: {
|
||||
required: ['account'],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientID = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
|
||||
@@ -67,6 +67,7 @@ describe('authModuleBitbucketProvider', () => {
|
||||
client_id: 'my-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/bitbucket/handler/frame`,
|
||||
state: expect.any(String),
|
||||
scope: 'account',
|
||||
});
|
||||
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface Config {
|
||||
clientSecret: string;
|
||||
callbackUrl?: string;
|
||||
enterpriseInstanceUrl?: string;
|
||||
additionalScopes?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -28,7 +28,10 @@ const ACCESS_TOKEN_PREFIX = 'access-token.';
|
||||
export const githubAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
shouldPersistScopes: true,
|
||||
scopes: {
|
||||
persist: true,
|
||||
required: ['read:user'],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
|
||||
@@ -67,12 +67,13 @@ describe('authModuleGithubProvider', () => {
|
||||
client_id: 'my-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/github/handler/frame`,
|
||||
state: expect.any(String),
|
||||
scope: 'read:user',
|
||||
});
|
||||
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
env: 'development',
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
scope: '',
|
||||
scope: 'read:user',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface Config {
|
||||
clientSecret: string;
|
||||
audience?: string;
|
||||
callbackUrl?: string;
|
||||
additionalScopes?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,6 +26,9 @@ import {
|
||||
export const gitlabAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
scopes: {
|
||||
required: ['read_user'],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface Config {
|
||||
*/
|
||||
clientSecret: string;
|
||||
callbackUrl?: string;
|
||||
additionalScopes?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,6 +27,13 @@ import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
||||
export const googleAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
scopes: {
|
||||
required: [
|
||||
'openid',
|
||||
`https://www.googleapis.com/auth/userinfo.email`,
|
||||
`https://www.googleapis.com/auth/userinfo.profile`,
|
||||
],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
|
||||
@@ -69,6 +69,8 @@ describe('authModuleGoogleProvider', () => {
|
||||
client_id: 'my-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/google/handler/frame`,
|
||||
state: expect.any(String),
|
||||
scope:
|
||||
'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
|
||||
});
|
||||
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface Config {
|
||||
clientSecret: string;
|
||||
domainHint?: string;
|
||||
callbackUrl?: string;
|
||||
additionalScopes?: string[];
|
||||
additionalScopes?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,9 @@ export interface Config {
|
||||
clientSecret: string;
|
||||
authorizationUrl: string;
|
||||
tokenUrl: string;
|
||||
/** @deprecated use `additionalScopes` instead */
|
||||
scope?: string;
|
||||
additionalScopes?: string | string[];
|
||||
disableRefresh?: boolean;
|
||||
includeBasicAuth?: boolean;
|
||||
};
|
||||
|
||||
@@ -31,9 +31,14 @@ export const oauth2Authenticator = createOAuthAuthenticator({
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const authorizationUrl = config.getString('authorizationUrl');
|
||||
const tokenUrl = config.getString('tokenUrl');
|
||||
const scope = config.getOptionalString('scope');
|
||||
const includeBasicAuth = config.getOptionalBoolean('includeBasicAuth');
|
||||
|
||||
if (config.has('scope')) {
|
||||
throw new Error(
|
||||
'The oauth2 provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.',
|
||||
);
|
||||
}
|
||||
|
||||
return PassportOAuthAuthenticatorHelper.from(
|
||||
new Oauth2Strategy(
|
||||
{
|
||||
@@ -43,7 +48,6 @@ export const oauth2Authenticator = createOAuthAuthenticator({
|
||||
authorizationURL: authorizationUrl,
|
||||
tokenURL: tokenUrl,
|
||||
passReqToCallback: false,
|
||||
scope: scope,
|
||||
customHeaders: includeBasicAuth
|
||||
? {
|
||||
Authorization: `Basic ${encodeClientCredentials(
|
||||
|
||||
@@ -19,7 +19,6 @@ export default authModuleOidcProvider;
|
||||
// @public (undocumented)
|
||||
export const oidcAuthenticator: OAuthAuthenticator<
|
||||
{
|
||||
initializedScope: string | undefined;
|
||||
initializedPrompt: string | undefined;
|
||||
promise: Promise<{
|
||||
helper: PassportOAuthAuthenticatorHelper;
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export interface Config {
|
||||
callbackUrl?: string;
|
||||
tokenEndpointAuthMethod?: string;
|
||||
tokenSignedResponseAlg?: string;
|
||||
scope?: string;
|
||||
additionalScopes?: string | string[];
|
||||
prompt?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -254,19 +254,6 @@ describe('oidcAuthenticator', () => {
|
||||
expect(fakeSession['oidc:oidc.test'].code_verifier).toBeDefined();
|
||||
});
|
||||
|
||||
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,
|
||||
|
||||
@@ -53,7 +53,6 @@ export type OidcAuthResult = {
|
||||
|
||||
/** @public */
|
||||
export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
shouldPersistScopes: true,
|
||||
defaultProfileTransform: async (
|
||||
input: OAuthAuthenticatorResult<OidcAuthResult>,
|
||||
) => ({
|
||||
@@ -63,6 +62,10 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
displayName: input.fullProfile.userinfo.name,
|
||||
},
|
||||
}),
|
||||
scopes: {
|
||||
persist: true,
|
||||
required: ['openid', 'profile', 'email'],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
@@ -74,9 +77,14 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
const tokenSignedResponseAlg = config.getOptionalString(
|
||||
'tokenSignedResponseAlg',
|
||||
);
|
||||
const initializedScope = config.getOptionalString('scope');
|
||||
const initializedPrompt = config.getOptionalString('prompt');
|
||||
|
||||
if (config.has('scope')) {
|
||||
throw new Error(
|
||||
'The oidc provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.',
|
||||
);
|
||||
}
|
||||
|
||||
Issuer[custom.http_options] = httpOptionsProvider;
|
||||
const promise = Issuer.discover(metadataUrl).then(issuer => {
|
||||
issuer[custom.http_options] = httpOptionsProvider;
|
||||
@@ -91,7 +99,6 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
token_endpoint_auth_method:
|
||||
tokenEndpointAuthMethod || 'client_secret_basic',
|
||||
id_token_signed_response_alg: tokenSignedResponseAlg || 'RS256',
|
||||
scope: initializedScope || '',
|
||||
});
|
||||
client[custom.http_options] = httpOptionsProvider;
|
||||
|
||||
@@ -123,14 +130,14 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
return { helper, client, strategy };
|
||||
});
|
||||
|
||||
return { initializedScope, initializedPrompt, promise };
|
||||
return { initializedPrompt, promise };
|
||||
},
|
||||
|
||||
async start(input, ctx) {
|
||||
const { initializedScope, initializedPrompt, promise } = ctx;
|
||||
const { initializedPrompt, promise } = ctx;
|
||||
const { helper, strategy } = await promise;
|
||||
const options: Record<string, string> = {
|
||||
scope: input.scope || initializedScope || 'openid profile email',
|
||||
scope: input.scope,
|
||||
state: input.state,
|
||||
nonce: crypto.randomBytes(16).toString('base64'),
|
||||
};
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('authModuleOidcProvider', () => {
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
env: 'development',
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
scope: '',
|
||||
scope: 'openid profile email',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export interface Config {
|
||||
authServerId?: string;
|
||||
idp?: string;
|
||||
callbackUrl?: string;
|
||||
additionalScopes?: string;
|
||||
additionalScopes?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,25 +26,15 @@ import {
|
||||
export const oktaAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
scopes: {
|
||||
required: ['openid', 'email', 'profile', 'offline_access'],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const audience = config.getOptionalString('audience') || 'https://okta.com';
|
||||
const authServerId = config.getOptionalString('authServerId');
|
||||
const idp = config.getOptionalString('idp');
|
||||
// default scopes are taken from
|
||||
// https://developer.okta.com/docs/reference/api/oidc/#response-example-success-refresh-token
|
||||
const defaultScopes = 'openid profile email';
|
||||
// additional scopes can be configured in the config as a space separated string
|
||||
const additionalScopes = config.getOptionalString('additionalScopes') || '';
|
||||
// combine default and additional scopes and remove duplicates
|
||||
const combineScopeStrings = (scopesA: string, scopesB: string) => {
|
||||
const scopesAArray = scopesA.split(' ');
|
||||
const scopesBArray = scopesB.split(' ');
|
||||
const combinedScopes = new Set([...scopesAArray, ...scopesBArray]);
|
||||
return Array.from(combinedScopes).join(' ');
|
||||
};
|
||||
const scope = combineScopeStrings(defaultScopes, additionalScopes);
|
||||
|
||||
return PassportOAuthAuthenticatorHelper.from(
|
||||
new OktaStrategy(
|
||||
@@ -57,7 +47,6 @@ export const oktaAuthenticator = createOAuthAuthenticator({
|
||||
idp: idp,
|
||||
passReqToCallback: false,
|
||||
response_type: 'code',
|
||||
scope,
|
||||
},
|
||||
(
|
||||
accessToken: string,
|
||||
|
||||
@@ -21,7 +21,6 @@ import { decodeOAuthState } from '@backstage/plugin-auth-node';
|
||||
|
||||
describe('authModuleOktaProvider', () => {
|
||||
it('should start', async () => {
|
||||
const additionalScopes = 'groups phone';
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
import('@backstage/plugin-auth-backend'),
|
||||
@@ -37,7 +36,7 @@ describe('authModuleOktaProvider', () => {
|
||||
development: {
|
||||
clientId: 'my-client-id',
|
||||
clientSecret: 'my-client-secret',
|
||||
additionalScopes,
|
||||
additionalScopes: 'groups phone',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -66,7 +65,7 @@ describe('authModuleOktaProvider', () => {
|
||||
expect(startUrl.pathname).toBe('/oauth2/v1/authorize');
|
||||
expect(Object.fromEntries(startUrl.searchParams)).toEqual({
|
||||
response_type: 'code',
|
||||
scope: additionalScopes,
|
||||
scope: 'openid email profile offline_access groups phone',
|
||||
client_id: 'my-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/okta/handler/frame`,
|
||||
state: expect.any(String),
|
||||
|
||||
@@ -249,22 +249,14 @@ describe('pinnipedAuthenticator', () => {
|
||||
expect(fakeSession['oidc:pinniped.test'].code_verifier).toBeDefined();
|
||||
});
|
||||
|
||||
it('requests sufficient scopes for token exchange by default', async () => {
|
||||
it('forwards scopes for token exchange', async () => {
|
||||
const startResponse = await pinnipedAuthenticator.start(
|
||||
startRequest,
|
||||
{ ...startRequest, scope: 'openid username' },
|
||||
authCtx,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
const scopes = searchParams.get('scope')?.split(' ') ?? [];
|
||||
|
||||
expect(scopes).toEqual(
|
||||
expect.arrayContaining([
|
||||
'openid',
|
||||
'pinniped:request-audience',
|
||||
'username',
|
||||
'offline_access',
|
||||
]),
|
||||
);
|
||||
expect(searchParams.get('scope')).toBe('openid username');
|
||||
});
|
||||
|
||||
it('encodes OAuth state in query param', async () => {
|
||||
|
||||
@@ -127,7 +127,6 @@ export class PinnipedStrategyCache {
|
||||
client_secret: this.config.getString('clientSecret'),
|
||||
redirect_uris: [this.callbackUrl],
|
||||
response_types: ['code'],
|
||||
scope: this.config.getOptionalString('scope') || '',
|
||||
id_token_signed_response_alg: 'ES256',
|
||||
});
|
||||
const providerStrategy = new OidcStrategy(
|
||||
@@ -154,7 +153,20 @@ export class PinnipedStrategyCache {
|
||||
/** @public */
|
||||
export const pinnipedAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform: async (_r, _c) => ({ profile: {} }),
|
||||
scopes: {
|
||||
required: [
|
||||
'openid',
|
||||
'pinniped:request-audience',
|
||||
'username',
|
||||
'offline_access',
|
||||
],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
if (config.has('scope')) {
|
||||
throw new Error(
|
||||
'The pinniped provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.',
|
||||
);
|
||||
}
|
||||
return new PinnipedStrategyCache(callbackUrl, config);
|
||||
},
|
||||
async start(input, ctx): Promise<{ url: string; status?: number }> {
|
||||
@@ -163,9 +175,7 @@ export const pinnipedAuthenticator = createOAuthAuthenticator({
|
||||
const decodedState = decodeOAuthState(input.state);
|
||||
const state = { ...decodedState, audience: stringifiedAudience };
|
||||
const options: Record<string, string> = {
|
||||
scope:
|
||||
input.scope ||
|
||||
'openid pinniped:request-audience username offline_access',
|
||||
scope: input.scope,
|
||||
state: encodeOAuthState(state),
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Config {
|
||||
organizationId: string;
|
||||
scope?: string;
|
||||
consoleEndpoint?: string;
|
||||
additionalScopes?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -163,9 +163,9 @@ describe('vmwareCloudAuthenticator', () => {
|
||||
expect(searchParams.get('redirect_uri')).toBe('http://callbackUrl');
|
||||
});
|
||||
|
||||
it('requests scopes for ID and refresh token', async () => {
|
||||
it('forwards scopes for ID and refresh token', async () => {
|
||||
const startResponse = await vmwareCloudAuthenticator.start(
|
||||
startRequest,
|
||||
{ ...startRequest, scope: 'openid offline_access' },
|
||||
authenticatorCtx,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
@@ -96,6 +96,9 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator<
|
||||
},
|
||||
};
|
||||
},
|
||||
scopes: {
|
||||
required: ['openid', 'offline_access'],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const consoleEndpoint =
|
||||
config.getOptionalString('consoleEndpoint') ??
|
||||
@@ -106,7 +109,12 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator<
|
||||
const clientSecret = '';
|
||||
const authorizationUrl = `${consoleEndpoint}/csp/gateway/discovery`;
|
||||
const tokenUrl = `${consoleEndpoint}/csp/gateway/am/api/auth/token`;
|
||||
const scope = config.getOptionalString('scope') ?? 'openid offline_access';
|
||||
|
||||
if (config.has('scope')) {
|
||||
throw new Error(
|
||||
'The vmware-cloud provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.',
|
||||
);
|
||||
}
|
||||
|
||||
const providerStrategy = new OAuth2Strategy(
|
||||
{
|
||||
@@ -118,7 +126,6 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator<
|
||||
passReqToCallback: false,
|
||||
pkce: true,
|
||||
state: true,
|
||||
scope: scope,
|
||||
customHeaders: {
|
||||
Authorization: `Basic ${encodeClientCredentials(
|
||||
clientId,
|
||||
|
||||
Reference in New Issue
Block a user