@oauth42/next 0.5.0 → 0.5.1
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 +117 -0
- package/dist/build-manifest.json +59 -0
- package/dist/client/index.d.mts +7 -6
- package/dist/client/index.d.ts +7 -6
- package/dist/client/index.js +13 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +13 -1
- package/dist/client/index.mjs.map +1 -1
- package/dist/{index-Cfqxl9r2.d.mts → index-BuZTiUds.d.mts} +51 -1
- package/dist/{index-Cfqxl9r2.d.ts → index-BuZTiUds.d.ts} +51 -1
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +469 -159
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +465 -158
- package/dist/index.mjs.map +1 -1
- package/dist/middleware/index.d.mts +2 -1
- package/dist/middleware/index.d.ts +2 -1
- package/dist/middleware/index.js +218 -66
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +219 -66
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/{auth-D3tAruwx.d.mts → par-Cyu6M1e6.d.mts} +27 -12
- package/dist/{auth-CYMPe3k0.d.ts → par-DQNt8j-s.d.ts} +27 -12
- package/dist/server/index.d.mts +7 -3
- package/dist/server/index.d.ts +7 -3
- package/dist/server/index.js +503 -178
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +499 -177
- package/dist/server/index.mjs.map +1 -1
- package/package.json +14 -5
- package/src/types/index.ts +2 -0
- package/src/types/next-auth.d.ts +2 -0
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,102 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
+
|
|
9
|
+
// src/server/client-assertion.ts
|
|
10
|
+
import { createHash, createPublicKey, generateKeyPairSync, randomBytes, sign } from "crypto";
|
|
11
|
+
var _key, _public;
|
|
12
|
+
var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
|
|
13
|
+
constructor(kid, key) {
|
|
14
|
+
__privateAdd(this, _key);
|
|
15
|
+
__privateAdd(this, _public);
|
|
16
|
+
if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
|
|
17
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
18
|
+
throw new Error("Client assertions require a private P-256 key");
|
|
19
|
+
}
|
|
20
|
+
__privateSet(this, _key, key);
|
|
21
|
+
this.kid = kid;
|
|
22
|
+
const publicKey = createPublicKey(key).export({ format: "jwk" });
|
|
23
|
+
__privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
|
|
24
|
+
this.thumbprint = createHash("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
|
|
25
|
+
}
|
|
26
|
+
static generate(kid) {
|
|
27
|
+
return new _OAuth42ClientAssertion(kid, generateKeyPairSync("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
28
|
+
}
|
|
29
|
+
get publicJwk() {
|
|
30
|
+
return { ...__privateGet(this, _public), kid: this.kid };
|
|
31
|
+
}
|
|
32
|
+
/** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
|
|
33
|
+
get oidcJwks() {
|
|
34
|
+
return { keys: [{
|
|
35
|
+
kty: "EC",
|
|
36
|
+
crv: "P-256",
|
|
37
|
+
x: __privateGet(this, _public).x,
|
|
38
|
+
y: __privateGet(this, _public).y,
|
|
39
|
+
d: __privateGet(this, _key).export({ format: "jwk" }).d,
|
|
40
|
+
kid: this.kid
|
|
41
|
+
}] };
|
|
42
|
+
}
|
|
43
|
+
static validateEndpoint(target) {
|
|
44
|
+
const url = new URL(target);
|
|
45
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
46
|
+
throw new Error("Client authentication requires HTTPS outside loopback development");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
assertion(clientId, audience) {
|
|
50
|
+
_OAuth42ClientAssertion.validateEndpoint(audience);
|
|
51
|
+
if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
|
|
52
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
53
|
+
const header = { alg: "ES256", typ: "JWT", kid: this.kid };
|
|
54
|
+
const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: randomBytes(32).toString("base64url") };
|
|
55
|
+
const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
|
|
56
|
+
return `${data}.${sign("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
57
|
+
}
|
|
58
|
+
async fetch(clientId, target, init, dpop) {
|
|
59
|
+
if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
|
|
60
|
+
throw new Error("Client assertions require a form POST");
|
|
61
|
+
}
|
|
62
|
+
const prepare = () => {
|
|
63
|
+
const body = new URLSearchParams(init.body);
|
|
64
|
+
body.delete("client_secret");
|
|
65
|
+
body.set("client_id", clientId);
|
|
66
|
+
body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
|
67
|
+
body.set("client_assertion", this.assertion(clientId, target));
|
|
68
|
+
const headers = new Headers(init.headers);
|
|
69
|
+
headers.delete("Authorization");
|
|
70
|
+
headers.delete("Content-Length");
|
|
71
|
+
headers.delete("Transfer-Encoding");
|
|
72
|
+
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
|
73
|
+
return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
|
|
74
|
+
};
|
|
75
|
+
return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
_key = new WeakMap();
|
|
79
|
+
_public = new WeakMap();
|
|
80
|
+
var OAuth42ClientAssertion = _OAuth42ClientAssertion;
|
|
81
|
+
|
|
1
82
|
// src/provider.ts
|
|
2
83
|
function OAuth42Provider(options) {
|
|
3
84
|
const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
4
85
|
const baseUrl = issuer.replace(/\/$/, "");
|
|
86
|
+
const { dpop, clientAssertionKey, ...providerOptions } = options;
|
|
87
|
+
if (clientAssertionKey) {
|
|
88
|
+
OAuth42ClientAssertion.validateEndpoint(`${baseUrl}/oauth2/token`);
|
|
89
|
+
providerOptions.clientSecret = void 0;
|
|
90
|
+
}
|
|
91
|
+
if (options.pkceEnabled === false) throw new Error("OAuth42 requires S256 PKCE");
|
|
5
92
|
return {
|
|
6
93
|
id: "oauth42",
|
|
7
94
|
name: "OAuth42",
|
|
8
95
|
type: "oauth",
|
|
9
96
|
version: "2.0",
|
|
10
|
-
//
|
|
11
|
-
|
|
97
|
+
// Pin O42 metadata: NextAuth v4 discovery does not compare the returned issuer.
|
|
98
|
+
// Constructing its OIDC client from these trusted endpoints preserves issuer binding.
|
|
99
|
+
jwks_endpoint: `${baseUrl}/.well-known/jwks.json`,
|
|
12
100
|
// Use ID token for user profile (standard OIDC pattern)
|
|
13
101
|
// This avoids an extra HTTP call to /userinfo on every auth flow
|
|
14
102
|
// Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
|
|
@@ -21,17 +109,26 @@ function OAuth42Provider(options) {
|
|
|
21
109
|
response_type: "code"
|
|
22
110
|
}
|
|
23
111
|
},
|
|
24
|
-
token:
|
|
112
|
+
token: dpop ? {
|
|
113
|
+
url: `${baseUrl}/oauth2/token`,
|
|
114
|
+
async request({ client, provider, params, checks }) {
|
|
115
|
+
const tokens = await client.callback(provider.callbackUrl, params, checks, { DPoP: dpop.oidcKey });
|
|
116
|
+
dpop.assertTokenResponse(tokens);
|
|
117
|
+
return { tokens };
|
|
118
|
+
}
|
|
119
|
+
} : `${baseUrl}/oauth2/token`,
|
|
25
120
|
userinfo: `${baseUrl}/oauth2/userinfo`,
|
|
121
|
+
...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
|
|
26
122
|
client: {
|
|
27
123
|
id: options.clientId,
|
|
28
|
-
secret: options.clientSecret,
|
|
29
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
124
|
+
secret: clientAssertionKey ? void 0 : options.clientSecret,
|
|
125
|
+
token_endpoint_auth_method: clientAssertionKey ? "private_key_jwt" : "client_secret_post",
|
|
126
|
+
...clientAssertionKey ? { token_endpoint_auth_signing_alg: "ES256" } : {},
|
|
30
127
|
id_token_signed_response_alg: "RS256"
|
|
31
128
|
// OAuth42 uses RS256 for ID tokens
|
|
32
129
|
},
|
|
33
|
-
issuer
|
|
34
|
-
checks:
|
|
130
|
+
issuer,
|
|
131
|
+
checks: ["pkce", "state", "nonce"],
|
|
35
132
|
profile(profile, tokens) {
|
|
36
133
|
return {
|
|
37
134
|
id: profile.sub || profile.id || profile.email,
|
|
@@ -47,45 +144,127 @@ function OAuth42Provider(options) {
|
|
|
47
144
|
bg: "#1e40af",
|
|
48
145
|
text: "#ffffff"
|
|
49
146
|
},
|
|
50
|
-
|
|
147
|
+
// NextAuth v4 types require a string even when private_key_jwt sends no secret.
|
|
148
|
+
options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/server/par.ts
|
|
153
|
+
async function pushAuthorizationURL(value, config) {
|
|
154
|
+
const base = config.issuer.replace(/\/$/, "");
|
|
155
|
+
let url;
|
|
156
|
+
try {
|
|
157
|
+
url = new URL(value);
|
|
158
|
+
} catch {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
if (`${url.origin}${url.pathname}` !== `${base}/oauth2/authorize`) return value;
|
|
162
|
+
if (url.username || url.password || url.hash) throw new Error("Invalid authorization endpoint");
|
|
163
|
+
const body = new URLSearchParams(url.search);
|
|
164
|
+
if (body.getAll("client_id").length !== 1 || body.get("client_id") !== config.clientId || body.get("response_type") !== "code" || !body.get("state") || !body.get("nonce") || body.get("code_challenge_method") !== "S256" || !/^[A-Za-z0-9_-]{43}$/.test(body.get("code_challenge") || "") || ["request_uri", "client_secret", "client_assertion", "client_assertion_type"].some((k) => body.has(k))) {
|
|
165
|
+
throw new Error("Invalid PAR authorization transaction");
|
|
166
|
+
}
|
|
167
|
+
const target = `${base}/oauth2/par`;
|
|
168
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
169
|
+
if (!config.clientAssertionKey) body.set("client_secret", config.clientSecret || "");
|
|
170
|
+
const init = {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
173
|
+
body,
|
|
174
|
+
redirect: "error",
|
|
175
|
+
cache: "no-store",
|
|
176
|
+
signal: AbortSignal.timeout(3e4)
|
|
177
|
+
};
|
|
178
|
+
const response = config.clientAssertionKey ? await config.clientAssertionKey.fetch(config.clientId, target, init, config.dpop) : config.dpop ? await config.dpop.fetch(target, init) : await fetch(target, init);
|
|
179
|
+
if (response.status !== 201) throw new Error("Pushed authorization failed");
|
|
180
|
+
const pushed = await response.json();
|
|
181
|
+
if (typeof pushed.request_uri !== "string" || !/^urn:ietf:params:oauth:request_uri:[A-Za-z0-9_-]{43}$/.test(pushed.request_uri) || !Number.isInteger(pushed.expires_in) || pushed.expires_in <= 0 || pushed.expires_in > 600) throw new Error("Invalid PAR response");
|
|
182
|
+
url.search = new URLSearchParams({ client_id: config.clientId, request_uri: pushed.request_uri }).toString();
|
|
183
|
+
return url.href;
|
|
184
|
+
}
|
|
185
|
+
function withPushedAuthorization(handler, config) {
|
|
186
|
+
return async (request, context) => {
|
|
187
|
+
if (!(request instanceof Request)) throw new Error("PAR handlers require Next.js App Router Request/Response");
|
|
188
|
+
const response = await handler(request, context);
|
|
189
|
+
const location = response.headers.get("Location");
|
|
190
|
+
if (location) {
|
|
191
|
+
const next = await pushAuthorizationURL(location, config);
|
|
192
|
+
if (next === location) return response;
|
|
193
|
+
const headers = new Headers(response.headers);
|
|
194
|
+
headers.set("Location", next);
|
|
195
|
+
headers.delete("content-length");
|
|
196
|
+
return new Response(response.body, { status: response.status, headers });
|
|
197
|
+
}
|
|
198
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
199
|
+
const body = await response.clone().json();
|
|
200
|
+
if (typeof body?.url === "string") {
|
|
201
|
+
const next = await pushAuthorizationURL(body.url, config);
|
|
202
|
+
if (next !== body.url) {
|
|
203
|
+
const headers = new Headers(response.headers);
|
|
204
|
+
headers.delete("content-length");
|
|
205
|
+
return new Response(JSON.stringify({ ...body, url: next }), { status: response.status, headers });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return response;
|
|
51
210
|
};
|
|
52
211
|
}
|
|
53
212
|
|
|
213
|
+
// src/server/verified-userinfo.ts
|
|
214
|
+
async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
|
|
215
|
+
if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
|
|
216
|
+
try {
|
|
217
|
+
const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
|
|
218
|
+
const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
|
|
219
|
+
const response = await fetcher(target, {
|
|
220
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
221
|
+
redirect: "error",
|
|
222
|
+
cache: "no-store",
|
|
223
|
+
signal: AbortSignal.timeout(3e4)
|
|
224
|
+
});
|
|
225
|
+
if (!response.ok) return null;
|
|
226
|
+
const user = await response.json();
|
|
227
|
+
return parseVerifiedUserInfo(user, subject);
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function parseVerifiedUserInfo(value, subject) {
|
|
233
|
+
if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
234
|
+
const user = value;
|
|
235
|
+
if (user.sub !== subject) return null;
|
|
236
|
+
if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
|
|
237
|
+
if (user.email !== void 0 && typeof user.email !== "string") return null;
|
|
238
|
+
if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
|
|
239
|
+
const profile = { sub: user.sub };
|
|
240
|
+
for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
|
|
241
|
+
if (user[field] != null) {
|
|
242
|
+
if (typeof user[field] !== "string") return null;
|
|
243
|
+
profile[field] = user[field];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
|
|
247
|
+
if (user.groups !== void 0) profile.groups = user.groups;
|
|
248
|
+
return profile;
|
|
249
|
+
}
|
|
250
|
+
async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
|
|
251
|
+
if (typeof session.access_token !== "string" || !session.access_token) return null;
|
|
252
|
+
if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
|
|
253
|
+
return parseVerifiedUserInfo(session.user_info, subject);
|
|
254
|
+
}
|
|
255
|
+
return verifiedUserInfo(issuer, session.access_token, subject, dpop);
|
|
256
|
+
}
|
|
257
|
+
|
|
54
258
|
// src/server/auth.ts
|
|
55
259
|
import NextAuthDefault from "next-auth";
|
|
56
260
|
|
|
57
261
|
// src/server/session.ts
|
|
58
262
|
import { getServerSession as getNextAuthSession } from "next-auth";
|
|
59
|
-
import { headers } from "next/headers";
|
|
60
|
-
var REFRESHED_TOKEN_HEADER = "x-oauth42-refreshed-token";
|
|
61
263
|
async function getOAuth42Session(...args) {
|
|
62
264
|
const session = await getNextAuthSession(...args);
|
|
63
|
-
if (!session) {
|
|
265
|
+
if (!session || session.error) {
|
|
64
266
|
return null;
|
|
65
267
|
}
|
|
66
|
-
let refreshedToken = null;
|
|
67
|
-
try {
|
|
68
|
-
if (args.length === 1) {
|
|
69
|
-
const headersList = await headers();
|
|
70
|
-
refreshedToken = headersList.get(REFRESHED_TOKEN_HEADER);
|
|
71
|
-
} else if (args.length === 3) {
|
|
72
|
-
const req = args[0];
|
|
73
|
-
refreshedToken = req.headers[REFRESHED_TOKEN_HEADER] || null;
|
|
74
|
-
}
|
|
75
|
-
} catch {
|
|
76
|
-
}
|
|
77
|
-
if (refreshedToken) {
|
|
78
|
-
const sessionToken = session.accessToken;
|
|
79
|
-
console.log("[OAuth42 Session] Using refreshed token from middleware:", {
|
|
80
|
-
refreshedPrefix: refreshedToken.substring(0, 20),
|
|
81
|
-
sessionPrefix: sessionToken?.substring(0, 20),
|
|
82
|
-
tokensMatch: refreshedToken === sessionToken
|
|
83
|
-
});
|
|
84
|
-
return {
|
|
85
|
-
...session,
|
|
86
|
-
accessToken: refreshedToken
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
268
|
return session;
|
|
90
269
|
}
|
|
91
270
|
function withOAuth42Session(handler, authOptions) {
|
|
@@ -123,66 +302,59 @@ function isBackendSessionMode(mode) {
|
|
|
123
302
|
function sessionEndpoint(issuer) {
|
|
124
303
|
return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
|
|
125
304
|
}
|
|
126
|
-
async function
|
|
127
|
-
|
|
128
|
-
|
|
305
|
+
async function sessionFetch(params, target, body, credential, method = "POST") {
|
|
306
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
307
|
+
const prepare = () => ({
|
|
308
|
+
method,
|
|
309
|
+
redirect: "error",
|
|
310
|
+
cache: "no-store",
|
|
311
|
+
signal: AbortSignal.timeout(3e4),
|
|
129
312
|
headers: { "Content-Type": "application/json" },
|
|
130
313
|
body: JSON.stringify({
|
|
131
|
-
|
|
132
|
-
refresh_token: params.refreshToken,
|
|
133
|
-
expires_at: params.expiresAt ?? Math.floor(Date.now() / 1e3) + 3600,
|
|
134
|
-
id_token: params.idToken,
|
|
314
|
+
...body,
|
|
135
315
|
client_id: params.clientId,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
316
|
+
...params.clientAssertionKey ? {
|
|
317
|
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
318
|
+
client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
|
|
319
|
+
} : { client_secret: params.clientSecret }
|
|
139
320
|
})
|
|
140
321
|
});
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
322
|
+
return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
|
|
323
|
+
}
|
|
324
|
+
async function sessionResponse(response, params) {
|
|
325
|
+
if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
|
|
326
|
+
const session = await response.json();
|
|
327
|
+
if (params.dpop) params.dpop.assertTokenResponse(session);
|
|
328
|
+
else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
|
|
329
|
+
return session;
|
|
330
|
+
}
|
|
331
|
+
async function createBackendSession(params) {
|
|
332
|
+
return sessionResponse(await sessionFetch(params, sessionEndpoint(params.issuer), {
|
|
333
|
+
access_token: params.accessToken,
|
|
334
|
+
refresh_token: params.refreshToken,
|
|
335
|
+
id_token: params.idToken,
|
|
336
|
+
user: params.user
|
|
337
|
+
}, params.accessToken), params);
|
|
145
338
|
}
|
|
146
339
|
async function resolveBackendSession(params) {
|
|
147
|
-
const response = await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
body: JSON.stringify({
|
|
151
|
-
session_id: params.sessionId,
|
|
152
|
-
client_id: params.clientId,
|
|
153
|
-
client_secret: params.clientSecret,
|
|
154
|
-
issuer: params.issuer
|
|
155
|
-
})
|
|
156
|
-
});
|
|
157
|
-
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
if (!response.ok) {
|
|
161
|
-
throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
|
|
162
|
-
}
|
|
163
|
-
return response.json();
|
|
340
|
+
const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
|
|
341
|
+
if ([401, 403, 404].includes(response.status)) return null;
|
|
342
|
+
return sessionResponse(response, params);
|
|
164
343
|
}
|
|
165
344
|
async function revokeBackendSession(params) {
|
|
166
|
-
await
|
|
167
|
-
|
|
168
|
-
headers: { "Content-Type": "application/json" },
|
|
169
|
-
body: JSON.stringify({
|
|
170
|
-
session_id: params.sessionId,
|
|
171
|
-
client_id: params.clientId,
|
|
172
|
-
client_secret: params.clientSecret
|
|
173
|
-
})
|
|
174
|
-
});
|
|
345
|
+
const response = await sessionFetch(params, sessionEndpoint(params.issuer), { session_id: params.sessionId }, params.sessionId, "DELETE");
|
|
346
|
+
if (!response.ok) throw new Error(`OAuth42 backend session revoke failed: ${response.status}`);
|
|
175
347
|
}
|
|
176
348
|
|
|
177
349
|
// src/server/auth.ts
|
|
178
350
|
var NextAuth = NextAuthDefault.default || NextAuthDefault;
|
|
179
|
-
var
|
|
351
|
+
var activeRefreshes = /* @__PURE__ */ new Map();
|
|
180
352
|
function createAuth(options = {}) {
|
|
181
353
|
const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
|
|
182
|
-
const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
354
|
+
const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
183
355
|
const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
184
356
|
const useBackendSessions = isBackendSessionMode(options.sessionStore);
|
|
185
|
-
if (!clientId || !clientSecret) {
|
|
357
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
186
358
|
throw new Error(
|
|
187
359
|
"OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
|
|
188
360
|
);
|
|
@@ -191,14 +363,17 @@ function createAuth(options = {}) {
|
|
|
191
363
|
providers: [
|
|
192
364
|
OAuth42Provider({
|
|
193
365
|
clientId,
|
|
194
|
-
clientSecret,
|
|
366
|
+
clientSecret: clientSecret || "",
|
|
195
367
|
issuer,
|
|
196
368
|
scopes: options.scopes,
|
|
197
|
-
pkceEnabled: options.pkceEnabled
|
|
369
|
+
pkceEnabled: options.pkceEnabled,
|
|
370
|
+
dpop: options.dpop,
|
|
371
|
+
clientAssertionKey: options.clientAssertionKey
|
|
198
372
|
})
|
|
199
373
|
],
|
|
200
374
|
callbacks: {
|
|
201
375
|
async jwt({ token, account, profile }) {
|
|
376
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
202
377
|
console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
|
|
203
378
|
if (account) {
|
|
204
379
|
if (useBackendSessions) {
|
|
@@ -207,9 +382,11 @@ function createAuth(options = {}) {
|
|
|
207
382
|
}
|
|
208
383
|
const oauth42Profile = profile;
|
|
209
384
|
const backendSession = await createBackendSession({
|
|
385
|
+
dpop: options.dpop,
|
|
386
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
210
387
|
issuer,
|
|
211
388
|
clientId,
|
|
212
|
-
clientSecret,
|
|
389
|
+
clientSecret: clientSecret || "",
|
|
213
390
|
accessToken: account.access_token,
|
|
214
391
|
refreshToken: account.refresh_token,
|
|
215
392
|
expiresAt: account.expires_at,
|
|
@@ -233,7 +410,8 @@ function createAuth(options = {}) {
|
|
|
233
410
|
token.expiresAt = account.expires_at;
|
|
234
411
|
token.idToken = account.id_token;
|
|
235
412
|
token.clientId = clientId;
|
|
236
|
-
|
|
413
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
414
|
+
else token.clientSecret = clientSecret;
|
|
237
415
|
token.issuer = issuer;
|
|
238
416
|
}
|
|
239
417
|
}
|
|
@@ -252,25 +430,36 @@ function createAuth(options = {}) {
|
|
|
252
430
|
},
|
|
253
431
|
async session({ session, token }) {
|
|
254
432
|
console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
|
|
433
|
+
const revokedSession = () => {
|
|
434
|
+
session.error = "SessionRevoked";
|
|
435
|
+
delete session.accessToken;
|
|
436
|
+
delete session.idToken;
|
|
437
|
+
delete session.userInfo;
|
|
438
|
+
delete session.user;
|
|
439
|
+
return session;
|
|
440
|
+
};
|
|
441
|
+
if (token.error) return revokedSession();
|
|
442
|
+
let verified;
|
|
255
443
|
if (useBackendSessions) {
|
|
256
444
|
const sessionId = token.oauth42SessionId;
|
|
257
445
|
if (!sessionId) {
|
|
258
|
-
|
|
259
|
-
return session;
|
|
446
|
+
return revokedSession();
|
|
260
447
|
}
|
|
261
448
|
const backendSession = await resolveBackendSession({
|
|
449
|
+
dpop: options.dpop,
|
|
450
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
262
451
|
issuer,
|
|
263
452
|
clientId,
|
|
264
|
-
clientSecret,
|
|
453
|
+
clientSecret: clientSecret || "",
|
|
265
454
|
sessionId
|
|
266
455
|
});
|
|
267
456
|
if (!backendSession) {
|
|
268
|
-
|
|
269
|
-
return session;
|
|
457
|
+
return revokedSession();
|
|
270
458
|
}
|
|
271
459
|
session.accessToken = backendSession.access_token;
|
|
272
460
|
session.idToken = backendSession.id_token;
|
|
273
461
|
token.expiresAt = backendSession.expires_at;
|
|
462
|
+
verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
|
|
274
463
|
if (backendSession.user && session.user) {
|
|
275
464
|
session.user.email = backendSession.user.email;
|
|
276
465
|
session.user.name = backendSession.user.name;
|
|
@@ -281,6 +470,23 @@ function createAuth(options = {}) {
|
|
|
281
470
|
} else {
|
|
282
471
|
session.accessToken = token.accessToken;
|
|
283
472
|
session.idToken = token.idToken;
|
|
473
|
+
verified = await verifiedUserInfo(issuer, session.accessToken, token.sub, options.dpop);
|
|
474
|
+
}
|
|
475
|
+
if (!verified) {
|
|
476
|
+
return revokedSession();
|
|
477
|
+
}
|
|
478
|
+
session.userInfo = verified;
|
|
479
|
+
token.email = verified.email;
|
|
480
|
+
token.name = verified.name;
|
|
481
|
+
token.username = verified.preferred_username ?? verified.username;
|
|
482
|
+
token.emailVerified = verified.email_verified;
|
|
483
|
+
token.groups = verified.groups ?? [];
|
|
484
|
+
if (session.user) {
|
|
485
|
+
session.user.email = verified.email;
|
|
486
|
+
session.user.name = verified.name;
|
|
487
|
+
session.user.username = verified.preferred_username ?? verified.username;
|
|
488
|
+
session.user.emailVerified = verified.email_verified;
|
|
489
|
+
session.user.groups = verified.groups ?? [];
|
|
284
490
|
}
|
|
285
491
|
if (token.error) {
|
|
286
492
|
session.error = token.error;
|
|
@@ -315,9 +521,11 @@ function createAuth(options = {}) {
|
|
|
315
521
|
const sessionId = message.token?.oauth42SessionId;
|
|
316
522
|
if (sessionId) {
|
|
317
523
|
await revokeBackendSession({
|
|
524
|
+
dpop: options.dpop,
|
|
525
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
318
526
|
issuer,
|
|
319
527
|
clientId,
|
|
320
|
-
clientSecret,
|
|
528
|
+
clientSecret: clientSecret || "",
|
|
321
529
|
sessionId
|
|
322
530
|
}).catch((error) => {
|
|
323
531
|
console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
|
|
@@ -404,31 +612,48 @@ function createAuth(options = {}) {
|
|
|
404
612
|
}
|
|
405
613
|
}
|
|
406
614
|
};
|
|
407
|
-
const
|
|
615
|
+
const nextAuthHandler = NextAuth(authOptions);
|
|
616
|
+
const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
|
|
408
617
|
return {
|
|
409
618
|
auth: authOptions,
|
|
410
619
|
handlers: { GET: handler, POST: handler }
|
|
411
620
|
};
|
|
412
621
|
}
|
|
413
622
|
var getServerSession = getOAuth42Session;
|
|
414
|
-
async function refreshAccessToken(token, clientId, clientSecret, issuer) {
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
623
|
+
async function refreshAccessToken(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
624
|
+
if (typeof token.refreshToken !== "string" || !token.refreshToken) {
|
|
625
|
+
return { ...token, error: "RefreshAccessTokenError" };
|
|
626
|
+
}
|
|
627
|
+
const key = JSON.stringify([issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com", clientId, clientSecret, token.sub, token.refreshToken, dpop?.thumbprint, clientAssertionKey?.thumbprint, clientAssertionKey?.kid]);
|
|
628
|
+
let refresh = activeRefreshes.get(key);
|
|
629
|
+
if (!refresh) {
|
|
630
|
+
refresh = doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey);
|
|
631
|
+
activeRefreshes.set(key, refresh);
|
|
418
632
|
}
|
|
419
|
-
activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
|
|
420
633
|
try {
|
|
421
|
-
|
|
634
|
+
const result = await refresh;
|
|
635
|
+
const updated = {
|
|
636
|
+
...token,
|
|
637
|
+
accessToken: result.accessToken,
|
|
638
|
+
refreshToken: result.refreshToken,
|
|
639
|
+
expiresAt: result.expiresAt,
|
|
640
|
+
error: result.error
|
|
641
|
+
};
|
|
642
|
+
if (clientAssertionKey) delete updated.clientSecret;
|
|
643
|
+
return updated;
|
|
422
644
|
} finally {
|
|
423
|
-
|
|
645
|
+
if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
|
|
424
646
|
}
|
|
425
647
|
}
|
|
426
|
-
async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
648
|
+
async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
427
649
|
try {
|
|
428
650
|
const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
429
651
|
const tokenUrl = `${baseUrl}/oauth2/token`;
|
|
430
652
|
const fetchOptions = {
|
|
431
653
|
method: "POST",
|
|
654
|
+
redirect: "error",
|
|
655
|
+
cache: "no-store",
|
|
656
|
+
signal: AbortSignal.timeout(3e4),
|
|
432
657
|
headers: {
|
|
433
658
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
434
659
|
},
|
|
@@ -439,17 +664,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
439
664
|
client_secret: clientSecret
|
|
440
665
|
})
|
|
441
666
|
};
|
|
442
|
-
|
|
443
|
-
const https = await import("https");
|
|
444
|
-
fetchOptions.agent = new https.Agent({
|
|
445
|
-
rejectUnauthorized: false
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
const response = await fetch(tokenUrl, fetchOptions);
|
|
667
|
+
const response = clientAssertionKey ? await clientAssertionKey.fetch(clientId, tokenUrl, fetchOptions, dpop) : dpop ? await dpop.fetch(tokenUrl, fetchOptions) : await fetch(tokenUrl, fetchOptions);
|
|
449
668
|
const refreshedTokens = await response.json();
|
|
450
669
|
if (!response.ok) {
|
|
451
670
|
throw refreshedTokens;
|
|
452
671
|
}
|
|
672
|
+
dpop?.assertTokenResponse(refreshedTokens);
|
|
453
673
|
console.log("[OAuth42] Token refreshed successfully");
|
|
454
674
|
return {
|
|
455
675
|
...token,
|
|
@@ -461,7 +681,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
461
681
|
error: void 0
|
|
462
682
|
};
|
|
463
683
|
} catch (error) {
|
|
464
|
-
console.error("[OAuth42] Failed to refresh access token
|
|
684
|
+
console.error("[OAuth42] Failed to refresh access token");
|
|
465
685
|
return {
|
|
466
686
|
...token,
|
|
467
687
|
error: "RefreshAccessTokenError"
|
|
@@ -475,14 +695,20 @@ import { getToken, encode } from "next-auth/jwt";
|
|
|
475
695
|
var ALLOWED_COOKIE_SIZE = 4096;
|
|
476
696
|
var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
|
|
477
697
|
var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
478
|
-
var REFRESH_RACE_GRACE_SECONDS = 60;
|
|
479
698
|
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
480
699
|
var pendingRefreshes = /* @__PURE__ */ new Map();
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
|
|
700
|
+
function forwardedHeaders(req, cookieName, jwt) {
|
|
701
|
+
const headers = new Headers(req.headers);
|
|
702
|
+
headers.delete("x-oauth42-refreshed-token");
|
|
703
|
+
if (cookieName && jwt) {
|
|
704
|
+
const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
|
|
705
|
+
if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
|
|
706
|
+
else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
|
|
707
|
+
cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
|
|
708
|
+
}
|
|
709
|
+
headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
|
|
484
710
|
}
|
|
485
|
-
return
|
|
711
|
+
return headers;
|
|
486
712
|
}
|
|
487
713
|
function resolveRefreshConfig(options, token) {
|
|
488
714
|
return {
|
|
@@ -491,11 +717,15 @@ function resolveRefreshConfig(options, token) {
|
|
|
491
717
|
issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
|
|
492
718
|
};
|
|
493
719
|
}
|
|
494
|
-
async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
720
|
+
async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
495
721
|
try {
|
|
496
722
|
const tokenUrl = `${issuer}/oauth2/token`;
|
|
497
|
-
const
|
|
723
|
+
const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
|
|
724
|
+
const response = await fetcher(tokenUrl, {
|
|
498
725
|
method: "POST",
|
|
726
|
+
redirect: "error",
|
|
727
|
+
cache: "no-store",
|
|
728
|
+
signal: AbortSignal.timeout(3e4),
|
|
499
729
|
headers: {
|
|
500
730
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
501
731
|
},
|
|
@@ -508,9 +738,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
508
738
|
});
|
|
509
739
|
const data = await response.json();
|
|
510
740
|
if (!response.ok) {
|
|
511
|
-
console.error("[OAuth42 Middleware] Token refresh failed:",
|
|
741
|
+
console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
|
|
512
742
|
return { success: false, error: data.error || "refresh_failed" };
|
|
513
743
|
}
|
|
744
|
+
dpop?.assertTokenResponse(data);
|
|
514
745
|
console.log("[OAuth42 Middleware] Token refreshed successfully");
|
|
515
746
|
return {
|
|
516
747
|
success: true,
|
|
@@ -519,14 +750,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
519
750
|
expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
|
|
520
751
|
};
|
|
521
752
|
} catch (error) {
|
|
522
|
-
console.error("[OAuth42 Middleware] Token refresh
|
|
753
|
+
console.error("[OAuth42 Middleware] Token refresh request failed");
|
|
523
754
|
return { success: false, error: "refresh_error" };
|
|
524
755
|
}
|
|
525
756
|
}
|
|
526
757
|
function withOAuth42Auth(options = {}) {
|
|
527
758
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
528
759
|
if (!secret) {
|
|
529
|
-
|
|
760
|
+
throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
|
|
530
761
|
}
|
|
531
762
|
return async function middleware(req) {
|
|
532
763
|
const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
|
|
@@ -537,22 +768,41 @@ function withOAuth42Auth(options = {}) {
|
|
|
537
768
|
});
|
|
538
769
|
const pathname = req.nextUrl.pathname;
|
|
539
770
|
if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
|
|
540
|
-
return NextResponse.next();
|
|
771
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
541
772
|
}
|
|
542
773
|
const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
|
|
543
774
|
if (!needsProtection) {
|
|
544
|
-
return NextResponse.next();
|
|
775
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
545
776
|
}
|
|
546
|
-
if (!token) {
|
|
777
|
+
if (!token || token.error) {
|
|
547
778
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
548
779
|
const url = new URL(signInUrl, req.url);
|
|
549
780
|
url.searchParams.set("callbackUrl", pathname);
|
|
550
781
|
return NextResponse.redirect(url);
|
|
551
782
|
}
|
|
552
783
|
const refreshConfig = resolveRefreshConfig(options, token);
|
|
784
|
+
const denied = () => {
|
|
785
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
786
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
787
|
+
const response = NextResponse.redirect(url);
|
|
788
|
+
for (const cookie of req.cookies.getAll()) {
|
|
789
|
+
if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
|
|
790
|
+
}
|
|
791
|
+
return response;
|
|
792
|
+
};
|
|
793
|
+
const verifyAndAuthorize = async (current, resolved) => {
|
|
794
|
+
const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
|
|
795
|
+
if (!user) return false;
|
|
796
|
+
current.groups = user.groups ?? [];
|
|
797
|
+
current.email = user.email;
|
|
798
|
+
current.name = user.name;
|
|
799
|
+
current.username = user.preferred_username ?? user.username;
|
|
800
|
+
current.emailVerified = user.email_verified;
|
|
801
|
+
return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
|
|
802
|
+
};
|
|
553
803
|
if (isBackendSessionMode(options.sessionStore)) {
|
|
554
804
|
const sessionId = token.oauth42SessionId;
|
|
555
|
-
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
|
|
805
|
+
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
|
|
556
806
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
557
807
|
const url = new URL(signInUrl, req.url);
|
|
558
808
|
url.searchParams.set("callbackUrl", pathname);
|
|
@@ -561,9 +811,11 @@ function withOAuth42Auth(options = {}) {
|
|
|
561
811
|
return response;
|
|
562
812
|
}
|
|
563
813
|
const backendSession = await resolveBackendSession({
|
|
814
|
+
dpop: options.dpop,
|
|
815
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
564
816
|
issuer: refreshConfig.issuer,
|
|
565
817
|
clientId: refreshConfig.clientId,
|
|
566
|
-
clientSecret: refreshConfig.clientSecret,
|
|
818
|
+
clientSecret: refreshConfig.clientSecret || "",
|
|
567
819
|
sessionId
|
|
568
820
|
}).catch((error) => {
|
|
569
821
|
console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
|
|
@@ -577,8 +829,9 @@ function withOAuth42Auth(options = {}) {
|
|
|
577
829
|
response.cookies.delete(cookieName);
|
|
578
830
|
return response;
|
|
579
831
|
}
|
|
580
|
-
const
|
|
581
|
-
|
|
832
|
+
const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
|
|
833
|
+
if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
|
|
834
|
+
const requestHeaders = forwardedHeaders(req);
|
|
582
835
|
return NextResponse.next({
|
|
583
836
|
request: {
|
|
584
837
|
headers: requestHeaders
|
|
@@ -594,13 +847,12 @@ function withOAuth42Auth(options = {}) {
|
|
|
594
847
|
expiresAt,
|
|
595
848
|
now,
|
|
596
849
|
expiredAgo: expiresAt ? now - expiresAt : "n/a",
|
|
597
|
-
accessTokenPrefix: token.accessToken?.substring(0, 20),
|
|
598
850
|
path: pathname
|
|
599
851
|
});
|
|
600
852
|
}
|
|
601
|
-
if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
|
|
602
|
-
const
|
|
603
|
-
let refreshPromise = pendingRefreshes.get(
|
|
853
|
+
if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
|
|
854
|
+
const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
|
|
855
|
+
let refreshPromise = pendingRefreshes.get(grantKey);
|
|
604
856
|
if (refreshPromise) {
|
|
605
857
|
console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
|
|
606
858
|
} else {
|
|
@@ -608,15 +860,17 @@ function withOAuth42Auth(options = {}) {
|
|
|
608
860
|
refreshPromise = refreshTokens(
|
|
609
861
|
token.refreshToken,
|
|
610
862
|
refreshConfig.clientId,
|
|
611
|
-
refreshConfig.clientSecret,
|
|
612
|
-
refreshConfig.issuer
|
|
863
|
+
refreshConfig.clientSecret || "",
|
|
864
|
+
refreshConfig.issuer,
|
|
865
|
+
options.dpop,
|
|
866
|
+
options.clientAssertionKey
|
|
613
867
|
).finally(() => {
|
|
614
|
-
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(
|
|
868
|
+
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
|
|
615
869
|
if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
|
|
616
870
|
cleanupTimer.unref();
|
|
617
871
|
}
|
|
618
872
|
});
|
|
619
|
-
pendingRefreshes.set(
|
|
873
|
+
pendingRefreshes.set(grantKey, refreshPromise);
|
|
620
874
|
}
|
|
621
875
|
const refreshed = await refreshPromise;
|
|
622
876
|
if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
|
|
@@ -626,12 +880,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
626
880
|
refreshToken: refreshed.refreshToken,
|
|
627
881
|
expiresAt: refreshed.expiresAt
|
|
628
882
|
};
|
|
883
|
+
if (options.clientAssertionKey) delete updatedToken.clientSecret;
|
|
884
|
+
if (!await verifyAndAuthorize(updatedToken)) return denied();
|
|
629
885
|
const newJwt = await encode({
|
|
630
886
|
token: updatedToken,
|
|
631
887
|
secret
|
|
632
888
|
});
|
|
633
|
-
const requestHeaders =
|
|
634
|
-
requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
|
|
889
|
+
const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
|
|
635
890
|
const response = NextResponse.next({
|
|
636
891
|
request: {
|
|
637
892
|
headers: requestHeaders
|
|
@@ -684,28 +939,11 @@ function withOAuth42Auth(options = {}) {
|
|
|
684
939
|
chunked: newJwt.length > CHUNK_SIZE,
|
|
685
940
|
existingBaseCooke: req.cookies.has(cookieName),
|
|
686
941
|
existingChunks: existingChunks.length > 0 ? existingChunks : "none",
|
|
687
|
-
newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
|
|
688
942
|
newExpiresAt: refreshed.expiresAt,
|
|
689
943
|
cookieName
|
|
690
944
|
});
|
|
691
945
|
return response;
|
|
692
946
|
} else {
|
|
693
|
-
if (shouldPreserveSessionOnRefreshFailure({
|
|
694
|
-
error: refreshed.error,
|
|
695
|
-
expiresAt,
|
|
696
|
-
now
|
|
697
|
-
})) {
|
|
698
|
-
console.warn(
|
|
699
|
-
"[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
|
|
700
|
-
{
|
|
701
|
-
expiresAt,
|
|
702
|
-
now,
|
|
703
|
-
expiredAgo: now - expiresAt,
|
|
704
|
-
path: pathname
|
|
705
|
-
}
|
|
706
|
-
);
|
|
707
|
-
return NextResponse.next();
|
|
708
|
-
}
|
|
709
947
|
console.log("[OAuth42 Middleware] Refresh failed, clearing session");
|
|
710
948
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
711
949
|
const url = new URL(signInUrl, req.url);
|
|
@@ -723,16 +961,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
723
961
|
return response;
|
|
724
962
|
}
|
|
725
963
|
}
|
|
726
|
-
if (
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const url = new URL(signInUrl, req.url);
|
|
731
|
-
url.searchParams.set("callbackUrl", pathname);
|
|
732
|
-
return NextResponse.redirect(url);
|
|
733
|
-
}
|
|
964
|
+
if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
|
|
965
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
966
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
967
|
+
return NextResponse.redirect(url);
|
|
734
968
|
}
|
|
735
|
-
return
|
|
969
|
+
if (!await verifyAndAuthorize(token)) return denied();
|
|
970
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
736
971
|
};
|
|
737
972
|
}
|
|
738
973
|
function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
|
|
@@ -751,15 +986,87 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
|
|
|
751
986
|
publicPaths
|
|
752
987
|
};
|
|
753
988
|
}
|
|
989
|
+
|
|
990
|
+
// src/server/dpop.ts
|
|
991
|
+
import { createHash as createHash2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2, randomBytes as randomBytes2, sign as sign2 } from "crypto";
|
|
992
|
+
var OAuth42DPoP = class _OAuth42DPoP {
|
|
993
|
+
constructor(key) {
|
|
994
|
+
this.nonces = /* @__PURE__ */ new Map();
|
|
995
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
996
|
+
throw new Error("DPoP requires a private P-256 key");
|
|
997
|
+
}
|
|
998
|
+
this.key = key;
|
|
999
|
+
const publicKey = createPublicKey2(key).export({ format: "jwk" });
|
|
1000
|
+
this.jwk = { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y };
|
|
1001
|
+
this.thumbprint = createHash2("sha256").update(JSON.stringify(this.jwk)).digest("base64url");
|
|
1002
|
+
}
|
|
1003
|
+
/** Persist keys in protected server storage; regeneration cannot recover a public refresh grant. */
|
|
1004
|
+
static generate() {
|
|
1005
|
+
return new _OAuth42DPoP(generateKeyPairSync2("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
1006
|
+
}
|
|
1007
|
+
/** Passed directly to openid-client so its callback keeps OIDC issuer/nonce/signature validation. */
|
|
1008
|
+
get oidcKey() {
|
|
1009
|
+
return this.key;
|
|
1010
|
+
}
|
|
1011
|
+
proof(method, target, nonce, accessToken) {
|
|
1012
|
+
const url = new URL(target);
|
|
1013
|
+
if (!["http:", "https:"].includes(url.protocol) || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
1014
|
+
throw new Error("DPoP requires HTTPS outside loopback development");
|
|
1015
|
+
}
|
|
1016
|
+
if (url.username || url.password) throw new Error("DPoP endpoint must not contain credentials");
|
|
1017
|
+
url.search = "";
|
|
1018
|
+
url.hash = "";
|
|
1019
|
+
const header = { typ: "dpop+jwt", alg: "ES256", jwk: this.jwk };
|
|
1020
|
+
const claims = {
|
|
1021
|
+
htm: method.toUpperCase(),
|
|
1022
|
+
htu: url.href,
|
|
1023
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
1024
|
+
jti: randomBytes2(32).toString("base64url"),
|
|
1025
|
+
...nonce ? { nonce } : {},
|
|
1026
|
+
...accessToken !== void 0 ? { ath: createHash2("sha256").update(accessToken).digest("base64url") } : {}
|
|
1027
|
+
};
|
|
1028
|
+
const input = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
|
|
1029
|
+
return `${input}.${sign2("sha256", Buffer.from(input), { key: this.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
1030
|
+
}
|
|
1031
|
+
assertTokenResponse(tokens) {
|
|
1032
|
+
if (typeof tokens.token_type !== "string" || tokens.token_type.toLowerCase() !== "dpop") {
|
|
1033
|
+
throw new Error("Server did not issue the requested DPoP-bound token");
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
async fetch(target, init = {}, accessToken, prepare) {
|
|
1037
|
+
if (init.body instanceof ReadableStream) throw new Error("DPoP nonce retries require a replayable request body");
|
|
1038
|
+
const cacheKey = `${new URL(target).origin}:${accessToken !== void 0 ? "resource" : "token"}`;
|
|
1039
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1040
|
+
const request = prepare ? prepare() : init;
|
|
1041
|
+
const headers = new Headers(request.headers);
|
|
1042
|
+
headers.set("DPoP", this.proof(request.method || "GET", target, this.nonces.get(cacheKey), accessToken));
|
|
1043
|
+
if (accessToken !== void 0) headers.set("Authorization", `DPoP ${accessToken}`);
|
|
1044
|
+
const response = await fetch(target, { ...request, headers, redirect: "error", cache: "no-store", signal: request.signal || AbortSignal.timeout(3e4) });
|
|
1045
|
+
const nonce = response.headers.get("DPoP-Nonce");
|
|
1046
|
+
if (nonce && /^[\x21-\x7e]{1,128}$/.test(nonce)) {
|
|
1047
|
+
if (this.nonces.size >= 256) this.nonces.clear();
|
|
1048
|
+
this.nonces.set(cacheKey, nonce);
|
|
1049
|
+
} else return response;
|
|
1050
|
+
if (attempt || ![400, 401].includes(response.status)) return response;
|
|
1051
|
+
const error = await response.clone().json().catch(() => null);
|
|
1052
|
+
if (error?.error !== "use_dpop_nonce") return response;
|
|
1053
|
+
}
|
|
1054
|
+
throw new Error("DPoP nonce negotiation failed");
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
754
1057
|
export {
|
|
1058
|
+
OAuth42ClientAssertion,
|
|
1059
|
+
OAuth42DPoP,
|
|
755
1060
|
OAuth42Provider,
|
|
756
1061
|
createAuth,
|
|
757
1062
|
createMiddlewareConfig,
|
|
758
1063
|
getOAuth42Session,
|
|
759
1064
|
getServerSession,
|
|
1065
|
+
pushAuthorizationURL,
|
|
760
1066
|
refreshAccessToken,
|
|
761
1067
|
withOAuth42Auth,
|
|
762
1068
|
withOAuth42ServerSideProps,
|
|
763
|
-
withOAuth42Session
|
|
1069
|
+
withOAuth42Session,
|
|
1070
|
+
withPushedAuthorization
|
|
764
1071
|
};
|
|
765
1072
|
//# sourceMappingURL=index.mjs.map
|