Skip to content

Commit

Permalink
Merge pull request #4 from caru-ini/feat/change-password-server
Browse files Browse the repository at this point in the history
パスワード変更機能(サーバー)
  • Loading branch information
solufa authored Jul 2, 2024
2 parents 616af5c + 46a50ae commit dcd9172
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 0 deletions.
9 changes: 9 additions & 0 deletions server/api/@types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ export type ResendConfirmationCodeTarget = TargetBody<
export type ListUserPoolsTarget = TargetBody<ListUserPoolsRequest, ListUserPoolsResponse>;

export type AdminDeleteUserTarget = TargetBody<AdminDeleteUserRequest, Record<string, never>>;
export type ChangePasswordTarget = TargetBody<
{
AccessToken: string;
PreviousPassword: string;
ProposedPassword: string;
},
Record<string, never>
>;

export type AmzTargets = {
'AWSCognitoIdentityProviderService.SignUp': SignUpTarget;
Expand All @@ -128,4 +136,5 @@ export type AmzTargets = {
'AWSCognitoIdentityProviderService.ResendConfirmationCode': ResendConfirmationCodeTarget;
'AWSCognitoIdentityProviderService.ListUserPools': ListUserPoolsTarget;
'AWSCognitoIdentityProviderService.AdminDeleteUser': AdminDeleteUserTarget;
'AWSCognitoIdentityProviderService.ChangePassword': ChangePasswordTarget;
};
8 changes: 8 additions & 0 deletions server/api/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ const targets: {
validator: z.object({ UserPoolId: brandedId.userPool.maybe, Username: z.string() }),
useCase: adminUseCase.deleteUser,
},
'AWSCognitoIdentityProviderService.ChangePassword': {
validator: z.object({
AccessToken: z.string(),
PreviousPassword: z.string(),
ProposedPassword: z.string(),
}),
useCase: authUseCase.changePassword,
},
};

const main = <T extends keyof AmzTargets>(target: T, body: AmzTargets[T]['reqBody']) => {
Expand Down
7 changes: 7 additions & 0 deletions server/domain/user/model/userMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,11 @@ export const userMethod = {

return brandedId.deletableUser.entity.parse(params.user.id);
},
changePassword: (params: { user: UserEntity; salt: string; verifier: string }): UserEntity => ({
...params.user,
verifier: params.verifier,
salt: params.salt,
refreshToken: ulid(),
challenge: undefined,
}),
};
2 changes: 2 additions & 0 deletions server/domain/user/repository/userCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const userCommand = {
email: user.email,
name: user.name,
verified: user.verified,
salt: user.salt,
verifier: user.verifier,
refreshToken: user.refreshToken,
confirmationCode: user.confirmationCode,
secretBlock: user.challenge?.secretBlock,
Expand Down
16 changes: 16 additions & 0 deletions server/domain/user/useCase/authUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
ChangePasswordTarget,
ConfirmSignUpTarget,
GetUserTarget,
ListUserPoolsTarget,
Expand Down Expand Up @@ -170,4 +171,19 @@ export const authUseCase = {
const pools = await userPoolQuery.listAll(tx, req.MaxResults);
return { UserPools: pools.map((p) => ({ Id: p.id })) };
}),
changePassword: (
req: ChangePasswordTarget['reqBody'],
): Promise<ChangePasswordTarget['resBody']> =>
transaction(async (tx) => {
const decoded = jwtDecode<AccessTokenJwt>(req.AccessToken);
const user = await userQuery.findById(tx, decoded.sub);
const { salt, verifier } = genCredentials({
poolId: user.userPoolId,
username: user.name,
password: req.ProposedPassword,
});

await userCommand.save(tx, userMethod.changePassword({ user, salt, verifier }));
return {};
}),
};
110 changes: 110 additions & 0 deletions server/tests/api/changePassword.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import assert from 'assert';
import crypto from 'crypto';
import { calcClientSignature } from 'domain/user/service/srp/calcClientSignature';
import { N, g } from 'domain/user/service/srp/constants';
import { fromBuffer, toBuffer } from 'domain/user/service/srp/util';
import { DEFAULT_USER_POOL_CLIENT_ID } from 'service/envValues';
import { test } from 'vitest';
import {
createUserClient,
deleteUser,
noCookieClient,
testPassword,
testUserName,
} from './apiClient';

test('changePassword', async () => {
await createUserClient();
const a = crypto.randomBytes(32);
const A = toBuffer(g.modPow(fromBuffer(a), N));
const res1 = await noCookieClient.$post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.InitiateAuth' },
body: {
AuthFlow: 'USER_SRP_AUTH',
AuthParameters: { USERNAME: testUserName, SRP_A: A.toString('hex') },
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
},
});

assert('ChallengeParameters' in res1);
const secretBlock1 = res1.ChallengeParameters.SECRET_BLOCK;
const signature1 = calcClientSignature({
secretBlock: secretBlock1,
username: testUserName,
password: testPassword,
salt: res1.ChallengeParameters.SALT,
timestamp: 'Thu Jan 01 00:00:00 UTC 1970',
A: A.toString('hex'),
a: fromBuffer(a),
B: res1.ChallengeParameters.SRP_B,
});

const res2 = await noCookieClient.$post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.RespondToAuthChallenge' },
body: {
ChallengeName: 'PASSWORD_VERIFIER',
ChallengeResponses: {
PASSWORD_CLAIM_SECRET_BLOCK: secretBlock1,
PASSWORD_CLAIM_SIGNATURE: signature1,
TIMESTAMP: 'Thu Jan 01 00:00:00 UTC 1970',
USERNAME: testUserName,
},
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
},
});

assert('AuthenticationResult' in res2);
assert('RefreshToken' in res2.AuthenticationResult);

const newPassword = 'Test-client-password2';

await noCookieClient.$post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.ChangePassword' },
body: {
AccessToken: res2.AuthenticationResult.AccessToken,
PreviousPassword: testPassword,
ProposedPassword: newPassword,
},
});

const res3 = await noCookieClient.$post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.InitiateAuth' },
body: {
AuthFlow: 'USER_SRP_AUTH',
AuthParameters: { USERNAME: testUserName, SRP_A: A.toString('hex') },
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
},
});

assert('ChallengeParameters' in res3);
const secretBlock2 = res3.ChallengeParameters.SECRET_BLOCK;
const signature2 = calcClientSignature({
secretBlock: secretBlock2,
username: testUserName,
password: newPassword,
salt: res3.ChallengeParameters.SALT,
timestamp: 'Thu Jan 01 00:00:00 UTC 1970',
A: A.toString('hex'),
a: fromBuffer(a),
B: res3.ChallengeParameters.SRP_B,
});

const res4 = await noCookieClient.$post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.RespondToAuthChallenge' },
body: {
ChallengeName: 'PASSWORD_VERIFIER',
ChallengeResponses: {
PASSWORD_CLAIM_SECRET_BLOCK: secretBlock2,
PASSWORD_CLAIM_SIGNATURE: signature2,
TIMESTAMP: 'Thu Jan 01 00:00:00 UTC 1970',
USERNAME: testUserName,
},
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
},
});

assert('AuthenticationResult' in res4);
assert('RefreshToken' in res4.AuthenticationResult);

await deleteUser();
});

0 comments on commit dcd9172

Please sign in to comment.