mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
bug fixes for auth service
This commit is contained in:
@@ -81,6 +81,13 @@ export class AppConfigService {
|
|||||||
return this.status === Status.LOADED;
|
return this.status === Status.LOADED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current authentication type as configured via `authType` property.
|
||||||
|
*/
|
||||||
|
get authType(): 'OAUTH' | 'BASIC' {
|
||||||
|
return this.get('authType');
|
||||||
|
}
|
||||||
|
|
||||||
constructor(protected http: HttpClient, protected extensionService: ExtensionService) {
|
constructor(protected http: HttpClient, protected extensionService: ExtensionService) {
|
||||||
this.onLoadSubject = new ReplaySubject();
|
this.onLoadSubject = new ReplaySubject();
|
||||||
this.onLoad = this.onLoadSubject.asObservable();
|
this.onLoad = this.onLoadSubject.asObservable();
|
||||||
|
|||||||
@@ -40,11 +40,6 @@ export class AuthConfigService {
|
|||||||
@Inject(AUTH_MODULE_CONFIG) private readonly authModuleConfig: AuthModuleConfig
|
@Inject(AUTH_MODULE_CONFIG) private readonly authModuleConfig: AuthModuleConfig
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private _authConfig!: AuthConfig;
|
|
||||||
get authConfig(): AuthConfig {
|
|
||||||
return this._authConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadConfig(): Promise<AuthConfig> {
|
loadConfig(): Promise<AuthConfig> {
|
||||||
return this.appConfigService.onLoad.pipe(take(1)).toPromise().then(this.loadAppConfig.bind(this));
|
return this.appConfigService.onLoad.pipe(take(1)).toPromise().then(this.loadAppConfig.bind(this));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { AuthRoutingModule } from './auth-routing.module';
|
|||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { RedirectAuthService } from './redirect-auth.service';
|
import { RedirectAuthService } from './redirect-auth.service';
|
||||||
import { AuthenticationConfirmationComponent } from './view/authentication-confirmation/authentication-confirmation.component';
|
import { AuthenticationConfirmationComponent } from './view/authentication-confirmation/authentication-confirmation.component';
|
||||||
|
import { AppConfigService } from '../../app-config/app-config.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Login Factory function
|
* Create a Login Factory function
|
||||||
@@ -36,8 +37,8 @@ import { AuthenticationConfirmationComponent } from './view/authentication-confi
|
|||||||
* @param config auth configuration
|
* @param config auth configuration
|
||||||
* @returns a factory function
|
* @returns a factory function
|
||||||
*/
|
*/
|
||||||
export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage, config: AuthConfig) {
|
export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage, appConfig: AppConfigService, config: AuthConfig) {
|
||||||
const service = new RedirectAuthService(oAuthService, storage, config);
|
const service = new RedirectAuthService(oAuthService, storage, appConfig, config);
|
||||||
return () => service.init();
|
return () => service.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,19 +50,18 @@ export function loginFactory(oAuthService: OAuthService, storage: OAuthStorage,
|
|||||||
// { provide: AuthGuard, useClass: OidcAuthGuard },
|
// { provide: AuthGuard, useClass: OidcAuthGuard },
|
||||||
// { provide: AuthGuardEcm, useClass: OidcAuthGuard },
|
// { provide: AuthGuardEcm, useClass: OidcAuthGuard },
|
||||||
// { provide: AuthGuardBpm, useClass: OidcAuthGuard },
|
// { provide: AuthGuardBpm, useClass: OidcAuthGuard },
|
||||||
{ provide: AuthenticationService},
|
AuthenticationService,
|
||||||
{ provide: AlfrescoApiService, useClass: AlfrescoApiNoAuthService },
|
{ provide: AlfrescoApiService, useClass: AlfrescoApiNoAuthService },
|
||||||
{
|
{
|
||||||
provide: AUTH_CONFIG,
|
provide: AUTH_CONFIG,
|
||||||
useFactory: authConfigFactory,
|
useFactory: authConfigFactory,
|
||||||
deps: [AuthConfigService]
|
deps: [AuthConfigService]
|
||||||
},
|
},
|
||||||
RedirectAuthService,
|
|
||||||
{ provide: AuthService, useExisting: RedirectAuthService },
|
{ provide: AuthService, useExisting: RedirectAuthService },
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
useFactory: loginFactory,
|
useFactory: loginFactory,
|
||||||
deps: [OAuthService, OAuthStorage, AUTH_CONFIG],
|
deps: [OAuthService, OAuthStorage, AUTH_CONFIG, AppConfigService],
|
||||||
multi: true
|
multi: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,152 +21,163 @@ 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 } from 'rxjs/operators';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
import { AppConfigService } from '../../app-config/app-config.service';
|
||||||
|
|
||||||
const isPromise = <T>(value: T | Promise<T>): value is Promise<T> => value && typeof (value as Promise<T>).then === 'function';
|
const isPromise = <T>(value: T | Promise<T>): value is Promise<T> => value && typeof (value as Promise<T>).then === 'function';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable({ providedIn: 'root' })
|
||||||
export class RedirectAuthService extends AuthService {
|
export class RedirectAuthService extends AuthService {
|
||||||
|
onLogin: Observable<any>;
|
||||||
|
|
||||||
onLogin: Observable<any>;
|
private _loadDiscoveryDocumentPromise = Promise.resolve(false);
|
||||||
|
|
||||||
private _loadDiscoveryDocumentPromise = Promise.resolve(false);
|
/** Subscribe to whether the user has valid Id/Access tokens. */
|
||||||
|
authenticated$!: Observable<boolean>;
|
||||||
|
|
||||||
/** Subscribe to whether the user has valid Id/Access tokens. */
|
/** Subscribe to errors reaching the IdP. */
|
||||||
authenticated$!: Observable<boolean>;
|
idpUnreachable$!: Observable<Error>;
|
||||||
|
|
||||||
/** Subscribe to errors reaching the IdP. */
|
/**
|
||||||
idpUnreachable$!: Observable<Error>;
|
* Get whether the user has valid Id/Access tokens.
|
||||||
|
*
|
||||||
/**
|
* @returns `true` if the user is authenticated, otherwise `false`
|
||||||
* Get whether the user has valid Id/Access tokens.
|
*/
|
||||||
*
|
get authenticated(): boolean {
|
||||||
* @returns `true` if the user is authenticated, otherwise `false`
|
return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken();
|
||||||
*/
|
|
||||||
get authenticated(): boolean {
|
|
||||||
return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
private authConfig!: AuthConfig | Promise<AuthConfig>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private oauthService: OAuthService,
|
|
||||||
private _oauthStorage: OAuthStorage,
|
|
||||||
@Inject(AUTH_CONFIG) authConfig: AuthConfig
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.authConfig = authConfig;
|
|
||||||
|
|
||||||
this.oauthService.clearHashAfterLogin = true;
|
|
||||||
|
|
||||||
this.authenticated$ = this.oauthService.events.pipe(
|
|
||||||
map(() => this.authenticated),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
shareReplay(1)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.onLogin = this.authenticated$.pipe(
|
|
||||||
filter((authenticated) => authenticated),
|
|
||||||
map(() => undefined)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.idpUnreachable$ = this.oauthService.events.pipe(
|
|
||||||
filter((event): event is OAuthErrorEvent => event.type === 'discovery_document_load_error'),
|
|
||||||
map((event) => event.reason as Error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (isPromise(this.authConfig)) {
|
|
||||||
return this.authConfig.then((config) => this.configureAuth(config));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.configureAuth(this.authConfig);
|
private authConfig!: AuthConfig | Promise<AuthConfig>;
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
constructor(
|
||||||
this.oauthService.logOut();
|
private oauthService: OAuthService,
|
||||||
}
|
private oauthStorage: OAuthStorage,
|
||||||
|
private appConfig: AppConfigService,
|
||||||
|
@Inject(AUTH_CONFIG) authConfig: AuthConfig
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.authConfig = authConfig;
|
||||||
|
|
||||||
ensureDiscoveryDocument(): Promise<boolean> {
|
this.oauthService.clearHashAfterLogin = true;
|
||||||
this._loadDiscoveryDocumentPromise = this._loadDiscoveryDocumentPromise
|
|
||||||
.catch(() => false)
|
this.authenticated$ = this.oauthService.events.pipe(
|
||||||
.then((loaded) => {
|
map(() => this.authenticated),
|
||||||
if (!loaded) {
|
distinctUntilChanged(),
|
||||||
return this.oauthService.loadDiscoveryDocument().then(() => true);
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.onLogin = this.authenticated$.pipe(
|
||||||
|
filter((authenticated) => authenticated),
|
||||||
|
map(() => undefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.idpUnreachable$ = this.oauthService.events.pipe(
|
||||||
|
filter((event): event is OAuthErrorEvent => event.type === 'discovery_document_load_error'),
|
||||||
|
map((event) => event.reason as Error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (isPromise(this.authConfig)) {
|
||||||
|
return this.authConfig.then((config) => this.configureAuth(config));
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
|
||||||
return this._loadDiscoveryDocumentPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return this.configureAuth(this.authConfig);
|
||||||
login(currentUrl?: string): void {
|
|
||||||
let stateKey: string | undefined;
|
|
||||||
|
|
||||||
if (currentUrl) {
|
|
||||||
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
|
|
||||||
stateKey = `auth_state_${randomValue}${Date.now()}`;
|
|
||||||
this._oauthStorage.setItem(stateKey, JSON.stringify(currentUrl || {}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initLoginFlow will initialize the login flow in either code or implicit depending on the configuration
|
logout() {
|
||||||
this.ensureDiscoveryDocument().then(() => void this.oauthService.initLoginFlow(stateKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
baseAuthLogin(username: string, password: string): Observable<TokenResponse> {
|
|
||||||
this.oauthService.useHttpBasicAuth = true;
|
|
||||||
|
|
||||||
return from(this.oauthService.fetchTokenUsingPasswordFlow(username, password)).pipe(
|
|
||||||
map((response) => {
|
|
||||||
const props = new Map<string, string>();
|
|
||||||
props.set('id_token', response.id_token);
|
|
||||||
// for backward compatibility we need to set the response in our storage
|
|
||||||
this.oauthService['storeAccessTokenResponse'](response.access_token, response.refresh_token, response.expires_in, response.scope, props);
|
|
||||||
return response;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loginCallback(): Promise<string | undefined> {
|
|
||||||
return this.ensureDiscoveryDocument()
|
|
||||||
.then(() => this.oauthService.tryLogin({ preventClearHashAfterLogin: false }))
|
|
||||||
.then(() => this._getRedirectUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getRedirectUrl() {
|
|
||||||
const DEFAULT_REDIRECT = '/';
|
|
||||||
const stateKey = this.oauthService.state;
|
|
||||||
|
|
||||||
if (stateKey) {
|
|
||||||
const stateStringified = this._oauthStorage.getItem(stateKey);
|
|
||||||
if (stateStringified) {
|
|
||||||
// cleanup state from storage
|
|
||||||
this._oauthStorage.removeItem(stateKey);
|
|
||||||
return JSON.parse(stateStringified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return DEFAULT_REDIRECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private configureAuth(config: AuthConfig) {
|
|
||||||
this.oauthService.configure(config);
|
|
||||||
this.oauthService.tokenValidationHandler = new JwksValidationHandler();
|
|
||||||
|
|
||||||
if (config.sessionChecksEnabled) {
|
|
||||||
this.oauthService.events.pipe(filter((event) => event.type === 'session_terminated')).subscribe(() => {
|
|
||||||
this.oauthService.logOut();
|
this.oauthService.logOut();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.ensureDiscoveryDocument().then(() =>
|
ensureDiscoveryDocument(): Promise<boolean> {
|
||||||
void this.oauthService.setupAutomaticSilentRefresh()
|
if (this.appConfig.authType === 'BASIC') {
|
||||||
).catch(() => {
|
return Promise.resolve(true);
|
||||||
// catch error to prevent the app from crashing when trying to access unprotected routes
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIDPConfiguration(config: AuthConfig) {
|
this._loadDiscoveryDocumentPromise = this._loadDiscoveryDocumentPromise
|
||||||
this.oauthService.configure(config);
|
.catch(() => false)
|
||||||
}
|
.then((loaded) => {
|
||||||
|
if (!loaded) {
|
||||||
|
return this.oauthService.loadDiscoveryDocument().then(() => true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return this._loadDiscoveryDocumentPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
login(currentUrl?: string): void {
|
||||||
|
let stateKey: string | undefined;
|
||||||
|
|
||||||
|
if (currentUrl) {
|
||||||
|
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||||
|
stateKey = `auth_state_${randomValue}${Date.now()}`;
|
||||||
|
this.oauthStorage.setItem(stateKey, JSON.stringify(currentUrl || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// initLoginFlow will initialize the login flow in either code or implicit depending on the configuration
|
||||||
|
this.ensureDiscoveryDocument().then(() => void this.oauthService.initLoginFlow(stateKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
baseAuthLogin(username: string, password: string): Observable<TokenResponse> {
|
||||||
|
this.oauthService.useHttpBasicAuth = true;
|
||||||
|
|
||||||
|
return from(this.oauthService.fetchTokenUsingPasswordFlow(username, password)).pipe(
|
||||||
|
map((response) => {
|
||||||
|
const props = new Map<string, string>();
|
||||||
|
props.set('id_token', response.id_token);
|
||||||
|
// for backward compatibility we need to set the response in our storage
|
||||||
|
this.oauthService['storeAccessTokenResponse'](
|
||||||
|
response.access_token,
|
||||||
|
response.refresh_token,
|
||||||
|
response.expires_in,
|
||||||
|
response.scope,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginCallback(): Promise<string | undefined> {
|
||||||
|
return this.ensureDiscoveryDocument()
|
||||||
|
.then(() => this.oauthService.tryLogin({ preventClearHashAfterLogin: false }))
|
||||||
|
.then(() => this._getRedirectUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getRedirectUrl() {
|
||||||
|
const DEFAULT_REDIRECT = '/';
|
||||||
|
const stateKey = this.oauthService.state;
|
||||||
|
|
||||||
|
if (stateKey) {
|
||||||
|
const stateStringified = this.oauthStorage.getItem(stateKey);
|
||||||
|
if (stateStringified) {
|
||||||
|
// cleanup state from storage
|
||||||
|
this.oauthStorage.removeItem(stateKey);
|
||||||
|
return JSON.parse(stateStringified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_REDIRECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async configureAuth(config: AuthConfig) {
|
||||||
|
this.oauthService.configure(config);
|
||||||
|
this.oauthService.tokenValidationHandler = new JwksValidationHandler();
|
||||||
|
|
||||||
|
if (config.sessionChecksEnabled) {
|
||||||
|
this.oauthService.events.pipe(filter((event) => event.type === 'session_terminated')).subscribe(() => {
|
||||||
|
this.oauthService.logOut();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.ensureDiscoveryDocument();
|
||||||
|
return void this.oauthService.setupAutomaticSilentRefresh();
|
||||||
|
} catch {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateIDPConfiguration(config: AuthConfig) {
|
||||||
|
this.oauthService.configure(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user