Skip to content

Commit 72f64ac

Browse files
authored
feat(core,schemas): add requireMfaOnSignIn (#7493)
1 parent 9fe30e6 commit 72f64ac

File tree

8 files changed

+349
-0
lines changed

8 files changed

+349
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ cache
3333
fly.toml
3434
dump.rdb
3535

36+
# Claude local files
37+
.claude/
38+
3639
# connectors
3740
/packages/core/connectors
3841

packages/core/src/__mocks__/user.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const mockUser: User = {
1919
},
2020
logtoConfig: {},
2121
mfaVerifications: [],
22+
requireMfaOnSignIn: true,
2223
customData: {},
2324
profile: {},
2425
applicationId: 'bar',
@@ -76,6 +77,7 @@ export const mockUserWithPassword: User = {
7677
customData: {},
7778
logtoConfig: {},
7879
mfaVerifications: [],
80+
requireMfaOnSignIn: true,
7981
profile: {},
8082
applicationId: 'bar',
8183
lastSignInAt: 1_650_969_465_789,
@@ -99,6 +101,7 @@ export const mockUserList: User[] = [
99101
customData: {},
100102
logtoConfig: {},
101103
mfaVerifications: [],
104+
requireMfaOnSignIn: true,
102105
profile: {},
103106
applicationId: 'bar',
104107
lastSignInAt: 1_650_969_465_000,
@@ -120,6 +123,7 @@ export const mockUserList: User[] = [
120123
customData: {},
121124
logtoConfig: {},
122125
mfaVerifications: [],
126+
requireMfaOnSignIn: true,
123127
profile: {},
124128
applicationId: 'bar',
125129
lastSignInAt: 1_650_969_465_000,
@@ -141,6 +145,7 @@ export const mockUserList: User[] = [
141145
customData: {},
142146
logtoConfig: {},
143147
mfaVerifications: [],
148+
requireMfaOnSignIn: true,
144149
profile: {},
145150
applicationId: 'bar',
146151
lastSignInAt: 1_650_969_465_000,
@@ -162,6 +167,7 @@ export const mockUserList: User[] = [
162167
customData: {},
163168
logtoConfig: {},
164169
mfaVerifications: [],
170+
requireMfaOnSignIn: true,
165171
profile: {},
166172
applicationId: 'bar',
167173
lastSignInAt: 1_650_969_465_000,
@@ -183,6 +189,7 @@ export const mockUserList: User[] = [
183189
customData: {},
184190
logtoConfig: {},
185191
mfaVerifications: [],
192+
requireMfaOnSignIn: true,
186193
profile: {},
187194
applicationId: 'bar',
188195
lastSignInAt: 1_650_969_465_000,

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,39 @@
139139
}
140140
}
141141
},
142+
"/api/my-account/mfa-settings": {
143+
"get": {
144+
"operationId": "GetMfaSettings",
145+
"summary": "Get MFA settings",
146+
"tags": ["Dev feature"],
147+
"description": "Get MFA settings for the user. This endpoint requires the Identities scope. Returns current MFA configuration preferences.",
148+
"responses": {
149+
"200": {
150+
"description": "The MFA settings were retrieved successfully."
151+
},
152+
"401": {
153+
"description": "Permission denied, insufficient scope or MFA field not enabled."
154+
}
155+
}
156+
},
157+
"patch": {
158+
"operationId": "UpdateMfaSettings",
159+
"summary": "Update MFA settings",
160+
"tags": ["Dev feature"],
161+
"description": "Update MFA settings for the user. This endpoint requires identity verification and the Identities scope. Controls whether MFA verification is required during sign-in when the user has MFA configured.",
162+
"responses": {
163+
"204": {
164+
"description": "The MFA settings were updated successfully."
165+
},
166+
"400": {
167+
"description": "The request body is invalid."
168+
},
169+
"401": {
170+
"description": "Permission denied, identity verification is required or insufficient scope."
171+
}
172+
}
173+
}
174+
},
142175
"/api/my-account/primary-email": {
143176
"post": {
144177
"operationId": "UpdatePrimaryEmail",

packages/core/src/routes/account/index.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { z } from 'zod';
99

1010
import koaGuard from '#src/middleware/koa-guard.js';
1111

12+
import { EnvSet } from '../../env-set/index.js';
1213
import RequestError from '../../errors/RequestError/index.js';
1314
import { encryptUserPassword } from '../../libraries/user.utils.js';
1415
import assertThat from '../../utils/assert-that.js';
@@ -180,6 +181,77 @@ export default function accountRoutes<T extends UserRouter>(...args: RouterInitA
180181
}
181182
);
182183

184+
if (EnvSet.values.isDevFeaturesEnabled) {
185+
router.get(
186+
`${accountApiPrefix}/mfa-settings`,
187+
koaGuard({
188+
response: z.object({
189+
requireMfaOnSignIn: z.boolean(),
190+
}),
191+
status: [200, 400, 401],
192+
}),
193+
async (ctx, next) => {
194+
const { id: userId, scopes } = ctx.auth;
195+
196+
assertThat(
197+
scopes.has(UserScope.Identities),
198+
new RequestError({ code: 'auth.unauthorized', status: 401 })
199+
);
200+
const { fields } = ctx.accountCenter;
201+
assertThat(
202+
fields.mfa === AccountCenterControlValue.Edit ||
203+
fields.mfa === AccountCenterControlValue.ReadOnly,
204+
new RequestError({ code: 'account_center.field_not_enabled', status: 400 })
205+
);
206+
207+
const user = await findUserById(userId);
208+
ctx.body = {
209+
requireMfaOnSignIn: user.requireMfaOnSignIn,
210+
};
211+
212+
return next();
213+
}
214+
);
215+
216+
router.patch(
217+
`${accountApiPrefix}/mfa-settings`,
218+
koaGuard({
219+
body: z.object({
220+
requireMfaOnSignIn: z.boolean(),
221+
}),
222+
status: [204, 400, 401],
223+
}),
224+
async (ctx, next) => {
225+
const { id: userId, identityVerified, scopes } = ctx.auth;
226+
227+
assertThat(
228+
identityVerified,
229+
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
230+
);
231+
assertThat(
232+
scopes.has(UserScope.Identities),
233+
new RequestError({ code: 'auth.unauthorized', status: 401 })
234+
);
235+
const { requireMfaOnSignIn } = ctx.guard.body;
236+
const { fields } = ctx.accountCenter;
237+
assertThat(
238+
fields.mfa === AccountCenterControlValue.Edit,
239+
new RequestError({ code: 'account_center.field_not_editable', status: 400 })
240+
);
241+
242+
const updatedUser = await updateUserById(userId, {
243+
requireMfaOnSignIn,
244+
});
245+
246+
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
247+
248+
ctx.status = 204;
249+
250+
return next();
251+
}
252+
);
253+
}
254+
183255
emailAndPhoneRoutes(...args);
184256
identitiesRoutes(...args);
185257
mfaVerificationsRoutes(...args);

packages/integration-tests/src/api/my-account.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,16 @@ export const deleteMfaVerification = async (
109109
api.delete(`api/my-account/mfa-verifications/${verificationId}`, {
110110
headers: { [verificationRecordIdHeader]: verificationRecordId },
111111
});
112+
113+
export const getMfaSettings = async (api: KyInstance) =>
114+
api.get('api/my-account/mfa-settings').json<{ requireMfaOnSignIn: boolean }>();
115+
116+
export const updateMfaSettings = async (
117+
api: KyInstance,
118+
verificationRecordId: string,
119+
requireMfaOnSignIn: boolean
120+
) =>
121+
api.patch('api/my-account/mfa-settings', {
122+
json: { requireMfaOnSignIn },
123+
headers: { [verificationRecordIdHeader]: verificationRecordId },
124+
});

0 commit comments

Comments
 (0)