Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions docs/core/components/front-channel-logout.component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
Title: Front Channel Logout component
Added: v1.0.0
Status: Active
Last reviewed: 2025-10-24
---

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

Handles an OpenID Connect (OIDC) Front-Channel Logout request by always triggering a local logout when the route is hit.

## Contents

- [Basic usage](#basic-usage)
- [Details](#details)
- [What is Front-Channel Logout?](#what-is-front-channel-logout)
- [How matching works](#how-matching-works)
- [Security considerations](#security-considerations)
- [Logout scenarios](#logout-scenarios)
- [See also](#see-also)

## Basic usage

This component has no UI; it performs logic on init. Add a route that points to it so that your Identity Provider (IdP) can call your application during a front-channel logout.

```ts
import { Routes } from '@angular/router';
import { FrontChannelLogoutComponent } from '@adf/core';

export const routes: Routes = [
{ path: 'oidc/front-channel-logout', component: FrontChannelLogoutComponent }
];
```

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`).

On initialisation the component always calls `logout()` via `AuthService`, regardless of any query parameters.

## Details

### What is Front-Channel Logout?

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.

### How it works

On `ngOnInit`, the component simply calls `authService.logout()`. There is no check for issuer or session ID; logout is unconditional.

### Security considerations

- The component does not inspect or require any query parameters.
- No sensitive data is read from the URL.

### Logout behavior

Whenever this route is hit, the user is always logged out, regardless of any parameters or state.

### See also

- [OIDC Session Management / Front-Channel Logout specification](https://openid.net/specs/openid-connect-frontchannel-1_0.html#ExampleFrontchannel)
- [Login component](login.component.md)
- [Login Dialog component](login-dialog.component.md)
4 changes: 3 additions & 1 deletion lib/core/src/lib/auth/oidc/auth.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import { Routes } from '@angular/router';
import { AuthenticationConfirmationComponent } from './view/authentication-confirmation/authentication-confirmation.component';
import { OidcAuthGuard } from './oidc-auth.guard';
import { FrontChannelLogoutComponent } from './front-channel-logout.component';

export const AUTH_ROUTES: Routes = [
{ path: 'view/authentication-confirmation', component: AuthenticationConfirmationComponent, canActivate: [OidcAuthGuard] }
{ path: 'view/authentication-confirmation', component: AuthenticationConfirmationComponent, canActivate: [OidcAuthGuard] },
{ path: 'oidc/frontchannel_logout', component: FrontChannelLogoutComponent }
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import { FrontChannelLogoutComponent } from './front-channel-logout.component';

describe('FrontChannelLogoutComponent', () => {
let component: FrontChannelLogoutComponent;
let fixture: ComponentFixture<FrontChannelLogoutComponent>;
let authServiceSpy: jasmine.SpyObj<AuthService>;

beforeEach(async () => {
authServiceSpy = jasmine.createSpyObj('AuthService', ['logout']);
await TestBed.configureTestingModule({
imports: [FrontChannelLogoutComponent],
providers: [{ provide: AuthService, useValue: authServiceSpy }]
}).compileComponents();
fixture = TestBed.createComponent(FrontChannelLogoutComponent);
component = fixture.componentInstance;
});

it('should create', () => {
expect(component).toBeTruthy();
});

describe('ngOnInit - logout logic', () => {
it('should always call logout on init', () => {
component.ngOnInit();
expect(authServiceSpy.logout).toHaveBeenCalledTimes(1);
});
});
});
28 changes: 28 additions & 0 deletions lib/core/src/lib/auth/oidc/front-channel-logout.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Component, inject, OnInit } from '@angular/core';
import { AuthService } from './auth.service';

@Component({ template: '', standalone: true })
export class FrontChannelLogoutComponent implements OnInit {
private readonly authService = inject(AuthService);

ngOnInit() {
this.authService.logout();
}
}
15 changes: 15 additions & 0 deletions lib/core/src/lib/auth/oidc/redirect-auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,21 @@ describe('RedirectAuthService', () => {
expect(oauthServiceSpy.logOut).toHaveBeenCalledTimes(1);
});

it('should logout user if sessionChecksEnabled is true and event type session_error is emitted', async () => {
const mockTimeSync = { outOfSync: false } as TimeSync;
timeSyncServiceSpy.checkTimeSync.and.returnValue(of(mockTimeSync));

ensureDiscoveryDocumentSpy.and.resolveTo(true);

authConfigSpy.sessionChecksEnabled = true;

await service.init();

oauthEvents$.next({ type: 'session_error' } as OAuthEvent);

expect(oauthServiceSpy.logOut).toHaveBeenCalledTimes(1);
});

it('should NOT logout user if login success', async () => {
ensureDiscoveryDocumentSpy.and.resolveTo(true);

Expand Down
9 changes: 7 additions & 2 deletions lib/core/src/lib/auth/oidc/redirect-auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,11 @@ export class RedirectAuthService extends AuthService {
'session_state'
];

constructor(private oauthService: OAuthService, private _oauthStorage: OAuthStorage, @Inject(AUTH_CONFIG) authConfig: AuthConfig) {
constructor(
private oauthService: OAuthService,
private _oauthStorage: OAuthStorage,
@Inject(AUTH_CONFIG) authConfig: AuthConfig
) {
super();

this.authConfig = authConfig;
Expand Down Expand Up @@ -335,7 +339,8 @@ export class RedirectAuthService extends AuthService {
this.oauthService.tokenValidationHandler = new JwksValidationHandler();

if (config.sessionChecksEnabled) {
this.oauthService.events.pipe(filter((event) => event.type === 'session_terminated')).subscribe(() => {
const sessionErrorTypesToPerformLogout = ['session_terminated', 'session_error'];
this.oauthService.events.pipe(filter((event) => sessionErrorTypesToPerformLogout.includes(event.type))).subscribe(() => {
this.oauthService.logOut();
});
}
Expand Down
Loading