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:
Patrik Oldsberg
2023-11-14 11:45:20 +01:00
committed by GitHub
5 changed files with 71 additions and 7 deletions
@@ -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, {