Skip to content

Commit 06d2bf4

Browse files
committed
AAE-37746 revert previous changes, update the front-channel-logout component logic to always logout the user
1 parent ff16cac commit 06d2bf4

File tree

6 files changed

+13
-280
lines changed

6 files changed

+13
-280
lines changed

docs/core/components/front-channel-logout.component.md

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Last reviewed: 2025-10-24
77

88
# [Front Channel Logout component](../../../lib/core/src/lib/auth/oidc/front-channel-logout.component.ts "Defined in front-channel-logout.component.ts")
99

10-
Handles an OpenID Connect (OIDC) Front-Channel Logout request by validating issuer and session identifiers and triggering a local logout when they match.
10+
Handles an OpenID Connect (OIDC) Front-Channel Logout request by always triggering a local logout when the route is hit.
1111

1212
## Contents
1313

@@ -32,62 +32,28 @@ export const routes: Routes = [
3232
];
3333
```
3434

35-
When the IdP performs a front-channel logout it will iframe / redirect the user's browser to a URL like:
35+
When the IdP performs a front-channel logout it will iframe or redirect the user's browser to the configured route (e.g. `/oidc/frontchannel_logout`).
3636

37-
```text
38-
/oidc/frontchannel_logout?iss=https://issuer.example.com&sid=abc123-session-id
39-
```
40-
41-
On initialisation the component compares those query parameters with locally stored values provided by `AuthService` and calls `logout()` if both match.
37+
On initialisation the component always calls `logout()` via `AuthService`, regardless of any query parameters.
4238

4339
## Details
4440

4541
### What is Front-Channel Logout?
4642

4743
Front-Channel Logout is part of the OIDC specification. The Identity Provider notifies relying parties (your SPA) of a logout by issuing an HTTP(S) request (often via an iframe). The client application must validate the request and clear its own session.
4844

49-
### How matching works
50-
51-
Inside `ngOnInit` the component:
45+
### How it works
5246

53-
1. Reads `iss` and `sid` from `ActivatedRoute.snapshot.queryParamMap`.
54-
2. Retrieves the stored issuer and session id via `AuthService.getStoredIssuer()` and `AuthService.getStoredSessionId()`.
55-
3. Compares both pairs. Logout is executed only if:
56-
- storedIssuer === issuerParam AND
57-
- storedSessionId === sessionIdParam (and none are falsy).
58-
59-
```ts
60-
const storedIssuerMatches = storedIssuer && issuerParam && storedIssuer === issuerParam;
61-
const storedSessionMatches = storedSessionId && sessionIdParam && storedSessionId === sessionIdParam;
62-
if (storedIssuerMatches && storedSessionMatches) {
63-
authService.logout();
64-
}
65-
```
66-
67-
If either value is missing or does not match, nothing happens.
47+
On `ngOnInit`, the component simply calls `authService.logout()`. There is no check for issuer or session ID; logout is unconditional.
6848

6949
### Security considerations
7050

71-
- The component performs strict equality checks; no partial matching.
72-
- Both parameters must be present and match; a single match will not trigger logout.
73-
- Avoid exposing sensitive data in query parameters beyond issuer (`iss`) and session identifier (`sid`).
74-
75-
### Logout scenarios
76-
77-
These scenarios outline when a logout is triggered or suppressed.
51+
- The component does not inspect or require any query parameters.
52+
- No sensitive data is read from the URL.
7853

79-
Key scenarios:
54+
### Logout behavior
8055

81-
| Scenario | Stored Issuer | URL Issuer | Stored SID | URL SID | Outcome |
82-
|----------|---------------|-----------|------------|---------|---------|
83-
| Full match | A | A | 123 | 123 | logout called |
84-
| Issuer mismatch | A | B | 123 | 123 | no logout |
85-
| SID mismatch | A | A | 123 | 999 | no logout |
86-
| Both mismatch | A | B | 123 | 999 | no logout |
87-
| Missing issuer | null | A | 123 | 123 | no logout |
88-
| Missing SID | A | A | null | 123 | no logout |
89-
| Missing URL issuer | A | null | 123 | 123 | no logout |
90-
| Missing URL SID | A | A | 123 | null | no logout |
56+
Whenever this route is hit, the user is always logged out, regardless of any parameters or state.
9157

9258
### See also
9359

lib/core/src/lib/auth/oidc/auth.service.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,4 @@ export abstract class AuthService {
7171
*/
7272
abstract loginCallback(loginOptions?: LoginOptions): Promise<string | undefined>;
7373
abstract updateIDPConfiguration(...args: any[]): void;
74-
75-
/**
76-
* Get the stored issuer URL.
77-
*
78-
* @returns stored issuer URL
79-
*/
80-
abstract getStoredIssuer(): string;
81-
82-
/**
83-
* Get the stored session ID.
84-
*
85-
* @returns stored session ID
86-
*/
87-
abstract getStoredSessionId(): string;
8874
}

lib/core/src/lib/auth/oidc/front-channel-logout.component.spec.ts

Lines changed: 3 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,20 @@
1616
*/
1717

1818
import { ComponentFixture, TestBed } from '@angular/core/testing';
19-
import { ActivatedRoute } from '@angular/router';
2019
import { AuthService } from './auth.service';
2120
import { FrontChannelLogoutComponent } from './front-channel-logout.component';
2221

2322
describe('FrontChannelLogoutComponent', () => {
2423
let component: FrontChannelLogoutComponent;
2524
let fixture: ComponentFixture<FrontChannelLogoutComponent>;
2625
let authServiceSpy: jasmine.SpyObj<AuthService>;
27-
let activatedRouteMock: any;
2826

2927
beforeEach(async () => {
30-
authServiceSpy = jasmine.createSpyObj('AuthService', ['logout', 'getStoredIssuer', 'getStoredSessionId']);
31-
activatedRouteMock = {
32-
snapshot: {
33-
queryParamMap: {
34-
get: jasmine.createSpy('get')
35-
}
36-
}
37-
};
38-
28+
authServiceSpy = jasmine.createSpyObj('AuthService', ['logout']);
3929
await TestBed.configureTestingModule({
4030
imports: [FrontChannelLogoutComponent],
41-
providers: [
42-
{ provide: AuthService, useValue: authServiceSpy },
43-
{ provide: ActivatedRoute, useValue: activatedRouteMock }
44-
]
31+
providers: [{ provide: AuthService, useValue: authServiceSpy }]
4532
}).compileComponents();
46-
4733
fixture = TestBed.createComponent(FrontChannelLogoutComponent);
4834
component = fixture.componentInstance;
4935
});
@@ -53,138 +39,9 @@ describe('FrontChannelLogoutComponent', () => {
5339
});
5440

5541
describe('ngOnInit - logout logic', () => {
56-
it('should call logout when both stored and URL issuer match AND both stored and URL session ID match', () => {
57-
const testIssuer = 'test-issuer';
58-
const testSessionId = 'test-session-id';
59-
authServiceSpy.getStoredIssuer.and.returnValue(testIssuer);
60-
authServiceSpy.getStoredSessionId.and.returnValue(testSessionId);
61-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
62-
if (param === 'iss') return testIssuer;
63-
if (param === 'sid') return testSessionId;
64-
return null;
65-
});
42+
it('should always call logout on init', () => {
6643
component.ngOnInit();
6744
expect(authServiceSpy.logout).toHaveBeenCalledTimes(1);
6845
});
69-
70-
it('should not call logout when issuer matches but session ID differs between stored and URL values', () => {
71-
const testIssuer = 'test-issuer';
72-
const storedSessionId = 'stored-session-id';
73-
const urlSessionId = 'different-session-id';
74-
authServiceSpy.getStoredIssuer.and.returnValue(testIssuer);
75-
authServiceSpy.getStoredSessionId.and.returnValue(storedSessionId);
76-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
77-
if (param === 'iss') return testIssuer;
78-
if (param === 'sid') return urlSessionId;
79-
return null;
80-
});
81-
component.ngOnInit();
82-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
83-
});
84-
85-
it('should not call logout when session ID matches but issuer differs between stored and URL values', () => {
86-
const testSessionId = 'test-session-id';
87-
const storedIssuer = 'stored-issuer';
88-
const urlIssuer = 'different-issuer';
89-
authServiceSpy.getStoredIssuer.and.returnValue(storedIssuer);
90-
authServiceSpy.getStoredSessionId.and.returnValue(testSessionId);
91-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
92-
if (param === 'iss') return urlIssuer;
93-
if (param === 'sid') return testSessionId;
94-
return null;
95-
});
96-
component.ngOnInit();
97-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
98-
});
99-
100-
it('should not call logout when both issuer and session ID differ between stored and URL values', () => {
101-
const storedIssuer = 'stored-issuer';
102-
const storedSessionId = 'stored-session-id';
103-
const urlIssuer = 'different-issuer';
104-
const urlSessionId = 'different-session-id';
105-
authServiceSpy.getStoredIssuer.and.returnValue(storedIssuer);
106-
authServiceSpy.getStoredSessionId.and.returnValue(storedSessionId);
107-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
108-
if (param === 'iss') return urlIssuer;
109-
if (param === 'sid') return urlSessionId;
110-
return null;
111-
});
112-
component.ngOnInit();
113-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
114-
});
115-
116-
it('should not call logout when stored issuer is null but URL parameters are valid', () => {
117-
const testSessionId = 'test-session-id';
118-
authServiceSpy.getStoredIssuer.and.returnValue(null);
119-
authServiceSpy.getStoredSessionId.and.returnValue(testSessionId);
120-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
121-
if (param === 'iss') return 'test-issuer';
122-
if (param === 'sid') return testSessionId;
123-
return null;
124-
});
125-
component.ngOnInit();
126-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
127-
});
128-
129-
it('should not call logout when stored session ID is null but URL parameters are valid', () => {
130-
const testIssuer = 'test-issuer';
131-
authServiceSpy.getStoredIssuer.and.returnValue(testIssuer);
132-
authServiceSpy.getStoredSessionId.and.returnValue(null);
133-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
134-
if (param === 'iss') return testIssuer;
135-
if (param === 'sid') return 'test-session-id';
136-
return null;
137-
});
138-
component.ngOnInit();
139-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
140-
});
141-
142-
it('should not call logout when URL issuer parameter is missing but stored values are valid', () => {
143-
const testIssuer = 'test-issuer';
144-
const testSessionId = 'test-session-id';
145-
authServiceSpy.getStoredIssuer.and.returnValue(testIssuer);
146-
authServiceSpy.getStoredSessionId.and.returnValue(testSessionId);
147-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
148-
if (param === 'iss') return null;
149-
if (param === 'sid') return testSessionId;
150-
return null;
151-
});
152-
component.ngOnInit();
153-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
154-
});
155-
156-
it('should not call logout when URL session ID parameter is missing but stored values are valid', () => {
157-
const testIssuer = 'test-issuer';
158-
const testSessionId = 'test-session-id';
159-
authServiceSpy.getStoredIssuer.and.returnValue(testIssuer);
160-
authServiceSpy.getStoredSessionId.and.returnValue(testSessionId);
161-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
162-
if (param === 'iss') return testIssuer;
163-
if (param === 'sid') return null;
164-
return null;
165-
});
166-
component.ngOnInit();
167-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
168-
});
169-
170-
it('should not call logout when both stored and URL values are empty strings', () => {
171-
authServiceSpy.getStoredIssuer.and.returnValue('');
172-
authServiceSpy.getStoredSessionId.and.returnValue('');
173-
activatedRouteMock.snapshot.queryParamMap.get.and.returnValue('');
174-
component.ngOnInit();
175-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
176-
});
177-
178-
it('should not call logout when AuthService returns undefined for stored issuer and session ID', () => {
179-
authServiceSpy.getStoredIssuer.and.returnValue(undefined);
180-
authServiceSpy.getStoredSessionId.and.returnValue(undefined);
181-
activatedRouteMock.snapshot.queryParamMap.get.and.callFake((param: string) => {
182-
if (param === 'iss') return 'test-issuer';
183-
if (param === 'sid') return 'test-session-id';
184-
return null;
185-
});
186-
expect(() => component.ngOnInit()).not.toThrow();
187-
expect(authServiceSpy.logout).not.toHaveBeenCalled();
188-
});
18946
});
19047
});

lib/core/src/lib/auth/oidc/front-channel-logout.component.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,41 +16,13 @@
1616
*/
1717

1818
import { Component, inject, OnInit } from '@angular/core';
19-
import { ActivatedRoute } from '@angular/router';
2019
import { AuthService } from './auth.service';
2120

2221
@Component({ template: '', standalone: true })
2322
export class FrontChannelLogoutComponent implements OnInit {
24-
private readonly activatedRoute = inject(ActivatedRoute);
2523
private readonly authService = inject(AuthService);
2624

2725
ngOnInit() {
28-
const { issuerParam, sessionIdParam } = this.getIssuerAndSessionIdFromRouteParams();
29-
30-
const { storedIssuer, storedSessionId } = this.getIssuerAndSessionIdFromAuthService();
31-
32-
this.logoutIfIssuerAndSessionIdMatch(storedIssuer, issuerParam, storedSessionId, sessionIdParam);
33-
}
34-
35-
private logoutIfIssuerAndSessionIdMatch(storedIssuer: string, issuerParam: string, storedSessionId: string, sessionIdParam: string) {
36-
const storedIssuerMatchUrlIssuerParam = storedIssuer && issuerParam && storedIssuer === issuerParam;
37-
const storedSessionIdMatchUrlSessionIdParam = storedSessionId && sessionIdParam && storedSessionId === sessionIdParam;
38-
39-
if (storedIssuerMatchUrlIssuerParam && storedSessionIdMatchUrlSessionIdParam) {
40-
this.authService.logout();
41-
}
42-
}
43-
44-
private getIssuerAndSessionIdFromAuthService() {
45-
const storedIssuer = this.authService.getStoredIssuer();
46-
const storedSessionId = this.authService.getStoredSessionId();
47-
return { storedIssuer, storedSessionId };
48-
}
49-
50-
private getIssuerAndSessionIdFromRouteParams() {
51-
const queryParamMap = this.activatedRoute.snapshot.queryParamMap;
52-
const issuerParam = queryParamMap.get('iss');
53-
const sessionIdParam = queryParamMap.get('sid');
54-
return { issuerParam, sessionIdParam };
26+
this.authService.logout();
5527
}
5628
}

lib/core/src/lib/auth/oidc/redirect-auth.service.spec.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -537,42 +537,4 @@ describe('RedirectAuthService', () => {
537537

538538
expect(expectedLogoutIsEmitted).toBeTrue();
539539
});
540-
541-
describe('getStoredIssuer', () => {
542-
it('should return the stored issuer from the OAuthStorage', () => {
543-
const expectedIssuer = 'https://example.com/auth';
544-
oauthServiceSpy.getIdentityClaims.and.returnValue({ iss: expectedIssuer });
545-
546-
const storedIssuer = service.getStoredIssuer();
547-
548-
expect(storedIssuer).toBe(expectedIssuer);
549-
});
550-
551-
it('should return empty string if no issuer is stored in the OAuthStorage', () => {
552-
oauthServiceSpy.getIdentityClaims.and.returnValue({ iss: null });
553-
554-
const storedIssuer = service.getStoredIssuer();
555-
556-
expect(storedIssuer).toBe('');
557-
});
558-
});
559-
560-
describe('getStoredSessionId', () => {
561-
it('should return the stored session id from the OAuthStorage', () => {
562-
const expectedSessionId = '12345678910';
563-
oauthServiceSpy.getIdentityClaims.and.returnValue({ sid: expectedSessionId });
564-
565-
const storedSessionId = service.getStoredSessionId();
566-
567-
expect(storedSessionId).toBe(expectedSessionId);
568-
});
569-
570-
it('should return string if no session id is stored in the OAuthStorage', () => {
571-
oauthServiceSpy.getIdentityClaims.and.returnValue({ sid: null });
572-
573-
const storedSessionId = service.getStoredSessionId();
574-
575-
expect(storedSessionId).toBe('');
576-
});
577-
});
578540
});

lib/core/src/lib/auth/oidc/redirect-auth.service.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,16 +318,6 @@ export class RedirectAuthService extends AuthService {
318318
.then(() => this._getRedirectUrl());
319319
}
320320

321-
getStoredIssuer(): string {
322-
const claims = this.oauthService.getIdentityClaims();
323-
return claims?.['iss'] || '';
324-
}
325-
326-
getStoredSessionId(): string {
327-
const claims = this.oauthService.getIdentityClaims();
328-
return claims?.['sid'] || '';
329-
}
330-
331321
private _getRedirectUrl() {
332322
const DEFAULT_REDIRECT = '/';
333323
const stateKey = this.oauthService.state;

0 commit comments

Comments
 (0)