Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keycloak integration #1381

Merged
merged 59 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
83259f6
added proxy for local keycloak
TheSlimvReal May 23, 2022
a713dca
remote session authorizes against keycloak
TheSlimvReal May 23, 2022
a79151a
Merge remote-tracking branch 'origin/master' into keycloak
TheSlimvReal Jul 17, 2022
fc74555
updated to latest version and added support for autologin. Currently …
TheSlimvReal Jul 17, 2022
7563404
added tests for remote session login using jwt
TheSlimvReal Jul 18, 2022
4b9fd51
addded token renewal
TheSlimvReal Jul 18, 2022
82a1445
cleaned up remote session
TheSlimvReal Jul 18, 2022
6a2f3a0
added automatic refresh of token when opening app
TheSlimvReal Jul 18, 2022
85d32c1
Merge branch 'master' into keycloak
TheSlimvReal Jul 19, 2022
a8e3c9d
added keycloak package
TheSlimvReal Jul 27, 2022
7d8299d
added proxy for keycloak
TheSlimvReal Jul 29, 2022
2485c72
Merge remote-tracking branch 'origin/master' into keycloak
TheSlimvReal Jul 29, 2022
4d3fd86
excluding /auth requests from service worker
TheSlimvReal Jul 29, 2022
d448d47
fixed tests
TheSlimvReal Jul 29, 2022
e713b24
Merge branch 'master' into keycloak
TheSlimvReal Jul 29, 2022
f3faa2d
cleaned up reset password flow
TheSlimvReal Jul 30, 2022
8fe5bf1
Merge remote-tracking branch 'origin/keycloak' into keycloak
TheSlimvReal Jul 30, 2022
99e040a
trying to get reverse proxy for keycloak working
TheSlimvReal Aug 1, 2022
a095611
using keycloak without a proxy
TheSlimvReal Aug 1, 2022
1c1fc7d
fixed tests
TheSlimvReal Aug 1, 2022
d048beb
Merge remote-tracking branch 'origin/master' into keycloak
TheSlimvReal Aug 8, 2022
0ad5868
fixed url for keycloak auth
TheSlimvReal Aug 8, 2022
af49273
some general code cleanup
TheSlimvReal Aug 10, 2022
25e16cd
some more general code cleanup
TheSlimvReal Aug 10, 2022
dc576b8
fixed test
TheSlimvReal Aug 10, 2022
2929778
app doesnt fail if keycloak couldnt be initialized
TheSlimvReal Aug 10, 2022
6d9b894
Merge remote-tracking branch 'origin/master' into keycloak
TheSlimvReal Aug 10, 2022
42c45f3
not initializing keycloak with app initializer
TheSlimvReal Aug 10, 2022
a9f0dab
cleaned up ngsw config
TheSlimvReal Aug 11, 2022
74903dd
refactored keycloak login logic to own service
TheSlimvReal Aug 11, 2022
cdcd433
re-implemented couchdb auth as alternative to keycloak
TheSlimvReal Aug 11, 2022
9d9e76c
re-added tests for CouchDB auth
TheSlimvReal Aug 11, 2022
1858bf3
added documenation and cleaned up code
TheSlimvReal Aug 11, 2022
d3f4dca
fix keycloak test
TheSlimvReal Aug 11, 2022
13ebe11
Merge branch 'master' into keycloak
TheSlimvReal Aug 12, 2022
23081a7
Merge branch 'master' into keycloak
TheSlimvReal Aug 15, 2022
169cab1
Fix typo
TheSlimvReal Aug 17, 2022
221d69e
properly named class for access tokens
TheSlimvReal Aug 17, 2022
4d809b5
Merge remote-tracking branch 'origin/keycloak' into keycloak
TheSlimvReal Aug 17, 2022
5662545
Merge remote-tracking branch 'origin/master' into keycloak
TheSlimvReal Aug 17, 2022
693c44f
integrated password-form component when using CouchDBAuth
TheSlimvReal Aug 18, 2022
0e18596
cleaned up some code
TheSlimvReal Aug 18, 2022
1426107
Merge branch 'master' into keycloak
TheSlimvReal Aug 23, 2022
5ce39fd
removed reserved route
TheSlimvReal Aug 23, 2022
71196c4
Merge remote-tracking branch 'origin/keycloak' into keycloak
TheSlimvReal Aug 23, 2022
812f4f5
undone changes to ngsw config
TheSlimvReal Aug 23, 2022
f4e6c5a
trying to make authenticator configurable, DI not working
TheSlimvReal Aug 23, 2022
c61c662
fixed AuthService DI
TheSlimvReal Aug 23, 2022
55a50f1
removed unused code
TheSlimvReal Aug 25, 2022
14b05e3
made CouchDB auth default and added enum with additional documentation.
TheSlimvReal Aug 25, 2022
ee6b12c
moved password reset button to own component
TheSlimvReal Aug 25, 2022
ef67e1b
fixed password form component
TheSlimvReal Aug 26, 2022
ce60bda
Merge branch 'master' into keycloak
TheSlimvReal Aug 26, 2022
d486adc
cleaned up code
TheSlimvReal Aug 26, 2022
86d92f0
fixed broken tests
TheSlimvReal Aug 26, 2022
26cf2c9
Merge remote-tracking branch 'origin/keycloak' into keycloak
TheSlimvReal Aug 26, 2022
39562a9
Merge branch 'master' into keycloak
TheSlimvReal Aug 26, 2022
b555879
Merge branch 'master' into keycloak
TheSlimvReal Aug 26, 2022
6df220f
removed promise rejection
TheSlimvReal Aug 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ngsw-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"!/**/*__*",
"!/**/*__*/**",
"!/db/",
"!/db/**"
"!/db/**",
"!/auth/**"
]
}
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"flag-icons": "^6.6.4",
"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",
Expand Down
42 changes: 42 additions & 0 deletions src/app/core/session/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { HttpHeaders } from "@angular/common/http";
import { DatabaseUser } from "../session-service/local-user";

/**
* Abstract class that handles user authentication and password change.
* Implement this for different authentication providers.
*/
export abstract class AuthService {

/**
* Authenticate a user with credentials.
* @param username The username of the user
* @param password The password of the user
* @returns Promise that resolves with the user if the login was successful, rejects otherwise.
*/
abstract authenticate(
username: string,
password: string
): Promise<DatabaseUser>;

/**
* Authenticate a user without credentials based on a still valid session.
* @returns Promise that resolves with the user if the session is still valid, rejects otherwise.
*/
abstract autoLogin(): Promise<DatabaseUser>;

/**
* Add headers to requests send by PouchDB if required for authentication.
* @param headers the object where further headers can be added
*/
abstract addAuthHeader(headers: HttpHeaders);

/**
* Clear the local session of the currently logged-in user.
*/
abstract logout(): Promise<void>;

/**
* Change the password of a user.
*/
abstract changePassword(): Promise<any>;
}
105 changes: 105 additions & 0 deletions src/app/core/session/auth/couchdb-auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { TestBed } from "@angular/core/testing";

import { CouchdbAuthService } from "./couchdb-auth.service";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { of, throwError } from "rxjs";
import { TEST_PASSWORD, TEST_USER } from "../../../utils/mocked-testing.module";
import { RemoteSession } from "../session-service/remote-session";

describe("CouchdbAuthService", () => {
let service: CouchdbAuthService;
let mockHttpClient: jasmine.SpyObj<HttpClient>;
let dbUser = { name: TEST_USER, roles: ["user_app"] };

beforeEach(() => {
mockHttpClient = jasmine.createSpyObj(["get", "post", "put"]);
mockHttpClient.get.and.returnValue(throwError(() => new Error()));
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,
})
);
}
});

TestBed.configureTestingModule({
providers: [
CouchdbAuthService,
{ provide: HttpClient, useValue: mockHttpClient },
],
});
service = TestBed.inject(CouchdbAuthService);
});

it("should be created", () => {
expect(service).toBeTruthy();
});

it("should return the current user after successful login", async () => {
const user = await service.authenticate(TEST_USER, TEST_PASSWORD);
expect(user).toEqual(dbUser);
});

it("should login, given that CouchDB cookie is still valid", async () => {
const responseObject = {
ok: true,
userCtx: dbUser,
info: {
authentication_handlers: ["cookie", "default"],
authenticated: "default",
},
};
mockHttpClient.get.and.returnValue(of(responseObject));
const user = await service.autoLogin();

expect(user).toEqual(responseObject.userCtx);
});

