@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/server/index.mjs
CHANGED
|
@@ -1,3 +1,190 @@
|
|
|
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
|
+
|
|
82
|
+
// src/server/par.ts
|
|
83
|
+
async function pushAuthorizationURL(value, config) {
|
|
84
|
+
const base = config.issuer.replace(/\/$/, "");
|
|
85
|
+
let url;
|
|
86
|
+
try {
|
|
87
|
+
url = new URL(value);
|
|
88
|
+
} catch {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
if (`${url.origin}${url.pathname}` !== `${base}/oauth2/authorize`) return value;
|
|
92
|
+
if (url.username || url.password || url.hash) throw new Error("Invalid authorization endpoint");
|
|
93
|
+
const body = new URLSearchParams(url.search);
|
|
94
|
+
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))) {
|
|
95
|
+
throw new Error("Invalid PAR authorization transaction");
|
|
96
|
+
}
|
|
97
|
+
const target = `${base}/oauth2/par`;
|
|
98
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
99
|
+
if (!config.clientAssertionKey) body.set("client_secret", config.clientSecret || "");
|
|
100
|
+
const init = {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
103
|
+
body,
|
|
104
|
+
redirect: "error",
|
|
105
|
+
cache: "no-store",
|
|
106
|
+
signal: AbortSignal.timeout(3e4)
|
|
107
|
+
};
|
|
108
|
+
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);
|
|
109
|
+
if (response.status !== 201) throw new Error("Pushed authorization failed");
|
|
110
|
+
const pushed = await response.json();
|
|
111
|
+
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");
|
|
112
|
+
url.search = new URLSearchParams({ client_id: config.clientId, request_uri: pushed.request_uri }).toString();
|
|
113
|
+
return url.href;
|
|
114
|
+
}
|
|
115
|
+
function withPushedAuthorization(handler, config) {
|
|
116
|
+
return async (request, context) => {
|
|
117
|
+
if (!(request instanceof Request)) throw new Error("PAR handlers require Next.js App Router Request/Response");
|
|
118
|
+
const response = await handler(request, context);
|
|
119
|
+
const location = response.headers.get("Location");
|
|
120
|
+
if (location) {
|
|
121
|
+
const next = await pushAuthorizationURL(location, config);
|
|
122
|
+
if (next === location) return response;
|
|
123
|
+
const headers = new Headers(response.headers);
|
|
124
|
+
headers.set("Location", next);
|
|
125
|
+
headers.delete("content-length");
|
|
126
|
+
return new Response(response.body, { status: response.status, headers });
|
|
127
|
+
}
|
|
128
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
129
|
+
const body = await response.clone().json();
|
|
130
|
+
if (typeof body?.url === "string") {
|
|
131
|
+
const next = await pushAuthorizationURL(body.url, config);
|
|
132
|
+
if (next !== body.url) {
|
|
133
|
+
const headers = new Headers(response.headers);
|
|
134
|
+
headers.delete("content-length");
|
|
135
|
+
return new Response(JSON.stringify({ ...body, url: next }), { status: response.status, headers });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return response;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/server/verified-userinfo.ts
|
|
144
|
+
async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
|
|
145
|
+
if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
|
|
146
|
+
try {
|
|
147
|
+
const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
|
|
148
|
+
const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
|
|
149
|
+
const response = await fetcher(target, {
|
|
150
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
151
|
+
redirect: "error",
|
|
152
|
+
cache: "no-store",
|
|
153
|
+
signal: AbortSignal.timeout(3e4)
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) return null;
|
|
156
|
+
const user = await response.json();
|
|
157
|
+
return parseVerifiedUserInfo(user, subject);
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function parseVerifiedUserInfo(value, subject) {
|
|
163
|
+
if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
164
|
+
const user = value;
|
|
165
|
+
if (user.sub !== subject) return null;
|
|
166
|
+
if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
|
|
167
|
+
if (user.email !== void 0 && typeof user.email !== "string") return null;
|
|
168
|
+
if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
|
|
169
|
+
const profile = { sub: user.sub };
|
|
170
|
+
for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
|
|
171
|
+
if (user[field] != null) {
|
|
172
|
+
if (typeof user[field] !== "string") return null;
|
|
173
|
+
profile[field] = user[field];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
|
|
177
|
+
if (user.groups !== void 0) profile.groups = user.groups;
|
|
178
|
+
return profile;
|
|
179
|
+
}
|
|
180
|
+
async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
|
|
181
|
+
if (typeof session.access_token !== "string" || !session.access_token) return null;
|
|
182
|
+
if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
|
|
183
|
+
return parseVerifiedUserInfo(session.user_info, subject);
|
|
184
|
+
}
|
|
185
|
+
return verifiedUserInfo(issuer, session.access_token, subject, dpop);
|
|
186
|
+
}
|
|
187
|
+
|
|
1
188
|
// src/server/auth.ts
|
|
2
189
|
import NextAuthDefault from "next-auth";
|
|
3
190
|
|
|
@@ -5,13 +192,20 @@ import NextAuthDefault from "next-auth";
|
|
|
5
192
|
function OAuth42Provider(options) {
|
|
6
193
|
const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
7
194
|
const baseUrl = issuer.replace(/\/$/, "");
|
|
195
|
+
const { dpop, clientAssertionKey, ...providerOptions } = options;
|
|
196
|
+
if (clientAssertionKey) {
|
|
197
|
+
OAuth42ClientAssertion.validateEndpoint(`${baseUrl}/oauth2/token`);
|
|
198
|
+
providerOptions.clientSecret = void 0;
|
|
199
|
+
}
|
|
200
|
+
if (options.pkceEnabled === false) throw new Error("OAuth42 requires S256 PKCE");
|
|
8
201
|
return {
|
|
9
202
|
id: "oauth42",
|
|
10
203
|
name: "OAuth42",
|
|
11
204
|
type: "oauth",
|
|
12
205
|
version: "2.0",
|
|
13
|
-
//
|
|
14
|
-
|
|
206
|
+
// Pin O42 metadata: NextAuth v4 discovery does not compare the returned issuer.
|
|
207
|
+
// Constructing its OIDC client from these trusted endpoints preserves issuer binding.
|
|
208
|
+
jwks_endpoint: `${baseUrl}/.well-known/jwks.json`,
|
|
15
209
|
// Use ID token for user profile (standard OIDC pattern)
|
|
16
210
|
// This avoids an extra HTTP call to /userinfo on every auth flow
|
|
17
211
|
// Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
|
|
@@ -24,17 +218,26 @@ function OAuth42Provider(options) {
|
|
|
24
218
|
response_type: "code"
|
|
25
219
|
}
|
|
26
220
|
},
|
|
27
|
-
token:
|
|
221
|
+
token: dpop ? {
|
|
222
|
+
url: `${baseUrl}/oauth2/token`,
|
|
223
|
+
async request({ client, provider, params, checks }) {
|
|
224
|
+
const tokens = await client.callback(provider.callbackUrl, params, checks, { DPoP: dpop.oidcKey });
|
|
225
|
+
dpop.assertTokenResponse(tokens);
|
|
226
|
+
return { tokens };
|
|
227
|
+
}
|
|
228
|
+
} : `${baseUrl}/oauth2/token`,
|
|
28
229
|
userinfo: `${baseUrl}/oauth2/userinfo`,
|
|
230
|
+
...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
|
|
29
231
|
client: {
|
|
30
232
|
id: options.clientId,
|
|
31
|
-
secret: options.clientSecret,
|
|
32
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
233
|
+
secret: clientAssertionKey ? void 0 : options.clientSecret,
|
|
234
|
+
token_endpoint_auth_method: clientAssertionKey ? "private_key_jwt" : "client_secret_post",
|
|
235
|
+
...clientAssertionKey ? { token_endpoint_auth_signing_alg: "ES256" } : {},
|
|
33
236
|
id_token_signed_response_alg: "RS256"
|
|
34
237
|
// OAuth42 uses RS256 for ID tokens
|
|
35
238
|
},
|
|
36
|
-
issuer
|
|
37
|
-
checks:
|
|
239
|
+
issuer,
|
|
240
|
+
checks: ["pkce", "state", "nonce"],
|
|
38
241
|
profile(profile, tokens) {
|
|
39
242
|
return {
|
|
40
243
|
id: profile.sub || profile.id || profile.email,
|
|
@@ -50,42 +253,18 @@ function OAuth42Provider(options) {
|
|
|
50
253
|
bg: "#1e40af",
|
|
51
254
|
text: "#ffffff"
|
|
52
255
|
},
|
|
53
|
-
|
|
256
|
+
// NextAuth v4 types require a string even when private_key_jwt sends no secret.
|
|
257
|
+
options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
|
|
54
258
|
};
|
|
55
259
|
}
|
|
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,7 +612,8 @@ 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 }
|
|
@@ -415,24 +624,40 @@ function createHandlers(authOptions) {
|
|
|
415
624
|
return { GET: handler, POST: handler };
|
|
416
625
|
}
|
|
417
626
|
var getServerSession = getOAuth42Session;
|
|
418
|
-
async function refreshAccessToken(token, clientId, clientSecret, issuer) {
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
627
|
+
async function refreshAccessToken(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
628
|
+
if (typeof token.refreshToken !== "string" || !token.refreshToken) {
|
|
629
|
+
return { ...token, error: "RefreshAccessTokenError" };
|
|
630
|
+
}
|
|
631
|
+
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]);
|
|
632
|
+
let refresh = activeRefreshes.get(key);
|
|
633
|
+
if (!refresh) {
|
|
634
|
+
refresh = doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey);
|
|
635
|
+
activeRefreshes.set(key, refresh);
|
|
422
636
|
}
|
|
423
|
-
activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
|
|
424
637
|
try {
|
|
425
|
-
|
|
638
|
+
const result = await refresh;
|
|
639
|
+
const updated = {
|
|
640
|
+
...token,
|
|
641
|
+
accessToken: result.accessToken,
|
|
642
|
+
refreshToken: result.refreshToken,
|
|
643
|
+
expiresAt: result.expiresAt,
|
|
644
|
+
error: result.error
|
|
645
|
+
};
|
|
646
|
+
if (clientAssertionKey) delete updated.clientSecret;
|
|
647
|
+
return updated;
|
|
426
648
|
} finally {
|
|
427
|
-
|
|
649
|
+
if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
|
|
428
650
|
}
|
|
429
651
|
}
|
|
430
|
-
async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
652
|
+
async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
431
653
|
try {
|
|
432
654
|
const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
433
655
|
const tokenUrl = `${baseUrl}/oauth2/token`;
|
|
434
656
|
const fetchOptions = {
|
|
435
657
|
method: "POST",
|
|
658
|
+
redirect: "error",
|
|
659
|
+
cache: "no-store",
|
|
660
|
+
signal: AbortSignal.timeout(3e4),
|
|
436
661
|
headers: {
|
|
437
662
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
438
663
|
},
|
|
@@ -443,17 +668,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
443
668
|
client_secret: clientSecret
|
|
444
669
|
})
|
|
445
670
|
};
|
|
446
|
-
|
|
447
|
-
const https2 = await import("https");
|
|
448
|
-
fetchOptions.agent = new https2.Agent({
|
|
449
|
-
rejectUnauthorized: false
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
const response = await fetch(tokenUrl, fetchOptions);
|
|
671
|
+
const response = clientAssertionKey ? await clientAssertionKey.fetch(clientId, tokenUrl, fetchOptions, dpop) : dpop ? await dpop.fetch(tokenUrl, fetchOptions) : await fetch(tokenUrl, fetchOptions);
|
|
453
672
|
const refreshedTokens = await response.json();
|
|
454
673
|
if (!response.ok) {
|
|
455
674
|
throw refreshedTokens;
|
|
456
675
|
}
|
|
676
|
+
dpop?.assertTokenResponse(refreshedTokens);
|
|
457
677
|
console.log("[OAuth42] Token refreshed successfully");
|
|
458
678
|
return {
|
|
459
679
|
...token,
|
|
@@ -465,7 +685,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
465
685
|
error: void 0
|
|
466
686
|
};
|
|
467
687
|
} catch (error) {
|
|
468
|
-
console.error("[OAuth42] Failed to refresh access token
|
|
688
|
+
console.error("[OAuth42] Failed to refresh access token");
|
|
469
689
|
return {
|
|
470
690
|
...token,
|
|
471
691
|
error: "RefreshAccessTokenError"
|
|
@@ -482,14 +702,20 @@ import { getToken, encode } from "next-auth/jwt";
|
|
|
482
702
|
var ALLOWED_COOKIE_SIZE = 4096;
|
|
483
703
|
var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
|
|
484
704
|
var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
485
|
-
var REFRESH_RACE_GRACE_SECONDS = 60;
|
|
486
705
|
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
487
706
|
var pendingRefreshes = /* @__PURE__ */ new Map();
|
|
488
|
-
function
|
|
489
|
-
|
|
490
|
-
|
|
707
|
+
function forwardedHeaders(req, cookieName, jwt) {
|
|
708
|
+
const headers = new Headers(req.headers);
|
|
709
|
+
headers.delete("x-oauth42-refreshed-token");
|
|
710
|
+
if (cookieName && jwt) {
|
|
711
|
+
const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
|
|
712
|
+
if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
|
|
713
|
+
else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
|
|
714
|
+
cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
|
|
715
|
+
}
|
|
716
|
+
headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
|
|
491
717
|
}
|
|
492
|
-
return
|
|
718
|
+
return headers;
|
|
493
719
|
}
|
|
494
720
|
function resolveRefreshConfig(options, token) {
|
|
495
721
|
return {
|
|
@@ -498,11 +724,15 @@ function resolveRefreshConfig(options, token) {
|
|
|
498
724
|
issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
|
|
499
725
|
};
|
|
500
726
|
}
|
|
501
|
-
async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
727
|
+
async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
502
728
|
try {
|
|
503
729
|
const tokenUrl = `${issuer}/oauth2/token`;
|
|
504
|
-
const
|
|
730
|
+
const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
|
|
731
|
+
const response = await fetcher(tokenUrl, {
|
|
505
732
|
method: "POST",
|
|
733
|
+
redirect: "error",
|
|
734
|
+
cache: "no-store",
|
|
735
|
+
signal: AbortSignal.timeout(3e4),
|
|
506
736
|
headers: {
|
|
507
737
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
508
738
|
},
|
|
@@ -515,9 +745,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
515
745
|
});
|
|
516
746
|
const data = await response.json();
|
|
517
747
|
if (!response.ok) {
|
|
518
|
-
console.error("[OAuth42 Middleware] Token refresh failed:",
|
|
748
|
+
console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
|
|
519
749
|
return { success: false, error: data.error || "refresh_failed" };
|
|
520
750
|
}
|
|
751
|
+
dpop?.assertTokenResponse(data);
|
|
521
752
|
console.log("[OAuth42 Middleware] Token refreshed successfully");
|
|
522
753
|
return {
|
|
523
754
|
success: true,
|
|
@@ -526,14 +757,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
526
757
|
expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
|
|
527
758
|
};
|
|
528
759
|
} catch (error) {
|
|
529
|
-
console.error("[OAuth42 Middleware] Token refresh
|
|
760
|
+
console.error("[OAuth42 Middleware] Token refresh request failed");
|
|
530
761
|
return { success: false, error: "refresh_error" };
|
|
531
762
|
}
|
|
532
763
|
}
|
|
533
764
|
function withOAuth42Auth(options = {}) {
|
|
534
765
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
535
766
|
if (!secret) {
|
|
536
|
-
|
|
767
|
+
throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
|
|
537
768
|
}
|
|
538
769
|
return async function middleware(req) {
|
|
539
770
|
const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
|
|
@@ -544,22 +775,41 @@ function withOAuth42Auth(options = {}) {
|
|
|
544
775
|
});
|
|
545
776
|
const pathname = req.nextUrl.pathname;
|
|
546
777
|
if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
|
|
547
|
-
return NextResponse.next();
|
|
778
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
548
779
|
}
|
|
549
780
|
const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
|
|
550
781
|
if (!needsProtection) {
|
|
551
|
-
return NextResponse.next();
|
|
782
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
552
783
|
}
|
|
553
|
-
if (!token) {
|
|
784
|
+
if (!token || token.error) {
|
|
554
785
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
555
786
|
const url = new URL(signInUrl, req.url);
|
|
556
787
|
url.searchParams.set("callbackUrl", pathname);
|
|
557
788
|
return NextResponse.redirect(url);
|
|
558
789
|
}
|
|
559
790
|
const refreshConfig = resolveRefreshConfig(options, token);
|
|
791
|
+
const denied = () => {
|
|
792
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
793
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
794
|
+
const response = NextResponse.redirect(url);
|
|
795
|
+
for (const cookie of req.cookies.getAll()) {
|
|
796
|
+
if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
|
|
797
|
+
}
|
|
798
|
+
return response;
|
|
799
|
+
};
|
|
800
|
+
const verifyAndAuthorize = async (current, resolved) => {
|
|
801
|
+
const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
|
|
802
|
+
if (!user) return false;
|
|
803
|
+
current.groups = user.groups ?? [];
|
|
804
|
+
current.email = user.email;
|
|
805
|
+
current.name = user.name;
|
|
806
|
+
current.username = user.preferred_username ?? user.username;
|
|
807
|
+
current.emailVerified = user.email_verified;
|
|
808
|
+
return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
|
|
809
|
+
};
|
|
560
810
|
if (isBackendSessionMode(options.sessionStore)) {
|
|
561
811
|
const sessionId = token.oauth42SessionId;
|
|
562
|
-
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
|
|
812
|
+
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
|
|
563
813
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
564
814
|
const url = new URL(signInUrl, req.url);
|
|
565
815
|
url.searchParams.set("callbackUrl", pathname);
|
|
@@ -568,9 +818,11 @@ function withOAuth42Auth(options = {}) {
|
|
|
568
818
|
return response;
|
|
569
819
|
}
|
|
570
820
|
const backendSession = await resolveBackendSession({
|
|
821
|
+
dpop: options.dpop,
|
|
822
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
571
823
|
issuer: refreshConfig.issuer,
|
|
572
824
|
clientId: refreshConfig.clientId,
|
|
573
|
-
clientSecret: refreshConfig.clientSecret,
|
|
825
|
+
clientSecret: refreshConfig.clientSecret || "",
|
|
574
826
|
sessionId
|
|
575
827
|
}).catch((error) => {
|
|
576
828
|
console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
|
|
@@ -584,8 +836,9 @@ function withOAuth42Auth(options = {}) {
|
|
|
584
836
|
response.cookies.delete(cookieName);
|
|
585
837
|
return response;
|
|
586
838
|
}
|
|
587
|
-
const
|
|
588
|
-
|
|
839
|
+
const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
|
|
840
|
+
if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
|
|
841
|
+
const requestHeaders = forwardedHeaders(req);
|
|
589
842
|
return NextResponse.next({
|
|
590
843
|
request: {
|
|
591
844
|
headers: requestHeaders
|
|
@@ -601,13 +854,12 @@ function withOAuth42Auth(options = {}) {
|
|
|
601
854
|
expiresAt,
|
|
602
855
|
now,
|
|
603
856
|
expiredAgo: expiresAt ? now - expiresAt : "n/a",
|
|
604
|
-
accessTokenPrefix: token.accessToken?.substring(0, 20),
|
|
605
857
|
path: pathname
|
|
606
858
|
});
|
|
607
859
|
}
|
|
608
|
-
if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
|
|
609
|
-
const
|
|
610
|
-
let refreshPromise = pendingRefreshes.get(
|
|
860
|
+
if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
|
|
861
|
+
const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
|
|
862
|
+
let refreshPromise = pendingRefreshes.get(grantKey);
|
|
611
863
|
if (refreshPromise) {
|
|
612
864
|
console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
|
|
613
865
|
} else {
|
|
@@ -615,15 +867,17 @@ function withOAuth42Auth(options = {}) {
|
|
|
615
867
|
refreshPromise = refreshTokens(
|
|
616
868
|
token.refreshToken,
|
|
617
869
|
refreshConfig.clientId,
|
|
618
|
-
refreshConfig.clientSecret,
|
|
619
|
-
refreshConfig.issuer
|
|
870
|
+
refreshConfig.clientSecret || "",
|
|
871
|
+
refreshConfig.issuer,
|
|
872
|
+
options.dpop,
|
|
873
|
+
options.clientAssertionKey
|
|
620
874
|
).finally(() => {
|
|
621
|
-
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(
|
|
875
|
+
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
|
|
622
876
|
if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
|
|
623
877
|
cleanupTimer.unref();
|
|
624
878
|
}
|
|
625
879
|
});
|
|
626
|
-
pendingRefreshes.set(
|
|
880
|
+
pendingRefreshes.set(grantKey, refreshPromise);
|
|
627
881
|
}
|
|
628
882
|
const refreshed = await refreshPromise;
|
|
629
883
|
if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
|
|
@@ -633,12 +887,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
633
887
|
refreshToken: refreshed.refreshToken,
|
|
634
888
|
expiresAt: refreshed.expiresAt
|
|
635
889
|
};
|
|
890
|
+
if (options.clientAssertionKey) delete updatedToken.clientSecret;
|
|
891
|
+
if (!await verifyAndAuthorize(updatedToken)) return denied();
|
|
636
892
|
const newJwt = await encode({
|
|
637
893
|
token: updatedToken,
|
|
638
894
|
secret
|
|
639
895
|
});
|
|
640
|
-
const requestHeaders =
|
|
641
|
-
requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
|
|
896
|
+
const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
|
|
642
897
|
const response = NextResponse.next({
|
|
643
898
|
request: {
|
|
644
899
|
headers: requestHeaders
|
|
@@ -691,28 +946,11 @@ function withOAuth42Auth(options = {}) {
|
|
|
691
946
|
chunked: newJwt.length > CHUNK_SIZE,
|
|
692
947
|
existingBaseCooke: req.cookies.has(cookieName),
|
|
693
948
|
existingChunks: existingChunks.length > 0 ? existingChunks : "none",
|
|
694
|
-
newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
|
|
695
949
|
newExpiresAt: refreshed.expiresAt,
|
|
696
950
|
cookieName
|
|
697
951
|
});
|
|
698
952
|
return response;
|
|
699
953
|
} else {
|
|
700
|
-
if (shouldPreserveSessionOnRefreshFailure({
|
|
701
|
-
error: refreshed.error,
|
|
702
|
-
expiresAt,
|
|
703
|
-
now
|
|
704
|
-
})) {
|
|
705
|
-
console.warn(
|
|
706
|
-
"[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
|
|
707
|
-
{
|
|
708
|
-
expiresAt,
|
|
709
|
-
now,
|
|
710
|
-
expiredAgo: now - expiresAt,
|
|
711
|
-
path: pathname
|
|
712
|
-
}
|
|
713
|
-
);
|
|
714
|
-
return NextResponse.next();
|
|
715
|
-
}
|
|
716
954
|
console.log("[OAuth42 Middleware] Refresh failed, clearing session");
|
|
717
955
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
718
956
|
const url = new URL(signInUrl, req.url);
|
|
@@ -730,16 +968,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
730
968
|
return response;
|
|
731
969
|
}
|
|
732
970
|
}
|
|
733
|
-
if (
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const url = new URL(signInUrl, req.url);
|
|
738
|
-
url.searchParams.set("callbackUrl", pathname);
|
|
739
|
-
return NextResponse.redirect(url);
|
|
740
|
-
}
|
|
971
|
+
if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
|
|
972
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
973
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
974
|
+
return NextResponse.redirect(url);
|
|
741
975
|
}
|
|
742
|
-
return
|
|
976
|
+
if (!await verifyAndAuthorize(token)) return denied();
|
|
977
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
743
978
|
};
|
|
744
979
|
}
|
|
745
980
|
function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
|
|
@@ -761,7 +996,7 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
|
|
|
761
996
|
|
|
762
997
|
// src/server/hosted-auth-callback.ts
|
|
763
998
|
import { NextResponse as NextResponse2 } from "next/server";
|
|
764
|
-
import
|
|
999
|
+
import { timingSafeEqual } from "crypto";
|
|
765
1000
|
import { encode as encode2 } from "next-auth/jwt";
|
|
766
1001
|
function createHostedAuthCallback(options = {}) {
|
|
767
1002
|
return async function GET(request) {
|
|
@@ -774,7 +1009,7 @@ function createHostedAuthCallback(options = {}) {
|
|
|
774
1009
|
redirectUrl = "/dashboard",
|
|
775
1010
|
errorUrl = "/"
|
|
776
1011
|
} = options;
|
|
777
|
-
if (!clientId || !clientSecret) {
|
|
1012
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
778
1013
|
console.error("OAuth42 client credentials are required");
|
|
779
1014
|
return NextResponse2.redirect(new URL(`${errorUrl}?error=missing_credentials`, request.url));
|
|
780
1015
|
}
|
|
@@ -784,6 +1019,14 @@ function createHostedAuthCallback(options = {}) {
|
|
|
784
1019
|
}
|
|
785
1020
|
const searchParams = request.nextUrl.searchParams;
|
|
786
1021
|
const code = searchParams.get("code");
|
|
1022
|
+
const cookiePrefix = process.env.NODE_ENV === "production" ? "__Host-" : request.nextUrl.protocol === "https:" ? "__Host-" : "";
|
|
1023
|
+
const expectedState = request.cookies.get(`${cookiePrefix}oauth42-state`)?.value;
|
|
1024
|
+
const returnedState = searchParams.get("state");
|
|
1025
|
+
const verifier = request.cookies.get(`${cookiePrefix}oauth42-pkce`)?.value;
|
|
1026
|
+
const validState = expectedState && returnedState && expectedState.length >= 32 && Buffer.byteLength(expectedState) === Buffer.byteLength(returnedState) && timingSafeEqual(Buffer.from(expectedState), Buffer.from(returnedState));
|
|
1027
|
+
if (!validState || !verifier || !/^[A-Za-z0-9._~-]{43,128}$/.test(verifier)) {
|
|
1028
|
+
return NextResponse2.redirect(new URL(`${errorUrl}?error=invalid_state`, request.url));
|
|
1029
|
+
}
|
|
787
1030
|
if (!code) {
|
|
788
1031
|
return NextResponse2.redirect(new URL(`${errorUrl}?error=missing_code`, request.url));
|
|
789
1032
|
}
|
|
@@ -791,39 +1034,38 @@ function createHostedAuthCallback(options = {}) {
|
|
|
791
1034
|
const tokenEndpoint = `${issuer}/oauth2/token`;
|
|
792
1035
|
const body = new URLSearchParams({
|
|
793
1036
|
grant_type: "authorization_code",
|
|
1037
|
+
code_verifier: verifier,
|
|
794
1038
|
code,
|
|
795
1039
|
redirect_uri: `${baseUrl}/api/oauth/callback`,
|
|
796
1040
|
client_id: clientId,
|
|
797
|
-
client_secret: clientSecret
|
|
1041
|
+
client_secret: clientSecret || ""
|
|
798
1042
|
});
|
|
799
1043
|
const fetchOptions = {
|
|
800
1044
|
method: "POST",
|
|
1045
|
+
redirect: "error",
|
|
1046
|
+
cache: "no-store",
|
|
1047
|
+
signal: AbortSignal.timeout(3e4),
|
|
801
1048
|
headers: {
|
|
802
1049
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
803
1050
|
},
|
|
804
1051
|
body: body.toString()
|
|
805
1052
|
};
|
|
806
|
-
|
|
807
|
-
const agent = new https.Agent({
|
|
808
|
-
rejectUnauthorized: false
|
|
809
|
-
});
|
|
810
|
-
fetchOptions.agent = agent;
|
|
811
|
-
}
|
|
812
|
-
const tokenResponse = await fetch(tokenEndpoint, fetchOptions);
|
|
1053
|
+
const tokenResponse = options.clientAssertionKey ? await options.clientAssertionKey.fetch(clientId, tokenEndpoint, fetchOptions, options.dpop) : options.dpop ? await options.dpop.fetch(tokenEndpoint, fetchOptions) : await fetch(tokenEndpoint, fetchOptions);
|
|
813
1054
|
const tokens = await tokenResponse.json();
|
|
814
1055
|
if (!tokenResponse.ok) {
|
|
815
|
-
console.error("Token exchange failed:",
|
|
1056
|
+
console.error("Token exchange failed:", tokenResponse.status);
|
|
816
1057
|
return NextResponse2.redirect(new URL(`${errorUrl}?error=token_exchange_failed`, request.url));
|
|
817
1058
|
}
|
|
1059
|
+
options.dpop?.assertTokenResponse(tokens);
|
|
818
1060
|
const userInfoEndpoint = `${issuer}/oauth2/userinfo`;
|
|
819
|
-
const
|
|
820
|
-
headers: {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1061
|
+
const infoOptions = {
|
|
1062
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
|
1063
|
+
redirect: "error",
|
|
1064
|
+
cache: "no-store",
|
|
1065
|
+
signal: AbortSignal.timeout(3e4)
|
|
1066
|
+
};
|
|
1067
|
+
const userInfoResponse = options.dpop ? await options.dpop.fetch(userInfoEndpoint, infoOptions, tokens.access_token) : await fetch(userInfoEndpoint, infoOptions);
|
|
1068
|
+
if (!userInfoResponse.ok) throw new Error("User info request failed");
|
|
827
1069
|
const userInfo = await userInfoResponse.json();
|
|
828
1070
|
const sessionToken = await encode2({
|
|
829
1071
|
token: {
|
|
@@ -848,6 +1090,14 @@ function createHostedAuthCallback(options = {}) {
|
|
|
848
1090
|
// 30 days
|
|
849
1091
|
path: "/"
|
|
850
1092
|
});
|
|
1093
|
+
for (const name of ["oauth42-state", "oauth42-pkce"]) {
|
|
1094
|
+
redirectResponse.cookies.set(`${cookiePrefix}${name}`, "", {
|
|
1095
|
+
path: "/",
|
|
1096
|
+
secure: cookiePrefix === "__Host-",
|
|
1097
|
+
sameSite: "lax",
|
|
1098
|
+
maxAge: 0
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
851
1101
|
return redirectResponse;
|
|
852
1102
|
} catch (error) {
|
|
853
1103
|
console.error("OAuth callback error:", error);
|
|
@@ -855,8 +1105,78 @@ function createHostedAuthCallback(options = {}) {
|
|
|
855
1105
|
}
|
|
856
1106
|
};
|
|
857
1107
|
}
|
|
1108
|
+
|
|
1109
|
+
// src/server/dpop.ts
|
|
1110
|
+
import { createHash as createHash2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2, randomBytes as randomBytes2, sign as sign2 } from "crypto";
|
|
1111
|
+
var OAuth42DPoP = class _OAuth42DPoP {
|
|
1112
|
+
constructor(key) {
|
|
1113
|
+
this.nonces = /* @__PURE__ */ new Map();
|
|
1114
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
1115
|
+
throw new Error("DPoP requires a private P-256 key");
|
|
1116
|
+
}
|
|
1117
|
+
this.key = key;
|
|
1118
|
+
const publicKey = createPublicKey2(key).export({ format: "jwk" });
|
|
1119
|
+
this.jwk = { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y };
|
|
1120
|
+
this.thumbprint = createHash2("sha256").update(JSON.stringify(this.jwk)).digest("base64url");
|
|
1121
|
+
}
|
|
1122
|
+
/** Persist keys in protected server storage; regeneration cannot recover a public refresh grant. */
|
|
1123
|
+
static generate() {
|
|
1124
|
+
return new _OAuth42DPoP(generateKeyPairSync2("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
1125
|
+
}
|
|
1126
|
+
/** Passed directly to openid-client so its callback keeps OIDC issuer/nonce/signature validation. */
|
|
1127
|
+
get oidcKey() {
|
|
1128
|
+
return this.key;
|
|
1129
|
+
}
|
|
1130
|
+
proof(method, target, nonce, accessToken) {
|
|
1131
|
+
const url = new URL(target);
|
|
1132
|
+
if (!["http:", "https:"].includes(url.protocol) || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
1133
|
+
throw new Error("DPoP requires HTTPS outside loopback development");
|
|
1134
|
+
}
|
|
1135
|
+
if (url.username || url.password) throw new Error("DPoP endpoint must not contain credentials");
|
|
1136
|
+
url.search = "";
|
|
1137
|
+
url.hash = "";
|
|
1138
|
+
const header = { typ: "dpop+jwt", alg: "ES256", jwk: this.jwk };
|
|
1139
|
+
const claims = {
|
|
1140
|
+
htm: method.toUpperCase(),
|
|
1141
|
+
htu: url.href,
|
|
1142
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
1143
|
+
jti: randomBytes2(32).toString("base64url"),
|
|
1144
|
+
...nonce ? { nonce } : {},
|
|
1145
|
+
...accessToken !== void 0 ? { ath: createHash2("sha256").update(accessToken).digest("base64url") } : {}
|
|
1146
|
+
};
|
|
1147
|
+
const input = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
|
|
1148
|
+
return `${input}.${sign2("sha256", Buffer.from(input), { key: this.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
1149
|
+
}
|
|
1150
|
+
assertTokenResponse(tokens) {
|
|
1151
|
+
if (typeof tokens.token_type !== "string" || tokens.token_type.toLowerCase() !== "dpop") {
|
|
1152
|
+
throw new Error("Server did not issue the requested DPoP-bound token");
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async fetch(target, init = {}, accessToken, prepare) {
|
|
1156
|
+
if (init.body instanceof ReadableStream) throw new Error("DPoP nonce retries require a replayable request body");
|
|
1157
|
+
const cacheKey = `${new URL(target).origin}:${accessToken !== void 0 ? "resource" : "token"}`;
|
|
1158
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1159
|
+
const request = prepare ? prepare() : init;
|
|
1160
|
+
const headers = new Headers(request.headers);
|
|
1161
|
+
headers.set("DPoP", this.proof(request.method || "GET", target, this.nonces.get(cacheKey), accessToken));
|
|
1162
|
+
if (accessToken !== void 0) headers.set("Authorization", `DPoP ${accessToken}`);
|
|
1163
|
+
const response = await fetch(target, { ...request, headers, redirect: "error", cache: "no-store", signal: request.signal || AbortSignal.timeout(3e4) });
|
|
1164
|
+
const nonce = response.headers.get("DPoP-Nonce");
|
|
1165
|
+
if (nonce && /^[\x21-\x7e]{1,128}$/.test(nonce)) {
|
|
1166
|
+
if (this.nonces.size >= 256) this.nonces.clear();
|
|
1167
|
+
this.nonces.set(cacheKey, nonce);
|
|
1168
|
+
} else return response;
|
|
1169
|
+
if (attempt || ![400, 401].includes(response.status)) return response;
|
|
1170
|
+
const error = await response.clone().json().catch(() => null);
|
|
1171
|
+
if (error?.error !== "use_dpop_nonce") return response;
|
|
1172
|
+
}
|
|
1173
|
+
throw new Error("DPoP nonce negotiation failed");
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
858
1176
|
export {
|
|
859
1177
|
default2 as NextAuth,
|
|
1178
|
+
OAuth42ClientAssertion,
|
|
1179
|
+
OAuth42DPoP,
|
|
860
1180
|
OAuth42Provider,
|
|
861
1181
|
createAuth,
|
|
862
1182
|
createBackendSession,
|
|
@@ -865,11 +1185,13 @@ export {
|
|
|
865
1185
|
createMiddlewareConfig,
|
|
866
1186
|
getOAuth42Session,
|
|
867
1187
|
getServerSession,
|
|
1188
|
+
pushAuthorizationURL,
|
|
868
1189
|
refreshAccessToken,
|
|
869
1190
|
resolveBackendSession,
|
|
870
1191
|
revokeBackendSession,
|
|
871
1192
|
withOAuth42Auth,
|
|
872
1193
|
withOAuth42ServerSideProps,
|
|
873
|
-
withOAuth42Session
|
|
1194
|
+
withOAuth42Session,
|
|
1195
|
+
withPushedAuthorization
|
|
874
1196
|
};
|
|
875
1197
|
//# sourceMappingURL=index.mjs.map
|