Skip to content

Commit

Permalink
feat: adopt otplib
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Dec 20, 2024
1 parent 84e73ac commit 999706f
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 2 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-alpine AS builder
FROM node:20-alpine3.20 AS builder

WORKDIR /usr/src/app

Expand All @@ -21,7 +21,7 @@ RUN npm run batch:writeVersion -- $VERSION
RUN npm run build
RUN npm ci --omit=dev --prefix server

FROM node:20-alpine
FROM node:20-alpine3.20

WORKDIR /usr/src/app

Expand Down
3 changes: 3 additions & 0 deletions server/api/controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'assert';
import { adminUseCase } from 'domain/user/useCase/adminUseCase';
import { authUseCase } from 'domain/user/useCase/authUseCase';
import { mfaUseCase } from 'domain/user/useCase/mfaUseCase';
import { signInUseCase } from 'domain/user/useCase/signInUseCase';
import { signUpUseCase } from 'domain/user/useCase/signUpUseCase';
import { userPoolUseCase } from 'domain/userPool/useCase/userPoolUseCase';
Expand Down Expand Up @@ -38,6 +39,8 @@ const useCases: {
'AWSCognitoIdentityProviderService.UpdateUserAttributes': authUseCase.updateUserAttributes,
'AWSCognitoIdentityProviderService.VerifyUserAttribute': authUseCase.verifyUserAttribute,
'AWSCognitoIdentityProviderService.DeleteUserAttributes': authUseCase.deleteUserAttributes,
'AWSCognitoIdentityProviderService.AssociateSoftwareToken': mfaUseCase.associateSoftwareToken,
'AWSCognitoIdentityProviderService.VerifySoftwareToken': mfaUseCase.verifySoftwareToken,
};

