-
Notifications
You must be signed in to change notification settings - Fork 1
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
47 changed files
with
1,272 additions
and
84 deletions.
There are no files selected for viewing
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 |
---|---|---|
|
@@ -11,7 +11,8 @@ | |
"BACKEND_ADDRESS": "http://localhost:9090", | ||
"OIDC_CALLBACK_URL": "https://localhost:8099/api/auth/login", | ||
"DEFAULT_LOGIN_REDIRECT": "https://localhost:8099/", | ||
"LOGOUT_REDIRECT": "https://localhost:8099/" | ||
"LOGOUT_REDIRECT": "https://localhost:8099/", | ||
"ERROR_PAGE_REDIRECT": "https://localhost:8099/login-error" | ||
}, | ||
"DB": { | ||
"CLIENT_URL": "postgres://admin:[email protected]:5432", | ||
|
44 changes: 44 additions & 0 deletions
44
src/modules/authentication/api/authentication-exception-filter.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,44 @@ | ||
import { ArgumentsHost } from '@nestjs/common'; | ||
import { DeepMocked, createMock } from '@golevelup/ts-jest'; | ||
import { Response } from 'express'; | ||
import { HttpArgumentsHost } from '@nestjs/common/interfaces/index.js'; | ||
import { AuthenticationDomainError } from '../domain/authentication-domain.error.js'; | ||
import { AuthenticationExceptionFilter } from './authentication-exception-filter.js'; | ||
import { AuthenticationErrorI18nTypes, DbiamAuthenticationError } from './dbiam-authentication.error.js'; | ||
|
||
describe('AuthenticationExceptionFilter', () => { | ||
let filter: AuthenticationExceptionFilter; | ||
const statusCode: number = 403; | ||
let responseMock: DeepMocked<Response>; | ||
let argumentsHost: DeepMocked<ArgumentsHost>; | ||
|
||
const generalBadRequestError: DbiamAuthenticationError = new DbiamAuthenticationError({ | ||
code: 403, | ||
i18nKey: AuthenticationErrorI18nTypes.AUTHENTICATION_ERROR, | ||
}); | ||
|
||
beforeEach(() => { | ||
filter = new AuthenticationExceptionFilter(); | ||
responseMock = createMock<Response>(); | ||
argumentsHost = createMock<ArgumentsHost>({ | ||
switchToHttp: () => | ||
createMock<HttpArgumentsHost>({ | ||
getResponse: () => responseMock, | ||
}), | ||
}); | ||
}); | ||
|
||
describe('catch', () => { | ||
describe('when filter catches undefined error', () => { | ||
it('should throw a general AuthenticationError', () => { | ||
const error: AuthenticationDomainError = new AuthenticationDomainError('error', undefined); | ||
|
||
filter.catch(error, argumentsHost); | ||
|
||
expect(responseMock.json).toHaveBeenCalled(); | ||
expect(responseMock.status).toHaveBeenCalledWith(statusCode); | ||
expect(responseMock.json).toHaveBeenCalledWith(generalBadRequestError); | ||
}); | ||
}); | ||
}); | ||
}); |
40 changes: 40 additions & 0 deletions
40
src/modules/authentication/api/authentication-exception-filter.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,40 @@ | ||
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; | ||
import { HttpArgumentsHost } from '@nestjs/common/interfaces/index.js'; | ||
import { Response } from 'express'; | ||
import { AuthenticationDomainError } from '../domain/authentication-domain.error.js'; | ||
import { KeycloakUserNotFoundError } from '../domain/keycloak-user-not-found.error.js'; | ||
import { AuthenticationErrorI18nTypes, DbiamAuthenticationError } from './dbiam-authentication.error.js'; | ||
|
||
@Catch(AuthenticationDomainError) | ||
export class AuthenticationExceptionFilter implements ExceptionFilter<AuthenticationDomainError> { | ||
private ERROR_MAPPINGS: Map<string, DbiamAuthenticationError> = new Map([ | ||
[ | ||
KeycloakUserNotFoundError.name, | ||
new DbiamAuthenticationError({ | ||
code: 403, | ||
i18nKey: AuthenticationErrorI18nTypes.KEYCLOAK_USER_NOT_FOUND, | ||
}), | ||
], | ||
]); | ||
|
||
public catch(exception: AuthenticationDomainError, host: ArgumentsHost): void { | ||
const ctx: HttpArgumentsHost = host.switchToHttp(); | ||
const response: Response = ctx.getResponse<Response>(); | ||
const status: number = 403; //all errors regarding organisation specifications are InternalServerErrors at the moment | ||
|
||
const dbiamAuthenticationError: DbiamAuthenticationError = this.mapDomainErrorToDbiamError(exception); | ||
|
||
response.status(status); | ||
response.json(dbiamAuthenticationError); | ||
} | ||
|
||
private mapDomainErrorToDbiamError(error: AuthenticationDomainError): DbiamAuthenticationError { | ||
return ( | ||
this.ERROR_MAPPINGS.get(error.constructor.name) ?? | ||
new DbiamAuthenticationError({ | ||
code: 403, | ||
i18nKey: AuthenticationErrorI18nTypes.AUTHENTICATION_ERROR, | ||
}) | ||
); | ||
} | ||
} |
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
21 changes: 21 additions & 0 deletions
21
src/modules/authentication/api/dbiam-authentication.error.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,21 @@ | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { DbiamError, DbiamErrorProps } from '../../../shared/error/dbiam.error.js'; | ||
|
||
export enum AuthenticationErrorI18nTypes { | ||
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR', | ||
KEYCLOAK_USER_NOT_FOUND = 'KEYCLOAK_USER_NOT_FOUND', | ||
} | ||
|
||
export type DbiamAuthenticationErrorProps = DbiamErrorProps & { | ||
i18nKey: AuthenticationErrorI18nTypes; | ||
}; | ||
|
||
export class DbiamAuthenticationError extends DbiamError { | ||
@ApiProperty({ enum: AuthenticationErrorI18nTypes }) | ||
public override readonly i18nKey: string; | ||
|
||
public constructor(props: DbiamAuthenticationErrorProps) { | ||
super(props); | ||
this.i18nKey = props.i18nKey; | ||
} | ||
} |
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
11 changes: 11 additions & 0 deletions
11
src/modules/authentication/domain/authentication-domain.error.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,11 @@ | ||
import { DomainError } from '../../../shared/error/index.js'; | ||
|
||
export class AuthenticationDomainError extends DomainError { | ||
public constructor( | ||
public override readonly message: string, | ||
public readonly entityId: string | undefined, | ||
details?: unknown[] | Record<string, undefined>, | ||
) { | ||
super(message, 'USER_COULD_NOT_BE_AUTHENTICATED', details); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/modules/authentication/domain/keycloak-user-not-found.error.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,13 @@ | ||
import { KeycloakUserNotFoundError } from './keycloak-user-not-found.error.js'; | ||
|
||
describe('KeycloakUserNotFoundError', () => { | ||
describe('constructor', () => { | ||
describe('when calling the constructor', () => { | ||
it('should set message and code', () => { | ||
const error: KeycloakUserNotFoundError = new KeycloakUserNotFoundError({}); | ||
expect(error.message).toBe('The Keycloak User does not exist.'); | ||
expect(error.code).toBe('USER_COULD_NOT_BE_AUTHENTICATED'); | ||
}); | ||
}); | ||
}); | ||
}); |
7 changes: 7 additions & 0 deletions
7
src/modules/authentication/domain/keycloak-user-not-found.error.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,7 @@ | ||
import { AuthenticationDomainError } from './authentication-domain.error.js'; | ||
|
||
export class KeycloakUserNotFoundError extends AuthenticationDomainError { | ||
public constructor(details?: unknown[] | Record<string, undefined>) { | ||
super('The Keycloak User does not exist.', undefined, details); | ||
} | ||
} |
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
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 |
---|---|---|
@@ -1,31 +1,55 @@ | ||
import { JwtStrategy } from './jwt.strategy.js'; | ||
import { createMock, DeepMocked } from '@golevelup/ts-jest'; | ||
import { BaseClient, Client } from 'openid-client'; | ||
import { PersonRepository } from '../../person/persistence/person.repository.js'; | ||
import { KeycloakUserNotFoundError } from '../domain/keycloak-user-not-found.error.js'; | ||
import jwt from 'jsonwebtoken'; | ||
import { faker } from '@faker-js/faker'; | ||
|
||
describe('JWT Strategy', () => { | ||
it('should extract a bearer token from the header and return it for session storage', () => { | ||
it('should extract a bearer token from the header and return it for session storage', async () => { | ||
const client: DeepMocked<BaseClient> = createMock<Client>({ | ||
issuer: { metadata: { jwks_uri: 'https://nowhere.example.com' } }, | ||
}); | ||
const sut: JwtStrategy = new JwtStrategy(client); | ||
const personRepositoryMock: DeepMocked<PersonRepository> = createMock<PersonRepository>(); | ||
const sut: JwtStrategy = new JwtStrategy(client, personRepositoryMock); | ||
const request: Request = createMock<Request & { headers: { authorization: string } }>({ | ||
headers: { authorization: 'Bearer 12345' }, | ||
}); | ||
const sessionContent: { access_token: string } = sut.validate(request, ''); | ||
const sessionContent: { access_token: string } = await sut.validate(request, ''); | ||
|
||
expect(sessionContent.access_token).toEqual('12345'); | ||
}); | ||
|
||
it('should return empty string if no accessToken can be extracted', () => { | ||
it('should return empty string if no accessToken can be extracted', async () => { | ||
const client: DeepMocked<BaseClient> = createMock<Client>({ | ||
issuer: { metadata: { jwks_uri: 'https://nowhere.example.com' } }, | ||
}); | ||
const sut: JwtStrategy = new JwtStrategy(client); | ||
const personRepositoryMock: DeepMocked<PersonRepository> = createMock<PersonRepository>(); | ||
const sut: JwtStrategy = new JwtStrategy(client, personRepositoryMock); | ||
const request: Request = createMock<Request & { headers: { authorization: string } }>({ | ||
headers: { authorization: '' }, | ||
}); | ||
const sessionContent: { access_token: string } = sut.validate(request, ''); | ||
const sessionContent: { access_token: string } = await sut.validate(request, ''); | ||
|
||
expect(sessionContent.access_token).toEqual(''); | ||
}); | ||
|
||
it('should throw KeycloakUserNotFoundError if the kc user does not exist', async () => { | ||
const client: DeepMocked<BaseClient> = createMock<Client>({ | ||
issuer: { metadata: { jwks_uri: 'https://nowhere.example.com' } }, | ||
}); | ||
const personRepositoryMock: DeepMocked<PersonRepository> = createMock<PersonRepository>(); | ||
personRepositoryMock.findByKeycloakUserId.mockResolvedValueOnce(undefined); | ||
const sut: JwtStrategy = new JwtStrategy(client, personRepositoryMock); | ||
|
||
jest.spyOn(jwt, 'decode').mockReturnValue({ | ||
sub: faker.string.uuid().toString(), | ||
}); | ||
const request: Request = createMock<Request & { headers: { authorization: string } }>({ | ||
headers: { authorization: 'Bearer 12345' }, | ||
}); | ||
|
||
await expect(sut.validate(request, '')).rejects.toThrow(KeycloakUserNotFoundError); | ||
}); | ||
}); |
Oops, something went wrong.