@robelest/convex-auth 0.0.2-preview.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +466 -63
- package/dist/client/index.d.ts +211 -30
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +673 -59
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +56 -1
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +93 -3
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +2 -0
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/index.d.ts +5 -3
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +5 -3
- package/dist/component/index.js.map +1 -1
- package/dist/component/portalBridge.d.ts +80 -0
- package/dist/component/portalBridge.d.ts.map +1 -0
- package/dist/component/portalBridge.js +102 -0
- package/dist/component/portalBridge.js.map +1 -0
- package/dist/component/public.d.ts +193 -9
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +204 -33
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +89 -9
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +68 -7
- package/dist/component/schema.js.map +1 -1
- package/dist/providers/{Anonymous.d.ts → anonymous.d.ts} +8 -8
- package/dist/providers/{Anonymous.d.ts.map → anonymous.d.ts.map} +1 -1
- package/dist/providers/{Anonymous.js → anonymous.js} +9 -10
- package/dist/providers/anonymous.js.map +1 -0
- package/dist/providers/{ConvexCredentials.d.ts → credentials.d.ts} +11 -11
- package/dist/providers/credentials.d.ts.map +1 -0
- package/dist/providers/{ConvexCredentials.js → credentials.js} +8 -8
- package/dist/providers/credentials.js.map +1 -0
- package/dist/providers/{Email.d.ts → email.d.ts} +6 -6
- package/dist/providers/email.d.ts.map +1 -0
- package/dist/providers/{Email.js → email.js} +6 -6
- package/dist/providers/email.js.map +1 -0
- package/dist/providers/passkey.d.ts +20 -0
- package/dist/providers/passkey.d.ts.map +1 -0
- package/dist/providers/passkey.js +32 -0
- package/dist/providers/passkey.js.map +1 -0
- package/dist/providers/{Password.d.ts → password.d.ts} +10 -10
- package/dist/providers/{Password.d.ts.map → password.d.ts.map} +1 -1
- package/dist/providers/{Password.js → password.js} +19 -20
- package/dist/providers/password.js.map +1 -0
- package/dist/providers/{Phone.d.ts → phone.d.ts} +3 -3
- package/dist/providers/{Phone.d.ts.map → phone.d.ts.map} +1 -1
- package/dist/providers/{Phone.js → phone.js} +3 -3
- package/dist/providers/{Phone.js.map → phone.js.map} +1 -1
- package/dist/providers/totp.d.ts +14 -0
- package/dist/providers/totp.d.ts.map +1 -0
- package/dist/providers/totp.js +23 -0
- package/dist/providers/totp.js.map +1 -0
- package/dist/server/convex-auth.d.ts +243 -0
- package/dist/server/convex-auth.d.ts.map +1 -0
- package/dist/server/convex-auth.js +365 -0
- package/dist/server/convex-auth.js.map +1 -0
- package/dist/server/implementation/index.d.ts +153 -166
- package/dist/server/implementation/index.d.ts.map +1 -1
- package/dist/server/implementation/index.js +162 -105
- package/dist/server/implementation/index.js.map +1 -1
- package/dist/server/implementation/passkey.d.ts +33 -0
- package/dist/server/implementation/passkey.d.ts.map +1 -0
- package/dist/server/implementation/passkey.js +450 -0
- package/dist/server/implementation/passkey.js.map +1 -0
- package/dist/server/implementation/redirects.d.ts.map +1 -1
- package/dist/server/implementation/redirects.js +4 -9
- package/dist/server/implementation/redirects.js.map +1 -1
- package/dist/server/implementation/sessions.d.ts +2 -20
- package/dist/server/implementation/sessions.d.ts.map +1 -1
- package/dist/server/implementation/sessions.js +2 -20
- package/dist/server/implementation/sessions.js.map +1 -1
- package/dist/server/implementation/signIn.d.ts +13 -0
- package/dist/server/implementation/signIn.d.ts.map +1 -1
- package/dist/server/implementation/signIn.js +26 -1
- package/dist/server/implementation/signIn.js.map +1 -1
- package/dist/server/implementation/totp.d.ts +40 -0
- package/dist/server/implementation/totp.d.ts.map +1 -0
- package/dist/server/implementation/totp.js +211 -0
- package/dist/server/implementation/totp.js.map +1 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +255 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/portal-email.d.ts +19 -0
- package/dist/server/portal-email.d.ts.map +1 -0
- package/dist/server/portal-email.js +89 -0
- package/dist/server/portal-email.js.map +1 -0
- package/dist/server/portal.d.ts +116 -0
- package/dist/server/portal.d.ts.map +1 -0
- package/dist/server/portal.js +294 -0
- package/dist/server/portal.js.map +1 -0
- package/dist/server/provider_utils.d.ts +1 -1
- package/dist/server/provider_utils.d.ts.map +1 -1
- package/dist/server/provider_utils.js +39 -1
- package/dist/server/provider_utils.js.map +1 -1
- package/dist/server/types.d.ts +128 -11
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/cli/index.ts +48 -6
- package/src/cli/portal-link.ts +112 -0
- package/src/cli/portal-upload.ts +411 -0
- package/src/client/index.ts +823 -109
- package/src/component/_generated/api.ts +72 -1
- package/src/component/_generated/component.ts +180 -4
- package/src/component/convex.config.ts +3 -0
- package/src/component/index.ts +5 -10
- package/src/component/portalBridge.ts +116 -0
- package/src/component/public.ts +231 -37
- package/src/component/schema.ts +70 -7
- package/src/providers/{Anonymous.ts → anonymous.ts} +10 -11
- package/src/providers/{ConvexCredentials.ts → credentials.ts} +11 -11
- package/src/providers/{Email.ts → email.ts} +5 -5
- package/src/providers/passkey.ts +35 -0
- package/src/providers/{Password.ts → password.ts} +22 -27
- package/src/providers/{Phone.ts → phone.ts} +2 -2
- package/src/providers/totp.ts +26 -0
- package/src/server/convex-auth.ts +470 -0
- package/src/server/implementation/index.ts +228 -239
- package/src/server/implementation/passkey.ts +650 -0
- package/src/server/implementation/redirects.ts +4 -11
- package/src/server/implementation/sessions.ts +2 -20
- package/src/server/implementation/signIn.ts +39 -1
- package/src/server/implementation/totp.ts +366 -0
- package/src/server/index.ts +373 -0
- package/src/server/portal-email.ts +95 -0
- package/src/server/portal.ts +375 -0
- package/src/server/provider_utils.ts +42 -1
- package/src/server/types.ts +161 -10
- package/dist/providers/Anonymous.js.map +0 -1
- package/dist/providers/ConvexCredentials.d.ts.map +0 -1
- package/dist/providers/ConvexCredentials.js.map +0 -1
- package/dist/providers/Email.d.ts.map +0 -1
- package/dist/providers/Email.js.map +0 -1
- package/dist/providers/Password.js.map +0 -1
- package/providers/Anonymous/package.json +0 -6
- package/providers/ConvexCredentials/package.json +0 -6
- package/providers/Email/package.json +0 -6
- package/providers/Password/package.json +0 -6
- package/providers/Phone/package.json +0 -6
- package/server/package.json +0 -6
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
ConvexCredentialsConfig,
|
|
5
5
|
EmailConfig,
|
|
6
6
|
GenericActionCtxWithAuthConfig,
|
|
7
|
+
PasskeyProviderConfig,
|
|
7
8
|
PhoneConfig,
|
|
9
|
+
TotpProviderConfig,
|
|
8
10
|
} from "../types.js";
|
|
9
11
|
import {
|
|
10
12
|
AuthDataModel,
|
|
@@ -17,12 +19,15 @@ import {
|
|
|
17
19
|
callRefreshSession,
|
|
18
20
|
callSignIn,
|
|
19
21
|
callVerifier,
|
|
22
|
+
callVerifierSignature,
|
|
20
23
|
callVerifyCodeAndSignIn,
|
|
21
24
|
} from "./mutations/index.js";
|
|
22
25
|
import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
|
|
23
26
|
import { requireEnv } from "../utils.js";
|
|
24
27
|
import { OAuth2Config, OIDCConfig } from "@auth/core/providers/oauth.js";
|
|
25
28
|
import { generateRandomString } from "./utils.js";
|
|
29
|
+
import { handlePasskey } from "./passkey.js";
|
|
30
|
+
import { handleTotp, checkTotpRequired } from "./totp.js";
|
|
26
31
|
|
|
27
32
|
const DEFAULT_EMAIL_VERIFICATION_CODE_DURATION_S = 60 * 60 * 24; // 24 hours
|
|
28
33
|
|
|
@@ -50,6 +55,12 @@ export async function signInImpl(
|
|
|
50
55
|
| { kind: "started"; started: true }
|
|
51
56
|
// OAuth2 and OIDC flows
|
|
52
57
|
| { kind: "redirect"; redirect: string; verifier: string }
|
|
58
|
+
// Passkey options (challenge + credential options)
|
|
59
|
+
| { kind: "passkeyOptions"; options: Record<string, any>; verifier: string }
|
|
60
|
+
// TOTP 2FA required after credentials sign-in
|
|
61
|
+
| { kind: "totpRequired"; verifier: string }
|
|
62
|
+
// TOTP setup response (enrollment)
|
|
63
|
+
| { kind: "totpSetup"; uri: string; secret: string; verifier: string; totpId: string }
|
|
53
64
|
> {
|
|
54
65
|
if (provider === null && args.refreshToken) {
|
|
55
66
|
const tokens: Tokens = (await callRefreshSession(ctx, {
|
|
@@ -84,6 +95,12 @@ export async function signInImpl(
|
|
|
84
95
|
if (provider.type === "oauth" || provider.type === "oidc") {
|
|
85
96
|
return handleOAuthProvider(ctx, provider, args, options);
|
|
86
97
|
}
|
|
98
|
+
if (provider.type === "passkey") {
|
|
99
|
+
return handlePasskey(ctx, provider, args);
|
|
100
|
+
}
|
|
101
|
+
if (provider.type === "totp") {
|
|
102
|
+
return handleTotp(ctx, provider, args);
|
|
103
|
+
}
|
|
87
104
|
const _typecheck: never = provider;
|
|
88
105
|
throw new Error(
|
|
89
106
|
`Provider type ${(provider as any).type} is not supported yet`,
|
|
@@ -187,11 +204,32 @@ async function handleCredentials(
|
|
|
187
204
|
options: {
|
|
188
205
|
generateTokens: boolean;
|
|
189
206
|
},
|
|
190
|
-
): Promise<
|
|
207
|
+
): Promise<
|
|
208
|
+
| { kind: "signedIn"; signedIn: SessionInfo | null }
|
|
209
|
+
| { kind: "totpRequired"; verifier: string }
|
|
210
|
+
> {
|
|
191
211
|
const result = await provider.authorize(args.params ?? {}, ctx);
|
|
192
212
|
if (result === null) {
|
|
193
213
|
return { kind: "signedIn", signedIn: null };
|
|
194
214
|
}
|
|
215
|
+
// Check if user has TOTP 2FA enrolled before issuing tokens
|
|
216
|
+
const hasTotpEnrolled = await checkTotpRequired(ctx, result.userId);
|
|
217
|
+
if (hasTotpEnrolled) {
|
|
218
|
+
// Create session but withhold tokens — TOTP verification needed
|
|
219
|
+
const idsWithoutTokens = await callSignIn(ctx, {
|
|
220
|
+
userId: result.userId,
|
|
221
|
+
sessionId: result.sessionId,
|
|
222
|
+
generateTokens: false,
|
|
223
|
+
});
|
|
224
|
+
// Store userId in verifier so the TOTP verify flow can complete sign-in
|
|
225
|
+
const verifier = await callVerifier(ctx);
|
|
226
|
+
await callVerifierSignature(ctx, {
|
|
227
|
+
verifier,
|
|
228
|
+
signature: JSON.stringify({ userId: result.userId }),
|
|
229
|
+
});
|
|
230
|
+
return { kind: "totpRequired", verifier };
|
|
231
|
+
}
|
|
232
|
+
|
|
195
233
|
const idsAndTokens = await callSignIn(ctx, {
|
|
196
234
|
userId: result.userId,
|
|
197
235
|
sessionId: result.sessionId,
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side TOTP ceremony logic for two-factor authentication.
|
|
3
|
+
*
|
|
4
|
+
* Handles the three phases of the TOTP flow:
|
|
5
|
+
* 1. setup — generate a TOTP secret and `otpauth://` URI for enrollment
|
|
6
|
+
* 2. confirm — verify the first code from the authenticator app and mark
|
|
7
|
+
* the enrollment as verified
|
|
8
|
+
* 3. verify — verify a TOTP code during sign-in (2FA challenge)
|
|
9
|
+
*
|
|
10
|
+
* Uses `@oslojs/otp` for TOTP generation / verification and
|
|
11
|
+
* `@oslojs/encoding` for base-32 secret encoding.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { GenericId } from "convex/values";
|
|
15
|
+
import {
|
|
16
|
+
generateTOTP,
|
|
17
|
+
verifyTOTPWithGracePeriod,
|
|
18
|
+
createTOTPKeyURI,
|
|
19
|
+
} from "@oslojs/otp";
|
|
20
|
+
import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding";
|
|
21
|
+
import {
|
|
22
|
+
TotpProviderConfig,
|
|
23
|
+
GenericActionCtxWithAuthConfig,
|
|
24
|
+
} from "../types.js";
|
|
25
|
+
import { AuthDataModel, SessionInfo } from "./types.js";
|
|
26
|
+
import { callSignIn, callVerifier } from "./mutations/index.js";
|
|
27
|
+
import { callVerifierSignature } from "./mutations/verifierSignature.js";
|
|
28
|
+
|
|
29
|
+
type EnrichedActionCtx = GenericActionCtxWithAuthConfig<AuthDataModel>;
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Setup flow
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Phase 1: Generate a TOTP secret and enrollment URI.
|
|
37
|
+
*
|
|
38
|
+
* Requires an authenticated user — TOTP enrollment always adds a second
|
|
39
|
+
* factor to an existing account. The userId is taken from the current
|
|
40
|
+
* session identity.
|
|
41
|
+
*/
|
|
42
|
+
async function handleSetup(
|
|
43
|
+
ctx: EnrichedActionCtx,
|
|
44
|
+
provider: TotpProviderConfig,
|
|
45
|
+
params: Record<string, any>,
|
|
46
|
+
): Promise<{
|
|
47
|
+
kind: "totpSetup";
|
|
48
|
+
uri: string;
|
|
49
|
+
secret: string;
|
|
50
|
+
verifier: string;
|
|
51
|
+
totpId: string;
|
|
52
|
+
}> {
|
|
53
|
+
// TOTP enrollment requires an authenticated user
|
|
54
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
55
|
+
if (identity === null) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"TOTP enrollment requires an authenticated user. " +
|
|
58
|
+
"Sign in first, then add TOTP to your account.",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const [userId] = identity.subject.split("|");
|
|
62
|
+
|
|
63
|
+
// Generate a 20-byte random secret (160 bits, per RFC 4226 recommendation)
|
|
64
|
+
const secret = new Uint8Array(20);
|
|
65
|
+
crypto.getRandomValues(secret);
|
|
66
|
+
|
|
67
|
+
// Resolve the account name for the otpauth:// URI
|
|
68
|
+
let accountName: string = params.accountName as string;
|
|
69
|
+
if (!accountName) {
|
|
70
|
+
const user = await ctx.runQuery(
|
|
71
|
+
ctx.auth.config.component.public.userGetById,
|
|
72
|
+
{ userId: userId! },
|
|
73
|
+
);
|
|
74
|
+
accountName = (user as any)?.email ?? "user";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Build the otpauth:// URI for QR code scanning
|
|
78
|
+
const uri = createTOTPKeyURI(
|
|
79
|
+
provider.options.issuer,
|
|
80
|
+
accountName,
|
|
81
|
+
secret,
|
|
82
|
+
provider.options.period,
|
|
83
|
+
provider.options.digits,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Encode the secret as base-32 for manual entry
|
|
87
|
+
const base32Secret = encodeBase32LowerCaseNoPadding(secret);
|
|
88
|
+
|
|
89
|
+
// Store enrolment metadata in a verifier so we can correlate the confirm step
|
|
90
|
+
const verifier = await callVerifier(ctx);
|
|
91
|
+
await callVerifierSignature(ctx, {
|
|
92
|
+
verifier,
|
|
93
|
+
signature: JSON.stringify({
|
|
94
|
+
secret: Array.from(secret),
|
|
95
|
+
userId,
|
|
96
|
+
digits: provider.options.digits,
|
|
97
|
+
period: provider.options.period,
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Insert an UNVERIFIED TOTP record in the DB
|
|
102
|
+
const totpId = await ctx.runMutation(
|
|
103
|
+
ctx.auth.config.component.public.totpInsert,
|
|
104
|
+
{
|
|
105
|
+
userId: userId as any,
|
|
106
|
+
secret: secret.buffer.slice(
|
|
107
|
+
secret.byteOffset,
|
|
108
|
+
secret.byteOffset + secret.byteLength,
|
|
109
|
+
),
|
|
110
|
+
digits: provider.options.digits,
|
|
111
|
+
period: provider.options.period,
|
|
112
|
+
verified: false,
|
|
113
|
+
name: params.name,
|
|
114
|
+
createdAt: Date.now(),
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
kind: "totpSetup" as const,
|
|
120
|
+
uri,
|
|
121
|
+
secret: base32Secret,
|
|
122
|
+
verifier,
|
|
123
|
+
totpId: totpId as string,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Confirm flow
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Phase 2: Verify the first code from the authenticator app.
|
|
133
|
+
*
|
|
134
|
+
* Requires an authenticated user. Marks the TOTP enrollment as verified
|
|
135
|
+
* after confirming the code is correct.
|
|
136
|
+
*/
|
|
137
|
+
async function handleConfirm(
|
|
138
|
+
ctx: EnrichedActionCtx,
|
|
139
|
+
provider: TotpProviderConfig,
|
|
140
|
+
params: Record<string, any>,
|
|
141
|
+
verifierValue: string | undefined,
|
|
142
|
+
): Promise<{ kind: "signedIn"; signedIn: SessionInfo | null }> {
|
|
143
|
+
// TOTP confirmation requires an authenticated user
|
|
144
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
145
|
+
if (identity === null) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"TOTP confirmation requires an authenticated user. " +
|
|
148
|
+
"Sign in first, then confirm your TOTP enrollment.",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const [userId] = identity.subject.split("|");
|
|
152
|
+
|
|
153
|
+
if (!verifierValue) {
|
|
154
|
+
throw new Error("Missing verifier");
|
|
155
|
+
}
|
|
156
|
+
if (!params.code) {
|
|
157
|
+
throw new Error("Missing `code` parameter");
|
|
158
|
+
}
|
|
159
|
+
if (!params.totpId) {
|
|
160
|
+
throw new Error("Missing `totpId` parameter");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Look up the TOTP record
|
|
164
|
+
const totpDoc = await ctx.runQuery(
|
|
165
|
+
ctx.auth.config.component.public.totpGetById,
|
|
166
|
+
{ totpId: params.totpId },
|
|
167
|
+
);
|
|
168
|
+
if (!totpDoc) {
|
|
169
|
+
throw new Error("TOTP enrollment not found");
|
|
170
|
+
}
|
|
171
|
+
if ((totpDoc as any).verified) {
|
|
172
|
+
throw new Error("TOTP enrollment is already verified");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Extract the secret from the TOTP record
|
|
176
|
+
const secret = new Uint8Array((totpDoc as any).secret);
|
|
177
|
+
|
|
178
|
+
// Verify the code with a 30-second grace period
|
|
179
|
+
const valid = verifyTOTPWithGracePeriod(
|
|
180
|
+
secret,
|
|
181
|
+
provider.options.period,
|
|
182
|
+
provider.options.digits,
|
|
183
|
+
params.code,
|
|
184
|
+
30,
|
|
185
|
+
);
|
|
186
|
+
if (!valid) {
|
|
187
|
+
throw new Error("Invalid TOTP code");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Mark the enrollment as verified
|
|
191
|
+
await ctx.runMutation(
|
|
192
|
+
ctx.auth.config.component.public.totpMarkVerified,
|
|
193
|
+
{ totpId: params.totpId as any, lastUsedAt: Date.now() },
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Clean up the verifier
|
|
197
|
+
await ctx.runMutation(
|
|
198
|
+
ctx.auth.config.component.public.verifierDelete,
|
|
199
|
+
{ verifierId: verifierValue },
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Return tokens for the existing session
|
|
203
|
+
const signInResult = await callSignIn(ctx, {
|
|
204
|
+
userId: userId!,
|
|
205
|
+
generateTokens: true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { kind: "signedIn", signedIn: signInResult };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Verify flow (2FA during sign-in)
|
|
213
|
+
// ============================================================================
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Phase 3: Verify a TOTP code during sign-in.
|
|
217
|
+
*
|
|
218
|
+
* Does NOT require an authenticated user — this runs mid-sign-in as a
|
|
219
|
+
* second-factor challenge. The userId is retrieved from the stored verifier.
|
|
220
|
+
*/
|
|
221
|
+
async function handleVerify(
|
|
222
|
+
ctx: EnrichedActionCtx,
|
|
223
|
+
provider: TotpProviderConfig,
|
|
224
|
+
params: Record<string, any>,
|
|
225
|
+
verifierValue: string | undefined,
|
|
226
|
+
): Promise<{ kind: "signedIn"; signedIn: SessionInfo | null }> {
|
|
227
|
+
if (!verifierValue) {
|
|
228
|
+
throw new Error("Missing verifier");
|
|
229
|
+
}
|
|
230
|
+
if (!params.code) {
|
|
231
|
+
throw new Error("Missing `code` parameter");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Look up the verifier to retrieve the stored userId
|
|
235
|
+
const verifierDoc = await ctx.runQuery(
|
|
236
|
+
ctx.auth.config.component.public.verifierGetById,
|
|
237
|
+
{ verifierId: verifierValue },
|
|
238
|
+
);
|
|
239
|
+
if (!verifierDoc) {
|
|
240
|
+
throw new Error("Invalid or expired verifier");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Parse the signature to extract userId
|
|
244
|
+
const signatureData = JSON.parse((verifierDoc as any).signature);
|
|
245
|
+
const userId = signatureData.userId as string;
|
|
246
|
+
|
|
247
|
+
// Look up the user's verified TOTP enrollment
|
|
248
|
+
const totpDoc = await ctx.runQuery(
|
|
249
|
+
ctx.auth.config.component.public.totpGetVerifiedByUserId,
|
|
250
|
+
{ userId: userId as any },
|
|
251
|
+
);
|
|
252
|
+
if (!totpDoc) {
|
|
253
|
+
throw new Error("No TOTP enrollment found");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Extract the secret from the TOTP record
|
|
257
|
+
const secret = new Uint8Array((totpDoc as any).secret);
|
|
258
|
+
|
|
259
|
+
// Verify the code with a 30-second grace period
|
|
260
|
+
const valid = verifyTOTPWithGracePeriod(
|
|
261
|
+
secret,
|
|
262
|
+
(totpDoc as any).period,
|
|
263
|
+
(totpDoc as any).digits,
|
|
264
|
+
params.code,
|
|
265
|
+
30,
|
|
266
|
+
);
|
|
267
|
+
if (!valid) {
|
|
268
|
+
throw new Error("Invalid TOTP code");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Update last used timestamp
|
|
272
|
+
await ctx.runMutation(
|
|
273
|
+
ctx.auth.config.component.public.totpUpdateLastUsed,
|
|
274
|
+
{ totpId: (totpDoc as any)._id, lastUsedAt: Date.now() },
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Clean up the verifier
|
|
278
|
+
await ctx.runMutation(
|
|
279
|
+
ctx.auth.config.component.public.verifierDelete,
|
|
280
|
+
{ verifierId: verifierValue },
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Sign in the user with tokens
|
|
284
|
+
const signInResult = await callSignIn(ctx, {
|
|
285
|
+
userId,
|
|
286
|
+
generateTokens: true,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return { kind: "signedIn", signedIn: signInResult };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Main dispatch
|
|
294
|
+
// ============================================================================
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Main TOTP handler dispatched from signIn.ts.
|
|
298
|
+
*
|
|
299
|
+
* Routes to the appropriate phase based on `params.flow`.
|
|
300
|
+
*/
|
|
301
|
+
export async function handleTotp(
|
|
302
|
+
ctx: EnrichedActionCtx,
|
|
303
|
+
provider: TotpProviderConfig,
|
|
304
|
+
args: {
|
|
305
|
+
params?: Record<string, any>;
|
|
306
|
+
verifier?: string;
|
|
307
|
+
},
|
|
308
|
+
): Promise<
|
|
309
|
+
| { kind: "signedIn"; signedIn: SessionInfo | null }
|
|
310
|
+
| {
|
|
311
|
+
kind: "totpSetup";
|
|
312
|
+
uri: string;
|
|
313
|
+
secret: string;
|
|
314
|
+
verifier: string;
|
|
315
|
+
totpId: string;
|
|
316
|
+
}
|
|
317
|
+
> {
|
|
318
|
+
const flow = args.params?.flow;
|
|
319
|
+
if (!flow) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
"Missing `flow` parameter. Expected one of: setup, confirm, verify",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
switch (flow) {
|
|
326
|
+
case "setup":
|
|
327
|
+
return handleSetup(ctx, provider, args.params ?? {});
|
|
328
|
+
case "confirm":
|
|
329
|
+
return handleConfirm(
|
|
330
|
+
ctx,
|
|
331
|
+
provider,
|
|
332
|
+
args.params ?? {},
|
|
333
|
+
args.verifier,
|
|
334
|
+
);
|
|
335
|
+
case "verify":
|
|
336
|
+
return handleVerify(
|
|
337
|
+
ctx,
|
|
338
|
+
provider,
|
|
339
|
+
args.params ?? {},
|
|
340
|
+
args.verifier,
|
|
341
|
+
);
|
|
342
|
+
default:
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Unknown TOTP flow: ${flow}. Expected one of: setup, confirm, verify`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// Helpers
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check if a user has a verified TOTP enrollment.
|
|
355
|
+
* Called after credentials sign-in to determine if 2FA is needed.
|
|
356
|
+
*/
|
|
357
|
+
export async function checkTotpRequired(
|
|
358
|
+
ctx: EnrichedActionCtx,
|
|
359
|
+
userId: string,
|
|
360
|
+
): Promise<boolean> {
|
|
361
|
+
const totpDoc = await ctx.runQuery(
|
|
362
|
+
ctx.auth.config.component.public.totpGetVerifiedByUserId,
|
|
363
|
+
{ userId: userId as any },
|
|
364
|
+
);
|
|
365
|
+
return totpDoc !== null;
|
|
366
|
+
}
|