diff --git a/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.spec.ts index 636ab269..b9d65d77 100644 --- a/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.spec.ts @@ -1,5 +1,5 @@ import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { of, throwError } from 'rxjs'; +import { of, Subject, throwError } from 'rxjs'; import { delay } from 'rxjs/operators'; import { mockProvider } from '../../test/auto-mock'; import { AuthStateService } from '../auth-state/auth-state.service'; @@ -10,6 +10,7 @@ import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.se import { SilentRenewService } from '../iframe/silent-renew.service'; import { LoggerService } from '../logging/logger.service'; import { LoginResponse } from '../login/login-response'; +import { EventTypes } from '../public-events/event-types'; import { PublicEventsService } from '../public-events/public-events.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { UserService } from '../user-data/user.service'; @@ -30,6 +31,8 @@ describe('RefreshSessionService ', () => { let refreshSessionIframeService: RefreshSessionIframeService; let refreshSessionRefreshTokenService: RefreshSessionRefreshTokenService; let authWellKnownService: AuthWellKnownService; + let publicEventsService: PublicEventsService; + let userService: UserService; beforeEach(() => { TestBed.configureTestingModule({ @@ -63,6 +66,8 @@ describe('RefreshSessionService ', () => { silentRenewService = TestBed.inject(SilentRenewService); authWellKnownService = TestBed.inject(AuthWellKnownService); storagePersistenceService = TestBed.inject(StoragePersistenceService); + publicEventsService = TestBed.inject(PublicEventsService); + userService = TestBed.inject(UserService); }); it('should create', () => { @@ -285,6 +290,143 @@ describe('RefreshSessionService ', () => { }); })); + it('returns tokens from the completed refresh result when auth-state getters are stale', waitForAsync(() => { + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + const refreshResult = { + authResult: { + id_token: 'fresh-id-token', + access_token: 'fresh-access-token', + }, + } as CallbackContext; + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn( + refreshSessionService as any, + 'waitForRunningRefreshSessionIfRequired' + ).and.returnValue(of(false)); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(refreshResult)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(authStateService, 'getIdToken').and.returnValue('stale-id-token'); + spyOn(authStateService, 'getAccessToken').and.returnValue( + 'stale-access-token' + ); + spyOn(userService, 'getUserDataFromStore').and.returnValue({ + sub: '123', + } as any); + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + idToken: 'fresh-id-token', + accessToken: 'fresh-access-token', + userData: { sub: '123' }, + isAuthenticated: true, + configId: 'configId1', + }); + }); + })); + + it('falls back to auth-state getters when no refresh auth result is available', waitForAsync(() => { + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn( + refreshSessionService as any, + 'waitForRunningRefreshSessionIfRequired' + ).and.returnValue(of(false)); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(authStateService, 'getIdToken').and.returnValue('stored-id-token'); + spyOn(authStateService, 'getAccessToken').and.returnValue( + 'stored-access-token' + ); + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result.idToken).toBe('stored-id-token'); + expect(result.accessToken).toBe('stored-access-token'); + }); + })); + + it('waits for the running renew process to publish a refresh result before emitting', fakeAsync(() => { + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + const events$ = new Subject(); + let actualResult: LoginResponse | undefined; + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); + spyOn(publicEventsService, 'registerForEvents').and.returnValue(events$); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(authStateService, 'getIdToken').and.returnValue('updated-id-token'); + spyOn(authStateService, 'getAccessToken').and.returnValue( + 'updated-access-token' + ); + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + actualResult = result; + }); + + tick(); + expect(actualResult).toBeUndefined(); + + events$.next({ + type: EventTypes.NewAuthenticationResult, + value: { + configId: 'configId1', + isRenewProcess: true, + }, + }); + tick(); + + expect(actualResult).toEqual({ + idToken: 'updated-id-token', + accessToken: 'updated-access-token', + userData: undefined, + isAuthenticated: true, + configId: 'configId1', + }); + })); + it('calls start refresh session and waits for completed, returns idtoken and accesstoken if auth is true', waitForAsync(() => { spyOn( flowHelper, diff --git a/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.ts b/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.ts index 8f098bb2..2c08c1d2 100644 --- a/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.ts @@ -8,6 +8,7 @@ import { timer, } from 'rxjs'; import { + catchError, filter, map, mergeMap, @@ -26,6 +27,8 @@ import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.se import { SilentRenewService } from '../iframe/silent-renew.service'; import { LoggerService } from '../logging/logger.service'; import { LoginResponse } from '../login/login-response'; +import { EventTypes } from '../public-events/event-types'; +import { PublicEventsService } from '../public-events/public-events.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { UserService } from '../user-data/user.service'; import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; @@ -50,6 +53,7 @@ export class RefreshSessionService { private readonly refreshSessionRefreshTokenService = inject( RefreshSessionRefreshTokenService ); + private readonly publicEventsService = inject(PublicEventsService); private readonly userService = inject(UserService); userForceRefreshSession( @@ -85,15 +89,31 @@ export class RefreshSessionService { }; if (this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)) { - return this.startRefreshSession(config, allConfigs, mergedParams).pipe( - map(() => { + return this.waitForRunningRefreshSessionIfRequired(config).pipe( + switchMap((shouldWaitForRunningRenew) => { + if (shouldWaitForRunningRenew) { + return of(null); + } + + return this.startRefreshSession(config, allConfigs, mergedParams); + }), + map((refreshSessionResult) => { const isAuthenticated = this.authStateService.areAuthStorageTokensValid(config); if (isAuthenticated) { + const authResult = + refreshSessionResult && typeof refreshSessionResult !== 'boolean' + ? refreshSessionResult.authResult + : null; + return { - idToken: this.authStateService.getIdToken(config), - accessToken: this.authStateService.getAccessToken(config), + idToken: + authResult?.id_token ?? + this.authStateService.getIdToken(config), + accessToken: + authResult?.access_token ?? + this.authStateService.getAccessToken(config), userData: this.userService.getUserDataFromStore(config), isAuthenticated, configId, @@ -216,12 +236,17 @@ export class RefreshSessionService { return of(null); } + this.flowsDataService.setSilentRenewRunning(config); + return this.authWellKnownService .queryAndStoreAuthWellKnownEndPoints(config) .pipe( - switchMap(() => { - this.flowsDataService.setSilentRenewRunning(config); + catchError((error) => { + this.flowsDataService.resetSilentRenewRunning(config); + return throwError(() => error); + }), + switchMap(() => { if (this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)) { // Refresh Session using Refresh tokens return this.refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens( @@ -239,4 +264,36 @@ export class RefreshSessionService { }) ); } + + private waitForRunningRefreshSessionIfRequired( + config: OpenIdConfiguration + ): Observable { + const isSilentRenewRunning = + this.flowsDataService.isSilentRenewRunning(config); + + if (!isSilentRenewRunning) { + return of(false); + } + + return this.publicEventsService.registerForEvents().pipe( + filter( + (notification) => + notification.type === EventTypes.NewAuthenticationResult + ), + map((notification) => notification.value), + filter( + ( + authStateResult + ): authStateResult is { + isRenewProcess: boolean; + configId?: string; + } => + !!authStateResult && + authStateResult.isRenewProcess === true && + authStateResult.configId === config.configId + ), + take(1), + map(() => true) + ); + } } diff --git a/projects/integration-tests/src/tests/token-refresh-refresh-tokens.spec.ts b/projects/integration-tests/src/tests/token-refresh-refresh-tokens.spec.ts new file mode 100644 index 00000000..d8b6d3bb --- /dev/null +++ b/projects/integration-tests/src/tests/token-refresh-refresh-tokens.spec.ts @@ -0,0 +1,123 @@ +import { provideHttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + AuthModule, + OidcSecurityService, + StsConfigLoader, +} from 'angular-auth-oidc-client'; +import { firstValueFrom, of } from 'rxjs'; + +const idpHost = 'http://localhost:8081'; +const configId = 'idp1'; + +@Injectable() +class TestStsConfigLoaderWithRefreshTokens extends StsConfigLoader { + override loadConfigs() { + return of([ + { + configId, + authority: `${idpHost}/idp1`, + redirectUrl: `${window.location.origin}/callback`, + postLogoutRedirectUri: `${window.location.origin}/`, + clientId: 'client-idp1', + responseType: 'code', + scope: 'openid profile email offline_access', + silentRenew: false, + useRefreshToken: true, + renewUserInfoAfterTokenRenew: true, + }, + ]); + } +} + +describe('Manual refresh-token concurrency', () => { + let oidcSecurityService: OidcSecurityService; + + beforeAll(async () => { + const healthResponse = await fetch(`${idpHost}/health`); + expect(healthResponse.ok).toBe(true); + }); + + beforeEach(() => { + localStorage.clear(); + TestBed.configureTestingModule({ + imports: [ + AuthModule.forRoot({ + loader: { + provide: StsConfigLoader, + useClass: TestStsConfigLoaderWithRefreshTokens, + }, + }), + ], + providers: [provideHttpClient()], + }); + + oidcSecurityService = TestBed.inject(OidcSecurityService); + }); + + afterEach(() => { + oidcSecurityService.logoffLocalMultiple(); + }); + + it('keeps concurrent forceRefreshSession calls aligned with the freshly stored tokens', async () => { + const authorizeUrl = await new Promise((resolve) => { + oidcSecurityService.authorize(configId, { + customParams: { + test_response_mode: 'json', + }, + urlHandler: resolve, + }); + }); + + const authorizeResponse = await fetch(authorizeUrl, { + credentials: 'include', + }); + const authorizeResult = await authorizeResponse.json(); + + expect(authorizeResult.redirect_to).toContain('/callback?code='); + + const loginResponse = await firstValueFrom( + oidcSecurityService.checkAuth(authorizeResult.redirect_to, configId) + ); + + expect(loginResponse.isAuthenticated).toBe(true); + + const initialAccessToken = await firstValueFrom( + oidcSecurityService.getAccessToken(configId) + ); + const initialRefreshToken = await firstValueFrom( + oidcSecurityService.getRefreshToken(configId) + ); + + const [firstRefreshResult, secondRefreshResult] = await Promise.all([ + firstValueFrom( + oidcSecurityService.forceRefreshSession(undefined, configId) + ), + firstValueFrom( + oidcSecurityService.forceRefreshSession(undefined, configId) + ), + ]); + + const currentAccessToken = await firstValueFrom( + oidcSecurityService.getAccessToken(configId) + ); + const currentIdToken = await firstValueFrom( + oidcSecurityService.getIdToken(configId) + ); + const currentRefreshToken = await firstValueFrom( + oidcSecurityService.getRefreshToken(configId) + ); + + expect(firstRefreshResult.isAuthenticated).toBe(true); + expect(secondRefreshResult.isAuthenticated).toBe(true); + expect(firstRefreshResult.accessToken).not.toBe(initialAccessToken); + expect(secondRefreshResult.accessToken).toBe( + firstRefreshResult.accessToken + ); + expect(secondRefreshResult.idToken).toBe(firstRefreshResult.idToken); + expect(currentAccessToken).toBe(firstRefreshResult.accessToken); + expect(currentIdToken).toBe(firstRefreshResult.idToken); + expect(currentRefreshToken).not.toBe(initialRefreshToken); + }); +}); diff --git a/projects/integration-tests/test-idp-server/src/TestIdpServer.ts b/projects/integration-tests/test-idp-server/src/TestIdpServer.ts index 90f5d9bf..05cbcf89 100644 --- a/projects/integration-tests/test-idp-server/src/TestIdpServer.ts +++ b/projects/integration-tests/test-idp-server/src/TestIdpServer.ts @@ -1,9 +1,9 @@ -import express, { Application, Request, Response, NextFunction } from 'express'; -import { Server } from 'http'; -import cors from 'cors'; import cookieParser from 'cookie-parser'; -import { SignJWT, exportJWK, importPKCS8, KeyLike } from 'jose'; +import cors from 'cors'; import crypto from 'crypto'; +import express, { Application, NextFunction, Request, Response } from 'express'; +import { Server } from 'http'; +import { exportJWK, importPKCS8, KeyLike, SignJWT } from 'jose'; export interface TestIdpServerOptions { port?: number; @@ -34,6 +34,7 @@ export class TestIdpServer { private realms: string[]; private keyPairs: Record = {}; private activeSessions: Map = new Map(); + private refreshTokenSessions: Map = new Map(); private loggedOutSessions: Set = new Set(); constructor(options: TestIdpServerOptions = {}) { @@ -72,26 +73,39 @@ export class TestIdpServer { const realmPath = `/${realm}`; // OIDC Discovery endpoint - this.app.get(`${realmPath}/.well-known/openid-configuration`, (req: Request, res: Response) => { - res.json({ - issuer: `http://localhost:${this.port}${realmPath}`, - authorization_endpoint: `http://localhost:${this.port}${realmPath}/authorize`, - token_endpoint: `http://localhost:${this.port}${realmPath}/token`, - userinfo_endpoint: `http://localhost:${this.port}${realmPath}/userinfo`, - end_session_endpoint: `http://localhost:${this.port}${realmPath}/logout`, - jwks_uri: `http://localhost:${this.port}${realmPath}/jwks`, - scopes_supported: this.getSupportedScopes(realm), - response_types_supported: ['code'], - grant_types_supported: ['authorization_code'], - subject_types_supported: ['public'], - id_token_signing_alg_values_supported: ['RS256'], - code_challenge_methods_supported: ['S256', 'plain'] - }); - }); + this.app.get( + `${realmPath}/.well-known/openid-configuration`, + (req: Request, res: Response) => { + res.json({ + issuer: `http://localhost:${this.port}${realmPath}`, + authorization_endpoint: `http://localhost:${this.port}${realmPath}/authorize`, + token_endpoint: `http://localhost:${this.port}${realmPath}/token`, + userinfo_endpoint: `http://localhost:${this.port}${realmPath}/userinfo`, + end_session_endpoint: `http://localhost:${this.port}${realmPath}/logout`, + jwks_uri: `http://localhost:${this.port}${realmPath}/jwks`, + scopes_supported: this.getSupportedScopes(realm), + response_types_supported: ['code'], + grant_types_supported: ['authorization_code', 'refresh_token'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + code_challenge_methods_supported: ['S256', 'plain'], + }); + } + ); // Authorization endpoint this.app.get(`${realmPath}/authorize`, (req: Request, res: Response) => { - const { response_type, client_id, redirect_uri, scope, state, prompt, code_challenge, code_challenge_method } = req.query; + const { + response_type, + client_id, + redirect_uri, + scope, + state, + prompt, + code_challenge, + code_challenge_method, + test_response_mode, + } = req.query; console.log(`Test IDP: ${realm} authorize request:`, { response_type, @@ -129,7 +143,15 @@ export class TestIdpServer { redirectUrl.searchParams.set('code', code); redirectUrl.searchParams.set('state', state as string); - console.log(`Test IDP: ${realm} silent auth success, redirecting to:`, redirectUrl.toString()); + console.log( + `Test IDP: ${realm} silent auth success, redirecting to:`, + redirectUrl.toString() + ); + + if (test_response_mode === 'json') { + return res.json({ redirect_to: redirectUrl.toString() }); + } + return res.redirect(redirectUrl.toString()); } @@ -143,21 +165,58 @@ export class TestIdpServer { res.cookie('SSO_SESSION', 'active', { httpOnly: true, sameSite: 'lax', - maxAge: 3600000 // 1 hour + maxAge: 3600000, // 1 hour }); + + if (test_response_mode === 'json') { + return res.json({ redirect_to: redirectUrl.toString() }); + } + res.redirect(redirectUrl.toString()); }); // Token endpoint this.app.post(`${realmPath}/token`, async (req: Request, res: Response) => { - const { grant_type, code, redirect_uri, client_id, code_verifier } = req.body; + const { + grant_type, + code, + redirect_uri, + client_id, + code_verifier, + refresh_token, + } = req.body; console.log(`Test IDP: ${realm} token request:`, { grant_type, code, - code_verifier: code_verifier ? 'present' : 'missing' + refresh_token, + code_verifier: code_verifier ? 'present' : 'missing', }); + if (grant_type === 'refresh_token') { + const session = this.refreshTokenSessions.get(refresh_token); + + if (!session || session.realm !== realm) { + return res.status(400).json({ error: 'invalid_grant' }); + } + + this.refreshTokenSessions.delete(refresh_token); + + const nextRefreshToken = this.createRefreshToken(realm); + this.refreshTokenSessions.set(nextRefreshToken, session); + + const tokenData = await this.getTokenForRealm(realm, session, false); + + return res.json({ + access_token: `${realm}-access-token-${Date.now()}`, + id_token: tokenData, + refresh_token: nextRefreshToken, + token_type: 'Bearer', + expires_in: 300, + scope: session.scope || this.getScopeString(realm), + }); + } + if (grant_type !== 'authorization_code') { return res.status(400).json({ error: 'unsupported_grant_type' }); } @@ -172,16 +231,29 @@ export class TestIdpServer { if (session.codeChallenge) { if (!code_verifier) { console.log(`Test IDP: ${realm} missing code_verifier for PKCE flow`); - return res.status(400).json({ error: 'invalid_request', error_description: 'code_verifier required' }); + return res + .status(400) + .json({ + error: 'invalid_request', + error_description: 'code_verifier required', + }); } // Verify the code_verifier matches the code_challenge const verifierBuffer = Buffer.from(code_verifier, 'utf-8'); - const challenge = crypto.createHash('sha256').update(verifierBuffer).digest('base64url'); + const challenge = crypto + .createHash('sha256') + .update(verifierBuffer) + .digest('base64url'); if (challenge !== session.codeChallenge) { console.log(`Test IDP: ${realm} code_verifier validation failed`); - return res.status(400).json({ error: 'invalid_grant', error_description: 'code_verifier validation failed' }); + return res + .status(400) + .json({ + error: 'invalid_grant', + error_description: 'code_verifier validation failed', + }); } console.log(`Test IDP: ${realm} PKCE validation successful`); @@ -190,15 +262,27 @@ export class TestIdpServer { // Remove used authorization code this.activeSessions.delete(code); + const supportsRefreshTokens = session.scope + ?.split(' ') + .includes('offline_access'); + const refreshToken = supportsRefreshTokens + ? this.createRefreshToken(realm) + : undefined; + + if (refreshToken) { + this.refreshTokenSessions.set(refreshToken, session); + } + // Get the appropriate test token for this realm with session data const tokenData = await this.getTokenForRealm(realm, session); res.json({ access_token: `${realm}-access-token-${Date.now()}`, id_token: tokenData, + ...(refreshToken ? { refresh_token: refreshToken } : {}), token_type: 'Bearer', expires_in: 300, // 5 minutes to match ID token - scope: session.scope || this.getScopeString(realm) + scope: session.scope || this.getScopeString(realm), }); return; }); @@ -216,12 +300,13 @@ export class TestIdpServer { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.includes(`${realm}-access-token`)) { - res.status(401).json({ error: 'unauthorized' }); - return; - } + res.status(401).json({ error: 'unauthorized' }); + return; + } - res.json(this.getUserInfoForRealm(realm)); - }); + res.json(this.getUserInfoForRealm(realm)); + } + ); // Logout endpoint (OIDC end_session_endpoint) this.app.get(`${realmPath}/logout`, async (req: Request, res: Response) => { @@ -279,19 +364,19 @@ export class TestIdpServer { private getSupportedScopes(realm: string): string[] { switch (realm) { case 'master-idp': - return ['openid', 'profile', 'email']; + return ['openid', 'profile', 'email', 'offline_access']; case 'secondary-idp-1': - return ['openid', 'profile']; + return ['openid', 'profile', 'offline_access']; case 'secondary-idp-2': - return ['openid', 'email']; + return ['openid', 'email', 'offline_access']; case 'idp1': - return ['openid', 'profile', 'email']; + return ['openid', 'profile', 'email', 'offline_access']; case 'idp2': - return ['openid', 'profile']; + return ['openid', 'profile', 'offline_access']; case 'idp3': - return ['openid', 'email']; + return ['openid', 'email', 'offline_access']; default: - return ['openid']; + return ['openid', 'offline_access']; } } @@ -299,9 +384,13 @@ export class TestIdpServer { return this.getSupportedScopes(realm).join(' '); } - private async getTokenForRealm(realm: string, session?: Session): Promise { + private async getTokenForRealm( + realm: string, + session?: Session, + includeNonce = true + ): Promise { const keyPair = this.getKeyPairForRealm(realm); - const claims = this.getClaimsForRealm(realm, session); + const claims = this.getClaimsForRealm(realm, session, includeNonce); // Add a unique timestamp with millisecond precision to ensure different IATs // Add light jitter (0-10ms) based on realm to ensure different IATs @@ -320,6 +409,12 @@ export class TestIdpServer { return jwt; } + private createRefreshToken(realm: string): string { + return `${realm}-refresh-token-${Date.now()}-${Math.random() + .toString(36) + .slice(2)}`; + } + private getKeyPairForRealm(realm: string): KeyPair { const realmKey = this.mapRealmToKey(realm); const keyPair = this.keyPairs[realmKey]; @@ -375,21 +470,25 @@ export class TestIdpServer { } } - private getClaimsForRealm(realm: string, session?: Session): any { + private getClaimsForRealm( + realm: string, + session?: Session, + includeNonce = true + ): any { const userInfo = this.getUserInfoForRealm(realm); const scope = session?.scope || this.getScopeString(realm); // Parse the scope to bp: prefixed roles const scopes = scope.split(' '); const bpRoles = scopes - .filter(s => s.startsWith('bp:')) - .map(s => s.substring(3)); + .filter((s) => s.startsWith('bp:')) + .map((s) => s.substring(3)); return { ...userInfo, - nonce: session?.nonce || 'test-nonce', + ...(includeNonce ? { nonce: session?.nonce || 'test-nonce' } : {}), azp: session?.clientId || this.getClientIdForRealm(realm), - scope: scope + scope: scope, }; } @@ -440,15 +539,24 @@ export class TestIdpServer { private async generateTestData(): Promise { // Generate key pairs for each realm - const keyRealms = ['master', 'secondary1', 'secondary2', 'idp1', 'idp2', 'idp3']; + const keyRealms = [ + 'master', + 'secondary1', + 'secondary2', + 'idp1', + 'idp2', + 'idp3', + ]; for (const realm of keyRealms) { const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, - privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }); - const kid = `${realm}-key-${Date.now()}-${Math.random().toString(36).substring(7)}`; + const kid = `${realm}-key-${Date.now()}-${Math.random() + .toString(36) + .substring(7)}`; const privateKeyJose = await importPKCS8(privateKey, 'RS256'); const publicKeyBuffer = crypto.createPublicKey(publicKey); const publicJWK = await exportJWK(publicKeyBuffer);