Skip to content

Commit 011c1f7

Browse files
authored
chore(core): publish mfa skip api (#7904)
1 parent 3090f72 commit 011c1f7

File tree

8 files changed

+118
-136
lines changed

8 files changed

+118
-136
lines changed

.changeset/little-suits-whisper.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
add API for MFA skip controls
66

77
expose logto_config endpoints in account and management APIs for managing MFA skip controls
8-
- /api/my-account/logto-config
9-
- /api/admin/users/:userId/logto-config
8+
- /api/my-account/logto-configs
9+
- /api/admin/users/:userId/logto-configs

packages/core/src/routes/account/index.openapi.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@
176176
},
177177
"/api/my-account/logto-configs": {
178178
"get": {
179-
"tags": ["Dev feature"],
180179
"operationId": "GetLogtoConfig",
181180
"summary": "Get logto config",
182181
"description": "Retrieve the exposed portion of the current user's logto config. This endpoint currently includes only the MFA skip state.",
@@ -193,7 +192,6 @@
193192
}
194193
},
195194
"patch": {
196-
"tags": ["Dev feature"],
197195
"operationId": "UpdateLogtoConfig",
198196
"summary": "Update logto config",
199197
"description": "Update the exposed portion of the current user's logto config. This endpoint currently allows updating only the MFA skip state.",

packages/core/src/routes/account/logto-config.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,13 @@ import { z } from 'zod';
44

55
import koaGuard from '#src/middleware/koa-guard.js';
66

7-
import { EnvSet } from '../../env-set/index.js';
87
import RequestError from '../../errors/RequestError/index.js';
98
import assertThat from '../../utils/assert-that.js';
109
import type { UserRouter, RouterInitArgs } from '../types.js';
1110

1211
import { accountApiPrefix } from './constants.js';
1312

1413
export default function logtoConfigRoutes<T extends UserRouter>(...args: RouterInitArgs<T>) {
15-
if (!EnvSet.values.isDevFeaturesEnabled) {
16-
return;
17-
}
18-
1914
const [router, { queries }] = args;
2015
const {
2116
users: { updateUserById, findUserById },

packages/core/src/routes/admin-user/basic.openapi.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@
110110
},
111111
"/api/users/{userId}/logto-configs": {
112112
"get": {
113-
"tags": ["Dev feature"],
114113
"responses": {
115114
"200": {
116115
"description": "Returns the exposed user logto config fields. Currently, only the MFA skip state is available."
@@ -120,7 +119,6 @@
120119
"description": "Retrieve the exposed portion of a user's logto config. This endpoint currently includes only the MFA skip state."
121120
},
122121
"patch": {
123-
"tags": ["Dev feature"],
124122
"requestBody": {
125123
"content": {
126124
"application/json": {

packages/core/src/routes/admin-user/basics.ts

Lines changed: 63 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { encryptUserPassword } from '#src/libraries/user.utils.js';
1919
import koaGuard from '#src/middleware/koa-guard.js';
2020
import assertThat from '#src/utils/assert-that.js';
2121

22-
import { EnvSet } from '../../env-set/index.js';
2322
import { parseLegacyPassword } from '../../utils/password.js';
2423
import { captureDeveloperEvent } from '../../utils/posthog.js';
2524
import { transpileUserProfileResponse } from '../../utils/user.js';
@@ -121,83 +120,81 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
121120
}
122121
);
123122

124-
if (EnvSet.values.isDevFeaturesEnabled) {
125-
router.get(
126-
'/users/:userId/logto-configs',
127-
koaGuard({
128-
params: object({ userId: string() }),
129-
response: object({
130-
mfa: object({
131-
skipped: boolean(),
132-
}),
123+
router.get(
124+
'/users/:userId/logto-configs',
125+
koaGuard({
126+
params: object({ userId: string() }),
127+
response: object({
128+
mfa: object({
129+
skipped: boolean(),
133130
}),
134-
status: [200, 404],
135131
}),
136-
async (ctx, next) => {
137-
const {
138-
params: { userId },
139-
} = ctx.guard;
132+
status: [200, 404],
133+
}),
134+
async (ctx, next) => {
135+
const {
136+
params: { userId },
137+
} = ctx.guard;
140138

141-
const user = await findUserById(userId);
142-
const existingMfaData = userMfaDataGuard.safeParse(user.logtoConfig[userMfaDataKey]);
139+
const user = await findUserById(userId);
140+
const existingMfaData = userMfaDataGuard.safeParse(user.logtoConfig[userMfaDataKey]);
143141

144-
ctx.body = {
145-
mfa: {
146-
skipped: existingMfaData.success ? Boolean(existingMfaData.data.skipped) : false,
147-
},
148-
};
142+
ctx.body = {
143+
mfa: {
144+
skipped: existingMfaData.success ? Boolean(existingMfaData.data.skipped) : false,
145+
},
146+
};
149147

150-
return next();
151-
}
152-
);
153-
154-
router.patch(
155-
'/users/:userId/logto-configs',
156-
koaGuard({
157-
params: object({ userId: string() }),
158-
body: object({
159-
mfa: object({
160-
skipped: boolean(),
161-
}),
148+
return next();
149+
}
150+
);
151+
152+
router.patch(
153+
'/users/:userId/logto-configs',
154+
koaGuard({
155+
params: object({ userId: string() }),
156+
body: object({
157+
mfa: object({
158+
skipped: boolean(),
162159
}),
163-
response: object({
164-
mfa: object({
165-
skipped: boolean(),
166-
}),
160+
}),
161+
response: object({
162+
mfa: object({
163+
skipped: boolean(),
167164
}),
168-
status: [200, 404],
169165
}),
170-
async (ctx, next) => {
171-
const {
172-
params: { userId },
173-
body: {
174-
mfa: { skipped },
175-
},
176-
} = ctx.guard;
177-
178-
const user = await findUserById(userId);
179-
const existingMfaData = userMfaDataGuard.safeParse(user.logtoConfig[userMfaDataKey]);
180-
181-
const updatedUser = await updateUserById(userId, {
182-
logtoConfig: {
183-
...user.logtoConfig,
184-
[userMfaDataKey]: {
185-
...(existingMfaData.success ? existingMfaData.data : {}),
186-
skipped,
187-
},
166+
status: [200, 404],
167+
}),
168+
async (ctx, next) => {
169+
const {
170+
params: { userId },
171+
body: {
172+
mfa: { skipped },
173+
},
174+
} = ctx.guard;
175+
176+
const user = await findUserById(userId);
177+
const existingMfaData = userMfaDataGuard.safeParse(user.logtoConfig[userMfaDataKey]);
178+
179+
const updatedUser = await updateUserById(userId, {
180+
logtoConfig: {
181+
...user.logtoConfig,
182+
[userMfaDataKey]: {
183+
...(existingMfaData.success ? existingMfaData.data : {}),
184+
skipped,
188185
},
189-
});
186+
},
187+
});
190188

191-
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
189+
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
192190

193-
ctx.body = {
194-
mfa: { skipped },
195-
};
191+
ctx.body = {
192+
mfa: { skipped },
193+
};
196194

197-
return next();
198-
}
199-
);
200-
}
195+
return next();
196+
}
197+
);
201198

202199
router.patch(
203200
'/users/:userId/profile',

packages/integration-tests/src/tests/api/account/mfa-settings.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
signInAndGetUserApi,
1717
} from '#src/helpers/profile.js';
1818
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
19-
import { devFeatureTest } from '#src/utils.js';
2019

2120
describe('my-account (mfa-settings)', () => {
2221
beforeAll(async () => {
@@ -194,7 +193,7 @@ describe('my-account (mfa-settings)', () => {
194193
});
195194
});
196195

197-
devFeatureTest.describe('PATCH /api/my-account/logto-configs', () => {
196+
describe('PATCH /api/my-account/logto-configs', () => {
198197
it('should update MFA skip state successfully', async () => {
199198
const { user, username, password } = await createDefaultTenantUserWithPassword();
200199
const api = await signInAndGetUserApi(username, password, {

packages/integration-tests/src/tests/api/admin-user.mfa-verifications.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
updateUserLogtoConfig,
1010
} from '#src/api/index.js';
1111
import { createUserByAdmin } from '#src/helpers/index.js';
12-
import { devFeatureTest } from '#src/utils.js';
1312

1413
describe('admin console user management (mfa verifications)', () => {
1514
it('should get empty list successfully', async () => {
@@ -58,7 +57,7 @@ describe('admin console user management (mfa verifications)', () => {
5857
await deleteUser(user.id);
5958
});
6059

61-
devFeatureTest.it('should update logto_config MFA skip state successfully', async () => {
60+
it('should update logto_config MFA skip state successfully', async () => {
6261
const user = await createUserByAdmin();
6362

6463
const config = await getUserLogtoConfig(user.id);

packages/integration-tests/src/tests/api/experience-api/bind-mfa/happpy-path.test.ts

Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
enableUserControlledMfaWithTotpOnlyAtSignIn,
2727
} from '#src/helpers/sign-in-experience.js';
2828
import { generateNewUserProfile, UserApiTest } from '#src/helpers/user.js';
29-
import { devFeatureTest } from '#src/utils.js';
3029

3130
describe('Bind MFA APIs happy path', () => {
3231
const userApi = new UserApiTest();
@@ -175,60 +174,57 @@ describe('Bind MFA APIs happy path', () => {
175174
await deleteUser(userId);
176175
});
177176

178-
devFeatureTest.it(
179-
'should prompt again after resetting skip state via management API',
180-
async () => {
181-
const { username, password } = generateNewUserProfile({ username: true, password: true });
182-
const client = await initExperienceClient({
183-
interactionEvent: InteractionEvent.Register,
184-
});
185-
186-
const { verificationId } = await client.createNewPasswordIdentityVerification({
187-
identifier: {
188-
type: SignInIdentifier.Username,
189-
value: username,
190-
},
191-
password,
192-
});
193-
194-
await client.identifyUser({ verificationId });
195-
196-
await expectRejects(client.submitInteraction(), {
197-
code: 'user.missing_mfa',
198-
status: 422,
199-
});
200-
201-
await client.skipMfaBinding();
202-
203-
const { redirectTo } = await client.submitInteraction();
204-
const userId = await processSession(client, redirectTo);
205-
await logoutClient(client);
206-
207-
const skippedConfig = await getUserLogtoConfig(userId);
208-
expect(skippedConfig.mfa.skipped).toBe(true);
209-
210-
await signInWithPassword({
211-
identifier: {
212-
type: SignInIdentifier.Username,
213-
value: username,
214-
},
215-
password,
216-
});
217-
218-
await updateUserLogtoConfig(userId, false);
219-
const resetConfig = await getUserLogtoConfig(userId);
220-
expect(resetConfig.mfa.skipped).toBe(false);
221-
222-
const client2 = await initExperienceClient();
223-
await identifyUserWithUsernamePassword(client2, username, password);
224-
await expectRejects(client2.submitInteraction(), {
225-
code: 'user.missing_mfa',
226-
status: 422,
227-
});
228-
229-
await deleteUser(userId);
230-
}
231-
);
177+
it('should prompt again after resetting skip state via management API', async () => {
178+
const { username, password } = generateNewUserProfile({ username: true, password: true });
179+
const client = await initExperienceClient({
180+
interactionEvent: InteractionEvent.Register,
181+
});
182+
183+
const { verificationId } = await client.createNewPasswordIdentityVerification({
184+
identifier: {
185+
type: SignInIdentifier.Username,
186+
value: username,
187+
},
188+
password,
189+
});
190+
191+
await client.identifyUser({ verificationId });
192+
193+
await expectRejects(client.submitInteraction(), {
194+
code: 'user.missing_mfa',
195+
status: 422,
196+
});
197+
198+
await client.skipMfaBinding();
199+
200+
const { redirectTo } = await client.submitInteraction();
201+
const userId = await processSession(client, redirectTo);
202+
await logoutClient(client);
203+
204+
const skippedConfig = await getUserLogtoConfig(userId);
205+
expect(skippedConfig.mfa.skipped).toBe(true);
206+
207+
await signInWithPassword({
208+
identifier: {
209+
type: SignInIdentifier.Username,
210+
value: username,
211+
},
212+
password,
213+
});
214+
215+
await updateUserLogtoConfig(userId, false);
216+
const resetConfig = await getUserLogtoConfig(userId);
217+
expect(resetConfig.mfa.skipped).toBe(false);
218+
219+
const client2 = await initExperienceClient();
220+
await identifyUserWithUsernamePassword(client2, username, password);
221+
await expectRejects(client2.submitInteraction(), {
222+
code: 'user.missing_mfa',
223+
status: 422,
224+
});
225+
226+
await deleteUser(userId);
227+
});
232228

233229
it('should able to skip MFA binding on sign-in', async () => {
234230
const { username, password } = generateNewUserProfile({ username: true, password: true });

0 commit comments

Comments
 (0)