-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
29 changed files
with
1,106 additions
and
562 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** | ||
* Available authentication providers. | ||
*/ | ||
export enum AuthProvider { | ||
/** | ||
* Default auth provider using CouchDB's `_users` database and permission settings. | ||
* This is the simplest setup as no other service besides the CouchDB is required. | ||
* However, this provider comes with limited functionality. | ||
*/ | ||
CouchDB = "couchdb", | ||
|
||
/** | ||
* Keycloak is used to authenticate and manage users. | ||
* This requires keycloak and potentially other services to be running. | ||
* Also, the client configuration has to be placed in a file called `keycloak.json` in the `assets` folder. | ||
*/ | ||
Keycloak = "keycloak", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
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. | ||
* See {@link AuthProvider} for available options. | ||
*/ | ||
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>; | ||
} |
111 changes: 111 additions & 0 deletions
111
src/app/core/session/auth/couchdb/couchdb-auth.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { TestBed } from "@angular/core/testing"; | ||
|
||
import { CouchdbAuthService } from "./couchdb-auth.service"; | ||
import { | ||
HttpClient, | ||
HttpErrorResponse, | ||
HttpStatusCode, | ||
} from "@angular/common/http"; | ||
import { of, throwError } from "rxjs"; | ||
import { | ||
TEST_PASSWORD, | ||
TEST_USER, | ||
} from "../../../../utils/mocked-testing.module"; | ||
|
||
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: HttpStatusCode.Unauthorized, | ||
}) | ||
); | ||
} | ||
}); | ||
|
||
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
112
src/app/core/session/auth/couchdb/couchdb-auth.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
HttpStatusCode, | ||
} from "@angular/common/http"; | ||
import { firstValueFrom } from "rxjs"; | ||
import { DatabaseUser } from "../../session-service/local-user"; | ||
import { AppSettings } from "../../../app-config/app-settings"; | ||
|
||
@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: HttpStatusCode.Unauthorized, | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* 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, | ||
}) | ||
); | ||
} | ||
} |
Oops, something went wrong.