@fonderie/auth 7.20.0 → 7.21.0
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/brain/outcomes.md +2 -1
- package/brain/signatures.md +4 -3
- package/dist/index.cjs +199 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +196 -118
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js.map +1 -1
- package/dist/{session-B-IgXFu8.d.cts → session-C1A21bvb.d.cts} +3 -2
- package/dist/{session-B-IgXFu8.d.ts → session-C1A21bvb.d.ts} +3 -2
- package/env.json +14 -7
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -3,8 +3,8 @@ export { IMfaChallenge, ISession } from './types.cjs';
|
|
|
3
3
|
import { IFonderieModule, IAdminDescription, IEventCatalogEntry, IReadinessProblem, IFonderieApp, IDefaultTemplate, IAdminRoute } from '@fonderie/core';
|
|
4
4
|
import { IStoreAdapter } from '@fonderie/store';
|
|
5
5
|
import { EventBus } from '@fonderie/events';
|
|
6
|
-
import { I as IAuthConfig, a as IRequestLocation } from './session-
|
|
7
|
-
export { A as AUTH_CONFIG_KEYS, b as AuthLimitedRoute, c as AuthMessageKey, C as CLIENT_KINDS, d as ClientKind, e as IAuthRateLimitConfig, f as IAuthRuntimeConfig, g as IAuthSecrets, h as IDataExportContributor, i as ILocationRequest, j as ISessionPolicy, L as LocationResolver, M as MESSAGE_KEYS, S as SESSION_POLICY_PRESETS, k as buildAuthAccountLimiter, l as buildAuthIpLimiter, m as clientKindOf, n as configForClient, r as readAuthRuntimeConfig, o as resolveLocation, s as sanitizeLocation, p as sessionPolicyConfigKeys, w as withSession } from './session-
|
|
6
|
+
import { I as IAuthConfig, a as IRequestLocation } from './session-C1A21bvb.cjs';
|
|
7
|
+
export { A as AUTH_CONFIG_KEYS, b as AuthLimitedRoute, c as AuthMessageKey, C as CLIENT_KINDS, d as ClientKind, e as IAuthRateLimitConfig, f as IAuthRuntimeConfig, g as IAuthSecrets, h as IDataExportContributor, i as ILocationRequest, j as ISessionPolicy, L as LocationResolver, M as MESSAGE_KEYS, S as SESSION_POLICY_PRESETS, k as buildAuthAccountLimiter, l as buildAuthIpLimiter, m as clientKindOf, n as configForClient, r as readAuthRuntimeConfig, o as resolveLocation, s as sanitizeLocation, p as sessionPolicyConfigKeys, w as withSession } from './session-C1A21bvb.cjs';
|
|
8
8
|
import { IUserDTO } from './dtos/user.cjs';
|
|
9
9
|
export { toUserDTO } from './dtos/user.cjs';
|
|
10
10
|
export { requireAuth, validate } from '@fonderie/core/middlewares';
|
|
@@ -75,6 +75,10 @@ declare const appleNativeSchema: z.ZodObject<{
|
|
|
75
75
|
identityToken: z.ZodString;
|
|
76
76
|
nonce: z.ZodOptional<z.ZodString>;
|
|
77
77
|
}, z.core.$strip>;
|
|
78
|
+
declare const googleNativeSchema: z.ZodObject<{
|
|
79
|
+
idToken: z.ZodString;
|
|
80
|
+
nonce: z.ZodOptional<z.ZodString>;
|
|
81
|
+
}, z.core.$strip>;
|
|
78
82
|
declare const refreshSchema: z.ZodObject<{
|
|
79
83
|
refreshToken: z.ZodOptional<z.ZodString>;
|
|
80
84
|
}, z.core.$strip>;
|
|
@@ -134,6 +138,7 @@ type schemas_ResetPasswordInput = ResetPasswordInput;
|
|
|
134
138
|
declare const schemas_appleNativeSchema: typeof appleNativeSchema;
|
|
135
139
|
declare const schemas_changePasswordSchema: typeof changePasswordSchema;
|
|
136
140
|
declare const schemas_forgotPasswordSchema: typeof forgotPasswordSchema;
|
|
141
|
+
declare const schemas_googleNativeSchema: typeof googleNativeSchema;
|
|
137
142
|
declare const schemas_loginSchema: typeof loginSchema;
|
|
138
143
|
declare const schemas_mfaTokenSchema: typeof mfaTokenSchema;
|
|
139
144
|
declare const schemas_refreshSchema: typeof refreshSchema;
|
|
@@ -145,7 +150,7 @@ declare const schemas_updatePreferencesSchema: typeof updatePreferencesSchema;
|
|
|
145
150
|
declare const schemas_updateProfileSchema: typeof updateProfileSchema;
|
|
146
151
|
declare const schemas_verifySchema: typeof verifySchema;
|
|
147
152
|
declare namespace schemas {
|
|
148
|
-
export { type schemas_ChangePasswordInput as ChangePasswordInput, type schemas_LoginInput as LoginInput, type schemas_RegisterInput as RegisterInput, type schemas_ResetPasswordInput as ResetPasswordInput, schemas_appleNativeSchema as appleNativeSchema, schemas_changePasswordSchema as changePasswordSchema, schemas_forgotPasswordSchema as forgotPasswordSchema, schemas_loginSchema as loginSchema, schemas_mfaTokenSchema as mfaTokenSchema, schemas_refreshSchema as refreshSchema, schemas_registerSchema as registerSchema, schemas_resetPasswordSchema as resetPasswordSchema, schemas_updateEmailSchema as updateEmailSchema, schemas_updatePhoneSchema as updatePhoneSchema, schemas_updatePreferencesSchema as updatePreferencesSchema, schemas_updateProfileSchema as updateProfileSchema, schemas_verifySchema as verifySchema };
|
|
153
|
+
export { type schemas_ChangePasswordInput as ChangePasswordInput, type schemas_LoginInput as LoginInput, type schemas_RegisterInput as RegisterInput, type schemas_ResetPasswordInput as ResetPasswordInput, schemas_appleNativeSchema as appleNativeSchema, schemas_changePasswordSchema as changePasswordSchema, schemas_forgotPasswordSchema as forgotPasswordSchema, schemas_googleNativeSchema as googleNativeSchema, schemas_loginSchema as loginSchema, schemas_mfaTokenSchema as mfaTokenSchema, schemas_refreshSchema as refreshSchema, schemas_registerSchema as registerSchema, schemas_resetPasswordSchema as resetPasswordSchema, schemas_updateEmailSchema as updateEmailSchema, schemas_updatePhoneSchema as updatePhoneSchema, schemas_updatePreferencesSchema as updatePreferencesSchema, schemas_updateProfileSchema as updateProfileSchema, schemas_verifySchema as verifySchema };
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
declare function normalizeEmail(email: string): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export { IMfaChallenge, ISession } from './types.js';
|
|
|
3
3
|
import { IFonderieModule, IAdminDescription, IEventCatalogEntry, IReadinessProblem, IFonderieApp, IDefaultTemplate, IAdminRoute } from '@fonderie/core';
|
|
4
4
|
import { IStoreAdapter } from '@fonderie/store';
|
|
5
5
|
import { EventBus } from '@fonderie/events';
|
|
6
|
-
import { I as IAuthConfig, a as IRequestLocation } from './session-
|
|
7
|
-
export { A as AUTH_CONFIG_KEYS, b as AuthLimitedRoute, c as AuthMessageKey, C as CLIENT_KINDS, d as ClientKind, e as IAuthRateLimitConfig, f as IAuthRuntimeConfig, g as IAuthSecrets, h as IDataExportContributor, i as ILocationRequest, j as ISessionPolicy, L as LocationResolver, M as MESSAGE_KEYS, S as SESSION_POLICY_PRESETS, k as buildAuthAccountLimiter, l as buildAuthIpLimiter, m as clientKindOf, n as configForClient, r as readAuthRuntimeConfig, o as resolveLocation, s as sanitizeLocation, p as sessionPolicyConfigKeys, w as withSession } from './session-
|
|
6
|
+
import { I as IAuthConfig, a as IRequestLocation } from './session-C1A21bvb.js';
|
|
7
|
+
export { A as AUTH_CONFIG_KEYS, b as AuthLimitedRoute, c as AuthMessageKey, C as CLIENT_KINDS, d as ClientKind, e as IAuthRateLimitConfig, f as IAuthRuntimeConfig, g as IAuthSecrets, h as IDataExportContributor, i as ILocationRequest, j as ISessionPolicy, L as LocationResolver, M as MESSAGE_KEYS, S as SESSION_POLICY_PRESETS, k as buildAuthAccountLimiter, l as buildAuthIpLimiter, m as clientKindOf, n as configForClient, r as readAuthRuntimeConfig, o as resolveLocation, s as sanitizeLocation, p as sessionPolicyConfigKeys, w as withSession } from './session-C1A21bvb.js';
|
|
8
8
|
import { IUserDTO } from './dtos/user.js';
|
|
9
9
|
export { toUserDTO } from './dtos/user.js';
|
|
10
10
|
export { requireAuth, validate } from '@fonderie/core/middlewares';
|
|
@@ -75,6 +75,10 @@ declare const appleNativeSchema: z.ZodObject<{
|
|
|
75
75
|
identityToken: z.ZodString;
|
|
76
76
|
nonce: z.ZodOptional<z.ZodString>;
|
|
77
77
|
}, z.core.$strip>;
|
|
78
|
+
declare const googleNativeSchema: z.ZodObject<{
|
|
79
|
+
idToken: z.ZodString;
|
|
80
|
+
nonce: z.ZodOptional<z.ZodString>;
|
|
81
|
+
}, z.core.$strip>;
|
|
78
82
|
declare const refreshSchema: z.ZodObject<{
|
|
79
83
|
refreshToken: z.ZodOptional<z.ZodString>;
|
|
80
84
|
}, z.core.$strip>;
|
|
@@ -134,6 +138,7 @@ type schemas_ResetPasswordInput = ResetPasswordInput;
|
|
|
134
138
|
declare const schemas_appleNativeSchema: typeof appleNativeSchema;
|
|
135
139
|
declare const schemas_changePasswordSchema: typeof changePasswordSchema;
|
|
136
140
|
declare const schemas_forgotPasswordSchema: typeof forgotPasswordSchema;
|
|
141
|
+
declare const schemas_googleNativeSchema: typeof googleNativeSchema;
|
|
137
142
|
declare const schemas_loginSchema: typeof loginSchema;
|
|
138
143
|
declare const schemas_mfaTokenSchema: typeof mfaTokenSchema;
|
|
139
144
|
declare const schemas_refreshSchema: typeof refreshSchema;
|
|
@@ -145,7 +150,7 @@ declare const schemas_updatePreferencesSchema: typeof updatePreferencesSchema;
|
|
|
145
150
|
declare const schemas_updateProfileSchema: typeof updateProfileSchema;
|
|
146
151
|
declare const schemas_verifySchema: typeof verifySchema;
|
|
147
152
|
declare namespace schemas {
|
|
148
|
-
export { type schemas_ChangePasswordInput as ChangePasswordInput, type schemas_LoginInput as LoginInput, type schemas_RegisterInput as RegisterInput, type schemas_ResetPasswordInput as ResetPasswordInput, schemas_appleNativeSchema as appleNativeSchema, schemas_changePasswordSchema as changePasswordSchema, schemas_forgotPasswordSchema as forgotPasswordSchema, schemas_loginSchema as loginSchema, schemas_mfaTokenSchema as mfaTokenSchema, schemas_refreshSchema as refreshSchema, schemas_registerSchema as registerSchema, schemas_resetPasswordSchema as resetPasswordSchema, schemas_updateEmailSchema as updateEmailSchema, schemas_updatePhoneSchema as updatePhoneSchema, schemas_updatePreferencesSchema as updatePreferencesSchema, schemas_updateProfileSchema as updateProfileSchema, schemas_verifySchema as verifySchema };
|
|
153
|
+
export { type schemas_ChangePasswordInput as ChangePasswordInput, type schemas_LoginInput as LoginInput, type schemas_RegisterInput as RegisterInput, type schemas_ResetPasswordInput as ResetPasswordInput, schemas_appleNativeSchema as appleNativeSchema, schemas_changePasswordSchema as changePasswordSchema, schemas_forgotPasswordSchema as forgotPasswordSchema, schemas_googleNativeSchema as googleNativeSchema, schemas_loginSchema as loginSchema, schemas_mfaTokenSchema as mfaTokenSchema, schemas_refreshSchema as refreshSchema, schemas_registerSchema as registerSchema, schemas_resetPasswordSchema as resetPasswordSchema, schemas_updateEmailSchema as updateEmailSchema, schemas_updatePhoneSchema as updatePhoneSchema, schemas_updatePreferencesSchema as updatePreferencesSchema, schemas_updateProfileSchema as updateProfileSchema, schemas_verifySchema as verifySchema };
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
declare function normalizeEmail(email: string): string;
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,10 @@ var requireEmailLogin = async (ctx, next) => {
|
|
|
59
59
|
}
|
|
60
60
|
return next();
|
|
61
61
|
};
|
|
62
|
+
var requireEmailLoginUnlessSigningIn = async (ctx, next) => {
|
|
63
|
+
if (ctx.user.mfaPending) return next();
|
|
64
|
+
return requireEmailLogin(ctx, next);
|
|
65
|
+
};
|
|
62
66
|
|
|
63
67
|
// src/middlewares/validate.ts
|
|
64
68
|
import { validate } from "@fonderie/core/middlewares";
|
|
@@ -132,6 +136,7 @@ __export(schemas_exports, {
|
|
|
132
136
|
appleNativeSchema: () => appleNativeSchema,
|
|
133
137
|
changePasswordSchema: () => changePasswordSchema,
|
|
134
138
|
forgotPasswordSchema: () => forgotPasswordSchema,
|
|
139
|
+
googleNativeSchema: () => googleNativeSchema,
|
|
135
140
|
loginSchema: () => loginSchema,
|
|
136
141
|
mfaTokenSchema: () => mfaTokenSchema,
|
|
137
142
|
refreshSchema: () => refreshSchema,
|
|
@@ -169,6 +174,12 @@ var appleNativeSchema = z.object({
|
|
|
169
174
|
// it against the token's `nonce` claim to defend against replay.
|
|
170
175
|
nonce: z.string().max(256).optional()
|
|
171
176
|
});
|
|
177
|
+
var googleNativeSchema = z.object({
|
|
178
|
+
idToken: z.string().min(1).max(8192),
|
|
179
|
+
// Optional nonce the app passed to the Google SDK; checked against the
|
|
180
|
+
// token's `nonce` claim (binds the token to this request).
|
|
181
|
+
nonce: z.string().max(256).optional()
|
|
182
|
+
});
|
|
172
183
|
var refreshSchema = z.object({ refreshToken: z.string().min(1).optional() });
|
|
173
184
|
var forgotPasswordSchema = z.object({ email });
|
|
174
185
|
var resetPasswordSchema = z.union([
|
|
@@ -2800,11 +2811,59 @@ function userController(store, config, bus) {
|
|
|
2800
2811
|
}
|
|
2801
2812
|
|
|
2802
2813
|
// src/controllers/oauth.controller.ts
|
|
2803
|
-
import { randomBytes as randomBytes4,
|
|
2814
|
+
import { randomBytes as randomBytes4, createHash as createHash4 } from "crypto";
|
|
2804
2815
|
import jwt2 from "jsonwebtoken";
|
|
2805
2816
|
import { setApiResponse as setApiResponse5, HTTP as HTTP5, constantTimeEqual as constantTimeEqual2, background as background5 } from "@fonderie/core";
|
|
2806
2817
|
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT4 } from "@fonderie/events";
|
|
2807
2818
|
|
|
2819
|
+
// src/services/jwks.ts
|
|
2820
|
+
import { createPublicKey } from "crypto";
|
|
2821
|
+
var TTL_MS = 60 * 60 * 1e3;
|
|
2822
|
+
var MIN_REFETCH_MS = 60 * 1e3;
|
|
2823
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
2824
|
+
function createJwksSource(url) {
|
|
2825
|
+
let cache = null;
|
|
2826
|
+
let inflight = null;
|
|
2827
|
+
let lastAttempt = 0;
|
|
2828
|
+
const refresh = () => {
|
|
2829
|
+
if (inflight) return inflight;
|
|
2830
|
+
lastAttempt = Date.now();
|
|
2831
|
+
inflight = (async () => {
|
|
2832
|
+
try {
|
|
2833
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
2834
|
+
const data = await res.json();
|
|
2835
|
+
if (Array.isArray(data.keys)) cache = { keys: data.keys, fetchedAt: Date.now() };
|
|
2836
|
+
} catch {
|
|
2837
|
+
} finally {
|
|
2838
|
+
inflight = null;
|
|
2839
|
+
}
|
|
2840
|
+
})();
|
|
2841
|
+
return inflight;
|
|
2842
|
+
};
|
|
2843
|
+
return {
|
|
2844
|
+
async getKey(kid) {
|
|
2845
|
+
const find = () => cache?.keys.find((k) => k.kid === kid) ?? null;
|
|
2846
|
+
const fresh = () => !!cache && Date.now() - cache.fetchedAt < TTL_MS;
|
|
2847
|
+
let jwk = find();
|
|
2848
|
+
if ((!jwk || !fresh()) && Date.now() - lastAttempt >= MIN_REFETCH_MS) {
|
|
2849
|
+
await refresh();
|
|
2850
|
+
jwk = find();
|
|
2851
|
+
}
|
|
2852
|
+
if (!jwk) return null;
|
|
2853
|
+
try {
|
|
2854
|
+
return createPublicKey({ key: jwk, format: "jwk" });
|
|
2855
|
+
} catch {
|
|
2856
|
+
return null;
|
|
2857
|
+
}
|
|
2858
|
+
},
|
|
2859
|
+
reset() {
|
|
2860
|
+
cache = null;
|
|
2861
|
+
inflight = null;
|
|
2862
|
+
lastAttempt = 0;
|
|
2863
|
+
}
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2808
2867
|
// src/models/consumed-token.model.ts
|
|
2809
2868
|
var ConsumedTokenModel = class {
|
|
2810
2869
|
constructor(store) {
|
|
@@ -2844,49 +2903,14 @@ function mintAppleClientSecret(apple) {
|
|
|
2844
2903
|
{ algorithm: "ES256", keyid: apple.keyId }
|
|
2845
2904
|
);
|
|
2846
2905
|
}
|
|
2847
|
-
var
|
|
2848
|
-
var appleKeyInflight = null;
|
|
2849
|
-
var appleKeyLastAttempt = 0;
|
|
2850
|
-
var APPLE_KEYS_TTL_MS = 60 * 60 * 1e3;
|
|
2851
|
-
var APPLE_KEYS_MIN_REFETCH_MS = 60 * 1e3;
|
|
2852
|
-
var APPLE_KEYS_FETCH_TIMEOUT_MS = 5e3;
|
|
2853
|
-
function refreshAppleKeys() {
|
|
2854
|
-
if (appleKeyInflight) return appleKeyInflight;
|
|
2855
|
-
appleKeyLastAttempt = Date.now();
|
|
2856
|
-
appleKeyInflight = (async () => {
|
|
2857
|
-
try {
|
|
2858
|
-
const res = await fetch(APPLE_KEYS_URL, { signal: AbortSignal.timeout(APPLE_KEYS_FETCH_TIMEOUT_MS) });
|
|
2859
|
-
const data = await res.json();
|
|
2860
|
-
if (Array.isArray(data.keys)) appleKeyCache = { keys: data.keys, fetchedAt: Date.now() };
|
|
2861
|
-
} catch {
|
|
2862
|
-
} finally {
|
|
2863
|
-
appleKeyInflight = null;
|
|
2864
|
-
}
|
|
2865
|
-
})();
|
|
2866
|
-
return appleKeyInflight;
|
|
2867
|
-
}
|
|
2868
|
-
async function getApplePublicKey(kid) {
|
|
2869
|
-
const find = () => appleKeyCache?.keys.find((k) => k.kid === kid) ?? null;
|
|
2870
|
-
const cacheFresh = () => !!appleKeyCache && Date.now() - appleKeyCache.fetchedAt < APPLE_KEYS_TTL_MS;
|
|
2871
|
-
let jwk = find();
|
|
2872
|
-
if ((!jwk || !cacheFresh()) && Date.now() - appleKeyLastAttempt >= APPLE_KEYS_MIN_REFETCH_MS) {
|
|
2873
|
-
await refreshAppleKeys();
|
|
2874
|
-
jwk = find();
|
|
2875
|
-
}
|
|
2876
|
-
if (!jwk) return null;
|
|
2877
|
-
try {
|
|
2878
|
-
return createPublicKey({ key: jwk, format: "jwk" });
|
|
2879
|
-
} catch {
|
|
2880
|
-
return null;
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2906
|
+
var appleKeys = createJwksSource(APPLE_KEYS_URL);
|
|
2883
2907
|
async function verifyAppleIdToken(idToken, opts) {
|
|
2884
2908
|
if (opts.audiences.length === 0) return null;
|
|
2885
2909
|
const decoded = jwt2.decode(idToken, { complete: true });
|
|
2886
2910
|
if (!decoded || typeof decoded === "string") return null;
|
|
2887
2911
|
const kid = decoded.header.kid;
|
|
2888
2912
|
if (!kid) return null;
|
|
2889
|
-
const key = await
|
|
2913
|
+
const key = await appleKeys.getKey(kid);
|
|
2890
2914
|
if (!key) return null;
|
|
2891
2915
|
try {
|
|
2892
2916
|
const payload = jwt2.verify(idToken, key, {
|
|
@@ -2904,6 +2928,30 @@ async function verifyAppleIdToken(idToken, opts) {
|
|
|
2904
2928
|
}
|
|
2905
2929
|
}
|
|
2906
2930
|
var appleEmailVerified = (c) => c.email_verified === true || c.email_verified === "true";
|
|
2931
|
+
var GOOGLE_KEYS_URL = "https://www.googleapis.com/oauth2/v3/certs";
|
|
2932
|
+
var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
|
|
2933
|
+
var googleKeys = createJwksSource(GOOGLE_KEYS_URL);
|
|
2934
|
+
async function verifyGoogleIdToken(idToken, opts) {
|
|
2935
|
+
if (opts.audiences.length === 0) return null;
|
|
2936
|
+
const decoded = jwt2.decode(idToken, { complete: true });
|
|
2937
|
+
if (!decoded || typeof decoded === "string") return null;
|
|
2938
|
+
const kid = decoded.header.kid;
|
|
2939
|
+
if (!kid) return null;
|
|
2940
|
+
const key = await googleKeys.getKey(kid);
|
|
2941
|
+
if (!key) return null;
|
|
2942
|
+
try {
|
|
2943
|
+
const payload = jwt2.verify(idToken, key, {
|
|
2944
|
+
algorithms: ["RS256"],
|
|
2945
|
+
issuer: [...GOOGLE_ISSUERS],
|
|
2946
|
+
audience: opts.audiences
|
|
2947
|
+
});
|
|
2948
|
+
if (opts.nonce !== void 0 && payload.nonce !== opts.nonce) return null;
|
|
2949
|
+
return payload;
|
|
2950
|
+
} catch {
|
|
2951
|
+
return null;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
var googleEmailVerified = (c) => c.email_verified === true || c.email_verified === "true";
|
|
2907
2955
|
function oauthController(store, config, bus) {
|
|
2908
2956
|
const users = new UserModel(store);
|
|
2909
2957
|
const sessions = new SessionModel(store, config.location);
|
|
@@ -2966,48 +3014,68 @@ function oauthController(store, config, bus) {
|
|
|
2966
3014
|
));
|
|
2967
3015
|
}
|
|
2968
3016
|
};
|
|
2969
|
-
const
|
|
3017
|
+
const PROVIDER = {
|
|
3018
|
+
apple: { label: "Apple", failed: "APPLE_AUTH_FAILED", success: "APPLE_AUTH_SUCCESS" },
|
|
3019
|
+
google: { label: "Google", failed: "GOOGLE_AUTH_FAILED", success: "GOOGLE_AUTH_SUCCESS" }
|
|
3020
|
+
};
|
|
3021
|
+
const completeOAuthLogin = async (ctx, provider, claims, clearCookies = []) => {
|
|
3022
|
+
const p = PROVIDER[provider];
|
|
3023
|
+
const method = provider === "apple" ? "oauth-apple" : "oauth-google";
|
|
2970
3024
|
const meta = requestMeta(ctx);
|
|
2971
3025
|
if (!claims.email) {
|
|
2972
|
-
return setApiResponse5(HTTP5.BAD_REQUEST,
|
|
3026
|
+
return setApiResponse5(HTTP5.BAD_REQUEST, p.failed, `No email in ${p.label} identity token`);
|
|
2973
3027
|
}
|
|
2974
3028
|
if (!claims.emailVerified) {
|
|
2975
|
-
return setApiResponse5(HTTP5.BAD_REQUEST,
|
|
3029
|
+
return setApiResponse5(HTTP5.BAD_REQUEST, p.failed, `${p.label} account email is not verified`);
|
|
2976
3030
|
}
|
|
2977
3031
|
const normalizedEmail = normalizeEmailSafe(claims.email);
|
|
2978
3032
|
if (!normalizedEmail) {
|
|
2979
|
-
return setApiResponse5(HTTP5.BAD_REQUEST,
|
|
3033
|
+
return setApiResponse5(HTTP5.BAD_REQUEST, p.failed, `Invalid email in ${p.label} identity token`);
|
|
2980
3034
|
}
|
|
2981
|
-
const upserted = await users.upsertByProvider(normalizedEmail,
|
|
3035
|
+
const upserted = await users.upsertByProvider(normalizedEmail, provider, claims.sub ?? "");
|
|
2982
3036
|
if (!upserted) {
|
|
2983
3037
|
await loginEvents.recordSafe({
|
|
2984
3038
|
userId: null,
|
|
2985
3039
|
emailAttempted: normalizedEmail,
|
|
2986
|
-
method
|
|
3040
|
+
method,
|
|
2987
3041
|
outcome: "failed",
|
|
2988
3042
|
failureReason: "provider_upsert_failed",
|
|
2989
3043
|
...meta
|
|
2990
3044
|
});
|
|
2991
|
-
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR",
|
|
3045
|
+
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
|
|
2992
3046
|
}
|
|
2993
3047
|
const fullUser = await users.findById(upserted.id);
|
|
2994
3048
|
if (!fullUser) {
|
|
2995
|
-
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR",
|
|
3049
|
+
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
|
|
2996
3050
|
}
|
|
2997
|
-
await announceOAuthUpsert(ctx, upserted,
|
|
2998
|
-
|
|
3051
|
+
await announceOAuthUpsert(ctx, upserted, provider, fullUser);
|
|
3052
|
+
if (fullUser.suspended) {
|
|
3053
|
+
return setApiResponse5(HTTP5.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
|
|
3054
|
+
}
|
|
3055
|
+
if (fullUser.mfaEnabled) {
|
|
3056
|
+
const mfaToken = issueMfaPendingToken(upserted.id, config, provider);
|
|
3057
|
+
return Response.json(
|
|
3058
|
+
{
|
|
3059
|
+
reason: "MFA_REQUIRED",
|
|
3060
|
+
explanation: "Multi-factor authentication required",
|
|
3061
|
+
result: { mfaToken }
|
|
3062
|
+
},
|
|
3063
|
+
{ status: 200, headers: cookieHeaders(clearCookies) }
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, configForClient(config, config.resolve?.(ctx), clientKindOf(ctx.request.headers)), { loginMethod: provider });
|
|
2999
3067
|
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
3000
3068
|
await loginEvents.recordSafe({
|
|
3001
3069
|
userId: upserted.id,
|
|
3002
3070
|
emailAttempted: normalizedEmail,
|
|
3003
|
-
method
|
|
3071
|
+
method,
|
|
3004
3072
|
outcome: "success",
|
|
3005
3073
|
...meta
|
|
3006
3074
|
});
|
|
3007
3075
|
return Response.json(
|
|
3008
3076
|
{
|
|
3009
|
-
reason:
|
|
3010
|
-
explanation:
|
|
3077
|
+
reason: p.success,
|
|
3078
|
+
explanation: `${p.label} authentication successful.`,
|
|
3011
3079
|
result: {
|
|
3012
3080
|
tokens: { access: accessToken, refresh: refreshToken },
|
|
3013
3081
|
user: toUserDTO(fullUser)
|
|
@@ -3015,18 +3083,16 @@ function oauthController(store, config, bus) {
|
|
|
3015
3083
|
},
|
|
3016
3084
|
{
|
|
3017
3085
|
status: 200,
|
|
3018
|
-
headers: cookieHeaders([
|
|
3019
|
-
...tokenPairCookies(accessToken, refreshToken, config),
|
|
3020
|
-
// One-time value — clear the (SameSite=None) state cookie.
|
|
3021
|
-
"oauth_state=; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=0"
|
|
3022
|
-
])
|
|
3086
|
+
headers: cookieHeaders([...tokenPairCookies(accessToken, refreshToken, config), ...clearCookies])
|
|
3023
3087
|
}
|
|
3024
3088
|
);
|
|
3025
3089
|
};
|
|
3090
|
+
const APPLE_STATE_CLEAR = "oauth_state=; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=0";
|
|
3091
|
+
const GOOGLE_STATE_CLEAR = "oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0";
|
|
3026
3092
|
return {
|
|
3027
3093
|
googleInit: async (_ctx) => {
|
|
3028
3094
|
const google = config.google;
|
|
3029
|
-
if (!google) {
|
|
3095
|
+
if (!google || !google.clientSecret || !google.redirectUri) {
|
|
3030
3096
|
return setApiResponse5(
|
|
3031
3097
|
HTTP5.NOT_IMPLEMENTED,
|
|
3032
3098
|
"NOT_CONFIGURED",
|
|
@@ -3061,7 +3127,7 @@ function oauthController(store, config, bus) {
|
|
|
3061
3127
|
},
|
|
3062
3128
|
googleCallback: async (ctx) => {
|
|
3063
3129
|
const google = config.google;
|
|
3064
|
-
if (!google) {
|
|
3130
|
+
if (!google || !google.clientSecret || !google.redirectUri) {
|
|
3065
3131
|
return setApiResponse5(
|
|
3066
3132
|
HTTP5.NOT_IMPLEMENTED,
|
|
3067
3133
|
"NOT_CONFIGURED",
|
|
@@ -3124,57 +3190,11 @@ function oauthController(store, config, bus) {
|
|
|
3124
3190
|
"Google account email is not verified"
|
|
3125
3191
|
);
|
|
3126
3192
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
const upserted = await users.upsertByProvider(normalizedEmail, "google", payload.sub ?? "");
|
|
3133
|
-
if (!upserted) {
|
|
3134
|
-
await loginEvents.recordSafe({
|
|
3135
|
-
userId: null,
|
|
3136
|
-
emailAttempted: normalizedEmail,
|
|
3137
|
-
method: "oauth-google",
|
|
3138
|
-
outcome: "failed",
|
|
3139
|
-
failureReason: "provider_upsert_failed",
|
|
3140
|
-
...meta
|
|
3141
|
-
});
|
|
3142
|
-
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
3143
|
-
}
|
|
3144
|
-
const fullUser = await users.findById(upserted.id);
|
|
3145
|
-
if (!fullUser) {
|
|
3146
|
-
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
3147
|
-
}
|
|
3148
|
-
await announceOAuthUpsert(ctx, upserted, "google", fullUser);
|
|
3149
|
-
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, configForClient(config, config.resolve?.(ctx), clientKindOf(ctx.request.headers)), {
|
|
3150
|
-
loginMethod: "google"
|
|
3151
|
-
});
|
|
3152
|
-
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
3153
|
-
await loginEvents.recordSafe({
|
|
3154
|
-
userId: upserted.id,
|
|
3155
|
-
emailAttempted: normalizedEmail,
|
|
3156
|
-
method: "oauth-google",
|
|
3157
|
-
outcome: "success",
|
|
3158
|
-
...meta
|
|
3159
|
-
});
|
|
3160
|
-
return Response.json(
|
|
3161
|
-
{
|
|
3162
|
-
reason: "GOOGLE_AUTH_SUCCESS",
|
|
3163
|
-
explanation: "Google authentication successful.",
|
|
3164
|
-
result: {
|
|
3165
|
-
tokens: { access: accessToken, refresh: refreshToken },
|
|
3166
|
-
user: toUserDTO(fullUser)
|
|
3167
|
-
}
|
|
3168
|
-
},
|
|
3169
|
-
{
|
|
3170
|
-
status: 200,
|
|
3171
|
-
headers: cookieHeaders([
|
|
3172
|
-
...tokenPairCookies(accessToken, refreshToken, config),
|
|
3173
|
-
// One-time value — clear it once the flow completes.
|
|
3174
|
-
"oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0"
|
|
3175
|
-
])
|
|
3176
|
-
}
|
|
3177
|
-
);
|
|
3193
|
+
return completeOAuthLogin(ctx, "google", {
|
|
3194
|
+
email: payload.email,
|
|
3195
|
+
sub: payload.sub,
|
|
3196
|
+
emailVerified: payload.email_verified === true
|
|
3197
|
+
}, [GOOGLE_STATE_CLEAR]);
|
|
3178
3198
|
},
|
|
3179
3199
|
appleInit: async (_ctx) => {
|
|
3180
3200
|
const apple = config.apple;
|
|
@@ -3247,11 +3267,11 @@ function oauthController(store, config, bus) {
|
|
|
3247
3267
|
if (!claims) {
|
|
3248
3268
|
return setApiResponse5(HTTP5.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
|
|
3249
3269
|
}
|
|
3250
|
-
return
|
|
3270
|
+
return completeOAuthLogin(ctx, "apple", {
|
|
3251
3271
|
email: claims.email,
|
|
3252
3272
|
sub: claims.sub,
|
|
3253
3273
|
emailVerified: appleEmailVerified(claims)
|
|
3254
|
-
});
|
|
3274
|
+
}, [APPLE_STATE_CLEAR]);
|
|
3255
3275
|
},
|
|
3256
3276
|
appleNative: async (ctx) => {
|
|
3257
3277
|
const apple = config.apple;
|
|
@@ -3297,10 +3317,54 @@ function oauthController(store, config, bus) {
|
|
|
3297
3317
|
);
|
|
3298
3318
|
}
|
|
3299
3319
|
consumedTokens.sweepExpiredSafe();
|
|
3300
|
-
return
|
|
3320
|
+
return completeOAuthLogin(ctx, "apple", {
|
|
3301
3321
|
email: claims.email,
|
|
3302
3322
|
sub: claims.sub,
|
|
3303
3323
|
emailVerified: appleEmailVerified(claims)
|
|
3324
|
+
}, [APPLE_STATE_CLEAR]);
|
|
3325
|
+
},
|
|
3326
|
+
// Sign in with Google from a native app: the Google SDK's ID token,
|
|
3327
|
+
// verified here (signature, issuer, audience, expiry), redeemed once.
|
|
3328
|
+
googleNative: async (ctx) => {
|
|
3329
|
+
const audiences = config.google?.nativeClientIds ?? [];
|
|
3330
|
+
if (!config.google || audiences.length === 0) {
|
|
3331
|
+
return setApiResponse5(HTTP5.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Native Google sign-in not configured");
|
|
3332
|
+
}
|
|
3333
|
+
const body = ctx.meta["body"];
|
|
3334
|
+
const idToken = body?.idToken;
|
|
3335
|
+
if (!idToken) {
|
|
3336
|
+
return setApiResponse5(HTTP5.BAD_REQUEST, "INVALID_PARAMETER", "Missing idToken");
|
|
3337
|
+
}
|
|
3338
|
+
const claims = await verifyGoogleIdToken(idToken, { audiences, nonce: body?.nonce });
|
|
3339
|
+
if (!claims) {
|
|
3340
|
+
await loginEvents.recordSafe({
|
|
3341
|
+
userId: null,
|
|
3342
|
+
emailAttempted: null,
|
|
3343
|
+
method: "oauth-google",
|
|
3344
|
+
outcome: "failed",
|
|
3345
|
+
failureReason: "invalid_identity_token",
|
|
3346
|
+
...requestMeta(ctx)
|
|
3347
|
+
});
|
|
3348
|
+
return setApiResponse5(HTTP5.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
|
|
3349
|
+
}
|
|
3350
|
+
const tokenHash = createHash4("sha256").update(idToken).digest("hex");
|
|
3351
|
+
const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
|
|
3352
|
+
if (!await consumedTokens.consumeOnce(tokenHash, new Date(expMs))) {
|
|
3353
|
+
await loginEvents.recordSafe({
|
|
3354
|
+
userId: null,
|
|
3355
|
+
emailAttempted: claims.email ?? null,
|
|
3356
|
+
method: "oauth-google",
|
|
3357
|
+
outcome: "failed",
|
|
3358
|
+
failureReason: "token_replayed",
|
|
3359
|
+
...requestMeta(ctx)
|
|
3360
|
+
});
|
|
3361
|
+
return setApiResponse5(HTTP5.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "This Google ID token has already been used");
|
|
3362
|
+
}
|
|
3363
|
+
consumedTokens.sweepExpiredSafe();
|
|
3364
|
+
return completeOAuthLogin(ctx, "google", {
|
|
3365
|
+
email: claims.email,
|
|
3366
|
+
sub: claims.sub,
|
|
3367
|
+
emailVerified: googleEmailVerified(claims)
|
|
3304
3368
|
});
|
|
3305
3369
|
}
|
|
3306
3370
|
};
|
|
@@ -3385,7 +3449,7 @@ function buildAuthRoutes(store, config, bus, locales) {
|
|
|
3385
3449
|
R("mfaSetup", "POST", "/auth/mfa/setup", requireAuth, requireEmailLogin, requireVerified, mfa.setup),
|
|
3386
3450
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
3387
3451
|
// and full tokens (setup confirmation), so requireAnyAuth is used here.
|
|
3388
|
-
R("mfaVerify", "POST", "/auth/mfa/verify", ipLimit("mfaVerify"), requireAnyAuth,
|
|
3452
|
+
R("mfaVerify", "POST", "/auth/mfa/verify", ipLimit("mfaVerify"), requireAnyAuth, requireEmailLoginUnlessSigningIn, requireVerified, validate(mfaTokenSchema), mfa.verify),
|
|
3389
3453
|
R("mfaDisable", "POST", "/auth/mfa/disable", requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.disable),
|
|
3390
3454
|
R("mfaBackupCodes", "POST", "/auth/mfa/backup-codes", requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.regenerateBackupCodes)
|
|
3391
3455
|
];
|
|
@@ -3394,7 +3458,9 @@ function buildAuthRoutes(store, config, bus, locales) {
|
|
|
3394
3458
|
["GET", "/auth/google", oauth.googleInit],
|
|
3395
3459
|
// ipLimit: an unauthenticated caller can otherwise force one outbound
|
|
3396
3460
|
// token-exchange to Google per request (the state check doesn't gate it).
|
|
3397
|
-
["GET", "/auth/google/callback", ipLimit("login"), oauth.googleCallback]
|
|
3461
|
+
["GET", "/auth/google/callback", ipLimit("login"), oauth.googleCallback],
|
|
3462
|
+
// Native (Android/iOS) flow: the app posts the Google SDK's ID token.
|
|
3463
|
+
["POST", "/auth/google/native", ipLimit("login"), validate(googleNativeSchema), oauth.googleNative]
|
|
3398
3464
|
);
|
|
3399
3465
|
}
|
|
3400
3466
|
if (config.providers.includes("apple")) {
|
|
@@ -3460,15 +3526,18 @@ function collectAuthConfigProblems(config) {
|
|
|
3460
3526
|
}
|
|
3461
3527
|
if (config.google) {
|
|
3462
3528
|
const clientSecret = config.google.clientSecret ?? "";
|
|
3463
|
-
|
|
3529
|
+
const web = Boolean(clientSecret || config.google.redirectUri);
|
|
3530
|
+
const webComplete = Boolean(clientSecret && config.google.redirectUri);
|
|
3531
|
+
const native = (config.google.nativeClientIds ?? []).filter(Boolean).length > 0;
|
|
3532
|
+
if (!config.google.clientId || web && !webComplete || !webComplete && !native) {
|
|
3464
3533
|
problems.push({
|
|
3465
3534
|
module: MODULE,
|
|
3466
3535
|
severity: "error",
|
|
3467
|
-
message: "google OAuth is configured but clientId, clientSecret
|
|
3536
|
+
message: "google OAuth is configured but incomplete: set clientId, and clientSecret + redirectUri (web sign-in) and/or nativeClientIds (app sign-in)",
|
|
3468
3537
|
domain: "auth",
|
|
3469
3538
|
reason: "GOOGLE_INCOMPLETE"
|
|
3470
3539
|
});
|
|
3471
|
-
} else if (PLACEHOLDER_SECRET.test(clientSecret)) {
|
|
3540
|
+
} else if (clientSecret && PLACEHOLDER_SECRET.test(clientSecret)) {
|
|
3472
3541
|
problems.push({
|
|
3473
3542
|
module: MODULE,
|
|
3474
3543
|
severity: "error",
|
|
@@ -3477,6 +3546,15 @@ function collectAuthConfigProblems(config) {
|
|
|
3477
3546
|
reason: "GOOGLE_SECRET_PLACEHOLDER"
|
|
3478
3547
|
});
|
|
3479
3548
|
}
|
|
3549
|
+
if (config.google.nativeClientIds?.some((id) => !id || id.trim() === "" || id.includes("*"))) {
|
|
3550
|
+
problems.push({
|
|
3551
|
+
module: MODULE,
|
|
3552
|
+
severity: "error",
|
|
3553
|
+
message: "google.nativeClientIds must be exact client ids \u2014 empty or wildcard entries would accept ID tokens minted for other apps",
|
|
3554
|
+
domain: "auth",
|
|
3555
|
+
reason: "GOOGLE_NATIVE_IDS_INVALID"
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3480
3558
|
}
|
|
3481
3559
|
if (config.apple) {
|
|
3482
3560
|
const a = config.apple;
|
|
@@ -3736,7 +3814,7 @@ var AuthModule = class {
|
|
|
3736
3814
|
name = "@fonderie/auth";
|
|
3737
3815
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
3738
3816
|
// say what is actually deployed rather than 'not reported'.
|
|
3739
|
-
version = "7.
|
|
3817
|
+
version = "7.21.0";
|
|
3740
3818
|
// Users, sessions, login history, suspend — only through @fonderie/admin.
|
|
3741
3819
|
describeAdmin() {
|
|
3742
3820
|
return { routes: describeAuthAdminRoutes(this.store, this.bus) };
|