Skip to content

Commit 7296d6d

Browse files
fix: refresh authWellKnownEndpoints when the server updates them
fix #1983
1 parent 6fcacb4 commit 7296d6d

6 files changed

Lines changed: 96 additions & 68 deletions

File tree

projects/angular-auth-oidc-client/src/lib/config/auth-well-known/auth-well-known-data.service.spec.ts

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,12 @@ describe('AuthWellKnownDataService', () => {
216216
});
217217
}));
218218

219-
it('should merge the mapped endpoints with the provided endpoints', waitForAsync(() => {
219+
it('maps only server-fetched endpoints without merging config overrides', waitForAsync(() => {
220220
spyOn(dataService, 'get').and.returnValue(of(DUMMY_WELL_KNOWN_DOCUMENT));
221221

222222
const expected: AuthWellKnownEndpoints = {
223-
endSessionEndpoint: 'config-endSessionEndpoint',
224-
revocationEndpoint: 'config-revocationEndpoint',
223+
endSessionEndpoint: DUMMY_WELL_KNOWN_DOCUMENT.end_session_endpoint,
224+
tokenEndpoint: DUMMY_WELL_KNOWN_DOCUMENT.token_endpoint,
225225
jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri
226226
};
227227

@@ -236,6 +236,10 @@ describe('AuthWellKnownDataService', () => {
236236
})
237237
.subscribe((result) => {
238238
expect(result).toEqual(jasmine.objectContaining(expected));
239+
expect(result.endSessionEndpoint).not.toBe('config-endSessionEndpoint');
240+
expect(result.tokenEndpoint).toBe(
241+
DUMMY_WELL_KNOWN_DOCUMENT.token_endpoint
242+
);
239243
});
240244
}));
241245

@@ -324,34 +328,37 @@ describe('AuthWellKnownDataService', () => {
324328
});
325329
}));
326330

327-
it('should merge the mapped endpoints with the provided endpoints and ignore issuer/authwellknownUrl mismatch', waitForAsync(() => {
331+
it('throws error for issuer mismatch even when authWellknownEndpoints has issuer override', waitForAsync(() => {
332+
const loggerSpy = spyOn(loggerService, 'logError');
328333
const maliciousWellKnown = {
329334
...DUMMY_WELL_KNOWN_DOCUMENT,
330335
issuer: DUMMY_MALICIOUS_URL
331336
};
332337

333338
spyOn(dataService, 'get').and.returnValue(of(maliciousWellKnown));
334339

335-
const expected: AuthWellKnownEndpoints = {
336-
endSessionEndpoint: 'config-endSessionEndpoint',
337-
revocationEndpoint: 'config-revocationEndpoint',
338-
jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri,
339-
issuer: DUMMY_WELL_KNOWN_DOCUMENT.issuer,
340+
const config = {
341+
configId: 'configId1',
342+
authWellknownEndpointUrl: DUMMY_WELL_KNOWN_DOCUMENT.issuer,
343+
authWellknownEndpoints: {
344+
endSessionEndpoint: 'config-endSessionEndpoint',
345+
revocationEndpoint: 'config-revocationEndpoint',
346+
issuer: DUMMY_WELL_KNOWN_DOCUMENT.issuer
347+
},
340348
};
341349

342-
service
343-
.getWellKnownEndPointsForConfig({
344-
configId: 'configId1',
345-
authWellknownEndpointUrl: DUMMY_WELL_KNOWN_DOCUMENT.issuer,
346-
authWellknownEndpoints: {
347-
endSessionEndpoint: 'config-endSessionEndpoint',
348-
revocationEndpoint: 'config-revocationEndpoint',
349-
issuer: DUMMY_WELL_KNOWN_DOCUMENT.issuer
350-
},
351-
})
352-
.subscribe((result) => {
353-
expect(result).toEqual(jasmine.objectContaining(expected));
354-
});
350+
service.getWellKnownEndPointsForConfig(config).subscribe({
351+
next: (result) => {
352+
fail(`Retrieval was supposed to fail. Well known endpoints returned : ${JSON.stringify(result)}`);
353+
},
354+
error: (error) => {
355+
expect(loggerSpy).toHaveBeenCalledOnceWith(
356+
config,
357+
`Issuer mismatch. Well known issuer ${DUMMY_MALICIOUS_URL} does not match configured well known url ${DUMMY_WELL_KNOWN_DOCUMENT.issuer}`
358+
);
359+
expect(error.message).toEqual(`Issuer mismatch. Well known issuer ${DUMMY_MALICIOUS_URL} does not match configured well known url ${DUMMY_WELL_KNOWN_DOCUMENT.issuer}`);
360+
}
361+
});
355362
}));
356363
});
357364
});

