transactional-auth-next 0.1.0
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 +312 -0
- package/dist/chunk-4S34DQOR.mjs +52 -0
- package/dist/client/index.d.mts +73 -0
- package/dist/client/index.d.ts +73 -0
- package/dist/client/index.js +110 -0
- package/dist/client/index.mjs +71 -0
- package/dist/index.d.mts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +80 -0
- package/dist/index.mjs +10 -0
- package/dist/middleware/index.d.mts +56 -0
- package/dist/middleware/index.d.ts +56 -0
- package/dist/middleware/index.js +119 -0
- package/dist/middleware/index.mjs +93 -0
- package/dist/server/index.d.mts +97 -0
- package/dist/server/index.d.ts +97 -0
- package/dist/server/index.js +369 -0
- package/dist/server/index.mjs +296 -0
- package/dist/types-D3JPYyLl.d.mts +57 -0
- package/dist/types-D3JPYyLl.d.ts +57 -0
- package/package.json +80 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/server/index.ts
|
|
31
|
+
var server_exports = {};
|
|
32
|
+
__export(server_exports, {
|
|
33
|
+
getAccessToken: () => getAccessToken,
|
|
34
|
+
getSession: () => getSession,
|
|
35
|
+
getUser: () => getUser,
|
|
36
|
+
handleCallback: () => handleCallback,
|
|
37
|
+
handleLogin: () => handleLogin,
|
|
38
|
+
handleLogout: () => handleLogout,
|
|
39
|
+
handleSession: () => handleSession,
|
|
40
|
+
isAuthenticated: () => isAuthenticated
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(server_exports);
|
|
43
|
+
|
|
44
|
+
// src/server/session.ts
|
|
45
|
+
var import_headers = require("next/headers");
|
|
46
|
+
var jose = __toESM(require("jose"));
|
|
47
|
+
|
|
48
|
+
// src/config.ts
|
|
49
|
+
var globalConfig = null;
|
|
50
|
+
function getConfig() {
|
|
51
|
+
if (!globalConfig) {
|
|
52
|
+
const domain = process.env.TRANSACTIONAL_AUTH_DOMAIN || process.env.NEXT_PUBLIC_TRANSACTIONAL_AUTH_DOMAIN;
|
|
53
|
+
const clientId = process.env.TRANSACTIONAL_AUTH_CLIENT_ID || process.env.NEXT_PUBLIC_TRANSACTIONAL_AUTH_CLIENT_ID;
|
|
54
|
+
const clientSecret = process.env.TRANSACTIONAL_AUTH_CLIENT_SECRET;
|
|
55
|
+
const baseUrl = process.env.TRANSACTIONAL_AUTH_BASE_URL || process.env.NEXT_PUBLIC_APP_URL;
|
|
56
|
+
if (domain && clientId) {
|
|
57
|
+
globalConfig = {
|
|
58
|
+
domain,
|
|
59
|
+
clientId,
|
|
60
|
+
clientSecret,
|
|
61
|
+
baseUrl,
|
|
62
|
+
scope: "openid profile email",
|
|
63
|
+
cookieName: "transactional_session",
|
|
64
|
+
cookieOptions: {
|
|
65
|
+
secure: process.env.NODE_ENV === "production",
|
|
66
|
+
sameSite: "lax",
|
|
67
|
+
maxAge: 7 * 24 * 60 * 60
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return globalConfig;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Transactional Auth not initialized. Call initTransactionalAuth() or set environment variables."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return globalConfig;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/server/session.ts
|
|
80
|
+
async function getSession() {
|
|
81
|
+
const config = getConfig();
|
|
82
|
+
const cookieStore = await (0, import_headers.cookies)();
|
|
83
|
+
const sessionCookie = cookieStore.get(config.cookieName || "transactional_session");
|
|
84
|
+
if (!sessionCookie?.value) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const sessionData = JSON.parse(
|
|
89
|
+
Buffer.from(sessionCookie.value, "base64").toString("utf-8")
|
|
90
|
+
);
|
|
91
|
+
if (sessionData.expiresAt < Date.now() / 1e3) {
|
|
92
|
+
if (sessionData.refreshToken) {
|
|
93
|
+
const newSession = await refreshSession(sessionData.refreshToken);
|
|
94
|
+
if (newSession) {
|
|
95
|
+
return newSession;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return sessionData;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function getUser() {
|
|
106
|
+
const session = await getSession();
|
|
107
|
+
return session?.user || null;
|
|
108
|
+
}
|
|
109
|
+
async function getAccessToken() {
|
|
110
|
+
const session = await getSession();
|
|
111
|
+
return session?.accessToken || null;
|
|
112
|
+
}
|
|
113
|
+
async function isAuthenticated() {
|
|
114
|
+
const session = await getSession();
|
|
115
|
+
return session !== null;
|
|
116
|
+
}
|
|
117
|
+
async function refreshSession(refreshToken) {
|
|
118
|
+
const config = getConfig();
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(`https://${config.domain}/token`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
123
|
+
body: new URLSearchParams({
|
|
124
|
+
grant_type: "refresh_token",
|
|
125
|
+
client_id: config.clientId,
|
|
126
|
+
...config.clientSecret ? { client_secret: config.clientSecret } : {},
|
|
127
|
+
refresh_token: refreshToken
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const tokens = await response.json();
|
|
134
|
+
const idToken = jose.decodeJwt(tokens.id_token);
|
|
135
|
+
const session = {
|
|
136
|
+
user: {
|
|
137
|
+
sub: idToken.sub,
|
|
138
|
+
email: idToken.email,
|
|
139
|
+
emailVerified: idToken.email_verified,
|
|
140
|
+
name: idToken.name,
|
|
141
|
+
givenName: idToken.given_name,
|
|
142
|
+
familyName: idToken.family_name,
|
|
143
|
+
picture: idToken.picture
|
|
144
|
+
},
|
|
145
|
+
accessToken: tokens.access_token,
|
|
146
|
+
refreshToken: tokens.refresh_token || refreshToken,
|
|
147
|
+
idToken: tokens.id_token,
|
|
148
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
149
|
+
};
|
|
150
|
+
const cookieStore = await (0, import_headers.cookies)();
|
|
151
|
+
cookieStore.set(
|
|
152
|
+
config.cookieName || "transactional_session",
|
|
153
|
+
Buffer.from(JSON.stringify(session)).toString("base64"),
|
|
154
|
+
{
|
|
155
|
+
httpOnly: true,
|
|
156
|
+
secure: config.cookieOptions?.secure ?? process.env.NODE_ENV === "production",
|
|
157
|
+
sameSite: config.cookieOptions?.sameSite ?? "lax",
|
|
158
|
+
maxAge: config.cookieOptions?.maxAge ?? 7 * 24 * 60 * 60,
|
|
159
|
+
path: "/"
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
return session;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/server/handlers.ts
|
|
169
|
+
var import_headers2 = require("next/headers");
|
|
170
|
+
var import_server = require("next/server");
|
|
171
|
+
var jose2 = __toESM(require("jose"));
|
|
172
|
+
function generateRandomString(length) {
|
|
173
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
174
|
+
let result = "";
|
|
175
|
+
const randomValues = new Uint8Array(length);
|
|
176
|
+
crypto.getRandomValues(randomValues);
|
|
177
|
+
for (let i = 0; i < length; i++) {
|
|
178
|
+
result += chars[randomValues[i] % chars.length];
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
async function generatePKCE() {
|
|
183
|
+
const verifier = generateRandomString(64);
|
|
184
|
+
const encoder = new TextEncoder();
|
|
185
|
+
const data = encoder.encode(verifier);
|
|
186
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
187
|
+
const challenge = Buffer.from(hash).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
188
|
+
return { verifier, challenge };
|
|
189
|
+
}
|
|
190
|
+
function handleLogin(options) {
|
|
191
|
+
return async (request) => {
|
|
192
|
+
const config = getConfig();
|
|
193
|
+
const { verifier, challenge } = await generatePKCE();
|
|
194
|
+
const state = generateRandomString(32);
|
|
195
|
+
const returnTo = options?.returnTo || request.nextUrl.searchParams.get("returnTo") || "/";
|
|
196
|
+
const cookieStore = await (0, import_headers2.cookies)();
|
|
197
|
+
cookieStore.set("transactional_pkce_verifier", verifier, {
|
|
198
|
+
httpOnly: true,
|
|
199
|
+
secure: process.env.NODE_ENV === "production",
|
|
200
|
+
sameSite: "lax",
|
|
201
|
+
maxAge: 300,
|
|
202
|
+
// 5 minutes
|
|
203
|
+
path: "/"
|
|
204
|
+
});
|
|
205
|
+
cookieStore.set("transactional_auth_state", JSON.stringify({ state, returnTo }), {
|
|
206
|
+
httpOnly: true,
|
|
207
|
+
secure: process.env.NODE_ENV === "production",
|
|
208
|
+
sameSite: "lax",
|
|
209
|
+
maxAge: 300,
|
|
210
|
+
path: "/"
|
|
211
|
+
});
|
|
212
|
+
const authUrl = new URL(`https://${config.domain}/authorize`);
|
|
213
|
+
authUrl.searchParams.set("client_id", config.clientId);
|
|
214
|
+
authUrl.searchParams.set("redirect_uri", `${config.baseUrl}/api/auth/callback`);
|
|
215
|
+
authUrl.searchParams.set("response_type", "code");
|
|
216
|
+
authUrl.searchParams.set("scope", config.scope || "openid profile email");
|
|
217
|
+
authUrl.searchParams.set("state", state);
|
|
218
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
219
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
220
|
+
if (options?.connection) {
|
|
221
|
+
authUrl.searchParams.set("connection", options.connection);
|
|
222
|
+
}
|
|
223
|
+
if (options?.loginHint) {
|
|
224
|
+
authUrl.searchParams.set("login_hint", options.loginHint);
|
|
225
|
+
}
|
|
226
|
+
if (config.audience) {
|
|
227
|
+
authUrl.searchParams.set("audience", config.audience);
|
|
228
|
+
}
|
|
229
|
+
return import_server.NextResponse.redirect(authUrl.toString());
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function handleCallback() {
|
|
233
|
+
return async (request) => {
|
|
234
|
+
const config = getConfig();
|
|
235
|
+
const searchParams = request.nextUrl.searchParams;
|
|
236
|
+
const code = searchParams.get("code");
|
|
237
|
+
const state = searchParams.get("state");
|
|
238
|
+
const error = searchParams.get("error");
|
|
239
|
+
if (error) {
|
|
240
|
+
const errorDescription = searchParams.get("error_description") || error;
|
|
241
|
+
return import_server.NextResponse.redirect(
|
|
242
|
+
`${config.baseUrl}/auth/error?error=${encodeURIComponent(errorDescription)}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (!code || !state) {
|
|
246
|
+
return import_server.NextResponse.redirect(`${config.baseUrl}/auth/error?error=missing_code_or_state`);
|
|
247
|
+
}
|
|
248
|
+
const cookieStore = await (0, import_headers2.cookies)();
|
|
249
|
+
const stateCookie = cookieStore.get("transactional_auth_state");
|
|
250
|
+
const verifierCookie = cookieStore.get("transactional_pkce_verifier");
|
|
251
|
+
if (!stateCookie?.value || !verifierCookie?.value) {
|
|
252
|
+
return import_server.NextResponse.redirect(`${config.baseUrl}/auth/error?error=missing_state_cookie`);
|
|
253
|
+
}
|
|
254
|
+
const storedState = JSON.parse(stateCookie.value);
|
|
255
|
+
if (storedState.state !== state) {
|
|
256
|
+
return import_server.NextResponse.redirect(`${config.baseUrl}/auth/error?error=state_mismatch`);
|
|
257
|
+
}
|
|
258
|
+
const tokenResponse = await fetch(`https://${config.domain}/token`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
261
|
+
body: new URLSearchParams({
|
|
262
|
+
grant_type: "authorization_code",
|
|
263
|
+
client_id: config.clientId,
|
|
264
|
+
...config.clientSecret ? { client_secret: config.clientSecret } : {},
|
|
265
|
+
code,
|
|
266
|
+
redirect_uri: `${config.baseUrl}/api/auth/callback`,
|
|
267
|
+
code_verifier: verifierCookie.value
|
|
268
|
+
})
|
|
269
|
+
});
|
|
270
|
+
if (!tokenResponse.ok) {
|
|
271
|
+
const errorData = await tokenResponse.json().catch(() => ({}));
|
|
272
|
+
console.error("Token exchange failed:", errorData);
|
|
273
|
+
return import_server.NextResponse.redirect(`${config.baseUrl}/auth/error?error=token_exchange_failed`);
|
|
274
|
+
}
|
|
275
|
+
const tokens = await tokenResponse.json();
|
|
276
|
+
const idToken = jose2.decodeJwt(tokens.id_token);
|
|
277
|
+
const session = {
|
|
278
|
+
user: {
|
|
279
|
+
sub: idToken.sub,
|
|
280
|
+
email: idToken.email,
|
|
281
|
+
emailVerified: idToken.email_verified,
|
|
282
|
+
name: idToken.name,
|
|
283
|
+
givenName: idToken.given_name,
|
|
284
|
+
familyName: idToken.family_name,
|
|
285
|
+
picture: idToken.picture
|
|
286
|
+
},
|
|
287
|
+
accessToken: tokens.access_token,
|
|
288
|
+
refreshToken: tokens.refresh_token,
|
|
289
|
+
idToken: tokens.id_token,
|
|
290
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
291
|
+
};
|
|
292
|
+
cookieStore.set(
|
|
293
|
+
config.cookieName || "transactional_session",
|
|
294
|
+
Buffer.from(JSON.stringify(session)).toString("base64"),
|
|
295
|
+
{
|
|
296
|
+
httpOnly: true,
|
|
297
|
+
secure: config.cookieOptions?.secure ?? process.env.NODE_ENV === "production",
|
|
298
|
+
sameSite: config.cookieOptions?.sameSite ?? "lax",
|
|
299
|
+
maxAge: config.cookieOptions?.maxAge ?? 7 * 24 * 60 * 60,
|
|
300
|
+
path: "/"
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
cookieStore.delete("transactional_pkce_verifier");
|
|
304
|
+
cookieStore.delete("transactional_auth_state");
|
|
305
|
+
return import_server.NextResponse.redirect(`${config.baseUrl}${storedState.returnTo || "/"}`);
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function handleLogout(options) {
|
|
309
|
+
return async (request) => {
|
|
310
|
+
const config = getConfig();
|
|
311
|
+
const cookieStore = await (0, import_headers2.cookies)();
|
|
312
|
+
const sessionCookie = cookieStore.get(config.cookieName || "transactional_session");
|
|
313
|
+
let idToken;
|
|
314
|
+
if (sessionCookie?.value) {
|
|
315
|
+
try {
|
|
316
|
+
const session = JSON.parse(
|
|
317
|
+
Buffer.from(sessionCookie.value, "base64").toString("utf-8")
|
|
318
|
+
);
|
|
319
|
+
idToken = session.idToken;
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
cookieStore.delete(config.cookieName || "transactional_session");
|
|
324
|
+
const returnTo = options?.returnTo || request.nextUrl.searchParams.get("returnTo") || config.baseUrl || "/";
|
|
325
|
+
const logoutUrl = new URL(`https://${config.domain}/session/end`);
|
|
326
|
+
logoutUrl.searchParams.set("post_logout_redirect_uri", returnTo);
|
|
327
|
+
if (idToken) {
|
|
328
|
+
logoutUrl.searchParams.set("id_token_hint", idToken);
|
|
329
|
+
}
|
|
330
|
+
return import_server.NextResponse.redirect(logoutUrl.toString());
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function handleSession() {
|
|
334
|
+
return async () => {
|
|
335
|
+
const config = getConfig();
|
|
336
|
+
const cookieStore = await (0, import_headers2.cookies)();
|
|
337
|
+
const sessionCookie = cookieStore.get(config.cookieName || "transactional_session");
|
|
338
|
+
if (!sessionCookie?.value) {
|
|
339
|
+
return import_server.NextResponse.json({ session: null });
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const session = JSON.parse(
|
|
343
|
+
Buffer.from(sessionCookie.value, "base64").toString("utf-8")
|
|
344
|
+
);
|
|
345
|
+
if (session.expiresAt < Date.now() / 1e3) {
|
|
346
|
+
return import_server.NextResponse.json({ session: null });
|
|
347
|
+
}
|
|
348
|
+
return import_server.NextResponse.json({
|
|
349
|
+
session: {
|
|
350
|
+
user: session.user,
|
|
351
|
+
expiresAt: session.expiresAt
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
} catch {
|
|
355
|
+
return import_server.NextResponse.json({ session: null });
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
360
|
+
0 && (module.exports = {
|
|
361
|
+
getAccessToken,
|
|
362
|
+
getSession,
|
|
363
|
+
getUser,
|
|
364
|
+
handleCallback,
|
|
365
|
+
handleLogin,
|
|
366
|
+
handleLogout,
|
|
367
|
+
handleSession,
|
|
368
|
+
isAuthenticated
|
|
369
|
+
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfig
|
|
3
|
+
} from "../chunk-4S34DQOR.mjs";
|
|
4
|
+
|
|
5
|
+
// src/server/session.ts
|
|
6
|
+
import { cookies } from "next/headers";
|
|
7
|
+
import * as jose from "jose";
|
|
8
|
+
async function getSession() {
|
|
9
|
+
const config = getConfig();
|
|
10
|
+
const cookieStore = await cookies();
|
|
11
|
+
const sessionCookie = cookieStore.get(config.cookieName || "transactional_session");
|
|
12
|
+
if (!sessionCookie?.value) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const sessionData = JSON.parse(
|
|
17
|
+
Buffer.from(sessionCookie.value, "base64").toString("utf-8")
|
|
18
|
+
);
|
|
19
|
+
if (sessionData.expiresAt < Date.now() / 1e3) {
|
|
20
|
+
if (sessionData.refreshToken) {
|
|
21
|
+
const newSession = await refreshSession(sessionData.refreshToken);
|
|
22
|
+
if (newSession) {
|
|
23
|
+
return newSession;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return sessionData;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function getUser() {
|
|
34
|
+
const session = await getSession();
|
|
35
|
+
return session?.user || null;
|
|
36
|
+
}
|
|
37
|
+
async function getAccessToken() {
|
|
38
|
+
const session = await getSession();
|
|
39
|
+
return session?.accessToken || null;
|
|
40
|
+
}
|
|
41
|
+
async function isAuthenticated() {
|
|
42
|
+
const session = await getSession();
|
|
43
|
+
return session !== null;
|
|
44
|
+
}
|
|
45
|
+
async function refreshSession(refreshToken) {
|
|
46
|
+
const config = getConfig();
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(`https://${config.domain}/token`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
51
|
+
body: new URLSearchParams({
|
|
52
|
+
grant_type: "refresh_token",
|
|
53
|
+
client_id: config.clientId,
|
|
54
|
+
...config.clientSecret ? { client_secret: config.clientSecret } : {},
|
|
55
|
+
refresh_token: refreshToken
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const tokens = await response.json();
|
|
62
|
+
const idToken = jose.decodeJwt(tokens.id_token);
|
|
63
|
+
const session = {
|
|
64
|
+
user: {
|
|
65
|
+
sub: idToken.sub,
|
|
66
|
+
email: idToken.email,
|
|
67
|
+
emailVerified: idToken.email_verified,
|
|
68
|
+
name: idToken.name,
|
|
69
|
+
givenName: idToken.given_name,
|
|
70
|
+
familyName: idToken.family_name,
|
|
71
|
+
picture: idToken.picture
|
|
72
|
+
},
|
|
73
|
+
accessToken: tokens.access_token,
|
|
74
|
+
refreshToken: tokens.refresh_token || refreshToken,
|
|
75
|
+
idToken: tokens.id_token,
|
|
76
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
77
|
+
};
|
|
78
|
+
const cookieStore = await cookies();
|
|
79
|
+
cookieStore.set(
|
|
80
|
+
config.cookieName || "transactional_session",
|
|
81
|
+
Buffer.from(JSON.stringify(session)).toString("base64"),
|
|
82
|
+
{
|
|
83
|
+
httpOnly: true,
|
|
84
|
+
secure: config.cookieOptions?.secure ?? process.env.NODE_ENV === "production",
|
|
85
|
+
sameSite: config.cookieOptions?.sameSite ?? "lax",
|
|
86
|
+
maxAge: config.cookieOptions?.maxAge ?? 7 * 24 * 60 * 60,
|
|
87
|
+
path: "/"
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
return session;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/server/handlers.ts
|
|
97
|
+
import { cookies as cookies2 } from "next/headers";
|
|
98
|
+
import { NextResponse } from "next/server";
|
|
99
|
+
import * as jose2 from "jose";
|
|
100
|
+
function generateRandomString(length) {
|
|
101
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
102
|
+
let result = "";
|
|
103
|
+
const randomValues = new Uint8Array(length);
|
|
104
|
+
crypto.getRandomValues(randomValues);
|
|
105
|
+
for (let i = 0; i < length; i++) {
|
|
106
|
+
result += chars[randomValues[i] % chars.length];
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
async function generatePKCE() {
|
|
111
|
+
const verifier = generateRandomString(64);
|
|
112
|
+
const encoder = new TextEncoder();
|
|
113
|
+
const data = encoder.encode(verifier);
|
|
114
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
115
|
+
const challenge = Buffer.from(hash).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
116
|
+
return { verifier, challenge };
|
|
117
|
+
}
|
|
118
|
+
function handleLogin(options) {
|
|
119
|
+
return async (request) => {
|
|
120
|
+
const config = getConfig();
|
|
121
|
+
const { verifier, challenge } = await generatePKCE();
|
|
122
|
+
const state = generateRandomString(32);
|
|
123
|
+
const returnTo = options?.returnTo || request.nextUrl.searchParams.get("returnTo") || "/";
|
|
124
|
+
const cookieStore = await cookies2();
|
|
125
|
+
cookieStore.set("transactional_pkce_verifier", verifier, {
|
|
126
|
+
httpOnly: true,
|
|
127
|
+
secure: process.env.NODE_ENV === "production",
|
|
128
|
+
sameSite: "lax",
|
|
129
|
+
maxAge: 300,
|
|
130
|
+
// 5 minutes
|
|
131
|
+
path: "/"
|
|
132
|
+
});
|
|
133
|
+
cookieStore.set("transactional_auth_state", JSON.stringify({ state, returnTo }), {
|
|
134
|
+
httpOnly: true,
|
|
135
|
+
secure: process.env.NODE_ENV === "production",
|
|
136
|
+
sameSite: "lax",
|
|
137
|
+
maxAge: 300,
|
|
138
|
+
path: "/"
|
|
139
|
+
});
|
|
140
|
+
const authUrl = new URL(`https://${config.domain}/authorize`);
|
|
141
|
+
authUrl.searchParams.set("client_id", config.clientId);
|
|
142
|
+
authUrl.searchParams.set("redirect_uri", `${config.baseUrl}/api/auth/callback`);
|
|
143
|
+
authUrl.searchParams.set("response_type", "code");
|
|
144
|
+
authUrl.searchParams.set("scope", config.scope || "openid profile email");
|
|
145
|
+
authUrl.searchParams.set("state", state);
|
|
146
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
147
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
148
|
+
if (options?.connection) {
|
|
149
|
+
authUrl.searchParams.set("connection", options.connection);
|
|
150
|
+
}
|
|
151
|
+
if (options?.loginHint) {
|
|
152
|
+
authUrl.searchParams.set("login_hint", options.loginHint);
|
|
153
|
+
}
|
|
154
|
+
if (config.audience) {
|
|
155
|
+
authUrl.searchParams.set("audience", config.audience);
|
|
156
|
+
}
|
|
157
|
+
return NextResponse.redirect(authUrl.toString());
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function handleCallback() {
|
|
161
|
+
return async (request) => {
|
|
162
|
+
const config = getConfig();
|
|
163
|
+
const searchParams = request.nextUrl.searchParams;
|
|
164
|
+
const code = searchParams.get("code");
|
|
165
|
+
const state = searchParams.get("state");
|
|
166
|
+
const error = searchParams.get("error");
|
|
167
|
+
if (error) {
|
|
168
|
+
const errorDescription = searchParams.get("error_description") || error;
|
|
169
|
+
return NextResponse.redirect(
|
|
170
|
+
`${config.baseUrl}/auth/error?error=${encodeURIComponent(errorDescription)}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (!code || !state) {
|
|
174
|
+
return NextResponse.redirect(`${config.baseUrl}/auth/error?error=missing_code_or_state`);
|
|
175
|
+
}
|
|
176
|
+
const cookieStore = await cookies2();
|
|
177
|
+
const stateCookie = cookieStore.get("transactional_auth_state");
|
|
178
|
+
const verifierCookie = cookieStore.get("transactional_pkce_verifier");
|
|
179
|
+
if (!stateCookie?.value || !verifierCookie?.value) {
|
|
180
|
+
return NextResponse.redirect(`${config.baseUrl}/auth/error?error=missing_state_cookie`);
|
|
181
|
+
}
|
|
182
|
+
const storedState = JSON.parse(stateCookie.value);
|
|
183
|
+
if (storedState.state !== state) {
|
|
184
|
+
return NextResponse.redirect(`${config.baseUrl}/auth/error?error=state_mismatch`);
|
|
185
|
+
}
|
|
186
|
+
const tokenResponse = await fetch(`https://${config.domain}/token`, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
189
|
+
body: new URLSearchParams({
|
|
190
|
+
grant_type: "authorization_code",
|
|
191
|
+
client_id: config.clientId,
|
|
192
|
+
...config.clientSecret ? { client_secret: config.clientSecret } : {},
|
|
193
|
+
code,
|
|
194
|
+
redirect_uri: `${config.baseUrl}/api/auth/callback`,
|
|
195
|
+
code_verifier: verifierCookie.value
|
|
196
|
+
})
|
|
197
|
+
});
|
|
198
|
+
if (!tokenResponse.ok) {
|
|
199
|
+
const errorData = await tokenResponse.json().catch(() => ({}));
|
|
200
|
+
console.error("Token exchange failed:", errorData);
|
|
201
|
+
return NextResponse.redirect(`${config.baseUrl}/auth/error?error=token_exchange_failed`);
|
|
202
|
+
}
|
|
203
|
+
const tokens = await tokenResponse.json();
|
|
204
|
+
const idToken = jose2.decodeJwt(tokens.id_token);
|
|
205
|
+
const session = {
|
|
206
|
+
user: {
|
|
207
|
+
sub: idToken.sub,
|
|
208
|
+
email: idToken.email,
|
|
209
|
+
emailVerified: idToken.email_verified,
|
|
210
|
+
name: idToken.name,
|
|
211
|
+
givenName: idToken.given_name,
|
|
212
|
+
familyName: idToken.family_name,
|
|
213
|
+
picture: idToken.picture
|
|
214
|
+
},
|
|
215
|
+
accessToken: tokens.access_token,
|
|
216
|
+
refreshToken: tokens.refresh_token,
|
|
217
|
+
idToken: tokens.id_token,
|
|
218
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
219
|
+
};
|
|
220
|
+
cookieStore.set(
|
|
221
|
+
config.cookieName || "transactional_session",
|
|
222
|
+
Buffer.from(JSON.stringify(session)).toString("base64"),
|
|
223
|
+
{
|
|
224
|
+
httpOnly: true,
|
|
225
|
+
secure: config.cookieOptions?.secure ?? process.env.NODE_ENV === "production",
|
|
226
|
+
sameSite: config.cookieOptions?.sameSite ?? "lax",
|
|
227
|
+
maxAge: config.cookieOptions?.maxAge ?? 7 * 24 * 60 * 60,
|
|
228
|
+
path: "/"
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
cookieStore.delete("transactional_pkce_verifier");
|
|
232
|
+
cookieStore.delete("transactional_auth_state");
|
|
233
|
+
return NextResponse.redirect(`${config.baseUrl}${storedState.returnTo || "/"}`);
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function handleLogout(options) {
|
|
237
|
+
return async (request) => {
|
|
238
|
+
const config = getConfig();
|
|
239
|
+
const cookieStore = await cookies2();
|
|
240
|
+
const sessionCookie = cookieStore.get(config.cookieName || "transactional_session");
|
|
241
|
+
let idToken;
|
|
242
|
+
if (sessionCookie?.value) {
|
|
243
|
+
try {
|
|
244
|
+
const session = JSON.parse(
|
|
245
|
+
Buffer.from(sessionCookie.value, "base64").toString("utf-8")
|
|
246
|
+
);
|
|
247
|
+
idToken = session.idToken;
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
cookieStore.delete(config.cookieName || "transactional_session");
|
|
252
|
+
const returnTo = options?.returnTo || request.nextUrl.searchParams.get("returnTo") || config.baseUrl || "/";
|
|
253
|
+
const logoutUrl = new URL(`https://${config.domain}/session/end`);
|
|
254
|
+
logoutUrl.searchParams.set("post_logout_redirect_uri", returnTo);
|
|
255
|
+
if (idToken) {
|
|
256
|
+
logoutUrl.searchParams.set("id_token_hint", idToken);
|
|
257
|
+
}
|
|
258
|
+
return NextResponse.redirect(logoutUrl.toString());
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function handleSession() {
|
|
262
|
+
return async () => {
|
|
263
|
+
const config = getConfig();
|
|
264
|
+
const cookieStore = await cookies2();
|
|
265
|
+
const sessionCookie = cookieStore.get(config.cookieName || "transactional_session");
|
|
266
|
+
if (!sessionCookie?.value) {
|
|
267
|
+
return NextResponse.json({ session: null });
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const session = JSON.parse(
|
|
271
|
+
Buffer.from(sessionCookie.value, "base64").toString("utf-8")
|
|
272
|
+
);
|
|
273
|
+
if (session.expiresAt < Date.now() / 1e3) {
|
|
274
|
+
return NextResponse.json({ session: null });
|
|
275
|
+
}
|
|
276
|
+
return NextResponse.json({
|
|
277
|
+
session: {
|
|
278
|
+
user: session.user,
|
|
279
|
+
expiresAt: session.expiresAt
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
} catch {
|
|
283
|
+
return NextResponse.json({ session: null });
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
export {
|
|
288
|
+
getAccessToken,
|
|
289
|
+
getSession,
|
|
290
|
+
getUser,
|
|
291
|
+
handleCallback,
|
|
292
|
+
handleLogin,
|
|
293
|
+
handleLogout,
|
|
294
|
+
handleSession,
|
|
295
|
+
isAuthenticated
|
|
296
|
+
};
|