From 8259d5ffd355560d3a8ef6c7afe5b9b9e12c1962 Mon Sep 17 00:00:00 2001 From: Daniel Moerner Date: Thu, 15 Oct 2026 15:33:03 -0400 Subject: [PATCH 1/2] feat(backend): add secondFactorVerification to createSignInToken A user with a second factor enabled who signs in with a sign-in token still has to complete their second factor. Some customers who issue these tokens from their backend want them to complete the sign-in without that prompt. The Backend API now accepts `second_factor_verification` on sign-in token creation (clerk/clerk_go#23112): `fresh` treats the second factor as verified at sign-in, and `stale` treats it as unverified, so a reverification that requires a second factor still prompts for one. When it is set, `expires_in_seconds` defaults to 10 minutes. This exposes it as `secondFactorVerification` on `createSignInToken()`, and makes `expiresInSeconds` optional, matching the API. Co-Authored-By: Claude Opus 5.5 --- .changeset/sign-in-token-second-factor.md | 5 +++ .../src/api/__tests__/SignInTokenApi.test.ts | 32 +++++++++++++++++++ .../src/api/endpoints/SignInTokenApi.ts | 6 ++-- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 .changeset/sign-in-token-second-factor.md diff --git a/.changeset/sign-in-token-second-factor.md b/.changeset/sign-in-token-second-factor.md new file mode 100644 index 00000000000..48d643fce47 --- /dev/null +++ b/.changeset/sign-in-token-second-factor.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +Add an optional `secondFactorVerification` parameter (`'fresh' | 'stale'`) to `createSignInToken()`, which lets a user with a second factor enabled complete the sign-in without being prompted for it. `expiresInSeconds` is now optional. diff --git a/packages/backend/src/api/__tests__/SignInTokenApi.test.ts b/packages/backend/src/api/__tests__/SignInTokenApi.test.ts index 6d144fecbc9..91ab1c55382 100644 --- a/packages/backend/src/api/__tests__/SignInTokenApi.test.ts +++ b/packages/backend/src/api/__tests__/SignInTokenApi.test.ts @@ -43,4 +43,36 @@ describe('SignInTokenAPI', () => { expect(response.id).toBe('sign_in_token_123'); }); + + it('sends the second factor verification as second_factor_verification', async () => { + server.use( + http.post( + 'https://api.clerk.test/v1/sign_in_tokens', + validateHeaders(async ({ request }) => { + const body = await request.json(); + expect(body).toEqual({ + user_id: 'user_123', + second_factor_verification: 'stale', + }); + + return HttpResponse.json({ + id: 'sign_in_token_123', + user_id: 'user_123', + token: 'token_123', + status: 'pending', + url: 'https://accounts.example.com/tickets/token_123', + created_at: 1, + updated_at: 1, + }); + }), + ), + ); + + const response = await apiClient.signInTokens.createSignInToken({ + userId: 'user_123', + secondFactorVerification: 'stale', + }); + + expect(response.id).toBe('sign_in_token_123'); + }); }); diff --git a/packages/backend/src/api/endpoints/SignInTokenApi.ts b/packages/backend/src/api/endpoints/SignInTokenApi.ts index 45cb23efb32..ec5c629503b 100644 --- a/packages/backend/src/api/endpoints/SignInTokenApi.ts +++ b/packages/backend/src/api/endpoints/SignInTokenApi.ts @@ -8,8 +8,10 @@ export type CreateSignInTokensParams = { userId: string; /** The ID of the organization to activate when the user signs in. Organizations must be enabled for the instance, and the user must be a member of the organization. */ orgId?: string; - /** The number of seconds until the sign-in token expires. By default, the duration is `2592000` (30 days). */ - expiresInSeconds: number; + /** The number of seconds until the sign-in token expires. By default, the duration is `2592000` (30 days), or `600` (10 minutes) when `secondFactorVerification` is set. */ + expiresInSeconds?: number; + /** Lets a user with a second factor enabled complete the sign-in without being prompted for it. With `fresh`, the second factor is treated as verified at sign-in. With `stale`, it is treated as unverified, so any reverification that requires a second factor prompts the user to complete one. Has no effect for users without a second factor enabled. */ + secondFactorVerification?: 'fresh' | 'stale'; }; const basePath = '/sign_in_tokens'; From fd0ec33281db75405f88e434b849269b99cfe1c8 Mon Sep 17 00:00:00 2001 From: Daniel Moerner Date: Thu, 15 Oct 2026 21:06:51 -0400 Subject: [PATCH 2/2] docs(backend): note 10-minute default expiry for second-factor sign-in tokens Co-Authored-By: Claude Opus 5.5 --- packages/backend/src/api/endpoints/SignInTokenApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/api/endpoints/SignInTokenApi.ts b/packages/backend/src/api/endpoints/SignInTokenApi.ts index ec5c629503b..006ea0c5c49 100644 --- a/packages/backend/src/api/endpoints/SignInTokenApi.ts +++ b/packages/backend/src/api/endpoints/SignInTokenApi.ts @@ -19,7 +19,7 @@ const basePath = '/sign_in_tokens'; /** @generateWithEmptyComment */ export class SignInTokenAPI extends AbstractAPI { /** - * Creates a new sign-in token for the given user. By default, sign-in tokens expire in 30 days. You can optionally specify a custom expiration time in seconds using the `expiresInSeconds` parameter. + * Creates a new sign-in token for the given user. By default, sign-in tokens expire in 30 days, or in 10 minutes when `secondFactorVerification` is set. You can optionally specify a custom expiration time in seconds using the `expiresInSeconds` parameter. * @returns The created [`SignInToken`](https://clerk.com/docs/reference/backend/types/backend-sign-in-token) object. */ public async createSignInToken(params: CreateSignInTokensParams) {