0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor(core): move verification record id to header (#6801)

This commit is contained in:
wangsijie 2024-11-15 13:42:28 +08:00 committed by GitHub
parent 640425414f
commit 859495b13b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 152 additions and 163 deletions

View file

@ -1,4 +1,3 @@
import RequestError from '../errors/RequestError/index.js';
import { expirationTime } from '../queries/verification-records.js';
import {
buildVerificationRecord,
@ -43,34 +42,6 @@ const getVerificationRecordById = async ({
return buildVerificationRecord(libraries, queries, result.data);
};
/**
* Verifies the user sensitive permission by checking if the verification record is valid
* and associated with the user.
*/
export const verifyUserSensitivePermission = async ({
userId,
id,
queries,
libraries,
}: {
userId: string;
id: string;
queries: Queries;
libraries: Libraries;
}): Promise<void> => {
try {
const record = await getVerificationRecordById({ id, queries, libraries, userId });
assertThat(record.isVerified, 'verification_record.not_found');
} catch (error) {
if (error instanceof RequestError) {
throw new RequestError({ code: 'verification_record.permission_denied', status: 401 });
}
throw error;
}
};
/**
* Builds a user verification record by its id and type.
* This is used to build a verification record for new identifier verifications,

View file

@ -0,0 +1 @@
export const verificationRecordIdHeader = 'logto-verification-id';

View file

@ -16,6 +16,7 @@ import { type WithAuthContext, type TokenInfo } from './types.js';
import { extractBearerTokenFromHeaders, getAdminTenantTokenValidationSet } from './utils.js';
export * from './types.js';
export * from './constants.js';
export const verifyBearerTokenFromRequest = async (
envSet: EnvSet,

View file

@ -7,11 +7,15 @@ import Sinon from 'sinon';
import RequestError from '#src/errors/RequestError/index.js';
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
import { MockTenant } from '../../test-utils/tenant.js';
import type { WithAuthContext } from './index.js';
const { jest } = import.meta;
const provider = new Provider('https://logto.test');
const tenantContext = new MockTenant(provider);
const mockAccessToken = {
accountId: 'fooUser',
clientId: 'fooClient',
@ -70,12 +74,19 @@ describe('koaOidcAuth middleware', () => {
},
};
Sinon.stub(provider.AccessToken, 'find').resolves(mockAccessToken);
await koaOidcAuth(provider)(ctx, next);
expect(ctx.auth).toEqual({ type: 'user', id: 'fooUser', scopes: new Set(['openid']) });
await koaOidcAuth(tenantContext)(ctx, next);
expect(ctx.auth).toEqual({
type: 'user',
id: 'fooUser',
scopes: new Set(['openid']),
identityVerified: false,
});
});
it('expect to throw if authorization header is missing', async () => {
await expect(koaOidcAuth(provider)(ctx, next)).rejects.toMatchError(authHeaderMissingError);
await expect(koaOidcAuth(tenantContext)(ctx, next)).rejects.toMatchError(
authHeaderMissingError
);
});
it('expect to throw if authorization header token type not recognized ', async () => {
@ -86,7 +97,9 @@ describe('koaOidcAuth middleware', () => {
},
};
await expect(koaOidcAuth(provider)(ctx, next)).rejects.toMatchError(tokenNotSupportedError);
await expect(koaOidcAuth(tenantContext)(ctx, next)).rejects.toMatchError(
tokenNotSupportedError
);
});
it('expect to throw if access token is not found', async () => {
@ -98,7 +111,7 @@ describe('koaOidcAuth middleware', () => {
};
Sinon.stub(provider.AccessToken, 'find').resolves();
await expect(koaOidcAuth(provider)(ctx, next)).rejects.toMatchError(unauthorizedError);
await expect(koaOidcAuth(tenantContext)(ctx, next)).rejects.toMatchError(unauthorizedError);
});
it('expect to throw if sub is missing', async () => {
@ -113,7 +126,7 @@ describe('koaOidcAuth middleware', () => {
accountId: undefined,
});
await expect(koaOidcAuth(provider)(ctx, next)).rejects.toMatchError(unauthorizedError);
await expect(koaOidcAuth(tenantContext)(ctx, next)).rejects.toMatchError(unauthorizedError);
});
it('expect to throw if access token does not have openid scope', async () => {
@ -128,6 +141,6 @@ describe('koaOidcAuth middleware', () => {
scopes: new Set(['foo']),
});
await expect(koaOidcAuth(provider)(ctx, next)).rejects.toMatchError(forbiddenError);
await expect(koaOidcAuth(tenantContext)(ctx, next)).rejects.toMatchError(forbiddenError);
});
});

View file

@ -1,18 +1,58 @@
import type { MiddlewareType } from 'koa';
import type { IRouterParamContext } from 'koa-router';
import type Provider from 'oidc-provider';
import RequestError from '#src/errors/RequestError/index.js';
import {
verificationRecordDataGuard,
buildVerificationRecord,
} from '#src/routes/experience/classes/verifications/index.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import assertThat from '#src/utils/assert-that.js';
import { verificationRecordIdHeader } from './constants.js';
import { type WithAuthContext } from './types.js';
import { extractBearerTokenFromHeaders } from './utils.js';
/**
* Builds a verification record by its id.
* The `userId` is optional and is only used for user sensitive permission verifications.
*/
const getVerificationRecordResultById = async ({
id,
queries,
libraries,
userId,
}: {
id: string;
queries: Queries;
libraries: Libraries;
userId: string;
}): Promise<boolean> => {
const record = await queries.verificationRecords.findActiveVerificationRecordById(id);
if (record?.userId !== userId) {
return false;
}
const result = verificationRecordDataGuard.safeParse({
...record.data,
id: record.id,
});
if (!result.success) {
return false;
}
const instance = buildVerificationRecord(libraries, queries, result.data);
return instance.isVerified;
};
/**
* Auth middleware for OIDC opaque token
*/
export default function koaOidcAuth<StateT, ContextT extends IRouterParamContext, ResponseBodyT>(
provider: Provider
tenant: TenantContext
): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
const authMiddleware: MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> = async (
ctx,
@ -20,7 +60,7 @@ export default function koaOidcAuth<StateT, ContextT extends IRouterParamContext
) => {
const { request } = ctx;
const accessTokenValue = extractBearerTokenFromHeaders(request.headers);
const accessToken = await provider.AccessToken.find(accessTokenValue);
const accessToken = await tenant.provider.AccessToken.find(accessTokenValue);
assertThat(accessToken, new RequestError({ code: 'auth.unauthorized', status: 401 }));
@ -28,10 +68,22 @@ export default function koaOidcAuth<StateT, ContextT extends IRouterParamContext
assertThat(accountId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
assertThat(scopes.has('openid'), new RequestError({ code: 'auth.forbidden', status: 403 }));
const verificationRecordId = request.headers[verificationRecordIdHeader];
const identityVerified =
typeof verificationRecordId === 'string'
? await getVerificationRecordResultById({
id: verificationRecordId,
queries: tenant.queries,
libraries: tenant.libraries,
userId: accountId,
})
: false;
ctx.auth = {
type: 'user',
id: accountId,
scopes,
identityVerified,
};
return next();

View file

@ -4,6 +4,8 @@ type Auth = {
type: 'user' | 'app';
id: string;
scopes: Set<string>;
/** If the request is verified by a verification record, this will be set to `true`. */
identityVerified?: boolean;
};
export type WithAuthContext<ContextT extends IRouterParamContext = IRouterParamContext> =

View file

@ -104,7 +104,7 @@ const createRouters = (tenant: TenantContext) => {
const anonymousRouter: AnonymousRouter = new Router();
const userRouter: UserRouter = new Router();
userRouter.use(koaOidcAuth(tenant.provider));
userRouter.use(koaOidcAuth(tenant));
// TODO(LOG-10147): Rename to koaApiHooks, this middleware is used for both management API and user API
userRouter.use(koaManagementApiHooks(tenant.libraries.hooks));
profileRoutes(userRouter, tenant);

View file

@ -118,7 +118,7 @@
"post": {
"operationId": "UpdatePassword",
"summary": "Update password",
"description": "Update password for the user, a verification record is required for checking sensitive permissions.",
"description": "Update password for the user, a logto-verification-id in header is required for checking sensitive permissions.",
"requestBody": {
"content": {
"application/json": {
@ -126,9 +126,6 @@
"properties": {
"password": {
"description": "The new password for the user."
},
"verificationRecordId": {
"description": "The verification record ID for checking sensitive permissions."
}
}
}
@ -149,7 +146,7 @@
"post": {
"operationId": "UpdatePrimaryEmail",
"summary": "Update primary email",
"description": "Update primary email for the user, a verification record is required for checking sensitive permissions, and a new identifier verification record is required for the new email ownership verification.",
"description": "Update primary email for the user, a logto-verification-id in header is required for checking sensitive permissions, and a new identifier verification record is required for the new email ownership verification.",
"requestBody": {
"content": {
"application/json": {
@ -158,9 +155,6 @@
"email": {
"description": "The new email for the user."
},
"verificationRecordId": {
"description": "The verification record ID for checking sensitive permissions."
},
"newIdentifierVerificationRecordId": {
"description": "The identifier verification record ID for the new email ownership verification."
}
@ -186,7 +180,7 @@
"post": {
"operationId": "UpdatePrimaryPhone",
"summary": "Update primary phone",
"description": "Update primary phone for the user, a verification record is required for checking sensitive permissions, and a new identifier verification record is required for the new phone ownership verification.",
"description": "Update primary phone for the user, a logto-verification-id in header is required for checking sensitive permissions, and a new identifier verification record is required for the new phone ownership verification.",
"requestBody": {
"content": {
"application/json": {
@ -195,9 +189,6 @@
"phone": {
"description": "The new phone for the user."
},
"verificationRecordId": {
"description": "The verification record ID for checking sensitive permissions."
},
"newIdentifierVerificationRecordId": {
"description": "The identifier verification record ID for the new phone ownership verification."
}
@ -223,15 +214,12 @@
"post": {
"operationId": "AddUserIdentities",
"summary": "Add a user identity",
"description": "Add an identity (social identity) to the user, a verification record is required for checking sensitive permissions, and a verification record for the social identity is required.",
"description": "Add an identity (social identity) to the user, a logto-verification-id in header is required for checking sensitive permissions, and a verification record for the social identity is required.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"verificationRecordId": {
"description": "The verification record ID for checking sensitive permissions."
},
"newIdentifierVerificationRecordId": {
"description": "The identifier verification record ID for the new social identity ownership verification."
}
@ -251,14 +239,7 @@
"delete": {
"operationId": "DeleteIdentity",
"summary": "Delete a user identity",
"description": "Delete an identity (social identity) from the user, a verification record is required for checking sensitive permissions.",
"parameters": [
{
"name": "verificationRecordId",
"in": "query",
"description": "The verification record ID for checking sensitive permissions."
}
],
"description": "Delete an identity (social identity) from the user, a logto-verification-id in header is required for checking sensitive permissions.",
"responses": {
"204": {
"description": "The identity was deleted successfully."

View file

@ -13,10 +13,7 @@ import koaGuard from '#src/middleware/koa-guard.js';
import { EnvSet } from '../../env-set/index.js';
import RequestError from '../../errors/RequestError/index.js';
import { encryptUserPassword } from '../../libraries/user.utils.js';
import {
buildVerificationRecordByIdAndType,
verifyUserSensitivePermission,
} from '../../libraries/verification.js';
import { buildVerificationRecordByIdAndType } from '../../libraries/verification.js';
import assertThat from '../../utils/assert-that.js';
import { PasswordValidator } from '../experience/classes/libraries/password-validator.js';
import type { UserRouter, RouterInitArgs } from '../types.js';
@ -144,12 +141,16 @@ export default function profileRoutes<T extends UserRouter>(
router.post(
'/profile/password',
koaGuard({
body: z.object({ password: z.string().min(1), verificationRecordId: z.string() }),
body: z.object({ password: z.string().min(1) }),
status: [204, 400, 401, 422],
}),
async (ctx, next) => {
const { id: userId } = ctx.auth;
const { password, verificationRecordId } = ctx.guard.body;
const { id: userId, identityVerified } = ctx.auth;
assertThat(
identityVerified,
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
);
const { password } = ctx.guard.body;
const { fields } = ctx.accountCenter;
assertThat(
fields.password === AccountCenterControlValue.Edit,
@ -161,13 +162,6 @@ export default function profileRoutes<T extends UserRouter>(
const passwordPolicyChecker = new PasswordValidator(signInExperience.passwordPolicy, user);
await passwordPolicyChecker.validatePassword(password, user);
await verifyUserSensitivePermission({
userId,
id: verificationRecordId,
queries,
libraries,
});
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
const updatedUser = await updateUserById(userId, {
passwordEncrypted,
@ -187,14 +181,17 @@ export default function profileRoutes<T extends UserRouter>(
koaGuard({
body: z.object({
email: z.string().regex(emailRegEx),
verificationRecordId: z.string(),
newIdentifierVerificationRecordId: z.string(),
}),
status: [204, 400, 401],
}),
async (ctx, next) => {
const { id: userId, scopes } = ctx.auth;
const { email, verificationRecordId, newIdentifierVerificationRecordId } = ctx.guard.body;
const { id: userId, scopes, identityVerified } = ctx.auth;
assertThat(
identityVerified,
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
);
const { email, newIdentifierVerificationRecordId } = ctx.guard.body;
const { fields } = ctx.accountCenter;
assertThat(
fields.email === AccountCenterControlValue.Edit,
@ -203,13 +200,6 @@ export default function profileRoutes<T extends UserRouter>(
assertThat(scopes.has(UserScope.Email), 'auth.unauthorized');
await verifyUserSensitivePermission({
userId,
id: verificationRecordId,
queries,
libraries,
});
// Check new identifier
const newVerificationRecord = await buildVerificationRecordByIdAndType({
type: VerificationType.EmailVerificationCode,
@ -237,14 +227,17 @@ export default function profileRoutes<T extends UserRouter>(
koaGuard({
body: z.object({
phone: z.string().regex(phoneRegEx),
verificationRecordId: z.string(),
newIdentifierVerificationRecordId: z.string(),
}),
status: [204, 400, 401],
}),
async (ctx, next) => {
const { id: userId, scopes } = ctx.auth;
const { phone, verificationRecordId, newIdentifierVerificationRecordId } = ctx.guard.body;
const { id: userId, scopes, identityVerified } = ctx.auth;
assertThat(
identityVerified,
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
);
const { phone, newIdentifierVerificationRecordId } = ctx.guard.body;
const { fields } = ctx.accountCenter;
assertThat(
fields.phone === AccountCenterControlValue.Edit,
@ -253,13 +246,6 @@ export default function profileRoutes<T extends UserRouter>(
assertThat(scopes.has(UserScope.Phone), 'auth.unauthorized');
await verifyUserSensitivePermission({
userId,
id: verificationRecordId,
queries,
libraries,
});
// Check new identifier
const newVerificationRecord = await buildVerificationRecordByIdAndType({
type: VerificationType.PhoneVerificationCode,
@ -286,14 +272,17 @@ export default function profileRoutes<T extends UserRouter>(
'/profile/identities',
koaGuard({
body: z.object({
verificationRecordId: z.string(),
newIdentifierVerificationRecordId: z.string(),
}),
status: [204, 400, 401],
}),
async (ctx, next) => {
const { id: userId, scopes } = ctx.auth;
const { verificationRecordId, newIdentifierVerificationRecordId } = ctx.guard.body;
const { id: userId, scopes, identityVerified } = ctx.auth;
assertThat(
identityVerified,
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
);
const { newIdentifierVerificationRecordId } = ctx.guard.body;
const { fields } = ctx.accountCenter;
assertThat(
fields.social === AccountCenterControlValue.Edit,
@ -302,13 +291,6 @@ export default function profileRoutes<T extends UserRouter>(
assertThat(scopes.has(UserScope.Identities), 'auth.unauthorized');
await verifyUserSensitivePermission({
userId,
id: verificationRecordId,
queries,
libraries,
});
// Check new identifier
const newVerificationRecord = await buildVerificationRecordByIdAndType({
type: VerificationType.Social,
@ -350,15 +332,14 @@ export default function profileRoutes<T extends UserRouter>(
'/profile/identities/:target',
koaGuard({
params: z.object({ target: z.string() }),
query: z.object({
// TODO: Move all sensitive permission checks to the header
verificationRecordId: z.string(),
}),
status: [204, 400, 401, 404],
}),
async (ctx, next) => {
const { id: userId, scopes } = ctx.auth;
const { verificationRecordId } = ctx.guard.query;
const { id: userId, scopes, identityVerified } = ctx.auth;
assertThat(
identityVerified,
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
);
const { target } = ctx.guard.params;
const { fields } = ctx.accountCenter;
assertThat(
@ -368,13 +349,6 @@ export default function profileRoutes<T extends UserRouter>(
assertThat(scopes.has(UserScope.Identities), 'auth.unauthorized');
await verifyUserSensitivePermission({
userId,
id: verificationRecordId,
queries,
libraries,
});
const user = await findUserById(userId);
assertThat(

View file

@ -1,11 +1,17 @@
import { type UserProfileResponse } from '@logto/schemas';
import { type KyInstance } from 'ky';
const verificationRecordIdHeader = 'logto-verification-id';
export const updatePassword = async (
api: KyInstance,
verificationRecordId: string,
password: string
) => api.post('api/profile/password', { json: { password, verificationRecordId } });
) =>
api.post('api/profile/password', {
json: { password },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
export const updatePrimaryEmail = async (
api: KyInstance,
@ -14,7 +20,8 @@ export const updatePrimaryEmail = async (
newIdentifierVerificationRecordId: string
) =>
api.post('api/profile/primary-email', {
json: { email, verificationRecordId, newIdentifierVerificationRecordId },
json: { email, newIdentifierVerificationRecordId },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
export const updatePrimaryPhone = async (
@ -24,7 +31,8 @@ export const updatePrimaryPhone = async (
newIdentifierVerificationRecordId: string
) =>
api.post('api/profile/primary-phone', {
json: { phone, verificationRecordId, newIdentifierVerificationRecordId },
json: { phone, newIdentifierVerificationRecordId },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
export const updateIdentities = async (
@ -33,7 +41,8 @@ export const updateIdentities = async (
newIdentifierVerificationRecordId: string
) =>
api.post('api/profile/identities', {
json: { verificationRecordId, newIdentifierVerificationRecordId },
json: { newIdentifierVerificationRecordId },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
export const deleteIdentity = async (
@ -42,7 +51,7 @@ export const deleteIdentity = async (
verificationRecordId: string
) =>
api.delete(`api/profile/identities/${target}`, {
searchParams: { verificationRecordId },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
export const updateUser = async (api: KyInstance, body: Record<string, unknown>) =>

View file

@ -13,12 +13,14 @@ import {
updatePrimaryPhone,
updateUser,
} from '#src/api/profile.js';
import { createVerificationRecordByPassword } from '#src/api/verification-record.js';
import { expectRejects } from '#src/helpers/index.js';
import {
createDefaultTenantUserWithPassword,
deleteDefaultTenantUser,
signInAndGetUserApi,
} from '#src/helpers/profile.js';
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
import { devFeatureTest, generateEmail, generatePhone } from '#src/utils.js';
const { describe, it } = devFeatureTest;
@ -30,6 +32,7 @@ const expectedError = {
describe('profile, account center fields disabled', () => {
beforeAll(async () => {
await enableAllPasswordSignInMethods();
await updateAccountCenter({
enabled: true,
fields: {
@ -79,38 +82,29 @@ describe('profile, account center fields disabled', () => {
status: 400,
});
await expectRejects(updatePassword(api, 'verification-record-id', 'new-password'), {
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(updatePassword(api, verificationRecordId, 'new-password'), {
code: 'account_center.filed_not_editable',
status: 400,
});
await expectRejects(
updatePrimaryEmail(
api,
generateEmail(),
'verification-record-id',
'new-verification-record-id'
),
updatePrimaryEmail(api, generateEmail(), verificationRecordId, 'new-verification-record-id'),
expectedError
);
await expectRejects(
updatePrimaryPhone(
api,
generatePhone(),
'verification-record-id',
'new-verification-record-id'
),
updatePrimaryPhone(api, generatePhone(), verificationRecordId, 'new-verification-record-id'),
expectedError
);
await expectRejects(
updateIdentities(api, 'verification-record-id', 'new-verification-record-id'),
updateIdentities(api, verificationRecordId, 'new-verification-record-id'),
expectedError
);
await expectRejects(
deleteIdentity(api, mockSocialConnectorTarget, 'verification-record-id'),
deleteIdentity(api, mockSocialConnectorTarget, verificationRecordId),
expectedError
);

View file

@ -33,14 +33,10 @@ describe('profile (email and phone)', () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const newEmail = generateEmail();
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(
updatePrimaryEmail(
api,
newEmail,
'invalid-verification-record-id',
'new-verification-record-id'
),
updatePrimaryEmail(api, newEmail, verificationRecordId, 'new-verification-record-id'),
{
code: 'auth.unauthorized',
status: 400,
@ -144,14 +140,10 @@ describe('profile (email and phone)', () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const newPhone = generatePhone();
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(
updatePrimaryPhone(
api,
newPhone,
'invalid-verification-record-id',
'new-verification-record-id'
),
updatePrimaryPhone(api, newPhone, verificationRecordId, 'new-verification-record-id'),
{
code: 'auth.unauthorized',
status: 400,

View file

@ -254,8 +254,9 @@ describe('profile', () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const newPassword = '123456';
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(updatePassword(api, 'invalid-varification-record-id', newPassword), {
await expectRejects(updatePassword(api, verificationRecordId, newPassword), {
code: 'password.rejected',
status: 422,
});

View file

@ -54,9 +54,10 @@ describe('profile (social)', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(
updateIdentities(api, 'invalid-verification-record-id', 'new-verification-record-id'),
updateIdentities(api, verificationRecordId, 'new-verification-record-id'),
{
code: 'auth.unauthorized',
status: 400,
@ -107,7 +108,6 @@ describe('profile (social)', () => {
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Profile, UserScope.Identities],
});
await expectRejects(
createSocialVerificationRecord(api, 'invalid-connector-id', state, redirectUri),
{
@ -173,14 +173,12 @@ describe('profile (social)', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(
deleteIdentity(api, mockSocialConnectorTarget, 'invalid-verification-record-id'),
{
code: 'auth.unauthorized',
status: 400,
}
);
await expectRejects(deleteIdentity(api, mockSocialConnectorTarget, verificationRecordId), {
code: 'auth.unauthorized',
status: 400,
});
await deleteDefaultTenantUser(user.id);
});