Skip to content

Commit

Permalink
Merge pull request #318 from conceptadev/release/5.1.0
Browse files Browse the repository at this point in the history
Release/5.1.0
  • Loading branch information
MrMaz authored Dec 7, 2024
2 parents a95d6aa + 8692fa5 commit 98cc122
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('AuthLocalController (e2e)', () => {
})
.then((response) => {
expect(response.body.message).toBe(
'The provided credentials are incorrect. Please try again.',
'The provided username or password is incorrect. Please try again.',
);
expect(response.status).toBe(401);
});
Expand Down
66 changes: 58 additions & 8 deletions packages/nestjs-auth-local/src/auth-local.strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { randomUUID } from 'crypto';
import { mock } from 'jest-mock-extended';
import { PasswordValidationService } from '@concepta/nestjs-password';
import { BadRequestException } from '@nestjs/common';
import { BadRequestException, HttpStatus } from '@nestjs/common';
import { AuthLocalStrategy } from './auth-local.strategy';
import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.interface';
import { AuthLocalUserLookupServiceInterface } from './interfaces/auth-local-user-lookup-service.interface';
Expand All @@ -11,8 +11,9 @@ import { AuthLocalValidateUserService } from './services/auth-local-validate-use
import { UserFixture } from './__fixtures__/user/user.entity.fixture';
import { ReferenceIdInterface } from '@concepta/ts-core';
import { AuthLocalValidateUserInterface } from './interfaces/auth-local-validate-user.interface';
import { InvalidCredentialsException } from './exceptions/invalid-credentials.exception';
import { InvalidLoginDataException } from './exceptions/invalid-login-data.exception';
import { AuthLocalException } from './exceptions/auth-local.exception';
import { AuthLocalInvalidCredentialsException } from './exceptions/auth-local-invalid-credentials.exception';
import { AuthLocalInvalidLoginDataException } from './exceptions/auth-local-invalid-login-data.exception';