projects/angular-auth-oidc-client/src/lib/config/auth-well-known/auth-well-known-data.service.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class AuthWellKnownDataService {
1616
getWellKnownEndPointsForConfig(
1717
config: OpenIdConfiguration
1818
): Observable<AuthWellKnownEndpoints> {
19-
const { authWellknownEndpointUrl, authWellknownEndpoints = {} } = config;
19+
const { authWellknownEndpointUrl } = config;
2020

2121
if (!authWellknownEndpointUrl) {
2222
const errorMessage = 'no authWellknownEndpoint given!';
@@ -43,16 +43,12 @@ export class AuthWellKnownDataService {
4343
wellKnownEndpoints.pushed_authorization_request_endpoint,
4444
} as AuthWellKnownEndpoints)
4545
),
46-
map((mappedWellKnownEndpoints) => ({
47-
...mappedWellKnownEndpoints,
48-
...authWellknownEndpoints,
49-
})),
5046
tap(
5147
(wellKnownEndpoints) => {
5248
const issuer = wellKnownEndpoints.issuer || "";
5349
const wellKnownSuffix = config.authWellknownUrlSuffix || WELL_KNOWN_SUFFIX;
5450
const configuredWellKnownEndpoint = authWellknownEndpointUrl.replace(wellKnownSuffix, "");
55-
51+
5652
if (!config.strictIssuerValidationOnWellKnownRetrievalOff && issuer !== configuredWellKnownEndpoint && issuer !== `${configuredWellKnownEndpoint}/`) {
5753
const errorMessage = `Issuer mismatch. Well known issuer ${wellKnownEndpoints.issuer} does not match configured well known url ${authWellknownEndpointUrl}`;
5854

projects/angular-auth-oidc-client/src/lib/config/auth-well-known/auth-well-known.service.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('AuthWellKnownService', () => {
4848
});
4949
}));
5050

51-
it('getAuthWellKnownEndPoints calls always dataservice', waitForAsync(() => {
51+
it('calls dataservice when no explicit endpoints are configured', waitForAsync(() => {
5252
const dataServiceSpy = spyOn(
5353
dataService,
5454
'getWellKnownEndPointsForConfig'
@@ -106,5 +106,58 @@ describe('AuthWellKnownService', () => {
106106
},
107107
});
108108
}));
109+
110+
it('does not call dataservice when authWellknownEndpoints is explicitly configured', waitForAsync(() => {
111+
const explicitEndpoints = { issuer: 'https://explicit.example.com', tokenEndpoint: 'https://explicit.example.com/token' };
112+
const dataServiceSpy = spyOn(dataService, 'getWellKnownEndPointsForConfig');
113+
114+
service
115+
.queryAndStoreAuthWellKnownEndPoints({
116+
configId: 'configId1',
117+
authWellknownEndpoints: explicitEndpoints,
118+
})
119+
.subscribe((result) => {
120+
expect(dataServiceSpy).not.toHaveBeenCalled();
121+
expect(result).toEqual(explicitEndpoints);
122+
});
123+
}));
124+
125+
it('stores and returns explicit endpoints without discovery request', waitForAsync(() => {
126+
const explicitEndpoints = { issuer: 'https://explicit.example.com', tokenEndpoint: 'https://explicit.example.com/token' };
127+
const storeSpy = spyOn(service, 'storeWellKnownEndpoints');
128+
129+
spyOn(dataService, 'getWellKnownEndPointsForConfig');
130+
131+
service
132+
.queryAndStoreAuthWellKnownEndPoints({
133+
configId: 'configId1',
134+
authWellknownEndpoints: explicitEndpoints,
135+
})
136+
.subscribe((result) => {
137+
expect(storeSpy).toHaveBeenCalledOnceWith(
138+
jasmine.objectContaining({ configId: 'configId1' }),
139+
explicitEndpoints
140+
);
141+
expect(result).toEqual(explicitEndpoints);
142+
});
143+
}));
144+
145+
it('always fetches fresh endpoints in discovery mode, ignoring previously stored values', waitForAsync(() => {
146+
const freshEndpoints = { issuer: 'https://server.example.com', tokenEndpoint: 'https://server.example.com/token/fresh' };
147+
const dataServiceSpy = spyOn(dataService, 'getWellKnownEndPointsForConfig').and.returnValue(of(freshEndpoints));
148+
const storeSpy = spyOn(service, 'storeWellKnownEndpoints');
149+
150+
// config has NO explicit authWellknownEndpoints → discovery path
151+
service
152+
.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' })
153+
.subscribe((result) => {
154+
expect(dataServiceSpy).toHaveBeenCalled();
155+
expect(storeSpy).toHaveBeenCalledOnceWith(
156+
jasmine.objectContaining({ configId: 'configId1' }),
157+
freshEndpoints
158+
);
159+
expect(result).toEqual(freshEndpoints);
160+
});
161+
}));
109162
});
110163
});

