@oauth42/next 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/dist/build-manifest.json +59 -0
- package/dist/client/index.d.mts +7 -6
- package/dist/client/index.d.ts +7 -6
- package/dist/client/index.js +13 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +13 -1
- package/dist/client/index.mjs.map +1 -1
- package/dist/{index-Cfqxl9r2.d.mts → index-BuZTiUds.d.mts} +51 -1
- package/dist/{index-Cfqxl9r2.d.ts → index-BuZTiUds.d.ts} +51 -1
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +469 -159
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +465 -158
- package/dist/index.mjs.map +1 -1
- package/dist/middleware/index.d.mts +2 -1
- package/dist/middleware/index.d.ts +2 -1
- package/dist/middleware/index.js +218 -66
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +219 -66
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/{auth-D3tAruwx.d.mts → par-Cyu6M1e6.d.mts} +27 -12
- package/dist/{auth-CYMPe3k0.d.ts → par-DQNt8j-s.d.ts} +27 -12
- package/dist/server/index.d.mts +7 -3
- package/dist/server/index.d.ts +7 -3
- package/dist/server/index.js +503 -178
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +499 -177
- package/dist/server/index.mjs.map +1 -1
- package/package.json +14 -5
- package/src/types/index.ts +2 -0
- package/src/types/next-auth.d.ts +2 -0
package/dist/server/index.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,11 +29,17 @@ 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/server/index.ts
|
|
31
38
|
var server_exports = {};
|
|
32
39
|
__export(server_exports, {
|
|
33
40
|
NextAuth: () => import_next_auth3.default,
|
|
41
|
+
OAuth42ClientAssertion: () => OAuth42ClientAssertion,
|
|
42
|
+
OAuth42DPoP: () => OAuth42DPoP,
|
|
34
43
|
OAuth42Provider: () => OAuth42Provider,
|
|
35
44
|
createAuth: () => createAuth,
|
|
36
45
|
createBackendSession: () => createBackendSession,
|
|
@@ -39,15 +48,196 @@ __export(server_exports, {
|
|
|
39
48
|
createMiddlewareConfig: () => createMiddlewareConfig,
|
|
40
49
|
getOAuth42Session: () => getOAuth42Session,
|
|
41
50
|
getServerSession: () => getServerSession,
|
|
51
|
+
pushAuthorizationURL: () => pushAuthorizationURL,
|
|
42
52
|
refreshAccessToken: () => refreshAccessToken,
|
|
43
53
|
resolveBackendSession: () => resolveBackendSession,
|
|
44
54
|
revokeBackendSession: () => revokeBackendSession,
|
|
45
55
|
withOAuth42Auth: () => withOAuth42Auth,
|
|
46
56
|
withOAuth42ServerSideProps: () => withOAuth42ServerSideProps,
|
|
47
|
-
withOAuth42Session: () => withOAuth42Session
|
|
57
|
+
withOAuth42Session: () => withOAuth42Session,
|
|
58
|
+
withPushedAuthorization: () => withPushedAuthorization
|
|
48
59
|
});
|
|
49
60
|
module.exports = __toCommonJS(server_exports);
|
|
50
61
|
|
|
62
|
+
// src/server/client-assertion.ts
|
|
63
|
+
var import_node_crypto = require("crypto");
|
|
64
|
+
var _key, _public;
|
|
65
|
+
var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
|
|
66
|
+
constructor(kid, key) {
|
|
67
|
+
__privateAdd(this, _key);
|
|
68
|
+
__privateAdd(this, _public);
|
|
69
|
+
if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
|
|
70
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
71
|
+
throw new Error("Client assertions require a private P-256 key");
|
|
72
|
+
}
|
|
73
|
+
__privateSet(this, _key, key);
|
|
74
|
+
this.kid = kid;
|
|
75
|
+
const publicKey = (0, import_node_crypto.createPublicKey)(key).export({ format: "jwk" });
|
|
76
|
+
__privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
|
|
77
|
+
this.thumbprint = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
|
|
78
|
+
}
|
|
79
|
+
static generate(kid) {
|
|
80
|
+
return new _OAuth42ClientAssertion(kid, (0, import_node_crypto.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
81
|
+
}
|
|
82
|
+
get publicJwk() {
|
|
83
|
+
return { ...__privateGet(this, _public), kid: this.kid };
|
|
84
|
+
}
|
|
85
|
+
/** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
|
|
86
|
+
get oidcJwks() {
|
|
87
|
+
return { keys: [{
|
|
88
|
+
kty: "EC",
|
|
89
|
+
crv: "P-256",
|
|
90
|
+
x: __privateGet(this, _public).x,
|
|
91
|
+
y: __privateGet(this, _public).y,
|
|
92
|
+
d: __privateGet(this, _key).export({ format: "jwk" }).d,
|
|
93
|
+
kid: this.kid
|
|
94
|
+
}] };
|
|
95
|
+
}
|
|
96
|
+
static validateEndpoint(target) {
|
|
97
|
+
const url = new URL(target);
|
|
98
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
99
|
+
throw new Error("Client authentication requires HTTPS outside loopback development");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
assertion(clientId, audience) {
|
|
103
|
+
_OAuth42ClientAssertion.validateEndpoint(audience);
|
|
104
|
+
if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
|
|
105
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
106
|
+
const header = { alg: "ES256", typ: "JWT", kid: this.kid };
|
|
107
|
+
const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: (0, import_node_crypto.randomBytes)(32).toString("base64url") };
|
|
108
|
+
const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
|
|
109
|
+
return `${data}.${(0, import_node_crypto.sign)("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
110
|
+
}
|
|
111
|
+
async fetch(clientId, target, init, dpop) {
|
|
112
|
+
if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
|
|
113
|
+
throw new Error("Client assertions require a form POST");
|
|
114
|
+
}
|
|
115
|
+
const prepare = () => {
|
|
116
|
+
const body = new URLSearchParams(init.body);
|
|
117
|
+
body.delete("client_secret");
|
|
118
|
+
body.set("client_id", clientId);
|
|
119
|
+
body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
|
120
|
+
body.set("client_assertion", this.assertion(clientId, target));
|
|
121
|
+
const headers = new Headers(init.headers);
|
|
122
|
+
headers.delete("Authorization");
|
|
123
|
+
headers.delete("Content-Length");
|
|
124
|
+
headers.delete("Transfer-Encoding");
|
|
125
|
+
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
|
126
|
+
return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
|
|
127
|
+
};
|
|
128
|
+
return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
_key = new WeakMap();
|
|
132
|
+
_public = new WeakMap();
|
|
133
|
+
var OAuth42ClientAssertion = _OAuth42ClientAssertion;
|
|
134
|
+
|
|
135
|
+
// src/server/par.ts
|
|
136
|
+
async function pushAuthorizationURL(value, config) {
|
|
137
|
+
const base = config.issuer.replace(/\/$/, "");
|
|
138
|
+
let url;
|
|
139
|
+
try {
|
|
140
|
+
url = new URL(value);
|
|
141
|
+
} catch {
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
if (`${url.origin}${url.pathname}` !== `${base}/oauth2/authorize`) return value;
|
|
145
|
+
if (url.username || url.password || url.hash) throw new Error("Invalid authorization endpoint");
|
|
146
|
+
const body = new URLSearchParams(url.search);
|
|
147
|
+
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))) {
|
|
148
|
+
throw new Error("Invalid PAR authorization transaction");
|
|
149
|
+
}
|
|
150
|
+
const target = `${base}/oauth2/par`;
|
|
151
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
152
|
+
if (!config.clientAssertionKey) body.set("client_secret", config.clientSecret || "");
|
|
153
|
+
const init = {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
156
|
+
body,
|
|
157
|
+
redirect: "error",
|
|
158
|
+
cache: "no-store",
|
|
159
|
+
signal: AbortSignal.timeout(3e4)
|
|
160
|
+
};
|
|
161
|
+
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);
|
|
162
|
+
if (response.status !== 201) throw new Error("Pushed authorization failed");
|
|
163
|
+
const pushed = await response.json();
|
|
164
|
+
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");
|
|
165
|
+
url.search = new URLSearchParams({ client_id: config.clientId, request_uri: pushed.request_uri }).toString();
|
|
166
|
+
return url.href;
|
|
167
|
+
}
|
|
168
|
+
function withPushedAuthorization(handler, config) {
|
|
169
|
+
return async (request, context) => {
|
|
170
|
+
if (!(request instanceof Request)) throw new Error("PAR handlers require Next.js App Router Request/Response");
|
|
171
|
+
const response = await handler(request, context);
|
|
172
|
+
const location = response.headers.get("Location");
|
|
173
|
+
if (location) {
|
|
174
|
+
const next = await pushAuthorizationURL(location, config);
|
|
175
|
+
if (next === location) return response;
|
|
176
|
+
const headers = new Headers(response.headers);
|
|
177
|
+
headers.set("Location", next);
|
|
178
|
+
headers.delete("content-length");
|
|
179
|
+
return new Response(response.body, { status: response.status, headers });
|
|
180
|
+
}
|
|
181
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
182
|
+
const body = await response.clone().json();
|
|
183
|
+
if (typeof body?.url === "string") {
|
|
184
|
+
const next = await pushAuthorizationURL(body.url, config);
|
|
185
|
+
if (next !== body.url) {
|
|
186
|
+
const headers = new Headers(response.headers);
|
|
187
|
+
headers.delete("content-length");
|
|
188
|
+
return new Response(JSON.stringify({ ...body, url: next }), { status: response.status, headers });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return response;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/server/verified-userinfo.ts
|
|
197
|
+
async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
|
|
198
|
+
if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
|
|
199
|
+
try {
|
|
200
|
+
const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
|
|
201
|
+
const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
|
|
202
|
+
const response = await fetcher(target, {
|
|
203
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
204
|
+
redirect: "error",
|
|
205
|
+
cache: "no-store",
|
|
206
|
+
signal: AbortSignal.timeout(3e4)
|
|
207
|
+
});
|
|
208
|
+
if (!response.ok) return null;
|
|
209
|
+
const user = await response.json();
|
|
210
|
+
return parseVerifiedUserInfo(user, subject);
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function parseVerifiedUserInfo(value, subject) {
|
|
216
|
+
if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
217
|
+
const user = value;
|
|
218
|
+
if (user.sub !== subject) return null;
|
|
219
|
+
if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
|
|
220
|
+
if (user.email !== void 0 && typeof user.email !== "string") return null;
|
|
221
|
+
if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
|
|
222
|
+
const profile = { sub: user.sub };
|
|
223
|
+
for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
|
|
224
|
+
if (user[field] != null) {
|
|
225
|
+
if (typeof user[field] !== "string") return null;
|
|
226
|
+
profile[field] = user[field];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
|
|
230
|
+
if (user.groups !== void 0) profile.groups = user.groups;
|
|
231
|
+
return profile;
|
|
232
|
+
}
|
|
233
|
+
async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
|
|
234
|
+
if (typeof session.access_token !== "string" || !session.access_token) return null;
|
|
235
|
+
if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
|
|
236
|
+
return parseVerifiedUserInfo(session.user_info, subject);
|
|
237
|
+
}
|
|
238
|
+
return verifiedUserInfo(issuer, session.access_token, subject, dpop);
|
|
239
|
+
}
|
|
240
|
+
|
|
51
241
|
// src/server/auth.ts
|
|
52
242
|
var import_next_auth2 = __toESM(require("next-auth"));
|
|
53
243
|
|
|
@@ -55,13 +245,20 @@ var import_next_auth2 = __toESM(require("next-auth"));
|
|
|
55
245
|
function OAuth42Provider(options) {
|
|
56
246
|
const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
57
247
|
const baseUrl = issuer.replace(/\/$/, "");
|
|
248
|
+
const { dpop, clientAssertionKey, ...providerOptions } = options;
|
|
249
|
+
if (clientAssertionKey) {
|
|
250
|
+
OAuth42ClientAssertion.validateEndpoint(`${baseUrl}/oauth2/token`);
|
|
251
|
+
providerOptions.clientSecret = void 0;
|
|
252
|
+
}
|
|
253
|
+
if (options.pkceEnabled === false) throw new Error("OAuth42 requires S256 PKCE");
|
|
58
254
|
return {
|
|
59
255
|
id: "oauth42",
|
|
60
256
|
name: "OAuth42",
|
|
61
257
|
type: "oauth",
|
|
62
258
|
version: "2.0",
|
|
63
|
-
//
|
|
64
|
-
|
|
259
|
+
// Pin O42 metadata: NextAuth v4 discovery does not compare the returned issuer.
|
|
260
|
+
// Constructing its OIDC client from these trusted endpoints preserves issuer binding.
|
|
261
|
+
jwks_endpoint: `${baseUrl}/.well-known/jwks.json`,
|
|
65
262
|
// Use ID token for user profile (standard OIDC pattern)
|
|
66
263
|
// This avoids an extra HTTP call to /userinfo on every auth flow
|
|
67
264
|
// Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
|
|
@@ -74,17 +271,26 @@ function OAuth42Provider(options) {
|
|
|
74
271
|
response_type: "code"
|
|
75
272
|
}
|
|
76
273
|
},
|
|
77
|
-
token:
|
|
274
|
+
token: dpop ? {
|
|
275
|
+
url: `${baseUrl}/oauth2/token`,
|
|
276
|
+
async request({ client, provider, params, checks }) {
|
|
277
|
+
const tokens = await client.callback(provider.callbackUrl, params, checks, { DPoP: dpop.oidcKey });
|
|
278
|
+
dpop.assertTokenResponse(tokens);
|
|
279
|
+
return { tokens };
|
|
280
|
+
}
|
|
281
|
+
} : `${baseUrl}/oauth2/token`,
|
|
78
282
|
userinfo: `${baseUrl}/oauth2/userinfo`,
|
|
283
|
+
...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
|
|
79
284
|
client: {
|
|
80
285
|
id: options.clientId,
|
|
81
|
-
secret: options.clientSecret,
|
|
82
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
286
|
+
secret: clientAssertionKey ? void 0 : options.clientSecret,
|
|
287
|
+
token_endpoint_auth_method: clientAssertionKey ? "private_key_jwt" : "client_secret_post",
|
|
288
|
+
...clientAssertionKey ? { token_endpoint_auth_signing_alg: "ES256" } : {},
|
|
83
289
|
id_token_signed_response_alg: "RS256"
|
|
84
290
|
// OAuth42 uses RS256 for ID tokens
|
|
85
291
|
},
|
|
86
|
-
issuer
|
|
87
|
-
checks:
|
|
292
|
+
issuer,
|
|
293
|
+
checks: ["pkce", "state", "nonce"],
|
|
88
294
|
profile(profile, tokens) {
|
|
89
295
|
return {
|
|
90
296
|
id: profile.sub || profile.id || profile.email,
|
|
@@ -100,42 +306,18 @@ function OAuth42Provider(options) {
|
|
|
100
306
|
bg: "#1e40af",
|
|
101
307
|
text: "#ffffff"
|
|
102
308
|
},
|
|
103
|
-
|
|
309
|
+
// NextAuth v4 types require a string even when private_key_jwt sends no secret.
|
|
310
|
+
options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
|
|
104
311
|
};
|
|
105
312
|
}
|
|
106
313
|
|
|
107
314
|
// src/server/session.ts
|
|
108
315
|
var import_next_auth = require("next-auth");
|
|
109
|
-
var import_headers = require("next/headers");
|
|
110
|
-
var REFRESHED_TOKEN_HEADER = "x-oauth42-refreshed-token";
|
|
111
316
|
async function getOAuth42Session(...args) {
|
|
112
317
|
const session = await (0, import_next_auth.getServerSession)(...args);
|
|
113
|
-
if (!session) {
|
|
318
|
+
if (!session || session.error) {
|
|
114
319
|
return null;
|
|
115
320
|
}
|
|
116
|
-
let refreshedToken = null;
|
|
117
|
-
try {
|
|
118
|
-
if (args.length === 1) {
|
|
119
|
-
const headersList = await (0, import_headers.headers)();
|
|
120
|
-
refreshedToken = headersList.get(REFRESHED_TOKEN_HEADER);
|
|
121
|
-
} else if (args.length === 3) {
|
|
122
|
-
const req = args[0];
|
|
123
|
-
refreshedToken = req.headers[REFRESHED_TOKEN_HEADER] || null;
|
|
124
|
-
}
|
|
125
|
-
} catch {
|
|
126
|
-
}
|
|
127
|
-
if (refreshedToken) {
|
|
128
|
-
const sessionToken = session.accessToken;
|
|
129
|
-
console.log("[OAuth42 Session] Using refreshed token from middleware:", {
|
|
130
|
-
refreshedPrefix: refreshedToken.substring(0, 20),
|
|
131
|
-
sessionPrefix: sessionToken?.substring(0, 20),
|
|
132
|
-
tokensMatch: refreshedToken === sessionToken
|
|
133
|
-
});
|
|
134
|
-
return {
|
|
135
|
-
...session,
|
|
136
|
-
accessToken: refreshedToken
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
321
|
return session;
|
|
140
322
|
}
|
|
141
323
|
function withOAuth42Session(handler, authOptions) {
|
|
@@ -173,66 +355,59 @@ function isBackendSessionMode(mode) {
|
|
|
173
355
|
function sessionEndpoint(issuer) {
|
|
174
356
|
return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
|
|
175
357
|
}
|
|
176
|
-
async function
|
|
177
|
-
|
|
178
|
-
|
|
358
|
+
async function sessionFetch(params, target, body, credential, method = "POST") {
|
|
359
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
360
|
+
const prepare = () => ({
|
|
361
|
+
method,
|
|
362
|
+
redirect: "error",
|
|
363
|
+
cache: "no-store",
|
|
364
|
+
signal: AbortSignal.timeout(3e4),
|
|
179
365
|
headers: { "Content-Type": "application/json" },
|
|
180
366
|
body: JSON.stringify({
|
|
181
|
-
|
|
182
|
-
refresh_token: params.refreshToken,
|
|
183
|
-
expires_at: params.expiresAt ?? Math.floor(Date.now() / 1e3) + 3600,
|
|
184
|
-
id_token: params.idToken,
|
|
367
|
+
...body,
|
|
185
368
|
client_id: params.clientId,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
369
|
+
...params.clientAssertionKey ? {
|
|
370
|
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
371
|
+
client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
|
|
372
|
+
} : { client_secret: params.clientSecret }
|
|
189
373
|
})
|
|
190
374
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
375
|
+
return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
|
|
376
|
+
}
|
|
377
|
+
async function sessionResponse(response, params) {
|
|
378
|
+
if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
|
|
379
|
+
const session = await response.json();
|
|
380
|
+
if (params.dpop) params.dpop.assertTokenResponse(session);
|
|
381
|
+
else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
|
|
382
|
+
return session;
|
|
383
|
+
}
|
|
384
|
+
async function createBackendSession(params) {
|
|
385
|
+
return sessionResponse(await sessionFetch(params, sessionEndpoint(params.issuer), {
|
|
386
|
+
access_token: params.accessToken,
|
|
387
|
+
refresh_token: params.refreshToken,
|
|
388
|
+
id_token: params.idToken,
|
|
389
|
+
user: params.user
|
|
390
|
+
}, params.accessToken), params);
|
|
195
391
|
}
|
|
196
392
|
async function resolveBackendSession(params) {
|
|
197
|
-
const response = await
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
body: JSON.stringify({
|
|
201
|
-
session_id: params.sessionId,
|
|
202
|
-
client_id: params.clientId,
|
|
203
|
-
client_secret: params.clientSecret,
|
|
204
|
-
issuer: params.issuer
|
|
205
|
-
})
|
|
206
|
-
});
|
|
207
|
-
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
if (!response.ok) {
|
|
211
|
-
throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
|
|
212
|
-
}
|
|
213
|
-
return response.json();
|
|
393
|
+
const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
|
|
394
|
+
if ([401, 403, 404].includes(response.status)) return null;
|
|
395
|
+
return sessionResponse(response, params);
|
|
214
396
|
}
|
|
215
397
|
async function revokeBackendSession(params) {
|
|
216
|
-
await
|
|
217
|
-
|
|
218
|
-
headers: { "Content-Type": "application/json" },
|
|
219
|
-
body: JSON.stringify({
|
|
220
|
-
session_id: params.sessionId,
|
|
221
|
-
client_id: params.clientId,
|
|
222
|
-
client_secret: params.clientSecret
|
|
223
|
-
})
|
|
224
|
-
});
|
|
398
|
+
const response = await sessionFetch(params, sessionEndpoint(params.issuer), { session_id: params.sessionId }, params.sessionId, "DELETE");
|
|
399
|
+
if (!response.ok) throw new Error(`OAuth42 backend session revoke failed: ${response.status}`);
|
|
225
400
|
}
|
|
226
401
|
|
|
227
402
|
// src/server/auth.ts
|
|
228
403
|
var NextAuth = import_next_auth2.default.default || import_next_auth2.default;
|
|
229
|
-
var
|
|
404
|
+
var activeRefreshes = /* @__PURE__ */ new Map();
|
|
230
405
|
function createAuth(options = {}) {
|
|
231
406
|
const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
|
|
232
|
-
const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
407
|
+
const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
233
408
|
const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
234
409
|
const useBackendSessions = isBackendSessionMode(options.sessionStore);
|
|
235
|
-
if (!clientId || !clientSecret) {
|
|
410
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
236
411
|
throw new Error(
|
|
237
412
|
"OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
|
|
238
413
|
);
|
|
@@ -241,14 +416,17 @@ function createAuth(options = {}) {
|
|
|
241
416
|
providers: [
|
|
242
417
|
OAuth42Provider({
|
|
243
418
|
clientId,
|
|
244
|
-
clientSecret,
|
|
419
|
+
clientSecret: clientSecret || "",
|
|
245
420
|
issuer,
|
|
246
421
|
scopes: options.scopes,
|
|
247
|
-
pkceEnabled: options.pkceEnabled
|
|
422
|
+
pkceEnabled: options.pkceEnabled,
|
|
423
|
+
dpop: options.dpop,
|
|
424
|
+
clientAssertionKey: options.clientAssertionKey
|
|
248
425
|
})
|
|
249
426
|
],
|
|
250
427
|
callbacks: {
|
|
251
428
|
async jwt({ token, account, profile }) {
|
|
429
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
252
430
|
console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
|
|
253
431
|
if (account) {
|
|
254
432
|
if (useBackendSessions) {
|
|
@@ -257,9 +435,11 @@ function createAuth(options = {}) {
|
|
|
257
435
|
}
|
|
258
436
|
const oauth42Profile = profile;
|
|
259
437
|
const backendSession = await createBackendSession({
|
|
438
|
+
dpop: options.dpop,
|
|
439
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
260
440
|
issuer,
|
|
261
441
|
clientId,
|
|
262
|
-
clientSecret,
|
|
442
|
+
clientSecret: clientSecret || "",
|
|
263
443
|
accessToken: account.access_token,
|
|
264
444
|
refreshToken: account.refresh_token,
|
|
265
445
|
expiresAt: account.expires_at,
|
|
@@ -283,7 +463,8 @@ function createAuth(options = {}) {
|
|
|
283
463
|
token.expiresAt = account.expires_at;
|
|
284
464
|
token.idToken = account.id_token;
|
|
285
465
|
token.clientId = clientId;
|
|
286
|
-
|
|
466
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
467
|
+
else token.clientSecret = clientSecret;
|
|
287
468
|
token.issuer = issuer;
|
|
288
469
|
}
|
|
289
470
|
}
|
|
@@ -302,25 +483,36 @@ function createAuth(options = {}) {
|
|
|
302
483
|
},
|
|
303
484
|
async session({ session, token }) {
|
|
304
485
|
console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
|
|
486
|
+
const revokedSession = () => {
|
|
487
|
+
session.error = "SessionRevoked";
|
|
488
|
+
delete session.accessToken;
|
|
489
|
+
delete session.idToken;
|
|
490
|
+
delete session.userInfo;
|
|
491
|
+
delete session.user;
|
|
492
|
+
return session;
|
|
493
|
+
};
|
|
494
|
+
if (token.error) return revokedSession();
|
|
495
|
+
let verified;
|
|
305
496
|
if (useBackendSessions) {
|
|
306
497
|
const sessionId = token.oauth42SessionId;
|
|
307
498
|
if (!sessionId) {
|
|
308
|
-
|
|
309
|
-
return session;
|
|
499
|
+
return revokedSession();
|
|
310
500
|
}
|
|
311
501
|
const backendSession = await resolveBackendSession({
|
|
502
|
+
dpop: options.dpop,
|
|
503
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
312
504
|
issuer,
|
|
313
505
|
clientId,
|
|
314
|
-
clientSecret,
|
|
506
|
+
clientSecret: clientSecret || "",
|
|
315
507
|
sessionId
|
|
316
508
|
});
|
|
317
509
|
if (!backendSession) {
|
|
318
|
-
|
|
319
|
-
return session;
|
|
510
|
+
return revokedSession();
|
|
320
511
|
}
|
|
321
512
|
session.accessToken = backendSession.access_token;
|
|
322
513
|
session.idToken = backendSession.id_token;
|
|
323
514
|
token.expiresAt = backendSession.expires_at;
|
|
515
|
+
verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
|
|
324
516
|
if (backendSession.user && session.user) {
|
|
325
517
|
session.user.email = backendSession.user.email;
|
|
326
518
|
session.user.name = backendSession.user.name;
|
|
@@ -331,6 +523,23 @@ function createAuth(options = {}) {
|
|
|
331
523
|
} else {
|
|
332
524
|
session.accessToken = token.accessToken;
|
|
333
525
|
session.idToken = token.idToken;
|
|
526
|
+
verified = await verifiedUserInfo(issuer, session.accessToken, token.sub, options.dpop);
|
|
527
|
+
}
|
|
528
|
+
if (!verified) {
|
|
529
|
+
return revokedSession();
|
|
530
|
+
}
|
|
531
|
+
session.userInfo = verified;
|
|
532
|
+
token.email = verified.email;
|
|
533
|
+
token.name = verified.name;
|
|
534
|
+
token.username = verified.preferred_username ?? verified.username;
|
|
535
|
+
token.emailVerified = verified.email_verified;
|
|
536
|
+
token.groups = verified.groups ?? [];
|
|
537
|
+
if (session.user) {
|
|
538
|
+
session.user.email = verified.email;
|
|
539
|
+
session.user.name = verified.name;
|
|
540
|
+
session.user.username = verified.preferred_username ?? verified.username;
|
|
541
|
+
session.user.emailVerified = verified.email_verified;
|
|
542
|
+
session.user.groups = verified.groups ?? [];
|
|
334
543
|
}
|
|
335
544
|
if (token.error) {
|
|
336
545
|
session.error = token.error;
|
|
@@ -365,9 +574,11 @@ function createAuth(options = {}) {
|
|
|
365
574
|
const sessionId = message.token?.oauth42SessionId;
|
|
366
575
|
if (sessionId) {
|
|
367
576
|
await revokeBackendSession({
|
|
577
|
+
dpop: options.dpop,
|
|
578
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
368
579
|
issuer,
|
|
369
580
|
clientId,
|
|
370
|
-
clientSecret,
|
|
581
|
+
clientSecret: clientSecret || "",
|
|
371
582
|
sessionId
|
|
372
583
|
}).catch((error) => {
|
|
373
584
|
console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
|
|
@@ -454,7 +665,8 @@ function createAuth(options = {}) {
|
|
|
454
665
|
}
|
|
455
666
|
}
|
|
456
667
|
};
|
|
457
|
-
const
|
|
668
|
+
const nextAuthHandler = NextAuth(authOptions);
|
|
669
|
+
const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
|
|
458
670
|
return {
|
|
459
671
|
auth: authOptions,
|
|
460
672
|
handlers: { GET: handler, POST: handler }
|
|
@@ -465,24 +677,40 @@ function createHandlers(authOptions) {
|
|
|
465
677
|
return { GET: handler, POST: handler };
|
|
466
678
|
}
|
|
467
679
|
var getServerSession = getOAuth42Session;
|
|
468
|
-
async function refreshAccessToken(token, clientId, clientSecret, issuer) {
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
680
|
+
async function refreshAccessToken(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
681
|
+
if (typeof token.refreshToken !== "string" || !token.refreshToken) {
|
|
682
|
+
return { ...token, error: "RefreshAccessTokenError" };
|
|
683
|
+
}
|
|
684
|
+
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]);
|
|
685
|
+
let refresh = activeRefreshes.get(key);
|
|
686
|
+
if (!refresh) {
|
|
687
|
+
refresh = doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey);
|
|
688
|
+
activeRefreshes.set(key, refresh);
|
|
472
689
|
}
|
|
473
|
-
activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
|
|
474
690
|
try {
|
|
475
|
-
|
|
691
|
+
const result = await refresh;
|
|
692
|
+
const updated = {
|
|
693
|
+
...token,
|
|
694
|
+
accessToken: result.accessToken,
|
|
695
|
+
refreshToken: result.refreshToken,
|
|
696
|
+
expiresAt: result.expiresAt,
|
|
697
|
+
error: result.error
|
|
698
|
+
};
|
|
699
|
+
if (clientAssertionKey) delete updated.clientSecret;
|
|
700
|
+
return updated;
|
|
476
701
|
} finally {
|
|
477
|
-
|
|
702
|
+
if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
|
|
478
703
|
}
|
|
479
704
|
}
|
|
480
|
-
async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
705
|
+
async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
481
706
|
try {
|
|
482
707
|
const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
483
708
|
const tokenUrl = `${baseUrl}/oauth2/token`;
|
|
484
709
|
const fetchOptions = {
|
|
485
710
|
method: "POST",
|
|
711
|
+
redirect: "error",
|
|
712
|
+
cache: "no-store",
|
|
713
|
+
signal: AbortSignal.timeout(3e4),
|
|
486
714
|
headers: {
|
|
487
715
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
488
716
|
},
|
|
@@ -493,17 +721,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
493
721
|
client_secret: clientSecret
|
|
494
722
|
})
|
|
495
723
|
};
|
|
496
|
-
|
|
497
|
-
const https2 = await import("https");
|
|
498
|
-
fetchOptions.agent = new https2.Agent({
|
|
499
|
-
rejectUnauthorized: false
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
const response = await fetch(tokenUrl, fetchOptions);
|
|
724
|
+
const response = clientAssertionKey ? await clientAssertionKey.fetch(clientId, tokenUrl, fetchOptions, dpop) : dpop ? await dpop.fetch(tokenUrl, fetchOptions) : await fetch(tokenUrl, fetchOptions);
|
|
503
725
|
const refreshedTokens = await response.json();
|
|
504
726
|
if (!response.ok) {
|
|
505
727
|
throw refreshedTokens;
|
|
506
728
|
}
|
|
729
|
+
dpop?.assertTokenResponse(refreshedTokens);
|
|
507
730
|
console.log("[OAuth42] Token refreshed successfully");
|
|
508
731
|
return {
|
|
509
732
|
...token,
|
|
@@ -515,7 +738,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
515
738
|
error: void 0
|
|
516
739
|
};
|
|
517
740
|
} catch (error) {
|
|
518
|
-
console.error("[OAuth42] Failed to refresh access token
|
|
741
|
+
console.error("[OAuth42] Failed to refresh access token");
|
|
519
742
|
return {
|
|
520
743
|
...token,
|
|
521
744
|
error: "RefreshAccessTokenError"
|
|
@@ -532,14 +755,20 @@ var import_jwt = require("next-auth/jwt");
|
|
|
532
755
|
var ALLOWED_COOKIE_SIZE = 4096;
|
|
533
756
|
var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
|
|
534
757
|
var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
535
|
-
var REFRESH_RACE_GRACE_SECONDS = 60;
|
|
536
758
|
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
537
759
|
var pendingRefreshes = /* @__PURE__ */ new Map();
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
760
|
+
function forwardedHeaders(req, cookieName, jwt) {
|
|
761
|
+
const headers = new Headers(req.headers);
|
|
762
|
+
headers.delete("x-oauth42-refreshed-token");
|
|
763
|
+
if (cookieName && jwt) {
|
|
764
|
+
const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
|
|
765
|
+
if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
|
|
766
|
+
else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
|
|
767
|
+
cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
|
|
768
|
+
}
|
|
769
|
+
headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
|
|
541
770
|
}
|
|
542
|
-
return
|
|
771
|
+
return headers;
|
|
543
772
|
}
|
|
544
773
|
function resolveRefreshConfig(options, token) {
|
|
545
774
|
return {
|
|
@@ -548,11 +777,15 @@ function resolveRefreshConfig(options, token) {
|
|
|
548
777
|
issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
|
|
549
778
|
};
|
|
550
779
|
}
|
|
551
|
-
async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
780
|
+
async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
552
781
|
try {
|
|
553
782
|
const tokenUrl = `${issuer}/oauth2/token`;
|
|
554
|
-
const
|
|
783
|
+
const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
|
|
784
|
+
const response = await fetcher(tokenUrl, {
|
|
555
785
|
method: "POST",
|
|
786
|
+
redirect: "error",
|
|
787
|
+
cache: "no-store",
|
|
788
|
+
signal: AbortSignal.timeout(3e4),
|
|
556
789
|
headers: {
|
|
557
790
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
558
791
|
},
|
|
@@ -565,9 +798,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
565
798
|
});
|
|
566
799
|
const data = await response.json();
|
|
567
800
|
if (!response.ok) {
|
|
568
|
-
console.error("[OAuth42 Middleware] Token refresh failed:",
|
|
801
|
+
console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
|
|
569
802
|
return { success: false, error: data.error || "refresh_failed" };
|
|
570
803
|
}
|
|
804
|
+
dpop?.assertTokenResponse(data);
|
|
571
805
|
console.log("[OAuth42 Middleware] Token refreshed successfully");
|
|
572
806
|
return {
|
|
573
807
|
success: true,
|
|
@@ -576,14 +810,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
576
810
|
expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
|
|
577
811
|
};
|
|
578
812
|
} catch (error) {
|
|
579
|
-
console.error("[OAuth42 Middleware] Token refresh
|
|
813
|
+
console.error("[OAuth42 Middleware] Token refresh request failed");
|
|
580
814
|
return { success: false, error: "refresh_error" };
|
|
581
815
|
}
|
|
582
816
|
}
|
|
583
817
|
function withOAuth42Auth(options = {}) {
|
|
584
818
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
585
819
|
if (!secret) {
|
|
586
|
-
|
|
820
|
+
throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
|
|
587
821
|
}
|
|
588
822
|
return async function middleware(req) {
|
|
589
823
|
const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
|
|
@@ -594,22 +828,41 @@ function withOAuth42Auth(options = {}) {
|
|
|
594
828
|
});
|
|
595
829
|
const pathname = req.nextUrl.pathname;
|
|
596
830
|
if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
|
|
597
|
-
return import_server.NextResponse.next();
|
|
831
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
598
832
|
}
|
|
599
833
|
const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
|
|
600
834
|
if (!needsProtection) {
|
|
601
|
-
return import_server.NextResponse.next();
|
|
835
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
602
836
|
}
|
|
603
|
-
if (!token) {
|
|
837
|
+
if (!token || token.error) {
|
|
604
838
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
605
839
|
const url = new URL(signInUrl, req.url);
|
|
606
840
|
url.searchParams.set("callbackUrl", pathname);
|
|
607
841
|
return import_server.NextResponse.redirect(url);
|
|
608
842
|
}
|
|
609
843
|
const refreshConfig = resolveRefreshConfig(options, token);
|
|
844
|
+
const denied = () => {
|
|
845
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
846
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
847
|
+
const response = import_server.NextResponse.redirect(url);
|
|
848
|
+
for (const cookie of req.cookies.getAll()) {
|
|
849
|
+
if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
|
|
850
|
+
}
|
|
851
|
+
return response;
|
|
852
|
+
};
|
|
853
|
+
const verifyAndAuthorize = async (current, resolved) => {
|
|
854
|
+
const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
|
|
855
|
+
if (!user) return false;
|
|
856
|
+
current.groups = user.groups ?? [];
|
|
857
|
+
current.email = user.email;
|
|
858
|
+
current.name = user.name;
|
|
859
|
+
current.username = user.preferred_username ?? user.username;
|
|
860
|
+
current.emailVerified = user.email_verified;
|
|
861
|
+
return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
|
|
862
|
+
};
|
|
610
863
|
if (isBackendSessionMode(options.sessionStore)) {
|
|
611
864
|
const sessionId = token.oauth42SessionId;
|
|
612
|
-
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
|
|
865
|
+
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
|
|
613
866
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
614
867
|
const url = new URL(signInUrl, req.url);
|
|
615
868
|
url.searchParams.set("callbackUrl", pathname);
|
|
@@ -618,9 +871,11 @@ function withOAuth42Auth(options = {}) {
|
|
|
618
871
|
return response;
|
|
619
872
|
}
|
|
620
873
|
const backendSession = await resolveBackendSession({
|
|
874
|
+
dpop: options.dpop,
|
|
875
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
621
876
|
issuer: refreshConfig.issuer,
|
|
622
877
|
clientId: refreshConfig.clientId,
|
|
623
|
-
clientSecret: refreshConfig.clientSecret,
|
|
878
|
+
clientSecret: refreshConfig.clientSecret || "",
|
|
624
879
|
sessionId
|
|
625
880
|
}).catch((error) => {
|
|
626
881
|
console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
|
|
@@ -634,8 +889,9 @@ function withOAuth42Auth(options = {}) {
|
|
|
634
889
|
response.cookies.delete(cookieName);
|
|
635
890
|
return response;
|
|
636
891
|
}
|
|
637
|
-
const
|
|
638
|
-
|
|
892
|
+
const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
|
|
893
|
+
if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
|
|
894
|
+
const requestHeaders = forwardedHeaders(req);
|
|
639
895
|
return import_server.NextResponse.next({
|
|
640
896
|
request: {
|
|
641
897
|
headers: requestHeaders
|
|
@@ -651,13 +907,12 @@ function withOAuth42Auth(options = {}) {
|
|
|
651
907
|
expiresAt,
|
|
652
908
|
now,
|
|
653
909
|
expiredAgo: expiresAt ? now - expiresAt : "n/a",
|
|
654
|
-
accessTokenPrefix: token.accessToken?.substring(0, 20),
|
|
655
910
|
path: pathname
|
|
656
911
|
});
|
|
657
912
|
}
|
|
658
|
-
if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
|
|
659
|
-
const
|
|
660
|
-
let refreshPromise = pendingRefreshes.get(
|
|
913
|
+
if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
|
|
914
|
+
const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
|
|
915
|
+
let refreshPromise = pendingRefreshes.get(grantKey);
|
|
661
916
|
if (refreshPromise) {
|
|
662
917
|
console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
|
|
663
918
|
} else {
|
|
@@ -665,15 +920,17 @@ function withOAuth42Auth(options = {}) {
|
|
|
665
920
|
refreshPromise = refreshTokens(
|
|
666
921
|
token.refreshToken,
|
|
667
922
|
refreshConfig.clientId,
|
|
668
|
-
refreshConfig.clientSecret,
|
|
669
|
-
refreshConfig.issuer
|
|
923
|
+
refreshConfig.clientSecret || "",
|
|
924
|
+
refreshConfig.issuer,
|
|
925
|
+
options.dpop,
|
|
926
|
+
options.clientAssertionKey
|
|
670
927
|
).finally(() => {
|
|
671
|
-
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(
|
|
928
|
+
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
|
|
672
929
|
if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
|
|
673
930
|
cleanupTimer.unref();
|
|
674
931
|
}
|
|
675
932
|
});
|
|
676
|
-
pendingRefreshes.set(
|
|
933
|
+
pendingRefreshes.set(grantKey, refreshPromise);
|
|
677
934
|
}
|
|
678
935
|
const refreshed = await refreshPromise;
|
|
679
936
|
if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
|
|
@@ -683,12 +940,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
683
940
|
refreshToken: refreshed.refreshToken,
|
|
684
941
|
expiresAt: refreshed.expiresAt
|
|
685
942
|
};
|
|
943
|
+
if (options.clientAssertionKey) delete updatedToken.clientSecret;
|
|
944
|
+
if (!await verifyAndAuthorize(updatedToken)) return denied();
|
|
686
945
|
const newJwt = await (0, import_jwt.encode)({
|
|
687
946
|
token: updatedToken,
|
|
688
947
|
secret
|
|
689
948
|
});
|
|
690
|
-
const requestHeaders =
|
|
691
|
-
requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
|
|
949
|
+
const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
|
|
692
950
|
const response = import_server.NextResponse.next({
|
|
693
951
|
request: {
|
|
694
952
|
headers: requestHeaders
|
|
@@ -741,28 +999,11 @@ function withOAuth42Auth(options = {}) {
|
|
|
741
999
|
chunked: newJwt.length > CHUNK_SIZE,
|
|
742
1000
|
existingBaseCooke: req.cookies.has(cookieName),
|
|
743
1001
|
existingChunks: existingChunks.length > 0 ? existingChunks : "none",
|
|
744
|
-
newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
|
|
745
1002
|
newExpiresAt: refreshed.expiresAt,
|
|
746
1003
|
cookieName
|
|
747
1004
|
});
|
|
748
1005
|
return response;
|
|
749
1006
|
} else {
|
|
750
|
-
if (shouldPreserveSessionOnRefreshFailure({
|
|
751
|
-
error: refreshed.error,
|
|
752
|
-
expiresAt,
|
|
753
|
-
now
|
|
754
|
-
})) {
|
|
755
|
-
console.warn(
|
|
756
|
-
"[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
|
|
757
|
-
{
|
|
758
|
-
expiresAt,
|
|
759
|
-
now,
|
|
760
|
-
expiredAgo: now - expiresAt,
|
|
761
|
-
path: pathname
|
|
762
|
-
}
|
|
763
|
-
);
|
|
764
|
-
return import_server.NextResponse.next();
|
|
765
|
-
}
|
|
766
1007
|
console.log("[OAuth42 Middleware] Refresh failed, clearing session");
|
|
767
1008
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
768
1009
|
const url = new URL(signInUrl, req.url);
|
|
@@ -780,16 +1021,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
780
1021
|
return response;
|
|
781
1022
|
}
|
|
782
1023
|
}
|
|
783
|
-
if (
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const url = new URL(signInUrl, req.url);
|
|
788
|
-
url.searchParams.set("callbackUrl", pathname);
|
|
789
|
-
return import_server.NextResponse.redirect(url);
|
|
790
|
-
}
|
|
1024
|
+
if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
|
|
1025
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
1026
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
1027
|
+
return import_server.NextResponse.redirect(url);
|
|
791
1028
|
}
|
|
792
|
-
return
|
|
1029
|
+
if (!await verifyAndAuthorize(token)) return denied();
|
|
1030
|
+
return import_server.NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
793
1031
|
};
|
|
794
1032
|
}
|
|
795
1033
|
function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
|
|
@@ -811,7 +1049,7 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
|
|
|
811
1049
|
|
|
812
1050
|
// src/server/hosted-auth-callback.ts
|
|
813
1051
|
var import_server2 = require("next/server");
|
|
814
|
-
var
|
|
1052
|
+
var import_crypto = require("crypto");
|
|
815
1053
|
var import_jwt2 = require("next-auth/jwt");
|
|
816
1054
|
function createHostedAuthCallback(options = {}) {
|
|
817
1055
|
return async function GET(request) {
|
|
@@ -824,7 +1062,7 @@ function createHostedAuthCallback(options = {}) {
|
|
|
824
1062
|
redirectUrl = "/dashboard",
|
|
825
1063
|
errorUrl = "/"
|
|
826
1064
|
} = options;
|
|
827
|
-
if (!clientId || !clientSecret) {
|
|
1065
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
828
1066
|
console.error("OAuth42 client credentials are required");
|
|
829
1067
|
return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=missing_credentials`, request.url));
|
|
830
1068
|
}
|
|
@@ -834,6 +1072,14 @@ function createHostedAuthCallback(options = {}) {
|
|
|
834
1072
|
}
|
|
835
1073
|
const searchParams = request.nextUrl.searchParams;
|
|
836
1074
|
const code = searchParams.get("code");
|
|
1075
|
+
const cookiePrefix = process.env.NODE_ENV === "production" ? "__Host-" : request.nextUrl.protocol === "https:" ? "__Host-" : "";
|
|
1076
|
+
const expectedState = request.cookies.get(`${cookiePrefix}oauth42-state`)?.value;
|
|
1077
|
+
const returnedState = searchParams.get("state");
|
|
1078
|
+
const verifier = request.cookies.get(`${cookiePrefix}oauth42-pkce`)?.value;
|
|
1079
|
+
const validState = expectedState && returnedState && expectedState.length >= 32 && Buffer.byteLength(expectedState) === Buffer.byteLength(returnedState) && (0, import_crypto.timingSafeEqual)(Buffer.from(expectedState), Buffer.from(returnedState));
|
|
1080
|
+
if (!validState || !verifier || !/^[A-Za-z0-9._~-]{43,128}$/.test(verifier)) {
|
|
1081
|
+
return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=invalid_state`, request.url));
|
|
1082
|
+
}
|
|
837
1083
|
if (!code) {
|
|
838
1084
|
return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=missing_code`, request.url));
|
|
839
1085
|
}
|
|
@@ -841,39 +1087,38 @@ function createHostedAuthCallback(options = {}) {
|
|
|
841
1087
|
const tokenEndpoint = `${issuer}/oauth2/token`;
|
|
842
1088
|
const body = new URLSearchParams({
|
|
843
1089
|
grant_type: "authorization_code",
|
|
1090
|
+
code_verifier: verifier,
|
|
844
1091
|
code,
|
|
845
1092
|
redirect_uri: `${baseUrl}/api/oauth/callback`,
|
|
846
1093
|
client_id: clientId,
|
|
847
|
-
client_secret: clientSecret
|
|
1094
|
+
client_secret: clientSecret || ""
|
|
848
1095
|
});
|
|
849
1096
|
const fetchOptions = {
|
|
850
1097
|
method: "POST",
|
|
1098
|
+
redirect: "error",
|
|
1099
|
+
cache: "no-store",
|
|
1100
|
+
signal: AbortSignal.timeout(3e4),
|
|
851
1101
|
headers: {
|
|
852
1102
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
853
1103
|
},
|
|
854
1104
|
body: body.toString()
|
|
855
1105
|
};
|
|
856
|
-
|
|
857
|
-
const agent = new import_https.default.Agent({
|
|
858
|
-
rejectUnauthorized: false
|
|
859
|
-
});
|
|
860
|
-
fetchOptions.agent = agent;
|
|
861
|
-
}
|
|
862
|
-
const tokenResponse = await fetch(tokenEndpoint, fetchOptions);
|
|
1106
|
+
const tokenResponse = options.clientAssertionKey ? await options.clientAssertionKey.fetch(clientId, tokenEndpoint, fetchOptions, options.dpop) : options.dpop ? await options.dpop.fetch(tokenEndpoint, fetchOptions) : await fetch(tokenEndpoint, fetchOptions);
|
|
863
1107
|
const tokens = await tokenResponse.json();
|
|
864
1108
|
if (!tokenResponse.ok) {
|
|
865
|
-
console.error("Token exchange failed:",
|
|
1109
|
+
console.error("Token exchange failed:", tokenResponse.status);
|
|
866
1110
|
return import_server2.NextResponse.redirect(new URL(`${errorUrl}?error=token_exchange_failed`, request.url));
|
|
867
1111
|
}
|
|
1112
|
+
options.dpop?.assertTokenResponse(tokens);
|
|
868
1113
|
const userInfoEndpoint = `${issuer}/oauth2/userinfo`;
|
|
869
|
-
const
|
|
870
|
-
headers: {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1114
|
+
const infoOptions = {
|
|
1115
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
|
1116
|
+
redirect: "error",
|
|
1117
|
+
cache: "no-store",
|
|
1118
|
+
signal: AbortSignal.timeout(3e4)
|
|
1119
|
+
};
|
|
1120
|
+
const userInfoResponse = options.dpop ? await options.dpop.fetch(userInfoEndpoint, infoOptions, tokens.access_token) : await fetch(userInfoEndpoint, infoOptions);
|
|
1121
|
+
if (!userInfoResponse.ok) throw new Error("User info request failed");
|
|
877
1122
|
const userInfo = await userInfoResponse.json();
|
|
878
1123
|
const sessionToken = await (0, import_jwt2.encode)({
|
|
879
1124
|
token: {
|
|
@@ -898,6 +1143,14 @@ function createHostedAuthCallback(options = {}) {
|
|
|
898
1143
|
// 30 days
|
|
899
1144
|
path: "/"
|
|
900
1145
|
});
|
|
1146
|
+
for (const name of ["oauth42-state", "oauth42-pkce"]) {
|
|
1147
|
+
redirectResponse.cookies.set(`${cookiePrefix}${name}`, "", {
|
|
1148
|
+
path: "/",
|
|
1149
|
+
secure: cookiePrefix === "__Host-",
|
|
1150
|
+
sameSite: "lax",
|
|
1151
|
+
maxAge: 0
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
901
1154
|
return redirectResponse;
|
|
902
1155
|
} catch (error) {
|
|
903
1156
|
console.error("OAuth callback error:", error);
|
|
@@ -905,9 +1158,79 @@ function createHostedAuthCallback(options = {}) {
|
|
|
905
1158
|
}
|
|
906
1159
|
};
|
|
907
1160
|
}
|
|
1161
|
+
|
|
1162
|
+
// src/server/dpop.ts
|
|
1163
|
+
var import_node_crypto2 = require("crypto");
|
|
1164
|
+
var OAuth42DPoP = class _OAuth42DPoP {
|
|
1165
|
+
constructor(key) {
|
|
1166
|
+
this.nonces = /* @__PURE__ */ new Map();
|
|
1167
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
1168
|
+
throw new Error("DPoP requires a private P-256 key");
|
|
1169
|
+
}
|
|
1170
|
+
this.key = key;
|
|
1171
|
+
const publicKey = (0, import_node_crypto2.createPublicKey)(key).export({ format: "jwk" });
|
|
1172
|
+
this.jwk = { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y };
|
|
1173
|
+
this.thumbprint = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(this.jwk)).digest("base64url");
|
|
1174
|
+
}
|
|
1175
|
+
/** Persist keys in protected server storage; regeneration cannot recover a public refresh grant. */
|
|
1176
|
+
static generate() {
|
|
1177
|
+
return new _OAuth42DPoP((0, import_node_crypto2.generateKeyPairSync)("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
1178
|
+
}
|
|
1179
|
+
/** Passed directly to openid-client so its callback keeps OIDC issuer/nonce/signature validation. */
|
|
1180
|
+
get oidcKey() {
|
|
1181
|
+
return this.key;
|
|
1182
|
+
}
|
|
1183
|
+
proof(method, target, nonce, accessToken) {
|
|
1184
|
+
const url = new URL(target);
|
|
1185
|
+
if (!["http:", "https:"].includes(url.protocol) || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
1186
|
+
throw new Error("DPoP requires HTTPS outside loopback development");
|
|
1187
|
+
}
|
|
1188
|
+
if (url.username || url.password) throw new Error("DPoP endpoint must not contain credentials");
|
|
1189
|
+
url.search = "";
|
|
1190
|
+
url.hash = "";
|
|
1191
|
+
const header = { typ: "dpop+jwt", alg: "ES256", jwk: this.jwk };
|
|
1192
|
+
const claims = {
|
|
1193
|
+
htm: method.toUpperCase(),
|
|
1194
|
+
htu: url.href,
|
|
1195
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
1196
|
+
jti: (0, import_node_crypto2.randomBytes)(32).toString("base64url"),
|
|
1197
|
+
...nonce ? { nonce } : {},
|
|
1198
|
+
...accessToken !== void 0 ? { ath: (0, import_node_crypto2.createHash)("sha256").update(accessToken).digest("base64url") } : {}
|
|
1199
|
+
};
|
|
1200
|
+
const input = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
|
|
1201
|
+
return `${input}.${(0, import_node_crypto2.sign)("sha256", Buffer.from(input), { key: this.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
1202
|
+
}
|
|
1203
|
+
assertTokenResponse(tokens) {
|
|
1204
|
+
if (typeof tokens.token_type !== "string" || tokens.token_type.toLowerCase() !== "dpop") {
|
|
1205
|
+
throw new Error("Server did not issue the requested DPoP-bound token");
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async fetch(target, init = {}, accessToken, prepare) {
|
|
1209
|
+
if (init.body instanceof ReadableStream) throw new Error("DPoP nonce retries require a replayable request body");
|
|
1210
|
+
const cacheKey = `${new URL(target).origin}:${accessToken !== void 0 ? "resource" : "token"}`;
|
|
1211
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1212
|
+
const request = prepare ? prepare() : init;
|
|
1213
|
+
const headers = new Headers(request.headers);
|
|
1214
|
+
headers.set("DPoP", this.proof(request.method || "GET", target, this.nonces.get(cacheKey), accessToken));
|
|
1215
|
+
if (accessToken !== void 0) headers.set("Authorization", `DPoP ${accessToken}`);
|
|
1216
|
+
const response = await fetch(target, { ...request, headers, redirect: "error", cache: "no-store", signal: request.signal || AbortSignal.timeout(3e4) });
|
|
1217
|
+
const nonce = response.headers.get("DPoP-Nonce");
|
|
1218
|
+
if (nonce && /^[\x21-\x7e]{1,128}$/.test(nonce)) {
|
|
1219
|
+
if (this.nonces.size >= 256) this.nonces.clear();
|
|
1220
|
+
this.nonces.set(cacheKey, nonce);
|
|
1221
|
+
} else return response;
|
|
1222
|
+
if (attempt || ![400, 401].includes(response.status)) return response;
|
|
1223
|
+
const error = await response.clone().json().catch(() => null);
|
|
1224
|
+
if (error?.error !== "use_dpop_nonce") return response;
|
|
1225
|
+
}
|
|
1226
|
+
throw new Error("DPoP nonce negotiation failed");
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
908
1229
|
// Annotate the CommonJS export names for ESM import in node:
|
|
909
1230
|
0 && (module.exports = {
|
|
910
1231
|
NextAuth,
|
|
1232
|
+
OAuth42ClientAssertion,
|
|
1233
|
+
OAuth42DPoP,
|
|
911
1234
|
OAuth42Provider,
|
|
912
1235
|
createAuth,
|
|
913
1236
|
createBackendSession,
|
|
@@ -916,11 +1239,13 @@ function createHostedAuthCallback(options = {}) {
|
|
|
916
1239
|
createMiddlewareConfig,
|
|
917
1240
|
getOAuth42Session,
|
|
918
1241
|
getServerSession,
|
|
1242
|
+
pushAuthorizationURL,
|
|
919
1243
|
refreshAccessToken,
|
|
920
1244
|
resolveBackendSession,
|
|
921
1245
|
revokeBackendSession,
|
|
922
1246
|
withOAuth42Auth,
|
|
923
1247
|
withOAuth42ServerSideProps,
|
|
924
|
-
withOAuth42Session
|
|
1248
|
+
withOAuth42Session,
|
|
1249
|
+
withPushedAuthorization
|
|
925
1250
|
});
|
|
926
1251
|
//# sourceMappingURL=index.js.map
|