describe(AuthLocalStrategy.name, () => {
const USERNAME = 'username';
Expand Down Expand Up @@ -43,6 +44,7 @@ describe(AuthLocalStrategy.name, () => {
user = new UserFixture();
user.id = randomUUID();
user.active = true;
jest.resetAllMocks();
jest.spyOn(userLookUpService, 'byUsername').mockResolvedValue(user);
});

Expand All @@ -64,15 +66,63 @@ describe(AuthLocalStrategy.name, () => {
expect(result.id).toBe(user.id);
});

it('should return user', async () => {
it('should fail to validate user', async () => {
jest
.spyOn(validateUserService, 'validateUser')
.mockImplementationOnce((_dto: AuthLocalValidateUserInterface) => {
return null as unknown as Promise<ReferenceIdInterface<string>>;
});

const t = () => authLocalStrategy.validate(USERNAME, PASSWORD);
await expect(t).rejects.toThrow(InvalidCredentialsException);
await expect(t).rejects.toThrow(AuthLocalInvalidCredentialsException);
});

it('should fail to validate user with custom message', async () => {
jest
.spyOn(validateUserService, 'validateUser')
.mockImplementation((_dto: AuthLocalValidateUserInterface) => {
throw new AuthLocalInvalidCredentialsException({
message: 'Custom message',
safeMessage: 'Custom safe message',
});
});

const t = () => authLocalStrategy.validate(USERNAME, PASSWORD);

try {
await t();
} catch (error: unknown) {
if (error instanceof AuthLocalInvalidCredentialsException) {
expect(error.httpStatus).toBe(HttpStatus.UNAUTHORIZED);
expect(error.message).toBe('Custom message');
expect(error.safeMessage).toBe('Custom safe message');
} else {
throw new Error('Wrong error type');
}
}
});

it('should fail with internal server error', async () => {
jest
.spyOn(validateUserService, 'validateUser')
.mockImplementation((_dto: AuthLocalValidateUserInterface) => {
throw new Error('This is really bad');
});

const t = () => authLocalStrategy.validate(USERNAME, PASSWORD);

try {
await t();
} catch (error: unknown) {
if (error instanceof AuthLocalException) {
expect(error?.httpStatus).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(error?.context?.originalError?.message).toBe(
'This is really bad',
);
} else {
throw new Error('Wrong error type');
}
}
});

it('should throw error on validateOrReject', async () => {
Expand All @@ -87,14 +137,14 @@ describe(AuthLocalStrategy.name, () => {
.mockRejectedValueOnce(BadRequestException);

const t = () => authLocalStrategy.validate(USERNAME, PASSWORD);
await expect(t).rejects.toThrow(InvalidLoginDataException);
await expect(t).rejects.toThrow(AuthLocalInvalidLoginDataException);
});

it('should return no user on userLookupService.byUsername', async () => {
jest.spyOn(userLookUpService, 'byUsername').mockResolvedValue(null);

const t = () => authLocalStrategy.validate(USERNAME, PASSWORD);
await expect(t).rejects.toThrow(InvalidCredentialsException);
await expect(t).rejects.toThrow(AuthLocalInvalidCredentialsException);
});

it('should be invalid on passwordService.validateObject', async () => {
Expand All @@ -103,7 +153,7 @@ describe(AuthLocalStrategy.name, () => {
.mockResolvedValue(false);

const t = () => authLocalStrategy.validate(USERNAME, PASSWORD);
await expect(t).rejects.toThrow(InvalidCredentialsException);
await expect(t).rejects.toThrow(AuthLocalInvalidCredentialsException);
});
});

Expand Down
44 changes: 26 additions & 18 deletions packages/nestjs-auth-local/src/auth-local.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import {

import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.interface';
import { AuthLocalValidateUserServiceInterface } from './interfaces/auth-local-validate-user-service.interface';
import { InvalidCredentialsException } from './exceptions/invalid-credentials.exception';
import { InvalidLoginDataException } from './exceptions/invalid-login-data.exception';
import { AuthLocalException } from './exceptions/auth-local.exception';
import { AuthLocalInvalidCredentialsException } from './exceptions/auth-local-invalid-credentials.exception';
import { AuthLocalInvalidLoginDataException } from './exceptions/auth-local-invalid-login-data.exception';
import { AuthLocalMissingLoginDtoException } from './exceptions/auth-local-missing-login-dto.exception';
import { AuthLocalMissingUsernameFieldException } from './exceptions/auth-local-missing-username-field.exception';
import { AuthLocalMissingPasswordFieldException } from './exceptions/auth-local-missing-password-field.exception';

/**
* Define the Local strategy using passport.
Expand Down Expand Up @@ -61,7 +65,7 @@ export class AuthLocalStrategy extends PassportStrategyFactory<Strategy>(
try {
await validateOrReject(dto);
} catch (e) {
throw new InvalidLoginDataException({
throw new AuthLocalInvalidLoginDataException({
originalError: e,
});
}
Expand All @@ -74,12 +78,23 @@ export class AuthLocalStrategy extends PassportStrategyFactory<Strategy>(
username,
password,
});
// did we get a valid user?
if (!validatedUser) {
throw new Error(`No valid user found: ${username}`);
}
} catch (e) {
throw new InvalidCredentialsException({ originalError: e });
// did they throw an invalid credentials exception?
if (e instanceof AuthLocalInvalidCredentialsException) {
// yes, use theirs
throw e;
} else {
// something else went wrong
throw new AuthLocalException({ originalError: e });
}
}

// did we get a valid user?
if (!validatedUser) {
throw new AuthLocalInvalidCredentialsException({
message: `Unable to validate user with username: %s`,
messageParams: [username],
});
}

return validatedUser;
Expand All @@ -93,24 +108,17 @@ export class AuthLocalStrategy extends PassportStrategyFactory<Strategy>(

// is the login dto missing?
if (!loginDto) {
// TODO: Change Error to a Exception
throw new Error('Login DTO is required, did someone remove the default?');
throw new AuthLocalMissingLoginDtoException();
}

// is the username field missing?
if (!usernameField) {
// TODO: Change Error to a Exception
throw new Error(
'Login username field is required, did someone remove the default?',
);
throw new AuthLocalMissingUsernameFieldException();
}

// is the password field missing?
if (!passwordField) {
// TODO: Change Error to a Exception
throw new Error(
'Login password field is required, did someone remove the default?',
);
throw new AuthLocalMissingPasswordFieldException();
}

return { loginDto, usernameField, passwordField };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { AuthLocalUnauthorizedException } from './auth-local-unauthorized.exception';

export class AuthLocalInvalidCredentialsException extends AuthLocalUnauthorizedException {
constructor(options?: Omit<RuntimeExceptionOptions, 'httpStatus'>) {
super({
safeMessage:
'The provided username or password is incorrect. Please try again.',
...options,
});

this.errorCode = 'AUTH_LOCAL_INVALID_CREDENTIALS_ERROR';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
RuntimeExceptionOptions,
} from '@concepta/nestjs-exception';

export class InvalidLoginDataException extends RuntimeException {
export class AuthLocalInvalidLoginDataException extends RuntimeException {
constructor(options?: RuntimeExceptionOptions) {
super({
message:
message: 'Data validation error occurred before user validation.',
safeMessage:
'The provided username or password is incorrect. Please try again.',
httpStatus: HttpStatus.BAD_REQUEST,
...options,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { AuthLocalInvalidCredentialsException } from './auth-local-invalid-credentials.exception';

export class AuthLocalInvalidPasswordException extends AuthLocalInvalidCredentialsException {
constructor(userName: string, options?: RuntimeExceptionOptions) {
super({
message: `Invalid password for username: %s`,
messageParams: [userName],
...options,
});

this.errorCode = 'AUTH_LOCAL_INVALID_PASSWORD_ERROR';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { HttpStatus } from '@nestjs/common';
import { AuthLocalException } from './auth-local.exception';

export class AuthLocalMissingLoginDtoException extends AuthLocalException {
constructor(options?: RuntimeExceptionOptions) {
super({
message: 'Login DTO is required, did someone remove the default?',
httpStatus: HttpStatus.BAD_REQUEST,
...options,
});

this.errorCode = 'AUTH_LOCAL_MISSING_LOGIN_DTO_ERROR';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { HttpStatus } from '@nestjs/common';
import { AuthLocalException } from './auth-local.exception';

export class AuthLocalMissingPasswordFieldException extends AuthLocalException {
constructor(options?: RuntimeExceptionOptions) {
super({
message:
'Login password field is required, did someone remove the default?',
httpStatus: HttpStatus.BAD_REQUEST,
...options,
});

this.errorCode = 'AUTH_LOCAL_MISSING_PASSWORD_FIELD_ERROR';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { HttpStatus } from '@nestjs/common';
import { AuthLocalException } from './auth-local.exception';

export class AuthLocalMissingUsernameFieldException extends AuthLocalException {
constructor(options?: RuntimeExceptionOptions) {
super({
message:
'Login username field is required, did someone remove the default?',
httpStatus: HttpStatus.BAD_REQUEST,
...options,
});

this.errorCode = 'AUTH_LOCAL_MISSING_USERNAME_FIELD_ERROR';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { HttpStatus } from '@nestjs/common';
import { AuthLocalException } from './auth-local.exception';

export class AuthLocalUnauthorizedException extends AuthLocalException {
constructor(options?: Omit<RuntimeExceptionOptions, 'httpStatus'>) {
super({
message: 'Unauthorized',
safeMessage: 'Unauthorized',
...options,
httpStatus: HttpStatus.UNAUTHORIZED,
});

this.errorCode = 'AUTH_LOCAL_UNAUTHORIZED_ERROR';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { AuthLocalInvalidCredentialsException } from './auth-local-invalid-credentials.exception';

export class AuthLocalUserInactiveException extends AuthLocalInvalidCredentialsException {
constructor(userName: string, options?: RuntimeExceptionOptions) {
super({
message: `User with username '%s' is inactive`,
messageParams: [userName],
...options,
});

this.errorCode = 'AUTH_LOCAL_USER_INACTIVE_ERROR';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { AuthLocalInvalidCredentialsException } from './auth-local-invalid-credentials.exception';

export class AuthLocalUsernameNotFoundException extends AuthLocalInvalidCredentialsException {
constructor(userName: string, options?: RuntimeExceptionOptions) {
super({
message: `No user found for username: %s`,
messageParams: [userName],
...options,
});

this.errorCode = 'AUTH_LOCAL_USERNAME_NOT_FOUND_ERROR';
}
}
14 changes: 14 additions & 0 deletions packages/nestjs-auth-local/src/exceptions/auth-local.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
RuntimeException,
RuntimeExceptionOptions,
} from '@concepta/nestjs-exception';

/**
* Generic auth local exception.
*/
export class AuthLocalException extends RuntimeException {
constructor(options?: RuntimeExceptionOptions) {
super(options);
this.errorCode = 'AUTH_LOCAL_ERROR';
}
}

This file was deleted.

7 changes: 7 additions & 0 deletions packages/nestjs-auth-local/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ export {
AuthLocalGuard,
AuthLocalGuard as LocalAuthGuard,
} from './auth-local.guard';

export { AuthLocalException } from './exceptions/auth-local.exception';
export { AuthLocalInvalidLoginDataException } from './exceptions/auth-local-invalid-login-data.exception';
export { AuthLocalInvalidCredentialsException } from './exceptions/auth-local-invalid-credentials.exception';
export { AuthLocalInvalidPasswordException } from './exceptions/auth-local-invalid-password.exception';
export { AuthLocalUserInactiveException } from './exceptions/auth-local-user-inactive.exception';
export { AuthLocalUsernameNotFoundException } from './exceptions/auth-local-username-not-found.exception';
Loading

0 comments on commit 98cc122

Please sign in to comment.