@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/server/index.mjs
CHANGED
|
@@ -1,3 +1,190 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
+
|
|
9
|
+
// src/server/client-assertion.ts
|
|
10
|
+
import { createHash, createPublicKey, generateKeyPairSync, randomBytes, sign } from "crypto";
|
|
11
|
+
var _key, _public;
|
|
12
|
+
var _OAuth42ClientAssertion = class _OAuth42ClientAssertion {
|
|
13
|
+
constructor(kid, key) {
|
|
14
|
+
__privateAdd(this, _key);
|
|
15
|
+
__privateAdd(this, _public);
|
|
16
|
+
if (typeof kid !== "string" || !/^[\x21-\x7e]{1,128}$/.test(kid)) throw new Error("Invalid client signing key ID");
|
|
17
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
18
|
+
throw new Error("Client assertions require a private P-256 key");
|
|
19
|
+
}
|
|
20
|
+
__privateSet(this, _key, key);
|
|
21
|
+
this.kid = kid;
|
|
22
|
+
const publicKey = createPublicKey(key).export({ format: "jwk" });
|
|
23
|
+
__privateSet(this, _public, { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y });
|
|
24
|
+
this.thumbprint = createHash("sha256").update(JSON.stringify(__privateGet(this, _public))).digest("base64url");
|
|
25
|
+
}
|
|
26
|
+
static generate(kid) {
|
|
27
|
+
return new _OAuth42ClientAssertion(kid, generateKeyPairSync("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
28
|
+
}
|
|
29
|
+
get publicJwk() {
|
|
30
|
+
return { ...__privateGet(this, _public), kid: this.kid };
|
|
31
|
+
}
|
|
32
|
+
/** Private keys for the server-only NextAuth/openid-client configuration, never registration. */
|
|
33
|
+
get oidcJwks() {
|
|
34
|
+
return { keys: [{
|
|
35
|
+
kty: "EC",
|
|
36
|
+
crv: "P-256",
|
|
37
|
+
x: __privateGet(this, _public).x,
|
|
38
|
+
y: __privateGet(this, _public).y,
|
|
39
|
+
d: __privateGet(this, _key).export({ format: "jwk" }).d,
|
|
40
|
+
kid: this.kid
|
|
41
|
+
}] };
|
|
42
|
+
}
|
|
43
|
+
static validateEndpoint(target) {
|
|
44
|
+
const url = new URL(target);
|
|
45
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
46
|
+
throw new Error("Client authentication requires HTTPS outside loopback development");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
assertion(clientId, audience) {
|
|
50
|
+
_OAuth42ClientAssertion.validateEndpoint(audience);
|
|
51
|
+
if (!clientId || Buffer.byteLength(clientId) > 256) throw new Error("Invalid client identity");
|
|
52
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
53
|
+
const header = { alg: "ES256", typ: "JWT", kid: this.kid };
|
|
54
|
+
const payload = { iss: clientId, sub: clientId, aud: audience, iat: now, exp: now + 60, jti: randomBytes(32).toString("base64url") };
|
|
55
|
+
const data = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
|
|
56
|
+
return `${data}.${sign("sha256", Buffer.from(data), { key: __privateGet(this, _key), dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
57
|
+
}
|
|
58
|
+
async fetch(clientId, target, init, dpop) {
|
|
59
|
+
if (init.method?.toUpperCase() !== "POST" || !(typeof init.body === "string" || init.body instanceof URLSearchParams)) {
|
|
60
|
+
throw new Error("Client assertions require a form POST");
|
|
61
|
+
}
|
|
62
|
+
const prepare = () => {
|
|
63
|
+
const body = new URLSearchParams(init.body);
|
|
64
|
+
body.delete("client_secret");
|
|
65
|
+
body.set("client_id", clientId);
|
|
66
|
+
body.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
|
67
|
+
body.set("client_assertion", this.assertion(clientId, target));
|
|
68
|
+
const headers = new Headers(init.headers);
|
|
69
|
+
headers.delete("Authorization");
|
|
70
|
+
headers.delete("Content-Length");
|
|
71
|
+
headers.delete("Transfer-Encoding");
|
|
72
|
+
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
|
73
|
+
return { ...init, body, headers, redirect: "error", cache: "no-store", signal: init.signal || AbortSignal.timeout(3e4) };
|
|
74
|
+
};
|
|
75
|
+
return dpop ? dpop.fetch(target, init, void 0, prepare) : fetch(target, prepare());
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
_key = new WeakMap();
|
|
79
|
+
_public = new WeakMap();
|
|
80
|
+
var OAuth42ClientAssertion = _OAuth42ClientAssertion;
|
|
81
|
+
|
|
82
|
+
// src/server/par.ts
|
|
83
|
+
async function pushAuthorizationURL(value, config) {
|
|
84
|
+
const base = config.issuer.replace(/\/$/, "");
|
|
85
|
+
let url;
|
|
86
|
+
try {
|
|
87
|
+
url = new URL(value);
|
|
88
|
+
} catch {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
if (`${url.origin}${url.pathname}` !== `${base}/oauth2/authorize`) return value;
|
|
92
|
+
if (url.username || url.password || url.hash) throw new Error("Invalid authorization endpoint");
|
|
93
|
+
const body = new URLSearchParams(url.search);
|
|
94
|
+
if (body.getAll("client_id").length !== 1 || body.get("client_id") !== config.clientId || body.get("response_type") !== "code" || !body.get("state") || !body.get("nonce") || body.get("code_challenge_method") !== "S256" || !/^[A-Za-z0-9_-]{43}$/.test(body.get("code_challenge") || "") || ["request_uri", "client_secret", "client_assertion", "client_assertion_type"].some((k) => body.has(k))) {
|
|
95
|
+
throw new Error("Invalid PAR authorization transaction");
|
|
96
|
+
}
|
|
97
|
+
const target = `${base}/oauth2/par`;
|
|
98
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
99
|
+
if (!config.clientAssertionKey) body.set("client_secret", config.clientSecret || "");
|
|
100
|
+
const init = {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
103
|
+
body,
|
|
104
|
+
redirect: "error",
|
|
105
|
+
cache: "no-store",
|
|
106
|
+
signal: AbortSignal.timeout(3e4)
|
|
107
|
+
};
|
|
108
|
+
const response = config.clientAssertionKey ? await config.clientAssertionKey.fetch(config.clientId, target, init, config.dpop) : config.dpop ? await config.dpop.fetch(target, init) : await fetch(target, init);
|
|
109
|
+
if (response.status !== 201) throw new Error("Pushed authorization failed");
|
|
110
|
+
const pushed = await response.json();
|
|
111
|
+
if (typeof pushed.request_uri !== "string" || !/^urn:ietf:params:oauth:request_uri:[A-Za-z0-9_-]{43}$/.test(pushed.request_uri) || !Number.isInteger(pushed.expires_in) || pushed.expires_in <= 0 || pushed.expires_in > 600) throw new Error("Invalid PAR response");
|
|
112
|
+
url.search = new URLSearchParams({ client_id: config.clientId, request_uri: pushed.request_uri }).toString();
|
|
113
|
+
return url.href;
|
|
114
|
+
}
|
|
115
|
+
function withPushedAuthorization(handler, config) {
|
|
116
|
+
return async (request, context) => {
|
|
117
|
+
if (!(request instanceof Request)) throw new Error("PAR handlers require Next.js App Router Request/Response");
|
|
118
|
+
const response = await handler(request, context);
|
|
119
|
+
const location = response.headers.get("Location");
|
|
120
|
+
if (location) {
|
|
121
|
+
const next = await pushAuthorizationURL(location, config);
|
|
122
|
+
if (next === location) return response;
|
|
123
|
+
const headers = new Headers(response.headers);
|
|
124
|
+
headers.set("Location", next);
|
|
125
|
+
headers.delete("content-length");
|
|
126
|
+
return new Response(response.body, { status: response.status, headers });
|
|
127
|
+
}
|
|
128
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
129
|
+
const body = await response.clone().json();
|
|
130
|
+
if (typeof body?.url === "string") {
|
|
131
|
+
const next = await pushAuthorizationURL(body.url, config);
|
|
132
|
+
if (next !== body.url) {
|
|
133
|
+
const headers = new Headers(response.headers);
|
|
134
|
+
headers.delete("content-length");
|
|
135
|
+
return new Response(JSON.stringify({ ...body, url: next }), { status: response.status, headers });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return response;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/server/verified-userinfo.ts
|
|
144
|
+
async function verifiedUserInfo(issuer, accessToken, subject, dpop) {
|
|
145
|
+
if (typeof accessToken !== "string" || !accessToken || typeof subject !== "string" || !subject) return null;
|
|
146
|
+
try {
|
|
147
|
+
const target = `${issuer.replace(/\/$/, "")}/oauth2/userinfo`;
|
|
148
|
+
const fetcher = dpop ? (url, init) => dpop.fetch(url, init, accessToken) : fetch;
|
|
149
|
+
const response = await fetcher(target, {
|
|
150
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
151
|
+
redirect: "error",
|
|
152
|
+
cache: "no-store",
|
|
153
|
+
signal: AbortSignal.timeout(3e4)
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) return null;
|
|
156
|
+
const user = await response.json();
|
|
157
|
+
return parseVerifiedUserInfo(user, subject);
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function parseVerifiedUserInfo(value, subject) {
|
|
163
|
+
if (typeof subject !== "string" || !subject || !value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
164
|
+
const user = value;
|
|
165
|
+
if (user.sub !== subject) return null;
|
|
166
|
+
if (user.groups !== void 0 && (!Array.isArray(user.groups) || user.groups.some((group) => typeof group !== "string"))) return null;
|
|
167
|
+
if (user.email !== void 0 && typeof user.email !== "string") return null;
|
|
168
|
+
if (user.email_verified !== void 0 && typeof user.email_verified !== "boolean") return null;
|
|
169
|
+
const profile = { sub: user.sub };
|
|
170
|
+
for (const field of ["email", "name", "preferred_username", "username", "given_name", "family_name", "company_id", "company_name"]) {
|
|
171
|
+
if (user[field] != null) {
|
|
172
|
+
if (typeof user[field] !== "string") return null;
|
|
173
|
+
profile[field] = user[field];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (user.email_verified !== void 0) profile.email_verified = user.email_verified;
|
|
177
|
+
if (user.groups !== void 0) profile.groups = user.groups;
|
|
178
|
+
return profile;
|
|
179
|
+
}
|
|
180
|
+
async function verifiedBackendSessionUserInfo(issuer, session, subject, dpop) {
|
|
181
|
+
if (typeof session.access_token !== "string" || !session.access_token) return null;
|
|
182
|
+
if (Object.prototype.hasOwnProperty.call(session, "user_info")) {
|
|
183
|
+
return parseVerifiedUserInfo(session.user_info, subject);
|
|
184
|
+
}
|
|
185
|
+
return verifiedUserInfo(issuer, session.access_token, subject, dpop);
|
|
186
|
+
}
|
|
187
|
+
|
|
1
188
|
// src/server/auth.ts
|
|
2
189
|
import NextAuthDefault from "next-auth";
|
|
3
190
|
|
|
@@ -5,13 +192,20 @@ import NextAuthDefault from "next-auth";
|
|
|
5
192
|
function OAuth42Provider(options) {
|
|
6
193
|
const issuer = options.issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
7
194
|
const baseUrl = issuer.replace(/\/$/, "");
|
|
195
|
+
const { dpop, clientAssertionKey, ...providerOptions } = options;
|
|
196
|
+
if (clientAssertionKey) {
|
|
197
|
+
OAuth42ClientAssertion.validateEndpoint(`${baseUrl}/oauth2/token`);
|
|
198
|
+
providerOptions.clientSecret = void 0;
|
|
199
|
+
}
|
|
200
|
+
if (options.pkceEnabled === false) throw new Error("OAuth42 requires S256 PKCE");
|
|
8
201
|
return {
|
|
9
202
|
id: "oauth42",
|
|
10
203
|
name: "OAuth42",
|
|
11
204
|
type: "oauth",
|
|
12
205
|
version: "2.0",
|
|
13
|
-
//
|
|
14
|
-
|
|
206
|
+
// Pin O42 metadata: NextAuth v4 discovery does not compare the returned issuer.
|
|
207
|
+
// Constructing its OIDC client from these trusted endpoints preserves issuer binding.
|
|
208
|
+
jwks_endpoint: `${baseUrl}/.well-known/jwks.json`,
|
|
15
209
|
// Use ID token for user profile (standard OIDC pattern)
|
|
16
210
|
// This avoids an extra HTTP call to /userinfo on every auth flow
|
|
17
211
|
// Apps can call /userinfo separately when they need extended claims (groups, company_id, etc.)
|
|
@@ -24,17 +218,26 @@ function OAuth42Provider(options) {
|
|
|
24
218
|
response_type: "code"
|
|
25
219
|
}
|
|
26
220
|
},
|
|
27
|
-
token:
|
|
221
|
+
token: dpop ? {
|
|
222
|
+
url: `${baseUrl}/oauth2/token`,
|
|
223
|
+
async request({ client, provider, params, checks }) {
|
|
224
|
+
const tokens = await client.callback(provider.callbackUrl, params, checks, { DPoP: dpop.oidcKey });
|
|
225
|
+
dpop.assertTokenResponse(tokens);
|
|
226
|
+
return { tokens };
|
|
227
|
+
}
|
|
228
|
+
} : `${baseUrl}/oauth2/token`,
|
|
28
229
|
userinfo: `${baseUrl}/oauth2/userinfo`,
|
|
230
|
+
...clientAssertionKey ? { jwks: clientAssertionKey.oidcJwks } : {},
|
|
29
231
|
client: {
|
|
30
232
|
id: options.clientId,
|
|
31
|
-
secret: options.clientSecret,
|
|
32
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
233
|
+
secret: clientAssertionKey ? void 0 : options.clientSecret,
|
|
234
|
+
token_endpoint_auth_method: clientAssertionKey ? "private_key_jwt" : "client_secret_post",
|
|
235
|
+
...clientAssertionKey ? { token_endpoint_auth_signing_alg: "ES256" } : {},
|
|
33
236
|
id_token_signed_response_alg: "RS256"
|
|
34
237
|
// OAuth42 uses RS256 for ID tokens
|
|
35
238
|
},
|
|
36
|
-
issuer
|
|
37
|
-
checks:
|
|
239
|
+
issuer,
|
|
240
|
+
checks: ["pkce", "state", "nonce"],
|
|
38
241
|
profile(profile, tokens) {
|
|
39
242
|
return {
|
|
40
243
|
id: profile.sub || profile.id || profile.email,
|
|
@@ -50,42 +253,18 @@ function OAuth42Provider(options) {
|
|
|
50
253
|
bg: "#1e40af",
|
|
51
254
|
text: "#ffffff"
|
|
52
255
|
},
|
|
53
|
-
|
|
256
|
+
// NextAuth v4 types require a string even when private_key_jwt sends no secret.
|
|
257
|
+
options: { ...providerOptions, clientSecret: providerOptions.clientSecret || "" }
|
|
54
258
|
};
|
|
55
259
|
}
|
|
56
260
|
|
|
57
261
|
// src/server/session.ts
|
|
58
262
|
import { getServerSession as getNextAuthSession } from "next-auth";
|
|
59
|
-
import { headers } from "next/headers";
|
|
60
|
-
var REFRESHED_TOKEN_HEADER = "x-oauth42-refreshed-token";
|
|
61
263
|
async function getOAuth42Session(...args) {
|
|
62
264
|
const session = await getNextAuthSession(...args);
|
|
63
|
-
if (!session) {
|
|
265
|
+
if (!session || session.error) {
|
|
64
266
|
return null;
|
|
65
267
|
}
|
|
66
|
-
let refreshedToken = null;
|
|
67
|
-
try {
|
|
68
|
-
if (args.length === 1) {
|
|
69
|
-
const headersList = await headers();
|
|
70
|
-
refreshedToken = headersList.get(REFRESHED_TOKEN_HEADER);
|
|
71
|
-
} else if (args.length === 3) {
|
|
72
|
-
const req = args[0];
|
|
73
|
-
refreshedToken = req.headers[REFRESHED_TOKEN_HEADER] || null;
|
|
74
|
-
}
|
|
75
|
-
} catch {
|
|
76
|
-
}
|
|
77
|
-
if (refreshedToken) {
|
|
78
|
-
const sessionToken = session.accessToken;
|
|
79
|
-
console.log("[OAuth42 Session] Using refreshed token from middleware:", {
|
|
80
|
-
refreshedPrefix: refreshedToken.substring(0, 20),
|
|
81
|
-
sessionPrefix: sessionToken?.substring(0, 20),
|
|
82
|
-
tokensMatch: refreshedToken === sessionToken
|
|
83
|
-
});
|
|
84
|
-
return {
|
|
85
|
-
...session,
|
|
86
|
-
accessToken: refreshedToken
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
268
|
return session;
|
|
90
269
|
}
|
|
91
270
|
function withOAuth42Session(handler, authOptions) {
|
|
@@ -116,13 +295,66 @@ function withOAuth42ServerSideProps(getServerSideProps, authOptions) {
|
|
|
116
295
|
};
|
|
117
296
|
}
|
|
118
297
|
|
|
298
|
+
// src/server/backend-session.ts
|
|
299
|
+
function isBackendSessionMode(mode) {
|
|
300
|
+
return mode === "backend";
|
|
301
|
+
}
|
|
302
|
+
function sessionEndpoint(issuer) {
|
|
303
|
+
return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
|
|
304
|
+
}
|
|
305
|
+
async function sessionFetch(params, target, body, credential, method = "POST") {
|
|
306
|
+
OAuth42ClientAssertion.validateEndpoint(target);
|
|
307
|
+
const prepare = () => ({
|
|
308
|
+
method,
|
|
309
|
+
redirect: "error",
|
|
310
|
+
cache: "no-store",
|
|
311
|
+
signal: AbortSignal.timeout(3e4),
|
|
312
|
+
headers: { "Content-Type": "application/json" },
|
|
313
|
+
body: JSON.stringify({
|
|
314
|
+
...body,
|
|
315
|
+
client_id: params.clientId,
|
|
316
|
+
...params.clientAssertionKey ? {
|
|
317
|
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
318
|
+
client_assertion: params.clientAssertionKey.assertion(params.clientId, target)
|
|
319
|
+
} : { client_secret: params.clientSecret }
|
|
320
|
+
})
|
|
321
|
+
});
|
|
322
|
+
return params.dpop ? params.dpop.fetch(target, {}, credential, prepare) : fetch(target, prepare());
|
|
323
|
+
}
|
|
324
|
+
async function sessionResponse(response, params) {
|
|
325
|
+
if (!response.ok) throw new Error(`OAuth42 backend session request failed: ${response.status}`);
|
|
326
|
+
const session = await response.json();
|
|
327
|
+
if (params.dpop) params.dpop.assertTokenResponse(session);
|
|
328
|
+
else if (session.token_type?.toLowerCase() === "dpop") throw new Error("Backend session requires its retained proof key");
|
|
329
|
+
return session;
|
|
330
|
+
}
|
|
331
|
+
async function createBackendSession(params) {
|
|
332
|
+
return sessionResponse(await sessionFetch(params, sessionEndpoint(params.issuer), {
|
|
333
|
+
access_token: params.accessToken,
|
|
334
|
+
refresh_token: params.refreshToken,
|
|
335
|
+
id_token: params.idToken,
|
|
336
|
+
user: params.user
|
|
337
|
+
}, params.accessToken), params);
|
|
338
|
+
}
|
|
339
|
+
async function resolveBackendSession(params) {
|
|
340
|
+
const response = await sessionFetch(params, `${sessionEndpoint(params.issuer)}/resolve`, { session_id: params.sessionId }, params.sessionId);
|
|
341
|
+
if ([401, 403, 404].includes(response.status)) return null;
|
|
342
|
+
return sessionResponse(response, params);
|
|
343
|
+
}
|
|
344
|
+
async function revokeBackendSession(params) {
|
|
345
|
+
const response = await sessionFetch(params, sessionEndpoint(params.issuer), { session_id: params.sessionId }, params.sessionId, "DELETE");
|
|
346
|
+
if (!response.ok) throw new Error(`OAuth42 backend session revoke failed: ${response.status}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
119
349
|
// src/server/auth.ts
|
|
120
350
|
var NextAuth = NextAuthDefault.default || NextAuthDefault;
|
|
121
|
-
var
|
|
351
|
+
var activeRefreshes = /* @__PURE__ */ new Map();
|
|
122
352
|
function createAuth(options = {}) {
|
|
123
353
|
const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
|
|
124
|
-
const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
125
|
-
|
|
354
|
+
const clientSecret = options.clientAssertionKey ? "" : options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
355
|
+
const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
356
|
+
const useBackendSessions = isBackendSessionMode(options.sessionStore);
|
|
357
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
126
358
|
throw new Error(
|
|
127
359
|
"OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
|
|
128
360
|
);
|
|
@@ -131,24 +363,57 @@ function createAuth(options = {}) {
|
|
|
131
363
|
providers: [
|
|
132
364
|
OAuth42Provider({
|
|
133
365
|
clientId,
|
|
134
|
-
clientSecret,
|
|
135
|
-
issuer
|
|
366
|
+
clientSecret: clientSecret || "",
|
|
367
|
+
issuer,
|
|
136
368
|
scopes: options.scopes,
|
|
137
|
-
pkceEnabled: options.pkceEnabled
|
|
369
|
+
pkceEnabled: options.pkceEnabled,
|
|
370
|
+
dpop: options.dpop,
|
|
371
|
+
clientAssertionKey: options.clientAssertionKey
|
|
138
372
|
})
|
|
139
373
|
],
|
|
140
374
|
callbacks: {
|
|
141
375
|
async jwt({ token, account, profile }) {
|
|
376
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
142
377
|
console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
|
|
143
378
|
if (account) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
379
|
+
if (useBackendSessions) {
|
|
380
|
+
if (!account.access_token || !account.refresh_token) {
|
|
381
|
+
throw new Error("OAuth42 backend sessions require access and refresh tokens");
|
|
382
|
+
}
|
|
383
|
+
const oauth42Profile = profile;
|
|
384
|
+
const backendSession = await createBackendSession({
|
|
385
|
+
dpop: options.dpop,
|
|
386
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
387
|
+
issuer,
|
|
388
|
+
clientId,
|
|
389
|
+
clientSecret: clientSecret || "",
|
|
390
|
+
accessToken: account.access_token,
|
|
391
|
+
refreshToken: account.refresh_token,
|
|
392
|
+
expiresAt: account.expires_at,
|
|
393
|
+
idToken: account.id_token,
|
|
394
|
+
user: {
|
|
395
|
+
sub: oauth42Profile?.sub,
|
|
396
|
+
email: oauth42Profile?.email,
|
|
397
|
+
username: oauth42Profile?.username,
|
|
398
|
+
name: token.name,
|
|
399
|
+
emailVerified: oauth42Profile?.email_verified,
|
|
400
|
+
groups: oauth42Profile?.groups
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
token.oauth42SessionId = backendSession.session_id;
|
|
404
|
+
token.expiresAt = backendSession.expires_at;
|
|
405
|
+
token.issuer = issuer;
|
|
406
|
+
} else {
|
|
407
|
+
console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
|
|
408
|
+
token.accessToken = account.access_token;
|
|
409
|
+
token.refreshToken = account.refresh_token;
|
|
410
|
+
token.expiresAt = account.expires_at;
|
|
411
|
+
token.idToken = account.id_token;
|
|
412
|
+
token.clientId = clientId;
|
|
413
|
+
if (options.clientAssertionKey) delete token.clientSecret;
|
|
414
|
+
else token.clientSecret = clientSecret;
|
|
415
|
+
token.issuer = issuer;
|
|
416
|
+
}
|
|
152
417
|
}
|
|
153
418
|
if (profile) {
|
|
154
419
|
const oauth42Profile = profile;
|
|
@@ -165,12 +430,68 @@ function createAuth(options = {}) {
|
|
|
165
430
|
},
|
|
166
431
|
async session({ session, token }) {
|
|
167
432
|
console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
|
|
168
|
-
|
|
169
|
-
|
|
433
|
+
const revokedSession = () => {
|
|
434
|
+
session.error = "SessionRevoked";
|
|
435
|
+
delete session.accessToken;
|
|
436
|
+
delete session.idToken;
|
|
437
|
+
delete session.userInfo;
|
|
438
|
+
delete session.user;
|
|
439
|
+
return session;
|
|
440
|
+
};
|
|
441
|
+
if (token.error) return revokedSession();
|
|
442
|
+
let verified;
|
|
443
|
+
if (useBackendSessions) {
|
|
444
|
+
const sessionId = token.oauth42SessionId;
|
|
445
|
+
if (!sessionId) {
|
|
446
|
+
return revokedSession();
|
|
447
|
+
}
|
|
448
|
+
const backendSession = await resolveBackendSession({
|
|
449
|
+
dpop: options.dpop,
|
|
450
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
451
|
+
issuer,
|
|
452
|
+
clientId,
|
|
453
|
+
clientSecret: clientSecret || "",
|
|
454
|
+
sessionId
|
|
455
|
+
});
|
|
456
|
+
if (!backendSession) {
|
|
457
|
+
return revokedSession();
|
|
458
|
+
}
|
|
459
|
+
session.accessToken = backendSession.access_token;
|
|
460
|
+
session.idToken = backendSession.id_token;
|
|
461
|
+
token.expiresAt = backendSession.expires_at;
|
|
462
|
+
verified = await verifiedBackendSessionUserInfo(issuer, backendSession, token.sub, options.dpop);
|
|
463
|
+
if (backendSession.user && session.user) {
|
|
464
|
+
session.user.email = backendSession.user.email;
|
|
465
|
+
session.user.name = backendSession.user.name;
|
|
466
|
+
session.user.username = backendSession.user.username;
|
|
467
|
+
session.user.emailVerified = backendSession.user.emailVerified;
|
|
468
|
+
session.user.groups = backendSession.user.groups;
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
session.accessToken = token.accessToken;
|
|
472
|
+
session.idToken = token.idToken;
|
|
473
|
+
verified = await verifiedUserInfo(issuer, session.accessToken, token.sub, options.dpop);
|
|
474
|
+
}
|
|
475
|
+
if (!verified) {
|
|
476
|
+
return revokedSession();
|
|
477
|
+
}
|
|
478
|
+
session.userInfo = verified;
|
|
479
|
+
token.email = verified.email;
|
|
480
|
+
token.name = verified.name;
|
|
481
|
+
token.username = verified.preferred_username ?? verified.username;
|
|
482
|
+
token.emailVerified = verified.email_verified;
|
|
483
|
+
token.groups = verified.groups ?? [];
|
|
484
|
+
if (session.user) {
|
|
485
|
+
session.user.email = verified.email;
|
|
486
|
+
session.user.name = verified.name;
|
|
487
|
+
session.user.username = verified.preferred_username ?? verified.username;
|
|
488
|
+
session.user.emailVerified = verified.email_verified;
|
|
489
|
+
session.user.groups = verified.groups ?? [];
|
|
490
|
+
}
|
|
170
491
|
if (token.error) {
|
|
171
492
|
session.error = token.error;
|
|
172
493
|
}
|
|
173
|
-
if (session.user) {
|
|
494
|
+
if (!useBackendSessions && session.user) {
|
|
174
495
|
session.user.email = token.email;
|
|
175
496
|
session.user.name = token.name;
|
|
176
497
|
session.user.username = token.username;
|
|
@@ -194,6 +515,28 @@ function createAuth(options = {}) {
|
|
|
194
515
|
strategy: "jwt",
|
|
195
516
|
...options.session
|
|
196
517
|
},
|
|
518
|
+
events: {
|
|
519
|
+
async signOut(message) {
|
|
520
|
+
if (useBackendSessions) {
|
|
521
|
+
const sessionId = message.token?.oauth42SessionId;
|
|
522
|
+
if (sessionId) {
|
|
523
|
+
await revokeBackendSession({
|
|
524
|
+
dpop: options.dpop,
|
|
525
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
526
|
+
issuer,
|
|
527
|
+
clientId,
|
|
528
|
+
clientSecret: clientSecret || "",
|
|
529
|
+
sessionId
|
|
530
|
+
}).catch((error) => {
|
|
531
|
+
console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (options.events?.signOut) {
|
|
536
|
+
await options.events.signOut(message);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
},
|
|
197
540
|
debug: options.debug || process.env.NODE_ENV === "development",
|
|
198
541
|
secret: process.env.NEXTAUTH_SECRET,
|
|
199
542
|
// Configure unique cookie names per app to prevent session conflicts on localhost
|
|
@@ -269,7 +612,8 @@ function createAuth(options = {}) {
|
|
|
269
612
|
}
|
|
270
613
|
}
|
|
271
614
|
};
|
|
272
|
-
const
|
|
615
|
+
const nextAuthHandler = NextAuth(authOptions);
|
|
616
|
+
const handler = options.pushedAuthorization ? withPushedAuthorization(nextAuthHandler, { issuer, clientId, clientSecret, dpop: options.dpop, clientAssertionKey: options.clientAssertionKey }) : nextAuthHandler;
|
|
273
617
|
return {
|
|
274
618
|
auth: authOptions,
|
|
275
619
|
handlers: { GET: handler, POST: handler }
|
|
@@ -280,24 +624,40 @@ function createHandlers(authOptions) {
|
|
|
280
624
|
return { GET: handler, POST: handler };
|
|
281
625
|
}
|
|
282
626
|
var getServerSession = getOAuth42Session;
|
|
283
|
-
async function refreshAccessToken(token, clientId, clientSecret, issuer) {
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
627
|
+
async function refreshAccessToken(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
628
|
+
if (typeof token.refreshToken !== "string" || !token.refreshToken) {
|
|
629
|
+
return { ...token, error: "RefreshAccessTokenError" };
|
|
630
|
+
}
|
|
631
|
+
const key = JSON.stringify([issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com", clientId, clientSecret, token.sub, token.refreshToken, dpop?.thumbprint, clientAssertionKey?.thumbprint, clientAssertionKey?.kid]);
|
|
632
|
+
let refresh = activeRefreshes.get(key);
|
|
633
|
+
if (!refresh) {
|
|
634
|
+
refresh = doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey);
|
|
635
|
+
activeRefreshes.set(key, refresh);
|
|
287
636
|
}
|
|
288
|
-
activeRefresh = doRefresh(token, clientId, clientSecret, issuer);
|
|
289
637
|
try {
|
|
290
|
-
|
|
638
|
+
const result = await refresh;
|
|
639
|
+
const updated = {
|
|
640
|
+
...token,
|
|
641
|
+
accessToken: result.accessToken,
|
|
642
|
+
refreshToken: result.refreshToken,
|
|
643
|
+
expiresAt: result.expiresAt,
|
|
644
|
+
error: result.error
|
|
645
|
+
};
|
|
646
|
+
if (clientAssertionKey) delete updated.clientSecret;
|
|
647
|
+
return updated;
|
|
291
648
|
} finally {
|
|
292
|
-
|
|
649
|
+
if (activeRefreshes.get(key) === refresh) activeRefreshes.delete(key);
|
|
293
650
|
}
|
|
294
651
|
}
|
|
295
|
-
async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
652
|
+
async function doRefresh(token, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
296
653
|
try {
|
|
297
654
|
const baseUrl = issuer || process.env.OAUTH42_ISSUER || "https://api.oauth42.com";
|
|
298
655
|
const tokenUrl = `${baseUrl}/oauth2/token`;
|
|
299
656
|
const fetchOptions = {
|
|
300
657
|
method: "POST",
|
|
658
|
+
redirect: "error",
|
|
659
|
+
cache: "no-store",
|
|
660
|
+
signal: AbortSignal.timeout(3e4),
|
|
301
661
|
headers: {
|
|
302
662
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
303
663
|
},
|
|
@@ -308,17 +668,12 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
308
668
|
client_secret: clientSecret
|
|
309
669
|
})
|
|
310
670
|
};
|
|
311
|
-
|
|
312
|
-
const https2 = await import("https");
|
|
313
|
-
fetchOptions.agent = new https2.Agent({
|
|
314
|
-
rejectUnauthorized: false
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
const response = await fetch(tokenUrl, fetchOptions);
|
|
671
|
+
const response = clientAssertionKey ? await clientAssertionKey.fetch(clientId, tokenUrl, fetchOptions, dpop) : dpop ? await dpop.fetch(tokenUrl, fetchOptions) : await fetch(tokenUrl, fetchOptions);
|
|
318
672
|
const refreshedTokens = await response.json();
|
|
319
673
|
if (!response.ok) {
|
|
320
674
|
throw refreshedTokens;
|
|
321
675
|
}
|
|
676
|
+
dpop?.assertTokenResponse(refreshedTokens);
|
|
322
677
|
console.log("[OAuth42] Token refreshed successfully");
|
|
323
678
|
return {
|
|
324
679
|
...token,
|
|
@@ -330,7 +685,7 @@ async function doRefresh(token, clientId, clientSecret, issuer) {
|
|
|
330
685
|
error: void 0
|
|
331
686
|
};
|
|
332
687
|
} catch (error) {
|
|
333
|
-
console.error("[OAuth42] Failed to refresh access token
|
|
688
|
+
console.error("[OAuth42] Failed to refresh access token");
|
|
334
689
|
return {
|
|
335
690
|
...token,
|
|
336
691
|
error: "RefreshAccessTokenError"
|
|
@@ -347,12 +702,37 @@ import { getToken, encode } from "next-auth/jwt";
|
|
|
347
702
|
var ALLOWED_COOKIE_SIZE = 4096;
|
|
348
703
|
var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
|
|
349
704
|
var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
705
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
350
706
|
var pendingRefreshes = /* @__PURE__ */ new Map();
|
|
351
|
-
|
|
707
|
+
function forwardedHeaders(req, cookieName, jwt) {
|
|
708
|
+
const headers = new Headers(req.headers);
|
|
709
|
+
headers.delete("x-oauth42-refreshed-token");
|
|
710
|
+
if (cookieName && jwt) {
|
|
711
|
+
const cookies = req.cookies.getAll().filter((cookie) => cookie.name !== cookieName && !cookie.name.startsWith(`${cookieName}.`));
|
|
712
|
+
if (jwt.length <= CHUNK_SIZE) cookies.push({ name: cookieName, value: jwt });
|
|
713
|
+
else for (let i = 0; i * CHUNK_SIZE < jwt.length; i++) {
|
|
714
|
+
cookies.push({ name: `${cookieName}.${i}`, value: jwt.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) });
|
|
715
|
+
}
|
|
716
|
+
headers.set("cookie", cookies.map((cookie) => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`).join("; "));
|
|
717
|
+
}
|
|
718
|
+
return headers;
|
|
719
|
+
}
|
|
720
|
+
function resolveRefreshConfig(options, token) {
|
|
721
|
+
return {
|
|
722
|
+
clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
|
|
723
|
+
clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
|
|
724
|
+
issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
async function refreshTokens(refreshToken, clientId, clientSecret, issuer, dpop, clientAssertionKey) {
|
|
352
728
|
try {
|
|
353
729
|
const tokenUrl = `${issuer}/oauth2/token`;
|
|
354
|
-
const
|
|
730
|
+
const fetcher = clientAssertionKey ? (url, init) => clientAssertionKey.fetch(clientId, url, init, dpop) : dpop ? dpop.fetch.bind(dpop) : fetch;
|
|
731
|
+
const response = await fetcher(tokenUrl, {
|
|
355
732
|
method: "POST",
|
|
733
|
+
redirect: "error",
|
|
734
|
+
cache: "no-store",
|
|
735
|
+
signal: AbortSignal.timeout(3e4),
|
|
356
736
|
headers: {
|
|
357
737
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
358
738
|
},
|
|
@@ -365,9 +745,10 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
365
745
|
});
|
|
366
746
|
const data = await response.json();
|
|
367
747
|
if (!response.ok) {
|
|
368
|
-
console.error("[OAuth42 Middleware] Token refresh failed:",
|
|
748
|
+
console.error("[OAuth42 Middleware] Token refresh failed:", response.status);
|
|
369
749
|
return { success: false, error: data.error || "refresh_failed" };
|
|
370
750
|
}
|
|
751
|
+
dpop?.assertTokenResponse(data);
|
|
371
752
|
console.log("[OAuth42 Middleware] Token refreshed successfully");
|
|
372
753
|
return {
|
|
373
754
|
success: true,
|
|
@@ -376,17 +757,14 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
376
757
|
expiresAt: Math.floor(Date.now() / 1e3) + (data.expires_in || 3600)
|
|
377
758
|
};
|
|
378
759
|
} catch (error) {
|
|
379
|
-
console.error("[OAuth42 Middleware] Token refresh
|
|
760
|
+
console.error("[OAuth42 Middleware] Token refresh request failed");
|
|
380
761
|
return { success: false, error: "refresh_error" };
|
|
381
762
|
}
|
|
382
763
|
}
|
|
383
764
|
function withOAuth42Auth(options = {}) {
|
|
384
765
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
385
|
-
const clientId = process.env.OAUTH42_CLIENT_ID;
|
|
386
|
-
const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
|
|
387
|
-
const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
388
766
|
if (!secret) {
|
|
389
|
-
|
|
767
|
+
throw new Error("NEXTAUTH_SECRET is required for OAuth42 middleware");
|
|
390
768
|
}
|
|
391
769
|
return async function middleware(req) {
|
|
392
770
|
const cookieName = options.cookiePrefix ? `${options.cookiePrefix}.session-token` : "next-auth.session-token";
|
|
@@ -397,18 +775,76 @@ function withOAuth42Auth(options = {}) {
|
|
|
397
775
|
});
|
|
398
776
|
const pathname = req.nextUrl.pathname;
|
|
399
777
|
if (options.publicPaths?.some((path) => pathname.startsWith(path))) {
|
|
400
|
-
return NextResponse.next();
|
|
778
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
401
779
|
}
|
|
402
780
|
const needsProtection = options.protectedPaths ? options.protectedPaths.some((path) => pathname.startsWith(path)) : true;
|
|
403
781
|
if (!needsProtection) {
|
|
404
|
-
return NextResponse.next();
|
|
782
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
405
783
|
}
|
|
406
|
-
if (!token) {
|
|
784
|
+
if (!token || token.error) {
|
|
407
785
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
408
786
|
const url = new URL(signInUrl, req.url);
|
|
409
787
|
url.searchParams.set("callbackUrl", pathname);
|
|
410
788
|
return NextResponse.redirect(url);
|
|
411
789
|
}
|
|
790
|
+
const refreshConfig = resolveRefreshConfig(options, token);
|
|
791
|
+
const denied = () => {
|
|
792
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
793
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
794
|
+
const response = NextResponse.redirect(url);
|
|
795
|
+
for (const cookie of req.cookies.getAll()) {
|
|
796
|
+
if (cookie.name === cookieName || cookie.name.startsWith(`${cookieName}.`)) response.cookies.delete(cookie.name);
|
|
797
|
+
}
|
|
798
|
+
return response;
|
|
799
|
+
};
|
|
800
|
+
const verifyAndAuthorize = async (current, resolved) => {
|
|
801
|
+
const user = resolved === void 0 ? await verifiedUserInfo(refreshConfig.issuer, current.accessToken, token.sub, options.dpop) : resolved;
|
|
802
|
+
if (!user) return false;
|
|
803
|
+
current.groups = user.groups ?? [];
|
|
804
|
+
current.email = user.email;
|
|
805
|
+
current.name = user.name;
|
|
806
|
+
current.username = user.preferred_username ?? user.username;
|
|
807
|
+
current.emailVerified = user.email_verified;
|
|
808
|
+
return options.callbacks?.authorized ? options.callbacks.authorized({ token: current, req }) : true;
|
|
809
|
+
};
|
|
810
|
+
if (isBackendSessionMode(options.sessionStore)) {
|
|
811
|
+
const sessionId = token.oauth42SessionId;
|
|
812
|
+
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret && !options.clientAssertionKey) {
|
|
813
|
+
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
814
|
+
const url = new URL(signInUrl, req.url);
|
|
815
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
816
|
+
const response = NextResponse.redirect(url);
|
|
817
|
+
response.cookies.delete(cookieName);
|
|
818
|
+
return response;
|
|
819
|
+
}
|
|
820
|
+
const backendSession = await resolveBackendSession({
|
|
821
|
+
dpop: options.dpop,
|
|
822
|
+
clientAssertionKey: options.clientAssertionKey,
|
|
823
|
+
issuer: refreshConfig.issuer,
|
|
824
|
+
clientId: refreshConfig.clientId,
|
|
825
|
+
clientSecret: refreshConfig.clientSecret || "",
|
|
826
|
+
sessionId
|
|
827
|
+
}).catch((error) => {
|
|
828
|
+
console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
|
|
829
|
+
return null;
|
|
830
|
+
});
|
|
831
|
+
if (!backendSession) {
|
|
832
|
+
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
833
|
+
const url = new URL(signInUrl, req.url);
|
|
834
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
835
|
+
const response = NextResponse.redirect(url);
|
|
836
|
+
response.cookies.delete(cookieName);
|
|
837
|
+
return response;
|
|
838
|
+
}
|
|
839
|
+
const verified = await verifiedBackendSessionUserInfo(refreshConfig.issuer, backendSession, token.sub, options.dpop);
|
|
840
|
+
if (!await verifyAndAuthorize({ ...token, accessToken: backendSession.access_token }, verified)) return denied();
|
|
841
|
+
const requestHeaders = forwardedHeaders(req);
|
|
842
|
+
return NextResponse.next({
|
|
843
|
+
request: {
|
|
844
|
+
headers: requestHeaders
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
}
|
|
412
848
|
const expiresAt = token.expiresAt;
|
|
413
849
|
const now = Math.floor(Date.now() / 1e3);
|
|
414
850
|
const bufferSeconds = 15;
|
|
@@ -418,26 +854,30 @@ function withOAuth42Auth(options = {}) {
|
|
|
418
854
|
expiresAt,
|
|
419
855
|
now,
|
|
420
856
|
expiredAgo: expiresAt ? now - expiresAt : "n/a",
|
|
421
|
-
accessTokenPrefix: token.accessToken?.substring(0, 20),
|
|
422
857
|
path: pathname
|
|
423
858
|
});
|
|
424
859
|
}
|
|
425
|
-
if (needsRefresh && token.refreshToken && clientId && clientSecret) {
|
|
426
|
-
const
|
|
427
|
-
let refreshPromise = pendingRefreshes.get(
|
|
860
|
+
if (needsRefresh && token.refreshToken && refreshConfig.clientId && (refreshConfig.clientSecret || options.clientAssertionKey)) {
|
|
861
|
+
const grantKey = JSON.stringify([refreshConfig.issuer, refreshConfig.clientId, refreshConfig.clientSecret, token.sub, token.refreshToken, options.dpop?.thumbprint, options.clientAssertionKey?.thumbprint, options.clientAssertionKey?.kid]);
|
|
862
|
+
let refreshPromise = pendingRefreshes.get(grantKey);
|
|
428
863
|
if (refreshPromise) {
|
|
429
864
|
console.log("[OAuth42 Middleware] Waiting for in-flight refresh...");
|
|
430
865
|
} else {
|
|
431
866
|
console.log("[OAuth42 Middleware] Access token expired, refreshing...");
|
|
432
867
|
refreshPromise = refreshTokens(
|
|
433
868
|
token.refreshToken,
|
|
434
|
-
clientId,
|
|
435
|
-
clientSecret,
|
|
436
|
-
issuer
|
|
869
|
+
refreshConfig.clientId,
|
|
870
|
+
refreshConfig.clientSecret || "",
|
|
871
|
+
refreshConfig.issuer,
|
|
872
|
+
options.dpop,
|
|
873
|
+
options.clientAssertionKey
|
|
437
874
|
).finally(() => {
|
|
438
|
-
setTimeout(() => pendingRefreshes.delete(
|
|
875
|
+
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(grantKey), 3e4);
|
|
876
|
+
if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
|
|
877
|
+
cleanupTimer.unref();
|
|
878
|
+
}
|
|
439
879
|
});
|
|
440
|
-
pendingRefreshes.set(
|
|
880
|
+
pendingRefreshes.set(grantKey, refreshPromise);
|
|
441
881
|
}
|
|
442
882
|
const refreshed = await refreshPromise;
|
|
443
883
|
if (refreshed.success && refreshed.accessToken && refreshed.refreshToken) {
|
|
@@ -447,12 +887,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
447
887
|
refreshToken: refreshed.refreshToken,
|
|
448
888
|
expiresAt: refreshed.expiresAt
|
|
449
889
|
};
|
|
890
|
+
if (options.clientAssertionKey) delete updatedToken.clientSecret;
|
|
891
|
+
if (!await verifyAndAuthorize(updatedToken)) return denied();
|
|
450
892
|
const newJwt = await encode({
|
|
451
893
|
token: updatedToken,
|
|
452
894
|
secret
|
|
453
895
|
});
|
|
454
|
-
const requestHeaders =
|
|
455
|
-
requestHeaders.set("x-oauth42-refreshed-token", refreshed.accessToken);
|
|
896
|
+
const requestHeaders = forwardedHeaders(req, cookieName, newJwt);
|
|
456
897
|
const response = NextResponse.next({
|
|
457
898
|
request: {
|
|
458
899
|
headers: requestHeaders
|
|
@@ -462,7 +903,8 @@ function withOAuth42Auth(options = {}) {
|
|
|
462
903
|
httpOnly: true,
|
|
463
904
|
sameSite: "lax",
|
|
464
905
|
path: "/",
|
|
465
|
-
secure: process.env.NODE_ENV === "production"
|
|
906
|
+
secure: process.env.NODE_ENV === "production",
|
|
907
|
+
maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
|
|
466
908
|
};
|
|
467
909
|
if (newJwt.length <= CHUNK_SIZE) {
|
|
468
910
|
response.cookies.set(cookieName, newJwt, cookieOptions);
|
|
@@ -504,13 +946,12 @@ function withOAuth42Auth(options = {}) {
|
|
|
504
946
|
chunked: newJwt.length > CHUNK_SIZE,
|
|
505
947
|
existingBaseCooke: req.cookies.has(cookieName),
|
|
506
948
|
existingChunks: existingChunks.length > 0 ? existingChunks : "none",
|
|
507
|
-
newAccessTokenPrefix: refreshed.accessToken?.substring(0, 20),
|
|
508
949
|
newExpiresAt: refreshed.expiresAt,
|
|
509
950
|
cookieName
|
|
510
951
|
});
|
|
511
952
|
return response;
|
|
512
953
|
} else {
|
|
513
|
-
console.log("[OAuth42 Middleware] Refresh failed
|
|
954
|
+
console.log("[OAuth42 Middleware] Refresh failed, clearing session");
|
|
514
955
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
515
956
|
const url = new URL(signInUrl, req.url);
|
|
516
957
|
url.searchParams.set("callbackUrl", pathname);
|
|
@@ -527,16 +968,13 @@ function withOAuth42Auth(options = {}) {
|
|
|
527
968
|
return response;
|
|
528
969
|
}
|
|
529
970
|
}
|
|
530
|
-
if (
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const url = new URL(signInUrl, req.url);
|
|
535
|
-
url.searchParams.set("callbackUrl", pathname);
|
|
536
|
-
return NextResponse.redirect(url);
|
|
537
|
-
}
|
|
971
|
+
if (!Number.isFinite(expiresAt) || now >= expiresAt || !token.accessToken) {
|
|
972
|
+
const url = new URL(options.pages?.signIn || "/auth/signin", req.url);
|
|
973
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
974
|
+
return NextResponse.redirect(url);
|
|
538
975
|
}
|
|
539
|
-
return
|
|
976
|
+
if (!await verifyAndAuthorize(token)) return denied();
|
|
977
|
+
return NextResponse.next({ request: { headers: forwardedHeaders(req) } });
|
|
540
978
|
};
|
|
541
979
|
}
|
|
542
980
|
function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = ["/auth", "/api/auth"]) {
|
|
@@ -558,7 +996,7 @@ function createMiddlewareConfig(protectedPaths = ["/protected"], publicPaths = [
|
|
|
558
996
|
|
|
559
997
|
// src/server/hosted-auth-callback.ts
|
|
560
998
|
import { NextResponse as NextResponse2 } from "next/server";
|
|
561
|
-
import
|
|
999
|
+
import { timingSafeEqual } from "crypto";
|
|
562
1000
|
import { encode as encode2 } from "next-auth/jwt";
|
|
563
1001
|
function createHostedAuthCallback(options = {}) {
|
|
564
1002
|
return async function GET(request) {
|
|
@@ -571,7 +1009,7 @@ function createHostedAuthCallback(options = {}) {
|
|
|
571
1009
|
redirectUrl = "/dashboard",
|
|
572
1010
|
errorUrl = "/"
|
|
573
1011
|
} = options;
|
|
574
|
-
if (!clientId || !clientSecret) {
|
|
1012
|
+
if (!clientId || !clientSecret && !options.clientAssertionKey) {
|
|
575
1013
|
console.error("OAuth42 client credentials are required");
|
|
576
1014
|
return NextResponse2.redirect(new URL(`${errorUrl}?error=missing_credentials`, request.url));
|
|
577
1015
|
}
|
|
@@ -581,6 +1019,14 @@ function createHostedAuthCallback(options = {}) {
|
|
|
581
1019
|
}
|
|
582
1020
|
const searchParams = request.nextUrl.searchParams;
|
|
583
1021
|
const code = searchParams.get("code");
|
|
1022
|
+
const cookiePrefix = process.env.NODE_ENV === "production" ? "__Host-" : request.nextUrl.protocol === "https:" ? "__Host-" : "";
|
|
1023
|
+
const expectedState = request.cookies.get(`${cookiePrefix}oauth42-state`)?.value;
|
|
1024
|
+
const returnedState = searchParams.get("state");
|
|
1025
|
+
const verifier = request.cookies.get(`${cookiePrefix}oauth42-pkce`)?.value;
|
|
1026
|
+
const validState = expectedState && returnedState && expectedState.length >= 32 && Buffer.byteLength(expectedState) === Buffer.byteLength(returnedState) && timingSafeEqual(Buffer.from(expectedState), Buffer.from(returnedState));
|
|
1027
|
+
if (!validState || !verifier || !/^[A-Za-z0-9._~-]{43,128}$/.test(verifier)) {
|
|
1028
|
+
return NextResponse2.redirect(new URL(`${errorUrl}?error=invalid_state`, request.url));
|
|
1029
|
+
}
|
|
584
1030
|
if (!code) {
|
|
585
1031
|
return NextResponse2.redirect(new URL(`${errorUrl}?error=missing_code`, request.url));
|
|
586
1032
|
}
|
|
@@ -588,39 +1034,38 @@ function createHostedAuthCallback(options = {}) {
|
|
|
588
1034
|
const tokenEndpoint = `${issuer}/oauth2/token`;
|
|
589
1035
|
const body = new URLSearchParams({
|
|
590
1036
|
grant_type: "authorization_code",
|
|
1037
|
+
code_verifier: verifier,
|
|
591
1038
|
code,
|
|
592
1039
|
redirect_uri: `${baseUrl}/api/oauth/callback`,
|
|
593
1040
|
client_id: clientId,
|
|
594
|
-
client_secret: clientSecret
|
|
1041
|
+
client_secret: clientSecret || ""
|
|
595
1042
|
});
|
|
596
1043
|
const fetchOptions = {
|
|
597
1044
|
method: "POST",
|
|
1045
|
+
redirect: "error",
|
|
1046
|
+
cache: "no-store",
|
|
1047
|
+
signal: AbortSignal.timeout(3e4),
|
|
598
1048
|
headers: {
|
|
599
1049
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
600
1050
|
},
|
|
601
1051
|
body: body.toString()
|
|
602
1052
|
};
|
|
603
|
-
|
|
604
|
-
const agent = new https.Agent({
|
|
605
|
-
rejectUnauthorized: false
|
|
606
|
-
});
|
|
607
|
-
fetchOptions.agent = agent;
|
|
608
|
-
}
|
|
609
|
-
const tokenResponse = await fetch(tokenEndpoint, fetchOptions);
|
|
1053
|
+
const tokenResponse = options.clientAssertionKey ? await options.clientAssertionKey.fetch(clientId, tokenEndpoint, fetchOptions, options.dpop) : options.dpop ? await options.dpop.fetch(tokenEndpoint, fetchOptions) : await fetch(tokenEndpoint, fetchOptions);
|
|
610
1054
|
const tokens = await tokenResponse.json();
|
|
611
1055
|
if (!tokenResponse.ok) {
|
|
612
|
-
console.error("Token exchange failed:",
|
|
1056
|
+
console.error("Token exchange failed:", tokenResponse.status);
|
|
613
1057
|
return NextResponse2.redirect(new URL(`${errorUrl}?error=token_exchange_failed`, request.url));
|
|
614
1058
|
}
|
|
1059
|
+
options.dpop?.assertTokenResponse(tokens);
|
|
615
1060
|
const userInfoEndpoint = `${issuer}/oauth2/userinfo`;
|
|
616
|
-
const
|
|
617
|
-
headers: {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1061
|
+
const infoOptions = {
|
|
1062
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
|
1063
|
+
redirect: "error",
|
|
1064
|
+
cache: "no-store",
|
|
1065
|
+
signal: AbortSignal.timeout(3e4)
|
|
1066
|
+
};
|
|
1067
|
+
const userInfoResponse = options.dpop ? await options.dpop.fetch(userInfoEndpoint, infoOptions, tokens.access_token) : await fetch(userInfoEndpoint, infoOptions);
|
|
1068
|
+
if (!userInfoResponse.ok) throw new Error("User info request failed");
|
|
624
1069
|
const userInfo = await userInfoResponse.json();
|
|
625
1070
|
const sessionToken = await encode2({
|
|
626
1071
|
token: {
|
|
@@ -645,6 +1090,14 @@ function createHostedAuthCallback(options = {}) {
|
|
|
645
1090
|
// 30 days
|
|
646
1091
|
path: "/"
|
|
647
1092
|
});
|
|
1093
|
+
for (const name of ["oauth42-state", "oauth42-pkce"]) {
|
|
1094
|
+
redirectResponse.cookies.set(`${cookiePrefix}${name}`, "", {
|
|
1095
|
+
path: "/",
|
|
1096
|
+
secure: cookiePrefix === "__Host-",
|
|
1097
|
+
sameSite: "lax",
|
|
1098
|
+
maxAge: 0
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
648
1101
|
return redirectResponse;
|
|
649
1102
|
} catch (error) {
|
|
650
1103
|
console.error("OAuth callback error:", error);
|
|
@@ -652,18 +1105,93 @@ function createHostedAuthCallback(options = {}) {
|
|
|
652
1105
|
}
|
|
653
1106
|
};
|
|
654
1107
|
}
|
|
1108
|
+
|
|
1109
|
+
// src/server/dpop.ts
|
|
1110
|
+
import { createHash as createHash2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2, randomBytes as randomBytes2, sign as sign2 } from "crypto";
|
|
1111
|
+
var OAuth42DPoP = class _OAuth42DPoP {
|
|
1112
|
+
constructor(key) {
|
|
1113
|
+
this.nonces = /* @__PURE__ */ new Map();
|
|
1114
|
+
if (key.type !== "private" || key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
1115
|
+
throw new Error("DPoP requires a private P-256 key");
|
|
1116
|
+
}
|
|
1117
|
+
this.key = key;
|
|
1118
|
+
const publicKey = createPublicKey2(key).export({ format: "jwk" });
|
|
1119
|
+
this.jwk = { crv: "P-256", kty: "EC", x: publicKey.x, y: publicKey.y };
|
|
1120
|
+
this.thumbprint = createHash2("sha256").update(JSON.stringify(this.jwk)).digest("base64url");
|
|
1121
|
+
}
|
|
1122
|
+
/** Persist keys in protected server storage; regeneration cannot recover a public refresh grant. */
|
|
1123
|
+
static generate() {
|
|
1124
|
+
return new _OAuth42DPoP(generateKeyPairSync2("ec", { namedCurve: "prime256v1" }).privateKey);
|
|
1125
|
+
}
|
|
1126
|
+
/** Passed directly to openid-client so its callback keeps OIDC issuer/nonce/signature validation. */
|
|
1127
|
+
get oidcKey() {
|
|
1128
|
+
return this.key;
|
|
1129
|
+
}
|
|
1130
|
+
proof(method, target, nonce, accessToken) {
|
|
1131
|
+
const url = new URL(target);
|
|
1132
|
+
if (!["http:", "https:"].includes(url.protocol) || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
1133
|
+
throw new Error("DPoP requires HTTPS outside loopback development");
|
|
1134
|
+
}
|
|
1135
|
+
if (url.username || url.password) throw new Error("DPoP endpoint must not contain credentials");
|
|
1136
|
+
url.search = "";
|
|
1137
|
+
url.hash = "";
|
|
1138
|
+
const header = { typ: "dpop+jwt", alg: "ES256", jwk: this.jwk };
|
|
1139
|
+
const claims = {
|
|
1140
|
+
htm: method.toUpperCase(),
|
|
1141
|
+
htu: url.href,
|
|
1142
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
1143
|
+
jti: randomBytes2(32).toString("base64url"),
|
|
1144
|
+
...nonce ? { nonce } : {},
|
|
1145
|
+
...accessToken !== void 0 ? { ath: createHash2("sha256").update(accessToken).digest("base64url") } : {}
|
|
1146
|
+
};
|
|
1147
|
+
const input = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
|
|
1148
|
+
return `${input}.${sign2("sha256", Buffer.from(input), { key: this.key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
1149
|
+
}
|
|
1150
|
+
assertTokenResponse(tokens) {
|
|
1151
|
+
if (typeof tokens.token_type !== "string" || tokens.token_type.toLowerCase() !== "dpop") {
|
|
1152
|
+
throw new Error("Server did not issue the requested DPoP-bound token");
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async fetch(target, init = {}, accessToken, prepare) {
|
|
1156
|
+
if (init.body instanceof ReadableStream) throw new Error("DPoP nonce retries require a replayable request body");
|
|
1157
|
+
const cacheKey = `${new URL(target).origin}:${accessToken !== void 0 ? "resource" : "token"}`;
|
|
1158
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1159
|
+
const request = prepare ? prepare() : init;
|
|
1160
|
+
const headers = new Headers(request.headers);
|
|
1161
|
+
headers.set("DPoP", this.proof(request.method || "GET", target, this.nonces.get(cacheKey), accessToken));
|
|
1162
|
+
if (accessToken !== void 0) headers.set("Authorization", `DPoP ${accessToken}`);
|
|
1163
|
+
const response = await fetch(target, { ...request, headers, redirect: "error", cache: "no-store", signal: request.signal || AbortSignal.timeout(3e4) });
|
|
1164
|
+
const nonce = response.headers.get("DPoP-Nonce");
|
|
1165
|
+
if (nonce && /^[\x21-\x7e]{1,128}$/.test(nonce)) {
|
|
1166
|
+
if (this.nonces.size >= 256) this.nonces.clear();
|
|
1167
|
+
this.nonces.set(cacheKey, nonce);
|
|
1168
|
+
} else return response;
|
|
1169
|
+
if (attempt || ![400, 401].includes(response.status)) return response;
|
|
1170
|
+
const error = await response.clone().json().catch(() => null);
|
|
1171
|
+
if (error?.error !== "use_dpop_nonce") return response;
|
|
1172
|
+
}
|
|
1173
|
+
throw new Error("DPoP nonce negotiation failed");
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
655
1176
|
export {
|
|
656
1177
|
default2 as NextAuth,
|
|
1178
|
+
OAuth42ClientAssertion,
|
|
1179
|
+
OAuth42DPoP,
|
|
657
1180
|
OAuth42Provider,
|
|
658
1181
|
createAuth,
|
|
1182
|
+
createBackendSession,
|
|
659
1183
|
createHandlers,
|
|
660
1184
|
createHostedAuthCallback,
|
|
661
1185
|
createMiddlewareConfig,
|
|
662
1186
|
getOAuth42Session,
|
|
663
1187
|
getServerSession,
|
|
1188
|
+
pushAuthorizationURL,
|
|
664
1189
|
refreshAccessToken,
|
|
1190
|
+
resolveBackendSession,
|
|
1191
|
+
revokeBackendSession,
|
|
665
1192
|
withOAuth42Auth,
|
|
666
1193
|
withOAuth42ServerSideProps,
|
|
667
|
-
withOAuth42Session
|
|
1194
|
+
withOAuth42Session,
|
|
1195
|
+
withPushedAuthorization
|
|
668
1196
|
};
|
|
669
1197
|
//# sourceMappingURL=index.mjs.map
|