From 230060ada049182050cf9eab887b092154b3b1e8 Mon Sep 17 00:00:00 2001 From: Tobias Soloschenko Date: Fri, 19 May 2023 12:19:52 +0200 Subject: [PATCH] feat: OAuth2 login page --- ui/src/app/app.component.html | 17 ++++++-- ui/src/app/app.component.ts | 2 + ui/src/app/app.module.ts | 3 +- .../security/service/security.service.spec.ts | 2 +- .../app/security/service/security.service.ts | 39 +++++++++++++++---- ui/src/app/security/store/security.action.ts | 12 ++++-- .../security/store/security.effect.spec.ts | 11 +++++- .../security/store/security.reducer.spec.ts | 20 ++++++++-- ui/src/app/security/store/security.reducer.ts | 16 ++++++-- ui/src/app/shared/model/security.model.ts | 1 + ui/src/app/tests/data/security.ts | 1 + 11 files changed, 99 insertions(+), 25 deletions(-) diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 29cd9edbb..b077bf9c1 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -5,12 +5,23 @@

{{ 'appRoot.welcomeTo' | translate }}

{{ 'appRoot.appTitle' | translate }}
{{ 'appRoot.youHaveToLogin' | translate }}
-
- Log In -
+
+ +
+ Log In +
+
+ +
+
+ {{ item }} +
+
+
+ diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 19b92a7cb..0969267fd 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -11,6 +11,8 @@ import {UrlUtilities} from './url-utilities.service'; export class AppComponent { shouldProtect = this.securityService.shouldProtect(); securityEnabled = this.securityService.securityEnabled(); + isOAuth2 = this.securityService.isOAuth2(); + clientRegistrations = this.securityService.clientRegistrations(); baseApiUrl = UrlUtilities.calculateBaseApiUrl(); constructor( diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index b073e026b..5d7ed44e3 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -83,7 +83,8 @@ import {UrlUtilities} from './url-utilities.service'; security.authenticationEnabled, security.authenticated, security.username, - security.roles + security.roles, + security.clientRegistrations ); if (security.authenticated || !security.authenticationEnabled) { return aboutService.load().pipe(map(() => security)); diff --git a/ui/src/app/security/service/security.service.spec.ts b/ui/src/app/security/service/security.service.spec.ts index 9565462f0..b2aa6f941 100644 --- a/ui/src/app/security/service/security.service.spec.ts +++ b/ui/src/app/security/service/security.service.spec.ts @@ -93,7 +93,7 @@ describe('security/service/security.service.ts integration', () => { }); it('should have proper state after loaded', () => { - service.loaded(false, true, 'fakeuser', ['role1']); + service.loaded(false, true, 'fakeuser', ['role1'], []); let expected = cold('(a|)', {a: false}); expect(service.securityEnabled().pipe(take(1))).toBeObservable(expected); diff --git a/ui/src/app/security/service/security.service.ts b/ui/src/app/security/service/security.service.ts index 634b2afd6..508d049d4 100644 --- a/ui/src/app/security/service/security.service.ts +++ b/ui/src/app/security/service/security.service.ts @@ -1,12 +1,21 @@ import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; -import {Store, select} from '@ngrx/store'; +import {Store, select, props} from '@ngrx/store'; import {Observable} from 'rxjs'; import {catchError, mergeMap, take} from 'rxjs/operators'; import {HttpUtils} from '../../shared/support/http.utils'; import {ErrorUtils} from '../../shared/support/error.utils'; import {Security} from '../../shared/model/security.model'; -import {State, getUsername, getRoles, getEnabled, getShouldProtect, getSecurity} from '../store/security.reducer'; +import { + State, + getUsername, + getRoles, + getEnabled, + getShouldProtect, + getSecurity, + getClientRegistrations, + isOAuth2 +} from '../store/security.reducer'; import {loaded, logout, unauthorised} from '../store/security.action'; import {UrlUtilities} from '../../url-utilities.service'; @@ -34,12 +43,19 @@ export class SecurityService { return arr1.some(i => arr2.includes(i)); } - loaded(enabled: boolean, authenticated: boolean, username: string, roles: string[]): void { - this.store.dispatch(loaded({enabled, authenticated, username, roles})); + loaded(enabled: boolean, authenticated: boolean, username: string, roles: string[], clientRegistrations: string[]): void { + this.store.dispatch(loaded({enabled, authenticated, username, roles, clientRegistrations})); } unauthorised(): void { - this.store.dispatch(unauthorised()); + this.store.dispatch( + unauthorised({ + authenticated: false, + enabled: false, + username: '', + roles: [], + clientRegistrations: [] + })); } securityEnabled(): Observable { @@ -50,6 +66,10 @@ export class SecurityService { return this.store.pipe(select(getUsername)); } + clientRegistrations(): Observable { + return this.store.pipe(select(getClientRegistrations)); + } + roles(): Observable { return this.store.pipe(select(getRoles)); } @@ -58,6 +78,10 @@ export class SecurityService { return this.store.pipe(select(getShouldProtect)); } + isOAuth2(): Observable { + return this.store.pipe(select(isOAuth2)); + } + load(): Observable { const headers = HttpUtils.getDefaultHttpHeaders(); return this.http @@ -73,8 +97,9 @@ export class SecurityService { const headers = HttpUtils.getDefaultHttpHeaders(); return this.http.get(UrlUtilities.calculateBaseApiUrl() + 'logout', {headers: headers, responseType: 'text'}).pipe( mergeMap(() => { - this.store.dispatch(logout()); - return this.load(); + const observable = this.load(); + observable.pipe().subscribe(securityContext => this.store.dispatch(logout(securityContext))); + return observable; }), catchError(ErrorUtils.catchError) ); diff --git a/ui/src/app/security/store/security.action.ts b/ui/src/app/security/store/security.action.ts index b912ca1bd..9fa05ce85 100644 --- a/ui/src/app/security/store/security.action.ts +++ b/ui/src/app/security/store/security.action.ts @@ -2,7 +2,13 @@ import {createAction, props} from '@ngrx/store'; export const loaded = createAction( '[Security] Loaded', - props<{enabled: boolean; authenticated: boolean; username: string; roles: string[]}>() + props<{enabled: boolean; authenticated: boolean; username: string; roles: string[]; clientRegistrations: string[]}>() +); +export const logout = createAction( + '[Security] Logout', + props<{enabled: boolean; authenticated: boolean; username: string; roles: string[]; clientRegistrations: string[]}>() +); +export const unauthorised = createAction( + '[Security] Unauthorised', + props<{enabled: boolean; authenticated: boolean; username: string; roles: string[]; clientRegistrations: string[]}>() ); -export const logout = createAction('[Security] Logout'); -export const unauthorised = createAction('[Security] Unauthorised'); diff --git a/ui/src/app/security/store/security.effect.spec.ts b/ui/src/app/security/store/security.effect.spec.ts index 701573961..54eac86a6 100644 --- a/ui/src/app/security/store/security.effect.spec.ts +++ b/ui/src/app/security/store/security.effect.spec.ts @@ -20,8 +20,15 @@ describe('Security Effect', () => { })); it('Unauthorised should logout', () => { - actions$ = of(SecurityAction.unauthorised()); - const expected = cold('(a|)', {a: SecurityAction.logout()}); + const props = { + authenticated: false, + enabled: false, + username: '', + roles: [], + clientRegistrations: [] + }; + actions$ = of(SecurityAction.unauthorised(props)); + const expected = cold('(a|)', {a: SecurityAction.logout(props)}); expect(effects.securityReset$).toBeObservable(expected); expect(routerSpy.navigate).toHaveBeenCalledWith(['/']); }); diff --git a/ui/src/app/security/store/security.reducer.spec.ts b/ui/src/app/security/store/security.reducer.spec.ts index ce6270c71..a8e34d616 100644 --- a/ui/src/app/security/store/security.reducer.spec.ts +++ b/ui/src/app/security/store/security.reducer.spec.ts @@ -13,7 +13,8 @@ describe('security/store/security.reducer.ts', () => { enabled: false, authenticated: true, username: 'fakeuser', - roles: ['role1', 'role2'] + roles: ['role1', 'role2'], + clientRegistrations: ['test_registration', 'test_registration2'] }; let newState = fromSecurity.reducer( undefined, @@ -21,7 +22,8 @@ describe('security/store/security.reducer.ts', () => { enabled: false, authenticated: true, username: 'fakeuser', - roles: ['role1', 'role2'] + roles: ['role1', 'role2'], + clientRegistrations: ['test_registration', 'test_registration2'] }) ); expect(newState).toEqual(expectedState); @@ -29,9 +31,19 @@ describe('security/store/security.reducer.ts', () => { enabled: false, authenticated: false, username: undefined, - roles: [] + roles: [], + clientRegistrations: ['test_registration', 'test_registration2'] }; - newState = fromSecurity.reducer(newState, SecurityActions.logout()); + newState = fromSecurity.reducer( + newState, + SecurityActions.logout({ + enabled: false, + authenticated: false, + username: undefined, + roles: [], + clientRegistrations: ['test_registration', 'test_registration2'] + }) + ); expect(newState).toEqual(expectedState); }); }); diff --git a/ui/src/app/security/store/security.reducer.ts b/ui/src/app/security/store/security.reducer.ts index 65b14b53f..df6cea443 100644 --- a/ui/src/app/security/store/security.reducer.ts +++ b/ui/src/app/security/store/security.reducer.ts @@ -9,6 +9,7 @@ export interface SecurityState { authenticated: boolean; username: string; roles: string[]; + clientRegistrations?: string[]; } export interface State extends fromRoot.State { @@ -23,17 +24,22 @@ export const getAuthenticated = (state: State): boolean => state[securityFeature export const getUsername = (state: State): string => state[securityFeatureKey].username; +export const getClientRegistrations = (state: State): string[] => state[securityFeatureKey].clientRegistrations; + export const getRoles = (state: State): string[] => state[securityFeatureKey].roles; export const getShouldProtect = createSelector(getEnabled, getAuthenticated, (enabled, authenticated) => !enabled ? false : enabled && !authenticated ); +export const isOAuth2 = createSelector(getClientRegistrations, clientRegistrations => clientRegistrations.length > 0); + export const initialState: SecurityState = { enabled: true, authenticated: false, username: undefined, - roles: [] + roles: [], + clientRegistrations: [] }; export const reducer = createReducer( @@ -42,12 +48,14 @@ export const reducer = createReducer( enabled: props.enabled, authenticated: props.authenticated, username: props.username, - roles: props.roles + roles: props.roles, + clientRegistrations: props.clientRegistrations })), - on(SecurityActions.logout, state => ({ + on(SecurityActions.logout, (state, props) => ({ enabled: state.enabled, authenticated: false, username: undefined, - roles: [] + roles: props.roles, + clientRegistrations: props.clientRegistrations })) ); diff --git a/ui/src/app/shared/model/security.model.ts b/ui/src/app/shared/model/security.model.ts index 3e8283819..91cca05f4 100644 --- a/ui/src/app/shared/model/security.model.ts +++ b/ui/src/app/shared/model/security.model.ts @@ -3,4 +3,5 @@ export interface Security { authenticated: boolean; username: string; roles: string[]; + clientRegistrations: string[]; } diff --git a/ui/src/app/tests/data/security.ts b/ui/src/app/tests/data/security.ts index 838fb7ecc..b7b51a302 100644 --- a/ui/src/app/tests/data/security.ts +++ b/ui/src/app/tests/data/security.ts @@ -3,5 +3,6 @@ export const LOAD = { authenticated: false, username: null, roles: [], + clientRegistrations: [], _links: {self: {href: 'http://localhost:4200/security/info'}} };