mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
AAE-24139 Fix unauthorized user error when access token is not valid (#10012)
* AAE-24139 Bump angular-oauth2-oidc version to 15 * AAE-24139 Allow to set sessionCheckEnabled and clockSkewInSec properties * AAE-24139 Update angular-oauth2-oidc version to 15 in the core deps * AAE-24139 Remove authentication tokens when the token is no longer valid and reload the page to let oauth library refresh the token * fix lint issue
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cropperjs": "^1.5.13",
|
"cropperjs": "^1.5.13",
|
||||||
"angular-oauth2-oidc": "^13.0.1",
|
"angular-oauth2-oidc": "^15.0.1",
|
||||||
"angular-oauth2-oidc-jwks": "^17.0.2"
|
"angular-oauth2-oidc-jwks": "^17.0.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@@ -31,4 +31,6 @@ export interface OauthConfigModel {
|
|||||||
redirectSilentIframeUri?: string;
|
redirectSilentIframeUri?: string;
|
||||||
refreshTokenTimeout?: number;
|
refreshTokenTimeout?: number;
|
||||||
publicUrls: string[];
|
publicUrls: string[];
|
||||||
|
clockSkewInSec?: number;
|
||||||
|
sessionChecksEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
@@ -221,4 +221,63 @@ describe('AuthConfigService', () => {
|
|||||||
expect(service.loadAppConfig().postLogoutRedirectUri).toBe('http://localhost:3000/asd');
|
expect(service.loadAppConfig().postLogoutRedirectUri).toBe('http://localhost:3000/asd');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('clockSkewInSec', () => {
|
||||||
|
it('should return clockSkewInSec equal to 0', () => {
|
||||||
|
const expectedClockSkewInSec = 0;
|
||||||
|
spyOnProperty(appConfigService, 'oauth2').and.returnValue({ clockSkewInSec: 0 } as any);
|
||||||
|
expect(service.loadAppConfig().clockSkewInSec).toBe(expectedClockSkewInSec);
|
||||||
|
});
|
||||||
|
it('should not return clockSkewInSec if is not defined', () => {
|
||||||
|
spyOnProperty(appConfigService, 'oauth2').and.returnValue({} as any);
|
||||||
|
expect(service.loadAppConfig().clockSkewInSec).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return clockSkewInSec if is undefined', () => {
|
||||||
|
spyOnProperty(appConfigService, 'oauth2').and.returnValue({ clockSkewInSec: undefined } as any);
|
||||||
|
expect(service.loadAppConfig().clockSkewInSec).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty object if clockSkewInSec is null', () => {
|
||||||
|
const mockOauth2Value = { clockSkewInSec: null } as any;
|
||||||
|
expect(service.getClockSkewInSec(mockOauth2Value)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty object if clockSkewInSec is a string', () => {
|
||||||
|
const mockOauth2Value = { clockSkewInSec: 'null' } as any;
|
||||||
|
expect(service.getClockSkewInSec(mockOauth2Value)).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sessionChecksEnabled', () => {
|
||||||
|
it('should return sessionChecksEnabled equal to true', () => {
|
||||||
|
spyOnProperty(appConfigService, 'oauth2').and.returnValue({ sessionChecksEnabled: true } as any);
|
||||||
|
expect(service.loadAppConfig().sessionChecksEnabled).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return sessionChecksEnabled equal to false', () => {
|
||||||
|
spyOnProperty(appConfigService, 'oauth2').and.returnValue({ sessionChecksEnabled: false } as any);
|
||||||
|
expect(service.loadAppConfig().sessionChecksEnabled).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return sessionChecksEnabled if is not defined', () => {
|
||||||
|
expect(service.getSessionCheckEnabled({} as any)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return sessionChecksEnabled if is a string', () => {
|
||||||
|
expect(service.getSessionCheckEnabled({ sessionChecksEnabled: 'fake' } as any)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return sessionChecksEnabled if is undefined', () => {
|
||||||
|
expect(service.getSessionCheckEnabled({ sessionChecksEnabled: undefined } as any)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return sessionChecksEnabled if is null', () => {
|
||||||
|
expect(service.getSessionCheckEnabled({ sessionChecksEnabled: null } as any)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return sessionChecksEnabled if is a number', () => {
|
||||||
|
expect(service.getSessionCheckEnabled({ sessionChecksEnabled: 666 } as any)).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -20,6 +20,7 @@ import { AuthConfig } from 'angular-oauth2-oidc';
|
|||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { AppConfigService } from '../../app-config/app-config.service';
|
import { AppConfigService } from '../../app-config/app-config.service';
|
||||||
import { AUTH_MODULE_CONFIG, AuthModuleConfig } from './auth-config';
|
import { AUTH_MODULE_CONFIG, AuthModuleConfig } from './auth-config';
|
||||||
|
import { OauthConfigModel } from '../models/oauth-config.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create auth configuration factory
|
* Create auth configuration factory
|
||||||
@@ -51,6 +52,8 @@ export class AuthConfigService {
|
|||||||
const origin = this.getLocationOrigin();
|
const origin = this.getLocationOrigin();
|
||||||
const redirectUri = this.getRedirectUri();
|
const redirectUri = this.getRedirectUri();
|
||||||
const customQueryParams = oauth2.audience ? { audience: oauth2.audience } : {};
|
const customQueryParams = oauth2.audience ? { audience: oauth2.audience } : {};
|
||||||
|
const clockSkewInSec = this.getClockSkewInSec(oauth2);
|
||||||
|
const sessionChecksEnabled = this.getSessionCheckEnabled(oauth2);
|
||||||
|
|
||||||
return new AuthConfig({
|
return new AuthConfig({
|
||||||
...oauth2,
|
...oauth2,
|
||||||
@@ -65,10 +68,20 @@ export class AuthConfigService {
|
|||||||
dummyClientSecret: oauth2.secret || '',
|
dummyClientSecret: oauth2.secret || '',
|
||||||
logoutUrl: oauth2.logoutUrl,
|
logoutUrl: oauth2.logoutUrl,
|
||||||
customQueryParams,
|
customQueryParams,
|
||||||
...(oauth2.codeFlow && { responseType: 'code' })
|
...(oauth2.codeFlow && { responseType: 'code' }),
|
||||||
|
...clockSkewInSec,
|
||||||
|
...sessionChecksEnabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSessionCheckEnabled(oauth2: OauthConfigModel) {
|
||||||
|
return typeof oauth2.sessionChecksEnabled === 'boolean' ? { sessionChecksEnabled: oauth2.sessionChecksEnabled } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getClockSkewInSec(oauth2: OauthConfigModel) {
|
||||||
|
return typeof oauth2.clockSkewInSec === 'number' ? { clockSkewInSec: oauth2.clockSkewInSec } : {};
|
||||||
|
}
|
||||||
|
|
||||||
getRedirectUri(): string {
|
getRedirectUri(): string {
|
||||||
// required for this package as we handle the returned token on this view, with is provided by the AuthModule
|
// required for this package as we handle the returned token on this view, with is provided by the AuthModule
|
||||||
const viewUrl = `view/authentication-confirmation`;
|
const viewUrl = `view/authentication-confirmation`;
|
||||||
|
@@ -34,6 +34,7 @@ describe('RedirectAuthService', () => {
|
|||||||
events: oauthEvents$,
|
events: oauthEvents$,
|
||||||
configure: () => {},
|
configure: () => {},
|
||||||
hasValidAccessToken: jasmine.createSpy().and.returnValue(true),
|
hasValidAccessToken: jasmine.createSpy().and.returnValue(true),
|
||||||
|
hasValidIdToken: jasmine.createSpy().and.returnValue(true),
|
||||||
setupAutomaticSilentRefresh: () => {
|
setupAutomaticSilentRefresh: () => {
|
||||||
mockOauthService.silentRefresh();
|
mockOauthService.silentRefresh();
|
||||||
mockOauthService.refreshToken();
|
mockOauthService.refreshToken();
|
||||||
@@ -53,6 +54,7 @@ describe('RedirectAuthService', () => {
|
|||||||
|
|
||||||
TestBed.inject(OAuthService);
|
TestBed.inject(OAuthService);
|
||||||
service = TestBed.inject(RedirectAuthService);
|
service = TestBed.inject(RedirectAuthService);
|
||||||
|
spyOn(service, 'reloadPage').and.callFake(() => {});
|
||||||
spyOn(service, 'ensureDiscoveryDocument').and.resolveTo(true);
|
spyOn(service, 'ensureDiscoveryDocument').and.resolveTo(true);
|
||||||
mockOauthService.getAccessToken = () => 'access-token';
|
mockOauthService.getAccessToken = () => 'access-token';
|
||||||
});
|
});
|
||||||
@@ -93,4 +95,25 @@ describe('RedirectAuthService', () => {
|
|||||||
expect(refreshTokenCalled).toBe(true);
|
expect(refreshTokenCalled).toBe(true);
|
||||||
expect(silentRefreshCalled).toBe(true);
|
expect(silentRefreshCalled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should remove all auth items from the storage if access token is set and is not authenticated', () => {
|
||||||
|
mockOauthService.getAccessToken = () => 'access-token';
|
||||||
|
spyOnProperty(service, 'authenticated', 'get').and.returnValue(false);
|
||||||
|
(mockOauthService.events as Subject<OAuthEvent>).next({ type: 'discovery_document_loaded' } as OAuthEvent);
|
||||||
|
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('access_token');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('access_token_stored_at');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('expires_at');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('granted_scopes');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('id_token');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('id_token_claims_obj');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('id_token_expires_at');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('id_token_stored_at');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('nonce');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('PKCE_verifier');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('refresh_token');
|
||||||
|
expect(mockOAuthStorage.removeItem).toHaveBeenCalledWith('session_state');
|
||||||
|
expect(service.reloadPage).toHaveBeenCalledOnceWith();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -19,7 +19,7 @@ import { Inject, Injectable, inject } from '@angular/core';
|
|||||||
import { AuthConfig, AUTH_CONFIG, OAuthErrorEvent, OAuthEvent, OAuthService, OAuthStorage, TokenResponse, LoginOptions, OAuthSuccessEvent } from 'angular-oauth2-oidc';
|
import { AuthConfig, AUTH_CONFIG, OAuthErrorEvent, OAuthEvent, OAuthService, OAuthStorage, TokenResponse, LoginOptions, OAuthSuccessEvent } from 'angular-oauth2-oidc';
|
||||||
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
|
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
|
||||||
import { from, Observable } from 'rxjs';
|
import { from, Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, shareReplay, take } from 'rxjs/operators';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AUTH_MODULE_CONFIG, AuthModuleConfig } from './auth-config';
|
import { AUTH_MODULE_CONFIG, AuthModuleConfig } from './auth-config';
|
||||||
|
|
||||||
@@ -53,6 +53,21 @@ export class RedirectAuthService extends AuthService {
|
|||||||
|
|
||||||
private authConfig!: AuthConfig | Promise<AuthConfig>;
|
private authConfig!: AuthConfig | Promise<AuthConfig>;
|
||||||
|
|
||||||
|
private readonly AUTH_STORAGE_ITEMS: string[] = [
|
||||||
|
'access_token',
|
||||||
|
'access_token_stored_at',
|
||||||
|
'expires_at',
|
||||||
|
'granted_scopes',
|
||||||
|
'id_token',
|
||||||
|
'id_token_claims_obj',
|
||||||
|
'id_token_expires_at',
|
||||||
|
'id_token_stored_at',
|
||||||
|
'nonce',
|
||||||
|
'PKCE_verifier',
|
||||||
|
'refresh_token',
|
||||||
|
'session_state'
|
||||||
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private oauthService: OAuthService,
|
private oauthService: OAuthService,
|
||||||
private _oauthStorage: OAuthStorage,
|
private _oauthStorage: OAuthStorage,
|
||||||
@@ -69,6 +84,13 @@ export class RedirectAuthService extends AuthService {
|
|||||||
shareReplay(1)
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.oauthService.events.pipe(take(1)).subscribe(() => {
|
||||||
|
if(this.oauthService.getAccessToken() && !this.authenticated){
|
||||||
|
this.AUTH_STORAGE_ITEMS.map((item: string) => this._oauthStorage.removeItem(item));
|
||||||
|
this.reloadPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.onLogin = this.authenticated$.pipe(
|
this.onLogin = this.authenticated$.pipe(
|
||||||
filter((authenticated) => authenticated),
|
filter((authenticated) => authenticated),
|
||||||
map(() => undefined)
|
map(() => undefined)
|
||||||
@@ -223,4 +245,9 @@ export class RedirectAuthService extends AuthService {
|
|||||||
updateIDPConfiguration(config: AuthConfig) {
|
updateIDPConfiguration(config: AuthConfig) {
|
||||||
this.oauthService.configure(config);
|
this.oauthService.configure(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadPage() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
18
package-lock.json
generated
18
package-lock.json
generated
@@ -26,7 +26,7 @@
|
|||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"@storybook/core-server": "8.2.6",
|
"@storybook/core-server": "8.2.6",
|
||||||
"@storybook/theming": "8.2.6",
|
"@storybook/theming": "8.2.6",
|
||||||
"angular-oauth2-oidc": "^13.0.1",
|
"angular-oauth2-oidc": "^15.0.1",
|
||||||
"angular-oauth2-oidc-jwks": "^17.0.2",
|
"angular-oauth2-oidc-jwks": "^17.0.2",
|
||||||
"apollo-angular": "^5.0.2",
|
"apollo-angular": "^5.0.2",
|
||||||
"chart.js": "^4.3.0",
|
"chart.js": "^4.3.0",
|
||||||
@@ -13900,16 +13900,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/angular-oauth2-oidc": {
|
"node_modules/angular-oauth2-oidc": {
|
||||||
"version": "13.0.1",
|
"version": "15.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-13.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-15.0.1.tgz",
|
||||||
"integrity": "sha512-aL1VIv9Jqoqq31lbpUXIeNpM3GeN/ldb3KOlq0cV92amGpZs9J4YA+2rlJ5V9zb6NFNbvd7XfTntMbnNuS0+CQ==",
|
"integrity": "sha512-5gpqO9QL+qFqMItYFHe8F6H5nOIEaowcNUc9iTDs3P1bfVYnoKoVAaijob53PuPTF4YwzdfwKWZi4Mq6P7GENQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-sha256": "^1.3.0",
|
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": ">=12.0.0",
|
"@angular/common": ">=14.0.0",
|
||||||
"@angular/core": ">=12.0.0"
|
"@angular/core": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/angular-oauth2-oidc-jwks": {
|
"node_modules/angular-oauth2-oidc-jwks": {
|
||||||
@@ -19157,11 +19156,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
|
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
|
||||||
},
|
},
|
||||||
"node_modules/fast-sha256": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="
|
|
||||||
},
|
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
|
||||||
|
@@ -46,7 +46,7 @@
|
|||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"@storybook/core-server": "8.2.6",
|
"@storybook/core-server": "8.2.6",
|
||||||
"@storybook/theming": "8.2.6",
|
"@storybook/theming": "8.2.6",
|
||||||
"angular-oauth2-oidc": "^13.0.1",
|
"angular-oauth2-oidc": "^15.0.1",
|
||||||
"angular-oauth2-oidc-jwks": "^17.0.2",
|
"angular-oauth2-oidc-jwks": "^17.0.2",
|
||||||
"apollo-angular": "^5.0.2",
|
"apollo-angular": "^5.0.2",
|
||||||
"chart.js": "^4.3.0",
|
"chart.js": "^4.3.0",
|
||||||
|
Reference in New Issue
Block a user