diff --git a/server/domain/user/model/adminMethod.ts b/server/domain/user/model/adminMethod.ts index 77f5321..55d249a 100644 --- a/server/domain/user/model/adminMethod.ts +++ b/server/domain/user/model/adminMethod.ts @@ -33,28 +33,21 @@ export const adminMethod = { status: 'FORCE_CHANGE_PASSWORD', }; }, - deleteUser: (params: { user: UserEntity; userPoolId: string }): EntityId['deletableUser'] => { - assert(params.user.userPoolId === params.userPoolId); + deleteUser: (user: UserEntity, userPoolId: string): EntityId['deletableUser'] => { + assert(user.userPoolId === userPoolId); - return brandedId.deletableUser.entity.parse(params.user.id); + return brandedId.deletableUser.entity.parse(user.id); }, - setUserPassword: (params: { - user: UserEntity; - req: AdminSetUserPasswordTarget['reqBody']; - }): UserEntity => { - assert(params.req.UserPoolId); - assert(params.req.Password); - validatePass(params.req.Password); + setUserPassword: (user: UserEntity, req: AdminSetUserPasswordTarget['reqBody']): UserEntity => { + assert(req.UserPoolId); + assert(req.Password); + validatePass(req.Password); return { - ...params.user, - ...genCredentials({ - poolId: params.user.userPoolId, - username: params.user.name, - password: params.req.Password, - }), - status: 'CONFIRMED', - password: params.req.Password, + ...user, + ...genCredentials({ poolId: user.userPoolId, username: user.name, password: req.Password }), + status: req.Permanent ? 'CONFIRMED' : 'FORCE_CHANGE_PASSWORD', + password: req.Password, refreshToken: ulid(), challenge: undefined, updatedTime: Date.now(), diff --git a/server/domain/user/service/createAttributes.ts b/server/domain/user/service/createAttributes.ts index 2a52709..c9ac59f 100644 --- a/server/domain/user/service/createAttributes.ts +++ b/server/domain/user/service/createAttributes.ts @@ -3,6 +3,7 @@ import type { UserAttributeEntity, UserEntity } from 'common/types/user'; import { brandedId } from 'service/brandedId'; import { ulid } from 'ulid'; import { z } from 'zod'; +import { isEmailVerified } from './isEmailVerified'; export const COMPUTED_ATTRIBUTE_NAMES = ['sub', 'email', 'email_verified', 'updated_at'] as const; @@ -28,7 +29,7 @@ export const toAttributeTypes = (user: UserEntity): AttributeType[] => { return [ { Name: 'sub', Value: user.id }, { Name: 'email', Value: user.email }, - { Name: 'email_verified', Value: user.status === 'CONFIRMED' ? 'true' : 'false' }, + { Name: 'email_verified', Value: isEmailVerified(user) ? 'true' : 'false' }, { Name: 'updated_at', Value: Math.floor(user.updatedTime / 1000).toString() }, ...user.attributes.map((attr) => ({ Name: attr.name, Value: attr.value })), ]; diff --git a/server/domain/user/service/genTokens.ts b/server/domain/user/service/genTokens.ts index ad534e7..e633c9d 100644 --- a/server/domain/user/service/genTokens.ts +++ b/server/domain/user/service/genTokens.ts @@ -6,6 +6,7 @@ import { EXPIRES_SEC } from 'service/constants'; import { PORT } from 'service/envValues'; import type { AccessTokenJwt, IdTokenJwt } from 'service/types'; import { ulid } from 'ulid'; +import { isEmailVerified } from './isEmailVerified'; export const genTokens = (params: { privateKey: string; @@ -39,7 +40,7 @@ export const genTokens = (params: { }; const idToken: IdTokenJwt = { ...comomn, - email_verified: params.user.status === 'CONFIRMED', + email_verified: isEmailVerified(params.user), 'cognito:username': params.user.name, aud: params.userPoolClientId, token_use: 'id', diff --git a/server/domain/user/service/isEmailVerified.ts b/server/domain/user/service/isEmailVerified.ts new file mode 100644 index 0000000..6c8b059 --- /dev/null +++ b/server/domain/user/service/isEmailVerified.ts @@ -0,0 +1,4 @@ +import type { UserEntity } from 'common/types/user'; + +export const isEmailVerified = (user: UserEntity): boolean => + user.status === 'CONFIRMED' || user.status === 'FORCE_CHANGE_PASSWORD'; diff --git a/server/domain/user/useCase/adminUseCase.ts b/server/domain/user/useCase/adminUseCase.ts index 4703dd2..3b53e77 100644 --- a/server/domain/user/useCase/adminUseCase.ts +++ b/server/domain/user/useCase/adminUseCase.ts @@ -71,7 +71,7 @@ export const adminUseCase = { assert(req.UserPoolId); const user = await userQuery.findByName(tx, req.Username); - const deletableId = adminMethod.deleteUser({ user, userPoolId: req.UserPoolId }); + const deletableId = adminMethod.deleteUser(user, req.UserPoolId); await userCommand.delete(tx, deletableId, user.attributes); @@ -115,7 +115,7 @@ export const adminUseCase = { const user = await userQuery.findByName(tx, req.Username); - await userCommand.save(tx, adminMethod.setUserPassword({ user, req })); + await userCommand.save(tx, adminMethod.setUserPassword(user, req)); return {}; }), diff --git a/server/domain/user/useCase/signInUseCase.ts b/server/domain/user/useCase/signInUseCase.ts index b41f679..3f15e13 100644 --- a/server/domain/user/useCase/signInUseCase.ts +++ b/server/domain/user/useCase/signInUseCase.ts @@ -12,6 +12,7 @@ import { cognitoAssert } from 'service/cognitoAssert'; import { EXPIRES_SEC } from 'service/constants'; import { transaction } from 'service/prismaClient'; import { signInMethod } from '../model/signInMethod'; +import { isEmailVerified } from '../service/isEmailVerified'; export const signInUseCase = { userSrpAuth: (req: UserSrpAuthTarget['reqBody']): Promise => @@ -65,7 +66,7 @@ export const signInUseCase = { assert(pool.id === poolClient.userPoolId); assert(user.challenge?.secretBlock === req.ChallengeResponses.PASSWORD_CLAIM_SECRET_BLOCK); - cognitoAssert(user.status === 'FORCE_CHANGE_PASSWORD', 'User is not confirmed.'); + cognitoAssert(isEmailVerified(user), 'User is not confirmed.'); const tokens = signInMethod.srpAuth({ user, diff --git a/server/tests/api/utils.ts b/server/tests/api/utils.ts index e8a34c8..0a7e2ca 100644 --- a/server/tests/api/utils.ts +++ b/server/tests/api/utils.ts @@ -1,6 +1,7 @@ import { AdminCreateUserCommand, AdminInitiateAuthCommand, + AdminSetUserPasswordCommand, } from '@aws-sdk/client-cognito-identity-provider'; import assert from 'assert'; import { InbucketAPIClient } from 'inbucket-js-client'; @@ -31,11 +32,19 @@ export const createUserAndToken = async (): Promise<{ AccessToken: string }> => new AdminCreateUserCommand({ UserPoolId: DEFAULT_USER_POOL_ID, Username: testUserName, - TemporaryPassword: testPassword, UserAttributes: [{ Name: 'email', Value: `${ulid()}@example.com` }], }), ); + await cognitoClient.send( + new AdminSetUserPasswordCommand({ + UserPoolId: DEFAULT_USER_POOL_ID, + Username: testUserName, + Permanent: true, + Password: testPassword, + }), + ); + const res = await cognitoClient.send( new AdminInitiateAuthCommand({ AuthFlow: 'ADMIN_NO_SRP_AUTH', diff --git a/server/tests/sdk/admin.test.ts b/server/tests/sdk/admin.test.ts index 8522a92..41a2c50 100644 --- a/server/tests/sdk/admin.test.ts +++ b/server/tests/sdk/admin.test.ts @@ -41,6 +41,7 @@ test(`${AdminCreateUserCommand.name} - specify TemporaryPassword`, async () => { await cognitoClient.send( new AdminSetUserPasswordCommand({ UserPoolId: DEFAULT_USER_POOL_ID, + Permanent: true, Username: testUserName, Password: testPassword, }), @@ -102,7 +103,7 @@ test(`${AdminCreateUserCommand.name} - unset TemporaryPassword`, async () => { new AdminGetUserCommand({ UserPoolId: DEFAULT_USER_POOL_ID, Username: testUserName }), ); - expect(res.UserStatus).toBe(UserStatusType.CONFIRMED); + expect(res.UserStatus).toBe(UserStatusType.FORCE_CHANGE_PASSWORD); }); test(AdminDeleteUserCommand.name, async () => {