Skip to content

Commit

Permalink
feat: implement AdminCreateUser useCase
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Jul 2, 2024
1 parent dcd9172 commit ee3cd6a
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 84 deletions.
14 changes: 14 additions & 0 deletions server/api/@types/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type {
AdminCreateUserRequest,
AdminCreateUserResponse,
AdminDeleteUserRequest,
AdminInitiateAuthRequest,
AdminInitiateAuthResponse,
ListUserPoolsRequest,
ListUserPoolsResponse,
} from '@aws-sdk/client-cognito-identity-provider';
Expand Down Expand Up @@ -116,7 +120,15 @@ export type ResendConfirmationCodeTarget = TargetBody<

export type ListUserPoolsTarget = TargetBody<ListUserPoolsRequest, ListUserPoolsResponse>;

export type AdminCreateUserTarget = TargetBody<AdminCreateUserRequest, AdminCreateUserResponse>;

export type AdminDeleteUserTarget = TargetBody<AdminDeleteUserRequest, Record<string, never>>;

export type AdminInitiateAuthTarget = TargetBody<
AdminInitiateAuthRequest,
AdminInitiateAuthResponse
>;

export type ChangePasswordTarget = TargetBody<
{
AccessToken: string;
Expand All @@ -135,6 +147,8 @@ export type AmzTargets = {
'AWSCognitoIdentityProviderService.RevokeToken': RevokeTokenTarget;
'AWSCognitoIdentityProviderService.ResendConfirmationCode': ResendConfirmationCodeTarget;
'AWSCognitoIdentityProviderService.ListUserPools': ListUserPoolsTarget;
'AWSCognitoIdentityProviderService.AdminCreateUser': AdminCreateUserTarget;
'AWSCognitoIdentityProviderService.AdminDeleteUser': AdminDeleteUserTarget;
'AWSCognitoIdentityProviderService.AdminInitiateAuth': AdminInitiateAuthTarget;
'AWSCognitoIdentityProviderService.ChangePassword': ChangePasswordTarget;
};
27 changes: 27 additions & 0 deletions server/api/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,37 @@ const targets: {
validator: z.object({ MaxResults: z.number(), NextToken: z.string().optional() }),
useCase: authUseCase.listUserPools,
},
'AWSCognitoIdentityProviderService.AdminCreateUser': {
validator: z.object({
UserPoolId: brandedId.userPool.maybe,
Username: z.string(),
UserAttributes: z
.array(z.object({ Name: z.string(), Value: z.string().optional() }))
.optional(),
ValidationData: z
.array(z.object({ Name: z.string(), Value: z.string().optional() }))
.optional(),
TemporaryPassword: z.string().optional(),
ForceAliasCreation: z.boolean().optional(),
MessageAction: z.enum(['RESEND', 'SUPPRESS']).optional(),
DesiredDeliveryMediums: z.array(z.enum(['EMAIL', 'SMS'])).optional(),
ClientMetadata: z.record(z.string()).optional(),
}),
useCase: adminUseCase.createUser,
},
'AWSCognitoIdentityProviderService.AdminDeleteUser': {
validator: z.object({ UserPoolId: brandedId.userPool.maybe, Username: z.string() }),
useCase: adminUseCase.deleteUser,
},
'AWSCognitoIdentityProviderService.AdminInitiateAuth': {
validator: z.object({
AuthFlow: z.literal('ADMIN_NO_SRP_AUTH'),
UserPoolId: brandedId.userPool.maybe,
ClientId: brandedId.userPoolClient.maybe,
AuthParameters: z.object({ USERNAME: z.string(), PASSWORD: z.string() }),
}),
useCase: adminUseCase.initiateAuth,
},
'AWSCognitoIdentityProviderService.ChangePassword': {
validator: z.object({
AccessToken: z.string(),
Expand Down
51 changes: 0 additions & 51 deletions server/api/public/backdoor/controller.ts

This file was deleted.

13 changes: 0 additions & 13 deletions server/api/public/backdoor/index.ts

This file was deleted.

25 changes: 14 additions & 11 deletions server/domain/user/model/userMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@ import { cognitoAssert } from 'service/cognitoAssert';
import { ulid } from 'ulid';
import { z } from 'zod';

type CreateUserVal = {
name: string;
password: string;
email: string;
salt: string;
verifier: string;
userPoolId: EntityId['userPool'];
};

export const userMethod = {
createUser: (
idCount: number,
val: {
name: string;
password: string;
email: string;
salt: string;
verifier: string;
userPoolId: EntityId['userPool'];
},
): UserEntity => {
createUser: (idCount: number, val: CreateUserVal): UserEntity => {
cognitoAssert(idCount === 0, 'User already exists');
cognitoAssert(
/^[a-z][a-z\d_-]/.test(val.name),
Expand Down Expand Up @@ -70,6 +69,10 @@ export const userMethod = {
createdTime: Date.now(),
};
},
createVerifiedUser: (idCount: number, val: CreateUserVal): UserEntity => ({
...userMethod.createUser(idCount, val),
verified: true,
}),
verifyUser: (user: UserEntity, confirmationCode: string): UserEntity => {
cognitoAssert(
user.confirmationCode === confirmationCode,
Expand Down
70 changes: 69 additions & 1 deletion server/domain/user/useCase/adminUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
import type { AdminDeleteUserTarget } from 'api/@types/auth';
import type {
AdminCreateUserTarget,
AdminDeleteUserTarget,
AdminInitiateAuthTarget,
} from 'api/@types/auth';
import assert from 'assert';
import { userPoolQuery } from 'domain/userPool/repository/userPoolQuery';
import { brandedId } from 'service/brandedId';
import { transaction } from 'service/prismaClient';
import { genJwks } from 'service/privateKey';
import { userMethod } from '../model/userMethod';
import { userCommand } from '../repository/userCommand';
import { userQuery } from '../repository/userQuery';
import { genCredentials } from '../service/genCredentials';
import { genTokens } from '../service/genTokens';

export const adminUseCase = {
createUser: (req: AdminCreateUserTarget['reqBody']): Promise<AdminCreateUserTarget['resBody']> =>
transaction(async (tx) => {
const email = req.UserAttributes?.find((attr) => attr.Name === 'email')?.Value;

assert(email);
assert(req.UserPoolId);
assert(req.Username);
assert(req.TemporaryPassword);

const userPool = await userPoolQuery.findById(tx, req.UserPoolId);
const { salt, verifier } = genCredentials({
poolId: userPool.id,
username: req.Username,
password: req.TemporaryPassword,
});
const idCount = await userQuery.countId(tx, req.Username);
const user = userMethod.createVerifiedUser(idCount, {
name: req.Username,
password: req.TemporaryPassword,
email,
salt,
verifier,
userPoolId: userPool.id,
});

await userCommand.save(tx, user);

return { User: { Username: user.name, Attributes: [{ Name: 'email', Value: user.email }] } };
}),
deleteUser: (req: AdminDeleteUserTarget['reqBody']): Promise<AdminDeleteUserTarget['resBody']> =>
transaction(async (tx) => {
assert(req.Username);
Expand All @@ -18,4 +56,34 @@ export const adminUseCase = {

return {};
}),
initiateAuth: (
req: AdminInitiateAuthTarget['reqBody'],
): Promise<AdminInitiateAuthTarget['resBody']> =>
transaction(async (tx) => {
assert(req.UserPoolId);
assert(req.AuthParameters);

const user = await userQuery.findByName(tx, req.AuthParameters.USERNAME);
const pool = await userPoolQuery.findById(tx, req.UserPoolId);
const poolClient = await userPoolQuery.findClientById(
tx,
brandedId.userPoolClient.maybe.parse(req.ClientId),
);
const jwks = await genJwks(pool.privateKey);
const tokens = genTokens({
privateKey: pool.privateKey,
userPoolClientId: poolClient.id,
jwks,
user,
});

return {
AuthenticationResult: {
...tokens,
ExpiresIn: 3600,
RefreshToken: user.refreshToken,
TokenType: 'Bearer',
},
};
}),
};
34 changes: 29 additions & 5 deletions server/tests/api/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import aspida from '@aspida/axios';
import { AdminDeleteUserCommand } from '@aws-sdk/client-cognito-identity-provider';
import {
AdminCreateUserCommand,
AdminDeleteUserCommand,
AdminInitiateAuthCommand,
} from '@aws-sdk/client-cognito-identity-provider';
import api from 'api/$api';
import assert from 'assert';
import axios from 'axios';
import { cognitoClient } from 'service/cognito';
import { COOKIE_NAME } from 'service/constants';
import { DEFAULT_USER_POOL_ID, PORT } from 'service/envValues';
import { DEFAULT_USER_POOL_CLIENT_ID, DEFAULT_USER_POOL_ID, PORT } from 'service/envValues';
import { ulid } from 'ulid';

const baseURL = `http://127.0.0.1:${PORT}`;
Expand All @@ -18,12 +23,31 @@ export const testUserName = 'test-client';
export const testPassword = 'Test-client-password1';

export const createUserClient = async (): Promise<typeof noCookieClient> => {
const tokens = await noCookieClient.public.backdoor.$post({
body: { username: testUserName, email: `${ulid()}@example.com`, password: testPassword },
const command1 = new AdminCreateUserCommand({
UserPoolId: DEFAULT_USER_POOL_ID,
Username: testUserName,
TemporaryPassword: testPassword,
UserAttributes: [{ Name: 'email', Value: `${ulid()}@example.com` }],
});

await cognitoClient.send(command1);

const command2 = new AdminInitiateAuthCommand({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
UserPoolId: DEFAULT_USER_POOL_ID,
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
AuthParameters: { USERNAME: testUserName, PASSWORD: testPassword },
});

const res = await cognitoClient.send(command2);
assert(res.AuthenticationResult);

const agent = axios.create({
baseURL,
headers: { cookie: `${COOKIE_NAME}=${tokens.IdToken}`, 'Content-Type': 'text/plain' },
headers: {
cookie: `${COOKIE_NAME}=${res.AuthenticationResult.IdToken}`,
'Content-Type': 'text/plain',
},
});

agent.interceptors.response.use(undefined, (err) =>
Expand Down
5 changes: 5 additions & 0 deletions server/tests/api/changePassword.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ test('changePassword', async () => {
});

assert('ChallengeParameters' in res1);
assert(res1.ChallengeParameters);
const secretBlock1 = res1.ChallengeParameters.SECRET_BLOCK;
const signature1 = calcClientSignature({
secretBlock: secretBlock1,
Expand Down Expand Up @@ -54,6 +55,8 @@ test('changePassword', async () => {
});

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

const newPassword = 'Test-client-password2';
Expand All @@ -77,6 +80,7 @@ test('changePassword', async () => {
});

assert('ChallengeParameters' in res3);
assert(res3.ChallengeParameters);
const secretBlock2 = res3.ChallengeParameters.SECRET_BLOCK;
const signature2 = calcClientSignature({
secretBlock: secretBlock2,
Expand Down Expand Up @@ -104,6 +108,7 @@ test('changePassword', async () => {
});

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

await deleteUser();
Expand Down
35 changes: 32 additions & 3 deletions server/tests/sdk/admin.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import { AdminDeleteUserCommand } from '@aws-sdk/client-cognito-identity-provider';
import {
AdminCreateUserCommand,
AdminDeleteUserCommand,
AdminInitiateAuthCommand,
} from '@aws-sdk/client-cognito-identity-provider';
import { cognitoClient } from 'service/cognito';
import { DEFAULT_USER_POOL_ID } from 'service/envValues';
import { createUserClient, testUserName } from 'tests/api/apiClient';
import { DEFAULT_USER_POOL_CLIENT_ID, DEFAULT_USER_POOL_ID } from 'service/envValues';
import { createUserClient, deleteUser, testPassword, testUserName } from 'tests/api/apiClient';
import { ulid } from 'ulid';
import { expect, test } from 'vitest';

test('AdminCreateUserCommand', async () => {
const command1 = new AdminCreateUserCommand({
UserPoolId: DEFAULT_USER_POOL_ID,
Username: testUserName,
TemporaryPassword: testPassword,
UserAttributes: [{ Name: 'email', Value: `${ulid()}@example.com` }],
});

await cognitoClient.send(command1);

const command2 = new AdminInitiateAuthCommand({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
UserPoolId: DEFAULT_USER_POOL_ID,
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
AuthParameters: { USERNAME: testUserName, PASSWORD: testPassword },
});

const tokens = await cognitoClient.send(command2);

expect(tokens.AuthenticationResult).toBeTruthy();

await deleteUser();
});

test('AdminDeleteUserCommand', async () => {
const userClient = await createUserClient();

Expand Down

0 comments on commit ee3cd6a

Please sign in to comment.