Skip to content

Commit

Permalink
feat: implement cognito errors
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Jun 20, 2024
1 parent 943018b commit 6ed88a4
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 27 deletions.
5 changes: 4 additions & 1 deletion server/api/public/backdoor/controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { UserEntity } from 'api/@types/user';
import { userMethod } from 'domain/user/model/userMethod';
import { userCommand } from 'domain/user/repository/userCommand';
import { userQuery } from 'domain/user/repository/userQuery';
import { genCredentials } from 'domain/user/service/genCredentials';
import { genTokens } from 'domain/user/service/genTokens';
import { userPoolQuery } from 'domain/userPool/repository/userPoolQuery';
Expand All @@ -18,9 +19,11 @@ export default defineController(() => ({
username: body.username,
password: body.password,
});
const idCount = await userQuery.countId(prismaClient, body.username);
const user: UserEntity = {
...userMethod.createUser({
...userMethod.createUser(idCount, {
name: body.username,
password: body.password,
email: body.email,
salt,
verifier,
Expand Down
78 changes: 58 additions & 20 deletions server/domain/user/model/userMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,67 @@ import { calculateSignature } from 'domain/user/service/srp/calcSignature';
import { calculateSrpB } from 'domain/user/service/srp/calcSrpB';
import { getPoolName } from 'domain/user/service/srp/util';
import { brandedId } from 'service/brandedId';
import { cognitoAssert } from 'service/cognitoAssert';
import { ulid } from 'ulid';
import { z } from 'zod';

export const userMethod = {
createUser: (val: {
name: string;
email: string;
salt: string;
verifier: string;
userPoolId: EntityId['userPool'];
}): UserEntity => ({
id: brandedId.user.entity.parse(val.name),
email: val.email,
name: val.name,
refreshToken: ulid(),
userPoolId: val.userPoolId,
verified: false,
confirmationCode: genConfirmationCode(),
salt: val.salt,
verifier: val.verifier,
createdTime: Date.now(),
}),
createUser: (
idCount: number,
val: {
name: string;
password: string;
email: string;
salt: string;
verifier: string;
userPoolId: EntityId['userPool'];
},
): UserEntity => {
cognitoAssert(idCount === 0, 'User already exists');
cognitoAssert(
/^[a-z][a-z\d_-]/.test(val.name),
"1 validation error detected: Value at 'username' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\p{L}\\p{M}\\p{S}\\p{N}\\p{P}]+",
);
cognitoAssert(
val.password.length >= 8,
'Password did not conform with policy: Password not long enough',
);
cognitoAssert(
/[a-z]/.test(val.password),
'Password did not conform with policy: Password must have lowercase characters',
);
cognitoAssert(
/[A-Z]/.test(val.password),
'Password did not conform with policy: Password must have uppercase characters',
);
cognitoAssert(
/[0-9]/.test(val.password),
'Password did not conform with policy: Password must have numeric characters',
);
cognitoAssert(
/[!-/:-@[-`{-~]/.test(val.password),
'Password did not conform with policy: Password must have symbol characters',
);
cognitoAssert(z.string().email().parse(val.email), 'Invalid email address format.');

return {
id: brandedId.user.entity.parse(val.name),
email: val.email,
name: val.name,
refreshToken: ulid(),
userPoolId: val.userPoolId,
verified: false,
confirmationCode: genConfirmationCode(),
salt: val.salt,
verifier: val.verifier,
createdTime: Date.now(),
};
},
verifyUser: (user: UserEntity, confirmationCode: string): UserEntity => {
assert(user.confirmationCode === confirmationCode);
cognitoAssert(
user.confirmationCode === confirmationCode,
'Invalid verification code provided, please try again.',
);

return { ...user, verified: true };
},
Expand Down Expand Up @@ -91,7 +129,7 @@ export const userMethod = {
scramblingParameter,
key: sessionKey,
});
assert(signature === params.clientSignature);
cognitoAssert(signature === params.clientSignature, 'Incorrect username or password.');

return genTokens({
privateKey: params.pool.privateKey,
Expand Down
2 changes: 2 additions & 0 deletions server/domain/user/repository/userQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { UserEntity } from 'api/@types/user';
import { toUserEntity } from './toUserEntity';

export const userQuery = {
countId: (tx: Prisma.TransactionClient, id: string): Promise<number> =>
tx.user.count({ where: { id } }),
findById: (tx: Prisma.TransactionClient, id: EntityId['user']): Promise<UserEntity> =>
tx.user.findUniqueOrThrow({ where: { id } }).then(toUserEntity),
findByName: (tx: Prisma.TransactionClient, name: string): Promise<UserEntity> =>
Expand Down
11 changes: 8 additions & 3 deletions server/domain/user/useCase/authUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { genCredentials } from 'domain/user/service/genCredentials';
import { genTokens } from 'domain/user/service/genTokens';
import { userPoolQuery } from 'domain/userPool/repository/userPoolQuery';
import { jwtDecode } from 'jwt-decode';
import { cognitoAssert } from 'service/cognitoAssert';
import { transaction } from 'service/prismaClient';
import { sendMail } from 'service/sendMail';
import type { AccessTokenJwt } from 'service/types';
Expand All @@ -28,8 +29,10 @@ export const authUseCase = {
username: req.Username,
password: req.Password,
});
const user = userMethod.createUser({
const idCount = await userQuery.countId(tx, req.Username);
const user = userMethod.createUser(idCount, {
name: req.Username,
password: req.Password,
email: req.UserAttributes[0].Value,
salt,
verifier,
Expand Down Expand Up @@ -63,8 +66,8 @@ export const authUseCase = {
}),
userSrpAuth: (req: UserSrpAuthTarget['reqBody']): Promise<UserSrpAuthTarget['resBody']> =>
transaction(async (tx) => {
const user = await userQuery.findByName(tx, req.AuthParameters.USERNAME);
assert(user.verified);
const user = await userQuery.findByName(tx, req.AuthParameters.USERNAME).catch(() => null);
cognitoAssert(user, 'Incorrect username or password.');

const { userWithChallenge, ChallengeParameters } = userMethod.createChallenge(
user,
Expand Down Expand Up @@ -115,6 +118,8 @@ export const authUseCase = {
assert(pool.id === poolClient.userPoolId);
assert(user.challenge?.secretBlock === req.ChallengeResponses.PASSWORD_CLAIM_SECRET_BLOCK);

cognitoAssert(user.verified, 'User is not confirmed.');

const tokens = userMethod.srpAuth({
user,
timestamp: req.ChallengeResponses.TIMESTAMP,
Expand Down
26 changes: 26 additions & 0 deletions server/service/cognitoAssert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import assert from 'assert';

export const COGNITO_ERRORS = {
'Incorrect username or password.': 'NotAuthorizedException',
'Invalid email address format.': 'InvalidParameterException',
"1 validation error detected: Value at 'username' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\p{L}\\p{M}\\p{S}\\p{N}\\p{P}]+":
'InvalidParameterException',
'Password did not conform with policy: Password must have lowercase characters':
'InvalidPasswordException',
'Password did not conform with policy: Password must have uppercase characters':
'InvalidPasswordException',
'Password did not conform with policy: Password must have numeric characters':
'InvalidPasswordException',
'Password did not conform with policy: Password must have symbol characters':
'InvalidPasswordException',
'Password did not conform with policy: Password not long enough': 'InvalidPasswordException',
'User already exists': 'UsernameExistsException',
'Invalid verification code provided, please try again.': 'CodeMismatchException',
'User is not confirmed.': 'UserNotConfirmedException',
};

export class CognitoError extends Error {}

export function cognitoAssert(val: unknown, msg: keyof typeof COGNITO_ERRORS): asserts val {
assert(val, new CognitoError(msg));
}
22 changes: 21 additions & 1 deletion server/service/returnStatus.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { COGNITO_ERRORS, CognitoError } from './cognitoAssert';

export const returnSuccess = <T>(val: T): { status: 200; body: T } => ({ status: 200, body: val });

const logErr = (e: unknown): void => {
Expand All @@ -12,7 +14,25 @@ export const returnGetError = (e: unknown): { status: 404 } => {
return { status: 404 };
};

export const returnPostError = (e: unknown): { status: 403; body: Record<string, never> } => {
export const returnPostError = (
e: unknown,
):
| {
status: 400;
headers: Record<'X-Amzn-Errormessage' | 'X-Amzn-Errortype', string>;
body: { message: string; __type: string };
}
| { status: 403; body: Record<string, never> } => {
if (e instanceof CognitoError) {
const type = COGNITO_ERRORS[e.message as keyof typeof COGNITO_ERRORS];

return {
status: 400,
headers: { 'X-Amzn-Errormessage': e.message, 'X-Amzn-Errortype': type },
body: { message: e.message, __type: type },
};
}

logErr(e);

return { status: 403, body: {} };
Expand Down
2 changes: 1 addition & 1 deletion server/tests/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const createUserClient = async (): Promise<typeof noCookieClient> => {
body: {
username: 'test-client',
email: `${ulid()}@example.com`,
password: 'test-client-password',
password: 'Test-client-password1',
},
});
const agent = axios.create({
Expand Down
2 changes: 1 addition & 1 deletion server/tests/api/signIn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test('signIn', async () => {
const signature = calcClientSignature({
secretBlock,
username: 'test-client',
password: 'test-client-password',
password: 'Test-client-password1',
salt: res1.ChallengeParameters.SALT,
timestamp: 'Thu Jan 01 00:00:00 UTC 1970',
A: A.toString('hex'),
Expand Down

0 comments on commit 6ed88a4

Please sign in to comment.