const main = <T extends keyof AmzTargets>(target: T, body: AmzTargets[T]['reqBody']) => {
Expand Down
16 changes: 16 additions & 0 deletions server/common/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type {
AdminSetUserPasswordResponse,
AdminUpdateUserAttributesRequest,
AdminUpdateUserAttributesResponse,
AssociateSoftwareTokenRequest,
AssociateSoftwareTokenResponse,
CodeDeliveryDetailsType,
DeleteUserAttributesRequest,
DeleteUserAttributesResponse,
Expand All @@ -24,6 +26,8 @@ import type {
SignUpResponse,
UpdateUserAttributesRequest,
UpdateUserAttributesResponse,
VerifySoftwareTokenRequest,
VerifySoftwareTokenResponse,
VerifyUserAttributeRequest,
VerifyUserAttributeResponse,
} from '@aws-sdk/client-cognito-identity-provider';
Expand Down Expand Up @@ -116,6 +120,16 @@ export type DeleteUserAttributesTarget = TargetBody<
DeleteUserAttributesResponse
>;

export type AssociateSoftwareTokenTarget = TargetBody<
AssociateSoftwareTokenRequest,
AssociateSoftwareTokenResponse
>;

export type VerifySoftwareTokenTarget = TargetBody<
VerifySoftwareTokenRequest,
VerifySoftwareTokenResponse
>;

export type AmzTargets = {
'AWSCognitoIdentityProviderService.SignUp': SignUpTarget;
'AWSCognitoIdentityProviderService.ConfirmSignUp': ConfirmSignUpTarget;
Expand All @@ -139,4 +153,6 @@ export type AmzTargets = {
'AWSCognitoIdentityProviderService.UpdateUserAttributes': UpdateUserAttributesTarget;
'AWSCognitoIdentityProviderService.VerifyUserAttribute': VerifyUserAttributeTarget;
'AWSCognitoIdentityProviderService.DeleteUserAttributes': DeleteUserAttributesTarget;
'AWSCognitoIdentityProviderService.AssociateSoftwareToken': AssociateSoftwareTokenTarget;
'AWSCognitoIdentityProviderService.VerifySoftwareToken': VerifySoftwareTokenTarget;
};
2 changes: 2 additions & 0 deletions server/common/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type SocialUserEntity = {
createdTime: number;
updatedTime: number;
challenge?: undefined;
totpSecretCode?: undefined;
};

export type CognitoUserEntity = {
Expand All @@ -57,6 +58,7 @@ export type CognitoUserEntity = {
createdTime: number;
updatedTime: number;
challenge?: ChallengeVal;
totpSecretCode?: string;
};

export type UserEntity = SocialUserEntity | CognitoUserEntity;
Expand Down
17 changes: 17 additions & 0 deletions server/domain/user/model/mfaMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { CognitoUserEntity } from 'common/types/user';
import { authenticator } from 'otplib';
import { cognitoAssert } from 'service/cognitoAssert';

export const mfaMethod = {
generateSecretCode: (user: CognitoUserEntity): CognitoUserEntity => {
return { ...user, totpSecretCode: authenticator.generateSecret() };
},
verify: (user: CognitoUserEntity, userCode: string | undefined): CognitoUserEntity => {
cognitoAssert(
userCode && user.totpSecretCode && authenticator.check(userCode, user.totpSecretCode),
'Invalid verification code provided, please try again.',
);

return user;
},
};
1 change: 1 addition & 0 deletions server/domain/user/repository/toUserEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const toCognitoUserEntity = (
value: attr.value,
}),
),
totpSecretCode: prismaUser.totpSecretCode ?? undefined,
createdTime: prismaUser.createdAt.getTime(),
updatedTime: prismaUser.updatedAt.getTime(),
};
Expand Down
2 changes: 2 additions & 0 deletions server/domain/user/repository/userCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const userCommand = {
pubA: user.challenge?.pubA,
pubB: user.challenge?.pubB,
secB: user.challenge?.secB,
totpSecretCode: user.totpSecretCode,
attributes: { createMany: { data: user.attributes } },
updatedAt: new Date(user.updatedTime),
},
Expand All @@ -44,6 +45,7 @@ export const userCommand = {
confirmationCode: user.confirmationCode,
authorizationCode: user.authorizationCode,
codeChallenge: user.codeChallenge,
totpSecretCode: user.totpSecretCode,
userPoolId: user.userPoolId,
attributes: { createMany: { data: user.attributes } },
createdAt: new Date(user.createdTime),
Expand Down
46 changes: 46 additions & 0 deletions server/domain/user/useCase/mfaUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { VerifySoftwareTokenResponseType } from '@aws-sdk/client-cognito-identity-provider';
import assert from 'assert';
import type { AssociateSoftwareTokenTarget, VerifySoftwareTokenTarget } from 'common/types/auth';
import { jwtDecode } from 'jwt-decode';
import { transaction } from 'service/prismaClient';
import type { AccessTokenJwt } from 'service/types';
import { mfaMethod } from '../model/mfaMethod';
import { userCommand } from '../repository/userCommand';
import { userQuery } from '../repository/userQuery';

export const mfaUseCase = {
associateSoftwareToken: (
req: AssociateSoftwareTokenTarget['reqBody'],
): Promise<AssociateSoftwareTokenTarget['resBody']> =>
transaction(async (tx) => {
assert(req.AccessToken);

const decoded = jwtDecode<AccessTokenJwt>(req.AccessToken);
const user = await userQuery.findById(tx, decoded.sub);

assert(user.kind === 'cognito');

const updated = mfaMethod.generateSecretCode(user);

await userCommand.save(tx, updated);

return { SecretCode: updated.totpSecretCode, Session: req.Session };
}),
verifySoftwareToken: (
req: VerifySoftwareTokenTarget['reqBody'],
): Promise<VerifySoftwareTokenTarget['resBody']> =>
transaction(async (tx) => {
assert(req.AccessToken);

const decoded = jwtDecode<AccessTokenJwt>(req.AccessToken);
const user = await userQuery.findById(tx, decoded.sub);

assert(user.kind === 'cognito');

const updated = mfaMethod.verify(user, req.UserCode);

await userCommand.save(tx, updated);

return { Status: VerifySoftwareTokenResponseType.SUCCESS, Session: req.Session };
}),
};
118 changes: 118 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"local-ssl-proxy": "^2.0.5",
"node-jose": "^2.2.0",
"nodemailer": "^6.9.15",
"otplib": "^12.0.1",
"prisma": "^5.19.1",
"serve": "^14.2.3",
"ulid": "^2.3.0",
Expand Down
2 changes: 2 additions & 0 deletions server/prisma/migrations/20240920051801_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "totpSecretCode" TEXT;
1 change: 1 addition & 0 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ model User {
pubA String?
pubB String?
secB String?
totpSecretCode String?
attributes UserAttribute[]
UserPool UserPool @relation(fields: [userPoolId], references: [id])
userPoolId String
Expand Down
Loading

0 comments on commit 999706f

Please sign in to comment.