Skip to content

Commit 0796432

Browse files
authored
Merge pull request damienbod#2125 from damienbod/feature/strictissuer-validation-OIDC-Discovery
Strictissuer validation OIDC discovery
2 parents 36eeb23 + bc29675 commit 0796432

File tree

3 files changed

+110
-5
lines changed

3 files changed

+110
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
### 2025-09-05 20.0.0
44

55
- Angular 20.2.3
6+
- Feat: Strict issuer validation on OIDC Discovery document retrieval
7+
- [PR](https://github.com/damienbod/angular-auth-oidc-client/pull/2116)
68

79
### 2025-07-22 19.0.2
810

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

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const DUMMY_WELL_KNOWN_DOCUMENT = {
2424
introspection_endpoint:
2525
'https://identity-server.test/realms/main/protocol/openid-connect/token/introspect',
2626
};
27+
const DUMMY_MALICIOUS_URL = 'https://malicious.test/realms/main';
2728

2829
describe('AuthWellKnownDataService', () => {
2930
let service: AuthWellKnownDataService;
@@ -174,7 +175,10 @@ describe('AuthWellKnownDataService', () => {
174175

175176
describe('getWellKnownEndPointsForConfig', () => {
176177
it('calling internal getWellKnownDocument and maps', waitForAsync(() => {
177-
spyOn(dataService, 'get').and.returnValue(of({ jwks_uri: 'jwks_uri' }));
178+
spyOn(dataService, 'get').and.returnValue(of({
179+
issuer: 'localhost',
180+
jwks_uri: 'jwks_uri'
181+
}));
178182

179183
const spy = spyOn(
180184
service as any,
@@ -184,12 +188,13 @@ describe('AuthWellKnownDataService', () => {
184188
service
185189
.getWellKnownEndPointsForConfig({
186190
configId: 'configId1',
187-
authWellknownEndpointUrl: 'any-url',
191+
authWellknownEndpointUrl: 'localhost',
188192
})
189193
.subscribe((result) => {
190194
expect(spy).toHaveBeenCalled();
191195
expect((result as any).jwks_uri).toBeUndefined();
192196
expect(result.jwksUri).toBe('jwks_uri');
197+
expect(result.issuer).toBe('localhost');
193198
});
194199
}));
195200

@@ -214,19 +219,103 @@ describe('AuthWellKnownDataService', () => {
214219
it('should merge the mapped endpoints with the provided endpoints', waitForAsync(() => {
215220
spyOn(dataService, 'get').and.returnValue(of(DUMMY_WELL_KNOWN_DOCUMENT));
216221

222+
const expected: AuthWellKnownEndpoints = {
223+
endSessionEndpoint: 'config-endSessionEndpoint',
224+
revocationEndpoint: 'config-revocationEndpoint',
225+
jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri
226+
};
227+
228+
service
229+
.getWellKnownEndPointsForConfig({
230+
configId: 'configId1',
231+
authWellknownEndpointUrl: DUMMY_WELL_KNOWN_DOCUMENT.issuer,
232+
authWellknownEndpoints: {
233+
endSessionEndpoint: 'config-endSessionEndpoint',
234+
revocationEndpoint: 'config-revocationEndpoint',
235+
},
236+
})
237+
.subscribe((result) => {
238+
expect(result).toEqual(jasmine.objectContaining(expected));
239+
});
240+
}));
241+
242+
it('throws error and logs if well known issuer does not match authwellknownUrl', waitForAsync(() => {
243+
const loggerSpy = spyOn(loggerService, 'logError');
244+
const maliciousWellKnown = {
245+
...DUMMY_WELL_KNOWN_DOCUMENT,
246+
issuer: DUMMY_MALICIOUS_URL
247+
};
248+
249+
spyOn(dataService, 'get').and.returnValue(
250+
createRetriableStream(
251+
of(maliciousWellKnown)
252+
)
253+
);
254+
255+
const config = {
256+
configId: 'configId1',
257+
authWellknownEndpointUrl: DUMMY_WELL_KNOWN_DOCUMENT.issuer,
258+
};
259+
260+
service.getWellKnownEndPointsForConfig(config).subscribe({
261+
next: (result) => {
262+
fail(`Retrieval was supposed to fail. Well known endpoints returned : ${JSON.stringify(result)}`);
263+
},
264+
error: (error) => {
265+
expect(loggerSpy).toHaveBeenCalledOnceWith(
266+
config,
267+
`Issuer mismatch. Well known issuer ${DUMMY_MALICIOUS_URL} does not match configured well known url ${DUMMY_WELL_KNOWN_DOCUMENT.issuer}`
268+
);
269+
expect(error.message).toEqual(`Issuer mismatch. Well known issuer ${DUMMY_MALICIOUS_URL} does not match configured well known url ${DUMMY_WELL_KNOWN_DOCUMENT.issuer}`);
270+
}
271+
});
272+
}));
273+
274+
it('should not throws error and logs if well known issuer has a trailing slash compared to authwellknownUrl ', waitForAsync(() => {
275+
const trailingSlashIssuerWellKnown = {
276+
...DUMMY_WELL_KNOWN_DOCUMENT,
277+
issuer: DUMMY_WELL_KNOWN_DOCUMENT.issuer+"/"
278+
};
279+
280+
spyOn(dataService, 'get').and.returnValue(of(trailingSlashIssuerWellKnown));
281+
282+
const expected: AuthWellKnownEndpoints = {
283+
issuer: DUMMY_WELL_KNOWN_DOCUMENT.issuer+"/",
284+
};
285+
286+
service
287+
.getWellKnownEndPointsForConfig({
288+
configId: 'configId1',
289+
authWellknownEndpointUrl: DUMMY_WELL_KNOWN_DOCUMENT.issuer
290+
})
291+
.subscribe((result) => {
292+
expect(result).toEqual(jasmine.objectContaining(expected));
293+
});
294+
}));
295+
296+
it('should merge the mapped endpoints with the provided endpoints and ignore issuer/authwellknownUrl mismatch', waitForAsync(() => {
297+
const maliciousWellKnown = {
298+
...DUMMY_WELL_KNOWN_DOCUMENT,
299+
issuer: DUMMY_MALICIOUS_URL
300+
};
301+
302+
spyOn(dataService, 'get').and.returnValue(of(maliciousWellKnown));
303+
217304
const expected: AuthWellKnownEndpoints = {
218305
endSessionEndpoint: 'config-endSessionEndpoint',
219306
revocationEndpoint: 'config-revocationEndpoint',
220307
jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri,
308+
issuer: DUMMY_WELL_KNOWN_DOCUMENT.issuer,
221309
};
222310

223311
service
224312
.getWellKnownEndPointsForConfig({
225313
configId: 'configId1',
226-
authWellknownEndpointUrl: 'any-url',
314+
authWellknownEndpointUrl: DUMMY_WELL_KNOWN_DOCUMENT.issuer,
227315
authWellknownEndpoints: {
228316
endSessionEndpoint: 'config-endSessionEndpoint',
229317
revocationEndpoint: 'config-revocationEndpoint',
318+
issuer: DUMMY_WELL_KNOWN_DOCUMENT.issuer
230319
},
231320
})
232321
.subscribe((result) => {

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { inject, Injectable } from '@angular/core';
22
import { Observable, throwError } from 'rxjs';
3-
import { map, retry } from 'rxjs/operators';
3+
import { map, retry, tap } from 'rxjs/operators';
44
import { DataService } from '../../api/data.service';
55
import { LoggerService } from '../../logging/logger.service';
66
import { OpenIdConfiguration } from '../openid-configuration';
@@ -46,7 +46,21 @@ export class AuthWellKnownDataService {
4646
map((mappedWellKnownEndpoints) => ({
4747
...mappedWellKnownEndpoints,
4848
...authWellknownEndpoints,
49-
}))
49+
})),
50+
tap(
51+
(wellKnownEndpoints) => {
52+
const issuer = wellKnownEndpoints.issuer || "";
53+
const wellKnownSuffix = config.authWellknownUrlSuffix || WELL_KNOWN_SUFFIX;
54+
const configuredWellKnownEndpoint = authWellknownEndpointUrl.replace(wellKnownSuffix, "");
55+
56+
if (issuer !== configuredWellKnownEndpoint && issuer !== `${configuredWellKnownEndpoint}/`) {
57+
const errorMessage = `Issuer mismatch. Well known issuer ${wellKnownEndpoints.issuer} does not match configured well known url ${authWellknownEndpointUrl}`;
58+
59+
this.loggerService.logError(config, errorMessage);
60+
throw new Error(errorMessage);
61+
}
62+
}
63+
)
5064
);
5165
}
5266

0 commit comments

Comments
 (0)