@openora/core 0.4.1-canary.140 → 0.4.1-canary.141
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/.tsbuildinfo +1 -1
- package/dist/contracts/schemas/events.d.ts +3 -1
- package/dist/contracts/schemas/events.d.ts.map +1 -1
- package/dist/contracts/schemas/events.js +3 -0
- package/dist/contracts/schemas/events.js.map +1 -1
- package/dist/contracts/schemas/identity.d.ts +31 -1
- package/dist/contracts/schemas/identity.d.ts.map +1 -1
- package/dist/contracts/schemas/identity.js +36 -7
- package/dist/contracts/schemas/identity.js.map +1 -1
- package/dist/contracts/schemas/mail.d.ts +9 -1
- package/dist/contracts/schemas/mail.d.ts.map +1 -1
- package/dist/contracts/schemas/mail.js +3 -0
- package/dist/contracts/schemas/mail.js.map +1 -1
- package/dist/mail/adapters/default-email-template-renderer.d.ts.map +1 -1
- package/dist/mail/adapters/default-email-template-renderer.js +6 -0
- package/dist/mail/adapters/default-email-template-renderer.js.map +1 -1
- package/dist/mail/contract/index.d.ts +5 -0
- package/dist/mail/contract/index.d.ts.map +1 -1
- package/dist/pam/identity/__tests__/identity.rate-limit.int.test.js +7 -1
- package/dist/pam/identity/__tests__/identity.rate-limit.int.test.js.map +1 -1
- package/dist/pam/identity/__tests__/identity.registration-gates.int.test.js +5 -0
- package/dist/pam/identity/__tests__/identity.registration-gates.int.test.js.map +1 -1
- package/dist/pam/identity/__tests__/identity.service.int.test.js +38 -4
- package/dist/pam/identity/__tests__/identity.service.int.test.js.map +1 -1
- package/dist/pam/identity/__tests__/two-factor-delivery.test.d.ts +2 -0
- package/dist/pam/identity/__tests__/two-factor-delivery.test.d.ts.map +1 -0
- package/dist/pam/identity/__tests__/two-factor-delivery.test.js +23 -0
- package/dist/pam/identity/__tests__/two-factor-delivery.test.js.map +1 -0
- package/dist/pam/identity/contract/index.d.ts +26 -1
- package/dist/pam/identity/contract/index.d.ts.map +1 -1
- package/dist/pam/identity/contract/index.js +14 -1
- package/dist/pam/identity/contract/index.js.map +1 -1
- package/dist/pam/identity/drizzle/migrations/0012_worried_nuke.sql +2 -0
- package/dist/pam/identity/drizzle/migrations/0013_backfill_two_factor_method.sql +6 -0
- package/dist/pam/identity/drizzle/migrations/meta/0012_snapshot.json +1090 -0
- package/dist/pam/identity/drizzle/migrations/meta/0013_snapshot.json +1090 -0
- package/dist/pam/identity/drizzle/migrations/meta/_journal.json +14 -0
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/identity/plugin.js +6 -0
- package/dist/pam/identity/plugin.js.map +1 -1
- package/dist/pam/identity/react/account.d.ts +10 -4
- package/dist/pam/identity/react/account.d.ts.map +1 -1
- package/dist/pam/identity/react/account.js +14 -0
- package/dist/pam/identity/react/account.js.map +1 -1
- package/dist/pam/identity/react/auth.d.ts +1 -0
- package/dist/pam/identity/react/auth.d.ts.map +1 -1
- package/dist/pam/identity/router/index.d.ts +26 -1
- package/dist/pam/identity/router/index.d.ts.map +1 -1
- package/dist/pam/identity/router/index.js +2 -0
- package/dist/pam/identity/router/index.js.map +1 -1
- package/dist/pam/identity/schema/index.d.ts +18 -0
- package/dist/pam/identity/schema/index.d.ts.map +1 -1
- package/dist/pam/identity/schema/index.js +6 -1
- package/dist/pam/identity/schema/index.js.map +1 -1
- package/dist/pam/identity/service/admin-security.service.d.ts.map +1 -1
- package/dist/pam/identity/service/admin-security.service.js +3 -0
- package/dist/pam/identity/service/admin-security.service.js.map +1 -1
- package/dist/pam/identity/service/fresh-reauthentication.service.d.ts.map +1 -1
- package/dist/pam/identity/service/fresh-reauthentication.service.js +4 -9
- package/dist/pam/identity/service/fresh-reauthentication.service.js.map +1 -1
- package/dist/pam/identity/service/identity.service.d.ts +46 -6
- package/dist/pam/identity/service/identity.service.d.ts.map +1 -1
- package/dist/pam/identity/service/identity.service.js +219 -22
- package/dist/pam/identity/service/identity.service.js.map +1 -1
- package/dist/pam/identity/service/two-factor-challenge.service.d.ts +36 -0
- package/dist/pam/identity/service/two-factor-challenge.service.d.ts.map +1 -0
- package/dist/pam/identity/service/two-factor-challenge.service.js +30 -0
- package/dist/pam/identity/service/two-factor-challenge.service.js.map +1 -0
- package/dist/pam/identity/service/two-factor-delivery.service.d.ts +67 -0
- package/dist/pam/identity/service/two-factor-delivery.service.d.ts.map +1 -0
- package/dist/pam/identity/service/two-factor-delivery.service.js +135 -0
- package/dist/pam/identity/service/two-factor-delivery.service.js.map +1 -0
- package/dist/pam/react.d.ts +1 -1
- package/dist/pam/react.d.ts.map +1 -1
- package/dist/pam/react.js +1 -1
- package/dist/pam/react.js.map +1 -1
- package/dist/server/auth/auth.d.ts.map +1 -1
- package/dist/server/auth/auth.js +3 -0
- package/dist/server/auth/auth.js.map +1 -1
- package/dist/testing/seed-player.d.ts +2 -0
- package/dist/testing/seed-player.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,8 @@ import { type EventBus, type NodeHeaders, DrizzleService } from '@openora/core/s
|
|
|
2
2
|
import type { SessionService } from './session.service.js';
|
|
3
3
|
import type { TrustedDeviceService } from './trusted-device.service.js';
|
|
4
4
|
import type { TwoFactorLockoutService } from './two-factor-lockout.service.js';
|
|
5
|
-
import
|
|
5
|
+
import { type TwoFactorDeliveryService } from './two-factor-delivery.service.js';
|
|
6
|
+
import type { CacheAdapter, RateLimiterAdapter, RateLimitKey, MailDispatchPort, LoginInput, RegisterInput, Enable2faInput, Enable2faResult, Verify2faInput, TwoFactorStatus, Disable2faInput, RegenerateBackupCodesInput, TrustCurrentDeviceInput, RequestPasswordResetInput, VerifyPasswordResetOtpInput, ResetPasswordInput, ResendEmailVerificationInput, VerifyEmailInput, UpdateProfileInput, User, IdentityReader, ChangePasswordInput, ChangeEmailInput, IdentityServiceOptions, PlatformConfig, ClientMeta, GeoCheckCommands, PlayerProvisioning, SecurityControls, SetLoginWithdrawalAlertsInput } from '@openora/core/contracts';
|
|
6
7
|
export declare const UserNotFoundError: new (id: string) => Error & {
|
|
7
8
|
data?: Record<string, string>;
|
|
8
9
|
};
|
|
@@ -23,6 +24,7 @@ export type IdentityServiceDeps = {
|
|
|
23
24
|
cache?: CacheAdapter;
|
|
24
25
|
trustedDevices?: TrustedDeviceService;
|
|
25
26
|
twoFactorLockout?: TwoFactorLockoutService;
|
|
27
|
+
twoFactorDelivery: TwoFactorDeliveryService;
|
|
26
28
|
sessions?: SessionService;
|
|
27
29
|
};
|
|
28
30
|
/**
|
|
@@ -49,8 +51,9 @@ export declare class IdentityService {
|
|
|
49
51
|
private readonly existingAccountSignUps;
|
|
50
52
|
private readonly trustedDevices?;
|
|
51
53
|
private readonly twoFactorLockout?;
|
|
54
|
+
private readonly twoFactorDelivery;
|
|
52
55
|
private readonly sessions?;
|
|
53
|
-
constructor({ drizzle, events, identityReader, mailDispatch, options, limiter, platformConfig, geoCheck, playerProvisioning, cache, trustedDevices, twoFactorLockout, sessions, }: IdentityServiceDeps);
|
|
56
|
+
constructor({ drizzle, events, identityReader, mailDispatch, options, limiter, platformConfig, geoCheck, playerProvisioning, cache, trustedDevices, twoFactorLockout, twoFactorDelivery, sessions, }: IdentityServiceDeps);
|
|
54
57
|
private findUserByEmail;
|
|
55
58
|
private get api();
|
|
56
59
|
private forwardCookies;
|
|
@@ -90,6 +93,7 @@ export declare class IdentityService {
|
|
|
90
93
|
* read a stale count and slip past the lockout threshold.
|
|
91
94
|
*/
|
|
92
95
|
login(input: LoginInput, reqHeaders: NodeHeaders, resHeaders: Headers): Promise<{
|
|
96
|
+
twoFactorMethod?: "email" | "app" | "sms" | undefined;
|
|
93
97
|
twoFactorRedirect: boolean;
|
|
94
98
|
user?: undefined;
|
|
95
99
|
session?: undefined;
|
|
@@ -127,7 +131,6 @@ export declare class IdentityService {
|
|
|
127
131
|
attemptsRemaining: number;
|
|
128
132
|
lockoutUntil: string | null;
|
|
129
133
|
};
|
|
130
|
-
twoFactorRedirect?: undefined;
|
|
131
134
|
}>;
|
|
132
135
|
private clearLockout;
|
|
133
136
|
private resetLockoutTier;
|
|
@@ -156,10 +159,47 @@ export declare class IdentityService {
|
|
|
156
159
|
createdAt: string;
|
|
157
160
|
updatedAt: string;
|
|
158
161
|
} | null>;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Starts enrolment. All three methods share the one secret better-auth mints here;
|
|
164
|
+
* `method` only decides how the code that activates it arrives, and is written
|
|
165
|
+
* before the send so the delivery hook knows where to route. better-auth flips
|
|
166
|
+
* `twoFactorEnabled` only once the first code clears, so abandoning a first
|
|
167
|
+
* enrolment leaves the account unprotected exactly as it was.
|
|
168
|
+
*
|
|
169
|
+
* A transport failure on the first send throws (see `sendOtpForCurrentMethod`)
|
|
170
|
+
* after `two_factor_method` is already written, but that write is inert on its
|
|
171
|
+
* own: `twoFactorStatus` reports it only once `enabled` is true, and the CONFLICT
|
|
172
|
+
* guard below only fires once it is. The caller recovers by retrying either
|
|
173
|
+
* `sendTwoFactorOtp` (resend) or this route again - both read the method that was
|
|
174
|
+
* already persisted, so neither loses the player's choice.
|
|
175
|
+
*/
|
|
176
|
+
enableTwoFactor(input: Enable2faInput, reqHeaders: NodeHeaders, resHeaders: Headers): Promise<Enable2faResult>;
|
|
177
|
+
/**
|
|
178
|
+
* Re-sends the one-time code for an account on the `email` or `sms` method - the
|
|
179
|
+
* "Resend code" action during enrolment, and the send leg of a login challenge.
|
|
180
|
+
* Resolves its subject from a live session first and the pending-challenge cookie
|
|
181
|
+
* second, because at login there is no session yet.
|
|
182
|
+
*/
|
|
183
|
+
sendTwoFactorOtp(reqHeaders: NodeHeaders, resHeaders: Headers): Promise<{
|
|
184
|
+
maskedDestination: string;
|
|
162
185
|
}>;
|
|
186
|
+
/** Whether a second factor is active, which method it uses, and where a code can go. */
|
|
187
|
+
twoFactorStatus(reqHeaders: NodeHeaders): Promise<TwoFactorStatus>;
|
|
188
|
+
/**
|
|
189
|
+
* Rejects a method the account cannot actually receive a code on. Enrolling into
|
|
190
|
+
* `sms` without a verified phone would mint a secret the player can never clear,
|
|
191
|
+
* locking them out of their own account at the next login.
|
|
192
|
+
*/
|
|
193
|
+
private assertMethodAvailable;
|
|
194
|
+
/**
|
|
195
|
+
* The account's delivery method, defaulting to `app`: an enrolment that predates the
|
|
196
|
+
* column reads as null, and every one of those was an authenticator.
|
|
197
|
+
*/
|
|
198
|
+
private resolveTwoFactorMethod;
|
|
199
|
+
private isTwoFactorEnabled;
|
|
200
|
+
private setTwoFactorMethod;
|
|
201
|
+
private verifyChallengeCode;
|
|
202
|
+
private sendOtpForCurrentMethod;
|
|
163
203
|
verifyTwoFactor(input: Verify2faInput, reqHeaders: NodeHeaders, resHeaders: Headers): Promise<{
|
|
164
204
|
success: true;
|
|
165
205
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.service.d.ts","sourceRoot":"","sources":["../../../../src/pam/identity/service/identity.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,cAAc,EAQf,MAAM,sBAAsB,CAAC;AAM9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,aAAa,EACb,cAAc,EACd,cAAc,
|
|
1
|
+
{"version":3,"file":"identity.service.d.ts","sourceRoot":"","sources":["../../../../src/pam/identity/service/identity.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,11 +6,13 @@ import { and, eq, isNull, sql } from 'drizzle-orm';
|
|
|
6
6
|
import * as z from 'zod';
|
|
7
7
|
import { user, session, account, verification, twoFactor } from '../schema/index.js';
|
|
8
8
|
import { captureTimezone } from './capture-timezone.service.js';
|
|
9
|
+
import { maskEmail, maskPhone, } from './two-factor-delivery.service.js';
|
|
9
10
|
import { RATE_LIMIT_KEYS, makeRateLimitKey } from '@openora/core/contracts';
|
|
10
11
|
import { assertSupportedLanguage } from '../../shared/language.js';
|
|
11
12
|
import { assertAccountNotBlocked } from './rg-guard.service.js';
|
|
12
13
|
import { DEFAULT_LOCKOUT_DURATION_MS, DEFAULT_MAX_LOGIN_ATTEMPTS, computeLockoutTier, createAccountLockedError, hasFailedLoginWindowExpired, makeLoginSecurityState, } from './lockout-policy.service.js';
|
|
13
14
|
import { getSecurityControls } from './security-controls.service.js';
|
|
15
|
+
import { resolveChallengeMethod, verifyChallengeCode } from './two-factor-challenge.service.js';
|
|
14
16
|
function nodeHeadersToHeaders(nodeHeaders) {
|
|
15
17
|
const headers = new Headers();
|
|
16
18
|
for (const [key, value] of Object.entries(nodeHeaders)) {
|
|
@@ -188,6 +190,14 @@ const VERIFY_EMAIL_RATE_LIMIT = {
|
|
|
188
190
|
};
|
|
189
191
|
const CHANGE_PASSWORD_RATE_LIMIT = { limit: 5, windowMs: 15 * MINUTE_MS };
|
|
190
192
|
const TWO_FACTOR_PASSWORD_RATE_LIMIT = { limit: 5, windowMs: 5 * MINUTE_MS };
|
|
193
|
+
// Fails closed: every call spends real money on an `sms` enrolment and fills a mailbox
|
|
194
|
+
// on an `email` one, and better-auth issues a fresh code per send, so an unbounded
|
|
195
|
+
// resend loop is both a toll-fraud channel and an unbounded guess budget.
|
|
196
|
+
const SEND_2FA_OTP_RATE_LIMIT = {
|
|
197
|
+
limit: 3,
|
|
198
|
+
windowMs: 5 * MINUTE_MS,
|
|
199
|
+
onUnavailable: 'deny',
|
|
200
|
+
};
|
|
191
201
|
const FAKE_LOGIN_SHADOW_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
192
202
|
function loginShadowKey(email) {
|
|
193
203
|
return `login-shadow:${email}`;
|
|
@@ -234,8 +244,9 @@ export class IdentityService {
|
|
|
234
244
|
existingAccountSignUps = new Set();
|
|
235
245
|
trustedDevices;
|
|
236
246
|
twoFactorLockout;
|
|
247
|
+
twoFactorDelivery;
|
|
237
248
|
sessions;
|
|
238
|
-
constructor({ drizzle, events, identityReader, mailDispatch, options, limiter, platformConfig, geoCheck, playerProvisioning, cache, trustedDevices, twoFactorLockout, sessions, }) {
|
|
249
|
+
constructor({ drizzle, events, identityReader, mailDispatch, options, limiter, platformConfig, geoCheck, playerProvisioning, cache, trustedDevices, twoFactorLockout, twoFactorDelivery, sessions, }) {
|
|
239
250
|
this.drizzle = drizzle;
|
|
240
251
|
this.events = events;
|
|
241
252
|
this.identityReader = identityReader;
|
|
@@ -248,6 +259,7 @@ export class IdentityService {
|
|
|
248
259
|
this.cache = cache;
|
|
249
260
|
this.trustedDevices = trustedDevices;
|
|
250
261
|
this.twoFactorLockout = twoFactorLockout;
|
|
262
|
+
this.twoFactorDelivery = twoFactorDelivery;
|
|
251
263
|
this.sessions = sessions;
|
|
252
264
|
this.auth = createAuth({
|
|
253
265
|
db: drizzle.db,
|
|
@@ -265,6 +277,9 @@ export class IdentityService {
|
|
|
265
277
|
},
|
|
266
278
|
}
|
|
267
279
|
: {}),
|
|
280
|
+
// Binds the plugin's OTP hook to the transport fan-out. Without it better-auth
|
|
281
|
+
// still mints codes for the `email`/`sms` methods and drops them on the floor.
|
|
282
|
+
twoFactor: { sendOtp: (args) => twoFactorDelivery.deliver(args) },
|
|
268
283
|
requireEmailVerification: this.platformConfig?.registration?.requireEmailVerification ?? false,
|
|
269
284
|
isExistingAccountSignUp: (lookupEmail) => this.existingAccountSignUps.has(lookupEmail.toLowerCase()),
|
|
270
285
|
isAdminPasswordReset: (lookupEmail) => this.isAdminPasswordReset(lookupEmail),
|
|
@@ -613,7 +628,15 @@ export class IdentityService {
|
|
|
613
628
|
this.forwardCookies(authResponse, resHeaders);
|
|
614
629
|
const body = (await authResponse.json());
|
|
615
630
|
if (body.twoFactorRedirect || !body.user || !body.token) {
|
|
616
|
-
|
|
631
|
+
// The challenge screen has to know whether to ask for an authenticator code or
|
|
632
|
+
// to push one, and there is no session yet to read the account's method from.
|
|
633
|
+
// Naming it here reveals nothing the challenge would not show anyway.
|
|
634
|
+
return {
|
|
635
|
+
twoFactorRedirect: true,
|
|
636
|
+
...(existingUser
|
|
637
|
+
? { twoFactorMethod: await this.resolveTwoFactorMethod(existingUser.id) }
|
|
638
|
+
: {}),
|
|
639
|
+
};
|
|
617
640
|
}
|
|
618
641
|
if (lockoutEnabled && existingUser) {
|
|
619
642
|
await this.clearLockout(existingUser.id);
|
|
@@ -945,10 +968,39 @@ export class IdentityService {
|
|
|
945
968
|
}
|
|
946
969
|
return toUser(session.user);
|
|
947
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Starts enrolment. All three methods share the one secret better-auth mints here;
|
|
973
|
+
* `method` only decides how the code that activates it arrives, and is written
|
|
974
|
+
* before the send so the delivery hook knows where to route. better-auth flips
|
|
975
|
+
* `twoFactorEnabled` only once the first code clears, so abandoning a first
|
|
976
|
+
* enrolment leaves the account unprotected exactly as it was.
|
|
977
|
+
*
|
|
978
|
+
* A transport failure on the first send throws (see `sendOtpForCurrentMethod`)
|
|
979
|
+
* after `two_factor_method` is already written, but that write is inert on its
|
|
980
|
+
* own: `twoFactorStatus` reports it only once `enabled` is true, and the CONFLICT
|
|
981
|
+
* guard below only fires once it is. The caller recovers by retrying either
|
|
982
|
+
* `sendTwoFactorOtp` (resend) or this route again - both read the method that was
|
|
983
|
+
* already persisted, so neither loses the player's choice.
|
|
984
|
+
*/
|
|
948
985
|
async enableTwoFactor(input, reqHeaders, resHeaders) {
|
|
949
986
|
const headers = nodeHeadersToHeaders(reqHeaders);
|
|
950
987
|
const userId = await this.currentUserId(headers);
|
|
951
988
|
await assertRateLimit(this.limiter, `enable2fa:${userId ?? 'anonymous'}`, TWO_FACTOR_PASSWORD_RATE_LIMIT);
|
|
989
|
+
if (!userId) {
|
|
990
|
+
throw new ORPCError('UNAUTHORIZED', { message: 'Not signed in' });
|
|
991
|
+
}
|
|
992
|
+
await this.assertMethodAvailable(userId, input.method);
|
|
993
|
+
// There is no switch-in-place: a player changes method by disabling and enrolling
|
|
994
|
+
// again. better-auth's enable leg deletes and recreates the `twoFactor` row with a
|
|
995
|
+
// fresh secret and fresh backup codes, and marks it verified straight away when one
|
|
996
|
+
// was already verified - so re-running it on a live account destroys the working
|
|
997
|
+
// authenticator the moment the call lands, and abandoning the step strands the
|
|
998
|
+
// player on a secret they never scanned with `two_factor_enabled` still true.
|
|
999
|
+
if (await this.isTwoFactorEnabled(userId)) {
|
|
1000
|
+
throw new ORPCError('CONFLICT', {
|
|
1001
|
+
message: 'Two-factor authentication is already enabled. Disable it first to change method.',
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
952
1004
|
const res = await this.api.enableTwoFactor({
|
|
953
1005
|
body: { password: input.password },
|
|
954
1006
|
headers,
|
|
@@ -957,7 +1009,148 @@ export class IdentityService {
|
|
|
957
1009
|
await ensureOk(res);
|
|
958
1010
|
this.forwardCookies(res, resHeaders);
|
|
959
1011
|
const body = (await res.json());
|
|
960
|
-
|
|
1012
|
+
await this.setTwoFactorMethod(userId, input.method);
|
|
1013
|
+
if (input.method === 'app') {
|
|
1014
|
+
return { totpUri: body.totpURI, backupCodes: body.backupCodes };
|
|
1015
|
+
}
|
|
1016
|
+
// The first code has to reach the player before they can activate the method, so
|
|
1017
|
+
// the send belongs to enrolment rather than a separate call the client must know
|
|
1018
|
+
// to make. "Resend code" then replays it through sendTwoFactorOtp.
|
|
1019
|
+
const masked = await this.sendOtpForCurrentMethod(userId, headers, resHeaders);
|
|
1020
|
+
return { backupCodes: body.backupCodes, maskedDestination: masked };
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Re-sends the one-time code for an account on the `email` or `sms` method - the
|
|
1024
|
+
* "Resend code" action during enrolment, and the send leg of a login challenge.
|
|
1025
|
+
* Resolves its subject from a live session first and the pending-challenge cookie
|
|
1026
|
+
* second, because at login there is no session yet.
|
|
1027
|
+
*/
|
|
1028
|
+
async sendTwoFactorOtp(reqHeaders, resHeaders) {
|
|
1029
|
+
const headers = nodeHeadersToHeaders(reqHeaders);
|
|
1030
|
+
const pendingCookie = twoFactorPendingCookieValue(headers);
|
|
1031
|
+
const userId = (await this.currentUserId(headers)) ??
|
|
1032
|
+
(await this.twoFactorLockout?.resolvePendingUserId(pendingCookie));
|
|
1033
|
+
if (!userId) {
|
|
1034
|
+
throw new ORPCError('UNAUTHORIZED', { message: 'Not signed in' });
|
|
1035
|
+
}
|
|
1036
|
+
await assertRateLimit(this.limiter, `send2faOtp:${userId}`, SEND_2FA_OTP_RATE_LIMIT);
|
|
1037
|
+
// An account inside a lockout window cannot spend a code, so minting one only
|
|
1038
|
+
// hands out fresh guesses - and, on `sms`, bills the operator for them.
|
|
1039
|
+
await this.twoFactorLockout?.assertNotLocked(userId);
|
|
1040
|
+
return { maskedDestination: await this.sendOtpForCurrentMethod(userId, headers, resHeaders) };
|
|
1041
|
+
}
|
|
1042
|
+
/** Whether a second factor is active, which method it uses, and where a code can go. */
|
|
1043
|
+
async twoFactorStatus(reqHeaders) {
|
|
1044
|
+
const headers = nodeHeadersToHeaders(reqHeaders);
|
|
1045
|
+
const userId = await this.currentUserId(headers);
|
|
1046
|
+
if (!userId) {
|
|
1047
|
+
throw new ORPCError('UNAUTHORIZED', { message: 'Not signed in' });
|
|
1048
|
+
}
|
|
1049
|
+
const [row] = await this.drizzle.db
|
|
1050
|
+
.select({
|
|
1051
|
+
email: user.email,
|
|
1052
|
+
phoneNumber: user.phoneNumber,
|
|
1053
|
+
phoneVerified: user.phoneVerified,
|
|
1054
|
+
twoFactorEnabled: user.twoFactorEnabled,
|
|
1055
|
+
twoFactorMethod: user.twoFactorMethod,
|
|
1056
|
+
})
|
|
1057
|
+
.from(user)
|
|
1058
|
+
.where(eq(user.id, userId))
|
|
1059
|
+
.limit(1);
|
|
1060
|
+
if (!row) {
|
|
1061
|
+
throw new UserNotFoundError(userId);
|
|
1062
|
+
}
|
|
1063
|
+
const enabled = row.twoFactorEnabled ?? false;
|
|
1064
|
+
return {
|
|
1065
|
+
enabled,
|
|
1066
|
+
// The column outlives an abandoned enrolment, so an account that never cleared
|
|
1067
|
+
// its first code must still read as having no active method.
|
|
1068
|
+
method: enabled ? (row.twoFactorMethod ?? null) : null,
|
|
1069
|
+
maskedEmail: maskEmail(row.email),
|
|
1070
|
+
maskedPhone: row.phoneVerified && row.phoneNumber ? maskPhone(row.phoneNumber) : null,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Rejects a method the account cannot actually receive a code on. Enrolling into
|
|
1075
|
+
* `sms` without a verified phone would mint a secret the player can never clear,
|
|
1076
|
+
* locking them out of their own account at the next login.
|
|
1077
|
+
*/
|
|
1078
|
+
async assertMethodAvailable(userId, method) {
|
|
1079
|
+
if (method === 'app') {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const [row] = await this.drizzle.db
|
|
1083
|
+
.select({
|
|
1084
|
+
emailVerified: user.emailVerified,
|
|
1085
|
+
phoneNumber: user.phoneNumber,
|
|
1086
|
+
phoneVerified: user.phoneVerified,
|
|
1087
|
+
})
|
|
1088
|
+
.from(user)
|
|
1089
|
+
.where(eq(user.id, userId))
|
|
1090
|
+
.limit(1);
|
|
1091
|
+
// A code can only guard the account if the address it goes to is one the account
|
|
1092
|
+
// has proven it holds. `registration.requireEmailVerification` is off by default,
|
|
1093
|
+
// so an unverified or mistyped address would otherwise enrol and lock the player
|
|
1094
|
+
// out at the next login - the same failure the phone check already prevents.
|
|
1095
|
+
if (method === 'email' && !row?.emailVerified) {
|
|
1096
|
+
throw new ORPCError('BAD_REQUEST', {
|
|
1097
|
+
message: 'Verify your email address before using email for two-factor authentication',
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
if (method === 'sms' && (!row?.phoneNumber || !row.phoneVerified)) {
|
|
1101
|
+
throw new ORPCError('BAD_REQUEST', {
|
|
1102
|
+
message: 'Add and verify a phone number before using SMS for two-factor authentication',
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* The account's delivery method, defaulting to `app`: an enrolment that predates the
|
|
1108
|
+
* column reads as null, and every one of those was an authenticator.
|
|
1109
|
+
*/
|
|
1110
|
+
async resolveTwoFactorMethod(userId) {
|
|
1111
|
+
const [row] = await this.drizzle.db
|
|
1112
|
+
.select({ twoFactorMethod: user.twoFactorMethod })
|
|
1113
|
+
.from(user)
|
|
1114
|
+
.where(eq(user.id, userId))
|
|
1115
|
+
.limit(1);
|
|
1116
|
+
return row?.twoFactorMethod ?? 'app';
|
|
1117
|
+
}
|
|
1118
|
+
async isTwoFactorEnabled(userId) {
|
|
1119
|
+
const [row] = await this.drizzle.db
|
|
1120
|
+
.select({ twoFactorEnabled: user.twoFactorEnabled })
|
|
1121
|
+
.from(user)
|
|
1122
|
+
.where(eq(user.id, userId))
|
|
1123
|
+
.limit(1);
|
|
1124
|
+
return row?.twoFactorEnabled ?? false;
|
|
1125
|
+
}
|
|
1126
|
+
async setTwoFactorMethod(userId, method) {
|
|
1127
|
+
await this.drizzle.db.update(user).set({ twoFactorMethod: method }).where(eq(user.id, userId));
|
|
1128
|
+
}
|
|
1129
|
+
verifyChallengeCode(method, body, headers) {
|
|
1130
|
+
return verifyChallengeCode(this.api, method, body, headers);
|
|
1131
|
+
}
|
|
1132
|
+
async sendOtpForCurrentMethod(userId, headers, resHeaders) {
|
|
1133
|
+
const destination = await this.twoFactorDelivery.describeDestination(userId);
|
|
1134
|
+
if (!destination) {
|
|
1135
|
+
throw new ORPCError('BAD_REQUEST', {
|
|
1136
|
+
message: 'This account does not receive two-factor codes by email or SMS',
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
const res = await this.api.sendTwoFactorOTP({ body: {}, headers, asResponse: true });
|
|
1140
|
+
await ensureOk(res);
|
|
1141
|
+
// A 200 here means the code was minted, not that it left the building: the send
|
|
1142
|
+
// hook is wrapped in `.catch(logger.error)` upstream. Telling the player a code is
|
|
1143
|
+
// on its way when the transport just failed strands them on a screen waiting for
|
|
1144
|
+
// something that is never coming.
|
|
1145
|
+
const failure = this.twoFactorDelivery.takeFailure(userId);
|
|
1146
|
+
if (failure) {
|
|
1147
|
+
throw new ORPCError('SERVICE_UNAVAILABLE', {
|
|
1148
|
+
message: 'Could not send your verification code. Please try again.',
|
|
1149
|
+
cause: failure,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
this.forwardCookies(res, resHeaders);
|
|
1153
|
+
return destination.masked;
|
|
961
1154
|
}
|
|
962
1155
|
async verifyTwoFactor(input, reqHeaders, resHeaders) {
|
|
963
1156
|
const { ip, userAgent } = extractClientMeta(reqHeaders);
|
|
@@ -971,12 +1164,11 @@ export class IdentityService {
|
|
|
971
1164
|
await this.twoFactorLockout?.assertNotLocked(challengedUserId);
|
|
972
1165
|
}
|
|
973
1166
|
// A backup code is a single-use recovery credential, not a second factor to bind a
|
|
974
|
-
// browser to: it clears the challenge but never buys the trust window.
|
|
975
|
-
|
|
1167
|
+
// browser to: it clears the challenge but never buys the trust window. A pushed
|
|
1168
|
+
// `otp` is a real factor and does, the same as a live authenticator code.
|
|
1169
|
+
const trustDevice = input.trustDevice && input.method !== 'backup_code' && this.trustedDevices !== undefined;
|
|
976
1170
|
const body = { code: input.code, trustDevice };
|
|
977
|
-
const res = input.method
|
|
978
|
-
? await this.api.verifyBackupCode({ body, headers, asResponse: true })
|
|
979
|
-
: await this.api.verifyTOTP({ body, headers, asResponse: true });
|
|
1171
|
+
const res = await this.verifyChallengeCode(input.method, body, headers);
|
|
980
1172
|
if (!res.ok && challengedUserId) {
|
|
981
1173
|
await this.twoFactorLockout?.recordFailure(challengedUserId, { ip, userAgent });
|
|
982
1174
|
}
|
|
@@ -1065,13 +1257,18 @@ export class IdentityService {
|
|
|
1065
1257
|
const challengeHeaders = new Headers(headers);
|
|
1066
1258
|
challengeHeaders.set('cookie', requestCookieHeader(signIn));
|
|
1067
1259
|
await this.twoFactorLockout?.assertNotLocked(userId);
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1260
|
+
// Authenticator accounts only. This route replays a fresh sign-in leg, and
|
|
1261
|
+
// better-auth keys a pushed code by the pending-challenge cookie that leg mints -
|
|
1262
|
+
// so an `email`/`sms` account has no code that can answer this challenge, and
|
|
1263
|
+
// letting the call through would only spend its lockout budget on a credential it
|
|
1264
|
+
// is impossible to present. Refusing says so instead of failing as a wrong code.
|
|
1265
|
+
const challengeMethod = await resolveChallengeMethod(this.drizzle, userId);
|
|
1266
|
+
if (challengeMethod !== 'totp') {
|
|
1267
|
+
throw new ORPCError('CONFLICT', {
|
|
1268
|
+
message: 'Trusting this device requires an authenticator app.',
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
const verified = await this.verifyChallengeCode(challengeMethod, { code: input.code, trustDevice: true }, challengeHeaders);
|
|
1075
1272
|
if (!verified.ok) {
|
|
1076
1273
|
await this.twoFactorLockout?.recordFailure(userId, { ip, userAgent });
|
|
1077
1274
|
}
|
|
@@ -1083,7 +1280,7 @@ export class IdentityService {
|
|
|
1083
1280
|
this.events.emit('identity.2fa.verified', {
|
|
1084
1281
|
userId,
|
|
1085
1282
|
playerId,
|
|
1086
|
-
method:
|
|
1283
|
+
method: challengeMethod,
|
|
1087
1284
|
trustedDevice: true,
|
|
1088
1285
|
ip,
|
|
1089
1286
|
userAgent,
|
|
@@ -1108,11 +1305,7 @@ export class IdentityService {
|
|
|
1108
1305
|
*/
|
|
1109
1306
|
async assertFreshSecondFactor(userId, headers, meta, code) {
|
|
1110
1307
|
await this.twoFactorLockout?.assertNotLocked(userId);
|
|
1111
|
-
const res = await this.
|
|
1112
|
-
body: { code, trustDevice: false },
|
|
1113
|
-
headers,
|
|
1114
|
-
asResponse: true,
|
|
1115
|
-
});
|
|
1308
|
+
const res = await this.verifyChallengeCode(await resolveChallengeMethod(this.drizzle, userId), { code, trustDevice: false }, headers);
|
|
1116
1309
|
if (!res.ok) {
|
|
1117
1310
|
await this.twoFactorLockout?.recordFailure(userId, meta);
|
|
1118
1311
|
}
|
|
@@ -1156,6 +1349,9 @@ export class IdentityService {
|
|
|
1156
1349
|
throw new ORPCError('UNAUTHORIZED', { message: 'Not signed in' });
|
|
1157
1350
|
}
|
|
1158
1351
|
await this.assertFreshSecondFactor(userId, headers, { ip, userAgent }, input.code);
|
|
1352
|
+
// Read before the teardown clears it, so the audit trail records which method the
|
|
1353
|
+
// account was actually protected by rather than a blank.
|
|
1354
|
+
const disabledMethod = await resolveChallengeMethod(this.drizzle, userId);
|
|
1159
1355
|
const res = await this.api.disableTwoFactor({
|
|
1160
1356
|
body: { password: input.password },
|
|
1161
1357
|
headers,
|
|
@@ -1163,6 +1359,7 @@ export class IdentityService {
|
|
|
1163
1359
|
});
|
|
1164
1360
|
await ensureOk(res);
|
|
1165
1361
|
this.forwardCookies(res, resHeaders);
|
|
1362
|
+
await this.setTwoFactorMethod(userId, null);
|
|
1166
1363
|
// Dropping the second factor takes every standing bypass with it, the same teardown
|
|
1167
1364
|
// a Super Admin reset performs - a browser must not keep the access 2FA was guarding.
|
|
1168
1365
|
await this.trustedDevices?.revokeAllForUser(userId, userId);
|
|
@@ -1170,7 +1367,7 @@ export class IdentityService {
|
|
|
1170
1367
|
this.events.emit('identity.2fa.disabled', {
|
|
1171
1368
|
userId,
|
|
1172
1369
|
playerId: await this.identityReader.getPlayerIdByUserIdSafe(userId),
|
|
1173
|
-
method:
|
|
1370
|
+
method: disabledMethod,
|
|
1174
1371
|
ip,
|
|
1175
1372
|
userAgent,
|
|
1176
1373
|
});
|