@shware/security 7.1.0 → 8.0.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/README.md +108 -30
- package/dist/auth/attributes.cjs +5 -5
- package/dist/auth/attributes.cjs.map +1 -1
- package/dist/auth/attributes.d.cts +3 -5
- package/dist/auth/attributes.d.cts.map +1 -1
- package/dist/auth/attributes.d.mts +3 -5
- package/dist/auth/attributes.d.mts.map +1 -1
- package/dist/auth/attributes.mjs +5 -5
- package/dist/auth/attributes.mjs.map +1 -1
- package/dist/auth/desktop-codes.cjs +57 -0
- package/dist/auth/desktop-codes.cjs.map +1 -0
- package/dist/auth/desktop-codes.d.cts +24 -0
- package/dist/auth/desktop-codes.d.cts.map +1 -0
- package/dist/auth/desktop-codes.d.mts +24 -0
- package/dist/auth/desktop-codes.d.mts.map +1 -0
- package/dist/auth/desktop-codes.mjs +55 -0
- package/dist/auth/desktop-codes.mjs.map +1 -0
- package/dist/auth/email-codes.cjs +77 -0
- package/dist/auth/email-codes.cjs.map +1 -0
- package/dist/auth/email-codes.d.cts +39 -0
- package/dist/auth/email-codes.d.cts.map +1 -0
- package/dist/auth/email-codes.d.mts +39 -0
- package/dist/auth/email-codes.d.mts.map +1 -0
- package/dist/auth/email-codes.mjs +75 -0
- package/dist/auth/email-codes.mjs.map +1 -0
- package/dist/auth/error.cjs +47 -0
- package/dist/auth/error.cjs.map +1 -0
- package/dist/auth/error.d.cts +88 -0
- package/dist/auth/error.d.cts.map +1 -0
- package/dist/auth/error.d.mts +88 -0
- package/dist/auth/error.d.mts.map +1 -0
- package/dist/auth/error.mjs +45 -0
- package/dist/auth/error.mjs.map +1 -0
- package/dist/auth/index.cjs +245 -385
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +66 -57
- package/dist/auth/index.d.cts.map +1 -1
- package/dist/auth/index.d.mts +66 -57
- package/dist/auth/index.d.mts.map +1 -1
- package/dist/auth/index.mjs +248 -388
- package/dist/auth/index.mjs.map +1 -1
- package/dist/auth/respond.cjs +99 -0
- package/dist/auth/respond.cjs.map +1 -0
- package/dist/auth/respond.d.cts +39 -0
- package/dist/auth/respond.d.cts.map +1 -0
- package/dist/auth/respond.d.mts +39 -0
- package/dist/auth/respond.d.mts.map +1 -0
- package/dist/auth/respond.mjs +92 -0
- package/dist/auth/respond.mjs.map +1 -0
- package/dist/auth/types.d.cts +27 -13
- package/dist/auth/types.d.cts.map +1 -1
- package/dist/auth/types.d.mts +27 -13
- package/dist/auth/types.d.mts.map +1 -1
- package/dist/core/index.cjs +1 -2
- package/dist/core/index.d.cts +0 -2
- package/dist/core/index.d.cts.map +1 -1
- package/dist/core/index.d.mts +0 -2
- package/dist/core/index.d.mts.map +1 -1
- package/dist/core/index.mjs +1 -2
- package/dist/email/schema.cjs +1 -2
- package/dist/email/schema.d.cts +0 -2
- package/dist/email/schema.d.cts.map +1 -1
- package/dist/email/schema.d.mts +0 -2
- package/dist/email/schema.d.mts.map +1 -1
- package/dist/email/schema.mjs +1 -2
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +4 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +3 -1
- package/dist/oauth2/client.cjs +1 -2
- package/dist/oauth2/client.d.cts +0 -2
- package/dist/oauth2/client.d.cts.map +1 -1
- package/dist/oauth2/client.d.mts +0 -2
- package/dist/oauth2/client.d.mts.map +1 -1
- package/dist/oauth2/client.mjs +1 -2
- package/dist/oauth2/error.cjs +1 -9
- package/dist/oauth2/error.cjs.map +1 -1
- package/dist/oauth2/error.d.cts +0 -5
- package/dist/oauth2/error.d.cts.map +1 -1
- package/dist/oauth2/error.d.mts +0 -5
- package/dist/oauth2/error.d.mts.map +1 -1
- package/dist/oauth2/error.mjs +2 -9
- package/dist/oauth2/error.mjs.map +1 -1
- package/dist/oauth2/name.cjs +1 -2
- package/dist/oauth2/name.d.cts +0 -2
- package/dist/oauth2/name.d.cts.map +1 -1
- package/dist/oauth2/name.d.mts +0 -2
- package/dist/oauth2/name.d.mts.map +1 -1
- package/dist/oauth2/name.mjs +1 -2
- package/dist/oauth2/provider/apple-client-secret.cjs +1 -2
- package/dist/oauth2/provider/apple-client-secret.d.cts +0 -2
- package/dist/oauth2/provider/apple-client-secret.d.cts.map +1 -1
- package/dist/oauth2/provider/apple-client-secret.d.mts +0 -2
- package/dist/oauth2/provider/apple-client-secret.d.mts.map +1 -1
- package/dist/oauth2/provider/apple-client-secret.mjs +1 -2
- package/dist/oauth2/provider/apple.cjs +1 -2
- package/dist/oauth2/provider/apple.d.cts +0 -2
- package/dist/oauth2/provider/apple.d.cts.map +1 -1
- package/dist/oauth2/provider/apple.d.mts +0 -2
- package/dist/oauth2/provider/apple.d.mts.map +1 -1
- package/dist/oauth2/provider/apple.mjs +1 -2
- package/dist/oauth2/provider/common.cjs +2 -3
- package/dist/oauth2/provider/common.cjs.map +1 -1
- package/dist/oauth2/provider/common.d.cts +0 -2
- package/dist/oauth2/provider/common.d.cts.map +1 -1
- package/dist/oauth2/provider/common.d.mts +0 -2
- package/dist/oauth2/provider/common.d.mts.map +1 -1
- package/dist/oauth2/provider/common.mjs +2 -3
- package/dist/oauth2/provider/common.mjs.map +1 -1
- package/dist/oauth2/provider/facebook.cjs +3 -6
- package/dist/oauth2/provider/facebook.cjs.map +1 -1
- package/dist/oauth2/provider/facebook.d.cts +0 -3
- package/dist/oauth2/provider/facebook.d.cts.map +1 -1
- package/dist/oauth2/provider/facebook.d.mts +0 -3
- package/dist/oauth2/provider/facebook.d.mts.map +1 -1
- package/dist/oauth2/provider/facebook.mjs +3 -6
- package/dist/oauth2/provider/facebook.mjs.map +1 -1
- package/dist/oauth2/provider/github.cjs +1 -2
- package/dist/oauth2/provider/github.d.cts +0 -2
- package/dist/oauth2/provider/github.d.cts.map +1 -1
- package/dist/oauth2/provider/github.d.mts +0 -2
- package/dist/oauth2/provider/github.d.mts.map +1 -1
- package/dist/oauth2/provider/github.mjs +1 -2
- package/dist/oauth2/provider/google.cjs +1 -2
- package/dist/oauth2/provider/google.d.cts +0 -2
- package/dist/oauth2/provider/google.d.cts.map +1 -1
- package/dist/oauth2/provider/google.d.mts +0 -2
- package/dist/oauth2/provider/google.d.mts.map +1 -1
- package/dist/oauth2/provider/google.mjs +1 -2
- package/dist/oauth2/provider/wechat.cjs +1 -2
- package/dist/oauth2/provider/wechat.d.cts +0 -2
- package/dist/oauth2/provider/wechat.d.cts.map +1 -1
- package/dist/oauth2/provider/wechat.d.mts +0 -2
- package/dist/oauth2/provider/wechat.d.mts.map +1 -1
- package/dist/oauth2/provider/wechat.mjs +1 -2
- package/dist/oauth2/provider/x.cjs +7 -6
- package/dist/oauth2/provider/x.cjs.map +1 -1
- package/dist/oauth2/provider/x.d.cts +3 -4
- package/dist/oauth2/provider/x.d.cts.map +1 -1
- package/dist/oauth2/provider/x.d.mts +3 -4
- package/dist/oauth2/provider/x.d.mts.map +1 -1
- package/dist/oauth2/provider/x.mjs +7 -6
- package/dist/oauth2/provider/x.mjs.map +1 -1
- package/dist/oauth2/types.cjs +1 -2
- package/dist/oauth2/types.cjs.map +1 -1
- package/dist/oauth2/types.d.cts +5 -2
- package/dist/oauth2/types.d.cts.map +1 -1
- package/dist/oauth2/types.d.mts +5 -2
- package/dist/oauth2/types.d.mts.map +1 -1
- package/dist/oauth2/types.mjs +1 -2
- package/dist/oauth2/types.mjs.map +1 -1
- package/dist/session/common.cjs +11 -15
- package/dist/session/common.cjs.map +1 -1
- package/dist/session/common.d.cts +7 -5
- package/dist/session/common.d.cts.map +1 -1
- package/dist/session/common.d.mts +7 -5
- package/dist/session/common.d.mts.map +1 -1
- package/dist/session/common.mjs +11 -14
- package/dist/session/common.mjs.map +1 -1
- package/dist/session/db.cjs +3 -4
- package/dist/session/db.cjs.map +1 -1
- package/dist/session/db.d.cts +0 -2
- package/dist/session/db.d.cts.map +1 -1
- package/dist/session/db.d.mts +0 -2
- package/dist/session/db.d.mts.map +1 -1
- package/dist/session/db.mjs +4 -5
- package/dist/session/db.mjs.map +1 -1
- package/dist/session/map-session.cjs +1 -2
- package/dist/session/map-session.d.cts +0 -2
- package/dist/session/map-session.d.cts.map +1 -1
- package/dist/session/map-session.d.mts +0 -2
- package/dist/session/map-session.d.mts.map +1 -1
- package/dist/session/map-session.mjs +1 -2
- package/dist/session/redis.cjs +7 -10
- package/dist/session/redis.cjs.map +1 -1
- package/dist/session/redis.d.cts +0 -2
- package/dist/session/redis.d.cts.map +1 -1
- package/dist/session/redis.d.mts +0 -2
- package/dist/session/redis.d.mts.map +1 -1
- package/dist/session/redis.mjs +8 -11
- package/dist/session/redis.mjs.map +1 -1
- package/dist/session/types.d.cts +0 -14
- package/dist/session/types.d.cts.map +1 -1
- package/dist/session/types.d.mts +0 -14
- package/dist/session/types.d.mts.map +1 -1
- package/dist/utils/http.cjs +1 -2
- package/dist/utils/http.d.cts +0 -2
- package/dist/utils/http.d.cts.map +1 -1
- package/dist/utils/http.d.mts +0 -2
- package/dist/utils/http.d.mts.map +1 -1
- package/dist/utils/http.mjs +1 -2
- package/dist/utils/oauth2.cjs +1 -2
- package/dist/utils/oauth2.d.cts +0 -2
- package/dist/utils/oauth2.d.cts.map +1 -1
- package/dist/utils/oauth2.d.mts +0 -2
- package/dist/utils/oauth2.d.mts.map +1 -1
- package/dist/utils/oauth2.mjs +1 -2
- package/dist/utils/timing.cjs +1 -2
- package/dist/utils/timing.d.cts +0 -2
- package/dist/utils/timing.d.cts.map +1 -1
- package/dist/utils/timing.d.mts +0 -2
- package/dist/utils/timing.d.mts.map +1 -1
- package/dist/utils/timing.mjs +1 -2
- package/dist/utils/valid.cjs +35 -54
- package/dist/utils/valid.cjs.map +1 -1
- package/dist/utils/valid.d.cts +8 -25
- package/dist/utils/valid.d.cts.map +1 -1
- package/dist/utils/valid.d.mts +8 -25
- package/dist/utils/valid.d.mts.map +1 -1
- package/dist/utils/valid.mjs +33 -52
- package/dist/utils/valid.mjs.map +1 -1
- package/package.json +4 -4
package/dist/auth/index.cjs
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_auth_error = require("./error.cjs");
|
|
3
|
+
const require_oauth2_error = require("../oauth2/error.cjs");
|
|
4
|
+
const require_auth_respond = require("./respond.cjs");
|
|
2
5
|
const require_auth_attributes = require("./attributes.cjs");
|
|
3
6
|
const require_email_schema = require("../email/schema.cjs");
|
|
4
|
-
const require_oauth2_error = require("../oauth2/error.cjs");
|
|
5
7
|
const require_oauth2_client = require("../oauth2/client.cjs");
|
|
6
8
|
const require_session_common = require("../session/common.cjs");
|
|
7
9
|
const require_utils_http = require("../utils/http.cjs");
|
|
8
10
|
const require_utils_timing = require("../utils/timing.cjs");
|
|
9
11
|
const require_utils_valid = require("../utils/valid.cjs");
|
|
12
|
+
const require_auth_desktop_codes = require("./desktop-codes.cjs");
|
|
13
|
+
const require_auth_email_codes = require("./email-codes.cjs");
|
|
10
14
|
let node_crypto = require("node:crypto");
|
|
11
15
|
let _shware_utils = require("@shware/utils");
|
|
12
|
-
|
|
16
|
+
let zod_mini = require("zod/mini");
|
|
17
|
+
// src/auth/index.ts
|
|
13
18
|
const PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;
|
|
14
19
|
const CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
/** The header a session id travels in besides the cookie, with `session.header` on. */
|
|
21
|
+
const SESSION_HEADER = "X-Auth-Token";
|
|
22
|
+
/** Seconds an OAuth2 state or nonce may be used within. */
|
|
23
|
+
const OAUTH2_PARAMETER_TTL = 600;
|
|
19
24
|
const MAX_ERROR_DESCRIPTION_LENGTH = 256;
|
|
20
25
|
/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */
|
|
21
26
|
function safeErrorType(error, fallback) {
|
|
@@ -42,14 +47,20 @@ function authorizationAttributes(attributes) {
|
|
|
42
47
|
return defined;
|
|
43
48
|
}
|
|
44
49
|
/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */
|
|
45
|
-
const INVALID_ATTRIBUTES = {
|
|
46
|
-
error: "invalid_request",
|
|
47
|
-
error_description: `attributes must map strings to strings, at most ${require_auth_attributes.MAX_ATTRIBUTES_BYTES} bytes`
|
|
48
|
-
};
|
|
49
50
|
function isFormPost(request) {
|
|
50
51
|
const contentType = request.headers.get("content-type")?.toLowerCase();
|
|
51
52
|
return request.method === "POST" && !!contentType?.startsWith("application/x-www-form-urlencoded");
|
|
52
53
|
}
|
|
54
|
+
/** Google's own form post carries no attributes; an app posting JSON may add them. */
|
|
55
|
+
const oneTapBodySchema = (0, zod_mini.extend)(require_oauth2_client.googleOneTapSchema, { attributes: require_auth_attributes.attributesSchema });
|
|
56
|
+
const desktopAuthorizeSchema = (0, zod_mini.object)({
|
|
57
|
+
code_challenge: (0, zod_mini.string)().check((0, zod_mini.regex)(PKCE_FORMAT)),
|
|
58
|
+
code_challenge_method: (0, zod_mini.literal)("S256")
|
|
59
|
+
});
|
|
60
|
+
const desktopExchangeSchema = (0, zod_mini.object)({
|
|
61
|
+
code: (0, zod_mini.string)(),
|
|
62
|
+
code_verifier: (0, zod_mini.string)().check((0, zod_mini.regex)(PKCE_FORMAT))
|
|
63
|
+
});
|
|
53
64
|
const PATH = {
|
|
54
65
|
CSRF: "/csrf",
|
|
55
66
|
LOGOUT: "/logout",
|
|
@@ -61,15 +72,18 @@ const PATH = {
|
|
|
61
72
|
LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:provider",
|
|
62
73
|
LOGIN_OAUTH2_ONETAP: "/login/oauth2/onetap/google",
|
|
63
74
|
LOGIN_EMAIL: "/login/email",
|
|
64
|
-
LOGIN_PHONE: "/login/phone",
|
|
65
75
|
SEND_EMAIL_VERIFICATION_CODE: "/auth/sendEmailVerificationCode",
|
|
66
|
-
SEND_PHONE_VERIFICATION_CODE: "/auth/sendPhoneVerificationCode",
|
|
67
76
|
DESKTOP_AUTHORIZE: "/auth/desktop/authorize",
|
|
68
77
|
DESKTOP_EXCHANGE: "/auth/desktop/exchange",
|
|
69
78
|
CLEANUP_EXPIRED_SESSIONS: "/sessions/expired/cleanup"
|
|
70
79
|
};
|
|
71
|
-
/**
|
|
72
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The login routes and session helpers. Every route is one straight line: read and validate the
|
|
82
|
+
* request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and
|
|
83
|
+
* answered in one place per kind of route: a JSON route runs under `guard`, which maps a
|
|
84
|
+
* `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see
|
|
85
|
+
* `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).
|
|
86
|
+
*/
|
|
73
87
|
var Auth = class {
|
|
74
88
|
timing;
|
|
75
89
|
cookieName;
|
|
@@ -77,21 +91,27 @@ var Auth = class {
|
|
|
77
91
|
kv;
|
|
78
92
|
repository;
|
|
79
93
|
turnstileSecretKey;
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
emailCodes;
|
|
95
|
+
desktopCodes;
|
|
82
96
|
oauth2Client;
|
|
83
97
|
onetapRequireNonce;
|
|
84
98
|
touchInterval;
|
|
99
|
+
sessionHeader;
|
|
85
100
|
ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
|
|
86
101
|
constructor({ sessionRepository, kvRepository, turnstileSecretKey, verificationCode, oauth2, cookie, session, timing }) {
|
|
87
102
|
this.timing = timing ?? false;
|
|
88
103
|
this.kv = kvRepository;
|
|
89
104
|
this.repository = sessionRepository;
|
|
90
105
|
this.turnstileSecretKey = turnstileSecretKey;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
(0, _shware_utils.invariant)(
|
|
94
|
-
(0, _shware_utils.invariant)(
|
|
106
|
+
const maxAttempts = verificationCode?.maxAttempts ?? 5;
|
|
107
|
+
const resendCooldown = verificationCode?.resendCooldown ?? 60;
|
|
108
|
+
(0, _shware_utils.invariant)(maxAttempts >= 1, "verificationCode.maxAttempts must be at least 1");
|
|
109
|
+
(0, _shware_utils.invariant)(resendCooldown >= 1, "verificationCode.resendCooldown must be at least 1 second");
|
|
110
|
+
this.emailCodes = new require_auth_email_codes.EmailVerificationCodes(kvRepository, {
|
|
111
|
+
maxAttempts,
|
|
112
|
+
resendCooldown
|
|
113
|
+
});
|
|
114
|
+
this.desktopCodes = new require_auth_desktop_codes.DesktopCodes(kvRepository);
|
|
95
115
|
const { name, ...cookieOptions } = cookie ?? {};
|
|
96
116
|
this.cookieName = name ?? "SESSION";
|
|
97
117
|
this.cookieOptions = {
|
|
@@ -104,14 +124,76 @@ var Auth = class {
|
|
|
104
124
|
this.oauth2Client = oauth2?.client ? new require_oauth2_client.OAuth2Client(oauth2.client) : null;
|
|
105
125
|
this.onetapRequireNonce = oauth2?.onetap?.requireNonce ?? true;
|
|
106
126
|
this.touchInterval = (session?.touchInterval ?? 3600) * 1e3;
|
|
127
|
+
this.sessionHeader = session?.header ?? false;
|
|
107
128
|
(0, _shware_utils.invariant)(this.touchInterval >= 0, "session.touchInterval must not be negative");
|
|
108
129
|
}
|
|
109
130
|
getSessionId(request) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
const fromCookie = require_utils_http.getCookie(request, this.cookieName);
|
|
132
|
+
if (fromCookie) return fromCookie;
|
|
133
|
+
return this.sessionHeader && request.headers.get(SESSION_HEADER) || void 0;
|
|
134
|
+
}
|
|
135
|
+
/** The session the request carries, if it exists. */
|
|
136
|
+
async loadSession(request) {
|
|
137
|
+
const sessionId = this.getSessionId(request);
|
|
138
|
+
return sessionId ? this.repository.findById(sessionId) : null;
|
|
139
|
+
}
|
|
140
|
+
/** The signed-in principal of a session, if any. */
|
|
141
|
+
principalOf(session) {
|
|
142
|
+
const name = session.getAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME);
|
|
143
|
+
return name ? { name: String(name) } : null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login
|
|
147
|
+
* session moves to the new one; the authorization request (state, PKCE verifier, flow
|
|
148
|
+
* attributes) is single-use and was consumed, and a principal left there from an earlier
|
|
149
|
+
* sign-in is replaced. The new id is never the one the browser brought, which may have been
|
|
150
|
+
* planted by an attacker who started the flow (session fixation).
|
|
151
|
+
*/
|
|
152
|
+
async signIn(principal, carryOver) {
|
|
153
|
+
const session = this.repository.createSession();
|
|
154
|
+
for (const attribute of carryOver?.getAttributeNames() ?? []) {
|
|
155
|
+
if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;
|
|
156
|
+
if (attribute === "principalName") continue;
|
|
157
|
+
const value = carryOver?.getAttribute(attribute);
|
|
158
|
+
if (value !== null && value !== void 0) session.setAttribute(attribute, value);
|
|
159
|
+
}
|
|
160
|
+
session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
161
|
+
await this.repository.save(session);
|
|
162
|
+
return session;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with
|
|
166
|
+
* `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.
|
|
167
|
+
*/
|
|
168
|
+
withSession(response, session, cookie) {
|
|
169
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
170
|
+
require_utils_http.setCookie(response, this.cookieName, session.getId(), {
|
|
171
|
+
...this.cookieOptions,
|
|
172
|
+
...cookie,
|
|
173
|
+
maxAge
|
|
174
|
+
});
|
|
175
|
+
if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
/** Runs a JSON route: a failure it knows is its response, anything else is the host's. */
|
|
179
|
+
async guard(run) {
|
|
180
|
+
try {
|
|
181
|
+
return await run();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const response = require_auth_respond.errorResponse(error);
|
|
184
|
+
if (response) return response;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */
|
|
189
|
+
redirect(error, description, refusal) {
|
|
190
|
+
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
191
|
+
const uri = new URL(this.oauth2Client.errorUri);
|
|
192
|
+
for (const [key, value] of Object.entries(refusal?.metadata ?? {})) if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);
|
|
193
|
+
uri.searchParams.set("error", error);
|
|
194
|
+
if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
|
|
195
|
+
if (refusal) uri.searchParams.set("reason", refusal.reason);
|
|
196
|
+
return Response.redirect(uri.href, 302);
|
|
115
197
|
}
|
|
116
198
|
csrf = async (_request) => {
|
|
117
199
|
const token = (0, node_crypto.randomUUID)();
|
|
@@ -137,21 +219,13 @@ var Auth = class {
|
|
|
137
219
|
return response;
|
|
138
220
|
};
|
|
139
221
|
logged = async (request, onLogged) => {
|
|
140
|
-
const
|
|
141
|
-
if (!sessionId) return Response.json({ data: false });
|
|
142
|
-
const session = await this.repository.findById(sessionId);
|
|
222
|
+
const session = await this.loadSession(request);
|
|
143
223
|
if (!session) return Response.json({ data: false });
|
|
144
|
-
const name = session.getAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME);
|
|
145
224
|
session.setLastAccessedTime(Date.now());
|
|
146
225
|
await this.repository.save(session);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
require_utils_http.setCookie(response, this.cookieName, session.getId(), {
|
|
151
|
-
...this.cookieOptions,
|
|
152
|
-
maxAge
|
|
153
|
-
});
|
|
154
|
-
return response;
|
|
226
|
+
const principal = this.principalOf(session);
|
|
227
|
+
if (principal) await onLogged?.(principal);
|
|
228
|
+
return this.withSession(Response.json({ data: principal !== null }), session);
|
|
155
229
|
};
|
|
156
230
|
createPkceParameters() {
|
|
157
231
|
const code_verifier = (0, node_crypto.randomUUID)();
|
|
@@ -167,34 +241,29 @@ var Auth = class {
|
|
|
167
241
|
getOauth2NonceKey(nonce) {
|
|
168
242
|
return `oauth2:nonce:${nonce}`;
|
|
169
243
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
getPhoneVerificationCodeKey(phone) {
|
|
180
|
-
return `phone:verification:${phone}`;
|
|
181
|
-
}
|
|
182
|
-
getDesktopCodeKey(code) {
|
|
183
|
-
return `desktop:code:${code}`;
|
|
244
|
+
/**
|
|
245
|
+
* Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,
|
|
246
|
+
* or was used already. An id token is replayable until it expires; the nonce it carries makes
|
|
247
|
+
* it sign in once.
|
|
248
|
+
*/
|
|
249
|
+
async consumeNonce(nonce, provider) {
|
|
250
|
+
const key = this.getOauth2NonceKey(nonce);
|
|
251
|
+
if (await this.kv.getItem(key) !== provider) throw new require_auth_error.AuthenticationError("NONCE_MISMATCH");
|
|
252
|
+
await this.kv.removeItem(key);
|
|
184
253
|
}
|
|
185
254
|
oauth2State = async (request) => {
|
|
186
255
|
const { provider } = require_utils_http.param(request, PATH.OAUTH2_STATE);
|
|
187
256
|
const state = (0, node_crypto.randomUUID)();
|
|
188
|
-
await this.kv.setItem(this.getOauth2StateKey(state), provider,
|
|
257
|
+
await this.kv.setItem(this.getOauth2StateKey(state), provider, OAUTH2_PARAMETER_TTL);
|
|
189
258
|
return Response.json({ state });
|
|
190
259
|
};
|
|
191
260
|
oauth2Nonce = async (request) => {
|
|
192
261
|
const { provider } = require_utils_http.param(request, PATH.OAUTH2_NONCE);
|
|
193
262
|
const nonce = (0, node_crypto.randomUUID)();
|
|
194
|
-
await this.kv.setItem(this.getOauth2NonceKey(nonce), provider,
|
|
263
|
+
await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, OAUTH2_PARAMETER_TTL);
|
|
195
264
|
return Response.json({ nonce });
|
|
196
265
|
};
|
|
197
|
-
oauth2Authorization =
|
|
266
|
+
oauth2Authorization = (request, options) => this.guard(async () => {
|
|
198
267
|
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
199
268
|
const attributes = authorizationAttributes(options?.attributes);
|
|
200
269
|
const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
|
|
@@ -206,8 +275,7 @@ var Auth = class {
|
|
|
206
275
|
state,
|
|
207
276
|
pkce
|
|
208
277
|
});
|
|
209
|
-
const
|
|
210
|
-
const session = sessionId ? await this.repository.findById(sessionId) ?? this.repository.createSession() : this.repository.createSession();
|
|
278
|
+
const session = await this.loadSession(request) ?? this.repository.createSession();
|
|
211
279
|
mark("fetch_session");
|
|
212
280
|
const authorizationRequest = {
|
|
213
281
|
state,
|
|
@@ -216,56 +284,38 @@ var Auth = class {
|
|
|
216
284
|
additionalParameters: pkce,
|
|
217
285
|
...attributes ? { attributes } : {}
|
|
218
286
|
};
|
|
219
|
-
|
|
220
|
-
session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);
|
|
287
|
+
session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, JSON.stringify(authorizationRequest));
|
|
221
288
|
await this.repository.save(session);
|
|
222
289
|
mark("save_session");
|
|
223
290
|
const response = new Response(null, {
|
|
224
291
|
status: 302,
|
|
225
292
|
headers: { location: uri.href }
|
|
226
293
|
});
|
|
227
|
-
const maxAge = session.getMaxInactiveInterval();
|
|
228
|
-
require_utils_http.setCookie(response, this.cookieName, session.getId(), {
|
|
229
|
-
...this.cookieOptions,
|
|
230
|
-
maxAge
|
|
231
|
-
});
|
|
232
294
|
setTiming(response);
|
|
233
|
-
return response;
|
|
234
|
-
};
|
|
235
|
-
redirect = (error, description) => {
|
|
236
|
-
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
237
|
-
const uri = new URL(this.oauth2Client.errorUri);
|
|
238
|
-
uri.searchParams.set("error", error);
|
|
239
|
-
if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
|
|
240
|
-
return Response.redirect(uri.href, 302);
|
|
241
|
-
};
|
|
295
|
+
return this.withSession(response, session);
|
|
296
|
+
});
|
|
242
297
|
loginOAuth2Code = async (request, handler) => {
|
|
243
298
|
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
244
299
|
const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
|
|
245
|
-
const
|
|
246
|
-
const session = sessionId ? await this.repository.findById(sessionId) : null;
|
|
300
|
+
const session = await this.loadSession(request);
|
|
247
301
|
if (!session) return this.redirect("invalid_request", "session not found");
|
|
248
302
|
mark("load_session");
|
|
249
303
|
const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
|
|
250
304
|
if (!json) return this.redirect("invalid_request", "authorization request not found");
|
|
251
305
|
const cached = JSON.parse(json);
|
|
252
306
|
const { provider } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_CODE);
|
|
253
|
-
|
|
254
|
-
if (isFormPost(request)) {
|
|
255
|
-
const params = new URLSearchParams(await request.text());
|
|
256
|
-
data = Object.fromEntries(params.entries());
|
|
257
|
-
} else data = require_utils_http.query(request);
|
|
307
|
+
const data = isFormPost(request) ? Object.fromEntries(new URLSearchParams(await request.text())) : require_utils_http.query(request);
|
|
258
308
|
const parsed = require_oauth2_client.oauth2RedirectQuerySchema.safeParse(data);
|
|
259
309
|
if (!parsed.success) return this.redirect("invalid_request", "invalid redirect query");
|
|
260
310
|
if (parsed.data.error) {
|
|
261
311
|
if (parsed.data.state && parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
|
|
262
312
|
return this.redirect(safeErrorType(parsed.data.error, "server_error"), parsed.data.error_description);
|
|
263
313
|
}
|
|
264
|
-
|
|
265
|
-
if (
|
|
314
|
+
const { code, state, user } = parsed.data;
|
|
315
|
+
if (!code || !state) return this.redirect("invalid_request", "invalid redirect query");
|
|
316
|
+
if (state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
|
|
266
317
|
if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
|
|
267
318
|
mark("validate_session");
|
|
268
|
-
const { code, user } = parsed.data;
|
|
269
319
|
let signedIn;
|
|
270
320
|
try {
|
|
271
321
|
const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
|
|
@@ -288,8 +338,7 @@ var Auth = class {
|
|
|
288
338
|
attributes: cached.attributes ?? {}
|
|
289
339
|
});
|
|
290
340
|
mark("save_principal");
|
|
291
|
-
signedIn = this.
|
|
292
|
-
await this.repository.save(signedIn);
|
|
341
|
+
signedIn = await this.signIn(principal, session);
|
|
293
342
|
await this.repository.deleteById(session.getId());
|
|
294
343
|
mark("update_session");
|
|
295
344
|
} catch (error) {
|
|
@@ -298,6 +347,10 @@ var Auth = class {
|
|
|
298
347
|
const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : "server_error";
|
|
299
348
|
return this.redirect(type);
|
|
300
349
|
}
|
|
350
|
+
if (error instanceof require_auth_error.AuthenticationError) {
|
|
351
|
+
console.warn("OAuth2 sign-in refused", error.reason, error.metadata);
|
|
352
|
+
return this.redirect("access_denied", void 0, error);
|
|
353
|
+
}
|
|
301
354
|
console.error("OAuth2 callback failed", error);
|
|
302
355
|
return this.redirect("server_error");
|
|
303
356
|
}
|
|
@@ -308,328 +361,157 @@ var Auth = class {
|
|
|
308
361
|
status: 302,
|
|
309
362
|
headers: { location: uri.href }
|
|
310
363
|
});
|
|
311
|
-
const maxAge = signedIn.getMaxInactiveInterval();
|
|
312
|
-
require_utils_http.setCookie(response, this.cookieName, signedIn.getId(), {
|
|
313
|
-
...this.cookieOptions,
|
|
314
|
-
maxAge
|
|
315
|
-
});
|
|
316
364
|
setTiming(response);
|
|
317
|
-
return response;
|
|
365
|
+
return this.withSession(response, signedIn);
|
|
318
366
|
};
|
|
319
|
-
/**
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
for (const attribute of previous.getAttributeNames()) {
|
|
327
|
-
if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;
|
|
328
|
-
if (attribute === "org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME") continue;
|
|
329
|
-
const value = previous.getAttribute(attribute);
|
|
330
|
-
if (value !== null) session.setAttribute(attribute, value);
|
|
331
|
-
}
|
|
332
|
-
session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, name);
|
|
333
|
-
return session;
|
|
367
|
+
/** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */
|
|
368
|
+
async authorize(request, handler, authorization, setTiming) {
|
|
369
|
+
const principal = await handler(request, authorization);
|
|
370
|
+
const session = await this.signIn(principal);
|
|
371
|
+
const response = Response.json(principal);
|
|
372
|
+
setTiming(response);
|
|
373
|
+
return this.withSession(response, session);
|
|
334
374
|
}
|
|
335
|
-
loginOAuth2Native =
|
|
375
|
+
loginOAuth2Native = (request, handler) => this.guard(async () => {
|
|
336
376
|
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
337
377
|
const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
|
|
338
378
|
const { provider } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_NATIVE);
|
|
339
|
-
const body = await
|
|
379
|
+
const body = await require_utils_valid.readJson(request);
|
|
340
380
|
const attributes = require_auth_attributes.bodyAttributes(body);
|
|
341
|
-
if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });
|
|
342
381
|
const { attributes: _, ...credentials } = body;
|
|
343
382
|
const { code_verifier } = credentials;
|
|
344
|
-
if (code_verifier !== void 0 && (typeof code_verifier !== "string" || !CODE_VERIFIER_FORMAT.test(code_verifier)))
|
|
383
|
+
if (code_verifier !== void 0 && (typeof code_verifier !== "string" || !CODE_VERIFIER_FORMAT.test(code_verifier))) throw require_auth_respond.invalidArgument("INVALID_CODE_VERIFIER", "code_verifier must be 43-128 characters from [A-Za-z0-9-._~]");
|
|
345
384
|
const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({
|
|
346
385
|
provider,
|
|
347
386
|
credentials
|
|
348
387
|
});
|
|
349
388
|
mark("login_oauth2_native");
|
|
350
389
|
const { nonce } = userInfo.data;
|
|
351
|
-
if (nonce) {
|
|
352
|
-
|
|
353
|
-
error: "invalid_request",
|
|
354
|
-
error_description: "nonce mismatch"
|
|
355
|
-
}, { status: 400 });
|
|
356
|
-
await this.kv.removeItem(this.getOauth2NonceKey(nonce));
|
|
390
|
+
if (typeof nonce === "string" && nonce) {
|
|
391
|
+
await this.consumeNonce(nonce, provider);
|
|
357
392
|
mark("check_nonce");
|
|
358
393
|
}
|
|
359
|
-
|
|
394
|
+
return this.authorize(request, handler, {
|
|
360
395
|
provider,
|
|
361
396
|
clientId,
|
|
362
397
|
userInfo,
|
|
363
398
|
token,
|
|
364
399
|
attributes
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
369
|
-
await this.repository.save(session);
|
|
370
|
-
const response = new Response(JSON.stringify(principal), {
|
|
371
|
-
status: 200,
|
|
372
|
-
headers: { "Content-Type": "application/json" }
|
|
373
|
-
});
|
|
374
|
-
const maxAge = session.getMaxInactiveInterval();
|
|
375
|
-
require_utils_http.setCookie(response, this.cookieName, session.getId(), {
|
|
376
|
-
...this.cookieOptions,
|
|
377
|
-
maxAge
|
|
378
|
-
});
|
|
379
|
-
setTiming(response);
|
|
380
|
-
return response;
|
|
381
|
-
};
|
|
382
|
-
loginOAuth2Onetap = async (request, handler, provider = "google") => {
|
|
400
|
+
}, setTiming);
|
|
401
|
+
});
|
|
402
|
+
loginOAuth2Onetap = (request, handler, provider = "google") => this.guard(async () => {
|
|
383
403
|
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
384
404
|
const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
} else if (request.headers.get("Content-Type")?.includes("application/json")) {
|
|
391
|
-
const json = await request.json();
|
|
392
|
-
attributes = require_auth_attributes.bodyAttributes(json);
|
|
393
|
-
body = json;
|
|
394
|
-
} else return Response.json({
|
|
395
|
-
error: "invalid_request",
|
|
396
|
-
error_description: "invalid content type"
|
|
397
|
-
}, { status: 400 });
|
|
398
|
-
if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });
|
|
399
|
-
const parsed = require_oauth2_client.googleOneTapSchema.parse(body);
|
|
400
|
-
let principal;
|
|
405
|
+
const body = (request.headers.get("content-type") ?? "").includes("application/json") ? await require_utils_valid.readJson(request) : await require_utils_valid.readForm(request);
|
|
406
|
+
const { attributes = {}, ...parsed } = require_utils_valid.parse(oneTapBodySchema, body);
|
|
407
|
+
let clientId;
|
|
408
|
+
let token;
|
|
409
|
+
let userInfo;
|
|
401
410
|
if (parsed.credential) {
|
|
402
|
-
|
|
411
|
+
({clientId, token, userInfo} = await this.oauth2Client.getTokenInfo({
|
|
403
412
|
provider,
|
|
404
413
|
credential: parsed.credential
|
|
405
|
-
});
|
|
414
|
+
}));
|
|
406
415
|
mark("verify_credential");
|
|
407
416
|
const { nonce } = userInfo.data;
|
|
408
417
|
if (typeof nonce === "string" && nonce) {
|
|
409
|
-
|
|
410
|
-
error: "invalid_request",
|
|
411
|
-
error_description: "nonce mismatch"
|
|
412
|
-
}, { status: 401 });
|
|
413
|
-
await this.kv.removeItem(this.getOauth2NonceKey(nonce));
|
|
418
|
+
await this.consumeNonce(nonce, provider);
|
|
414
419
|
mark("check_nonce");
|
|
415
|
-
} else if (this.onetapRequireNonce)
|
|
416
|
-
error: "invalid_request",
|
|
417
|
-
error_description: "nonce missing"
|
|
418
|
-
}, { status: 401 });
|
|
419
|
-
principal = await handler(request, {
|
|
420
|
-
provider,
|
|
421
|
-
clientId,
|
|
422
|
-
userInfo,
|
|
423
|
-
token,
|
|
424
|
-
attributes
|
|
425
|
-
});
|
|
420
|
+
} else if (this.onetapRequireNonce) throw new require_auth_error.AuthenticationError("NONCE_MISSING");
|
|
426
421
|
} else if (parsed.code) {
|
|
427
|
-
|
|
422
|
+
({clientId, token} = await this.oauth2Client.exchangeAuthorizationCode({
|
|
428
423
|
provider,
|
|
429
424
|
code: parsed.code
|
|
430
|
-
});
|
|
431
|
-
|
|
425
|
+
}));
|
|
426
|
+
userInfo = await this.oauth2Client.getUserInfo({
|
|
432
427
|
provider,
|
|
433
428
|
clientId,
|
|
434
|
-
|
|
435
|
-
provider,
|
|
436
|
-
clientId,
|
|
437
|
-
token
|
|
438
|
-
}),
|
|
439
|
-
token,
|
|
440
|
-
attributes
|
|
429
|
+
token
|
|
441
430
|
});
|
|
442
|
-
} else throw
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
require_utils_http.setCookie(response, this.cookieName, session.getId(), {
|
|
453
|
-
...this.cookieOptions,
|
|
454
|
-
maxAge
|
|
455
|
-
});
|
|
456
|
-
setTiming(response);
|
|
457
|
-
return response;
|
|
458
|
-
};
|
|
459
|
-
sendEmailVerificationCode = async (request, sender) => {
|
|
431
|
+
} else throw require_auth_respond.invalidArgument("INVALID_ARGUMENT", "credential or code is required");
|
|
432
|
+
return this.authorize(request, handler, {
|
|
433
|
+
provider,
|
|
434
|
+
clientId,
|
|
435
|
+
userInfo,
|
|
436
|
+
token,
|
|
437
|
+
attributes
|
|
438
|
+
}, setTiming);
|
|
439
|
+
});
|
|
440
|
+
sendEmailVerificationCode = (request, sender) => this.guard(async () => {
|
|
460
441
|
(0, _shware_utils.invariant)(this.turnstileSecretKey, "turnstileSecretKey is not configured");
|
|
461
|
-
const {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
await this.kv.removeItem(this.getEmailVerificationAttemptsKey(email));
|
|
478
|
-
await this.kv.setItem(this.getEmailVerificationCodeKey(email), verificationCode, EMAIL_VERIFICATION_CODE_TTL);
|
|
479
|
-
return Response.json({});
|
|
480
|
-
} catch (e) {
|
|
481
|
-
console.error("Failed to send email verification code", e);
|
|
482
|
-
await this.kv.removeItem(cooldownKey);
|
|
483
|
-
return Response.json(require_utils_valid.invalidArgument("EMAIL_DELIVERY_FAILED"), { status: 400 });
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
loginEmail = async (request, handler) => {
|
|
487
|
-
const { data, error } = require_utils_valid.valid(require_email_schema.loginEmailSchema, await request.json());
|
|
488
|
-
if (error) return Response.json(error, { status: error.error.code });
|
|
489
|
-
const { email, verificationCode, attributes } = data;
|
|
490
|
-
const key = this.getEmailVerificationCodeKey(email);
|
|
491
|
-
const attemptsKey = this.getEmailVerificationAttemptsKey(email);
|
|
492
|
-
const stored = await this.kv.getItem(key);
|
|
493
|
-
if (typeof stored !== "string") return Response.json(require_utils_valid.invalidArgument("INVALID_VERIFICATION_CODE"), { status: 400 });
|
|
494
|
-
if (await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL) > this.maxVerificationAttempts) {
|
|
495
|
-
await this.kv.removeItem(key);
|
|
496
|
-
const error = require_utils_valid.invalidArgument("VERIFICATION_CODE_ATTEMPTS_EXCEEDED");
|
|
497
|
-
return Response.json(error, { status: error.error.code });
|
|
498
|
-
}
|
|
499
|
-
if (stored.length !== verificationCode.length || !(0, node_crypto.timingSafeEqual)(Buffer.from(stored), Buffer.from(verificationCode))) return Response.json(require_utils_valid.invalidArgument("INVALID_VERIFICATION_CODE"), { status: 400 });
|
|
500
|
-
await this.kv.removeItem(key);
|
|
501
|
-
await this.kv.removeItem(attemptsKey);
|
|
502
|
-
const principal = await handler(request, {
|
|
442
|
+
const { email, turnstileToken } = require_utils_valid.parse(require_email_schema.sendEmailVerificationCodeSchema, await require_utils_valid.readJson(request));
|
|
443
|
+
await require_utils_valid.verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);
|
|
444
|
+
await this.emailCodes.send(email, sender);
|
|
445
|
+
return Response.json({});
|
|
446
|
+
});
|
|
447
|
+
verifyEmailCode = (email, verificationCode) => this.emailCodes.verify(email, verificationCode);
|
|
448
|
+
loginEmail = (request, handler) => this.guard(async () => {
|
|
449
|
+
const { setTiming } = require_utils_timing.timing({ enabled: this.timing });
|
|
450
|
+
const { email, verificationCode, attributes = {} } = require_utils_valid.parse(require_email_schema.loginEmailSchema, await require_utils_valid.readJson(request));
|
|
451
|
+
await this.emailCodes.verify(email, verificationCode);
|
|
452
|
+
const claims = {
|
|
453
|
+
sub: email,
|
|
454
|
+
email,
|
|
455
|
+
email_verified: true
|
|
456
|
+
};
|
|
457
|
+
return this.authorize(request, handler, {
|
|
503
458
|
provider: "EMAIL",
|
|
504
459
|
userInfo: {
|
|
505
|
-
claims
|
|
506
|
-
sub: email,
|
|
507
|
-
email,
|
|
508
|
-
email_verified: true
|
|
509
|
-
},
|
|
460
|
+
claims,
|
|
510
461
|
data: {}
|
|
511
462
|
},
|
|
512
|
-
attributes
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
516
|
-
await this.repository.save(session);
|
|
517
|
-
const response = new Response(JSON.stringify(principal), {
|
|
518
|
-
status: 200,
|
|
519
|
-
headers: { "Content-Type": "application/json" }
|
|
520
|
-
});
|
|
521
|
-
const maxAge = session.getMaxInactiveInterval();
|
|
522
|
-
require_utils_http.setCookie(response, this.cookieName, session.getId(), {
|
|
523
|
-
...this.cookieOptions,
|
|
524
|
-
maxAge
|
|
525
|
-
});
|
|
526
|
-
return response;
|
|
527
|
-
};
|
|
463
|
+
attributes
|
|
464
|
+
}, setTiming);
|
|
465
|
+
});
|
|
528
466
|
/**
|
|
529
|
-
* Step 1 of the desktop / native app sign-in handshake. The caller already
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* desktop client via a loopback redirect.
|
|
467
|
+
* Step 1 of the desktop / native app sign-in handshake. The caller already completed normal
|
|
468
|
+
* sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that
|
|
469
|
+
* the web page hands off to the desktop client via a loopback redirect.
|
|
533
470
|
*
|
|
534
|
-
* PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier`
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
* the
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* capture on loopback) from exchanging it.
|
|
471
|
+
* PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the
|
|
472
|
+
* browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge
|
|
473
|
+
* through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`
|
|
474
|
+
* enforces that the caller knows the matching verifier. This blocks an attacker who only
|
|
475
|
+
* intercepts the auth code (browser history, malicious extension, packet capture on loopback)
|
|
476
|
+
* from exchanging it.
|
|
541
477
|
*
|
|
542
|
-
* Requires an authenticated cookie session
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
* upstream sign-in flow already passed CAPTCHA / OAuth / verification.
|
|
478
|
+
* Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse
|
|
479
|
+
* protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does
|
|
480
|
+
* not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.
|
|
546
481
|
*/
|
|
547
|
-
desktopAuthorize = async (
|
|
482
|
+
desktopAuthorize = (request) => this.guard(async () => {
|
|
548
483
|
const principal = await this.getPrincipal(request);
|
|
549
|
-
if (!principal)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
message: "sign in first",
|
|
553
|
-
details: []
|
|
554
|
-
} }, { status: 401 });
|
|
555
|
-
let body;
|
|
556
|
-
try {
|
|
557
|
-
body = await request.json();
|
|
558
|
-
} catch {
|
|
559
|
-
return Response.json(require_utils_valid.invalidArgument("INVALID_ARGUMENT"), { status: 400 });
|
|
560
|
-
}
|
|
561
|
-
const codeChallenge = typeof body.code_challenge === "string" ? body.code_challenge : null;
|
|
562
|
-
if (!codeChallenge || body.code_challenge_method !== "S256" || !PKCE_FORMAT.test(codeChallenge)) return Response.json(require_utils_valid.invalidArgument("INVALID_ARGUMENT"), { status: 400 });
|
|
563
|
-
const code = (0, node_crypto.randomUUID)();
|
|
564
|
-
const record = {
|
|
565
|
-
name: principal.name,
|
|
566
|
-
cc: codeChallenge
|
|
567
|
-
};
|
|
568
|
-
await this.kv.setItem(this.getDesktopCodeKey(code), JSON.stringify(record), 300);
|
|
484
|
+
if (!principal) throw require_auth_respond.unauthenticated("Sign in first");
|
|
485
|
+
const { code_challenge } = require_utils_valid.parse(desktopAuthorizeSchema, await require_utils_valid.readJson(request));
|
|
486
|
+
const code = await this.desktopCodes.issue(principal, code_challenge);
|
|
569
487
|
return Response.json({ code });
|
|
570
|
-
};
|
|
488
|
+
});
|
|
571
489
|
/**
|
|
572
|
-
* Step 2 of the desktop / native app sign-in handshake. The desktop
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
* either side doesn't cascade to the other.
|
|
490
|
+
* Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth
|
|
491
|
+
* code it received via the loopback callback, and we mint a fresh session for it, independent
|
|
492
|
+
* of the web cookie session that originally authorized the code, so logging out on either side
|
|
493
|
+
* does not cascade to the other.
|
|
577
494
|
*
|
|
578
|
-
* Cookie attributes:
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
* backend this combination is rejected by Chromium — see the desktop
|
|
584
|
-
* sign-in PRD for workarounds.
|
|
495
|
+
* Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the
|
|
496
|
+
* desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide
|
|
497
|
+
* everything else (`domain`, `path`, `httpOnly`); the override is targeted at the
|
|
498
|
+
* SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by
|
|
499
|
+
* Chromium, see the desktop sign-in PRD for workarounds.
|
|
585
500
|
*
|
|
586
|
-
* No CSRF check needed
|
|
587
|
-
*
|
|
501
|
+
* No CSRF check needed: the request carries no cookie, the auth code itself is the credential,
|
|
502
|
+
* and the code's single-use removal blocks replay.
|
|
588
503
|
*/
|
|
589
|
-
desktopExchange = async (
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
} catch {
|
|
594
|
-
return Response.json(require_utils_valid.invalidArgument("INVALID_ARGUMENT"), { status: 400 });
|
|
595
|
-
}
|
|
596
|
-
const code = typeof body.code === "string" ? body.code : null;
|
|
597
|
-
const verifier = typeof body.code_verifier === "string" ? body.code_verifier : null;
|
|
598
|
-
if (!code || !verifier || !PKCE_FORMAT.test(verifier)) return Response.json(require_utils_valid.invalidArgument("INVALID_ARGUMENT"), { status: 400 });
|
|
599
|
-
const key = this.getDesktopCodeKey(code);
|
|
600
|
-
const raw = await this.kv.getItem(key);
|
|
601
|
-
if (typeof raw !== "string") return Response.json(require_utils_valid.invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
|
|
602
|
-
let record;
|
|
603
|
-
try {
|
|
604
|
-
record = JSON.parse(raw);
|
|
605
|
-
} catch {
|
|
606
|
-
await this.kv.removeItem(key);
|
|
607
|
-
return Response.json(require_utils_valid.invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
|
|
608
|
-
}
|
|
609
|
-
const computed = (0, node_crypto.createHash)("sha256").update(verifier).digest();
|
|
610
|
-
const stored = Buffer.from(record.cc, "base64url");
|
|
611
|
-
if (stored.length !== computed.length || !(0, node_crypto.timingSafeEqual)(computed, stored)) return Response.json(require_utils_valid.invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
|
|
612
|
-
await this.kv.removeItem(key);
|
|
613
|
-
const session = this.repository.createSession();
|
|
614
|
-
session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, record.name);
|
|
504
|
+
desktopExchange = (request) => this.guard(async () => {
|
|
505
|
+
const { code, code_verifier } = require_utils_valid.parse(desktopExchangeSchema, await require_utils_valid.readJson(request));
|
|
506
|
+
const principal = await this.desktopCodes.redeem(code, code_verifier);
|
|
507
|
+
const session = await this.signIn(principal);
|
|
615
508
|
session.setAttribute("client_type", "desktop");
|
|
616
509
|
await this.repository.save(session);
|
|
617
|
-
|
|
618
|
-
const maxAge = session.getMaxInactiveInterval();
|
|
619
|
-
require_utils_http.setCookie(response, this.cookieName, session.getId(), {
|
|
620
|
-
...this.cookieOptions,
|
|
510
|
+
return this.withSession(Response.json({ ok: true }), session, {
|
|
621
511
|
sameSite: "none",
|
|
622
|
-
secure: true
|
|
623
|
-
maxAge
|
|
512
|
+
secure: true
|
|
624
513
|
});
|
|
625
|
-
|
|
626
|
-
};
|
|
627
|
-
sendPhoneVerificationCode = async (_request, _sender) => {
|
|
628
|
-
return Response.json({ error: "not_implemented" }, { status: 501 });
|
|
629
|
-
};
|
|
630
|
-
loginPhone = async (_request, _handler) => {
|
|
631
|
-
return Response.json({ error: "not_implemented" }, { status: 501 });
|
|
632
|
-
};
|
|
514
|
+
});
|
|
633
515
|
kick = async (principal) => {
|
|
634
516
|
const sessions = await this.repository.findByPrincipalName(principal.name);
|
|
635
517
|
for (const sessionId of sessions.keys()) await this.repository.deleteById(sessionId);
|
|
@@ -647,47 +529,26 @@ var Auth = class {
|
|
|
647
529
|
session.setLastAccessedTime(now);
|
|
648
530
|
await this.repository.save(session);
|
|
649
531
|
}
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (!session) return false;
|
|
655
|
-
if (!session.getAttribute("org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME")) return false;
|
|
532
|
+
getPrincipal = async (request) => {
|
|
533
|
+
const session = await this.loadSession(request);
|
|
534
|
+
const principal = session && this.principalOf(session);
|
|
535
|
+
if (!principal) return null;
|
|
656
536
|
await this.touch(session);
|
|
657
|
-
return
|
|
537
|
+
return principal;
|
|
538
|
+
};
|
|
539
|
+
isAuthenticated = async (request) => {
|
|
540
|
+
return await this.getPrincipal(request) !== null;
|
|
658
541
|
};
|
|
659
542
|
getSession = async (request, create) => {
|
|
660
|
-
const
|
|
661
|
-
if (!
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return session;
|
|
666
|
-
} else return null;
|
|
667
|
-
}
|
|
668
|
-
const session = await this.repository.findById(sessionId);
|
|
669
|
-
if (!session) {
|
|
670
|
-
if (create) {
|
|
671
|
-
const session = this.repository.createSession();
|
|
672
|
-
await this.repository.save(session);
|
|
673
|
-
return session;
|
|
674
|
-
} else return null;
|
|
675
|
-
}
|
|
676
|
-
return session;
|
|
543
|
+
const session = await this.loadSession(request);
|
|
544
|
+
if (session || !create) return session;
|
|
545
|
+
const created = this.repository.createSession();
|
|
546
|
+
await this.repository.save(created);
|
|
547
|
+
return created;
|
|
677
548
|
};
|
|
678
549
|
deleteSession = async (sessionId) => {
|
|
679
550
|
await this.repository.deleteById(sessionId);
|
|
680
551
|
};
|
|
681
|
-
getPrincipal = async (request) => {
|
|
682
|
-
const sessionId = this.getSessionId(request);
|
|
683
|
-
if (!sessionId) return null;
|
|
684
|
-
const session = await this.repository.findById(sessionId);
|
|
685
|
-
if (!session) return null;
|
|
686
|
-
const name = session.getAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME);
|
|
687
|
-
if (!name) return null;
|
|
688
|
-
await this.touch(session);
|
|
689
|
-
return { name: String(name) };
|
|
690
|
-
};
|
|
691
552
|
listSessions = async (principal) => {
|
|
692
553
|
const sessions = await this.repository.findByPrincipalName(principal.name);
|
|
693
554
|
return Array.from(sessions.values());
|
|
@@ -697,7 +558,6 @@ var Auth = class {
|
|
|
697
558
|
return Response.json({});
|
|
698
559
|
};
|
|
699
560
|
};
|
|
700
|
-
//#endregion
|
|
701
561
|
exports.Auth = Auth;
|
|
702
562
|
exports.PATH = PATH;
|
|
703
563
|
|