Skip to content

chore: add changeset of account api mfa #7529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/many-needles-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@logto/core": minor
---

add skip mfa for sign in

Users can now set `skipMfaOnSignIn` value via Account API's endpoint `PUT /api/my-account/mfa-settings` to skip MFA verification when signing in.

By default, this value is set to `false`, meaning MFA verification is required when signing in if the user has available MFA methods, which is the same behavior as before.

When set to `true`, users can sign in without MFA verification, even if they have available MFA methods. This is useful for users who want to skip MFA verification when signing in, but still want to use MFA for other actions.
14 changes: 14 additions & 0 deletions .changeset/neat-points-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@logto/core": minor
---

add totp and backup code via account api

Users can now add TOTP and backup code via Account API.

The endpoints are:

- `POST /api/my-account/mfa-verifications/totp-secret/generate`: Generate a TOTP secret.
- `POST /api/my-account/mfa-verifications/backup-codes/generate`: Generate backup codes.
- `POST /api/my-account/mfa-verifications`: Add a TOTP or backup code using the generated secret or codes.
- `GET /api/my-account/mfa-verifications/backup-codes`: Retrieve backup codes.
6 changes: 0 additions & 6 deletions packages/core/src/routes/account/index.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@
"get": {
"operationId": "GetMfaSettings",
"summary": "Get MFA settings",
"tags": ["Dev feature"],
"description": "Get MFA settings for the user. This endpoint requires the Identities scope. Returns current MFA configuration preferences.",
"responses": {
"200": {
Expand All @@ -161,7 +160,6 @@
"patch": {
"operationId": "UpdateMfaSettings",
"summary": "Update MFA settings",
"tags": ["Dev feature"],
"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.",
"responses": {
"204": {
Expand Down Expand Up @@ -390,7 +388,6 @@
"post": {
"operationId": "GenerateTotpSecret",
"summary": "Generate a TOTP secret",
"tags": ["Dev feature"],
"description": "Generate a TOTP secret for the user.",
"responses": {
"200": {
Expand All @@ -403,7 +400,6 @@
"post": {
"operationId": "GenerateMyAccountBackupCodes",
"summary": "Generate backup codes",
"tags": ["Dev feature"],
"description": "Generate backup codes for the user.",
"responses": {
"200": {
Expand All @@ -416,7 +412,6 @@
"get": {
"operationId": "GetBackupCodes",
"summary": "Get backup codes",
"tags": ["Dev feature"],
"description": "Get all backup codes for the user with their usage status. Requires identity verification.",
"responses": {
"200": {
Expand Down Expand Up @@ -483,7 +478,6 @@
"post": {
"operationId": "VerifyTotpMfa",
"summary": "Verify TOTP MFA code",
"tags": ["Dev feature"],
"description": "Verify a TOTP code using the user's existing TOTP MFA factor. This endpoint is used to authenticate the user with their configured TOTP device.",
"requestBody": {
"content": {
Expand Down
147 changes: 72 additions & 75 deletions packages/core/src/routes/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

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 assertThat from '../../utils/assert-that.js';
Expand Down Expand Up @@ -198,86 +197,84 @@
}
);

if (EnvSet.values.isDevFeaturesEnabled) {
router.get(
`${accountApiPrefix}/mfa-settings`,
koaGuard({
response: z.object({
skipMfaOnSignIn: z.boolean(),
}),
status: [200, 400, 401],
router.get(
`${accountApiPrefix}/mfa-settings`,
koaGuard({
response: z.object({
skipMfaOnSignIn: z.boolean(),
}),
async (ctx, next) => {
const { id: userId, scopes } = ctx.auth;

assertThat(
scopes.has(UserScope.Identities),
new RequestError({ code: 'auth.unauthorized', status: 401 })
);
const { fields } = ctx.accountCenter;
assertThat(
fields.mfa === AccountCenterControlValue.Edit ||
fields.mfa === AccountCenterControlValue.ReadOnly,
new RequestError({ code: 'account_center.field_not_enabled', status: 400 })
);

const user = await findUserById(userId);
const mfaData = userMfaDataGuard.safeParse(user.logtoConfig[userMfaDataKey]);
const skipMfaOnSignIn = mfaData.success ? (mfaData.data.skipMfaOnSignIn ?? false) : false;

ctx.body = { skipMfaOnSignIn };

return next();
}
);

router.patch(
`${accountApiPrefix}/mfa-settings`,
koaGuard({
body: z.object({
skipMfaOnSignIn: z.boolean(),
}),
status: [204, 400, 401],
status: [200, 400, 401],
}),
async (ctx, next) => {
const { id: userId, scopes } = ctx.auth;

assertThat(
scopes.has(UserScope.Identities),
new RequestError({ code: 'auth.unauthorized', status: 401 })
);
const { fields } = ctx.accountCenter;
assertThat(
fields.mfa === AccountCenterControlValue.Edit ||
fields.mfa === AccountCenterControlValue.ReadOnly,
new RequestError({ code: 'account_center.field_not_enabled', status: 400 })
);

const user = await findUserById(userId);
const mfaData = userMfaDataGuard.safeParse(user.logtoConfig[userMfaDataKey]);
const skipMfaOnSignIn = mfaData.success ? (mfaData.data.skipMfaOnSignIn ?? false) : false;

ctx.body = { skipMfaOnSignIn };

return next();
}

Check warning on line 229 in packages/core/src/routes/account/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/account/index.ts#L209-L229

Added lines #L209 - L229 were not covered by tests
);

router.patch(
`${accountApiPrefix}/mfa-settings`,
koaGuard({
body: z.object({
skipMfaOnSignIn: z.boolean(),
}),
async (ctx, next) => {
const { id: userId, identityVerified, scopes } = ctx.auth;

assertThat(
identityVerified,
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
);
assertThat(
scopes.has(UserScope.Identities),
new RequestError({ code: 'auth.unauthorized', status: 401 })
);
const { skipMfaOnSignIn } = ctx.guard.body;
const { fields } = ctx.accountCenter;
assertThat(
fields.mfa === AccountCenterControlValue.Edit,
new RequestError({ code: 'account_center.field_not_editable', status: 400 })
);

const user = await findUserById(userId);
const existingMfaData = userMfaDataGuard.safeParse(user.logtoConfig[userMfaDataKey]);

const updatedUser = await updateUserById(userId, {
logtoConfig: {
...user.logtoConfig,
[userMfaDataKey]: {
...(existingMfaData.success ? existingMfaData.data : {}),
skipMfaOnSignIn,
},
status: [204, 400, 401],
}),
async (ctx, next) => {
const { id: userId, identityVerified, scopes } = ctx.auth;

assertThat(
identityVerified,
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
);
assertThat(
scopes.has(UserScope.Identities),
new RequestError({ code: 'auth.unauthorized', status: 401 })
);
const { skipMfaOnSignIn } = ctx.guard.body;
const { fields } = ctx.accountCenter;
assertThat(
fields.mfa === AccountCenterControlValue.Edit,
new RequestError({ code: 'account_center.field_not_editable', status: 400 })
);

const user = await findUserById(userId);
const existingMfaData = userMfaDataGuard.safeParse(user.logtoConfig[userMfaDataKey]);

const updatedUser = await updateUserById(userId, {
logtoConfig: {
...user.logtoConfig,
[userMfaDataKey]: {
...(existingMfaData.success ? existingMfaData.data : {}),
skipMfaOnSignIn,

Check warning on line 266 in packages/core/src/routes/account/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/account/index.ts#L241-L266

Added lines #L241 - L266 were not covered by tests
},
});
},
});

Check warning on line 269 in packages/core/src/routes/account/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/account/index.ts#L268-L269

Added lines #L268 - L269 were not covered by tests

ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });

Check warning on line 271 in packages/core/src/routes/account/index.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L271 was not covered by tests

ctx.status = 204;
ctx.status = 204;

Check warning on line 273 in packages/core/src/routes/account/index.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L273 was not covered by tests

return next();
}
);
}
return next();
}

Check warning on line 276 in packages/core/src/routes/account/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/account/index.ts#L275-L276

Added lines #L275 - L276 were not covered by tests
);

emailAndPhoneRoutes(...args);
identitiesRoutes(...args);
Expand Down
Loading
Loading