Skip to content

Commit

Permalink
Merge pull request #7 from caru-ini/feat/forgot-password-server
Browse files Browse the repository at this point in the history
パスワードリセット機能の実装
  • Loading branch information
solufa authored Jul 9, 2024
2 parents 2699a1d + 23e205f commit 1c5991a
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 0 deletions.
20 changes: 20 additions & 0 deletions server/api/@types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,24 @@ export type ChangePasswordTarget = TargetBody<
Record<string, never>
>;

export type ForgotPasswordTarget = TargetBody<
{
ClientId: MaybeId['userPoolClient'];
Username: string;
},
{ CodeDeliveryDetails: CodeDeliveryDetails }
>;

export type ConfirmForgotPasswordTarget = TargetBody<
{
ClientId: MaybeId['userPoolClient'];
ConfirmationCode: string;
Password: string;
Username: string;
},
Record<string, never>
>;

export type AmzTargets = {
'AWSCognitoIdentityProviderService.SignUp': SignUpTarget;
'AWSCognitoIdentityProviderService.ConfirmSignUp': ConfirmSignUpTarget;
Expand All @@ -151,4 +169,6 @@ export type AmzTargets = {
'AWSCognitoIdentityProviderService.AdminDeleteUser': AdminDeleteUserTarget;
'AWSCognitoIdentityProviderService.AdminInitiateAuth': AdminInitiateAuthTarget;
'AWSCognitoIdentityProviderService.ChangePassword': ChangePasswordTarget;
'AWSCognitoIdentityProviderService.ForgotPassword': ForgotPasswordTarget;
'AWSCognitoIdentityProviderService.ConfirmForgotPassword': ConfirmForgotPasswordTarget;
};
16 changes: 16 additions & 0 deletions server/api/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ const targets: {
}),
useCase: authUseCase.changePassword,
},
'AWSCognitoIdentityProviderService.ForgotPassword': {
validator: z.object({
ClientId: brandedId.userPoolClient.maybe,
Username: z.string(),
}),
useCase: authUseCase.forgotPassword,
},
'AWSCognitoIdentityProviderService.ConfirmForgotPassword': {
validator: z.object({
ClientId: brandedId.userPoolClient.maybe,
ConfirmationCode: z.string(),
Password: z.string(),
Username: z.string(),
}),
useCase: authUseCase.confirmForgotPassword,
},
};

const main = <T extends keyof AmzTargets>(target: T, body: AmzTargets[T]['reqBody']) => {
Expand Down
27 changes: 27 additions & 0 deletions server/domain/user/model/userMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,31 @@ export const userMethod = {
challenge: undefined,
};
},
forgotPassword: (user: UserEntity): UserEntity => {
const confirmationCode = genConfirmationCode();

return { ...user, confirmationCode };
},
confirmForgotPassword: (params: {
user: UserEntity;
confirmationCode: string;
password: string;
}): UserEntity => {
const { user, confirmationCode } = params;
cognitoAssert(
user.confirmationCode === confirmationCode,
'Invalid verification code provided, please try again.',
);
validatePass(params.password);

return {
...user,
...genCredentials({
poolId: user.userPoolId,
username: user.name,
password: params.password,
}),
confirmationCode: '',
};
},
};
34 changes: 34 additions & 0 deletions server/domain/user/useCase/authUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable max-lines */
import type {
ChangePasswordTarget,
ConfirmForgotPasswordTarget,
ConfirmSignUpTarget,
ForgotPasswordTarget,
GetUserTarget,
ListUserPoolsTarget,
RefreshTokenAuthTarget,
Expand Down Expand Up @@ -172,6 +175,37 @@ export const authUseCase = {

await userCommand.save(tx, userMethod.changePassword({ user, req }));

return {};
}),
forgotPassword: (
req: ForgotPasswordTarget['reqBody'],
): Promise<ForgotPasswordTarget['resBody']> =>
transaction(async (tx) => {
const poolClient = await userPoolQuery.findClientById(tx, req.ClientId);
const user = await userQuery.findByName(tx, req.Username);
assert(poolClient.userPoolId === user.userPoolId);

const forgotUser = userMethod.forgotPassword(user);
await userCommand.save(tx, forgotUser);
await sendConfirmationCode(forgotUser);

return { CodeDeliveryDetails: genCodeDeliveryDetails(forgotUser) };
}),
confirmForgotPassword: (
req: ConfirmForgotPasswordTarget['reqBody'],
): Promise<ConfirmForgotPasswordTarget['resBody']> =>
transaction(async (tx) => {
const user = await userQuery.findByName(tx, req.Username);

await userCommand.save(
tx,
userMethod.confirmForgotPassword({
user,
confirmationCode: req.ConfirmationCode,
password: req.Password,
}),
);

return {};
}),
};
72 changes: 72 additions & 0 deletions server/tests/api/forgotPassword.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import assert from 'assert';
import { InbucketAPIClient } from 'inbucket-js-client';
import { DEFAULT_USER_POOL_CLIENT_ID } from 'service/envValues';
import { ulid } from 'ulid';
import { expect, test } from 'vitest';
import { deleteUser, noCookieClient, testUserName } from './apiClient';

test('ForgotPassword', async () => {
const email = `${ulid()}@example.com`;

await noCookieClient.post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.SignUp' },
body: {
Username: testUserName,
Password: 'Test-client-password2',
UserAttributes: [{ Name: 'email', Value: email }],
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
},
});

await noCookieClient.post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.ResendConfirmationCode' },
body: { ClientId: DEFAULT_USER_POOL_CLIENT_ID, Username: testUserName },
});

assert(process.env.INBUCKET_URL);

const inbucketClient = new InbucketAPIClient(process.env.INBUCKET_URL);
const inbox = await inbucketClient.mailbox(email);
const message = await inbucketClient.message(email, inbox[0].id);
const res1 = await noCookieClient.post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.ConfirmSignUp' },
body: {
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
ConfirmationCode: message.body.text.trim().split(' ').at(-1) ?? '',
Username: testUserName,
},
});

expect(res1.status).toBe(200);

await inbucketClient.deleteMessage(email, inbox[0].id);
await inbucketClient.deleteMessage(email, inbox[1].id);

const res2 = await noCookieClient.$post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.ForgotPassword' },
body: {
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
Username: testUserName,
},
});
assert('CodeDeliveryDetails' in res2);

const inbox2 = await inbucketClient.mailbox(email);
const message2 = await inbucketClient.message(email, inbox2[0].id);

const res3 = await noCookieClient.post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.ConfirmForgotPassword' },
body: {
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
ConfirmationCode: message2.body.text.trim().split(' ').at(-1) ?? '',
Password: 'Test-client-password2',
Username: testUserName,
},
});

await inbucketClient.deleteMessage(email, inbox2[0].id);

expect(res3.status).toBe(200);

await deleteUser();
});

0 comments on commit 1c5991a

Please sign in to comment.