projects/angular-auth-oidc-client/src/lib/config/auth-well-known/auth-well-known.service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { inject, Injectable } from '@angular/core';
2-
import { Observable, throwError } from 'rxjs';
2+
import { Observable, of, throwError } from 'rxjs';
33
import { catchError, tap } from 'rxjs/operators';
44
import { EventTypes } from '../../public-events/event-types';
55
import { PublicEventsService } from '../../public-events/public-events.service';
@@ -39,6 +39,12 @@ export class AuthWellKnownService {
3939
);
4040
}
4141

42+
if (config.authWellknownEndpoints) {
43+
this.storeWellKnownEndpoints(config, config.authWellknownEndpoints);
44+
45+
return of(config.authWellknownEndpoints);
46+
}
47+
4248
return this.dataService.getWellKnownEndPointsForConfig(config).pipe(
4349
tap((mappedWellKnownEndpoints) =>
4450
this.storeWellKnownEndpoints(config, mappedWellKnownEndpoints)

projects/angular-auth-oidc-client/src/lib/config/config.service.spec.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { mockAbstractProvider, mockProvider } from '../../test/auto-mock';
44
import { LoggerService } from '../logging/logger.service';
55
import { EventTypes } from '../public-events/event-types';
66
import { PublicEventsService } from '../public-events/public-events.service';
7-
import { StoragePersistenceService } from '../storage/storage-persistence.service';
87
import { PlatformProvider } from '../utils/platform-provider/platform.provider';
98
import { AuthWellKnownService } from './auth-well-known/auth-well-known.service';
109
import { ConfigurationService } from './config.service';
@@ -16,7 +15,6 @@ describe('Configuration Service', () => {
1615
let configService: ConfigurationService;
1716
let publicEventsService: PublicEventsService;
1817
let authWellKnownService: AuthWellKnownService;
19-
let storagePersistenceService: StoragePersistenceService;
2018
let configValidationService: ConfigValidationService;
2119
let platformProvider: PlatformProvider;
2220
let stsConfigLoader: StsConfigLoader;
@@ -27,7 +25,6 @@ describe('Configuration Service', () => {
2725
ConfigurationService,
2826
mockProvider(LoggerService),
2927
PublicEventsService,
30-
mockProvider(StoragePersistenceService),
3128
ConfigValidationService,
3229
mockProvider(PlatformProvider),
3330
mockProvider(AuthWellKnownService),
@@ -40,7 +37,6 @@ describe('Configuration Service', () => {
4037
configService = TestBed.inject(ConfigurationService);
4138
publicEventsService = TestBed.inject(PublicEventsService);
4239
authWellKnownService = TestBed.inject(AuthWellKnownService);
43-
storagePersistenceService = TestBed.inject(StoragePersistenceService);
4440
stsConfigLoader = TestBed.inject(StsConfigLoader);
4541
platformProvider = TestBed.inject(PlatformProvider);
4642
configValidationService = TestBed.inject(ConfigValidationService);
@@ -141,33 +137,24 @@ describe('Configuration Service', () => {
141137
});
142138
}));
143139

144-
it(`sets authWellKnownEndPoints on config if authWellKnownEndPoints is stored`, waitForAsync(() => {
140+
it(`does not set authWellKnownEndPoints on config from storage`, waitForAsync(() => {
145141
const configs = [{ configId: 'configId1' }];
146142

147143
spyOn(configService as any, 'loadConfigs').and.returnValue(of(configs));
148144
spyOn(configValidationService, 'validateConfig').and.returnValue(true);
149145
const consoleSpy = spyOn(console, 'warn');
150146

151-
spyOn(storagePersistenceService, 'read').and.returnValue({
152-
issuer: 'auth-well-known',
153-
});
154-
155147
configService.getOpenIDConfiguration('configId1').subscribe((config) => {
156-
expect(config?.authWellknownEndpoints).toEqual({
157-
issuer: 'auth-well-known',
158-
});
159-
expect(consoleSpy).not.toHaveBeenCalled()
148+
expect(config?.authWellknownEndpoints).toBeUndefined();
149+
expect(consoleSpy).not.toHaveBeenCalled();
160150
});
161151
}));
162152

163-
it(`fires ConfigLoaded if authWellKnownEndPoints is stored`, waitForAsync(() => {
153+
it(`fires ConfigLoaded when configuration is loaded`, waitForAsync(() => {
164154
const configs = [{ configId: 'configId1' }];
165155

166156
spyOn(configService as any, 'loadConfigs').and.returnValue(of(configs));
167157
spyOn(configValidationService, 'validateConfig').and.returnValue(true);
168-
spyOn(storagePersistenceService, 'read').and.returnValue({
169-
issuer: 'auth-well-known',
170-
});
171158

172159
const spy = spyOn(publicEventsService, 'fireEvent');
173160

@@ -189,7 +176,6 @@ describe('Configuration Service', () => {
189176

190177
spyOn(configService as any, 'loadConfigs').and.returnValue(of(configs));
191178
spyOn(configValidationService, 'validateConfig').and.returnValue(true);
192-
spyOn(storagePersistenceService, 'read').and.returnValue(null);
193179

194180
const fireEventSpy = spyOn(publicEventsService, 'fireEvent');
195181
const storeWellKnownEndpointsSpy = spyOn(

projects/angular-auth-oidc-client/src/lib/config/config.service.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { concatMap, map } from 'rxjs/operators';
44
import { LoggerService } from '../logging/logger.service';
55
import { EventTypes } from '../public-events/event-types';
66
import { PublicEventsService } from '../public-events/public-events.service';
7-
import { StoragePersistenceService } from '../storage/storage-persistence.service';
87
import { PlatformProvider } from '../utils/platform-provider/platform.provider';
98
import { AuthWellKnownService } from './auth-well-known/auth-well-known.service';
109
import { DEFAULT_CONFIG } from './default-config';
@@ -18,9 +17,6 @@ export class ConfigurationService {
1817

1918
private readonly loggerService = inject(LoggerService);
2019
private readonly publicEventsService = inject(PublicEventsService);
21-
private readonly storagePersistenceService = inject(
22-
StoragePersistenceService
23-
);
2420
private readonly platformProvider = inject(PlatformProvider);
2521
private readonly authWellKnownService = inject(AuthWellKnownService);
2622
private readonly loader = inject(StsConfigLoader);
@@ -154,29 +150,13 @@ export class ConfigurationService {
154150
private enhanceConfigWithWellKnownEndpoint(
155151
configuration: OpenIdConfiguration
156152
): OpenIdConfiguration {
157-
const alreadyExistingAuthWellKnownEndpoints =
158-
this.storagePersistenceService.read(
159-
'authWellKnownEndPoints',
160-
configuration
161-
);
162-
163-
if (!!alreadyExistingAuthWellKnownEndpoints) {
164-
configuration.authWellknownEndpoints =
165-
alreadyExistingAuthWellKnownEndpoints;
166-
167-
return configuration;
168-
}
169-
170153
const passedAuthWellKnownEndpoints = configuration.authWellknownEndpoints;
171154

172155
if (!!passedAuthWellKnownEndpoints) {
173156
this.authWellKnownService.storeWellKnownEndpoints(
174157
configuration,
175158
passedAuthWellKnownEndpoints
176159
);
177-
configuration.authWellknownEndpoints = passedAuthWellKnownEndpoints;
178-
179-
return configuration;
180160
}
181161

182162
return configuration;

0 commit comments

Comments
 (0)