@rpcbase/auth 0.109.0 → 0.111.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/dist/handler-BH38xcvj.js +60 -0
- package/dist/handler-BH38xcvj.js.map +1 -0
- package/dist/handler-Bjxe8iM2.js +67 -0
- package/dist/handler-Bjxe8iM2.js.map +1 -0
- package/dist/handler-CVeU9Nyf.js +85 -0
- package/dist/handler-CVeU9Nyf.js.map +1 -0
- package/dist/handler-CrTy-N1A.js +51 -0
- package/dist/handler-CrTy-N1A.js.map +1 -0
- package/dist/handler-D2-FmmDc.js +56 -0
- package/dist/handler-D2-FmmDc.js.map +1 -0
- package/dist/handler-D4-sXlBe.js +74 -0
- package/dist/handler-D4-sXlBe.js.map +1 -0
- package/dist/handler-D87G4mz9.js +67 -0
- package/dist/handler-D87G4mz9.js.map +1 -0
- package/dist/handler-DKrwSIQz.js +19 -0
- package/dist/handler-DKrwSIQz.js.map +1 -0
- package/dist/handler-tJUJWqII.js +59 -0
- package/dist/handler-tJUJWqII.js.map +1 -0
- package/dist/index.js +657 -684
- package/dist/index.js.map +1 -1
- package/dist/middleware-BbKZ_rOe.js +18 -0
- package/dist/middleware-BbKZ_rOe.js.map +1 -0
- package/dist/oauth/index.js +625 -746
- package/dist/oauth/index.js.map +1 -1
- package/dist/routes.js +18 -9
- package/dist/routes.js.map +1 -1
- package/dist/schemas-BKnjeqQ9.js +3380 -0
- package/dist/schemas-BKnjeqQ9.js.map +1 -0
- package/dist/sign-in-C9a-NvBu.js +18 -0
- package/dist/sign-in-C9a-NvBu.js.map +1 -0
- package/dist/sign-up-DqDJxb2D.js +18 -0
- package/dist/sign-up-DqDJxb2D.js.map +1 -0
- package/package.json +1 -1
- package/dist/handler-BNDemOGd.js +0 -79
- package/dist/handler-BNDemOGd.js.map +0 -1
- package/dist/handler-Bt53h0sk.js +0 -64
- package/dist/handler-Bt53h0sk.js.map +0 -1
- package/dist/handler-C4cw739Z.js +0 -59
- package/dist/handler-C4cw739Z.js.map +0 -1
- package/dist/handler-Ck7oLQ_R.js +0 -87
- package/dist/handler-Ck7oLQ_R.js.map +0 -1
- package/dist/handler-CyP6R8FM.js +0 -24
- package/dist/handler-CyP6R8FM.js.map +0 -1
- package/dist/handler-D6zJn86A.js +0 -82
- package/dist/handler-D6zJn86A.js.map +0 -1
- package/dist/handler-D8HfTbUs.js +0 -58
- package/dist/handler-D8HfTbUs.js.map +0 -1
- package/dist/handler-DfEsSB4T.js +0 -74
- package/dist/handler-DfEsSB4T.js.map +0 -1
- package/dist/handler-xEpHnzkZ.js +0 -58
- package/dist/handler-xEpHnzkZ.js.map +0 -1
- package/dist/index-Bxz6YdiB.js +0 -20
- package/dist/index-Bxz6YdiB.js.map +0 -1
- package/dist/index-C_uBu_fP.js +0 -20
- package/dist/index-C_uBu_fP.js.map +0 -1
- package/dist/middleware-5Zwy7HRL.js +0 -25
- package/dist/middleware-5Zwy7HRL.js.map +0 -1
- package/dist/schemas-Dn3gHDGz.js +0 -3706
- package/dist/schemas-Dn3gHDGz.js.map +0 -1
package/dist/oauth/index.js
CHANGED
|
@@ -1,772 +1,651 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import { models } from "@rpcbase/db";
|
|
3
3
|
import { hashPasswordForStorage } from "@rpcbase/server";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
anyGlobal[STORE_KEY] = created;
|
|
15
|
-
return created;
|
|
4
|
+
//#region src/oauth/config.ts
|
|
5
|
+
var STORE_KEY = Symbol.for("@rpcbase/auth/oauthProviders");
|
|
6
|
+
var getStore = () => {
|
|
7
|
+
const anyGlobal = globalThis;
|
|
8
|
+
const existing = anyGlobal[STORE_KEY];
|
|
9
|
+
if (existing && typeof existing === "object" && existing.providers && typeof existing.providers === "object") return existing;
|
|
10
|
+
const created = { providers: {} };
|
|
11
|
+
anyGlobal[STORE_KEY] = created;
|
|
12
|
+
return created;
|
|
16
13
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
14
|
+
var normalizeProviders = (providers) => {
|
|
15
|
+
const result = {};
|
|
16
|
+
const isSafeRedirectPath = (candidate) => {
|
|
17
|
+
if (!candidate.startsWith("/")) return false;
|
|
18
|
+
if (candidate.startsWith("//")) return false;
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
for (const [providerIdRaw, value] of Object.entries(providers)) {
|
|
22
|
+
const providerId = providerIdRaw.trim();
|
|
23
|
+
if (!providerId) continue;
|
|
24
|
+
const issuer = typeof value?.issuer === "string" ? value.issuer.trim() : "";
|
|
25
|
+
const clientId = typeof value?.clientId === "string" ? value.clientId.trim() : "";
|
|
26
|
+
if (!issuer) throw new Error(`oauth provider "${providerId}" missing issuer`);
|
|
27
|
+
if (!clientId) throw new Error(`oauth provider "${providerId}" missing clientId`);
|
|
28
|
+
const clientSecret = typeof value?.clientSecret === "string" && value.clientSecret.trim() ? value.clientSecret : void 0;
|
|
29
|
+
const scope = typeof value?.scope === "string" ? value.scope.trim() : "";
|
|
30
|
+
if (!scope) throw new Error(`oauth provider "${providerId}" missing scope`);
|
|
31
|
+
const callbackPath = typeof value?.callbackPath === "string" ? value.callbackPath.trim() : "";
|
|
32
|
+
if (!callbackPath) throw new Error(`oauth provider "${providerId}" missing callbackPath`);
|
|
33
|
+
if (!isSafeRedirectPath(callbackPath)) throw new Error(`oauth provider "${providerId}" has invalid callbackPath`);
|
|
34
|
+
result[providerId] = {
|
|
35
|
+
issuer,
|
|
36
|
+
clientId,
|
|
37
|
+
clientSecret,
|
|
38
|
+
scope,
|
|
39
|
+
callbackPath
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
46
43
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
var configureOAuthProviders = (providers) => {
|
|
45
|
+
const store = getStore();
|
|
46
|
+
const normalized = normalizeProviders(providers);
|
|
47
|
+
store.providers = {
|
|
48
|
+
...store.providers,
|
|
49
|
+
...normalized
|
|
50
|
+
};
|
|
54
51
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
var setOAuthProviders = (providers) => {
|
|
53
|
+
const store = getStore();
|
|
54
|
+
store.providers = normalizeProviders(providers);
|
|
58
55
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return store.providers[providerId] ?? null;
|
|
56
|
+
var getOAuthProviderConfig = (providerId) => {
|
|
57
|
+
return getStore().providers[providerId] ?? null;
|
|
62
58
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/oauth/jwt.ts
|
|
61
|
+
var decodeBase64Url = (value) => {
|
|
62
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((value.length + 3) % 4);
|
|
63
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
66
64
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
var decodeJwtPayload = (token) => {
|
|
66
|
+
const parts = token.split(".");
|
|
67
|
+
if (parts.length < 2) return null;
|
|
68
|
+
try {
|
|
69
|
+
const json = decodeBase64Url(parts[1]);
|
|
70
|
+
const parsed = JSON.parse(json);
|
|
71
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
72
|
+
return parsed;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
78
76
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
jwks_uri: jwksUri
|
|
119
|
-
};
|
|
120
|
-
})();
|
|
121
|
-
cache.set(issuer, loader);
|
|
122
|
-
try {
|
|
123
|
-
return await loader;
|
|
124
|
-
} catch (err) {
|
|
125
|
-
cache.delete(issuer);
|
|
126
|
-
throw err;
|
|
127
|
-
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/oauth/oidc.ts
|
|
79
|
+
var cache = /* @__PURE__ */ new Map();
|
|
80
|
+
var normalizeIssuer = (raw) => raw.trim().replace(/\/+$/, "");
|
|
81
|
+
var getOidcWellKnown = async (issuerRaw) => {
|
|
82
|
+
const issuer = normalizeIssuer(issuerRaw);
|
|
83
|
+
if (!issuer) throw new Error("OIDC issuer is required");
|
|
84
|
+
const existing = cache.get(issuer);
|
|
85
|
+
if (existing) return existing;
|
|
86
|
+
const loader = (async () => {
|
|
87
|
+
const url = new URL("/.well-known/openid-configuration", issuer);
|
|
88
|
+
const response = await fetch(url, { headers: { Accept: "application/json" } });
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const body = await response.text().catch(() => "");
|
|
91
|
+
throw new Error(`Failed to fetch OIDC discovery document: ${response.status} ${body}`);
|
|
92
|
+
}
|
|
93
|
+
const json = await response.json().catch(() => null);
|
|
94
|
+
if (!json || typeof json !== "object") throw new Error("Invalid OIDC discovery document");
|
|
95
|
+
const authorizationEndpoint = typeof json.authorization_endpoint === "string" ? json.authorization_endpoint : "";
|
|
96
|
+
const tokenEndpoint = typeof json.token_endpoint === "string" ? json.token_endpoint : "";
|
|
97
|
+
const issuerValue = typeof json.issuer === "string" ? json.issuer : issuer;
|
|
98
|
+
const userinfoEndpoint = typeof json.userinfo_endpoint === "string" ? json.userinfo_endpoint : void 0;
|
|
99
|
+
const jwksUri = typeof json.jwks_uri === "string" ? json.jwks_uri : void 0;
|
|
100
|
+
if (!authorizationEndpoint || !tokenEndpoint) throw new Error("OIDC discovery document missing required endpoints");
|
|
101
|
+
return {
|
|
102
|
+
issuer: issuerValue,
|
|
103
|
+
authorization_endpoint: authorizationEndpoint,
|
|
104
|
+
token_endpoint: tokenEndpoint,
|
|
105
|
+
userinfo_endpoint: userinfoEndpoint,
|
|
106
|
+
jwks_uri: jwksUri
|
|
107
|
+
};
|
|
108
|
+
})();
|
|
109
|
+
cache.set(issuer, loader);
|
|
110
|
+
try {
|
|
111
|
+
return await loader;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
cache.delete(issuer);
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
128
116
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/oauth/pkce.ts
|
|
119
|
+
var base64UrlEncode$1 = (input) => input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
120
|
+
var generatePkcePair = () => {
|
|
121
|
+
const verifier = base64UrlEncode$1(crypto.randomBytes(32));
|
|
122
|
+
return {
|
|
123
|
+
verifier,
|
|
124
|
+
challenge: base64UrlEncode$1(crypto.createHash("sha256").update(verifier).digest())
|
|
125
|
+
};
|
|
137
126
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/oauth/providerId.ts
|
|
129
|
+
var RESERVED_KEYS = new Set([
|
|
130
|
+
"__proto__",
|
|
131
|
+
"prototype",
|
|
132
|
+
"constructor"
|
|
133
|
+
]);
|
|
134
|
+
var isSafeOAuthProviderId = (value) => {
|
|
135
|
+
const providerId = value.trim();
|
|
136
|
+
if (!providerId) return false;
|
|
137
|
+
if (providerId.length > 128) return false;
|
|
138
|
+
if (RESERVED_KEYS.has(providerId)) return false;
|
|
139
|
+
return /^[a-zA-Z0-9_-]+$/.test(providerId);
|
|
145
140
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/oauth/lib.ts
|
|
143
|
+
var OAUTH_REQUEST_TTL_MS = 600 * 1e3;
|
|
144
|
+
var OAUTH_STATE_COOKIE_NAME = "rb_oauth_state";
|
|
145
|
+
var getQueryString = (value) => {
|
|
146
|
+
if (typeof value === "string") return value;
|
|
147
|
+
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
148
|
+
return null;
|
|
152
149
|
};
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
150
|
+
var getCookieValue = (ctx, name) => {
|
|
151
|
+
const header = ctx.req.headers?.cookie;
|
|
152
|
+
if (typeof header !== "string" || !header) return null;
|
|
153
|
+
for (const part of header.split(";")) {
|
|
154
|
+
const [rawKey, ...rest] = part.split("=");
|
|
155
|
+
const key = rawKey?.trim();
|
|
156
|
+
if (!key || key !== name) continue;
|
|
157
|
+
const rawValue = rest.join("=").trim();
|
|
158
|
+
if (!rawValue) return "";
|
|
159
|
+
try {
|
|
160
|
+
return decodeURIComponent(rawValue);
|
|
161
|
+
} catch {
|
|
162
|
+
return rawValue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
169
166
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
var isSafeRedirectPath = (candidate) => {
|
|
168
|
+
if (!candidate.startsWith("/")) return false;
|
|
169
|
+
if (candidate.startsWith("//")) return false;
|
|
170
|
+
return true;
|
|
174
171
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
var getRequestOrigin = (ctx) => {
|
|
173
|
+
const protoHeader = ctx.req.headers["x-forwarded-proto"];
|
|
174
|
+
const hostHeader = ctx.req.headers["x-forwarded-host"];
|
|
175
|
+
const protocol = typeof protoHeader === "string" ? protoHeader.split(",")[0].trim() : ctx.req.protocol;
|
|
176
|
+
const host = typeof hostHeader === "string" ? hostHeader.split(",")[0].trim() : ctx.req.get("host");
|
|
177
|
+
return protocol && host ? `${protocol}://${host}` : "";
|
|
181
178
|
};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
var resolveCallbackPath = (callbackPath) => {
|
|
180
|
+
const trimmed = callbackPath.trim();
|
|
181
|
+
return trimmed && isSafeRedirectPath(trimmed) ? trimmed : null;
|
|
185
182
|
};
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
req.on("end", () => resolve(body));
|
|
198
|
-
req.on("error", reject);
|
|
183
|
+
var readTextBody = async (req) => await new Promise((resolve, reject) => {
|
|
184
|
+
let body = "";
|
|
185
|
+
if (typeof req.setEncoding === "function") req.setEncoding("utf8");
|
|
186
|
+
req.on("data", (chunk) => {
|
|
187
|
+
body += String(chunk);
|
|
188
|
+
if (body.length > 32768) reject(/* @__PURE__ */ new Error("request_body_too_large"));
|
|
189
|
+
});
|
|
190
|
+
req.on("end", () => resolve(body));
|
|
191
|
+
req.on("error", reject);
|
|
199
192
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
result[key] = value;
|
|
209
|
-
}
|
|
210
|
-
return result;
|
|
193
|
+
var getFormPostParams = async (ctx) => {
|
|
194
|
+
if (ctx.req.method !== "POST") return null;
|
|
195
|
+
if (!(typeof ctx.req.headers["content-type"] === "string" ? ctx.req.headers["content-type"] : "").includes("application/x-www-form-urlencoded")) return null;
|
|
196
|
+
const body = await readTextBody(ctx.req);
|
|
197
|
+
const params = new URLSearchParams(body);
|
|
198
|
+
const result = {};
|
|
199
|
+
for (const [key, value] of params.entries()) result[key] = value;
|
|
200
|
+
return result;
|
|
211
201
|
};
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
202
|
+
var resolveProviderId = (ctx, providerIdOverride) => (providerIdOverride ?? String(ctx.req.params?.provider ?? "")).trim();
|
|
203
|
+
var RESERVED_AUTH_PARAM_KEYS = new Set([
|
|
204
|
+
"__proto__",
|
|
205
|
+
"prototype",
|
|
206
|
+
"constructor"
|
|
207
|
+
]);
|
|
208
|
+
var normalizeAuthorizationParams = (value) => {
|
|
209
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
210
|
+
const result = {};
|
|
211
|
+
for (const [keyRaw, valueRaw] of Object.entries(value)) {
|
|
212
|
+
const key = keyRaw.trim();
|
|
213
|
+
if (!key) continue;
|
|
214
|
+
if (RESERVED_AUTH_PARAM_KEYS.has(key)) continue;
|
|
215
|
+
const val = typeof valueRaw === "string" ? valueRaw.trim() : "";
|
|
216
|
+
if (!val) continue;
|
|
217
|
+
result[key] = val;
|
|
218
|
+
}
|
|
219
|
+
return Object.keys(result).length ? result : void 0;
|
|
226
220
|
};
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const redirectUri = `${origin}${callbackPath}`;
|
|
316
|
-
const url = new URL(oidc.authorization_endpoint);
|
|
317
|
-
url.searchParams.set("client_id", provider.clientId);
|
|
318
|
-
url.searchParams.set("redirect_uri", redirectUri);
|
|
319
|
-
url.searchParams.set("response_type", "code");
|
|
320
|
-
url.searchParams.set("scope", provider.scope);
|
|
321
|
-
url.searchParams.set("state", state);
|
|
322
|
-
url.searchParams.set("code_challenge", challenge);
|
|
323
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
324
|
-
const extraAuthorizationParams = normalizeAuthorizationParams(authorizationParams);
|
|
325
|
-
for (const [key, value] of Object.entries(extraAuthorizationParams ?? {})) {
|
|
326
|
-
if (url.searchParams.has(key)) continue;
|
|
327
|
-
url.searchParams.set(key, value);
|
|
328
|
-
}
|
|
329
|
-
return {
|
|
330
|
-
success: true,
|
|
331
|
-
redirectUrl: url.toString()
|
|
332
|
-
};
|
|
221
|
+
var getOAuthStartRedirectUrl = async ({ ctx, providerId: providerIdOverride, returnTo, authorizationParams }) => {
|
|
222
|
+
const providerId = resolveProviderId(ctx, providerIdOverride);
|
|
223
|
+
if (!providerId) return {
|
|
224
|
+
success: false,
|
|
225
|
+
error: "missing_provider",
|
|
226
|
+
statusCode: 400
|
|
227
|
+
};
|
|
228
|
+
if (!isSafeOAuthProviderId(providerId)) return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: "invalid_provider",
|
|
231
|
+
statusCode: 400
|
|
232
|
+
};
|
|
233
|
+
const provider = getOAuthProviderConfig(providerId);
|
|
234
|
+
if (!provider) return {
|
|
235
|
+
success: false,
|
|
236
|
+
error: "unknown_provider",
|
|
237
|
+
statusCode: 404
|
|
238
|
+
};
|
|
239
|
+
const origin = getRequestOrigin(ctx);
|
|
240
|
+
if (!origin) return {
|
|
241
|
+
success: false,
|
|
242
|
+
error: "origin_unavailable",
|
|
243
|
+
statusCode: 500
|
|
244
|
+
};
|
|
245
|
+
const isHttps = origin.startsWith("https://");
|
|
246
|
+
const callbackPath = resolveCallbackPath(provider.callbackPath);
|
|
247
|
+
if (!callbackPath) return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: "invalid_callback_path",
|
|
250
|
+
statusCode: 500
|
|
251
|
+
};
|
|
252
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
253
|
+
const { verifier, challenge } = generatePkcePair();
|
|
254
|
+
const safeReturnTo = typeof returnTo === "string" && isSafeRedirectPath(returnTo) ? returnTo : void 0;
|
|
255
|
+
try {
|
|
256
|
+
const OAuthRequest = await models.getGlobal("RBOAuthRequest", ctx);
|
|
257
|
+
const now = /* @__PURE__ */ new Date();
|
|
258
|
+
await OAuthRequest.create({
|
|
259
|
+
_id: state,
|
|
260
|
+
providerId,
|
|
261
|
+
codeVerifier: verifier,
|
|
262
|
+
returnTo: safeReturnTo,
|
|
263
|
+
createdAt: now,
|
|
264
|
+
expiresAt: new Date(now.getTime() + OAUTH_REQUEST_TTL_MS)
|
|
265
|
+
});
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.warn("oauth::failed_to_store_request", err);
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
error: "oauth_request_store_failed",
|
|
271
|
+
statusCode: 500
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
ctx.res.cookie(OAUTH_STATE_COOKIE_NAME, `${providerId}:${state}`, {
|
|
276
|
+
httpOnly: true,
|
|
277
|
+
secure: isHttps,
|
|
278
|
+
sameSite: isHttps ? "none" : "lax",
|
|
279
|
+
path: callbackPath,
|
|
280
|
+
maxAge: OAUTH_REQUEST_TTL_MS
|
|
281
|
+
});
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.warn("oauth::failed_to_set_state_cookie", err);
|
|
284
|
+
return {
|
|
285
|
+
success: false,
|
|
286
|
+
error: "oauth_cookie_set_failed",
|
|
287
|
+
statusCode: 500
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const oidc = await getOidcWellKnown(provider.issuer);
|
|
291
|
+
const redirectUri = `${origin}${callbackPath}`;
|
|
292
|
+
const url = new URL(oidc.authorization_endpoint);
|
|
293
|
+
url.searchParams.set("client_id", provider.clientId);
|
|
294
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
295
|
+
url.searchParams.set("response_type", "code");
|
|
296
|
+
url.searchParams.set("scope", provider.scope);
|
|
297
|
+
url.searchParams.set("state", state);
|
|
298
|
+
url.searchParams.set("code_challenge", challenge);
|
|
299
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
300
|
+
const extraAuthorizationParams = normalizeAuthorizationParams(authorizationParams);
|
|
301
|
+
for (const [key, value] of Object.entries(extraAuthorizationParams ?? {})) {
|
|
302
|
+
if (url.searchParams.has(key)) continue;
|
|
303
|
+
url.searchParams.set(key, value);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
redirectUrl: url.toString()
|
|
308
|
+
};
|
|
333
309
|
};
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
await user.save();
|
|
598
|
-
try {
|
|
599
|
-
await Tenant.create({
|
|
600
|
-
tenantId: tenantId2,
|
|
601
|
-
name: resolvedEmail || subject
|
|
602
|
-
});
|
|
603
|
-
} catch (err) {
|
|
604
|
-
console.warn("oauth::failed_to_create_tenant", err);
|
|
605
|
-
}
|
|
606
|
-
} else {
|
|
607
|
-
const setFields = {
|
|
608
|
-
[`oauthProviders.${providerId}`]: oauthProviderPayload
|
|
609
|
-
};
|
|
610
|
-
if (!user.email && resolvedEmail) {
|
|
611
|
-
setFields.email = resolvedEmail;
|
|
612
|
-
}
|
|
613
|
-
if (!user.name && resolvedName) {
|
|
614
|
-
setFields.name = resolvedName;
|
|
615
|
-
}
|
|
616
|
-
await User.updateOne({
|
|
617
|
-
_id: user._id
|
|
618
|
-
}, {
|
|
619
|
-
$set: setFields
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
const tenantId = user.tenants?.[0]?.toString?.() || "00000000";
|
|
623
|
-
const signedInTenants = (user.tenants || []).map(String);
|
|
624
|
-
const tenantRolesMap = user.get("tenantRoles");
|
|
625
|
-
const tenantRoles = tenantRolesMap ? Object.fromEntries(tenantRolesMap.entries()) : void 0;
|
|
626
|
-
ctx.req.session.user = {
|
|
627
|
-
id: user._id.toString(),
|
|
628
|
-
currentTenantId: tenantId,
|
|
629
|
-
signedInTenants: signedInTenants.length ? signedInTenants : [tenantId],
|
|
630
|
-
isEntryGateAuthorized: true,
|
|
631
|
-
tenantRoles
|
|
632
|
-
};
|
|
633
|
-
try {
|
|
634
|
-
if (OAuthRequestModel) {
|
|
635
|
-
await OAuthRequestModel.deleteOne({
|
|
636
|
-
_id: state,
|
|
637
|
-
providerId
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
} catch (err) {
|
|
641
|
-
console.warn("oauth::failed_to_delete_request", err);
|
|
642
|
-
}
|
|
643
|
-
try {
|
|
644
|
-
ctx.res.clearCookie(OAUTH_STATE_COOKIE_NAME, {
|
|
645
|
-
httpOnly: true,
|
|
646
|
-
secure: isHttps,
|
|
647
|
-
sameSite: isHttps ? "none" : "lax",
|
|
648
|
-
path: callbackPath
|
|
649
|
-
});
|
|
650
|
-
} catch (err) {
|
|
651
|
-
console.warn("oauth::failed_to_clear_state_cookie", err);
|
|
652
|
-
}
|
|
653
|
-
const redirectPath = returnTo && isSafeRedirectPath(returnTo) ? returnTo : successRedirectPath && isSafeRedirectPath(successRedirectPath) ? successRedirectPath : OAUTH_SUCCESS_REDIRECT_PATH;
|
|
654
|
-
return {
|
|
655
|
-
success: true,
|
|
656
|
-
type: "signed_in",
|
|
657
|
-
redirectPath,
|
|
658
|
-
userId: user._id.toString(),
|
|
659
|
-
tenantId,
|
|
660
|
-
signedInTenants: signedInTenants.length ? signedInTenants : [tenantId]
|
|
661
|
-
};
|
|
310
|
+
var OAUTH_SUCCESS_REDIRECT_PATH = "/onboarding";
|
|
311
|
+
var AUTH_ERROR_REDIRECT_BASE = "/auth/sign-in";
|
|
312
|
+
var processOAuthCallback = async ({ ctx, providerId: providerIdOverride, createUserOnFirstSignIn, missingUserRedirectPath, successRedirectPath }) => {
|
|
313
|
+
const providerId = resolveProviderId(ctx, providerIdOverride);
|
|
314
|
+
if (!providerId) return {
|
|
315
|
+
success: false,
|
|
316
|
+
type: "error",
|
|
317
|
+
error: "missing_provider",
|
|
318
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=missing_provider`
|
|
319
|
+
};
|
|
320
|
+
if (!isSafeOAuthProviderId(providerId)) return {
|
|
321
|
+
success: false,
|
|
322
|
+
type: "error",
|
|
323
|
+
error: "invalid_provider",
|
|
324
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=invalid_provider`
|
|
325
|
+
};
|
|
326
|
+
const provider = getOAuthProviderConfig(providerId);
|
|
327
|
+
if (!provider) return {
|
|
328
|
+
success: false,
|
|
329
|
+
type: "error",
|
|
330
|
+
error: "unknown_provider",
|
|
331
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=unknown_provider`
|
|
332
|
+
};
|
|
333
|
+
const origin = getRequestOrigin(ctx);
|
|
334
|
+
if (!origin) return {
|
|
335
|
+
success: false,
|
|
336
|
+
type: "error",
|
|
337
|
+
error: "origin_unavailable",
|
|
338
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=origin_unavailable`
|
|
339
|
+
};
|
|
340
|
+
const isHttps = origin.startsWith("https://");
|
|
341
|
+
const callbackPath = resolveCallbackPath(provider.callbackPath);
|
|
342
|
+
if (!callbackPath) return {
|
|
343
|
+
success: false,
|
|
344
|
+
type: "error",
|
|
345
|
+
error: "invalid_callback_path",
|
|
346
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=invalid_callback_path`
|
|
347
|
+
};
|
|
348
|
+
const bodyParams = await getFormPostParams(ctx);
|
|
349
|
+
const code = (getQueryString(bodyParams?.code) ?? getQueryString(ctx.req.query?.code))?.trim() ?? "";
|
|
350
|
+
const state = (getQueryString(bodyParams?.state) ?? getQueryString(ctx.req.query?.state))?.trim() ?? "";
|
|
351
|
+
const oauthUserRaw = (getQueryString(bodyParams?.user) ?? getQueryString(ctx.req.query?.user))?.trim() ?? "";
|
|
352
|
+
if (!code || !state) return {
|
|
353
|
+
success: false,
|
|
354
|
+
type: "error",
|
|
355
|
+
error: "missing_code_or_state",
|
|
356
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=missing_code_or_state`
|
|
357
|
+
};
|
|
358
|
+
if (getCookieValue(ctx, OAUTH_STATE_COOKIE_NAME) !== `${providerId}:${state}`) return {
|
|
359
|
+
success: false,
|
|
360
|
+
type: "error",
|
|
361
|
+
error: "invalid_state",
|
|
362
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=invalid_state`
|
|
363
|
+
};
|
|
364
|
+
let oauthRequest = null;
|
|
365
|
+
let OAuthRequestModel = null;
|
|
366
|
+
try {
|
|
367
|
+
OAuthRequestModel = await models.getGlobal("RBOAuthRequest", ctx);
|
|
368
|
+
oauthRequest = await OAuthRequestModel.findOne({
|
|
369
|
+
_id: state,
|
|
370
|
+
providerId
|
|
371
|
+
}).lean();
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.warn("oauth::failed_to_load_request", err);
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
type: "error",
|
|
377
|
+
error: "oauth_request_load_failed",
|
|
378
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=oauth_request_load_failed`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (!oauthRequest) return {
|
|
382
|
+
success: false,
|
|
383
|
+
type: "error",
|
|
384
|
+
error: "invalid_state",
|
|
385
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=invalid_state`
|
|
386
|
+
};
|
|
387
|
+
const returnTo = typeof oauthRequest.returnTo === "string" ? oauthRequest.returnTo : void 0;
|
|
388
|
+
const codeVerifier = typeof oauthRequest.codeVerifier === "string" ? oauthRequest.codeVerifier : "";
|
|
389
|
+
if (!codeVerifier) return {
|
|
390
|
+
success: false,
|
|
391
|
+
type: "error",
|
|
392
|
+
error: "missing_code_verifier",
|
|
393
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=missing_code_verifier`
|
|
394
|
+
};
|
|
395
|
+
const oidc = await getOidcWellKnown(provider.issuer);
|
|
396
|
+
const redirectUri = `${origin}${callbackPath}`;
|
|
397
|
+
const tokenBody = new URLSearchParams();
|
|
398
|
+
tokenBody.set("grant_type", "authorization_code");
|
|
399
|
+
tokenBody.set("code", code);
|
|
400
|
+
tokenBody.set("redirect_uri", redirectUri);
|
|
401
|
+
tokenBody.set("client_id", provider.clientId);
|
|
402
|
+
tokenBody.set("code_verifier", codeVerifier);
|
|
403
|
+
if (provider.clientSecret) tokenBody.set("client_secret", provider.clientSecret);
|
|
404
|
+
const tokenResponse = await fetch(oidc.token_endpoint, {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: {
|
|
407
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
408
|
+
Accept: "application/json"
|
|
409
|
+
},
|
|
410
|
+
body: tokenBody.toString()
|
|
411
|
+
});
|
|
412
|
+
const tokenJson = await tokenResponse.json().catch(() => null);
|
|
413
|
+
if (!tokenResponse.ok || !tokenJson || typeof tokenJson !== "object") {
|
|
414
|
+
const body = tokenJson ? JSON.stringify(tokenJson) : "";
|
|
415
|
+
console.warn("oauth::token_exchange failed", tokenResponse.status, body);
|
|
416
|
+
return {
|
|
417
|
+
success: false,
|
|
418
|
+
type: "error",
|
|
419
|
+
error: "token_exchange_failed",
|
|
420
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=token_exchange_failed`
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : "";
|
|
424
|
+
const refreshToken = typeof tokenJson.refresh_token === "string" ? tokenJson.refresh_token : void 0;
|
|
425
|
+
const idToken = typeof tokenJson.id_token === "string" ? tokenJson.id_token : void 0;
|
|
426
|
+
const scope = typeof tokenJson.scope === "string" ? tokenJson.scope : void 0;
|
|
427
|
+
const tokenType = typeof tokenJson.token_type === "string" ? tokenJson.token_type : void 0;
|
|
428
|
+
const expiresIn = typeof tokenJson.expires_in === "number" ? tokenJson.expires_in : typeof tokenJson.expires_in === "string" ? Number(tokenJson.expires_in) : void 0;
|
|
429
|
+
const expiresInSeconds = typeof expiresIn === "number" && Number.isFinite(expiresIn) ? expiresIn : null;
|
|
430
|
+
const expiresAt = expiresInSeconds !== null ? new Date(Date.now() + expiresInSeconds * 1e3) : void 0;
|
|
431
|
+
let userInfo = null;
|
|
432
|
+
if (oidc.userinfo_endpoint && accessToken) {
|
|
433
|
+
const userInfoResponse = await fetch(oidc.userinfo_endpoint, { headers: {
|
|
434
|
+
Authorization: `Bearer ${accessToken}`,
|
|
435
|
+
Accept: "application/json"
|
|
436
|
+
} });
|
|
437
|
+
if (userInfoResponse.ok) userInfo = await userInfoResponse.json().catch(() => null);
|
|
438
|
+
}
|
|
439
|
+
const idTokenPayload = idToken ? decodeJwtPayload(idToken) : null;
|
|
440
|
+
const accessTokenPayload = decodeJwtPayload(accessToken);
|
|
441
|
+
const subject = typeof userInfo?.sub === "string" ? userInfo.sub : typeof idTokenPayload?.sub === "string" ? idTokenPayload.sub : typeof accessTokenPayload?.sub === "string" ? accessTokenPayload.sub : "";
|
|
442
|
+
if (!subject) return {
|
|
443
|
+
success: false,
|
|
444
|
+
type: "error",
|
|
445
|
+
error: "missing_subject",
|
|
446
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=missing_subject`
|
|
447
|
+
};
|
|
448
|
+
const email = typeof userInfo?.email === "string" ? userInfo.email : typeof idTokenPayload?.email === "string" ? idTokenPayload.email : void 0;
|
|
449
|
+
const name = typeof userInfo?.name === "string" ? userInfo.name : typeof idTokenPayload?.name === "string" ? idTokenPayload.name : void 0;
|
|
450
|
+
let oauthUser = null;
|
|
451
|
+
if (oauthUserRaw) try {
|
|
452
|
+
oauthUser = JSON.parse(oauthUserRaw);
|
|
453
|
+
} catch {
|
|
454
|
+
oauthUser = null;
|
|
455
|
+
}
|
|
456
|
+
const oauthUserRecord = oauthUser && typeof oauthUser === "object" ? oauthUser : null;
|
|
457
|
+
const oauthUserEmail = typeof oauthUserRecord?.email === "string" ? oauthUserRecord.email.trim() : "";
|
|
458
|
+
const oauthUserNameRecord = oauthUserRecord?.name && typeof oauthUserRecord.name === "object" ? oauthUserRecord.name : null;
|
|
459
|
+
const oauthUserName = [typeof oauthUserNameRecord?.firstName === "string" ? oauthUserNameRecord.firstName.trim() : "", typeof oauthUserNameRecord?.lastName === "string" ? oauthUserNameRecord.lastName.trim() : ""].filter(Boolean).join(" ").trim();
|
|
460
|
+
const resolvedEmail = email ?? (oauthUserEmail || void 0);
|
|
461
|
+
const resolvedName = name ?? (oauthUserName || void 0);
|
|
462
|
+
const [User, Tenant] = await Promise.all([models.getGlobal("RBUser", ctx), models.getGlobal("RBTenant", ctx)]);
|
|
463
|
+
const subjectQueryKey = `oauthProviders.${providerId}.subject`;
|
|
464
|
+
let user = await User.findOne({ [subjectQueryKey]: subject });
|
|
465
|
+
if (!user && resolvedEmail) user = await User.findOne({ email: resolvedEmail });
|
|
466
|
+
if (!user && !(createUserOnFirstSignIn ?? true)) {
|
|
467
|
+
const redirectPath = missingUserRedirectPath;
|
|
468
|
+
const result = {
|
|
469
|
+
success: false,
|
|
470
|
+
type: "missing_user",
|
|
471
|
+
providerId,
|
|
472
|
+
subject,
|
|
473
|
+
email: resolvedEmail,
|
|
474
|
+
name: resolvedName
|
|
475
|
+
};
|
|
476
|
+
if (redirectPath && isSafeRedirectPath(redirectPath)) {
|
|
477
|
+
const url = new URL(redirectPath, origin);
|
|
478
|
+
url.searchParams.set("oauth_provider", providerId);
|
|
479
|
+
if (resolvedEmail) url.searchParams.set("email", resolvedEmail);
|
|
480
|
+
if (resolvedName) url.searchParams.set("name", resolvedName);
|
|
481
|
+
result.redirectPath = `${url.pathname}${url.search}`;
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
if (!ctx.req.session) return {
|
|
486
|
+
success: false,
|
|
487
|
+
type: "error",
|
|
488
|
+
error: "session_unavailable",
|
|
489
|
+
redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=session_unavailable`
|
|
490
|
+
};
|
|
491
|
+
const now = /* @__PURE__ */ new Date();
|
|
492
|
+
let providerCreatedAt;
|
|
493
|
+
const existingProvider = (user?.get("oauthProviders"))?.get(providerId);
|
|
494
|
+
if (existingProvider?.createdAt instanceof Date) providerCreatedAt = existingProvider.createdAt;
|
|
495
|
+
const oauthProviderPayload = {
|
|
496
|
+
subject,
|
|
497
|
+
email: resolvedEmail,
|
|
498
|
+
name: resolvedName,
|
|
499
|
+
accessToken,
|
|
500
|
+
refreshToken,
|
|
501
|
+
idToken,
|
|
502
|
+
scope,
|
|
503
|
+
tokenType,
|
|
504
|
+
expiresAt,
|
|
505
|
+
rawUserInfo: userInfo ?? void 0,
|
|
506
|
+
createdAt: providerCreatedAt ?? now,
|
|
507
|
+
updatedAt: now
|
|
508
|
+
};
|
|
509
|
+
if (!user) {
|
|
510
|
+
const tenantId = crypto.randomUUID();
|
|
511
|
+
user = new User({
|
|
512
|
+
email: resolvedEmail,
|
|
513
|
+
name: resolvedName,
|
|
514
|
+
password: await hashPasswordForStorage(crypto.randomBytes(32).toString("hex")),
|
|
515
|
+
tenants: [tenantId],
|
|
516
|
+
tenantRoles: { [tenantId]: ["owner"] },
|
|
517
|
+
oauthProviders: { [providerId]: oauthProviderPayload }
|
|
518
|
+
});
|
|
519
|
+
await user.save();
|
|
520
|
+
try {
|
|
521
|
+
await Tenant.create({
|
|
522
|
+
tenantId,
|
|
523
|
+
name: resolvedEmail || subject
|
|
524
|
+
});
|
|
525
|
+
} catch (err) {
|
|
526
|
+
console.warn("oauth::failed_to_create_tenant", err);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
const setFields = { [`oauthProviders.${providerId}`]: oauthProviderPayload };
|
|
530
|
+
if (!user.email && resolvedEmail) setFields.email = resolvedEmail;
|
|
531
|
+
if (!user.name && resolvedName) setFields.name = resolvedName;
|
|
532
|
+
await User.updateOne({ _id: user._id }, { $set: setFields });
|
|
533
|
+
}
|
|
534
|
+
const tenantId = user.tenants?.[0]?.toString?.() || "00000000";
|
|
535
|
+
const signedInTenants = (user.tenants || []).map(String);
|
|
536
|
+
const tenantRolesMap = user.get("tenantRoles");
|
|
537
|
+
const tenantRoles = tenantRolesMap ? Object.fromEntries(tenantRolesMap.entries()) : void 0;
|
|
538
|
+
ctx.req.session.user = {
|
|
539
|
+
id: user._id.toString(),
|
|
540
|
+
currentTenantId: tenantId,
|
|
541
|
+
signedInTenants: signedInTenants.length ? signedInTenants : [tenantId],
|
|
542
|
+
isEntryGateAuthorized: true,
|
|
543
|
+
tenantRoles
|
|
544
|
+
};
|
|
545
|
+
try {
|
|
546
|
+
if (OAuthRequestModel) await OAuthRequestModel.deleteOne({
|
|
547
|
+
_id: state,
|
|
548
|
+
providerId
|
|
549
|
+
});
|
|
550
|
+
} catch (err) {
|
|
551
|
+
console.warn("oauth::failed_to_delete_request", err);
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
ctx.res.clearCookie(OAUTH_STATE_COOKIE_NAME, {
|
|
555
|
+
httpOnly: true,
|
|
556
|
+
secure: isHttps,
|
|
557
|
+
sameSite: isHttps ? "none" : "lax",
|
|
558
|
+
path: callbackPath
|
|
559
|
+
});
|
|
560
|
+
} catch (err) {
|
|
561
|
+
console.warn("oauth::failed_to_clear_state_cookie", err);
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
success: true,
|
|
565
|
+
type: "signed_in",
|
|
566
|
+
redirectPath: returnTo && isSafeRedirectPath(returnTo) ? returnTo : successRedirectPath && isSafeRedirectPath(successRedirectPath) ? successRedirectPath : OAUTH_SUCCESS_REDIRECT_PATH,
|
|
567
|
+
userId: user._id.toString(),
|
|
568
|
+
tenantId,
|
|
569
|
+
signedInTenants: signedInTenants.length ? signedInTenants : [tenantId]
|
|
570
|
+
};
|
|
662
571
|
};
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
ctx.res.redirect(result.redirectUrl);
|
|
684
|
-
return {
|
|
685
|
-
success: true
|
|
686
|
-
};
|
|
572
|
+
var createOAuthStartHandler = ({ providerId, getAuthorizationParams } = {}) => async (_payload, ctx) => {
|
|
573
|
+
const resolvedProviderId = resolveProviderId(ctx, providerId);
|
|
574
|
+
const providerAuthorizationParams = resolvedProviderId && getAuthorizationParams ? getAuthorizationParams(resolvedProviderId) : void 0;
|
|
575
|
+
const returnTo = getQueryString(ctx.req.query?.returnTo)?.trim();
|
|
576
|
+
const result = await getOAuthStartRedirectUrl({
|
|
577
|
+
ctx,
|
|
578
|
+
providerId: resolvedProviderId,
|
|
579
|
+
returnTo,
|
|
580
|
+
authorizationParams: providerAuthorizationParams
|
|
581
|
+
});
|
|
582
|
+
if (!result.success) {
|
|
583
|
+
ctx.res.status(result.statusCode);
|
|
584
|
+
return {
|
|
585
|
+
success: false,
|
|
586
|
+
error: result.error
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
ctx.res.redirect(result.redirectUrl);
|
|
590
|
+
return { success: true };
|
|
687
591
|
};
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
return {
|
|
715
|
-
success: false,
|
|
716
|
-
error: "user_not_found"
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
ctx.res.redirect(result.redirectPath);
|
|
720
|
-
return {
|
|
721
|
-
success: true
|
|
722
|
-
};
|
|
592
|
+
var createOAuthCallbackHandler = ({ providerId, createUserOnFirstSignIn, missingUserRedirectPath, successRedirectPath } = {}) => async (_payload, ctx) => {
|
|
593
|
+
const result = await processOAuthCallback({
|
|
594
|
+
ctx,
|
|
595
|
+
providerId,
|
|
596
|
+
createUserOnFirstSignIn,
|
|
597
|
+
missingUserRedirectPath,
|
|
598
|
+
successRedirectPath
|
|
599
|
+
});
|
|
600
|
+
if (result.type === "error") {
|
|
601
|
+
ctx.res.redirect(result.redirectPath);
|
|
602
|
+
return {
|
|
603
|
+
success: false,
|
|
604
|
+
error: result.error
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
if (result.type === "missing_user") {
|
|
608
|
+
if (result.redirectPath) ctx.res.redirect(result.redirectPath);
|
|
609
|
+
else ctx.res.redirect("/auth/sign-up");
|
|
610
|
+
return {
|
|
611
|
+
success: false,
|
|
612
|
+
error: "user_not_found"
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
ctx.res.redirect(result.redirectPath);
|
|
616
|
+
return { success: true };
|
|
723
617
|
};
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
aud: "https://appleid.apple.com",
|
|
754
|
-
sub: clientIdValue
|
|
755
|
-
};
|
|
756
|
-
const signingInput = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(payload))}`;
|
|
757
|
-
const signature = crypto.sign("sha256", Buffer.from(signingInput), {
|
|
758
|
-
key: privateKeyPemValue,
|
|
759
|
-
dsaEncoding: "ieee-p1363"
|
|
760
|
-
});
|
|
761
|
-
return `${signingInput}.${base64UrlEncode(signature)}`;
|
|
618
|
+
var base64UrlEncode = (input) => Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
619
|
+
var createAppleClientSecret = ({ teamId, clientId, keyId, privateKeyPem, now = /* @__PURE__ */ new Date(), expiresInSeconds = 600 }) => {
|
|
620
|
+
const teamIdValue = teamId.trim();
|
|
621
|
+
const clientIdValue = clientId.trim();
|
|
622
|
+
const keyIdValue = keyId.trim();
|
|
623
|
+
const privateKeyPemValue = privateKeyPem.trim();
|
|
624
|
+
if (!teamIdValue) throw new Error("teamId is required");
|
|
625
|
+
if (!clientIdValue) throw new Error("clientId is required");
|
|
626
|
+
if (!keyIdValue) throw new Error("keyId is required");
|
|
627
|
+
if (!privateKeyPemValue) throw new Error("privateKeyPem is required");
|
|
628
|
+
const nowSeconds = Math.floor(now.getTime() / 1e3);
|
|
629
|
+
const exp = nowSeconds + Math.max(60, Math.min(Number.isFinite(expiresInSeconds) ? expiresInSeconds : 600, 3600 * 24 * 180));
|
|
630
|
+
const header = {
|
|
631
|
+
alg: "ES256",
|
|
632
|
+
kid: keyIdValue,
|
|
633
|
+
typ: "JWT"
|
|
634
|
+
};
|
|
635
|
+
const payload = {
|
|
636
|
+
iss: teamIdValue,
|
|
637
|
+
iat: nowSeconds,
|
|
638
|
+
exp,
|
|
639
|
+
aud: "https://appleid.apple.com",
|
|
640
|
+
sub: clientIdValue
|
|
641
|
+
};
|
|
642
|
+
const signingInput = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(payload))}`;
|
|
643
|
+
return `${signingInput}.${base64UrlEncode(crypto.sign("sha256", Buffer.from(signingInput), {
|
|
644
|
+
key: privateKeyPemValue,
|
|
645
|
+
dsaEncoding: "ieee-p1363"
|
|
646
|
+
}))}`;
|
|
762
647
|
};
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
createOAuthStartHandler,
|
|
768
|
-
getOAuthStartRedirectUrl,
|
|
769
|
-
processOAuthCallback,
|
|
770
|
-
setOAuthProviders
|
|
771
|
-
};
|
|
772
|
-
//# sourceMappingURL=index.js.map
|
|
648
|
+
//#endregion
|
|
649
|
+
export { configureOAuthProviders, createAppleClientSecret, createOAuthCallbackHandler, createOAuthStartHandler, getOAuthStartRedirectUrl, processOAuthCallback, setOAuthProviders };
|
|
650
|
+
|
|
651
|
+
//# sourceMappingURL=index.js.map
|