it("should not login, given that there is no valid CouchDB cookie", () => {
const responseObject = {
ok: true,
userCtx: {
name: null,
roles: [],
},
info: {
authentication_handlers: ["cookie", "default"],
},
};
mockHttpClient.get.and.returnValue(of(responseObject));
return expectAsync(service.autoLogin()).toBeRejected();
});

it("should reject if current user cant be fetched", () => {
mockHttpClient.get.and.returnValue(throwError(() => new Error()));

return expectAsync(
service.changePassword("username", "wrongPW", "")
).toBeRejected();
});

it("should report error when new Password cannot be saved", async () => {
mockHttpClient.get.and.returnValues(of({}));
mockHttpClient.put.and.returnValue(throwError(() => new Error()));

await expectAsync(
service.changePassword("username", "testPW", "")
).toBeRejected();
expect(mockHttpClient.get).toHaveBeenCalled();
expect(mockHttpClient.put).toHaveBeenCalled();
});

it("should not fail if get and put requests are successful", () => {
mockHttpClient.get.and.returnValues(of({}));
mockHttpClient.put.and.returnValues(of({}));

return expectAsync(
service.changePassword("username", "testPW", "newPW")
).not.toBeRejected();
});
});
112 changes: 112 additions & 0 deletions src/app/core/session/auth/couchdb-auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Injectable } from "@angular/core";
import { AuthService } from "./auth.service";
import {
HttpClient,
HttpErrorResponse,
HttpHeaders,
} from "@angular/common/http";
import { firstValueFrom } from "rxjs";
import { DatabaseUser } from "../session-service/local-user";
import { AppSettings } from "../../app-config/app-settings";
import { RemoteSession } from "../session-service/remote-session";

@Injectable()
export class CouchdbAuthService extends AuthService {
private static readonly COUCHDB_USER_ENDPOINT = `${AppSettings.DB_PROXY_PREFIX}/_users/org.couchdb.user`;

constructor(private http: HttpClient) {
super();
}

addAuthHeader() {
// auth happens through cookie
return;
}

authenticate(username: string, password: string): Promise<DatabaseUser> {
return firstValueFrom(
this.http.post<DatabaseUser>(
`${AppSettings.DB_PROXY_PREFIX}/_session`,
{ name: username, password: password },
{ withCredentials: true }
)
);
}

autoLogin(): Promise<any> {
return firstValueFrom(
this.http.get<{ userCtx: DatabaseUser }>(
`${AppSettings.DB_PROXY_PREFIX}/_session`,
{ withCredentials: true }
)
).then((res: any) => {
if (res.userCtx.name) {
return res.userCtx;
} else {
throw new HttpErrorResponse({
status: RemoteSession.UNAUTHORIZED_STATUS_CODE,
});
}
});
}

/**
* Function to change the password of a user
* @param username The username for which the password should be changed
* @param oldPassword The current plaintext password of the user
* @param newPassword The new plaintext password of the user
* @return Promise that resolves once the password is changed in _user and the database
*/
public async changePassword(
username?: string,
oldPassword?: string,
newPassword?: string
): Promise<void> {
let userResponse;
try {
// TODO due to cookie-auth, the old password is actually not checked
userResponse = await this.getCouchDBUser(username, oldPassword);
} catch (e) {
throw new Error("Current password incorrect or server not available");
}

userResponse.password = newPassword;
try {
await this.saveNewPasswordToCouchDB(username, oldPassword, userResponse);
} catch (e) {
throw new Error(
"Could not save new password, please contact your system administrator"
);
}
}

private getCouchDBUser(username: string, password: string): Promise<any> {
const userUrl = CouchdbAuthService.COUCHDB_USER_ENDPOINT + ":" + username;
const headers: HttpHeaders = new HttpHeaders({
Authorization: "Basic " + btoa(username + ":" + password),
});
return firstValueFrom(this.http.get(userUrl, { headers: headers }));
}

private saveNewPasswordToCouchDB(
username: string,
oldPassword: string,
userObj: any
): Promise<any> {
const userUrl = CouchdbAuthService.COUCHDB_USER_ENDPOINT + ":" + username;
const headers: HttpHeaders = new HttpHeaders({
Authorization: "Basic " + btoa(username + ":" + oldPassword),
});
return firstValueFrom(
this.http.put(userUrl, userObj, { headers: headers })
);
}

logout(): Promise<any> {
return firstValueFrom(
this.http.delete(`${AppSettings.DB_PROXY_PREFIX}/_session`, {
withCredentials: true,
})
);
}
}
Loading