From 83259f64320a5a92902846cbef2f502988fcf484 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 23 May 2022 23:34:45 +0200 Subject: [PATCH 01/42] added proxy for local keycloak --- proxy.conf.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/proxy.conf.json b/proxy.conf.json index ed34a75bf5..7adc593987 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -16,5 +16,14 @@ "pathRewrite": { "^/nextcloud": "" } + }, + "/auth": { + "target": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token", + "secure": true, + "logLevel": "debug", + "changeOrigin": true, + "pathRewrite": { + "^/auth": "" + } } } From a713dcad5e0bdd6d8878f36e40d8a8cadb1de356 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 23 May 2022 23:35:45 +0200 Subject: [PATCH 02/42] remote session authorizes against keycloak --- .../session/session-service/remote-session.ts | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts index 672d74cec3..6d7fedd28c 100644 --- a/src/app/core/session/session-service/remote-session.ts +++ b/src/app/core/session/session-service/remote-session.ts @@ -16,12 +16,17 @@ */ import { AppConfig } from "../../app-config/app-config"; import { Injectable } from "@angular/core"; -import { HttpClient, HttpErrorResponse } from "@angular/common/http"; +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, +} from "@angular/common/http"; import { DatabaseUser } from "./local-user"; import { SessionService } from "./session.service"; import { LoginState } from "../session-states/login-state.enum"; import { PouchDatabase } from "../../database/pouch-database"; import { LoggingService } from "../../logging/logging.service"; +import PouchDB from "pouchdb-browser"; /** * Responsibilities: @@ -46,12 +51,7 @@ export class RemoteSession extends SessionService { private loggingService: LoggingService ) { super(); - this.database = new PouchDatabase(this.loggingService).initIndexedDB( - AppConfig.settings.database.remote_url + AppConfig.settings.database.name, - { - skip_setup: true, - } - ); + this.database = new PouchDatabase(this.loggingService); } /** @@ -61,15 +61,42 @@ export class RemoteSession extends SessionService { */ public async login(username: string, password: string): Promise { try { + const body = new URLSearchParams(); + body.set("username", username); + body.set("password", password); + body.set("grant_type", "password"); + body.set("client_id", "myclient"); + const options = { + headers: new HttpHeaders().set( + "Content-Type", + "application/x-www-form-urlencoded" + ), + }; const response = await this.httpClient - .post( - `${AppConfig.settings.database.remote_url}_session`, - { name: username, password: password }, - { withCredentials: true } - ) + .post(`/auth`, body.toString(), options) + .toPromise(); + this.database.initIndexedDB( + AppConfig.settings.database.remote_url + + AppConfig.settings.database.name, + { + skip_setup: true, + fetch: (url, opts) => { + // @ts-ignore + opts.headers.set( + "Authorization", + "Bearer " + response.access_token + ); + return PouchDB.fetch(url, opts); + }, + } + ); + const user = await this.httpClient + .get(`${AppConfig.settings.database.remote_url}_session`, { + withCredentials: true, + headers: { Authorization: "Bearer " + response.access_token }, + }) .toPromise(); - await this.handleSuccessfulLogin(response); - this.assignDatabaseUser(response); + await this.handleSuccessfulLogin(user.userCtx); localStorage.setItem( RemoteSession.LAST_LOGIN_KEY, new Date().toISOString() @@ -86,13 +113,6 @@ export class RemoteSession extends SessionService { return this.loginState.value; } - private assignDatabaseUser(couchDBResponse: any) { - this.currentDBUser = { - name: couchDBResponse.name, - roles: couchDBResponse.roles, - }; - } - public async handleSuccessfulLogin(userObject: DatabaseUser) { this.currentDBUser = userObject; this.loginState.next(LoginState.LOGGED_IN); From fc74555c68a62f007723f478fa69ed4508e7b4e7 Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 17 Jul 2022 22:14:10 +0200 Subject: [PATCH 03/42] updated to latest version and added support for autologin. Currently done with access_token, this should be a refresh token instead. Currently token validity is not limited --- proxy.conf.json | 2 +- .../session/session-service/remote-session.ts | 77 +++++++++++-------- .../session-service/synced-session.service.ts | 14 ++-- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/proxy.conf.json b/proxy.conf.json index 7adc593987..380a64bb44 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -1,6 +1,6 @@ { "/db": { - "target": "https://dev.aam-digital.com/db", + "target": "http://localhost:5984", "secure": true, "logLevel": "debug", "changeOrigin": true, diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts index 6d7fedd28c..7e6f87c49d 100644 --- a/src/app/core/session/session-service/remote-session.ts +++ b/src/app/core/session/session-service/remote-session.ts @@ -27,6 +27,7 @@ import { LoginState } from "../session-states/login-state.enum"; import { PouchDatabase } from "../../database/pouch-database"; import { LoggingService } from "../../logging/logging.service"; import PouchDB from "pouchdb-browser"; +import { firstValueFrom } from "rxjs"; /** * Responsibilities: @@ -61,41 +62,21 @@ export class RemoteSession extends SessionService { */ public async login(username: string, password: string): Promise { try { - const body = new URLSearchParams(); - body.set("username", username); - body.set("password", password); - body.set("grant_type", "password"); - body.set("client_id", "myclient"); - const options = { - headers: new HttpHeaders().set( - "Content-Type", - "application/x-www-form-urlencoded" - ), - }; - const response = await this.httpClient - .post(`/auth`, body.toString(), options) - .toPromise(); - this.database.initIndexedDB( - AppConfig.settings.database.remote_url + - AppConfig.settings.database.name, - { - skip_setup: true, - fetch: (url, opts) => { - // @ts-ignore - opts.headers.set( - "Authorization", - "Bearer " + response.access_token - ); - return PouchDB.fetch(url, opts); - }, - } + let token = window.localStorage.getItem("token"); + if (!token) { + const response = await this.getUserToken(username, password); + token = response.access_token; + } + window.localStorage.setItem("token", token); + const user = await firstValueFrom( + this.httpClient.get( + `${AppConfig.settings.database.remote_url}_session`, + { + withCredentials: true, + headers: { Authorization: "Bearer " + token }, + } + ) ); - const user = await this.httpClient - .get(`${AppConfig.settings.database.remote_url}_session`, { - withCredentials: true, - headers: { Authorization: "Bearer " + response.access_token }, - }) - .toPromise(); await this.handleSuccessfulLogin(user.userCtx); localStorage.setItem( RemoteSession.LAST_LOGIN_KEY, @@ -113,7 +94,35 @@ export class RemoteSession extends SessionService { return this.loginState.value; } + private getUserToken(username: string, password: string) { + const body = new URLSearchParams(); + body.set("username", username); + body.set("password", password); + body.set("grant_type", "password"); + body.set("client_id", "myclient"); + const options = { + headers: new HttpHeaders().set( + "Content-Type", + "application/x-www-form-urlencoded" + ), + }; + return firstValueFrom( + this.httpClient.post(`/auth`, body.toString(), options) + ); + } + public async handleSuccessfulLogin(userObject: DatabaseUser) { + let token = window.localStorage.getItem("token"); + this.database.initIndexedDB( + AppConfig.settings.database.remote_url + AppConfig.settings.database.name, + { + skip_setup: true, + fetch: (url, opts: any) => { + opts.headers.set("Authorization", "Bearer " + token); + return PouchDB.fetch(url, opts); + }, + } + ); this.currentDBUser = userObject; this.loginState.next(LoginState.LOGGED_IN); } diff --git a/src/app/core/session/session-service/synced-session.service.ts b/src/app/core/session/session-service/synced-session.service.ts index ba353ae5d9..2ea29ca7dd 100644 --- a/src/app/core/session/session-service/synced-session.service.ts +++ b/src/app/core/session/session-service/synced-session.service.ts @@ -74,9 +74,11 @@ export class SyncedSessionService extends SessionService { * Do login automatically if there is still a valid CouchDB cookie from last login with username and password */ checkForValidSession() { + const token = window.localStorage.getItem("token"); this.httpClient - .get(`${AppConfig.settings.database.remote_url}_session`, { + .get(`${AppConfig.settings.database.remote_url}_session`, { withCredentials: true, + headers: { Authorization: "Bearer " + token }, }) .subscribe((res: any) => { if (res.userCtx.name) { @@ -225,10 +227,12 @@ export class SyncedSessionService extends SessionService { this.syncState.next(SyncState.STARTED); const localPouchDB = this.localSession.getDatabase().getPouchDB(); const remotePouchDB = this.remoteSession.getDatabase().getPouchDB(); - this._liveSyncHandle = (localPouchDB.sync(remotePouchDB, { - live: true, - retry: true, - }) as any) + this._liveSyncHandle = ( + localPouchDB.sync(remotePouchDB, { + live: true, + retry: true, + }) as any + ) .on("paused", (info) => { // replication was paused: either because sync is finished or because of a failed sync (mostly due to lost connection). info is empty. if (this.remoteSession.loginState.value === LoginState.LOGGED_IN) { From 75634046a500c785523674112cd5cb9d91a5b188 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 18 Jul 2022 12:18:59 +0200 Subject: [PATCH 04/42] added tests for remote session login using jwt --- .../session-service/remote-session.spec.ts | 72 ++++++++++++------- .../session/session-service/remote-session.ts | 42 ++++++----- src/app/utils/utils.ts | 14 ++++ 3 files changed, 82 insertions(+), 46 deletions(-) diff --git a/src/app/core/session/session-service/remote-session.spec.ts b/src/app/core/session/session-service/remote-session.spec.ts index 7dd3d53d35..44a1a97c60 100644 --- a/src/app/core/session/session-service/remote-session.spec.ts +++ b/src/app/core/session/session-service/remote-session.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from "@angular/core/testing"; -import { RemoteSession } from "./remote-session"; +import { JwtToken, RemoteSession } from "./remote-session"; import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { of, throwError } from "rxjs"; import { AppConfig } from "../../app-config/app-config"; @@ -14,6 +14,25 @@ describe("RemoteSessionService", () => { let service: RemoteSession; let mockHttpClient: jasmine.SpyObj; let dbUser: DatabaseUser; + /** + * Check {@link jwt.io} to decode the JWT token. + * Extract: + * ```json + * { + * "sub": "test", + * "exp": 1658138259, + * "_couchdb.roles": [ + * "user_app" + * ], + * ... + * } + * ``` + */ + const jwtTokenResponse: JwtToken = { + access_token: + "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzV2FQaEdOYy1GSWpTdHBYVk96Y29oMjdSbVBpYVRwZjRlWUItUHpwU1VVIn0.eyJleHAiOjE2NTgxMzk0NTUsImlhdCI6MTY1ODEzOTM5NSwianRpIjoiY2YwZTYzNTMtNTJiMy00NzgyLTg1YjAtOWNkMzQ4MTM4YmI4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9teXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6InRlc3QiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJteWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI2ZWE5YzRkYi0wOGZmLTQ4MjQtOTUzMS1lNTIzODg1Y2E2NTIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHBzOi8vd3d3LmtleWNsb2FrLm9yZyJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1teXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im15Y2xpZW50Ijp7InJvbGVzIjpbInVzZXJfYXBwIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI2ZWE5YzRkYi0wOGZmLTQ4MjQtOTUzMS1lNTIzODg1Y2E2NTIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIl9jb3VjaGRiLnJvbGVzIjpbInVzZXJfYXBwIl0sInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QifQ.eynm8Zoox4Ovad0K4fYkia4mIyJUJpSfxEM0ZivQAD4LzhzDXuixsEJLQ3RFHI421k2q4wOMaorQAhjVbhmuu9CVQbPjTvNWfeO5DfTdo103KzWmQirWgBHP47H7dwF_2ksyag5HWFMTMCoD9BrNNPJPGjgkabXEFz4-dXg0GaslWd5MIO21cYqkZc_8YhFFLj9oP5gaY-9f3WTQg5YWTg7wk2YKM8IFlKrO67DMLTuO361lSdltnPNIBGzzAEC_oCM3GNFB1d8OkUnA5No89DhxCqQGHeQTKiYiJKoCdnZNQr7pIY0Ml-uNtAEHKAMV9Q6_sxqNcIFjROirdv3kkA", + refresh_token: "test-refresh-token", + }; beforeEach(() => { AppConfig.settings = { @@ -44,24 +63,20 @@ describe("RemoteSessionService", () => { return of(dbUser as any); } else { return throwError( - new HttpErrorResponse({ status: service.UNAUTHORIZED_STATUS_CODE }) + () => + new HttpErrorResponse({ status: service.UNAUTHORIZED_STATUS_CODE }) ); } }); }); - it("should be connected after successful login", async () => { - expect(service.loginState.value).toBe(LoginState.LOGGED_OUT); - - await service.login(TEST_USER, TEST_PASSWORD); - - expect(mockHttpClient.post).toHaveBeenCalled(); - expect(service.loginState.value).toBe(LoginState.LOGGED_IN); - }); + afterEach(() => + window.localStorage.removeItem(RemoteSession.REFRESH_TOKEN_KEY) + ); it("should be unavailable if requests fails with error other than 401", async () => { mockHttpClient.post.and.returnValue( - throwError(new HttpErrorResponse({ status: 501 })) + throwError(() => new HttpErrorResponse({ status: 501 })) ); await service.login(TEST_USER, TEST_PASSWORD); @@ -69,32 +84,35 @@ describe("RemoteSessionService", () => { expect(service.loginState.value).toBe(LoginState.UNAVAILABLE); }); - it("should be rejected if login is unauthorized", async () => { - await service.login(TEST_USER, "wrongPassword"); - - expect(service.loginState.value).toBe(LoginState.LOGIN_FAILED); + it("should not throw error when remote logout is not possible", () => { + mockHttpClient.delete.and.returnValue(throwError(() => new Error())); + return expectAsync(service.logout()).not.toBeRejected(); }); - it("should disconnect after logout", async () => { - await service.login(TEST_USER, TEST_PASSWORD); + it("should request token and store refresh token in local storage", async () => { + mockHttpClient.post.and.returnValue(of(jwtTokenResponse)); - await service.logout(); + await service.login(TEST_USER, TEST_PASSWORD); - expect(service.loginState.value).toBe(LoginState.LOGGED_OUT); + expect(window.localStorage.getItem(RemoteSession.REFRESH_TOKEN_KEY)).toBe( + "test-refresh-token" + ); }); - it("should assign the current user after successful login", async () => { + it("should store access token in remote session", async () => { + mockHttpClient.post.and.returnValue(of(jwtTokenResponse)); + await service.login(TEST_USER, TEST_PASSWORD); - expect(service.getCurrentUser()).toEqual({ - name: dbUser.name, - roles: dbUser.roles, - }); + expect(service.accessToken).toBe(jwtTokenResponse.access_token); }); - it("should not throw error when remote logout is not possible", () => { - mockHttpClient.delete.and.returnValue(throwError(new Error())); - return expectAsync(service.logout()).not.toBeRejected(); + it("should take username and roles from jwtToken", async () => { + mockHttpClient.post.and.returnValue(of(jwtTokenResponse)); + + await service.login(TEST_USER, TEST_PASSWORD); + + expect(service.getCurrentUser()).toEqual(dbUser); }); testSessionServiceImplementation(() => Promise.resolve(service)); diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts index 7e6f87c49d..75154e2f26 100644 --- a/src/app/core/session/session-service/remote-session.ts +++ b/src/app/core/session/session-service/remote-session.ts @@ -28,6 +28,7 @@ import { PouchDatabase } from "../../database/pouch-database"; import { LoggingService } from "../../logging/logging.service"; import PouchDB from "pouchdb-browser"; import { firstValueFrom } from "rxjs"; +import { parseJwt } from "../../../utils/utils"; /** * Responsibilities: @@ -38,12 +39,15 @@ import { firstValueFrom } from "rxjs"; @Injectable() export class RemoteSession extends SessionService { static readonly LAST_LOGIN_KEY = "LAST_REMOTE_LOGIN"; + static readonly REFRESH_TOKEN_KEY = "REFRESH_TOKEN"; // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 readonly UNAUTHORIZED_STATUS_CODE = 401; /** remote (!) PouchDB */ private readonly database: PouchDatabase; private currentDBUser: DatabaseUser; + public accessToken: string; + /** * Create a RemoteSession and set up connection to the remote CouchDB server configured in AppConfig. */ @@ -62,22 +66,17 @@ export class RemoteSession extends SessionService { */ public async login(username: string, password: string): Promise { try { - let token = window.localStorage.getItem("token"); - if (!token) { - const response = await this.getUserToken(username, password); - token = response.access_token; - } - window.localStorage.setItem("token", token); - const user = await firstValueFrom( - this.httpClient.get( - `${AppConfig.settings.database.remote_url}_session`, - { - withCredentials: true, - headers: { Authorization: "Bearer " + token }, - } - ) + let response = await this.getUserToken(username, password); + localStorage.setItem( + RemoteSession.REFRESH_TOKEN_KEY, + response.refresh_token ); - await this.handleSuccessfulLogin(user.userCtx); + this.accessToken = response.access_token; + const parsedToken = parseJwt(response.access_token); + await this.handleSuccessfulLogin({ + name: parsedToken.sub, + roles: parsedToken["_couchdb.roles"], + }); localStorage.setItem( RemoteSession.LAST_LOGIN_KEY, new Date().toISOString() @@ -94,7 +93,7 @@ export class RemoteSession extends SessionService { return this.loginState.value; } - private getUserToken(username: string, password: string) { + private getUserToken(username: string, password: string): Promise { const body = new URLSearchParams(); body.set("username", username); body.set("password", password); @@ -107,18 +106,17 @@ export class RemoteSession extends SessionService { ), }; return firstValueFrom( - this.httpClient.post(`/auth`, body.toString(), options) + this.httpClient.post(`/auth`, body.toString(), options) ); } public async handleSuccessfulLogin(userObject: DatabaseUser) { - let token = window.localStorage.getItem("token"); this.database.initIndexedDB( AppConfig.settings.database.remote_url + AppConfig.settings.database.name, { skip_setup: true, fetch: (url, opts: any) => { - opts.headers.set("Authorization", "Bearer " + token); + opts.headers.set("Authorization", "Bearer " + this.accessToken); return PouchDB.fetch(url, opts); }, } @@ -131,6 +129,7 @@ export class RemoteSession extends SessionService { * Logout at the remote database. */ public async logout(): Promise { + window.localStorage.removeItem(RemoteSession.REFRESH_TOKEN_KEY); await this.httpClient .delete(`${AppConfig.settings.database.remote_url}_session`, { withCredentials: true, @@ -154,3 +153,8 @@ export class RemoteSession extends SessionService { return this.database; } } + +export interface JwtToken { + access_token: string; + refresh_token: string; +} diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index 999ce3fcca..782fb4e5be 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -97,3 +97,17 @@ export function compareEnums( ): boolean { return a?.id === b?.id; } + +export function parseJwt(token) { + const base64Url = token.split(".")[1]; + const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); + const jsonPayload = decodeURIComponent( + window + .atob(base64) + .split("") + .map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)) + .join("") + ); + + return JSON.parse(jsonPayload); +} From 4b9fd51e141041db185e7bb18300352e8e0da3d8 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 18 Jul 2022 23:03:12 +0200 Subject: [PATCH 05/42] addded token renewal --- .../session-service/remote-session.spec.ts | 106 ++++++++++-------- .../session/session-service/remote-session.ts | 76 ++++++++----- .../session-service/session.service.spec.ts | 2 +- .../synced-session.service.spec.ts | 23 ++-- .../session-service/synced-session.service.ts | 4 +- src/app/utils/utils.ts | 2 +- 6 files changed, 126 insertions(+), 87 deletions(-) diff --git a/src/app/core/session/session-service/remote-session.spec.ts b/src/app/core/session/session-service/remote-session.spec.ts index 44a1a97c60..c4b3191705 100644 --- a/src/app/core/session/session-service/remote-session.spec.ts +++ b/src/app/core/session/session-service/remote-session.spec.ts @@ -1,4 +1,4 @@ -import { TestBed } from "@angular/core/testing"; +import { fakeAsync, TestBed, tick } from "@angular/core/testing"; import { JwtToken, RemoteSession } from "./remote-session"; import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { of, throwError } from "rxjs"; @@ -10,29 +10,49 @@ import { DatabaseUser } from "./local-user"; import { LoginState } from "../session-states/login-state.enum"; import { TEST_PASSWORD, TEST_USER } from "../../../utils/mocked-testing.module"; +export function remoteSessionHttpFake(url, body) { + const params = new URLSearchParams(body); + const isValidPassword = + params.get("username") === TEST_USER && + params.get("password") === TEST_PASSWORD; + const isValidToken = params.get("refresh_token") === "test-refresh-token"; + if (isValidPassword || isValidToken) { + return of(jwtTokenResponse as any); + } else { + return throwError( + () => + new HttpErrorResponse({ + status: RemoteSession.UNAUTHORIZED_STATUS_CODE, + }) + ); + } +} + +/** + * Check {@link https://jwt.io} to decode the access_token. + * Extract: + * ```json + * { + * "sub": "test", + * "exp": 1658138259, + * "_couchdb.roles": [ + * "user_app" + * ], + * ... + * } + * ``` + */ +const jwtTokenResponse: JwtToken = { + access_token: + "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzV2FQaEdOYy1GSWpTdHBYVk96Y29oMjdSbVBpYVRwZjRlWUItUHpwU1VVIn0.eyJleHAiOjE2NTgxMzk0NTUsImlhdCI6MTY1ODEzOTM5NSwianRpIjoiY2YwZTYzNTMtNTJiMy00NzgyLTg1YjAtOWNkMzQ4MTM4YmI4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9teXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6InRlc3QiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJteWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI2ZWE5YzRkYi0wOGZmLTQ4MjQtOTUzMS1lNTIzODg1Y2E2NTIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHBzOi8vd3d3LmtleWNsb2FrLm9yZyJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1teXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im15Y2xpZW50Ijp7InJvbGVzIjpbInVzZXJfYXBwIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI2ZWE5YzRkYi0wOGZmLTQ4MjQtOTUzMS1lNTIzODg1Y2E2NTIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIl9jb3VjaGRiLnJvbGVzIjpbInVzZXJfYXBwIl0sInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QifQ.eynm8Zoox4Ovad0K4fYkia4mIyJUJpSfxEM0ZivQAD4LzhzDXuixsEJLQ3RFHI421k2q4wOMaorQAhjVbhmuu9CVQbPjTvNWfeO5DfTdo103KzWmQirWgBHP47H7dwF_2ksyag5HWFMTMCoD9BrNNPJPGjgkabXEFz4-dXg0GaslWd5MIO21cYqkZc_8YhFFLj9oP5gaY-9f3WTQg5YWTg7wk2YKM8IFlKrO67DMLTuO361lSdltnPNIBGzzAEC_oCM3GNFB1d8OkUnA5No89DhxCqQGHeQTKiYiJKoCdnZNQr7pIY0Ml-uNtAEHKAMV9Q6_sxqNcIFjROirdv3kkA", + refresh_token: "test-refresh-token", + expires_in: 120, +}; + describe("RemoteSessionService", () => { let service: RemoteSession; let mockHttpClient: jasmine.SpyObj; let dbUser: DatabaseUser; - /** - * Check {@link jwt.io} to decode the JWT token. - * Extract: - * ```json - * { - * "sub": "test", - * "exp": 1658138259, - * "_couchdb.roles": [ - * "user_app" - * ], - * ... - * } - * ``` - */ - const jwtTokenResponse: JwtToken = { - access_token: - "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzV2FQaEdOYy1GSWpTdHBYVk96Y29oMjdSbVBpYVRwZjRlWUItUHpwU1VVIn0.eyJleHAiOjE2NTgxMzk0NTUsImlhdCI6MTY1ODEzOTM5NSwianRpIjoiY2YwZTYzNTMtNTJiMy00NzgyLTg1YjAtOWNkMzQ4MTM4YmI4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9teXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6InRlc3QiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJteWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI2ZWE5YzRkYi0wOGZmLTQ4MjQtOTUzMS1lNTIzODg1Y2E2NTIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHBzOi8vd3d3LmtleWNsb2FrLm9yZyJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1teXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im15Y2xpZW50Ijp7InJvbGVzIjpbInVzZXJfYXBwIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI2ZWE5YzRkYi0wOGZmLTQ4MjQtOTUzMS1lNTIzODg1Y2E2NTIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIl9jb3VjaGRiLnJvbGVzIjpbInVzZXJfYXBwIl0sInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QifQ.eynm8Zoox4Ovad0K4fYkia4mIyJUJpSfxEM0ZivQAD4LzhzDXuixsEJLQ3RFHI421k2q4wOMaorQAhjVbhmuu9CVQbPjTvNWfeO5DfTdo103KzWmQirWgBHP47H7dwF_2ksyag5HWFMTMCoD9BrNNPJPGjgkabXEFz4-dXg0GaslWd5MIO21cYqkZc_8YhFFLj9oP5gaY-9f3WTQg5YWTg7wk2YKM8IFlKrO67DMLTuO361lSdltnPNIBGzzAEC_oCM3GNFB1d8OkUnA5No89DhxCqQGHeQTKiYiJKoCdnZNQr7pIY0Ml-uNtAEHKAMV9Q6_sxqNcIFjROirdv3kkA", - refresh_token: "test-refresh-token", - }; beforeEach(() => { AppConfig.settings = { @@ -43,8 +63,8 @@ describe("RemoteSessionService", () => { remote_url: "database_url", }, }; - mockHttpClient = jasmine.createSpyObj(["post", "delete"]); - mockHttpClient.delete.and.returnValue(of()); + mockHttpClient = jasmine.createSpyObj(["post"]); + mockHttpClient.post.and.callFake(remoteSessionHttpFake); TestBed.configureTestingModule({ providers: [ @@ -57,17 +77,6 @@ describe("RemoteSessionService", () => { // Remote session allows TEST_USER and TEST_PASSWORD as valid credentials dbUser = { name: TEST_USER, roles: ["user_app"] }; service = TestBed.inject(RemoteSession); - - mockHttpClient.post.and.callFake((url, body) => { - if (body.name === TEST_USER && body.password === TEST_PASSWORD) { - return of(dbUser as any); - } else { - return throwError( - () => - new HttpErrorResponse({ status: service.UNAUTHORIZED_STATUS_CODE }) - ); - } - }); }); afterEach(() => @@ -84,14 +93,7 @@ describe("RemoteSessionService", () => { expect(service.loginState.value).toBe(LoginState.UNAVAILABLE); }); - it("should not throw error when remote logout is not possible", () => { - mockHttpClient.delete.and.returnValue(throwError(() => new Error())); - return expectAsync(service.logout()).not.toBeRejected(); - }); - it("should request token and store refresh token in local storage", async () => { - mockHttpClient.post.and.returnValue(of(jwtTokenResponse)); - await service.login(TEST_USER, TEST_PASSWORD); expect(window.localStorage.getItem(RemoteSession.REFRESH_TOKEN_KEY)).toBe( @@ -100,20 +102,36 @@ describe("RemoteSessionService", () => { }); it("should store access token in remote session", async () => { - mockHttpClient.post.and.returnValue(of(jwtTokenResponse)); - await service.login(TEST_USER, TEST_PASSWORD); expect(service.accessToken).toBe(jwtTokenResponse.access_token); }); it("should take username and roles from jwtToken", async () => { - mockHttpClient.post.and.returnValue(of(jwtTokenResponse)); - await service.login(TEST_USER, TEST_PASSWORD); expect(service.getCurrentUser()).toEqual(dbUser); }); + it("should update token before it expires", fakeAsync(() => { + // token has 2 minutes expiration time + service.login(TEST_USER, TEST_PASSWORD); + tick(); + + mockHttpClient.post.calls.reset(); + const newToken = { ...jwtTokenResponse, access_token: "new.token" }; + // mock token cannot be parsed as JwtToken + spyOn(window, "atob").and.returnValue('{"decoded": "token"}'); + mockHttpClient.post.and.returnValue(of(newToken)); + // should refresh token one minute before it expires + tick(60 * 1000); + + expect(mockHttpClient.post).toHaveBeenCalled(); + expect(service.accessToken).toBe("new.token"); + + // clear timeouts + service.logout(); + })); + testSessionServiceImplementation(() => Promise.resolve(service)); }); diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts index 75154e2f26..6c53ba2e75 100644 --- a/src/app/core/session/session-service/remote-session.ts +++ b/src/app/core/session/session-service/remote-session.ts @@ -41,12 +41,13 @@ export class RemoteSession extends SessionService { static readonly LAST_LOGIN_KEY = "LAST_REMOTE_LOGIN"; static readonly REFRESH_TOKEN_KEY = "REFRESH_TOKEN"; // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 - readonly UNAUTHORIZED_STATUS_CODE = 401; + static readonly UNAUTHORIZED_STATUS_CODE = 401; /** remote (!) PouchDB */ private readonly database: PouchDatabase; private currentDBUser: DatabaseUser; public accessToken: string; + private refreshTokenTimeout; /** * Create a RemoteSession and set up connection to the remote CouchDB server configured in AppConfig. @@ -66,17 +67,8 @@ export class RemoteSession extends SessionService { */ public async login(username: string, password: string): Promise { try { - let response = await this.getUserToken(username, password); - localStorage.setItem( - RemoteSession.REFRESH_TOKEN_KEY, - response.refresh_token - ); - this.accessToken = response.access_token; - const parsedToken = parseJwt(response.access_token); - await this.handleSuccessfulLogin({ - name: parsedToken.sub, - roles: parsedToken["_couchdb.roles"], - }); + let user = await this.verifyCredentials(username, password); + await this.handleSuccessfulLogin(user); localStorage.setItem( RemoteSession.LAST_LOGIN_KEY, new Date().toISOString() @@ -84,7 +76,7 @@ export class RemoteSession extends SessionService { this.loginState.next(LoginState.LOGGED_IN); } catch (error) { const httpError = error as HttpErrorResponse; - if (httpError?.status === this.UNAUTHORIZED_STATUS_CODE) { + if (httpError?.status === RemoteSession.UNAUTHORIZED_STATUS_CODE) { this.loginState.next(LoginState.LOGIN_FAILED); } else { this.loginState.next(LoginState.UNAVAILABLE); @@ -93,11 +85,37 @@ export class RemoteSession extends SessionService { return this.loginState.value; } - private getUserToken(username: string, password: string): Promise { + private async verifyCredentials( + username: string, + password: string + ): Promise { + const { body, options } = this.buildAuthRequest(username, password); + const response = await firstValueFrom( + this.httpClient.post(`/auth`, body.toString(), options) + ); + this.accessToken = response.access_token; + this.refreshTokenBeforeExpiry(response.expires_in, response.refresh_token); + localStorage.setItem( + RemoteSession.REFRESH_TOKEN_KEY, + response.refresh_token + ); + const parsedToken = parseJwt(this.accessToken); + return { + name: parsedToken.sub, + roles: parsedToken["_couchdb.roles"], + }; + } + + private buildAuthRequest(username: string, password: string) { const body = new URLSearchParams(); - body.set("username", username); - body.set("password", password); - body.set("grant_type", "password"); + if (username) { + body.set("username", username); + body.set("password", password); + body.set("grant_type", "password"); + } else { + body.set("refresh_token", password); + body.set("grant_type", "refresh_token"); + } body.set("client_id", "myclient"); const options = { headers: new HttpHeaders().set( @@ -105,8 +123,18 @@ export class RemoteSession extends SessionService { "application/x-www-form-urlencoded" ), }; - return firstValueFrom( - this.httpClient.post(`/auth`, body.toString(), options) + return { body, options }; + } + + private refreshTokenBeforeExpiry( + secondsTillExpiration: number, + refreshToken: string + ) { + // Refresh token one minute before it expires or after ten seconds + const refreshTimeout = Math.max(10, secondsTillExpiration - 60); + this.refreshTokenTimeout = setTimeout( + () => this.verifyCredentials("", refreshToken), + refreshTimeout * 1000 ); } @@ -128,14 +156,9 @@ export class RemoteSession extends SessionService { /** * Logout at the remote database. */ - public async logout(): Promise { + public logout(): void { + clearTimeout(this.refreshTokenTimeout); window.localStorage.removeItem(RemoteSession.REFRESH_TOKEN_KEY); - await this.httpClient - .delete(`${AppConfig.settings.database.remote_url}_session`, { - withCredentials: true, - }) - .toPromise() - .catch(() => undefined); this.currentDBUser = undefined; this.loginState.next(LoginState.LOGGED_OUT); } @@ -157,4 +180,5 @@ export class RemoteSession extends SessionService { export interface JwtToken { access_token: string; refresh_token: string; + expires_in: number; } diff --git a/src/app/core/session/session-service/session.service.spec.ts b/src/app/core/session/session-service/session.service.spec.ts index ee52ff8f77..12adf3ef24 100644 --- a/src/app/core/session/session-service/session.service.spec.ts +++ b/src/app/core/session/session-service/session.service.spec.ts @@ -82,7 +82,7 @@ export function testSessionServiceImplementation( const loginResult = await sessionService.login(TEST_USER, TEST_PASSWORD); expect(loginResult).toEqual(LoginState.LOGGED_IN); - await sessionService.logout(); + sessionService.logout(); expectNotToBeLoggedIn(LoginState.LOGGED_OUT); }); diff --git a/src/app/core/session/session-service/synced-session.service.spec.ts b/src/app/core/session/session-service/synced-session.service.spec.ts index 6a21796a46..94091c1958 100644 --- a/src/app/core/session/session-service/synced-session.service.spec.ts +++ b/src/app/core/session/session-service/synced-session.service.spec.ts @@ -32,6 +32,7 @@ import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testi import { PouchDatabase } from "../../database/pouch-database"; import { SessionModule } from "../session.module"; import { LOCATION_TOKEN } from "../../../utils/di-tokens"; +import { remoteSessionHttpFake } from "./remote-session.spec"; describe("SyncedSessionService", () => { let sessionService: SyncedSessionService; @@ -79,17 +80,7 @@ describe("SyncedSessionService", () => { // Setting up local and remote session to accept TEST_USER and TEST_PASSWORD as valid credentials dbUser = { name: TEST_USER, roles: ["user_app"] }; localSession.saveUser({ name: TEST_USER, roles: [] }, TEST_PASSWORD); - mockHttpClient.post.and.callFake((url, body) => { - if (body.name === TEST_USER && body.password === TEST_PASSWORD) { - return of(dbUser as any); - } else { - return throwError( - new HttpErrorResponse({ - status: remoteSession.UNAUTHORIZED_STATUS_CODE, - }) - ); - } - }); + mockHttpClient.post.and.callFake(remoteSessionHttpFake); localLoginSpy = spyOn(localSession, "login").and.callThrough(); remoteLoginSpy = spyOn(remoteSession, "login").and.callThrough(); @@ -196,6 +187,9 @@ describe("SyncedSessionService", () => { expect(syncSpy).toHaveBeenCalled(); expect(liveSyncSpy).toHaveBeenCalled(); expectAsync(login).toBeResolvedTo(LoginState.LOGGED_IN); + + // clear timeouts and intervals + sessionService.logout(); flush(); })); @@ -295,14 +289,17 @@ describe("SyncedSessionService", () => { testSessionServiceImplementation(() => Promise.resolve(sessionService)); function passRemoteLogin(response: DatabaseUser = { name: "", roles: [] }) { - mockHttpClient.post.and.returnValue(of(response)); + remoteLoginSpy.and.callFake(() => { + remoteSession.handleSuccessfulLogin(response); + return Promise.resolve(LoginState.LOGGED_IN); + }); } function failRemoteLogin(offline = false) { let rejectError; if (!offline) { rejectError = new HttpErrorResponse({ - status: remoteSession.UNAUTHORIZED_STATUS_CODE, + status: RemoteSession.UNAUTHORIZED_STATUS_CODE, }); } mockHttpClient.post.and.returnValue(throwError(rejectError)); diff --git a/src/app/core/session/session-service/synced-session.service.ts b/src/app/core/session/session-service/synced-session.service.ts index 2ea29ca7dd..3465f39518 100644 --- a/src/app/core/session/session-service/synced-session.service.ts +++ b/src/app/core/session/session-service/synced-session.service.ts @@ -301,11 +301,11 @@ export class SyncedSessionService extends SessionService { * Logout and stop any existing sync. * also see {@link SessionService} */ - public async logout() { + public logout() { this.cancelLoginOfflineRetry(); this.cancelLiveSync(); this.localSession.logout(); - await this.remoteSession.logout(); + this.remoteSession.logout(); this.location.reload(); this.loginState.next(LoginState.LOGGED_OUT); } diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index 782fb4e5be..154ef8d30a 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -98,7 +98,7 @@ export function compareEnums( return a?.id === b?.id; } -export function parseJwt(token) { +export function parseJwt(token): { sub: string } { const base64Url = token.split(".")[1]; const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const jsonPayload = decodeURIComponent( From 82a14455c47ad59ff54a245c031066921f53ca08 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 18 Jul 2022 23:19:21 +0200 Subject: [PATCH 06/42] cleaned up remote session --- .../session/session-service/remote-session.ts | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts index 6c53ba2e75..f1ec3df8f0 100644 --- a/src/app/core/session/session-service/remote-session.ts +++ b/src/app/core/session/session-service/remote-session.ts @@ -67,7 +67,8 @@ export class RemoteSession extends SessionService { */ public async login(username: string, password: string): Promise { try { - let user = await this.verifyCredentials(username, password); + const token = await this.authenticate(username, password); + const user = await this.processToken(token); await this.handleSuccessfulLogin(user); localStorage.setItem( RemoteSession.LAST_LOGIN_KEY, @@ -85,37 +86,22 @@ export class RemoteSession extends SessionService { return this.loginState.value; } - private async verifyCredentials( - username: string, - password: string - ): Promise { - const { body, options } = this.buildAuthRequest(username, password); - const response = await firstValueFrom( - this.httpClient.post(`/auth`, body.toString(), options) - ); - this.accessToken = response.access_token; - this.refreshTokenBeforeExpiry(response.expires_in, response.refresh_token); - localStorage.setItem( - RemoteSession.REFRESH_TOKEN_KEY, - response.refresh_token - ); - const parsedToken = parseJwt(this.accessToken); - return { - name: parsedToken.sub, - roles: parsedToken["_couchdb.roles"], - }; + private authenticate(username: string, password: string): Promise { + const body = new URLSearchParams(); + body.set("username", username); + body.set("password", password); + body.set("grant_type", "password"); + return this.getToken(body); } - private buildAuthRequest(username: string, password: string) { + private refreshToken(token: string): Promise { const body = new URLSearchParams(); - if (username) { - body.set("username", username); - body.set("password", password); - body.set("grant_type", "password"); - } else { - body.set("refresh_token", password); - body.set("grant_type", "refresh_token"); - } + body.set("refresh_token", token); + body.set("grant_type", "refresh_token"); + return this.getToken(body); + } + + private getToken(body: URLSearchParams): Promise { body.set("client_id", "myclient"); const options = { headers: new HttpHeaders().set( @@ -123,7 +109,20 @@ export class RemoteSession extends SessionService { "application/x-www-form-urlencoded" ), }; - return { body, options }; + return firstValueFrom( + this.httpClient.post(`/auth`, body.toString(), options) + ); + } + + private async processToken(token: JwtToken): Promise { + this.accessToken = token.access_token; + this.refreshTokenBeforeExpiry(token.expires_in, token.refresh_token); + localStorage.setItem(RemoteSession.REFRESH_TOKEN_KEY, token.refresh_token); + const parsedToken = parseJwt(this.accessToken); + return { + name: parsedToken.sub, + roles: parsedToken["_couchdb.roles"], + }; } private refreshTokenBeforeExpiry( @@ -132,10 +131,10 @@ export class RemoteSession extends SessionService { ) { // Refresh token one minute before it expires or after ten seconds const refreshTimeout = Math.max(10, secondsTillExpiration - 60); - this.refreshTokenTimeout = setTimeout( - () => this.verifyCredentials("", refreshToken), - refreshTimeout * 1000 - ); + this.refreshTokenTimeout = setTimeout(async () => { + const token = await this.refreshToken(refreshToken); + await this.processToken(token); + }, refreshTimeout * 1000); } public async handleSuccessfulLogin(userObject: DatabaseUser) { From 6a2f3a07828e434efbab1e187c4d6c7ffc041688 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 18 Jul 2022 23:39:52 +0200 Subject: [PATCH 07/42] added automatic refresh of token when opening app --- .../session-service/remote-session.spec.ts | 2 +- .../session/session-service/remote-session.ts | 14 +++---- .../synced-session.service.spec.ts | 41 ++++++------------- .../session-service/synced-session.service.ts | 17 +++----- 4 files changed, 25 insertions(+), 49 deletions(-) diff --git a/src/app/core/session/session-service/remote-session.spec.ts b/src/app/core/session/session-service/remote-session.spec.ts index c4b3191705..9a79fef08e 100644 --- a/src/app/core/session/session-service/remote-session.spec.ts +++ b/src/app/core/session/session-service/remote-session.spec.ts @@ -42,7 +42,7 @@ export function remoteSessionHttpFake(url, body) { * } * ``` */ -const jwtTokenResponse: JwtToken = { +export const jwtTokenResponse: JwtToken = { access_token: "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzV2FQaEdOYy1GSWpTdHBYVk96Y29oMjdSbVBpYVRwZjRlWUItUHpwU1VVIn0.eyJleHAiOjE2NTgxMzk0NTUsImlhdCI6MTY1ODEzOTM5NSwianRpIjoiY2YwZTYzNTMtNTJiMy00NzgyLTg1YjAtOWNkMzQ4MTM4YmI4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9teXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6InRlc3QiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJteWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI2ZWE5YzRkYi0wOGZmLTQ4MjQtOTUzMS1lNTIzODg1Y2E2NTIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHBzOi8vd3d3LmtleWNsb2FrLm9yZyJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1teXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im15Y2xpZW50Ijp7InJvbGVzIjpbInVzZXJfYXBwIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI2ZWE5YzRkYi0wOGZmLTQ4MjQtOTUzMS1lNTIzODg1Y2E2NTIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIl9jb3VjaGRiLnJvbGVzIjpbInVzZXJfYXBwIl0sInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QifQ.eynm8Zoox4Ovad0K4fYkia4mIyJUJpSfxEM0ZivQAD4LzhzDXuixsEJLQ3RFHI421k2q4wOMaorQAhjVbhmuu9CVQbPjTvNWfeO5DfTdo103KzWmQirWgBHP47H7dwF_2ksyag5HWFMTMCoD9BrNNPJPGjgkabXEFz4-dXg0GaslWd5MIO21cYqkZc_8YhFFLj9oP5gaY-9f3WTQg5YWTg7wk2YKM8IFlKrO67DMLTuO361lSdltnPNIBGzzAEC_oCM3GNFB1d8OkUnA5No89DhxCqQGHeQTKiYiJKoCdnZNQr7pIY0Ml-uNtAEHKAMV9Q6_sxqNcIFjROirdv3kkA", refresh_token: "test-refresh-token", diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts index f1ec3df8f0..6669555ef2 100644 --- a/src/app/core/session/session-service/remote-session.ts +++ b/src/app/core/session/session-service/remote-session.ts @@ -94,8 +94,9 @@ export class RemoteSession extends SessionService { return this.getToken(body); } - private refreshToken(token: string): Promise { + public refreshToken(): Promise { const body = new URLSearchParams(); + const token = localStorage.getItem(RemoteSession.REFRESH_TOKEN_KEY); body.set("refresh_token", token); body.set("grant_type", "refresh_token"); return this.getToken(body); @@ -114,10 +115,10 @@ export class RemoteSession extends SessionService { ); } - private async processToken(token: JwtToken): Promise { + public async processToken(token: JwtToken): Promise { this.accessToken = token.access_token; - this.refreshTokenBeforeExpiry(token.expires_in, token.refresh_token); localStorage.setItem(RemoteSession.REFRESH_TOKEN_KEY, token.refresh_token); + this.refreshTokenBeforeExpiry(token.expires_in); const parsedToken = parseJwt(this.accessToken); return { name: parsedToken.sub, @@ -125,14 +126,11 @@ export class RemoteSession extends SessionService { }; } - private refreshTokenBeforeExpiry( - secondsTillExpiration: number, - refreshToken: string - ) { + private refreshTokenBeforeExpiry(secondsTillExpiration: number) { // Refresh token one minute before it expires or after ten seconds const refreshTimeout = Math.max(10, secondsTillExpiration - 60); this.refreshTokenTimeout = setTimeout(async () => { - const token = await this.refreshToken(refreshToken); + const token = await this.refreshToken(); await this.processToken(token); }, refreshTimeout * 1000); } diff --git a/src/app/core/session/session-service/synced-session.service.spec.ts b/src/app/core/session/session-service/synced-session.service.spec.ts index 94091c1958..9f129679a6 100644 --- a/src/app/core/session/session-service/synced-session.service.spec.ts +++ b/src/app/core/session/session-service/synced-session.service.spec.ts @@ -32,7 +32,7 @@ import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testi import { PouchDatabase } from "../../database/pouch-database"; import { SessionModule } from "../session.module"; import { LOCATION_TOKEN } from "../../../utils/di-tokens"; -import { remoteSessionHttpFake } from "./remote-session.spec"; +import { jwtTokenResponse, remoteSessionHttpFake } from "./remote-session.spec"; describe("SyncedSessionService", () => { let sessionService: SyncedSessionService; @@ -51,9 +51,10 @@ describe("SyncedSessionService", () => { let mockLocation: jasmine.SpyObj; beforeEach(() => { - mockHttpClient = jasmine.createSpyObj(["post", "delete", "get"]); - mockHttpClient.delete.and.returnValue(of()); - mockHttpClient.get.and.returnValue(of()); + mockHttpClient = jasmine.createSpyObj(["post"]); + mockHttpClient.post.and.returnValue( + throwError(() => new HttpErrorResponse({})) + ); mockLocation = jasmine.createSpyObj(["reload"]); TestBed.configureTestingModule({ imports: [SessionModule, NoopAnimationsModule, FontAwesomeTestingModule], @@ -251,36 +252,20 @@ describe("SyncedSessionService", () => { tick(); })); - it("should login, given that CouchDB cookie is still valid", fakeAsync(() => { - const responseObject = { - ok: true, - userCtx: { - name: "demo", - roles: ["user_app"], - }, - info: { - authentication_handlers: ["cookie", "default"], - authenticated: "default", - }, - }; - mockHttpClient.get.and.returnValue(of(responseObject)); + it("should login, if there is a valid refresh token", fakeAsync(() => { + localStorage.setItem(RemoteSession.REFRESH_TOKEN_KEY, "some-refresh-token"); + mockHttpClient.post.and.returnValue(of(jwtTokenResponse)); sessionService.checkForValidSession(); tick(); expect(sessionService.loginState.value).toEqual(LoginState.LOGGED_IN); + localStorage.removeItem(RemoteSession.REFRESH_TOKEN_KEY); + sessionService.logout(); })); it("should not login, given that there is no valid CouchDB cookie", fakeAsync(() => { - const responseObject = { - ok: true, - userCtx: { - name: null, - roles: [], - }, - info: { - authentication_handlers: ["cookie", "default"], - }, - }; - mockHttpClient.get.and.returnValue(of(responseObject)); + mockHttpClient.post.and.returnValue( + throwError(() => new HttpErrorResponse({})) + ); sessionService.checkForValidSession(); tick(); expect(sessionService.loginState.value).toEqual(LoginState.LOGGED_OUT); diff --git a/src/app/core/session/session-service/synced-session.service.ts b/src/app/core/session/session-service/synced-session.service.ts index 3465f39518..b59ee2094a 100644 --- a/src/app/core/session/session-service/synced-session.service.ts +++ b/src/app/core/session/session-service/synced-session.service.ts @@ -29,7 +29,6 @@ import { HttpClient } from "@angular/common/http"; import { DatabaseUser } from "./local-user"; import { waitForChangeTo } from "../session-states/session-utils"; import { zip } from "rxjs"; -import { AppConfig } from "app/core/app-config/app-config"; import { filter } from "rxjs/operators"; import { LOCATION_TOKEN } from "../../../utils/di-tokens"; @@ -74,17 +73,11 @@ export class SyncedSessionService extends SessionService { * Do login automatically if there is still a valid CouchDB cookie from last login with username and password */ checkForValidSession() { - const token = window.localStorage.getItem("token"); - this.httpClient - .get(`${AppConfig.settings.database.remote_url}_session`, { - withCredentials: true, - headers: { Authorization: "Bearer " + token }, - }) - .subscribe((res: any) => { - if (res.userCtx.name) { - this.handleSuccessfulLogin(res.userCtx); - } - }); + this.remoteSession + .refreshToken() + .then((token) => this.remoteSession.processToken(token)) + .then((user) => this.handleSuccessfulLogin(user)) + .catch((err) => {}); } async handleSuccessfulLogin(userObject: DatabaseUser) { From a8e3c9dc674eb6c3ce4dd94819ceca4ef9a9bde1 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 27 Jul 2022 09:53:56 +0200 Subject: [PATCH 08/42] added keycloak package --- package-lock.json | 29 ++++++++++++++++ package.json | 1 + proxy.conf.json | 15 ++++++--- .../session/session-service/remote-session.ts | 12 +++++-- .../user-account/user-account.component.html | 11 ++++--- .../user-account/user-account.component.ts | 33 ++++++++++++++++++- src/app/utils/utils.ts | 7 +++- 7 files changed, 93 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index d59d2be0ac..9d435905b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "flag-icons": "^6.6.2", "hammerjs": "^2.0.8", "json-query": "^2.2.2", + "keycloak-js": "^18.0.1", "lodash-es": "^4.17.21", "md5": "^2.3.0", "moment": "^2.29.4", @@ -25371,6 +25372,11 @@ "node": ">=8" } }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "node_modules/js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -25792,6 +25798,15 @@ "node": ">= 12" } }, + "node_modules/keycloak-js": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-18.0.1.tgz", + "integrity": "sha512-IRXToYpbIrkyfLeNNJly2OjUCf11ncx2Sdsg257NVDwjOYE923osu47w8pfxEVWpTaS14/Y2QjbTHciuBK0RBQ==", + "dependencies": { + "base64-js": "^1.5.1", + "js-sha256": "^0.9.0" + } + }, "node_modules/khroma": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", @@ -57253,6 +57268,11 @@ } } }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -57588,6 +57608,15 @@ } } }, + "keycloak-js": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-18.0.1.tgz", + "integrity": "sha512-IRXToYpbIrkyfLeNNJly2OjUCf11ncx2Sdsg257NVDwjOYE923osu47w8pfxEVWpTaS14/Y2QjbTHciuBK0RBQ==", + "requires": { + "base64-js": "^1.5.1", + "js-sha256": "^0.9.0" + } + }, "khroma": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", diff --git a/package.json b/package.json index cf0a458208..c65c5a8265 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "flag-icons": "^6.6.2", "hammerjs": "^2.0.8", "json-query": "^2.2.2", + "keycloak-js": "^18.0.1", "lodash-es": "^4.17.21", "md5": "^2.3.0", "moment": "^2.29.4", diff --git a/proxy.conf.json b/proxy.conf.json index 380a64bb44..1c4d1c6f1f 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -1,6 +1,6 @@ { "/db": { - "target": "http://localhost:5984", + "target": "https://keycloak-test.aam-digital.com/db", "secure": true, "logLevel": "debug", "changeOrigin": true, @@ -18,12 +18,17 @@ } }, "/auth": { - "target": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token", - "secure": true, + "target": "http://localhost:8080", + "secure": false, "logLevel": "debug", - "changeOrigin": true, + "changeOrigin": false, + "withCredentials": true, + "xfwd": true, "pathRewrite": { "^/auth": "" - } + }, + "cookiePathRewrite": "/", + "hostRewrite": "/auth", + "followRedirects": true } } diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts index 6669555ef2..1584209ff8 100644 --- a/src/app/core/session/session-service/remote-session.ts +++ b/src/app/core/session/session-service/remote-session.ts @@ -103,7 +103,7 @@ export class RemoteSession extends SessionService { } private getToken(body: URLSearchParams): Promise { - body.set("client_id", "myclient"); + body.set("client_id", "app"); const options = { headers: new HttpHeaders().set( "Content-Type", @@ -111,7 +111,11 @@ export class RemoteSession extends SessionService { ), }; return firstValueFrom( - this.httpClient.post(`/auth`, body.toString(), options) + this.httpClient.post( + `/auth/realms/local/protocol/openid-connect/token`, + body.toString(), + options + ) ); } @@ -120,8 +124,9 @@ export class RemoteSession extends SessionService { localStorage.setItem(RemoteSession.REFRESH_TOKEN_KEY, token.refresh_token); this.refreshTokenBeforeExpiry(token.expires_in); const parsedToken = parseJwt(this.accessToken); + document.cookie = `KEYCLOAK_SESSION=keycloak-test/${parsedToken.sub}/${parsedToken.sid}`; return { - name: parsedToken.sub, + name: parsedToken.username, roles: parsedToken["_couchdb.roles"], }; } @@ -178,4 +183,5 @@ export interface JwtToken { access_token: string; refresh_token: string; expires_in: number; + session_state: string; } diff --git a/src/app/core/user/user-account/user-account.component.html b/src/app/core/user/user-account/user-account.component.html index 44a3a26009..8cda866ae6 100644 --- a/src/app/core/user/user-account/user-account.component.html +++ b/src/app/core/user/user-account/user-account.component.html @@ -18,6 +18,7 @@ + diff --git a/src/app/core/user/user-account/user-account.component.ts b/src/app/core/user/user-account/user-account.component.ts index ab45624b45..90fdbe4166 100644 --- a/src/app/core/user/user-account/user-account.component.ts +++ b/src/app/core/user/user-account/user-account.component.ts @@ -20,7 +20,6 @@ import { SessionService } from "../../session/session-service/session.service"; import { environment } from "../../../../environments/environment"; import { SessionType } from "../../session/session-type"; import { AuthService } from "../../session/auth/auth.service"; -import { CouchdbAuthService } from "../../session/auth/couchdb-auth.service"; /** * User account form to allow the user to view and edit information. @@ -36,16 +35,11 @@ export class UserAccountComponent implements OnInit { passwordChangeDisabled = false; tooltipText; - couchdbAuthService: CouchdbAuthService; constructor( private sessionService: SessionService, public authService: AuthService - ) { - if (this.authService instanceof CouchdbAuthService) { - this.couchdbAuthService = this.authService; - } - } + ) {} ngOnInit() { this.checkIfPasswordChangeAllowed(); diff --git a/src/app/core/user/user.module.ts b/src/app/core/user/user.module.ts index f83db20ae0..58cb619671 100644 --- a/src/app/core/user/user.module.ts +++ b/src/app/core/user/user.module.ts @@ -25,9 +25,9 @@ import { MatTabsModule } from "@angular/material/tabs"; import { TabStateModule } from "../../utils/tab-state/tab-state.module"; import { MatTooltipModule } from "@angular/material/tooltip"; import { Angulartics2Module } from "angulartics2"; -import { PasswordFormComponent } from "./password-form/password-form.component"; import { ReactiveFormsModule } from "@angular/forms"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { SessionModule } from "../session/session.module"; /** * Provides a User functionality including user account forms. @@ -43,9 +43,9 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; MatTooltipModule, Angulartics2Module, ReactiveFormsModule, - FontAwesomeModule + FontAwesomeModule, + SessionModule, ], - declarations: [UserAccountComponent, PasswordFormComponent], + declarations: [UserAccountComponent], }) -export class UserModule { -} +export class UserModule {} From ef67e1bacbf9c1275f36deb22a0a4546a60d09ed Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 26 Aug 2022 11:38:07 +0200 Subject: [PATCH 39/42] fixed password form component --- src/app/core/session/auth/auth.service.ts | 5 ----- .../couchdb/password-form/password-form.component.html | 10 ++++++++-- .../couchdb/password-form/password-form.component.ts | 10 +++------- .../core/user/user-account/user-account.component.html | 5 +++-- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/app/core/session/auth/auth.service.ts b/src/app/core/session/auth/auth.service.ts index 05457c2631..0167879840 100644 --- a/src/app/core/session/auth/auth.service.ts +++ b/src/app/core/session/auth/auth.service.ts @@ -34,9 +34,4 @@ export abstract class AuthService { * Clear the local session of the currently logged-in user. */ abstract logout(): Promise; - - /** - * Change the password of a user. - */ - abstract changePassword(): Promise; } diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.html b/src/app/core/session/auth/couchdb/password-form/password-form.component.html index d08d0588c5..02b8a40904 100644 --- a/src/app/core/session/auth/couchdb/password-form/password-form.component.html +++ b/src/app/core/session/auth/couchdb/password-form/password-form.component.html @@ -26,6 +26,12 @@ +
+ Please enter a new password. +
- Confirmation does not match your new password. + Passwords don't match.
@@ -78,7 +84,7 @@
From d486adc371e25683d13d4860d0eff1fc140d1520 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 26 Aug 2022 12:22:33 +0200 Subject: [PATCH 40/42] cleaned up code --- .../auth/couchdb/password-form/password-form.component.scss | 0 .../auth/couchdb/password-form/password-form.component.ts | 3 +-- .../keycloak/password-button/password-button.component.scss | 0 .../auth/keycloak/password-button/password-button.component.ts | 3 +-- 4 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 src/app/core/session/auth/couchdb/password-form/password-form.component.scss delete mode 100644 src/app/core/session/auth/keycloak/password-button/password-button.component.scss diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.scss b/src/app/core/session/auth/couchdb/password-form/password-form.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.ts b/src/app/core/session/auth/couchdb/password-form/password-form.component.ts index 83b7f41382..ad09cf5d19 100644 --- a/src/app/core/session/auth/couchdb/password-form/password-form.component.ts +++ b/src/app/core/session/auth/couchdb/password-form/password-form.component.ts @@ -11,7 +11,6 @@ import { CouchdbAuthService } from "../couchdb-auth.service"; @Component({ selector: "app-password-form", templateUrl: "./password-form.component.html", - styleUrls: ["./password-form.component.scss"], }) export class PasswordFormComponent implements OnInit { @Input() username: string; @@ -31,7 +30,7 @@ export class PasswordFormComponent implements OnInit { Validators.minLength(8), Validators.pattern(/[A-Z]/), Validators.pattern(/[a-z]/), - Validators.pattern(/[0-9]/), + Validators.pattern(/\d/), Validators.pattern(/[^A-Za-z0-9]/), ], ], diff --git a/src/app/core/session/auth/keycloak/password-button/password-button.component.scss b/src/app/core/session/auth/keycloak/password-button/password-button.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/core/session/auth/keycloak/password-button/password-button.component.ts b/src/app/core/session/auth/keycloak/password-button/password-button.component.ts index f567fb85d7..2984fd1a7a 100644 --- a/src/app/core/session/auth/keycloak/password-button/password-button.component.ts +++ b/src/app/core/session/auth/keycloak/password-button/password-button.component.ts @@ -1,11 +1,10 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { AuthService } from "../../auth.service"; import { KeycloakAuthService } from "../keycloak-auth.service"; @Component({ selector: "app-password-button", templateUrl: "./password-button.component.html", - styleUrls: ["./password-button.component.scss"], }) export class PasswordButtonComponent { @Input() disabled: boolean; From 86d92f0cad9d253d14ce5494633e12b9362a44d5 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 26 Aug 2022 12:37:53 +0200 Subject: [PATCH 41/42] fixed broken tests --- .../password-form.component.spec.ts | 16 ++++++++++++---- .../password-form/password-form.component.ts | 9 +++++---- .../password-button.component.spec.ts | 17 ++++++++++------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts b/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts index 02b36e27da..48701890b1 100644 --- a/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts +++ b/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts @@ -29,6 +29,7 @@ describe("PasswordFormComponent", () => { fixture = TestBed.createComponent(PasswordFormComponent); component = fixture.componentInstance; component.couchdbAuthService = mockCouchDBAuth; + component.username = "testUser"; fixture.detectChanges(); }); @@ -36,6 +37,12 @@ describe("PasswordFormComponent", () => { expect(component).toBeTruthy(); }); + it("should disable the form when disabled is passed to component", () => { + component.disabled = true; + component.ngOnInit(); + expect(component.passwordForm.disabled).toBeTrue(); + }); + it("should set error when password is incorrect", () => { component.passwordForm.get("currentPassword").setValue("wrongPW"); mockSessionService.checkPassword.and.returnValue(false); @@ -48,8 +55,9 @@ describe("PasswordFormComponent", () => { }); it("should set error when password change fails", fakeAsync(() => { - component.username = "testUser"; component.passwordForm.get("currentPassword").setValue("testPW"); + component.passwordForm.get("newPassword").setValue("Password1-"); + component.passwordForm.get("confirmPassword").setValue("Password1-"); mockSessionService.checkPassword.and.returnValue(true); mockCouchDBAuth.changePassword.and.rejectWith(new Error("pw change error")); @@ -62,9 +70,9 @@ describe("PasswordFormComponent", () => { })); it("should set success and re-login when password change worked", fakeAsync(() => { - component.username = "testUser"; component.passwordForm.get("currentPassword").setValue("testPW"); - component.passwordForm.get("newPassword").setValue("changedPassword"); + component.passwordForm.get("newPassword").setValue("Password1-"); + component.passwordForm.get("confirmPassword").setValue("Password1-"); mockSessionService.checkPassword.and.returnValue(true); mockCouchDBAuth.changePassword.and.resolveTo(); mockSessionService.login.and.resolveTo(null); @@ -74,7 +82,7 @@ describe("PasswordFormComponent", () => { expect(component.passwordChangeResult.success).toBeTrue(); expect(mockSessionService.login).toHaveBeenCalledWith( "testUser", - "changedPassword" + "Password1-" ); })); }); diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.ts b/src/app/core/session/auth/couchdb/password-form/password-form.component.ts index ad09cf5d19..aa5cbc749f 100644 --- a/src/app/core/session/auth/couchdb/password-form/password-form.component.ts +++ b/src/app/core/session/auth/couchdb/password-form/password-form.component.ts @@ -57,9 +57,6 @@ export class PasswordFormComponent implements OnInit { } changePassword(): Promise { - if (this.passwordForm.invalid) { - return; - } this.passwordChangeResult = undefined; const currentPassword = this.passwordForm.get("currentPassword").value; @@ -68,7 +65,11 @@ export class PasswordFormComponent implements OnInit { this.passwordForm .get("currentPassword") .setErrors({ incorrectPassword: true }); - return; + return Promise.reject(); + } + + if (this.passwordForm.invalid) { + return Promise.reject(); } const newPassword = this.passwordForm.get("newPassword").value; diff --git a/src/app/core/session/auth/keycloak/password-button/password-button.component.spec.ts b/src/app/core/session/auth/keycloak/password-button/password-button.component.spec.ts index 1f43fa5377..6a6d9972f2 100644 --- a/src/app/core/session/auth/keycloak/password-button/password-button.component.spec.ts +++ b/src/app/core/session/auth/keycloak/password-button/password-button.component.spec.ts @@ -1,23 +1,26 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { PasswordButtonComponent } from './password-button.component'; +import { PasswordButtonComponent } from "./password-button.component"; +import { AuthService } from "../../auth.service"; -describe('PasswordButtonComponent', () => { +describe("PasswordButtonComponent", () => { let component: PasswordButtonComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ PasswordButtonComponent ] - }) - .compileComponents(); + declarations: [PasswordButtonComponent], + providers: [ + { provide: AuthService, useValue: { changePassword: () => undefined } }, + ], + }).compileComponents(); fixture = TestBed.createComponent(PasswordButtonComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + it("should create", () => { expect(component).toBeTruthy(); }); }); From 6df220f3de62b7748d24ccd1af07d89286481aec Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 26 Aug 2022 15:55:54 +0200 Subject: [PATCH 42/42] removed promise rejection --- .../auth/couchdb/password-form/password-form.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.ts b/src/app/core/session/auth/couchdb/password-form/password-form.component.ts index aa5cbc749f..59f3ee7912 100644 --- a/src/app/core/session/auth/couchdb/password-form/password-form.component.ts +++ b/src/app/core/session/auth/couchdb/password-form/password-form.component.ts @@ -65,11 +65,11 @@ export class PasswordFormComponent implements OnInit { this.passwordForm .get("currentPassword") .setErrors({ incorrectPassword: true }); - return Promise.reject(); + return; } if (this.passwordForm.invalid) { - return Promise.reject(); + return; } const newPassword = this.passwordForm.get("newPassword").value;