@oauth42/next 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/dist/build-manifest.json +59 -0
- package/dist/client/index.d.mts +7 -6
- package/dist/client/index.d.ts +7 -6
- package/dist/client/index.js +13 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +13 -1
- package/dist/client/index.mjs.map +1 -1
- package/dist/{index-Cfqxl9r2.d.mts → index-BuZTiUds.d.mts} +51 -1
- package/dist/{index-Cfqxl9r2.d.ts → index-BuZTiUds.d.ts} +51 -1
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +469 -159
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +465 -158
- package/dist/index.mjs.map +1 -1
- package/dist/middleware/index.d.mts +2 -1
- package/dist/middleware/index.d.ts +2 -1
- package/dist/middleware/index.js +218 -66
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +219 -66
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/{auth-D3tAruwx.d.mts → par-Cyu6M1e6.d.mts} +27 -12
- package/dist/{auth-CYMPe3k0.d.ts → par-DQNt8j-s.d.ts} +27 -12
- package/dist/server/index.d.mts +7 -3
- package/dist/server/index.d.ts +7 -3
- package/dist/server/index.js +503 -178
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +499 -177
- package/dist/server/index.mjs.map +1 -1
- package/package.json +14 -5
- package/src/types/index.ts +2 -0
- package/src/types/next-auth.d.ts +2 -0
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __typeError = (msg) => {
|
|
9
|
+
throw TypeError(msg);
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -26,33 +29,121 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
29
|
mod
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
33
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
34
|
+
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);
|
|
35
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
29
36
|
|
|
30
37
|
// src/index.ts
|
|
31
38
|
var src_exports = {};
|
|
32
39
|
__export(src_exports, {
|
|
40
|
+
OAuth42ClientAssertion: () => OAuth42ClientAssertion,
|
|
41
|
+
OAuth42DPoP: () => OAuth42DPoP,
|
|
33
42
|
OAuth42Provider: () => OAuth42Provider,
|
|
34
43
|
createAuth: () => createAuth,
|
|
35
44
|
createMiddlewareConfig: () => createMiddlewareConfig,
|
|
36
45
|
getOAuth42Session: () => getOAuth42Session,
|
|
37
46
|
getServerSession: () => getServerSession,
|
|
47
|
+
pushAuthorizationURL: () => pushAuthorizationURL,
|
|
38
48
|
refreshAccessToken: () => refreshAccessToken,
|
|
39
49
|
withOAuth42Auth: () => withOAuth42Auth,
|
|
40
50
|
withOAuth42ServerSideProps: () => withOAuth42ServerSideProps,
|
|
41
|
-
withOAuth42Session: () => withOAuth42Session
|
|
51
|
+
withOAuth42Session: () => withOAuth42Session,
|
|
52
|
+
withPushedAuthorization: () => withPushedAuthorization
|
|
42
53
|
});
|
|
43
54
|
module.exports = __toCommonJS(src_exports);
|
|
44
55
|
|
|
56
|
+
// src/server/client-assertion.ts
|
|
57
|
+
var import_node_crypto = require("crypto");
|
|
58
|
+
var _key, _public;
|
|
59
|
+
var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
|
|
60
|
+
constructor(kid, key) {
|
|
61
|
+
__privateAdd(this, _key);
|
|
62
|
+
__privateAdd(this, _public);
|
|
63
|
+
if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
|
|
64
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
65
|
+
throw new Error("Client assertions require a private P-256 key");
|
|
66
|
+
}
|
|
67
|
+
__privateSet(this, _key, key);
|
|
68
|
+
this.kid = kid;
|
|
69
|
+
const publicKey = (0, import_node_crypto.createPublicKey)(key).export({ format: "jwk" });
|
|
70
|
+
__privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
|
|
71
|
+
this.thumbprint = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
|
|
72
|
+
}
|
|
73
|
+
static generate(kid) {
|
|
74
|
+
return new _OAuth42ClientAssertion(kid, (0, import_node_crypto.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
75
|
+
}
|
|
76
|
+
get publicJwk() {
|
|
77
|
+
return { ...__privateGet(this, _public), kid: this.kid };
|
|
78
|
+
}
|
|
79
|
+
/** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
|
|
80
|
+
get oidcJwks() {
|
|
81
|
+
return { keys: [{
|
|
82
|
+
kty: "EC",
|
|
83
|
+
crv: "P-256",
|
|
84
|
+
x: __privateGet(this, _public).x,
|
|
85
|
+
y: __privateGet(this, _public).y,
|
|
86
|
+
d: __privateGet(this, _key).export({ format: "jwk" }).d,
|
|
87
|
+
kid: this.kid
|
|
88
|
+
}] };
|
|
89
|
+
}
|
|
90
|
+
static validateEndpoint(target) {
|
|
91
|
+
const url = new URL(target);
|
|
92
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
93
|
+
throw new Error("Client authentication requires HTTPS outside loopback development");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
assertion(clientId, audience) {
|
|
97
|
+
_OAuth42ClientAssertion.validateEndpoint(audience);
|
|
98
|
+
if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
|
|
99
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
100
|
+
const header = { alg: "ES256", typ: "JWT", kid: this.kid };
|
|
101
|
+
const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: (0, import_node_crypto.randomBytes)(32).toString("base64url") };
|
|
102
|
+
const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
|
|
103
|
+
return `${data}.${(0, import_node_crypto.sign)("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
104
|
+
}
|
|
105
|
+
async fetch(clientId, target, init, dpop) {
|
|
106
|
+
if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
|
|
107
|
+
throw new Error("Client assertions require a form POST");
|
|
108
|
+
}
|
|
109
|
+
const prepare = () => {
|
|
110
|
+
const body = new URLSearchParams(init.body);
|
|
111
|
+
body.delete("client_secret");
|
|
112
|
+
body.set("client_id", clientId);
|
|
113
|
+
body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
|
114
|
+
body.set("client_assertion", this.assertion(clientId, target));
|
|
115
|
+
const headers = new Headers(init.headers);
|
|
116
|
+
headers.delete("Authorization");
|
|
117
|
+
headers.delete("Content-Length");
|
|
118
|
+
headers.delete("Transfer-Encoding");
|
|
119
|
+
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
|
120
|
+
return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
|
|
121
|
+
};
|
|
122
|
+
return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
_key = new WeakMap();
|
|
126
|
+
_public = new WeakMap();
|
|
127
|
+
var OAuth42ClientAssertion = _OAuth42ClientAssertion;
|
|
128
|
+
|
|
45
129
|
// src/provider.ts
|
|
46
130
|
function OAuth42Provider(options) {
|
|
47
131
|
const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
48
132
|
const baseUrl = issuer.replace(/\/$/, "");
|
|
133
|
+
const { dpop, clientAssertionKey, ...providerOptions } = options;
|
|
134
|
+
if (clientAssertionKey) {
|
|
135
|
+
OAuth42ClientAssertion.validateEndpoint(`${baseUrl}/oauth2/token`);
|
|
136
|
+
providerOptions.clientSecret = void 0;
|
|
137
|
+
}
|
|
138
|
+
if (options.pkceEnabled === false) throw new Error("OAuth42 requires S256 PKCE");
|
|
49
139
|
return {
|
|
50
140
|
id: "oauth42",
|
|
51
141
|
name: "OAuth42",
|
|
52
142
|
type: "oauth",
|
|
53
143
|
version: "2.0",
|
|
54
|
-
//
|
|
55
|
-
|
|
144
|
+
// Pin O42 metadata: NextAuth v4 discovery does not compare the returned issuer.
|
|
145
|
+
// Constructing its OIDC client from these trusted endpoints preserves issuer binding.
|
|
146
|
+
jwks_endpoint: `${baseUrl}/.well-known/jwks.json`,
|
|
56
147
|
// Use ID token for user profile (standard OIDC pattern)
|
|
57
148
|
// This avoids an extra HTTP call to /userinfo on every auth flow
|
|
58
149
|
// Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
|
|
@@ -65,17 +156,26 @@ function OAuth42Provider(options) {
|
|
|
65
156
|
response_type: "code"
|
|
66
157
|
}
|
|
67
158
|
},
|
|
68
|
-
token:
|
|
159
|
+
token: dpop ? {
|
|
160
|
+
url: `${baseUrl}/oauth2/token`,
|
|
161
|
+
async request({ client, provider, params, checks }) {
|
|
162
|
+
const tokens = await client.callback(provider.callbackUrl, params, checks, { DPoP: dpop.oidcKey });
|
|
163
|
+
dpop.assertTokenResponse(tokens);
|
|
164
|
+
return { tokens };
|
|
165
|
+
}
|
|
166
|
+
} : `${baseUrl}/oauth2/token`,
|
|
69
167
|
userinfo: `${baseUrl}/oauth2/userinfo`,
|
|
168
|
+
...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
|
|
70
169
|
client: {
|
|
71
170
|
id: options.clientId,
|
|
72
|
-
secret: options.clientSecret,
|
|
73
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
171
|
+
secret: clientAssertionKey ? void 0 : options.clientSecret,
|
|
172
|
+
token_endpoint_auth_method: clientAssertionKey ? "private_key_jwt" : "client_secret_post",
|
|
173
|
+
...clientAssertionKey ? { token_endpoint_auth_signing_alg: "ES256" } : {},
|
|
74
174
|
id_token_signed_response_alg: "RS256"
|
|
75
175
|
// OAuth42 uses RS256 for ID tokens
|
|
76
176
|
},
|
|
77
|
-
issuer
|
|
78
|
-
checks:
|
|
177
|
+
issuer,
|
|
178
|
+
checks: ["pkce", "state", "nonce"],
|
|
79
179
|
profile(profile, tokens) {
|
|
80
180
|
return {
|
|
81
181
|
id: profile.sub || profile.id || profile.email,
|
|
@@ -91,45 +191,127 @@ function OAuth42Provider(options) {
|
|
|
91
191
|
bg: "#1e40af",
|
|
92
192
|
text: "#ffffff"
|
|
93
193
|
},
|
|
94
|
-
|
|
194
|
+
// NextAuth v4 types require a string even when private_key_jwt sends no secret.
|
|
195
|
+
options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
|
|
95
196
|
};
|
|
96
197
|
}
|
|
97
198
|
|
|
199
|
+
// src/server/par.ts
|
|
200
|
+
async function pushAuthorizationURL(value, config) {
|
|
201
|
+
const base = config.issuer.replace(/\/$/, "");
|
|
202
|
+
let url;
|
|
203
|
+
try {
|
|
204
|
+
url = new URL(value);
|
|
205
|
+
} catch {
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
if (`${url.origin}${url.pathname}` !== `${base}/oauth2/authorize`) return value;
|
|
209
|
+
if (url.username || url.password || url.hash) throw new Error("Invalid authorization endpoint");
|
|
210
|
+
const body = new URLSearchParams(url.search);
|
|
211
|
+
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))) {
|
|
212
|
+
throw new Error("Invalid PAR authorization transaction");
|
|
213
|
+
}
|
|
214
|
+
const target = `${base}/oauth2/par`;
|
|
215
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
216
|
+
if (!config.clientAssertionKey) body.set("client_secret", config.clientSecret || "");
|
|
217
|
+
const init = {
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
220
|
+
body,
|
|
221
|
+
redirect: "error",
|
|
222
|
+
cache: "no-store",
|
|
223
|
+
signal: AbortSignal.timeout(3e4)
|
|
224
|
+
};
|
|
225
|
+
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);
|
|
226
|
+
if (response.status !== 201) throw new Error("Pushed authorization failed");
|
|
227
|
+
const pushed = await response.json();
|
|
228
|
+
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");
|
|
229
|
+
url.search = new URLSearchParams({ client_id: config.clientId, request_uri: pushed.request_uri }).toString();
|
|
230
|
+
return url.href;
|
|
231
|
+
}
|
|
232
|
+
function withPushedAuthorization(handler, config) {
|
|
233
|
+
return async (request, context) => {
|
|
234
|
+
if (!(request instanceof Request)) throw new Error("PAR handlers require Next.js App Router Request/Response");
|
|
235
|
+
const response = await handler(request, context);
|
|
236
|
+
const location = response.headers.get("Location");
|
|
237
|
+
if (location) {
|
|
238
|
+
const next = await pushAuthorizationURL(location, config);
|
|
239
|
+
if (next === location) return response;
|
|
240
|
+
const headers = new Headers(response.headers);
|
|
241
|
+
headers.set("Location", next);
|
|
242
|
+
headers.delete("content-length");
|
|
243
|
+
return new Response(response.body, { status: response.status, headers });
|
|
244
|
+
}
|
|
245
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
246
|
+
const body = await response.clone().json();
|
|
247
|
+
if (typeof body?.url === "string") {
|
|
248
|
+
const next = await pushAuthorizationURL(body.url, config);
|
|
249
|
+
if (next !== body.url) {
|
|
250
|
+
const headers = new Headers(response.headers);
|
|
251
|
+
headers.delete("content-length");
|
|
252
|
+
return new Response(JSON.stringify({ ...body, url: next }), { status: response.status, headers });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return response;
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/server/verified-userinfo.ts
|
|
261
|
+
async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
|
|
262
|
+
if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
|
|
263
|
+
try {
|
|
264
|
+
const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
|
|
265
|
+
const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
|
|
266
|
+
const response = await fetcher(target, {
|
|
267
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
268
|
+
redirect: "error",
|
|
269
|
+
cache: "no-store",
|
|
270
|
+
signal: AbortSignal.timeout(3e4)
|
|
271
|
+
});
|
|
272
|
+
if (!response.ok) return null;
|
|
273
|
+
const user = await response.json();
|
|
274
|
+
return parseVerifiedUserInfo(user, subject);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function parseVerifiedUserInfo(value, subject) {
|
|
280
|
+
if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
281
|
+
const user = value;
|
|
282
|
+
if (user.sub !== subject) return null;
|
|
283
|
+
if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
|
|
284
|
+
if (user.email !== void 0 && typeof user.email !== "string") return null;
|
|
285
|
+
if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
|
|
286
|
+
const profile = { sub: user.sub };
|
|
287
|
+
for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
|
|
288
|
+
if (user[field] != null) {
|
|
289
|
+
if (typeof user[field] !== "string") return null;
|
|
290
|
+
profile[field] = user[field];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
|
|
294
|
+
if (user.groups !== void 0) profile.groups = user.groups;
|
|
295
|
+
return profile;
|
|
296
|
+
}
|
|
297
|
+
async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
|
|
298
|
+
if (typeof session.access_token !== "string" || !session.access_token) return null;
|
|
299
|
+
if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
|
|
300
|
+
return parseVerifiedUserInfo(session.user_info, subject);
|
|
301
|
+
}
|
|
302
|
+
return verifiedUserInfo(issuer, session.access_token, subject, dpop);
|
|
303
|
+
}
|
|
304
|
+
|
|
98
305
|
// src/server/auth.ts
|
|
99
306
|
var import_next_auth2 = __toESM(require("next-auth"));
|
|
100
307
|
|
|
101
308
|
// src/server/session.ts
|
|
102
309
|
var import_next_auth = require("next-auth");
|
|
103
|
-
var import_headers = require("next/headers");
|
|
104
|
-
var REFRESHED_TOKEN_HEADER = "x-oauth42-refreshed-token";
|
|
105
310
|
async function getOAuth42Session(...args) {
|
|
106
311
|
const session = await (0, import_next_auth.getServerSession)(...args);
|
|
107
|
-
if (!session) {
|
|
312
|
+
if (!session || session.error) {
|
|
108
313
|
return null;
|
|
109
314
|
}
|
|
110
|
-
let refreshedToken = null;
|
|
111
|
-
try {
|
|
112
|
-
if (args.length === 1) {
|
|
113
|
-
const headersList = await (0, import_headers.headers)();
|
|
114
|
-
refreshedToken = headersList.get(REFRESHED_TOKEN_HEADER);
|
|
115
|
-
} else if (args.length === 3) {
|
|
116
|
-
const req = args[0];
|
|
117
|
-
refreshedToken = req.headers[REFRESHED_TOKEN_HEADER] || null;
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
}
|
|
121
|
-
if (refreshedToken) {
|
|
122
|
-
const sessionToken = session.accessToken;
|
|
123
|
-
console.log("[OAuth42 Session] Using refreshed token from middleware:", {
|
|
124
|
-
refreshedPrefix: refreshedToken.substring(0, 20),
|
|
125
|
-
sessionPrefix: sessionToken?.substring(0, 20),
|
|
126
|
-
tokensMatch: refreshedToken === sessionToken
|
|
127
|
-
});
|
|
128
|
-
return {
|
|
129
|
-
...session,
|
|
130
|
-
accessToken: refreshedToken
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
315
|
return session;
|
|
134
316
|
}
|
|
135
317
|
function withOAuth42Session(handler, authOptions) {
|
|
@@ -167,66 +349,59 @@ function isBackendSessionMode(mode) {
|
|
|
167
349
|
function sessionEndpoint(issuer) {
|
|
168
350
|
return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
|
|
169
351
|
}
|
|
170
|
-
async function
|
|
171
|
-
|
|
172
|
-
|
|
352
|
+
async function sessionFetch(params, target, body, credential, method = "POST") {
|
|
353
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
354
|
+
const prepare = () => ({
|
|
355
|
+
method,
|
|
356
|
+
redirect: "error",
|
|
357
|
+
cache: "no-store",
|
|
358
|
+
signal: AbortSignal.timeout(3e4),
|
|
173
359
|
headers: { "Content-Type": "application/json" },
|
|
174
360
|
body: JSON.stringify({
|
|
175
|
-
|
|
176
|
-
refresh_token: params.refreshToken,
|
|
177
|
-
expires_at: params.expiresAt ?? Math.floor(Date.now() / 1e3) + 3600,
|
|
178
|
-
id_token: params.idToken,
|
|
361
|
+
...body,
|
|
179
362
|
client_id: params.clientId,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
363
|
+
...params.clientAssertionKey ? {
|
|
364
|
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
365
|
+
client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
|
|
366
|
+
} : { client_secret: params.clientSecret }
|
|
183
367
|
})
|
|
184
368
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
369
|
+
return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
|
|
370
|
+
}
|
|
371
|
+
async function sessionResponse(response, params) {
|
|
372
|
+
if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
|
|
373
|
+
const session = await response.json();
|
|
374
|
+
if (params.dpop) params.dpop.assertTokenResponse(session);
|
|
375
|
+
else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
|
|
376
|
+
return session;
|
|
377
|
+
}
|
|
378
|
+
async function createBackendSession(params) {
|
|
379
|
+
return sessionResponse(await sessionFetch(params, sessionEndpoint(params.issuer), {
|
|
380
|
+
access_token: params.accessToken,
|
|
381
|
+
refresh_token: params.refreshToken,
|
|
382
|
+
id_token: params.idToken,
|
|
383
|
+
user: params.user
|
|
384
|
+
}, params.accessToken), params);
|
|
189
385
|
}
|
|
190
386
|
async function resolveBackendSession(params) {
|
|
191
|
-
const response = await
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
body: JSON.stringify({
|
|
195
|
-
session_id: params.sessionId,
|
|
196
|
-
client_id: params.clientId,
|
|
197
|
-
client_secret: params.clientSecret,
|
|
198
|
-
issuer: params.issuer
|
|
199
|
-
})
|
|
200
|
-
});
|
|
201
|
-
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
if (!response.ok) {
|
|
205
|
-
throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
|
|
206
|
-
}
|
|
207
|
-
return response.json();
|
|
387
|
+
const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
|
|
388
|
+
if ([401, 403, 404].includes(response.status)) return null;
|
|
389
|
+
return sessionResponse(response, params);
|
|
208
390
|
}
|
|
209
391
|
async function revokeBackendSession(params) {
|
|
210
|
-
await
|
|
211
|
-
|
|
212
|
-
headers: { "Content-Type": "application/json" },
|
|
213
|
-
body: JSON.stringify({
|
|
214
|
-
session_id: params.sessionId,
|
|
215
|
-
client_id: params.clientId,
|
|
216
|
-
client_secret: params.clientSecret
|
|
217
|
-
})
|
|
218
|
-
});
|
|
392
|
+
const response = await sessionFetch(params, sessionEndpoint(params.issuer), { session_id: params.sessionId }, params.sessionId, "DELETE");
|
|
393
|
+
if (!response.ok) throw new Error(`OAuth42 backend session revoke failed: ${response.status}`);
|
|
219
394
|
}
|
|
220
395
|
|
|
221
396
|
// src/server/auth.ts
|
|
222
397
|
var NextAuth = import_next_auth2.default.default || import_next_auth2.default;
|
|
223
|
-
var
|
|
398
|
+
var activeRefreshes = /* @__PURE__ */ new Map();
|
|
224
399
|
function createAuth(options = {}) {
|
|
225
400
|
const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
|
|
226
|
-
const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
401
|
+
const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
227
402
|
const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
228
403
|
const useBackendSessions = isBackendSessionMode(options.sessionStore);
|
|
229
|
-
if (!clientId || !clientSecret) {
|
|
404
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
230
405
|
throw new Error(
|
|
231
406
|
"OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
|
|
232
407
|
);
|
|
@@ -235,14 +410,17 @@ function createAuth(options = {}) {
|
|
|
235
410
|
providers: [
|
|
236
411
|
OAuth42Provider({
|
|
237
412
|
clientId,
|
|
238
|
-
clientSecret,
|
|
413
|
+
clientSecret: clientSecret || "",
|
|
239
414
|
issuer,
|
|
240
415
|
scopes: options.scopes,
|
|
241
|
-
pkceEnabled: options.pkceEnabled
|
|
416
|
+
pkceEnabled: options.pkceEnabled,
|
|
417
|
+
dpop: options.dpop,
|
|
418
|
+
clientAssertionKey: options.clientAssertionKey
|
|
242
419
|
})
|
|
243
420
|
],
|
|
244
421
|
callbacks: {
|
|
245
422
|
async jwt({ token, account, profile }) {
|
|
423
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
246
424
|
console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
|
|
247
425
|
if (account) {
|
|
248
426
|
if (useBackendSessions) {
|
|
@@ -251,9 +429,11 @@ function createAuth(options = {}) {
|
|
|
251
429
|
}
|
|
252
430
|
const oauth42Profile = profile;
|
|
253
431
|
const backendSession = await createBackendSession({
|
|
432
|
+
dpop: options.dpop,
|
|
433
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
254
434
|
issuer,
|
|
255
435
|
clientId,
|
|
256
|
-
clientSecret,
|
|
436
|
+
clientSecret: clientSecret || "",
|
|
257
437
|
accessToken: account.access_token,
|
|
258
438
|
refreshToken: account.refresh_token,
|
|
259
439
|
expiresAt: account.expires_at,
|
|
@@ -277,7 +457,8 @@ function createAuth(options = {}) {
|
|
|
277
457
|
token.expiresAt = account.expires_at;
|
|
278
458
|
token.idToken = account.id_token;
|
|
279
459
|
token.clientId = clientId;
|
|
280
|
-
|
|
460
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
461
|
+
else token.clientSecret = clientSecret;
|
|
281
462
|
token.issuer = issuer;
|
|
282
463
|
}
|
|
283
464
|
}
|
|
@@ -296,25 +477,36 @@ function createAuth(options = {}) {
|
|
|
296
477
|
},
|
|
297
478
|
async session({ session, token }) {
|
|
298
479
|
console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
|
|
480
|
+
const revokedSession = () => {
|
|
481
|
+
session.error = "SessionRevoked";
|
|
482
|
+
delete session.accessToken;
|
|
483
|
+
delete session.idToken;
|
|
484
|
+
delete session.userInfo;
|
|
485
|
+
delete session.user;
|
|
486
|
+
return session;
|
|
487
|
+
};
|
|
488
|
+
if (token.error) return revokedSession();
|
|
489
|
+
let verified;
|
|
299
490
|
if (useBackendSessions) {
|
|
300
491
|
const sessionId = token.oauth42SessionId;
|
|
301
492
|
if (!sessionId) {
|
|
302
|
-
|
|
303
|
-
return session;
|
|
493
|
+
return revokedSession();
|
|
304
494
|
}
|
|
305
495
|
const backendSession = await resolveBackendSession({
|
|
496
|
+
dpop: options.dpop,
|
|
497
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
306
498
|
issuer,
|
|
307
499
|
clientId,
|
|
308
|
-
clientSecret,
|
|
500
|
+
clientSecret: clientSecret || "",
|
|
309
501
|
sessionId
|
|
310
502
|
});
|
|
311
503
|
if (!backendSession) {
|
|
312
|
-
|
|
313
|
-
return session;
|
|
504
|
+
return revokedSession();
|
|
314
505
|
}
|
|
315
506
|
session.accessToken = backendSession.access_token;
|
|
316
507
|
session.idToken = backendSession.id_token;
|
|
317
508
|
token.expiresAt = backendSession.expires_at;
|
|
509
|
+
verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
|
|
318
510
|
if (backendSession.user && session.user) {
|
|
319
511
|
session.user.email = backendSession.user.email;
|
|
320
512
|
session.user.name = backendSession.user.name;
|
|
@@ -325,6 +517,23 @@ function createAuth(options = {}) {
|
|
|
325
517
|
} else {
|
|
326
518
|
session.accessToken = token.accessToken;
|
|
327
519
|
session.idToken = token.idToken;
|
|
520
|
+
verified = await verifiedUserInfo(issuer, session.accessToken, token.sub, options.dpop);
|
|
521
|
+
}
|
|
522
|
+
if (!verified) {
|
|
523
|
+
return revokedSession();
|
|
524
|
+
}
|
|
525
|
+
session.userInfo = verified;
|
|
526
|
+
token.email = verified.email;
|
|
527
|
+
token.name = verified.name;
|
|
528
|
+
token.username = verified.preferred_username ?? verified.username;
|
|
529
|
+
token.emailVerified = verified.email_verified;
|
|
530
|
+
token.groups = verified.groups ?? [];
|
|
531
|
+
if (session.user) {
|
|
532
|
+
session.user.email = verified.email;
|
|
533
|
+
session.user.name = verified.name;
|
|
534
|
+
session.user.username = verified.preferred_username ?? verified.username;
|
|
535
|
+
session.user.emailVerified = verified.email_verified;
|
|
536
|
+
session.user.groups = verified.groups ?? [];
|
|
328
537
|
}
|
|
329
538
|
if (token.error) {
|
|
330
539
|
session.error = token.error;
|
|
@@ -359,9 +568,11 @@ function createAuth(options = {}) {
|
|
|
359
568
|
const sessionId = message.token?.oauth42SessionId;
|
|
360
569
|
if (sessionId) {
|
|
361
570
|
await revokeBackendSession({
|
|
571
|
+
dpop: options.dpop,
|
|
572
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
362
573
|
issuer,
|
|
363
574
|
clientId,
|
|
364
|
-
clientSecret,
|
|
575
|
+
clientSecret: clientSecret || "",
|
|
365
576
|
sessionId
|
|
366
577
|
}).catch((error) => {
|
|
367
578
|
console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
|
|
@@ -448,31 +659,48 @@ function createAuth(options = {}) {
|
|
|
448
659
|
}
|
|
449
660
|
}
|
|
450
661
|
};
|
|
451
|
-
const
|
|
662
|
+
const nextAuthHandler = NextAuth(authOptions);
|
|
663
|
+
const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
|
|
452
664
|
return {
|
|
453
665
|
auth: authOptions,
|
|
454
666
|
handlers: { GET: handler, POST: handler }
|
|
455
667
|
};
|
|
456
668
|
}
|
|
457
669
|
var getServerSession = getOAuth42Session;
|
|
458
|
-
async function refreshAccessToken(token, clientId, clientSecret, issuer) {
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
670
|
+
async function refreshAccessToken(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
671
|
+
if (typeof token.refreshToken !== "string" || !token.refreshToken) {
|
|
672
|
+
return { ...token, error: "RefreshAccessTokenError" };
|
|
673
|
+
}
|
|
674
|
+
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]);
|
|
675
|
+
let refresh = activeRefreshes.get(key);
|
|
676
|
+
if (!refresh) {
|
|
677
|
+
refresh = doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey);
|
|
678
|
+
activeRefreshes.set(key, refresh);
|
|
462
679
|
}
|
|
463
|
-
activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
|
|
464
680
|
try {
|
|
465
|
-
|
|
681
|
+
const result = await refresh;
|
|
682
|
+
const updated = {
|
|
683
|
+
...token,
|
|
684
|
+
accessToken: result.accessToken,
|
|
685
|
+
refreshToken: result.refreshToken,
|
|
686
|
+
expiresAt: result.expiresAt,
|
|
687
|
+
error: result.error
|
|
688
|
+
};
|
|
689
|
+
if (clientAssertionKey) delete updated.clientSecret;
|
|
690
|
+
return updated;
|
|
466
691
|
} finally {
|
|
467
|
-
|
|
692
|
+
if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
|
|
468
693
|
}
|
|
469
694
|
}
|
|
470
|
-
async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
695
|
+
async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
471
696
|
try {
|
|
472
697
|
const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
473
698
|
const tokenUrl = `${baseUrl}/oauth2/token`;
|
|
474
699
|
const fetchOptions = {
|
|
475
700
|
method: "POST",
|
|
701
|
+
redirect: "error",
|
|
702
|
+
cache: "no-store",
|
|
703
|
+
signal: AbortSignal.timeout(3e4),
|
|
476
704
|
headers: {
|
|
477
705
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
478
706
|
},
|
|
@@ -483,17 +711,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
483
711
|
client_secret: clientSecret
|
|
484
712
|
})
|
|
485
713
|
};
|
|
486
|
-
|
|
487
|
-
const https = await import("https");
|
|
488
|
-
fetchOptions.agent = new https.Agent({
|
|
489
|
-
rejectUnauthorized: false
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
const response = await fetch(tokenUrl, fetchOptions);
|
|
714
|
+
const response = clientAssertionKey ? await clientAssertionKey.fetch(clientId, tokenUrl, fetchOptions, dpop) : dpop ? await dpop.fetch(tokenUrl, fetchOptions) : await fetch(tokenUrl, fetchOptions);
|
|
493
715
|
const refreshedTokens = await response.json();
|
|
494
716
|
if (!response.ok) {
|
|
495
717
|
throw refreshedTokens;
|
|
496
718
|
}
|
|
719
|
+
dpop?.assertTokenResponse(refreshedTokens);
|
|
497
720
|
console.log("[OAuth42] Token refreshed successfully");
|
|
498
721
|
return {
|
|
499
722
|
...token,
|
|
@@ -505,7 +728,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
505
728
|
error: void 0
|
|
506
729
|
};
|
|
507
730
|
} catch (error) {
|
|
508
|
-
console.error("[OAuth42] Failed to refresh access token
|
|
731
|
+
console.error("[OAuth42] Failed to refresh access token");
|
|
509
732
|
return {
|
|
510
733
|
...token,
|
|
511
734
|
error: "RefreshAccessTokenError"
|
|
@@ -519,14 +742,20 @@ var import_jwt = require("next-auth/jwt");
|
|
|
519
742
|
var ALLOWED_COOKIE_SIZE = 4096;
|
|
520
743
|
var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
|
|
521
744
|
var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
522
|
-
var REFRESH_RACE_GRACE_SECONDS = 60;
|
|
523
745
|
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
524
746
|
var pendingRefreshes = /* @__PURE__ */ new Map();
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
747
|
+
function forwardedHeaders(req, cookieName, jwt) {
|
|
748
|
+
const headers = new Headers(req.headers);
|
|
749
|
+
headers.delete("x-oauth42-refreshed-token");
|
|
750
|
+
if (cookieName && jwt) {
|
|
751
|
+
const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
|
|
752
|
+
if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
|
|
753
|
+
else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
|
|
754
|
+
cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
|
|
755
|
+
}
|
|
756
|
+
headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
|
|
528
757
|
}
|
|
529
|
-
return
|
|
758
|
+
return headers;
|
|
530
759
|
}
|
|
531
760
|
function resolveRefreshConfig(options, token) {
|
|
532
761
|
return {
|
|
@@ -535,11 +764,15 @@ function resolveRefreshConfig(options, token) {
|
|
|
535
764
|
issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
|
|
536
765
|
};
|
|
537
766
|
}
|
|
538
|
-
async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
767
|
+
async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
539
768
|
try {
|
|
540
769
|
const tokenUrl = `${issuer}/oauth2/token`;
|
|
541
|
-
const
|
|
770
|
+
const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
|
|
771
|
+
const response = await fetcher(tokenUrl, {
|
|
542
772
|
method: "POST",
|
|
773
|
+
redirect: "error",
|
|
774
|
+
cache: "no-store",
|
|
775
|
+
signal: AbortSignal.timeout(3e4),
|
|
543
776
|
headers: {
|
|
544
777
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
545
778
|
},
|
|
@@ -552,9 +785,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
552
785
|
});
|
|
553
786
|
const data = await response.json();
|
|
554
787
|
if (!response.ok) {
|
|
555
|
-
console.error("[OAuth42 Middleware] Token refresh failed:",
|
|
788
|
+
console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
|
|
556
789
|
return { success: false, error: data.error || "refresh_failed" };
|
|
557
790
|
}
|
|
791
|
+
dpop?.assertTokenResponse(data);
|
|
558
792
|
console.log("[OAuth42 Middleware] Token refreshed successfully");
|
|
559
793
|
return {
|
|
560
794
|
success: true,
|
|
@@ -563,14 +797,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
563
797
|
expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
|
|
564
798
|
};
|
|
565
799
|
} catch (error) {
|
|
566
|
-
console.error("[OAuth42 Middleware] Token refresh
|
|
800
|
+
console.error("[OAuth42 Middleware] Token refresh request failed");
|
|
567
801
|
return { success: false, error: "refresh_error" };
|
|
568
802
|
}
|
|
569
803
|
}
|
|
570
804
|
function withOAuth42Auth(options = {}) {
|
|
571
805
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
572
806
|
if (!secret) {
|
|
573
|
-
|
|
807
|
+
throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
|
|
574
808
|
}
|
|
575
809
|
return async function middleware(req) {
|
|
576
810
|
const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
|
|
@@ -581,22 +815,41 @@ function withOAuth42Auth(options = {}) {
|
|
|
581
815
|
});
|
|
582
816
|
const pathname = req.nextUrl.pathname;
|
|
583
817
|
if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
|
|
584
|
-
return import_server.NextResponse.next();
|
|
818
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
585
819
|
}
|
|
586
820
|
const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
|
|
587
821
|
if (!needsProtection) {
|
|
588
|
-
return import_server.NextResponse.next();
|
|
822
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
589
823
|
}
|
|
590
|
-
if (!token) {
|
|
824
|
+
if (!token || token.error) {
|
|
591
825
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
592
826
|
const url = new URL(signInUrl, req.url);
|
|
593
827
|
url.searchParams.set("callbackUrl", pathname);
|
|
594
828
|
return import_server.NextResponse.redirect(url);
|
|
595
829
|
}
|
|
596
830
|
const refreshConfig = resolveRefreshConfig(options, token);
|
|
831
|
+
const denied = () => {
|
|
832
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
833
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
834
|
+
const response = import_server.NextResponse.redirect(url);
|
|
835
|
+
for (const cookie of req.cookies.getAll()) {
|
|
836
|
+
if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
|
|
837
|
+
}
|
|
838
|
+
return response;
|
|
839
|
+
};
|
|
840
|
+
const verifyAndAuthorize = async (current, resolved) => {
|
|
841
|
+
const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
|
|
842
|
+
if (!user) return false;
|
|
843
|
+
current.groups = user.groups ?? [];
|
|
844
|
+
current.email = user.email;
|
|
845
|
+
current.name = user.name;
|
|
846
|
+
current.username = user.preferred_username ?? user.username;
|
|
847
|
+
current.emailVerified = user.email_verified;
|
|
848
|
+
return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
|
|
849
|
+
};
|
|
597
850
|
if (isBackendSessionMode(options.sessionStore)) {
|
|
598
851
|
const sessionId = token.oauth42SessionId;
|
|
599
|
-
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
|
|
852
|
+
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
|
|
600
853
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
601
854
|
const url = new URL(signInUrl, req.url);
|
|
602
855
|
url.searchParams.set("callbackUrl", pathname);
|
|
@@ -605,9 +858,11 @@ function withOAuth42Auth(options = {}) {
|
|
|
605
858
|
return response;
|
|
606
859
|
}
|
|
607
860
|
const backendSession = await resolveBackendSession({
|
|
861
|
+
dpop: options.dpop,
|
|
862
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
608
863
|
issuer: refreshConfig.issuer,
|
|
609
864
|
clientId: refreshConfig.clientId,
|
|
610
|
-
clientSecret: refreshConfig.clientSecret,
|
|
865
|
+
clientSecret: refreshConfig.clientSecret || "",
|
|
611
866
|
sessionId
|
|
612
867
|
}).catch((error) => {
|
|
613
868
|
console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
|
|
@@ -621,8 +876,9 @@ function withOAuth42Auth(options = {}) {
|
|
|
621
876
|
response.cookies.delete(cookieName);
|
|
622
877
|
return response;
|
|
623
878
|
}
|
|
624
|
-
const
|
|
625
|
-
|
|
879
|
+
const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
|
|
880
|
+
if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
|
|
881
|
+
const requestHeaders = forwardedHeaders(req);
|
|
626
882
|
return import_server.NextResponse.next({
|
|
627
883
|
request: {
|
|
628
884
|
headers: requestHeaders
|
|
@@ -638,13 +894,12 @@ function withOAuth42Auth(options = {}) {
|
|
|
638
894
|
expiresAt,
|
|
639
895
|
now,
|
|
640
896
|
expiredAgo: expiresAt ? now - expiresAt : "n/a",
|
|
641
|
-
accessTokenPrefix: token.accessToken?.substring(0, 20),
|
|
642
897
|
path: pathname
|
|
643
898
|
});
|
|
644
899
|
}
|
|
645
|
-
if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
|
|
646
|
-
const
|
|
647
|
-
let refreshPromise = pendingRefreshes.get(
|
|
900
|
+
if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
|
|
901
|
+
const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
|
|
902
|
+
let refreshPromise = pendingRefreshes.get(grantKey);
|
|
648
903
|
if (refreshPromise) {
|
|
649
904
|
console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
|
|
650
905
|
} else {
|
|
@@ -652,15 +907,17 @@ function withOAuth42Auth(options = {}) {
|
|
|
652
907
|
refreshPromise = refreshTokens(
|
|
653
908
|
token.refreshToken,
|
|
654
909
|
refreshConfig.clientId,
|
|
655
|
-
refreshConfig.clientSecret,
|
|
656
|
-
refreshConfig.issuer
|
|
910
|
+
refreshConfig.clientSecret || "",
|
|
911
|
+
refreshConfig.issuer,
|
|
912
|
+
options.dpop,
|
|
913
|
+
options.clientAssertionKey
|
|
657
914
|
).finally(() => {
|
|
658
|
-
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(
|
|
915
|
+
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
|
|
659
916
|
if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
|
|
660
917
|
cleanupTimer.unref();
|
|
661
918
|
}
|
|
662
919
|
});
|
|
663
|
-
pendingRefreshes.set(
|
|
920
|
+
pendingRefreshes.set(grantKey, refreshPromise);
|
|
664
921
|
}
|
|
665
922
|
const refreshed = await refreshPromise;
|
|
666
923
|
if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
|
|
@@ -670,12 +927,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
670
927
|
refreshToken: refreshed.refreshToken,
|
|
671
928
|
expiresAt: refreshed.expiresAt
|
|
672
929
|
};
|
|
930
|
+
if (options.clientAssertionKey) delete updatedToken.clientSecret;
|
|
931
|
+
if (!await verifyAndAuthorize(updatedToken)) return denied();
|
|
673
932
|
const newJwt = await (0, import_jwt.encode)({
|
|
674
933
|
token: updatedToken,
|
|
675
934
|
secret
|
|
676
935
|
});
|
|
677
|
-
const requestHeaders =
|
|
678
|
-
requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
|
|
936
|
+
const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
|
|
679
937
|
const response = import_server.NextResponse.next({
|
|
680
938
|
request: {
|
|
681
939
|
headers: requestHeaders
|
|
@@ -728,28 +986,11 @@ function withOAuth42Auth(options = {}) {
|
|
|
728
986
|
chunked: newJwt.length > CHUNK_SIZE,
|
|
729
987
|
existingBaseCooke: req.cookies.has(cookieName),
|
|
730
988
|
existingChunks: existingChunks.length > 0 ? existingChunks : "none",
|
|
731
|
-
newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
|
|
732
989
|
newExpiresAt: refreshed.expiresAt,
|
|
733
990
|
cookieName
|
|
734
991
|
});
|
|
735
992
|
return response;
|
|
736
993
|
} else {
|
|
737
|
-
if (shouldPreserveSessionOnRefreshFailure({
|
|
738
|
-
error: refreshed.error,
|
|
739
|
-
expiresAt,
|
|
740
|
-
now
|
|
741
|
-
})) {
|
|
742
|
-
console.warn(
|
|
743
|
-
"[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
|
|
744
|
-
{
|
|
745
|
-
expiresAt,
|
|
746
|
-
now,
|
|
747
|
-
expiredAgo: now - expiresAt,
|
|
748
|
-
path: pathname
|
|
749
|
-
}
|
|
750
|
-
);
|
|
751
|
-
return import_server.NextResponse.next();
|
|
752
|
-
}
|
|
753
994
|
console.log("[OAuth42 Middleware] Refresh failed, clearing session");
|
|
754
995
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
755
996
|
const url = new URL(signInUrl, req.url);
|
|
@@ -767,16 +1008,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
767
1008
|
return response;
|
|
768
1009
|
}
|
|
769
1010
|
}
|
|
770
|
-
if (
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const url = new URL(signInUrl, req.url);
|
|
775
|
-
url.searchParams.set("callbackUrl", pathname);
|
|
776
|
-
return import_server.NextResponse.redirect(url);
|
|
777
|
-
}
|
|
1011
|
+
if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
|
|
1012
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
1013
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
1014
|
+
return import_server.NextResponse.redirect(url);
|
|
778
1015
|
}
|
|
779
|
-
return
|
|
1016
|
+
if (!await verifyAndAuthorize(token)) return denied();
|
|
1017
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
780
1018
|
};
|
|
781
1019
|
}
|
|
782
1020
|
function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
|
|
@@ -795,16 +1033,88 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
|
|
|
795
1033
|
publicPaths
|
|
796
1034
|
};
|
|
797
1035
|
}
|
|
1036
|
+
|
|
1037
|
+
// src/server/dpop.ts
|
|
1038
|
+
var import_node_crypto2 = require("crypto");
|
|
1039
|
+
var OAuth42DPoP = class _OAuth42DPoP {
|
|
1040
|
+
constructor(key) {
|
|
1041
|
+
this.nonces = /* @__PURE__ */ new Map();
|
|
1042
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
1043
|
+
throw new Error("DPoP requires a private P-256 key");
|
|
1044
|
+
}
|
|
1045
|
+
this.key = key;
|
|
1046
|
+
const publicKey = (0, import_node_crypto2.createPublicKey)(key).export({ format: "jwk" });
|
|
1047
|
+
this.jwk = { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y };
|
|
1048
|
+
this.thumbprint = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(this.jwk)).digest("base64url");
|
|
1049
|
+
}
|
|
1050
|
+
/** Persist keys in protected server storage; regeneration cannot recover a public refresh grant. */
|
|
1051
|
+
static generate() {
|
|
1052
|
+
return new _OAuth42DPoP((0, import_node_crypto2.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
1053
|
+
}
|
|
1054
|
+
/** Passed directly to openid-client so its callback keeps OIDC issuer/nonce/signature validation. */
|
|
1055
|
+
get oidcKey() {
|
|
1056
|
+
return this.key;
|
|
1057
|
+
}
|
|
1058
|
+
proof(method, target, nonce, accessToken) {
|
|
1059
|
+
const url = new URL(target);
|
|
1060
|
+
if (!["http:", "https:"].includes(url.protocol) || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
1061
|
+
throw new Error("DPoP requires HTTPS outside loopback development");
|
|
1062
|
+
}
|
|
1063
|
+
if (url.username || url.password) throw new Error("DPoP endpoint must not contain credentials");
|
|
1064
|
+
url.search = "";
|
|
1065
|
+
url.hash = "";
|
|
1066
|
+
const header = { typ: "dpop+jwt", alg: "ES256", jwk: this.jwk };
|
|
1067
|
+
const claims = {
|
|
1068
|
+
htm: method.toUpperCase(),
|
|
1069
|
+
htu: url.href,
|
|
1070
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
1071
|
+
jti: (0, import_node_crypto2.randomBytes)(32).toString("base64url"),
|
|
1072
|
+
...nonce ? { nonce } : {},
|
|
1073
|
+
...accessToken !== void 0 ? { ath: (0, import_node_crypto2.createHash)("sha256").update(accessToken).digest("base64url") } : {}
|
|
1074
|
+
};
|
|
1075
|
+
const input = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
|
|
1076
|
+
return `${input}.${(0, import_node_crypto2.sign)("sha256", Buffer.from(input), { key: this.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
1077
|
+
}
|
|
1078
|
+
assertTokenResponse(tokens) {
|
|
1079
|
+
if (typeof tokens.token_type !== "string" || tokens.token_type.toLowerCase() !== "dpop") {
|
|
1080
|
+
throw new Error("Server did not issue the requested DPoP-bound token");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async fetch(target, init = {}, accessToken, prepare) {
|
|
1084
|
+
if (init.body instanceof ReadableStream) throw new Error("DPoP nonce retries require a replayable request body");
|
|
1085
|
+
const cacheKey = `${new URL(target).origin}:${accessToken !== void 0 ? "resource" : "token"}`;
|
|
1086
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1087
|
+
const request = prepare ? prepare() : init;
|
|
1088
|
+
const headers = new Headers(request.headers);
|
|
1089
|
+
headers.set("DPoP", this.proof(request.method || "GET", target, this.nonces.get(cacheKey), accessToken));
|
|
1090
|
+
if (accessToken !== void 0) headers.set("Authorization", `DPoP ${accessToken}`);
|
|
1091
|
+
const response = await fetch(target, { ...request, headers, redirect: "error", cache: "no-store", signal: request.signal || AbortSignal.timeout(3e4) });
|
|
1092
|
+
const nonce = response.headers.get("DPoP-Nonce");
|
|
1093
|
+
if (nonce && /^[\x21-\x7e]{1,128}$/.test(nonce)) {
|
|
1094
|
+
if (this.nonces.size >= 256) this.nonces.clear();
|
|
1095
|
+
this.nonces.set(cacheKey, nonce);
|
|
1096
|
+
} else return response;
|
|
1097
|
+
if (attempt || ![400, 401].includes(response.status)) return response;
|
|
1098
|
+
const error = await response.clone().json().catch(() => null);
|
|
1099
|
+
if (error?.error !== "use_dpop_nonce") return response;
|
|
1100
|
+
}
|
|
1101
|
+
throw new Error("DPoP nonce negotiation failed");
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
798
1104
|
// Annotate the CommonJS export names for ESM import in node:
|
|
799
1105
|
0 && (module.exports = {
|
|
1106
|
+
OAuth42ClientAssertion,
|
|
1107
|
+
OAuth42DPoP,
|
|
800
1108
|
OAuth42Provider,
|
|
801
1109
|
createAuth,
|
|
802
1110
|
createMiddlewareConfig,
|
|
803
1111
|
getOAuth42Session,
|
|
804
1112
|
getServerSession,
|
|
1113
|
+
pushAuthorizationURL,
|
|
805
1114
|
refreshAccessToken,
|
|
806
1115
|
withOAuth42Auth,
|
|
807
1116
|
withOAuth42ServerSideProps,
|
|
808
|
-
withOAuth42Session
|
|
1117
|
+
withOAuth42Session,
|
|
1118
|
+
withPushedAuthorization
|
|
809
1119
|
});
|
|
810
1120
|
//# sourceMappingURL=index.js.map
|