Merge pull request #20580 from ataylorme/feat/okta-auth-scope-option
Allow user-defined scopes for Okta auth in config yaml
This commit is contained in:
@@ -17,13 +17,14 @@
|
||||
import { OktaAuthProvider } from './provider';
|
||||
import * as helpers from '../../lib/passport/PassportStrategyHelper';
|
||||
import { OAuthResult } from '../../lib/oauth';
|
||||
import { AuthResolverContext } from '../types';
|
||||
import { AuthResolverContext, OAuthStartResponse } from '../types';
|
||||
|
||||
jest.mock('../../lib/passport/PassportStrategyHelper', () => {
|
||||
return {
|
||||
executeFrameHandlerStrategy: jest.fn(),
|
||||
executeRefreshTokenStrategy: jest.fn(),
|
||||
executeFetchUserProfileStrategy: jest.fn(),
|
||||
executeRedirectStrategy: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -34,6 +35,11 @@ const mockFrameHandler = jest.spyOn(
|
||||
() => Promise<{ result: OAuthResult; privateInfo: any }>
|
||||
>;
|
||||
|
||||
const mockRedirectStrategy = jest.spyOn(
|
||||
helpers,
|
||||
'executeRedirectStrategy',
|
||||
) as unknown as jest.MockedFunction<() => Promise<OAuthStartResponse>>;
|
||||
|
||||
describe('createOktaProvider', () => {
|
||||
it('should auth', async () => {
|
||||
const provider = new OktaAuthProvider({
|
||||
@@ -97,4 +103,41 @@ describe('createOktaProvider', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass a custom scope to start and refresh requests', async () => {
|
||||
const additionalScopes = 'groups';
|
||||
const reqScope = 'openid profile email offline_access';
|
||||
const combinedScope = `${reqScope} ${additionalScopes}`;
|
||||
const provider = new OktaAuthProvider({
|
||||
resolverContext: {} as AuthResolverContext,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: {
|
||||
email: fullProfile.emails![0]!.value,
|
||||
displayName: fullProfile.displayName,
|
||||
},
|
||||
}),
|
||||
audience: 'http://example.com',
|
||||
clientId: 'mock',
|
||||
clientSecret: 'mock',
|
||||
callbackUrl: 'mock',
|
||||
additionalScopes,
|
||||
});
|
||||
|
||||
mockRedirectStrategy.mockResolvedValueOnce({
|
||||
url: 'http://example.com/',
|
||||
});
|
||||
|
||||
const req: any = {
|
||||
state: {
|
||||
nonce: 'nonce',
|
||||
env: 'development',
|
||||
},
|
||||
scope: reqScope,
|
||||
};
|
||||
|
||||
await provider.start(req);
|
||||
const mockCallScope = (mockRedirectStrategy.mock.calls[0] as any)?.[2]
|
||||
?.scope;
|
||||
expect(mockCallScope).toBe(combinedScope);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ export type OktaAuthProviderOptions = OAuthProviderOptions & {
|
||||
signInResolver?: SignInResolver<OAuthResult>;
|
||||
authHandler: AuthHandler<OAuthResult>;
|
||||
resolverContext: AuthResolverContext;
|
||||
additionalScopes?: string;
|
||||
};
|
||||
|
||||
export class OktaAuthProvider implements OAuthHandlers {
|
||||
@@ -67,6 +68,7 @@ export class OktaAuthProvider implements OAuthHandlers {
|
||||
private readonly signInResolver?: SignInResolver<OAuthResult>;
|
||||
private readonly authHandler: AuthHandler<OAuthResult>;
|
||||
private readonly resolverContext: AuthResolverContext;
|
||||
private readonly additionalScopes: string;
|
||||
|
||||
/**
|
||||
* Due to passport-okta-oauth forcing options.state = true,
|
||||
@@ -89,6 +91,7 @@ export class OktaAuthProvider implements OAuthHandlers {
|
||||
this.signInResolver = options.signInResolver;
|
||||
this.authHandler = options.authHandler;
|
||||
this.resolverContext = options.resolverContext;
|
||||
this.additionalScopes = options.additionalScopes || '';
|
||||
|
||||
this.strategy = new OktaStrategy(
|
||||
{
|
||||
@@ -125,11 +128,19 @@ export class OktaAuthProvider implements OAuthHandlers {
|
||||
);
|
||||
}
|
||||
|
||||
private combineScopeStrings(scopesA: string, scopesB: string) {
|
||||
const scopesAArray = scopesA.split(' ');
|
||||
const scopesBArray = scopesB.split(' ');
|
||||
const combinedScopes = new Set([...scopesAArray, ...scopesBArray]);
|
||||
return Array.from(combinedScopes).join(' ');
|
||||
}
|
||||
|
||||
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
|
||||
const scope = this.combineScopeStrings(req.scope, this.additionalScopes);
|
||||
return await executeRedirectStrategy(req, this.strategy, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
scope: req.scope,
|
||||
scope: scope,
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
@@ -147,12 +158,9 @@ export class OktaAuthProvider implements OAuthHandlers {
|
||||
}
|
||||
|
||||
async refresh(req: OAuthRefreshRequest) {
|
||||
const scope = this.combineScopeStrings(req.scope, this.additionalScopes);
|
||||
const { accessToken, refreshToken, params } =
|
||||
await executeRefreshTokenStrategy(
|
||||
this.strategy,
|
||||
req.refreshToken,
|
||||
req.scope,
|
||||
);
|
||||
await executeRefreshTokenStrategy(this.strategy, req.refreshToken, scope);
|
||||
|
||||
const fullProfile = await executeFetchUserProfileStrategy(
|
||||
this.strategy,
|
||||
@@ -230,6 +238,8 @@ export const okta = createAuthProviderIntegration({
|
||||
const callbackUrl =
|
||||
customCallbackUrl ||
|
||||
`${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
const additionalScopes =
|
||||
envConfig.getOptionalString('additionalScopes');
|
||||
|
||||
// This is a safe assumption as `passport-okta-oauth` uses the audience
|
||||
// as the base for building the authorization, token, and user info URLs.
|
||||
@@ -254,6 +264,7 @@ export const okta = createAuthProviderIntegration({
|
||||
authHandler,
|
||||
signInResolver: options?.signIn?.resolver,
|
||||
resolverContext,
|
||||
additionalScopes,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
|
||||
Reference in New Issue
Block a user