From be0e30d1b95a045851b815664b13fb2ca8a37ff4 Mon Sep 17 00:00:00 2001 From: DPDS93CT Date: Tue, 10 Oct 2023 08:52:45 +0200 Subject: [PATCH 1/5] created controllers, services for /api/login endpoint for user authentication on keycloak --- config/config.dev.json | 6 + config/config.test.json | 6 + package-lock.json | 97 +++++++++++++++ package.json | 2 + .../api/keycloak-exception-filter.ts | 6 + .../ui-backend/api/login.controller.spec.ts | 116 ++++++++++++++++++ .../ui-backend/api/login.controller.ts | 47 +++++++ .../api/ui-backend-exception-filter.spec.ts | 60 +++++++++ .../api/ui-backend-exception-filter.ts | 20 +++ ...-authentication-failed-exception-filter.ts | 6 + src/modules/ui-backend/api/user.params.ts | 15 +++ .../ui-backend/domain/login.service.spec.ts | 57 +++++++++ .../ui-backend/domain/login.service.ts | 32 +++++ .../domain/new-login.service.spec.ts | 86 +++++++++++++ .../ui-backend/domain/new-login.service.ts | 45 +++++++ .../ui-backend/ui-backend-api.module.ts | 12 ++ src/server/server.module.ts | 2 + src/shared/config/json.config.ts | 4 + src/shared/error/index.ts | 1 + .../error/user-authentication-failed.error.ts | 7 ++ 20 files changed, 627 insertions(+) create mode 100644 src/modules/ui-backend/api/keycloak-exception-filter.ts create mode 100644 src/modules/ui-backend/api/login.controller.spec.ts create mode 100644 src/modules/ui-backend/api/login.controller.ts create mode 100644 src/modules/ui-backend/api/ui-backend-exception-filter.spec.ts create mode 100644 src/modules/ui-backend/api/ui-backend-exception-filter.ts create mode 100644 src/modules/ui-backend/api/user-authentication-failed-exception-filter.ts create mode 100644 src/modules/ui-backend/api/user.params.ts create mode 100644 src/modules/ui-backend/domain/login.service.spec.ts create mode 100644 src/modules/ui-backend/domain/login.service.ts create mode 100644 src/modules/ui-backend/domain/new-login.service.spec.ts create mode 100644 src/modules/ui-backend/domain/new-login.service.ts create mode 100644 src/modules/ui-backend/ui-backend-api.module.ts create mode 100644 src/shared/error/user-authentication-failed.error.ts diff --git a/config/config.dev.json b/config/config.dev.json index 88b9a3ff1..f8f706ad4 100644 --- a/config/config.dev.json +++ b/config/config.dev.json @@ -13,5 +13,11 @@ "BASE_URL": "http://127.0.0.1:8080", "REALM_NAME": "master", "CLIENT_ID": "admin-cli" + }, + "SCHULPORTAL": { + "BASE_URL": "http://127.0.0.1:8080", + "REALM_NAME": "schulportal", + "CLIENT_ID": "schulportal", + "USERNAME": "dummy" } } diff --git a/config/config.test.json b/config/config.test.json index 27b978f67..09e3ecb1f 100644 --- a/config/config.test.json +++ b/config/config.test.json @@ -13,5 +13,11 @@ "BASE_URL": "http://127.0.0.1:8080", "REALM_NAME": "master", "CLIENT_ID": "admin-cli" + }, + "SCHULPORTAL": { + "BASE_URL": "http://127.0.0.1:8080", + "REALM_NAME": "schulportal", + "CLIENT_ID": "schulportal", + "USERNAME": "dummy" } } diff --git a/package-lock.json b/package-lock.json index 192e5f4b3..4487561df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,10 +27,12 @@ "axios": "^1.5.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "express": "^4.18.2", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "nest-commander": "^3.9.0", "nest-keycloak-connect": "^1.9.2", + "openid-client": "^5.6.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" }, @@ -7382,6 +7384,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.2.tgz", + "integrity": "sha512-IY73F228OXRl9ar3jJagh7Vnuhj/GzBunPiZP13K0lOl7Am9SoWW3kEzq3MCllJMTtZqHTiDXQvoRd4U95aU6A==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-sdsl": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", @@ -8149,6 +8159,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -8247,6 +8265,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -8281,6 +8307,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.0.tgz", + "integrity": "sha512-uFTkN/iqgKvSnmpVAS/T6SNThukRMBcmymTQ71Ngus1F60tdtKVap7zCrleocY+fogPtpmoxi5Q1YdrgYuTlkA==", + "dependencies": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -16481,6 +16537,11 @@ } } }, + "jose": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.2.tgz", + "integrity": "sha512-IY73F228OXRl9ar3jJagh7Vnuhj/GzBunPiZP13K0lOl7Am9SoWW3kEzq3MCllJMTtZqHTiDXQvoRd4U95aU6A==" + }, "js-sdsl": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", @@ -17046,6 +17107,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -17117,6 +17183,11 @@ "es-abstract": "^1.20.4" } }, + "oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -17142,6 +17213,32 @@ "mimic-fn": "^2.1.0" } }, + "openid-client": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.0.tgz", + "integrity": "sha512-uFTkN/iqgKvSnmpVAS/T6SNThukRMBcmymTQ71Ngus1F60tdtKVap7zCrleocY+fogPtpmoxi5Q1YdrgYuTlkA==", + "requires": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", diff --git a/package.json b/package.json index c5941e3f9..6ffc14c63 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,12 @@ "axios": "^1.5.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "express": "^4.18.2", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "nest-commander": "^3.9.0", "nest-keycloak-connect": "^1.9.2", + "openid-client": "^5.6.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" }, diff --git a/src/modules/ui-backend/api/keycloak-exception-filter.ts b/src/modules/ui-backend/api/keycloak-exception-filter.ts new file mode 100644 index 000000000..f418f2f5f --- /dev/null +++ b/src/modules/ui-backend/api/keycloak-exception-filter.ts @@ -0,0 +1,6 @@ +import { Catch } from '@nestjs/common'; +import { KeycloakClientError } from '../../../shared/error/index.js'; +import { UiBackendExceptionFilter } from './ui-backend-exception-filter.js'; + +@Catch(KeycloakClientError) +export class KeyCloakExceptionFilter extends UiBackendExceptionFilter {} diff --git a/src/modules/ui-backend/api/login.controller.spec.ts b/src/modules/ui-backend/api/login.controller.spec.ts new file mode 100644 index 000000000..1a3fe3397 --- /dev/null +++ b/src/modules/ui-backend/api/login.controller.spec.ts @@ -0,0 +1,116 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LoginController } from './login.controller.js'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { LoginService } from '../domain/login.service.js'; +import { faker } from '@faker-js/faker'; +import { TokenSet } from 'openid-client'; +import { UserParams } from './user.params.js'; +import { KeycloakClientError, UserAuthenticationFailedError } from '../../../shared/error/index.js'; +import { NewLoginService } from '../domain/new-login.service.js'; + +describe('LoginController', () => { + let module: TestingModule; + let loginController: LoginController; + let loginServiceMock: DeepMocked; + let someServiceMock: DeepMocked; + let tokenSet: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [], + providers: [ + LoginController, + { + provide: LoginService, + useValue: createMock(), + }, + { + provide: NewLoginService, + useValue: createMock(), + }, + { + provide: TokenSet, + useValue: createMock(), + }, + ], + }).compile(); + loginController = module.get(LoginController); + loginServiceMock = module.get(LoginService); + someServiceMock = module.get(NewLoginService); + tokenSet = module.get(TokenSet); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(loginController).toBeDefined(); + }); + + describe('when getting result from service', () => { + it('should not throw', async () => { + const userParams: UserParams = { + username: faker.string.alpha(), + password: faker.string.alpha(), + }; + loginServiceMock.getTokenForUser.mockResolvedValue(tokenSet); + await expect(loginController.loginUser(userParams)).resolves.not.toThrow(); + expect(loginServiceMock.getTokenForUser).toHaveBeenCalledTimes(1); + }); + }); + + describe('when getting KeyCloak-error from service', () => { + it('should throw', async () => { + const errorMsg: string = 'keycloak not available'; + const userParams: UserParams = { + username: faker.string.alpha(), + password: faker.string.alpha(), + }; + loginServiceMock.getTokenForUser.mockImplementation(() => { + throw new KeycloakClientError(errorMsg); + }); + await expect(loginController.loginUser(userParams)).rejects.toThrow(errorMsg); + expect(loginServiceMock.getTokenForUser).toHaveBeenCalledTimes(1); + }); + }); + + describe('when getting User-authentication-failed-error from service', () => { + it('should throw', async () => { + const errorMsg: string = 'user could not be authenticated'; + const userParams: UserParams = { + username: faker.string.alpha(), + password: faker.string.alpha(), + }; + loginServiceMock.getTokenForUser.mockImplementation(() => { + throw new UserAuthenticationFailedError(errorMsg); + }); + await expect(loginController.loginUser(userParams)).rejects.toThrow(errorMsg); + expect(loginServiceMock.getTokenForUser).toHaveBeenCalledTimes(1); + }); + }); + + describe('when getting User-authentication-failed-error from service', () => { + it('should throw', async () => { + const userParams: UserParams = { + username: faker.string.alpha(), + password: faker.string.alpha(), + }; + someServiceMock.auth.mockResolvedValueOnce({ + ok: false, + error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), + }); + await expect(loginController.loginUserResult(userParams)).resolves.toStrictEqual< + Result + >({ + ok: false, + error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), + }); + expect(someServiceMock.auth).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/modules/ui-backend/api/login.controller.ts b/src/modules/ui-backend/api/login.controller.ts new file mode 100644 index 000000000..19b2a91cb --- /dev/null +++ b/src/modules/ui-backend/api/login.controller.ts @@ -0,0 +1,47 @@ +import { Body, Controller, HttpStatus, Post, UseFilters } from '@nestjs/common'; +import { + ApiInternalServerErrorResponse, + ApiNotFoundResponse, + ApiServiceUnavailableResponse, + ApiTags, +} from '@nestjs/swagger'; +import { UserParams } from './user.params.js'; +import { LoginService } from '../domain/login.service.js'; +import { TokenSet } from 'openid-client'; +import { KeyCloakExceptionFilter } from './keycloak-exception-filter.js'; +import { UserAuthenticationFailedExceptionFilter } from './user-authentication-failed-exception-filter.js'; +import { NewLoginService } from '../domain/new-login.service.js'; +import { DomainError } from '../../../shared/error/index.js'; + +@ApiTags('api/login') +@Controller({ path: 'login'}) +export class LoginController { + public constructor(private loginService: LoginService, private someService: NewLoginService) {} + + @Post() + @UseFilters( + new KeyCloakExceptionFilter(HttpStatus.SERVICE_UNAVAILABLE), + new UserAuthenticationFailedExceptionFilter(HttpStatus.NOT_FOUND), + ) + @ApiNotFoundResponse({ + description: 'USER_AUTHENTICATION_FAILED_ERROR: User could not be authenticated successfully.', + }) + @ApiInternalServerErrorResponse({ description: 'Internal server error while retrieving token.' }) + @ApiServiceUnavailableResponse({ description: 'KEYCLOAK_CLIENT_ERROR: KeyCloak service did not respond.' }) + public async loginUser(@Body() params: UserParams): Promise { + return this.loginService.getTokenForUser(params.username, params.password); + } + + @Post('result') + @UseFilters( + new KeyCloakExceptionFilter(HttpStatus.SERVICE_UNAVAILABLE), + new UserAuthenticationFailedExceptionFilter(HttpStatus.NOT_FOUND), + ) + @ApiNotFoundResponse({ + description: 'USER_AUTHENTICATION_FAILED_ERROR: User could not be authenticated successfully.', + }) + @ApiInternalServerErrorResponse({ description: 'Internal server error while retrieving token.' }) + public async loginUserResult(@Body() params: UserParams): Promise> { + return this.someService.auth(params.username, params.password); + } +} diff --git a/src/modules/ui-backend/api/ui-backend-exception-filter.spec.ts b/src/modules/ui-backend/api/ui-backend-exception-filter.spec.ts new file mode 100644 index 000000000..3b791582b --- /dev/null +++ b/src/modules/ui-backend/api/ui-backend-exception-filter.spec.ts @@ -0,0 +1,60 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UiBackendExceptionFilter } from './ui-backend-exception-filter.js'; +import { KeycloakClientError } from '../../../shared/error/index.js'; +import Mock = jest.Mock; +import { ArgumentsHost } from '@nestjs/common'; + +const mockJson: Mock = jest.fn(); +const mockUrl: Mock = jest.fn().mockImplementation(() => ({ + url: mockUrl, +})); +const mockStatus: Mock = jest.fn().mockImplementation(() => ({ + json: mockJson, +})); +const mockGetRequest: Mock = jest.fn().mockImplementation(() => ({ + url: mockUrl, +})); +const mockGetResponse: Mock = jest.fn().mockImplementation(() => ({ + status: mockStatus, +})); +const mockHttpArgumentsHost: Mock = jest.fn().mockImplementation(() => ({ + getResponse: mockGetResponse, + getRequest: mockGetRequest, +})); + +const mockArgumentsHost: ArgumentsHost = { + switchToHttp: mockHttpArgumentsHost, + getArgByIndex: jest.fn(), + getArgs: jest.fn(), + getType: jest.fn(), + switchToRpc: jest.fn(), + switchToWs: jest.fn(), +}; + +describe('System header validation service', () => { + let service: UiBackendExceptionFilter; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [UiBackendExceptionFilter], + }).compile(); + service = module.get>(UiBackendExceptionFilter); + }); + + describe('UI Backend exception filter tests', () => { + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('Http exception', () => { + service.catch(new KeycloakClientError(''), mockArgumentsHost); + expect(mockHttpArgumentsHost).toBeCalledTimes(1); + expect(mockHttpArgumentsHost).toBeCalledWith(); + expect(mockGetResponse).toBeCalledTimes(1); + expect(mockGetResponse).toBeCalledWith(); + expect(mockStatus).toBeCalledTimes(1); + expect(mockJson).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/modules/ui-backend/api/ui-backend-exception-filter.ts b/src/modules/ui-backend/api/ui-backend-exception-filter.ts new file mode 100644 index 000000000..374346c34 --- /dev/null +++ b/src/modules/ui-backend/api/ui-backend-exception-filter.ts @@ -0,0 +1,20 @@ +import { ExceptionFilter, ArgumentsHost, HttpStatus } from '@nestjs/common'; +import { Request, Response } from 'express'; +import { DomainError } from '../../../shared/error/index.js'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; + +export class UiBackendExceptionFilter implements ExceptionFilter { + public constructor(private httpStatusCode: HttpStatus) {} + + public catch(exception: R, host: ArgumentsHost): void { + const ctx: HttpArgumentsHost = host.switchToHttp(); + const response: Response = ctx.getResponse(); + const request: Request = ctx.getRequest(); + + response.status(this.httpStatusCode).json({ + code: exception.code, + message: exception.message, + path: request.url, + }); + } +} diff --git a/src/modules/ui-backend/api/user-authentication-failed-exception-filter.ts b/src/modules/ui-backend/api/user-authentication-failed-exception-filter.ts new file mode 100644 index 000000000..8a2ace05b --- /dev/null +++ b/src/modules/ui-backend/api/user-authentication-failed-exception-filter.ts @@ -0,0 +1,6 @@ +import { Catch } from '@nestjs/common'; +import { UserAuthenticationFailedError } from '../../../shared/error/index.js'; +import { UiBackendExceptionFilter } from './ui-backend-exception-filter.js'; + +@Catch(UserAuthenticationFailedError) +export class UserAuthenticationFailedExceptionFilter extends UiBackendExceptionFilter {} diff --git a/src/modules/ui-backend/api/user.params.ts b/src/modules/ui-backend/api/user.params.ts new file mode 100644 index 000000000..14ce5d5c6 --- /dev/null +++ b/src/modules/ui-backend/api/user.params.ts @@ -0,0 +1,15 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class UserParams { + @AutoMap() + @IsString() + @ApiProperty({ name: 'username', required: true }) + public readonly username!: string; + + @AutoMap() + @IsString() + @ApiProperty({ name: 'password', required: true }) + public readonly password!: string; +} diff --git a/src/modules/ui-backend/domain/login.service.spec.ts b/src/modules/ui-backend/domain/login.service.spec.ts new file mode 100644 index 000000000..663872087 --- /dev/null +++ b/src/modules/ui-backend/domain/login.service.spec.ts @@ -0,0 +1,57 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LoginService } from './login.service.js'; +import { errors, Issuer } from 'openid-client'; +import { createMock } from '@golevelup/ts-jest'; +import { KeycloakClientError, UserAuthenticationFailedError } from '../../../shared/error/index.js'; +import OPError = errors.OPError; + +const issuerDiscoverMock: jest.Mock = jest.fn(); +Issuer.discover = issuerDiscoverMock; + +describe('LoginService', () => { + let module: TestingModule; + let loginService: LoginService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [LoginService], + }).compile(); + loginService = module.get(LoginService); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(loginService).toBeDefined(); + }); + + describe('should execute getTokenForUser', () => { + it('expect no exceptions when Keycloak is mocked', async () => { + issuerDiscoverMock.mockResolvedValueOnce(createMock(Issuer)); + await expect(loginService.getTokenForUser('u', 'p')).resolves.not.toThrow(); + expect(issuerDiscoverMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('should fail during execution of getTokenForUser', () => { + it('expect KeycloakClientError', async () => { + const expectedError: KeycloakClientError = new KeycloakClientError('KeyCloak service did not respond.'); + issuerDiscoverMock.mockRejectedValueOnce(expectedError); + await expect(loginService.getTokenForUser('u', 'p')).rejects.toThrow(KeycloakClientError); + expect(issuerDiscoverMock).toHaveBeenCalledTimes(1); + }); + + it('expect UserAuthenticationFailedError', async () => { + const expectedError: OPError = new OPError({ error: 'invalid_grant' }); + issuerDiscoverMock.mockRejectedValueOnce(expectedError); + await expect(loginService.getTokenForUser('u', 'p')).rejects.toThrow(UserAuthenticationFailedError); + expect(issuerDiscoverMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/modules/ui-backend/domain/login.service.ts b/src/modules/ui-backend/domain/login.service.ts new file mode 100644 index 000000000..6e375c4ce --- /dev/null +++ b/src/modules/ui-backend/domain/login.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { Client, errors, Issuer, TokenSet } from 'openid-client'; +import OPError = errors.OPError; +import { KeycloakClientError } from '../../../shared/error/index.js'; +import { UserAuthenticationFailedError } from '../../../shared/error/user-authentication-failed.error.js'; + +@Injectable() +export class LoginService { + private static readonly REALM_NAME: string = 'http://localhost:8080/realms/schulportal'; + + private static readonly CLIENT_ID: string = 'schulportal'; + + public async getTokenForUser(username: string, password: string): Promise { + try { + const keycloakIssuer: Issuer = await Issuer.discover(LoginService.REALM_NAME); + const client: Client = new keycloakIssuer.Client({ + client_id: LoginService.CLIENT_ID, + token_endpoint_auth_method: 'none', + }); + return await client.grant({ + grant_type: 'password', + username: username, + password: password, + }); + } catch (e) { + if (e instanceof OPError && e.error === 'invalid_grant') { + throw new UserAuthenticationFailedError('User could not be authenticated successfully.'); + } + throw new KeycloakClientError('KeyCloak service did not respond.'); + } + } +} diff --git a/src/modules/ui-backend/domain/new-login.service.spec.ts b/src/modules/ui-backend/domain/new-login.service.spec.ts new file mode 100644 index 000000000..d3db2faa6 --- /dev/null +++ b/src/modules/ui-backend/domain/new-login.service.spec.ts @@ -0,0 +1,86 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Issuer } from 'openid-client'; +import { NewLoginService } from './new-login.service.js'; +import { ConfigTestModule } from '../../../../test/utils/index.js'; +import { KeycloakAdminClient } from '@s3pweb/keycloak-admin-client-cjs'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { KeycloakClientError, UserAuthenticationFailedError } from '../../../shared/error/index.js'; + +const issuerDiscoverMock: jest.Mock = jest.fn(); +Issuer.discover = issuerDiscoverMock; + +describe('SomeService', () => { + let module: TestingModule; + let someService: NewLoginService; + let kcAdminClient: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ConfigTestModule], + providers: [ + NewLoginService, + { + provide: KeycloakAdminClient, + useValue: createMock(), + }, + ], + }).compile(); + someService = module.get(NewLoginService); + kcAdminClient = module.get(KeycloakAdminClient); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('should execute getTokenForUser', () => { + it('expect no exceptions when Keycloak is mocked', async () => { + kcAdminClient.getAccessToken.mockResolvedValueOnce(Promise.resolve('thisIsATokenString')); + const result: Result = await someService.auth('test', 'pass'); + expect(result).toStrictEqual>({ + ok: true, + value: 'thisIsATokenString', + }); + await someService.auth('user', 'password'); + expect(kcAdminClient.auth).toHaveBeenCalledTimes(2); + }); + }); + + describe('should execute getTokenForUser', () => { + it('expect exception when auth() fails', async () => { + kcAdminClient.auth.mockRejectedValueOnce(KeycloakClientError); + const result: Result = await someService.auth('test', 'pass'); + expect(result).toStrictEqual>({ + ok: false, + error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), + }); + expect(kcAdminClient.auth).toHaveBeenCalledTimes(1); + }); + + it('expect exception for accessToken === undefined', async () => { + kcAdminClient.auth.mockImplementationOnce(() => Promise.resolve()); + kcAdminClient.getAccessToken.mockResolvedValueOnce(Promise.resolve(undefined)); + const result: Result = await someService.auth('test', 'pass'); + expect(result).toStrictEqual>({ + ok: false, + error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), + }); + expect(kcAdminClient.auth).toHaveBeenCalledTimes(1); + }); + + it('expect exception when getAccessToken throws exception', async () => { + kcAdminClient.auth.mockImplementationOnce(() => Promise.resolve()); + kcAdminClient.getAccessToken.mockRejectedValueOnce(Promise.resolve('test')); + const result: Result = await someService.auth('test', 'pass'); + expect(result).toStrictEqual>({ + ok: false, + error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), + }); + expect(kcAdminClient.auth).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/modules/ui-backend/domain/new-login.service.ts b/src/modules/ui-backend/domain/new-login.service.ts new file mode 100644 index 000000000..fccc0ac42 --- /dev/null +++ b/src/modules/ui-backend/domain/new-login.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { Credentials, KeycloakAdminClient } from '@s3pweb/keycloak-admin-client-cjs'; +import { ConfigService } from '@nestjs/config'; +import { KeycloakConfig } from '../../../shared/config/index.js'; +import { DomainError, UserAuthenticationFailedError } from '../../../shared/error/index.js'; + +@Injectable() +export class NewLoginService { + private kcConfig: KeycloakConfig; + + public constructor(private readonly kcAdminClient: KeycloakAdminClient, private readonly config: ConfigService) { + this.kcConfig = this.config.getOrThrow('SCHULPORTAL'); + this.kcAdminClient.setConfig({ + baseUrl: this.kcConfig.BASE_URL, + realmName: this.kcConfig.REALM_NAME, + }); + } + + public async auth(username: string, password: string): Promise> { + try { + const credentials: Credentials = { + grantType: 'password', + clientId: this.kcConfig.CLIENT_ID, + username: username, + password: password, + }; + await this.kcAdminClient.auth(credentials); + const accessToken: string | undefined = await this.kcAdminClient.getAccessToken(); + if (accessToken !== undefined) { + return { ok: true, value: accessToken }; + } else { + // kcAdminClient will throw an exception if credentials are wrong, not return undefined + return { + ok: false, + error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), + }; + } + } catch (err) { + return { + ok: false, + error: new UserAuthenticationFailedError('User could not be authenticated successfully.'), + }; + } + } +} diff --git a/src/modules/ui-backend/ui-backend-api.module.ts b/src/modules/ui-backend/ui-backend-api.module.ts new file mode 100644 index 000000000..3b5310908 --- /dev/null +++ b/src/modules/ui-backend/ui-backend-api.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { LoginController } from './api/login.controller.js'; +import { LoginService } from './domain/login.service.js'; +import { NewLoginService } from './domain/new-login.service.js'; +import { KeycloakAdminClient } from '@s3pweb/keycloak-admin-client-cjs'; + +@Module({ + imports: [], + providers: [KeycloakAdminClient, LoginService, NewLoginService], + controllers: [LoginController], +}) +export class UiBackendApiModule {} diff --git a/src/server/server.module.ts b/src/server/server.module.ts index cfd2deacb..6fe647b3b 100644 --- a/src/server/server.module.ts +++ b/src/server/server.module.ts @@ -11,6 +11,7 @@ import { PersonApiModule } from '../modules/person/person-api.module.js'; import { HealthModule } from '../health/health.module.js'; import { KeycloakAdministrationModule } from '../modules/keycloak-administration/keycloak-administration.module.js'; import { OrganisationApiModule } from '../modules/organisation/organisation-api.module.js'; +import { UiBackendApiModule } from '../modules/ui-backend/ui-backend-api.module.js'; @Module({ imports: [ @@ -47,6 +48,7 @@ import { OrganisationApiModule } from '../modules/organisation/organisation-api. OrganisationApiModule, KeycloakAdministrationModule, HealthModule, + UiBackendApiModule, ], }) export class ServerModule {} diff --git a/src/shared/config/json.config.ts b/src/shared/config/json.config.ts index f1676b9c6..bf69d2f2a 100644 --- a/src/shared/config/json.config.ts +++ b/src/shared/config/json.config.ts @@ -21,4 +21,8 @@ export class JsonConfig { @ValidateNested() @Type(() => KeycloakConfig) public readonly KEYCLOAK!: KeycloakConfig; + + @ValidateNested() + @Type(() => KeycloakConfig) + public readonly SCHULPORTAL!: KeycloakConfig; } diff --git a/src/shared/error/index.ts b/src/shared/error/index.ts index 9d0526111..ce9b8a0aa 100644 --- a/src/shared/error/index.ts +++ b/src/shared/error/index.ts @@ -4,3 +4,4 @@ export * from './keycloak-client.error.js'; export * from './mapping.error.js'; export * from './person-already-exists.error.js'; export * from './entity-not-found.error.js'; +export * from './user-authentication-failed.error.js'; diff --git a/src/shared/error/user-authentication-failed.error.ts b/src/shared/error/user-authentication-failed.error.ts new file mode 100644 index 000000000..9156e10d0 --- /dev/null +++ b/src/shared/error/user-authentication-failed.error.ts @@ -0,0 +1,7 @@ +import { DomainError } from './domain.error.js'; + +export class UserAuthenticationFailedError extends DomainError { + public constructor(message: string, details?: unknown[] | Record) { + super(message, 'USER_AUTHENTICATION_FAILED_ERROR', details); + } +} From 121cce32ab6df591f18e6192f700688908e552ca Mon Sep 17 00:00:00 2001 From: DPDS93CT Date: Tue, 10 Oct 2023 09:03:17 +0200 Subject: [PATCH 2/5] include dummy values for Schulportal in config-files --- config/config.dev.json | 4 +++- config/config.test.json | 4 +++- src/modules/ui-backend/api/login.controller.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/config/config.dev.json b/config/config.dev.json index f8f706ad4..dc812de86 100644 --- a/config/config.dev.json +++ b/config/config.dev.json @@ -18,6 +18,8 @@ "BASE_URL": "http://127.0.0.1:8080", "REALM_NAME": "schulportal", "CLIENT_ID": "schulportal", - "USERNAME": "dummy" + "USERNAME": "dummy", + "PASSWORD": "dummy", + "SECRET": "dummy" } } diff --git a/config/config.test.json b/config/config.test.json index 09e3ecb1f..7b7415f97 100644 --- a/config/config.test.json +++ b/config/config.test.json @@ -18,6 +18,8 @@ "BASE_URL": "http://127.0.0.1:8080", "REALM_NAME": "schulportal", "CLIENT_ID": "schulportal", - "USERNAME": "dummy" + "USERNAME": "dummy", + "PASSWORD": "dummy", + "SECRET": "dummy" } } diff --git a/src/modules/ui-backend/api/login.controller.ts b/src/modules/ui-backend/api/login.controller.ts index 19b2a91cb..4ce33b579 100644 --- a/src/modules/ui-backend/api/login.controller.ts +++ b/src/modules/ui-backend/api/login.controller.ts @@ -14,7 +14,7 @@ import { NewLoginService } from '../domain/new-login.service.js'; import { DomainError } from '../../../shared/error/index.js'; @ApiTags('api/login') -@Controller({ path: 'login'}) +@Controller({ path: 'login' }) export class LoginController { public constructor(private loginService: LoginService, private someService: NewLoginService) {} From 8dd1d9bfc6709313852a26ac3d67ff7411e02e4f Mon Sep 17 00:00:00 2001 From: DPDS93CT Date: Mon, 16 Oct 2023 09:27:41 +0200 Subject: [PATCH 3/5] put config for schulportal realm and client-id in existing kc-config --- config/config.dev.json | 12 +++--------- config/config.test.json | 12 +++--------- src/health/health.controller.spec.ts | 2 ++ .../ui-backend/domain/login.service.spec.ts | 2 ++ src/modules/ui-backend/domain/login.service.ts | 14 ++++++++++---- src/modules/ui-backend/domain/new-login.service.ts | 6 +++--- src/modules/ui-backend/ui-backend-api.module.ts | 3 ++- src/shared/config/config.loader.spec.ts | 4 ++++ src/shared/config/json.config.ts | 4 ---- src/shared/config/keycloak.config.ts | 8 ++++++++ 10 files changed, 37 insertions(+), 30 deletions(-) diff --git a/config/config.dev.json b/config/config.dev.json index dc812de86..66b113396 100644 --- a/config/config.dev.json +++ b/config/config.dev.json @@ -12,14 +12,8 @@ "KEYCLOAK": { "BASE_URL": "http://127.0.0.1:8080", "REALM_NAME": "master", - "CLIENT_ID": "admin-cli" - }, - "SCHULPORTAL": { - "BASE_URL": "http://127.0.0.1:8080", - "REALM_NAME": "schulportal", - "CLIENT_ID": "schulportal", - "USERNAME": "dummy", - "PASSWORD": "dummy", - "SECRET": "dummy" + "CLIENT_ID": "admin-cli", + "SCHULPORTAL_REALM_NAME": "schulportal", + "SCHULPORTAL_CLIENT_ID": "schulportal" } } diff --git a/config/config.test.json b/config/config.test.json index 7b7415f97..9515936ae 100644 --- a/config/config.test.json +++ b/config/config.test.json @@ -12,14 +12,8 @@ "KEYCLOAK": { "BASE_URL": "http://127.0.0.1:8080", "REALM_NAME": "master", - "CLIENT_ID": "admin-cli" - }, - "SCHULPORTAL": { - "BASE_URL": "http://127.0.0.1:8080", - "REALM_NAME": "schulportal", - "CLIENT_ID": "schulportal", - "USERNAME": "dummy", - "PASSWORD": "dummy", - "SECRET": "dummy" + "CLIENT_ID": "admin-cli", + "SCHULPORTAL_REALM_NAME": "schulportal", + "SCHULPORTAL_CLIENT_ID": "schulportal" } } diff --git a/src/health/health.controller.spec.ts b/src/health/health.controller.spec.ts index 318cc2ada..9b304d75a 100644 --- a/src/health/health.controller.spec.ts +++ b/src/health/health.controller.spec.ts @@ -24,6 +24,8 @@ describe('HealthController', () => { SECRET: '', REALM_NAME: '', BASE_URL: 'http://keycloak.test', + SCHULPORTAL_REALM_NAME: '', + SCHULPORTAL_CLIENT_ID: '', }; let configService: DeepMocked; diff --git a/src/modules/ui-backend/domain/login.service.spec.ts b/src/modules/ui-backend/domain/login.service.spec.ts index 663872087..07c0d29b7 100644 --- a/src/modules/ui-backend/domain/login.service.spec.ts +++ b/src/modules/ui-backend/domain/login.service.spec.ts @@ -4,6 +4,7 @@ import { errors, Issuer } from 'openid-client'; import { createMock } from '@golevelup/ts-jest'; import { KeycloakClientError, UserAuthenticationFailedError } from '../../../shared/error/index.js'; import OPError = errors.OPError; +import { ConfigTestModule } from '../../../../test/utils/index.js'; const issuerDiscoverMock: jest.Mock = jest.fn(); Issuer.discover = issuerDiscoverMock; @@ -14,6 +15,7 @@ describe('LoginService', () => { beforeAll(async () => { module = await Test.createTestingModule({ + imports: [ConfigTestModule], providers: [LoginService], }).compile(); loginService = module.get(LoginService); diff --git a/src/modules/ui-backend/domain/login.service.ts b/src/modules/ui-backend/domain/login.service.ts index 6e375c4ce..085880e76 100644 --- a/src/modules/ui-backend/domain/login.service.ts +++ b/src/modules/ui-backend/domain/login.service.ts @@ -3,18 +3,24 @@ import { Client, errors, Issuer, TokenSet } from 'openid-client'; import OPError = errors.OPError; import { KeycloakClientError } from '../../../shared/error/index.js'; import { UserAuthenticationFailedError } from '../../../shared/error/user-authentication-failed.error.js'; +import { KeycloakConfig } from '../../../shared/config/index.js'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class LoginService { - private static readonly REALM_NAME: string = 'http://localhost:8080/realms/schulportal'; + private kcConfig: KeycloakConfig; - private static readonly CLIENT_ID: string = 'schulportal'; + public constructor(private readonly config: ConfigService) { + this.kcConfig = this.config.getOrThrow('KEYCLOAK'); + } public async getTokenForUser(username: string, password: string): Promise { try { - const keycloakIssuer: Issuer = await Issuer.discover(LoginService.REALM_NAME); + const keycloakIssuer: Issuer = await Issuer.discover( + this.kcConfig.BASE_URL + '/realms/' + this.kcConfig.SCHULPORTAL_REALM_NAME, + ); const client: Client = new keycloakIssuer.Client({ - client_id: LoginService.CLIENT_ID, + client_id: this.kcConfig.SCHULPORTAL_CLIENT_ID, token_endpoint_auth_method: 'none', }); return await client.grant({ diff --git a/src/modules/ui-backend/domain/new-login.service.ts b/src/modules/ui-backend/domain/new-login.service.ts index fccc0ac42..c630d3fb2 100644 --- a/src/modules/ui-backend/domain/new-login.service.ts +++ b/src/modules/ui-backend/domain/new-login.service.ts @@ -9,10 +9,10 @@ export class NewLoginService { private kcConfig: KeycloakConfig; public constructor(private readonly kcAdminClient: KeycloakAdminClient, private readonly config: ConfigService) { - this.kcConfig = this.config.getOrThrow('SCHULPORTAL'); + this.kcConfig = this.config.getOrThrow('KEYCLOAK'); this.kcAdminClient.setConfig({ baseUrl: this.kcConfig.BASE_URL, - realmName: this.kcConfig.REALM_NAME, + realmName: this.kcConfig.SCHULPORTAL_REALM_NAME, }); } @@ -20,7 +20,7 @@ export class NewLoginService { try { const credentials: Credentials = { grantType: 'password', - clientId: this.kcConfig.CLIENT_ID, + clientId: this.kcConfig.SCHULPORTAL_CLIENT_ID, username: username, password: password, }; diff --git a/src/modules/ui-backend/ui-backend-api.module.ts b/src/modules/ui-backend/ui-backend-api.module.ts index 3b5310908..4aff43dc6 100644 --- a/src/modules/ui-backend/ui-backend-api.module.ts +++ b/src/modules/ui-backend/ui-backend-api.module.ts @@ -3,10 +3,11 @@ import { LoginController } from './api/login.controller.js'; import { LoginService } from './domain/login.service.js'; import { NewLoginService } from './domain/new-login.service.js'; import { KeycloakAdminClient } from '@s3pweb/keycloak-admin-client-cjs'; +import { ConfigService } from '@nestjs/config'; @Module({ imports: [], - providers: [KeycloakAdminClient, LoginService, NewLoginService], + providers: [KeycloakAdminClient, ConfigService, LoginService, NewLoginService], controllers: [LoginController], }) export class UiBackendApiModule {} diff --git a/src/shared/config/config.loader.spec.ts b/src/shared/config/config.loader.spec.ts index c4f72f8cd..41fec39ba 100644 --- a/src/shared/config/config.loader.spec.ts +++ b/src/shared/config/config.loader.spec.ts @@ -40,6 +40,8 @@ describe('configloader', () => { BASE_URL: 'localhost:8080', CLIENT_ID: 'admin-cli', REALM_NAME: 'master', + SCHULPORTAL_REALM_NAME: 'schulportal', + SCHULPORTAL_CLIENT_ID: 'schulportal', }, }; @@ -82,6 +84,8 @@ describe('configloader', () => { BASE_URL: '', CLIENT_ID: '', REALM_NAME: '', + SCHULPORTAL_REALM_NAME: '', + SCHULPORTAL_CLIENT_ID: '', }, }; diff --git a/src/shared/config/json.config.ts b/src/shared/config/json.config.ts index bf69d2f2a..f1676b9c6 100644 --- a/src/shared/config/json.config.ts +++ b/src/shared/config/json.config.ts @@ -21,8 +21,4 @@ export class JsonConfig { @ValidateNested() @Type(() => KeycloakConfig) public readonly KEYCLOAK!: KeycloakConfig; - - @ValidateNested() - @Type(() => KeycloakConfig) - public readonly SCHULPORTAL!: KeycloakConfig; } diff --git a/src/shared/config/keycloak.config.ts b/src/shared/config/keycloak.config.ts index c74b1a737..7422977da 100644 --- a/src/shared/config/keycloak.config.ts +++ b/src/shared/config/keycloak.config.ts @@ -16,4 +16,12 @@ export class KeycloakConfig { @IsString() @IsNotEmpty() public readonly SECRET!: string; + + @IsString() + @IsNotEmpty() + public readonly SCHULPORTAL_REALM_NAME!: string; + + @IsString() + @IsNotEmpty() + public readonly SCHULPORTAL_CLIENT_ID!: string; } From cb5453c7cb15f58da6e93f569ca49588c66141c2 Mon Sep 17 00:00:00 2001 From: DPDS93CT Date: Mon, 16 Oct 2023 11:58:07 +0200 Subject: [PATCH 4/5] adjust realm names: realm and client-id for admin renamed --- config/config.dev.json | 8 ++++---- config/config.test.json | 8 ++++---- src/frontend/frontend.module.ts | 6 +++--- src/health/health.controller.spec.ts | 10 +++++----- .../domain/keycloak-admin-client.service.ts | 6 +++--- src/modules/ui-backend/domain/login.service.ts | 4 ++-- .../ui-backend/domain/new-login.service.ts | 4 ++-- src/shared/config/config.loader.spec.ts | 16 ++++++++-------- src/shared/config/keycloak.config.ts | 10 +++++----- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/config/config.dev.json b/config/config.dev.json index 66b113396..936a4f4fb 100644 --- a/config/config.dev.json +++ b/config/config.dev.json @@ -11,9 +11,9 @@ }, "KEYCLOAK": { "BASE_URL": "http://127.0.0.1:8080", - "REALM_NAME": "master", - "CLIENT_ID": "admin-cli", - "SCHULPORTAL_REALM_NAME": "schulportal", - "SCHULPORTAL_CLIENT_ID": "schulportal" + "ADMIN_REALM_NAME": "master", + "ADMIN_CLIENT_ID": "admin-cli", + "REALM_NAME": "schulportal", + "CLIENT_ID": "schulportal" } } diff --git a/config/config.test.json b/config/config.test.json index 9515936ae..8f2fb1186 100644 --- a/config/config.test.json +++ b/config/config.test.json @@ -11,9 +11,9 @@ }, "KEYCLOAK": { "BASE_URL": "http://127.0.0.1:8080", - "REALM_NAME": "master", - "CLIENT_ID": "admin-cli", - "SCHULPORTAL_REALM_NAME": "schulportal", - "SCHULPORTAL_CLIENT_ID": "schulportal" + "ADMIN_REALM_NAME": "master", + "ADMIN_CLIENT_ID": "admin-cli", + "REALM_NAME": "schulportal", + "CLIENT_ID": "schulportal" } } diff --git a/src/frontend/frontend.module.ts b/src/frontend/frontend.module.ts index 21692031c..f631b6aac 100644 --- a/src/frontend/frontend.module.ts +++ b/src/frontend/frontend.module.ts @@ -42,9 +42,9 @@ import { mappingErrorHandler } from '../shared/error/mapping.error.js'; return { authServerUrl: keycloakConfig.BASE_URL, - realm: keycloakConfig.REALM_NAME, - clientId: keycloakConfig.CLIENT_ID, - secret: keycloakConfig.SECRET, + realm: keycloakConfig.ADMIN_REALM_NAME, + clientId: keycloakConfig.ADMIN_CLIENT_ID, + secret: keycloakConfig.ADMIN_SECRET, }; }, inject: [ConfigService], diff --git a/src/health/health.controller.spec.ts b/src/health/health.controller.spec.ts index 9b304d75a..123c04e46 100644 --- a/src/health/health.controller.spec.ts +++ b/src/health/health.controller.spec.ts @@ -20,12 +20,12 @@ describe('HealthController', () => { let entityManager: SqlEntityManager; let httpHealthIndicator: DeepMocked; const keycloakConfig: KeycloakConfig = { - CLIENT_ID: '', - SECRET: '', - REALM_NAME: '', + ADMIN_CLIENT_ID: '', + ADMIN_SECRET: '', + ADMIN_REALM_NAME: '', BASE_URL: 'http://keycloak.test', - SCHULPORTAL_REALM_NAME: '', - SCHULPORTAL_CLIENT_ID: '', + REALM_NAME: '', + CLIENT_ID: '', }; let configService: DeepMocked; diff --git a/src/modules/keycloak-administration/domain/keycloak-admin-client.service.ts b/src/modules/keycloak-administration/domain/keycloak-admin-client.service.ts index 8a4a6b217..17311ede4 100644 --- a/src/modules/keycloak-administration/domain/keycloak-admin-client.service.ts +++ b/src/modules/keycloak-administration/domain/keycloak-admin-client.service.ts @@ -18,7 +18,7 @@ export class KeycloakAdministrationService { this.kcAdminClient.setConfig({ baseUrl: this.kcConfig.BASE_URL, - realmName: this.kcConfig.REALM_NAME, + realmName: this.kcConfig.ADMIN_REALM_NAME, }); } @@ -45,8 +45,8 @@ export class KeycloakAdministrationService { try { const credentials: Credentials = { grantType: 'client_credentials', - clientId: this.kcConfig.CLIENT_ID, - clientSecret: this.kcConfig.SECRET, + clientId: this.kcConfig.ADMIN_CLIENT_ID, + clientSecret: this.kcConfig.ADMIN_SECRET, }; await this.kcAdminClient.auth(credentials); diff --git a/src/modules/ui-backend/domain/login.service.ts b/src/modules/ui-backend/domain/login.service.ts index 085880e76..d71a58302 100644 --- a/src/modules/ui-backend/domain/login.service.ts +++ b/src/modules/ui-backend/domain/login.service.ts @@ -17,10 +17,10 @@ export class LoginService { public async getTokenForUser(username: string, password: string): Promise { try { const keycloakIssuer: Issuer = await Issuer.discover( - this.kcConfig.BASE_URL + '/realms/' + this.kcConfig.SCHULPORTAL_REALM_NAME, + this.kcConfig.BASE_URL + '/realms/' + this.kcConfig.REALM_NAME, ); const client: Client = new keycloakIssuer.Client({ - client_id: this.kcConfig.SCHULPORTAL_CLIENT_ID, + client_id: this.kcConfig.CLIENT_ID, token_endpoint_auth_method: 'none', }); return await client.grant({ diff --git a/src/modules/ui-backend/domain/new-login.service.ts b/src/modules/ui-backend/domain/new-login.service.ts index c630d3fb2..f2c5ec4af 100644 --- a/src/modules/ui-backend/domain/new-login.service.ts +++ b/src/modules/ui-backend/domain/new-login.service.ts @@ -12,7 +12,7 @@ export class NewLoginService { this.kcConfig = this.config.getOrThrow('KEYCLOAK'); this.kcAdminClient.setConfig({ baseUrl: this.kcConfig.BASE_URL, - realmName: this.kcConfig.SCHULPORTAL_REALM_NAME, + realmName: this.kcConfig.REALM_NAME, }); } @@ -20,7 +20,7 @@ export class NewLoginService { try { const credentials: Credentials = { grantType: 'password', - clientId: this.kcConfig.SCHULPORTAL_CLIENT_ID, + clientId: this.kcConfig.CLIENT_ID, username: username, password: password, }; diff --git a/src/shared/config/config.loader.spec.ts b/src/shared/config/config.loader.spec.ts index 41fec39ba..7388d5589 100644 --- a/src/shared/config/config.loader.spec.ts +++ b/src/shared/config/config.loader.spec.ts @@ -38,16 +38,16 @@ describe('configloader', () => { }, KEYCLOAK: { BASE_URL: 'localhost:8080', - CLIENT_ID: 'admin-cli', - REALM_NAME: 'master', - SCHULPORTAL_REALM_NAME: 'schulportal', - SCHULPORTAL_CLIENT_ID: 'schulportal', + ADMIN_CLIENT_ID: 'admin-cli', + ADMIN_REALM_NAME: 'master', + REALM_NAME: 'schulportal', + CLIENT_ID: 'schulportal', }, }; const secrets: DeepPartial = { DB: { SECRET: 'SuperSecretSecret' }, - KEYCLOAK: { SECRET: 'ClientSecret' }, + KEYCLOAK: { ADMIN_SECRET: 'ClientSecret' }, }; beforeAll(() => { @@ -82,10 +82,10 @@ describe('configloader', () => { }, KEYCLOAK: { BASE_URL: '', - CLIENT_ID: '', + ADMIN_CLIENT_ID: '', + ADMIN_REALM_NAME: '', REALM_NAME: '', - SCHULPORTAL_REALM_NAME: '', - SCHULPORTAL_CLIENT_ID: '', + CLIENT_ID: '', }, }; diff --git a/src/shared/config/keycloak.config.ts b/src/shared/config/keycloak.config.ts index 7422977da..07911a5ce 100644 --- a/src/shared/config/keycloak.config.ts +++ b/src/shared/config/keycloak.config.ts @@ -7,21 +7,21 @@ export class KeycloakConfig { @IsString() @IsNotEmpty() - public readonly REALM_NAME!: string; + public readonly ADMIN_REALM_NAME!: string; @IsString() @IsNotEmpty() - public readonly CLIENT_ID!: string; + public readonly ADMIN_CLIENT_ID!: string; @IsString() @IsNotEmpty() - public readonly SECRET!: string; + public readonly ADMIN_SECRET!: string; @IsString() @IsNotEmpty() - public readonly SCHULPORTAL_REALM_NAME!: string; + public readonly REALM_NAME!: string; @IsString() @IsNotEmpty() - public readonly SCHULPORTAL_CLIENT_ID!: string; + public readonly CLIENT_ID!: string; } From 6b1e4694db704e795a6c16c5b8a48e0a86611a43 Mon Sep 17 00:00:00 2001 From: DPDS93CT Date: Mon, 16 Oct 2023 12:14:26 +0200 Subject: [PATCH 5/5] adjust base64-encoded string because kc-admin-secret variable was renamed --- .github/workflows/reusable_job_nest_test_sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable_job_nest_test_sonarcloud.yml b/.github/workflows/reusable_job_nest_test_sonarcloud.yml index 20eaffb27..dadceee09 100644 --- a/.github/workflows/reusable_job_nest_test_sonarcloud.yml +++ b/.github/workflows/reusable_job_nest_test_sonarcloud.yml @@ -23,7 +23,7 @@ jobs: fileName: 'secrets.json' fileDir: './config/' # These are placeholder secrets without any significance - encodedString: ewogICAgIkRCIjogewogICAgICAgICJTRUNSRVQiOiAiVmVyeSBoaWRkZW4gc2VjcmV0IgogICAgfSwKICAgICJLRVlDTE9BSyI6IHsKICAgICAgICAiU0VDUkVUIjogIkNsaWVudCBTZWNyZXQiCiAgICB9Cn0= + encodedString: ewogICAgIkRCIjogewogICAgICAgICJTRUNSRVQiOiAiVmVyeSBoaWRkZW4gc2VjcmV0IgogICAgfSwKICAgICJLRVlDTE9BSyI6IHsKICAgICAgICAiQURNSU5fU0VDUkVUIjogIkNsaWVudCBTZWNyZXQiCiAgICB9Cn0= - name: Setup node uses: actions/setup-node@v3 with: