@oauth42/next 0.4.8 → 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 +164 -2
- package/dist/build-manifest.json +59 -0
- package/dist/client/index.d.mts +15 -6
- package/dist/client/index.d.ts +15 -6
- package/dist/client/index.js +23 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +23 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/index-BuZTiUds.d.mts +144 -0
- package/dist/index-BuZTiUds.d.ts +144 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +617 -104
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +613 -103
- package/dist/index.mjs.map +1 -1
- package/dist/middleware/index.d.mts +3 -39
- package/dist/middleware/index.d.ts +3 -39
- package/dist/middleware/index.js +283 -34
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +284 -34
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/{auth-BThRQMzh.d.ts → par-Cyu6M1e6.d.mts} +36 -11
- package/dist/{auth-BThRQMzh.d.mts → par-DQNt8j-s.d.ts} +36 -11
- package/dist/server/index.d.mts +7 -3
- package/dist/server/index.d.ts +7 -3
- package/dist/server/index.js +657 -123
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +650 -122
- 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,8 +144,115 @@ 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)
|
|
51
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;
|
|
210
|
+
};
|
|
211
|
+
}
|
|
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);
|
|
52
256
|
}
|
|
53
257
|
|
|
54
258
|
// src/server/auth.ts
|
|
@@ -56,36 +260,11 @@ 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) {
|
|
@@ -116,13 +295,66 @@ function withOAuth42ServerSideProps(getServerSideProps, authOptions) {
|
|
|
116
295
|
};
|
|
117
296
|
}
|
|
118
297
|
|
|
298
|
+
// src/server/backend-session.ts
|
|
299
|
+
function isBackendSessionMode(mode) {
|
|
300
|
+
return mode === "backend";
|
|
301
|
+
}
|
|
302
|
+
function sessionEndpoint(issuer) {
|
|
303
|
+
return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
|
|
304
|
+
}
|
|
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),
|
|
312
|
+
headers: { "Content-Type": "application/json" },
|
|
313
|
+
body: JSON.stringify({
|
|
314
|
+
...body,
|
|
315
|
+
client_id: params.clientId,
|
|
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 }
|
|
320
|
+
})
|
|
321
|
+
});
|
|
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);
|
|
338
|
+
}
|
|
339
|
+
async function resolveBackendSession(params) {
|
|
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);
|
|
343
|
+
}
|
|
344
|
+
async function revokeBackendSession(params) {
|
|
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}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
119
349
|
// src/server/auth.ts
|
|
120
350
|
var NextAuth = NextAuthDefault.default || NextAuthDefault;
|
|
121
|
-
var
|
|
351
|
+
var activeRefreshes = /* @__PURE__ */ new Map();
|
|
122
352
|
function createAuth(options = {}) {
|
|
123
353
|
const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
|
|
124
|
-
const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
125
|
-
|
|
354
|
+
const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
355
|
+
const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
356
|
+
const useBackendSessions = isBackendSessionMode(options.sessionStore);
|
|
357
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
126
358
|
throw new Error(
|
|
127
359
|
"OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
|
|
128
360
|
);
|
|
@@ -131,24 +363,57 @@ function createAuth(options = {}) {
|
|
|
131
363
|
providers: [
|
|
132
364
|
OAuth42Provider({
|
|
133
365
|
clientId,
|
|
134
|
-
clientSecret,
|
|
135
|
-
issuer
|
|
366
|
+
clientSecret: clientSecret || "",
|
|
367
|
+
issuer,
|
|
136
368
|
scopes: options.scopes,
|
|
137
|
-
pkceEnabled: options.pkceEnabled
|
|
369
|
+
pkceEnabled: options.pkceEnabled,
|
|
370
|
+
dpop: options.dpop,
|
|
371
|
+
clientAssertionKey: options.clientAssertionKey
|
|
138
372
|
})
|
|
139
373
|
],
|
|
140
374
|
callbacks: {
|
|
141
375
|
async jwt({ token, account, profile }) {
|
|
376
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
142
377
|
console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
|
|
143
378
|
if (account) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
379
|
+
if (useBackendSessions) {
|
|
380
|
+
if (!account.access_token || !account.refresh_token) {
|
|
381
|
+
throw new Error("OAuth42 backend sessions require access and refresh tokens");
|
|
382
|
+
}
|
|
383
|
+
const oauth42Profile = profile;
|
|
384
|
+
const backendSession = await createBackendSession({
|
|
385
|
+
dpop: options.dpop,
|
|
386
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
387
|
+
issuer,
|
|
388
|
+
clientId,
|
|
389
|
+
clientSecret: clientSecret || "",
|
|
390
|
+
accessToken: account.access_token,
|
|
391
|
+
refreshToken: account.refresh_token,
|
|
392
|
+
expiresAt: account.expires_at,
|
|
393
|
+
idToken: account.id_token,
|
|
394
|
+
user: {
|
|
395
|
+
sub: oauth42Profile?.sub,
|
|
396
|
+
email: oauth42Profile?.email,
|
|
397
|
+
username: oauth42Profile?.username,
|
|
398
|
+
name: token.name,
|
|
399
|
+
emailVerified: oauth42Profile?.email_verified,
|
|
400
|
+
groups: oauth42Profile?.groups
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
token.oauth42SessionId = backendSession.session_id;
|
|
404
|
+
token.expiresAt = backendSession.expires_at;
|
|
405
|
+
token.issuer = issuer;
|
|
406
|
+
} else {
|
|
407
|
+
console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
|
|
408
|
+
token.accessToken = account.access_token;
|
|
409
|
+
token.refreshToken = account.refresh_token;
|
|
410
|
+
token.expiresAt = account.expires_at;
|
|
411
|
+
token.idToken = account.id_token;
|
|
412
|
+
token.clientId = clientId;
|
|
413
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
414
|
+
else token.clientSecret = clientSecret;
|
|
415
|
+
token.issuer = issuer;
|
|
416
|
+
}
|
|
152
417
|
}
|
|
153
418
|
if (profile) {
|
|
154
419
|
const oauth42Profile = profile;
|
|
@@ -165,12 +430,68 @@ function createAuth(options = {}) {
|
|
|
165
430
|
},
|
|
166
431
|
async session({ session, token }) {
|
|
167
432
|
console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
|
|
168
|
-
|
|
169
|
-
|
|
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;
|
|
443
|
+
if (useBackendSessions) {
|
|
444
|
+
const sessionId = token.oauth42SessionId;
|
|
445
|
+
if (!sessionId) {
|
|
446
|
+
return revokedSession();
|
|
447
|
+
}
|
|
448
|
+
const backendSession = await resolveBackendSession({
|
|
449
|
+
dpop: options.dpop,
|
|
450
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
451
|
+
issuer,
|
|
452
|
+
clientId,
|
|
453
|
+
clientSecret: clientSecret || "",
|
|
454
|
+
sessionId
|
|
455
|
+
});
|
|
456
|
+
if (!backendSession) {
|
|
457
|
+
return revokedSession();
|
|
458
|
+
}
|
|
459
|
+
session.accessToken = backendSession.access_token;
|
|
460
|
+
session.idToken = backendSession.id_token;
|
|
461
|
+
token.expiresAt = backendSession.expires_at;
|
|
462
|
+
verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
|
|
463
|
+
if (backendSession.user && session.user) {
|
|
464
|
+
session.user.email = backendSession.user.email;
|
|
465
|
+
session.user.name = backendSession.user.name;
|
|
466
|
+
session.user.username = backendSession.user.username;
|
|
467
|
+
session.user.emailVerified = backendSession.user.emailVerified;
|
|
468
|
+
session.user.groups = backendSession.user.groups;
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
session.accessToken = token.accessToken;
|
|
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 ?? [];
|
|
490
|
+
}
|
|
170
491
|
if (token.error) {
|
|
171
492
|
session.error = token.error;
|
|
172
493
|
}
|
|
173
|
-
if (session.user) {
|
|
494
|
+
if (!useBackendSessions && session.user) {
|
|
174
495
|
session.user.email = token.email;
|
|
175
496
|
session.user.name = token.name;
|
|
176
497
|
session.user.username = token.username;
|
|
@@ -194,6 +515,28 @@ function createAuth(options = {}) {
|
|
|
194
515
|
strategy: "jwt",
|
|
195
516
|
...options.session
|
|
196
517
|
},
|
|
518
|
+
events: {
|
|
519
|
+
async signOut(message) {
|
|
520
|
+
if (useBackendSessions) {
|
|
521
|
+
const sessionId = message.token?.oauth42SessionId;
|
|
522
|
+
if (sessionId) {
|
|
523
|
+
await revokeBackendSession({
|
|
524
|
+
dpop: options.dpop,
|
|
525
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
526
|
+
issuer,
|
|
527
|
+
clientId,
|
|
528
|
+
clientSecret: clientSecret || "",
|
|
529
|
+
sessionId
|
|
530
|
+
}).catch((error) => {
|
|
531
|
+
console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (options.events?.signOut) {
|
|
536
|
+
await options.events.signOut(message);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
},
|
|
197
540
|
debug: options.debug || process.env.NODE_ENV === "development",
|
|
198
541
|
secret: process.env.NEXTAUTH_SECRET,
|
|
199
542
|
// Configure unique cookie names per app to prevent session conflicts on localhost
|
|
@@ -269,31 +612,48 @@ function createAuth(options = {}) {
|
|
|
269
612
|
}
|
|
270
613
|
}
|
|
271
614
|
};
|
|
272
|
-
const
|
|
615
|
+
const nextAuthHandler = NextAuth(authOptions);
|
|
616
|
+
const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
|
|
273
617
|
return {
|
|
274
618
|
auth: authOptions,
|
|
275
619
|
handlers: { GET: handler, POST: handler }
|
|
276
620
|
};
|
|
277
621
|
}
|
|
278
622
|
var getServerSession = getOAuth42Session;
|
|
279
|
-
async function refreshAccessToken(token, clientId, clientSecret, issuer) {
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
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);
|
|
283
632
|
}
|
|
284
|
-
activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
|
|
285
633
|
try {
|
|
286
|
-
|
|
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;
|
|
287
644
|
} finally {
|
|
288
|
-
|
|
645
|
+
if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
|
|
289
646
|
}
|
|
290
647
|
}
|
|
291
|
-
async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
648
|
+
async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
292
649
|
try {
|
|
293
650
|
const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
294
651
|
const tokenUrl = `${baseUrl}/oauth2/token`;
|
|
295
652
|
const fetchOptions = {
|
|
296
653
|
method: "POST",
|
|
654
|
+
redirect: "error",
|
|
655
|
+
cache: "no-store",
|
|
656
|
+
signal: AbortSignal.timeout(3e4),
|
|
297
657
|
headers: {
|
|
298
658
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
299
659
|
},
|
|
@@ -304,17 +664,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
304
664
|
client_secret: clientSecret
|
|
305
665
|
})
|
|
306
666
|
};
|
|
307
|
-
|
|
308
|
-
const https = await import("https");
|
|
309
|
-
fetchOptions.agent = new https.Agent({
|
|
310
|
-
rejectUnauthorized: false
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
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);
|
|
314
668
|
const refreshedTokens = await response.json();
|
|
315
669
|
if (!response.ok) {
|
|
316
670
|
throw refreshedTokens;
|
|
317
671
|
}
|
|
672
|
+
dpop?.assertTokenResponse(refreshedTokens);
|
|
318
673
|
console.log("[OAuth42] Token refreshed successfully");
|
|
319
674
|
return {
|
|
320
675
|
...token,
|
|
@@ -326,7 +681,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
326
681
|
error: void 0
|
|
327
682
|
};
|
|
328
683
|
} catch (error) {
|
|
329
|
-
console.error("[OAuth42] Failed to refresh access token
|
|
684
|
+
console.error("[OAuth42] Failed to refresh access token");
|
|
330
685
|
return {
|
|
331
686
|
...token,
|
|
332
687
|
error: "RefreshAccessTokenError"
|
|
@@ -340,12 +695,37 @@ import { getToken, encode } from "next-auth/jwt";
|
|
|
340
695
|
var ALLOWED_COOKIE_SIZE = 4096;
|
|
341
696
|
var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
|
|
342
697
|
var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
698
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
343
699
|
var pendingRefreshes = /* @__PURE__ */ new Map();
|
|
344
|
-
|
|
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("; "));
|
|
710
|
+
}
|
|
711
|
+
return headers;
|
|
712
|
+
}
|
|
713
|
+
function resolveRefreshConfig(options, token) {
|
|
714
|
+
return {
|
|
715
|
+
clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
|
|
716
|
+
clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
|
|
717
|
+
issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
345
721
|
try {
|
|
346
722
|
const tokenUrl = `${issuer}/oauth2/token`;
|
|
347
|
-
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, {
|
|
348
725
|
method: "POST",
|
|
726
|
+
redirect: "error",
|
|
727
|
+
cache: "no-store",
|
|
728
|
+
signal: AbortSignal.timeout(3e4),
|
|
349
729
|
headers: {
|
|
350
730
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
351
731
|
},
|
|
@@ -358,9 +738,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
358
738
|
});
|
|
359
739
|
const data = await response.json();
|
|
360
740
|
if (!response.ok) {
|
|
361
|
-
console.error("[OAuth42 Middleware] Token refresh failed:",
|
|
741
|
+
console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
|
|
362
742
|
return { success: false, error: data.error || "refresh_failed" };
|
|
363
743
|
}
|
|
744
|
+
dpop?.assertTokenResponse(data);
|
|
364
745
|
console.log("[OAuth42 Middleware] Token refreshed successfully");
|
|
365
746
|
return {
|
|
366
747
|
success: true,
|
|
@@ -369,17 +750,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
369
750
|
expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
|
|
370
751
|
};
|
|
371
752
|
} catch (error) {
|
|
372
|
-
console.error("[OAuth42 Middleware] Token refresh
|
|
753
|
+
console.error("[OAuth42 Middleware] Token refresh request failed");
|
|
373
754
|
return { success: false, error: "refresh_error" };
|
|
374
755
|
}
|
|
375
756
|
}
|
|
376
757
|
function withOAuth42Auth(options = {}) {
|
|
377
758
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
378
|
-
const clientId = process.env.OAUTH42_CLIENT_ID;
|
|
379
|
-
const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
|
|
380
|
-
const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
381
759
|
if (!secret) {
|
|
382
|
-
|
|
760
|
+
throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
|
|
383
761
|
}
|
|
384
762
|
return async function middleware(req) {
|
|
385
763
|
const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
|
|
@@ -390,18 +768,76 @@ function withOAuth42Auth(options = {}) {
|
|
|
390
768
|
});
|
|
391
769
|
const pathname = req.nextUrl.pathname;
|
|
392
770
|
if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
|
|
393
|
-
return NextResponse.next();
|
|
771
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
394
772
|
}
|
|
395
773
|
const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
|
|
396
774
|
if (!needsProtection) {
|
|
397
|
-
return NextResponse.next();
|
|
775
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
398
776
|
}
|
|
399
|
-
if (!token) {
|
|
777
|
+
if (!token || token.error) {
|
|
400
778
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
401
779
|
const url = new URL(signInUrl, req.url);
|
|
402
780
|
url.searchParams.set("callbackUrl", pathname);
|
|
403
781
|
return NextResponse.redirect(url);
|
|
404
782
|
}
|
|
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
|
+
};
|
|
803
|
+
if (isBackendSessionMode(options.sessionStore)) {
|
|
804
|
+
const sessionId = token.oauth42SessionId;
|
|
805
|
+
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
|
|
806
|
+
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
807
|
+
const url = new URL(signInUrl, req.url);
|
|
808
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
809
|
+
const response = NextResponse.redirect(url);
|
|
810
|
+
response.cookies.delete(cookieName);
|
|
811
|
+
return response;
|
|
812
|
+
}
|
|
813
|
+
const backendSession = await resolveBackendSession({
|
|
814
|
+
dpop: options.dpop,
|
|
815
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
816
|
+
issuer: refreshConfig.issuer,
|
|
817
|
+
clientId: refreshConfig.clientId,
|
|
818
|
+
clientSecret: refreshConfig.clientSecret || "",
|
|
819
|
+
sessionId
|
|
820
|
+
}).catch((error) => {
|
|
821
|
+
console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
|
|
822
|
+
return null;
|
|
823
|
+
});
|
|
824
|
+
if (!backendSession) {
|
|
825
|
+
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
826
|
+
const url = new URL(signInUrl, req.url);
|
|
827
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
828
|
+
const response = NextResponse.redirect(url);
|
|
829
|
+
response.cookies.delete(cookieName);
|
|
830
|
+
return response;
|
|
831
|
+
}
|
|
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);
|
|
835
|
+
return NextResponse.next({
|
|
836
|
+
request: {
|
|
837
|
+
headers: requestHeaders
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
405
841
|
const expiresAt = token.expiresAt;
|
|
406
842
|
const now = Math.floor(Date.now() / 1e3);
|
|
407
843
|
const bufferSeconds = 15;
|
|
@@ -411,26 +847,30 @@ function withOAuth42Auth(options = {}) {
|
|
|
411
847
|
expiresAt,
|
|
412
848
|
now,
|
|
413
849
|
expiredAgo: expiresAt ? now - expiresAt : "n/a",
|
|
414
|
-
accessTokenPrefix: token.accessToken?.substring(0, 20),
|
|
415
850
|
path: pathname
|
|
416
851
|
});
|
|
417
852
|
}
|
|
418
|
-
if (needsRefresh && token.refreshToken && clientId && clientSecret) {
|
|
419
|
-
const
|
|
420
|
-
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);
|
|
421
856
|
if (refreshPromise) {
|
|
422
857
|
console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
|
|
423
858
|
} else {
|
|
424
859
|
console.log("[OAuth42 Middleware] Access token expired, refreshing...");
|
|
425
860
|
refreshPromise = refreshTokens(
|
|
426
861
|
token.refreshToken,
|
|
427
|
-
clientId,
|
|
428
|
-
clientSecret,
|
|
429
|
-
issuer
|
|
862
|
+
refreshConfig.clientId,
|
|
863
|
+
refreshConfig.clientSecret || "",
|
|
864
|
+
refreshConfig.issuer,
|
|
865
|
+
options.dpop,
|
|
866
|
+
options.clientAssertionKey
|
|
430
867
|
).finally(() => {
|
|
431
|
-
setTimeout(() => pendingRefreshes.delete(
|
|
868
|
+
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
|
|
869
|
+
if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
|
|
870
|
+
cleanupTimer.unref();
|
|
871
|
+
}
|
|
432
872
|
});
|
|
433
|
-
pendingRefreshes.set(
|
|
873
|
+
pendingRefreshes.set(grantKey, refreshPromise);
|
|
434
874
|
}
|
|
435
875
|
const refreshed = await refreshPromise;
|
|
436
876
|
if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
|
|
@@ -440,12 +880,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
440
880
|
refreshToken: refreshed.refreshToken,
|
|
441
881
|
expiresAt: refreshed.expiresAt
|
|
442
882
|
};
|
|
883
|
+
if (options.clientAssertionKey) delete updatedToken.clientSecret;
|
|
884
|
+
if (!await verifyAndAuthorize(updatedToken)) return denied();
|
|
443
885
|
const newJwt = await encode({
|
|
444
886
|
token: updatedToken,
|
|
445
887
|
secret
|
|
446
888
|
});
|
|
447
|
-
const requestHeaders =
|
|
448
|
-
requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
|
|
889
|
+
const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
|
|
449
890
|
const response = NextResponse.next({
|
|
450
891
|
request: {
|
|
451
892
|
headers: requestHeaders
|
|
@@ -455,7 +896,8 @@ function withOAuth42Auth(options = {}) {
|
|
|
455
896
|
httpOnly: true,
|
|
456
897
|
sameSite: "lax",
|
|
457
898
|
path: "/",
|
|
458
|
-
secure: process.env.NODE_ENV === "production"
|
|
899
|
+
secure: process.env.NODE_ENV === "production",
|
|
900
|
+
maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
|
|
459
901
|
};
|
|
460
902
|
if (newJwt.length <= CHUNK_SIZE) {
|
|
461
903
|
response.cookies.set(cookieName, newJwt, cookieOptions);
|
|
@@ -497,13 +939,12 @@ function withOAuth42Auth(options = {}) {
|
|
|
497
939
|
chunked: newJwt.length > CHUNK_SIZE,
|
|
498
940
|
existingBaseCooke: req.cookies.has(cookieName),
|
|
499
941
|
existingChunks: existingChunks.length > 0 ? existingChunks : "none",
|
|
500
|
-
newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
|
|
501
942
|
newExpiresAt: refreshed.expiresAt,
|
|
502
943
|
cookieName
|
|
503
944
|
});
|
|
504
945
|
return response;
|
|
505
946
|
} else {
|
|
506
|
-
console.log("[OAuth42 Middleware] Refresh failed
|
|
947
|
+
console.log("[OAuth42 Middleware] Refresh failed, clearing session");
|
|
507
948
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
508
949
|
const url = new URL(signInUrl, req.url);
|
|
509
950
|
url.searchParams.set("callbackUrl", pathname);
|
|
@@ -520,16 +961,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
520
961
|
return response;
|
|
521
962
|
}
|
|
522
963
|
}
|
|
523
|
-
if (
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const url = new URL(signInUrl, req.url);
|
|
528
|
-
url.searchParams.set("callbackUrl", pathname);
|
|
529
|
-
return NextResponse.redirect(url);
|
|
530
|
-
}
|
|
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);
|
|
531
968
|
}
|
|
532
|
-
return
|
|
969
|
+
if (!await verifyAndAuthorize(token)) return denied();
|
|
970
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
533
971
|
};
|
|
534
972
|
}
|
|
535
973
|
function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
|
|
@@ -548,15 +986,87 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
|
|
|
548
986
|
publicPaths
|
|
549
987
|
};
|
|
550
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
|
+
};
|
|
551
1057
|
export {
|
|
1058
|
+
OAuth42ClientAssertion,
|
|
1059
|
+
OAuth42DPoP,
|
|
552
1060
|
OAuth42Provider,
|
|
553
1061
|
createAuth,
|
|
554
1062
|
createMiddlewareConfig,
|
|
555
1063
|
getOAuth42Session,
|
|
556
1064
|
getServerSession,
|
|
1065
|
+
pushAuthorizationURL,
|
|
557
1066
|
refreshAccessToken,
|
|
558
1067
|
withOAuth42Auth,
|
|
559
1068
|
withOAuth42ServerSideProps,
|
|
560
|
-
withOAuth42Session
|
|
1069
|
+
withOAuth42Session,
|
|
1070
|
+
withPushedAuthorization
|
|
561
1071
|
};
|
|
562
1072
|
//# sourceMappingURL=index.mjs.map
|