@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.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) {
|
|
@@ -160,13 +342,66 @@ function withOAuth42ServerSideProps(getServerSideProps, authOptions) {
|
|
|
160
342
|
};
|
|
161
343
|
}
|
|
162
344
|
|
|
345
|
+
// src/server/backend-session.ts
|
|
346
|
+
function isBackendSessionMode(mode) {
|
|
347
|
+
return mode === "backend";
|
|
348
|
+
}
|
|
349
|
+
function sessionEndpoint(issuer) {
|
|
350
|
+
return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
|
|
351
|
+
}
|
|
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),
|
|
359
|
+
headers: { "Content-Type": "application/json" },
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
...body,
|
|
362
|
+
client_id: params.clientId,
|
|
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 }
|
|
367
|
+
})
|
|
368
|
+
});
|
|
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);
|
|
385
|
+
}
|
|
386
|
+
async function resolveBackendSession(params) {
|
|
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);
|
|
390
|
+
}
|
|
391
|
+
async function revokeBackendSession(params) {
|
|
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}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
163
396
|
// src/server/auth.ts
|
|
164
397
|
var NextAuth = import_next_auth2.default.default || import_next_auth2.default;
|
|
165
|
-
var
|
|
398
|
+
var activeRefreshes = /* @__PURE__ */ new Map();
|
|
166
399
|
function createAuth(options = {}) {
|
|
167
400
|
const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
|
|
168
|
-
const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
169
|
-
|
|
401
|
+
const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
402
|
+
const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
403
|
+
const useBackendSessions = isBackendSessionMode(options.sessionStore);
|
|
404
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
170
405
|
throw new Error(
|
|
171
406
|
"OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
|
|
172
407
|
);
|
|
@@ -175,24 +410,57 @@ function createAuth(options = {}) {
|
|
|
175
410
|
providers: [
|
|
176
411
|
OAuth42Provider({
|
|
177
412
|
clientId,
|
|
178
|
-
clientSecret,
|
|
179
|
-
issuer
|
|
413
|
+
clientSecret: clientSecret || "",
|
|
414
|
+
issuer,
|
|
180
415
|
scopes: options.scopes,
|
|
181
|
-
pkceEnabled: options.pkceEnabled
|
|
416
|
+
pkceEnabled: options.pkceEnabled,
|
|
417
|
+
dpop: options.dpop,
|
|
418
|
+
clientAssertionKey: options.clientAssertionKey
|
|
182
419
|
})
|
|
183
420
|
],
|
|
184
421
|
callbacks: {
|
|
185
422
|
async jwt({ token, account, profile }) {
|
|
423
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
186
424
|
console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
|
|
187
425
|
if (account) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
426
|
+
if (useBackendSessions) {
|
|
427
|
+
if (!account.access_token || !account.refresh_token) {
|
|
428
|
+
throw new Error("OAuth42 backend sessions require access and refresh tokens");
|
|
429
|
+
}
|
|
430
|
+
const oauth42Profile = profile;
|
|
431
|
+
const backendSession = await createBackendSession({
|
|
432
|
+
dpop: options.dpop,
|
|
433
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
434
|
+
issuer,
|
|
435
|
+
clientId,
|
|
436
|
+
clientSecret: clientSecret || "",
|
|
437
|
+
accessToken: account.access_token,
|
|
438
|
+
refreshToken: account.refresh_token,
|
|
439
|
+
expiresAt: account.expires_at,
|
|
440
|
+
idToken: account.id_token,
|
|
441
|
+
user: {
|
|
442
|
+
sub: oauth42Profile?.sub,
|
|
443
|
+
email: oauth42Profile?.email,
|
|
444
|
+
username: oauth42Profile?.username,
|
|
445
|
+
name: token.name,
|
|
446
|
+
emailVerified: oauth42Profile?.email_verified,
|
|
447
|
+
groups: oauth42Profile?.groups
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
token.oauth42SessionId = backendSession.session_id;
|
|
451
|
+
token.expiresAt = backendSession.expires_at;
|
|
452
|
+
token.issuer = issuer;
|
|
453
|
+
} else {
|
|
454
|
+
console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
|
|
455
|
+
token.accessToken = account.access_token;
|
|
456
|
+
token.refreshToken = account.refresh_token;
|
|
457
|
+
token.expiresAt = account.expires_at;
|
|
458
|
+
token.idToken = account.id_token;
|
|
459
|
+
token.clientId = clientId;
|
|
460
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
461
|
+
else token.clientSecret = clientSecret;
|
|
462
|
+
token.issuer = issuer;
|
|
463
|
+
}
|
|
196
464
|
}
|
|
197
465
|
if (profile) {
|
|
198
466
|
const oauth42Profile = profile;
|
|
@@ -209,12 +477,68 @@ function createAuth(options = {}) {
|
|
|
209
477
|
},
|
|
210
478
|
async session({ session, token }) {
|
|
211
479
|
console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
490
|
+
if (useBackendSessions) {
|
|
491
|
+
const sessionId = token.oauth42SessionId;
|
|
492
|
+
if (!sessionId) {
|
|
493
|
+
return revokedSession();
|
|
494
|
+
}
|
|
495
|
+
const backendSession = await resolveBackendSession({
|
|
496
|
+
dpop: options.dpop,
|
|
497
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
498
|
+
issuer,
|
|
499
|
+
clientId,
|
|
500
|
+
clientSecret: clientSecret || "",
|
|
501
|
+
sessionId
|
|
502
|
+
});
|
|
503
|
+
if (!backendSession) {
|
|
504
|
+
return revokedSession();
|
|
505
|
+
}
|
|
506
|
+
session.accessToken = backendSession.access_token;
|
|
507
|
+
session.idToken = backendSession.id_token;
|
|
508
|
+
token.expiresAt = backendSession.expires_at;
|
|
509
|
+
verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
|
|
510
|
+
if (backendSession.user && session.user) {
|
|
511
|
+
session.user.email = backendSession.user.email;
|
|
512
|
+
session.user.name = backendSession.user.name;
|
|
513
|
+
session.user.username = backendSession.user.username;
|
|
514
|
+
session.user.emailVerified = backendSession.user.emailVerified;
|
|
515
|
+
session.user.groups = backendSession.user.groups;
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
session.accessToken = token.accessToken;
|
|
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 ?? [];
|
|
537
|
+
}
|
|
214
538
|
if (token.error) {
|
|
215
539
|
session.error = token.error;
|
|
216
540
|
}
|
|
217
|
-
if (session.user) {
|
|
541
|
+
if (!useBackendSessions && session.user) {
|
|
218
542
|
session.user.email = token.email;
|
|
219
543
|
session.user.name = token.name;
|
|
220
544
|
session.user.username = token.username;
|
|
@@ -238,6 +562,28 @@ function createAuth(options = {}) {
|
|
|
238
562
|
strategy: "jwt",
|
|
239
563
|
...options.session
|
|
240
564
|
},
|
|
565
|
+
events: {
|
|
566
|
+
async signOut(message) {
|
|
567
|
+
if (useBackendSessions) {
|
|
568
|
+
const sessionId = message.token?.oauth42SessionId;
|
|
569
|
+
if (sessionId) {
|
|
570
|
+
await revokeBackendSession({
|
|
571
|
+
dpop: options.dpop,
|
|
572
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
573
|
+
issuer,
|
|
574
|
+
clientId,
|
|
575
|
+
clientSecret: clientSecret || "",
|
|
576
|
+
sessionId
|
|
577
|
+
}).catch((error) => {
|
|
578
|
+
console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (options.events?.signOut) {
|
|
583
|
+
await options.events.signOut(message);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
},
|
|
241
587
|
debug: options.debug || process.env.NODE_ENV === "development",
|
|
242
588
|
secret: process.env.NEXTAUTH_SECRET,
|
|
243
589
|
// Configure unique cookie names per app to prevent session conflicts on localhost
|
|
@@ -313,31 +659,48 @@ function createAuth(options = {}) {
|
|
|
313
659
|
}
|
|
314
660
|
}
|
|
315
661
|
};
|
|
316
|
-
const
|
|
662
|
+
const nextAuthHandler = NextAuth(authOptions);
|
|
663
|
+
const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
|
|
317
664
|
return {
|
|
318
665
|
auth: authOptions,
|
|
319
666
|
handlers: { GET: handler, POST: handler }
|
|
320
667
|
};
|
|
321
668
|
}
|
|
322
669
|
var getServerSession = getOAuth42Session;
|
|
323
|
-
async function refreshAccessToken(token, clientId, clientSecret, issuer) {
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
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);
|
|
327
679
|
}
|
|
328
|
-
activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
|
|
329
680
|
try {
|
|
330
|
-
|
|
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;
|
|
331
691
|
} finally {
|
|
332
|
-
|
|
692
|
+
if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
|
|
333
693
|
}
|
|
334
694
|
}
|
|
335
|
-
async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
695
|
+
async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
336
696
|
try {
|
|
337
697
|
const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
338
698
|
const tokenUrl = `${baseUrl}/oauth2/token`;
|
|
339
699
|
const fetchOptions = {
|
|
340
700
|
method: "POST",
|
|
701
|
+
redirect: "error",
|
|
702
|
+
cache: "no-store",
|
|
703
|
+
signal: AbortSignal.timeout(3e4),
|
|
341
704
|
headers: {
|
|
342
705
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
343
706
|
},
|
|
@@ -348,17 +711,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
348
711
|
client_secret: clientSecret
|
|
349
712
|
})
|
|
350
713
|
};
|
|
351
|
-
|
|
352
|
-
const https = await import("https");
|
|
353
|
-
fetchOptions.agent = new https.Agent({
|
|
354
|
-
rejectUnauthorized: false
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
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);
|
|
358
715
|
const refreshedTokens = await response.json();
|
|
359
716
|
if (!response.ok) {
|
|
360
717
|
throw refreshedTokens;
|
|
361
718
|
}
|
|
719
|
+
dpop?.assertTokenResponse(refreshedTokens);
|
|
362
720
|
console.log("[OAuth42] Token refreshed successfully");
|
|
363
721
|
return {
|
|
364
722
|
...token,
|
|
@@ -370,7 +728,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
370
728
|
error: void 0
|
|
371
729
|
};
|
|
372
730
|
} catch (error) {
|
|
373
|
-
console.error("[OAuth42] Failed to refresh access token
|
|
731
|
+
console.error("[OAuth42] Failed to refresh access token");
|
|
374
732
|
return {
|
|
375
733
|
...token,
|
|
376
734
|
error: "RefreshAccessTokenError"
|
|
@@ -384,12 +742,37 @@ var import_jwt = require("next-auth/jwt");
|
|
|
384
742
|
var ALLOWED_COOKIE_SIZE = 4096;
|
|
385
743
|
var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
|
|
386
744
|
var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
745
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
387
746
|
var pendingRefreshes = /* @__PURE__ */ new Map();
|
|
388
|
-
|
|
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("; "));
|
|
757
|
+
}
|
|
758
|
+
return headers;
|
|
759
|
+
}
|
|
760
|
+
function resolveRefreshConfig(options, token) {
|
|
761
|
+
return {
|
|
762
|
+
clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
|
|
763
|
+
clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
|
|
764
|
+
issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
389
768
|
try {
|
|
390
769
|
const tokenUrl = `${issuer}/oauth2/token`;
|
|
391
|
-
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, {
|
|
392
772
|
method: "POST",
|
|
773
|
+
redirect: "error",
|
|
774
|
+
cache: "no-store",
|
|
775
|
+
signal: AbortSignal.timeout(3e4),
|
|
393
776
|
headers: {
|
|
394
777
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
395
778
|
},
|
|
@@ -402,9 +785,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
402
785
|
});
|
|
403
786
|
const data = await response.json();
|
|
404
787
|
if (!response.ok) {
|
|
405
|
-
console.error("[OAuth42 Middleware] Token refresh failed:",
|
|
788
|
+
console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
|
|
406
789
|
return { success: false, error: data.error || "refresh_failed" };
|
|
407
790
|
}
|
|
791
|
+
dpop?.assertTokenResponse(data);
|
|
408
792
|
console.log("[OAuth42 Middleware] Token refreshed successfully");
|
|
409
793
|
return {
|
|
410
794
|
success: true,
|
|
@@ -413,17 +797,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
413
797
|
expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
|
|
414
798
|
};
|
|
415
799
|
} catch (error) {
|
|
416
|
-
console.error("[OAuth42 Middleware] Token refresh
|
|
800
|
+
console.error("[OAuth42 Middleware] Token refresh request failed");
|
|
417
801
|
return { success: false, error: "refresh_error" };
|
|
418
802
|
}
|
|
419
803
|
}
|
|
420
804
|
function withOAuth42Auth(options = {}) {
|
|
421
805
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
422
|
-
const clientId = process.env.OAUTH42_CLIENT_ID;
|
|
423
|
-
const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
|
|
424
|
-
const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
425
806
|
if (!secret) {
|
|
426
|
-
|
|
807
|
+
throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
|
|
427
808
|
}
|
|
428
809
|
return async function middleware(req) {
|
|
429
810
|
const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
|
|
@@ -434,18 +815,76 @@ function withOAuth42Auth(options = {}) {
|
|
|
434
815
|
});
|
|
435
816
|
const pathname = req.nextUrl.pathname;
|
|
436
817
|
if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
|
|
437
|
-
return import_server.NextResponse.next();
|
|
818
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
438
819
|
}
|
|
439
820
|
const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
|
|
440
821
|
if (!needsProtection) {
|
|
441
|
-
return import_server.NextResponse.next();
|
|
822
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
442
823
|
}
|
|
443
|
-
if (!token) {
|
|
824
|
+
if (!token || token.error) {
|
|
444
825
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
445
826
|
const url = new URL(signInUrl, req.url);
|
|
446
827
|
url.searchParams.set("callbackUrl", pathname);
|
|
447
828
|
return import_server.NextResponse.redirect(url);
|
|
448
829
|
}
|
|
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
|
+
};
|
|
850
|
+
if (isBackendSessionMode(options.sessionStore)) {
|
|
851
|
+
const sessionId = token.oauth42SessionId;
|
|
852
|
+
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
|
|
853
|
+
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
854
|
+
const url = new URL(signInUrl, req.url);
|
|
855
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
856
|
+
const response = import_server.NextResponse.redirect(url);
|
|
857
|
+
response.cookies.delete(cookieName);
|
|
858
|
+
return response;
|
|
859
|
+
}
|
|
860
|
+
const backendSession = await resolveBackendSession({
|
|
861
|
+
dpop: options.dpop,
|
|
862
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
863
|
+
issuer: refreshConfig.issuer,
|
|
864
|
+
clientId: refreshConfig.clientId,
|
|
865
|
+
clientSecret: refreshConfig.clientSecret || "",
|
|
866
|
+
sessionId
|
|
867
|
+
}).catch((error) => {
|
|
868
|
+
console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
|
|
869
|
+
return null;
|
|
870
|
+
});
|
|
871
|
+
if (!backendSession) {
|
|
872
|
+
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
873
|
+
const url = new URL(signInUrl, req.url);
|
|
874
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
875
|
+
const response = import_server.NextResponse.redirect(url);
|
|
876
|
+
response.cookies.delete(cookieName);
|
|
877
|
+
return response;
|
|
878
|
+
}
|
|
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);
|
|
882
|
+
return import_server.NextResponse.next({
|
|
883
|
+
request: {
|
|
884
|
+
headers: requestHeaders
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
449
888
|
const expiresAt = token.expiresAt;
|
|
450
889
|
const now = Math.floor(Date.now() / 1e3);
|
|
451
890
|
const bufferSeconds = 15;
|
|
@@ -455,26 +894,30 @@ function withOAuth42Auth(options = {}) {
|
|
|
455
894
|
expiresAt,
|
|
456
895
|
now,
|
|
457
896
|
expiredAgo: expiresAt ? now - expiresAt : "n/a",
|
|
458
|
-
accessTokenPrefix: token.accessToken?.substring(0, 20),
|
|
459
897
|
path: pathname
|
|
460
898
|
});
|
|
461
899
|
}
|
|
462
|
-
if (needsRefresh && token.refreshToken && clientId && clientSecret) {
|
|
463
|
-
const
|
|
464
|
-
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);
|
|
465
903
|
if (refreshPromise) {
|
|
466
904
|
console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
|
|
467
905
|
} else {
|
|
468
906
|
console.log("[OAuth42 Middleware] Access token expired, refreshing...");
|
|
469
907
|
refreshPromise = refreshTokens(
|
|
470
908
|
token.refreshToken,
|
|
471
|
-
clientId,
|
|
472
|
-
clientSecret,
|
|
473
|
-
issuer
|
|
909
|
+
refreshConfig.clientId,
|
|
910
|
+
refreshConfig.clientSecret || "",
|
|
911
|
+
refreshConfig.issuer,
|
|
912
|
+
options.dpop,
|
|
913
|
+
options.clientAssertionKey
|
|
474
914
|
).finally(() => {
|
|
475
|
-
setTimeout(() => pendingRefreshes.delete(
|
|
915
|
+
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
|
|
916
|
+
if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
|
|
917
|
+
cleanupTimer.unref();
|
|
918
|
+
}
|
|
476
919
|
});
|
|
477
|
-
pendingRefreshes.set(
|
|
920
|
+
pendingRefreshes.set(grantKey, refreshPromise);
|
|
478
921
|
}
|
|
479
922
|
const refreshed = await refreshPromise;
|
|
480
923
|
if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
|
|
@@ -484,12 +927,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
484
927
|
refreshToken: refreshed.refreshToken,
|
|
485
928
|
expiresAt: refreshed.expiresAt
|
|
486
929
|
};
|
|
930
|
+
if (options.clientAssertionKey) delete updatedToken.clientSecret;
|
|
931
|
+
if (!await verifyAndAuthorize(updatedToken)) return denied();
|
|
487
932
|
const newJwt = await (0, import_jwt.encode)({
|
|
488
933
|
token: updatedToken,
|
|
489
934
|
secret
|
|
490
935
|
});
|
|
491
|
-
const requestHeaders =
|
|
492
|
-
requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
|
|
936
|
+
const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
|
|
493
937
|
const response = import_server.NextResponse.next({
|
|
494
938
|
request: {
|
|
495
939
|
headers: requestHeaders
|
|
@@ -499,7 +943,8 @@ function withOAuth42Auth(options = {}) {
|
|
|
499
943
|
httpOnly: true,
|
|
500
944
|
sameSite: "lax",
|
|
501
945
|
path: "/",
|
|
502
|
-
secure: process.env.NODE_ENV === "production"
|
|
946
|
+
secure: process.env.NODE_ENV === "production",
|
|
947
|
+
maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
|
|
503
948
|
};
|
|
504
949
|
if (newJwt.length <= CHUNK_SIZE) {
|
|
505
950
|
response.cookies.set(cookieName, newJwt, cookieOptions);
|
|
@@ -541,13 +986,12 @@ function withOAuth42Auth(options = {}) {
|
|
|
541
986
|
chunked: newJwt.length > CHUNK_SIZE,
|
|
542
987
|
existingBaseCooke: req.cookies.has(cookieName),
|
|
543
988
|
existingChunks: existingChunks.length > 0 ? existingChunks : "none",
|
|
544
|
-
newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
|
|
545
989
|
newExpiresAt: refreshed.expiresAt,
|
|
546
990
|
cookieName
|
|
547
991
|
});
|
|
548
992
|
return response;
|
|
549
993
|
} else {
|
|
550
|
-
console.log("[OAuth42 Middleware] Refresh failed
|
|
994
|
+
console.log("[OAuth42 Middleware] Refresh failed, clearing session");
|
|
551
995
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
552
996
|
const url = new URL(signInUrl, req.url);
|
|
553
997
|
url.searchParams.set("callbackUrl", pathname);
|
|
@@ -564,16 +1008,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
564
1008
|
return response;
|
|
565
1009
|
}
|
|
566
1010
|
}
|
|
567
|
-
if (
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const url = new URL(signInUrl, req.url);
|
|
572
|
-
url.searchParams.set("callbackUrl", pathname);
|
|
573
|
-
return import_server.NextResponse.redirect(url);
|
|
574
|
-
}
|
|
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);
|
|
575
1015
|
}
|
|
576
|
-
return
|
|
1016
|
+
if (!await verifyAndAuthorize(token)) return denied();
|
|
1017
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
577
1018
|
};
|
|
578
1019
|
}
|
|
579
1020
|
function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
|
|
@@ -592,16 +1033,88 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
|
|
|
592
1033
|
publicPaths
|
|
593
1034
|
};
|
|
594
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
|
+
};
|
|
595
1104
|
// Annotate the CommonJS export names for ESM import in node:
|
|
596
1105
|
0 && (module.exports = {
|
|
1106
|
+
OAuth42ClientAssertion,
|
|
1107
|
+
OAuth42DPoP,
|
|
597
1108
|
OAuth42Provider,
|
|
598
1109
|
createAuth,
|
|
599
1110
|
createMiddlewareConfig,
|
|
600
1111
|
getOAuth42Session,
|
|
601
1112
|
getServerSession,
|
|
1113
|
+
pushAuthorizationURL,
|
|
602
1114
|
refreshAccessToken,
|
|
603
1115
|
withOAuth42Auth,
|
|
604
1116
|
withOAuth42ServerSideProps,
|
|
605
|
-
withOAuth42Session
|
|
1117
|
+
withOAuth42Session,
|
|
1118
|
+
withPushedAuthorization
|
|
606
1119
|
});
|
|
607
1120
|
//# sourceMappingURL=index.js.map
|