@authaz/next 0.0.1 → 1.0.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 +29 -174
- package/dist/index.d.ts +209 -11
- package/dist/index.js +434 -22
- package/package.json +66 -50
- package/CHANGELOG.md +0 -64
- package/CLAUDE.md +0 -118
- package/docs/ENVIRONMENT-CONFIG.md +0 -171
- package/docs/README-AXIOS-INSTANCE.md +0 -276
- package/docs/REFACTORING.md +0 -163
- package/docs/STATUS-CODES.md +0 -141
- package/jest.config.js +0 -25
- package/src/index.tsx +0 -34
- package/tsconfig.json +0 -12
- package/tsdown.config.ts +0 -21
package/dist/index.js
CHANGED
|
@@ -1,30 +1,442 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { AuthazError, COOKIE_NAMES, createAuthazClient, fetchUserinfo, getSecureCookieOptions, isOk, mapUserinfoToUser, timingSafeCompare } from "@authaz/sdk";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
3
|
|
|
4
4
|
//#region src/index.tsx
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Lazy load next/headers at runtime to avoid MODULE_NOT_FOUND on Vercel/serverless
|
|
7
|
+
* when the package is resolved from a different node_modules context (e.g. pnpm).
|
|
8
|
+
*/
|
|
9
|
+
const getCookieStore = async () => {
|
|
10
|
+
const { cookies } = await import("next/headers");
|
|
11
|
+
return cookies();
|
|
12
|
+
};
|
|
13
|
+
const isRequestHttps = (request) => {
|
|
14
|
+
if (request.headers.get("x-forwarded-proto") === "https") return true;
|
|
15
|
+
return new URL(request.url).protocol === "https:";
|
|
16
|
+
};
|
|
17
|
+
const toNextCookieOptions = (options) => {
|
|
18
|
+
return {
|
|
19
|
+
httpOnly: options.httpOnly,
|
|
20
|
+
secure: options.secure,
|
|
21
|
+
sameSite: options.sameSite,
|
|
22
|
+
path: options.path,
|
|
23
|
+
maxAge: options.maxAge
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Creates the Authaz Next.js authentication handler.
|
|
28
|
+
*
|
|
29
|
+
* This creates a route handler that manages the complete OAuth flow:
|
|
30
|
+
* - GET /api/auth/login - Redirects to Universal Login
|
|
31
|
+
* - POST /api/auth/callback - Handles OAuth callback (receives code via form POST)
|
|
32
|
+
* - POST /api/auth/logout - Clears session and redirects to logout (POST-only for CSRF protection)
|
|
33
|
+
* - GET /api/auth/me - Returns current user info (requires valid session)
|
|
34
|
+
* - POST /api/auth/refresh - Refreshes the access token
|
|
35
|
+
*
|
|
36
|
+
* IMPORTANT: The OAuth callback from the identity provider arrives as GET.
|
|
37
|
+
* You need a callback page that POSTs to /api/auth/callback:
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* // app/auth/callback/page.tsx
|
|
41
|
+
* 'use client';
|
|
42
|
+
* import { useEffect } from 'react';
|
|
43
|
+
* import { useSearchParams } from 'next/navigation';
|
|
44
|
+
*
|
|
45
|
+
* export default function CallbackPage() {
|
|
46
|
+
* const searchParams = useSearchParams();
|
|
47
|
+
*
|
|
48
|
+
* useEffect(() => {
|
|
49
|
+
* const form = document.createElement('form');
|
|
50
|
+
* form.method = 'POST';
|
|
51
|
+
* form.action = '/api/auth/callback';
|
|
52
|
+
*
|
|
53
|
+
* const code = searchParams.get('code');
|
|
54
|
+
* const state = searchParams.get('state');
|
|
55
|
+
* const error = searchParams.get('error');
|
|
56
|
+
*
|
|
57
|
+
* ['code', 'state', 'error', 'error_description'].forEach(param => {
|
|
58
|
+
* const value = searchParams.get(param);
|
|
59
|
+
* if (value) {
|
|
60
|
+
* const input = document.createElement('input');
|
|
61
|
+
* input.type = 'hidden';
|
|
62
|
+
* input.name = param;
|
|
63
|
+
* input.value = value;
|
|
64
|
+
* form.appendChild(input);
|
|
65
|
+
* }
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* document.body.appendChild(form);
|
|
69
|
+
* form.submit();
|
|
70
|
+
* }, [searchParams]);
|
|
71
|
+
*
|
|
72
|
+
* return <div>Completing login...</div>;
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // app/api/auth/[...authaz]/route.ts
|
|
79
|
+
* import { createAuthazHandler } from '@authaz/next';
|
|
80
|
+
*
|
|
81
|
+
* export const { GET, POST } = createAuthazHandler({
|
|
82
|
+
* clientId: process.env.AUTHAZ_CLIENT_ID!,
|
|
83
|
+
* clientSecret: process.env.AUTHAZ_CLIENT_SECRET!,
|
|
84
|
+
* tenantId: process.env.AUTHAZ_TENANT_ID!,
|
|
85
|
+
* organizationId: process.env.AUTHAZ_ORGANIZATION_ID!,
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
const createAuthazHandler = (config) => {
|
|
90
|
+
let _client = null;
|
|
91
|
+
const getClient = () => {
|
|
92
|
+
if (!_client) _client = createAuthazClient(config);
|
|
93
|
+
return _client;
|
|
94
|
+
};
|
|
95
|
+
const afterLoginUrl = config.afterLoginUrl || "/";
|
|
96
|
+
const afterLogoutUrl = config.afterLogoutUrl || "/";
|
|
97
|
+
const authazDomain = config.authazDomain || "https://authaz.com";
|
|
98
|
+
const fixedRedirectUri = config.redirectUri;
|
|
99
|
+
const isDebug = config.debug || false;
|
|
100
|
+
const apiKey = config.apiKey || config.clientSecret;
|
|
101
|
+
const log = (message, data) => {
|
|
102
|
+
if (isDebug) console.log(`[authaz-next] ${message}`, data || "");
|
|
103
|
+
};
|
|
104
|
+
const logError = (message, error) => {
|
|
105
|
+
if (isDebug) console.error(`[authaz-next] ${message}`, error || "");
|
|
106
|
+
};
|
|
107
|
+
const getAction = (request) => {
|
|
108
|
+
const url = new URL(request.url);
|
|
109
|
+
const pathParts = url.pathname.split("/");
|
|
110
|
+
const action = pathParts[pathParts.length - 1];
|
|
111
|
+
log(`getAction: pathname=${url.pathname}, action=${action}`);
|
|
112
|
+
return action;
|
|
113
|
+
};
|
|
114
|
+
const getBaseUrl = (request) => {
|
|
115
|
+
const url = new URL(request.url);
|
|
116
|
+
return `${url.protocol}//${url.host}`;
|
|
117
|
+
};
|
|
118
|
+
const handleLogin = async (request) => {
|
|
119
|
+
log("Starting login flow");
|
|
120
|
+
let redirectUri;
|
|
121
|
+
if (fixedRedirectUri) {
|
|
122
|
+
redirectUri = fixedRedirectUri;
|
|
123
|
+
log("Using fixed redirectUri from config", { redirectUri });
|
|
124
|
+
} else {
|
|
125
|
+
const baseUrl = getBaseUrl(request);
|
|
126
|
+
const callbackPage = new URL(request.url).searchParams.get("callbackUrl") || "/auth/callback";
|
|
127
|
+
redirectUri = callbackPage.startsWith("http") ? callbackPage : `${baseUrl}${callbackPage}`;
|
|
128
|
+
log("Using dynamic redirectUri", { redirectUri });
|
|
129
|
+
}
|
|
130
|
+
const result = await getClient().auth.getLoginUrl(redirectUri);
|
|
131
|
+
if (!isOk(result)) {
|
|
132
|
+
logError("Failed to generate login URL", result.error);
|
|
133
|
+
return NextResponse.json({ error: "Failed to generate login URL" }, { status: 500 });
|
|
134
|
+
}
|
|
135
|
+
const loginResult = result.data;
|
|
136
|
+
const response = NextResponse.redirect(loginResult.url);
|
|
137
|
+
const oauthCookieOptions = toNextCookieOptions(getSecureCookieOptions(600, isRequestHttps(request)));
|
|
138
|
+
response.cookies.set(COOKIE_NAMES.CODE_VERIFIER, loginResult.codeVerifier, oauthCookieOptions);
|
|
139
|
+
response.cookies.set(COOKIE_NAMES.STATE, loginResult.state, oauthCookieOptions);
|
|
140
|
+
response.cookies.set(COOKIE_NAMES.NONCE, loginResult.nonce, oauthCookieOptions);
|
|
141
|
+
log("Redirecting to Universal Login", { url: loginResult.url });
|
|
142
|
+
return response;
|
|
143
|
+
};
|
|
144
|
+
const handleCallback = async (request) => {
|
|
145
|
+
log("Handling OAuth callback");
|
|
146
|
+
const formData = await request.formData();
|
|
147
|
+
const code = formData.get("code");
|
|
148
|
+
const state = formData.get("state");
|
|
149
|
+
const error = formData.get("error");
|
|
150
|
+
const errorDescription = formData.get("error_description");
|
|
151
|
+
log("Callback params", {
|
|
152
|
+
code: code?.substring(0, 10),
|
|
153
|
+
state: state?.substring(0, 10),
|
|
154
|
+
error
|
|
155
|
+
});
|
|
156
|
+
const baseUrl = getBaseUrl(request);
|
|
157
|
+
if (error) {
|
|
158
|
+
logError("OAuth error", {
|
|
159
|
+
error,
|
|
160
|
+
errorDescription
|
|
161
|
+
});
|
|
162
|
+
return NextResponse.redirect(`${baseUrl}${afterLoginUrl}?error=${encodeURIComponent(error)}`);
|
|
163
|
+
}
|
|
164
|
+
if (!code) {
|
|
165
|
+
logError("Missing authorization code");
|
|
166
|
+
return NextResponse.redirect(`${baseUrl}${afterLoginUrl}?error=missing_code`);
|
|
167
|
+
}
|
|
168
|
+
const cookieStore = await getCookieStore();
|
|
169
|
+
const codeVerifier = cookieStore.get(COOKIE_NAMES.CODE_VERIFIER)?.value;
|
|
170
|
+
const storedState = cookieStore.get(COOKIE_NAMES.STATE)?.value;
|
|
171
|
+
log("All cookies", cookieStore.getAll().map((c) => c.name));
|
|
172
|
+
if (!codeVerifier) {
|
|
173
|
+
logError("Missing code verifier cookie");
|
|
174
|
+
return NextResponse.redirect(`${baseUrl}${afterLoginUrl}?error=missing_verifier`);
|
|
175
|
+
}
|
|
176
|
+
if (!storedState || !state || !timingSafeCompare(state, storedState)) {
|
|
177
|
+
logError("State mismatch", {
|
|
178
|
+
received: state,
|
|
179
|
+
stored: storedState
|
|
180
|
+
});
|
|
181
|
+
return NextResponse.redirect(`${baseUrl}${afterLoginUrl}?error=state_mismatch`);
|
|
182
|
+
}
|
|
183
|
+
const redirectUri = fixedRedirectUri || `${baseUrl}/auth/redirect`;
|
|
184
|
+
log("Using redirectUri for token exchange", { redirectUri });
|
|
185
|
+
const result = await getClient().auth.exchangeCode(code, codeVerifier, redirectUri);
|
|
186
|
+
if (!isOk(result)) {
|
|
187
|
+
logError("Token exchange failed", result.error);
|
|
188
|
+
return NextResponse.redirect(`${baseUrl}${afterLoginUrl}?error=token_exchange_failed`);
|
|
189
|
+
}
|
|
190
|
+
const tokens = result.data;
|
|
191
|
+
log("Token exchange successful");
|
|
192
|
+
const redirectUrl = `${baseUrl}${afterLoginUrl}`;
|
|
193
|
+
const response = NextResponse.redirect(redirectUrl, { status: 303 });
|
|
194
|
+
const isHttps = isRequestHttps(request);
|
|
195
|
+
const accessTokenOptions = toNextCookieOptions(getSecureCookieOptions(tokens.expiresIn || 3600, isHttps));
|
|
196
|
+
const refreshTokenOptions = toNextCookieOptions(getSecureCookieOptions(3600 * 24 * 30, isHttps));
|
|
197
|
+
log("Setting cookies", {
|
|
198
|
+
isHttps,
|
|
199
|
+
secure: accessTokenOptions.secure,
|
|
200
|
+
sameSite: accessTokenOptions.sameSite,
|
|
201
|
+
accessTokenLength: tokens.accessToken?.length,
|
|
202
|
+
redirectUrl: `${baseUrl}${afterLoginUrl}`
|
|
203
|
+
});
|
|
204
|
+
response.cookies.set(COOKIE_NAMES.ACCESS_TOKEN, tokens.accessToken, accessTokenOptions);
|
|
205
|
+
if (tokens.refreshToken) response.cookies.set(COOKIE_NAMES.REFRESH_TOKEN, tokens.refreshToken, refreshTokenOptions);
|
|
206
|
+
response.cookies.delete(COOKIE_NAMES.CODE_VERIFIER);
|
|
207
|
+
response.cookies.delete(COOKIE_NAMES.STATE);
|
|
208
|
+
response.cookies.delete(COOKIE_NAMES.NONCE);
|
|
209
|
+
return response;
|
|
210
|
+
};
|
|
211
|
+
const handleLogout = async (request) => {
|
|
212
|
+
log("Handling logout");
|
|
213
|
+
const baseUrl = getBaseUrl(request);
|
|
214
|
+
const postLogoutRedirectUri = `${baseUrl}${afterLogoutUrl}`;
|
|
215
|
+
const result = getClient().auth.getLogoutUrl(postLogoutRedirectUri);
|
|
216
|
+
if (!isOk(result)) {
|
|
217
|
+
logError("Failed to generate logout URL", result.error);
|
|
218
|
+
const response$1 = NextResponse.redirect(`${baseUrl}${afterLogoutUrl}`);
|
|
219
|
+
response$1.cookies.delete(COOKIE_NAMES.ACCESS_TOKEN);
|
|
220
|
+
response$1.cookies.delete(COOKIE_NAMES.REFRESH_TOKEN);
|
|
221
|
+
return response$1;
|
|
222
|
+
}
|
|
223
|
+
const response = NextResponse.redirect(result.data);
|
|
224
|
+
response.cookies.delete(COOKIE_NAMES.ACCESS_TOKEN);
|
|
225
|
+
response.cookies.delete(COOKIE_NAMES.REFRESH_TOKEN);
|
|
226
|
+
response.cookies.delete(COOKIE_NAMES.CODE_VERIFIER);
|
|
227
|
+
response.cookies.delete(COOKIE_NAMES.STATE);
|
|
228
|
+
response.cookies.delete(COOKIE_NAMES.NONCE);
|
|
229
|
+
return response;
|
|
230
|
+
};
|
|
231
|
+
const handleMe = async () => {
|
|
232
|
+
log("Getting current user");
|
|
233
|
+
const accessToken = (await getCookieStore()).get(COOKIE_NAMES.ACCESS_TOKEN)?.value;
|
|
234
|
+
if (!accessToken) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
235
|
+
const userinfo = await fetchUserinfo(authazDomain, accessToken, apiKey);
|
|
236
|
+
if (!userinfo) {
|
|
237
|
+
logError("Failed to fetch userinfo");
|
|
238
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
239
|
+
}
|
|
240
|
+
const user = mapUserinfoToUser(userinfo);
|
|
241
|
+
return NextResponse.json({
|
|
242
|
+
authenticated: true,
|
|
243
|
+
user
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
const handleRefresh = async (request) => {
|
|
247
|
+
log("Handling token refresh");
|
|
248
|
+
const refreshToken = (await getCookieStore()).get(COOKIE_NAMES.REFRESH_TOKEN)?.value;
|
|
249
|
+
if (!refreshToken) return NextResponse.json({ error: "No refresh token" }, { status: 401 });
|
|
250
|
+
const result = await getClient().auth.refreshTokens(refreshToken);
|
|
251
|
+
if (!isOk(result)) {
|
|
252
|
+
logError("Token refresh failed", result.error);
|
|
253
|
+
const response$1 = NextResponse.json({ error: "Refresh failed" }, { status: 401 });
|
|
254
|
+
response$1.cookies.delete(COOKIE_NAMES.ACCESS_TOKEN);
|
|
255
|
+
response$1.cookies.delete(COOKIE_NAMES.REFRESH_TOKEN);
|
|
256
|
+
return response$1;
|
|
257
|
+
}
|
|
258
|
+
const tokens = result.data;
|
|
259
|
+
log("Token refresh successful");
|
|
260
|
+
const response = NextResponse.json({ success: true });
|
|
261
|
+
const isHttps = isRequestHttps(request);
|
|
262
|
+
const accessTokenOptions = toNextCookieOptions(getSecureCookieOptions(tokens.expiresIn || 3600, isHttps));
|
|
263
|
+
const refreshTokenOptions = toNextCookieOptions(getSecureCookieOptions(3600 * 24 * 30, isHttps));
|
|
264
|
+
response.cookies.set(COOKIE_NAMES.ACCESS_TOKEN, tokens.accessToken, accessTokenOptions);
|
|
265
|
+
if (tokens.refreshToken) response.cookies.set(COOKIE_NAMES.REFRESH_TOKEN, tokens.refreshToken, refreshTokenOptions);
|
|
266
|
+
return response;
|
|
267
|
+
};
|
|
268
|
+
const GET = async (request) => {
|
|
269
|
+
const action = getAction(request);
|
|
270
|
+
switch (action) {
|
|
271
|
+
case "login": return handleLogin(request);
|
|
272
|
+
case "me": return handleMe();
|
|
273
|
+
case "callback":
|
|
274
|
+
case "logout":
|
|
275
|
+
case "refresh": return NextResponse.json({ error: "Method not allowed. Use POST." }, { status: 405 });
|
|
276
|
+
default: return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 404 });
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const POST = async (request) => {
|
|
280
|
+
const action = getAction(request);
|
|
281
|
+
switch (action) {
|
|
282
|
+
case "callback": return handleCallback(request);
|
|
283
|
+
case "logout": return handleLogout(request);
|
|
284
|
+
case "refresh": return handleRefresh(request);
|
|
285
|
+
case "login":
|
|
286
|
+
case "me": return NextResponse.json({ error: "Method not allowed. Use GET." }, { status: 405 });
|
|
287
|
+
default: return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 404 });
|
|
288
|
+
}
|
|
22
289
|
};
|
|
23
290
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
291
|
+
GET,
|
|
292
|
+
POST
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* Gets the current access token from cookies.
|
|
297
|
+
*/
|
|
298
|
+
const getAccessToken = async () => {
|
|
299
|
+
return (await getCookieStore()).get(COOKIE_NAMES.ACCESS_TOKEN)?.value || null;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Gets the current refresh token from cookies.
|
|
303
|
+
*/
|
|
304
|
+
const getRefreshToken = async () => {
|
|
305
|
+
return (await getCookieStore()).get(COOKIE_NAMES.REFRESH_TOKEN)?.value || null;
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Checks if the user is authenticated (has an access token).
|
|
309
|
+
*/
|
|
310
|
+
const isAuthenticated = async () => {
|
|
311
|
+
return await getAccessToken() !== null;
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* Creates helper functions that require the authazDomain for API calls.
|
|
315
|
+
*/
|
|
316
|
+
const createAuthazHelpers = (config) => {
|
|
317
|
+
const authazDomain = config.authazDomain || "https://authaz.com";
|
|
318
|
+
const apiKey = config.apiKey || config.clientSecret;
|
|
319
|
+
const getUser = async () => {
|
|
320
|
+
const accessToken = (await getCookieStore()).get(COOKIE_NAMES.ACCESS_TOKEN)?.value;
|
|
321
|
+
if (!accessToken) return null;
|
|
322
|
+
const userinfo = await fetchUserinfo(authazDomain, accessToken, apiKey);
|
|
323
|
+
if (!userinfo) return null;
|
|
324
|
+
return mapUserinfoToUser(userinfo);
|
|
325
|
+
};
|
|
326
|
+
return { getUser };
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* Creates middleware config for protecting routes in Next.js middleware.ts
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* // middleware.ts
|
|
334
|
+
* import { createAuthMiddleware } from '@authaz/next';
|
|
335
|
+
*
|
|
336
|
+
* export const middleware = createAuthMiddleware({
|
|
337
|
+
* publicPaths: ['/', '/login', '/api/auth'],
|
|
338
|
+
* loginPath: '/api/auth/login',
|
|
339
|
+
* });
|
|
340
|
+
*
|
|
341
|
+
* export const config = {
|
|
342
|
+
* matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
343
|
+
* };
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
const createAuthMiddleware = (options) => {
|
|
347
|
+
const publicPaths = options.publicPaths || [];
|
|
348
|
+
const loginPath = options.loginPath || "/api/auth/login";
|
|
349
|
+
const isPublicPath = (pathname) => {
|
|
350
|
+
return publicPaths.some((pattern) => {
|
|
351
|
+
if (pattern.endsWith("*")) return pathname.startsWith(pattern.slice(0, -1));
|
|
352
|
+
return pathname === pattern;
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
return async (request) => {
|
|
356
|
+
const pathname = request.nextUrl.pathname;
|
|
357
|
+
if (isPublicPath(pathname)) return NextResponse.next();
|
|
358
|
+
if (!request.cookies.get(COOKIE_NAMES.ACCESS_TOKEN)?.value) {
|
|
359
|
+
const loginUrl = new URL(loginPath, request.url);
|
|
360
|
+
loginUrl.searchParams.set("returnTo", pathname);
|
|
361
|
+
return NextResponse.redirect(loginUrl);
|
|
362
|
+
}
|
|
363
|
+
return NextResponse.next();
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Wrapper for API route handlers that require authentication.
|
|
368
|
+
* Returns 401 if not authenticated.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```typescript
|
|
372
|
+
* // app/api/protected/route.ts
|
|
373
|
+
* import { withAuth } from '@authaz/next';
|
|
374
|
+
*
|
|
375
|
+
* export const GET = withAuth(async (request) => {
|
|
376
|
+
* // User is authenticated
|
|
377
|
+
* return Response.json({ message: 'Protected data' });
|
|
378
|
+
* });
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
const withAuth = (handler) => {
|
|
382
|
+
return async (request) => {
|
|
383
|
+
if (!request.cookies.get(COOKIE_NAMES.ACCESS_TOKEN)?.value) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
384
|
+
return handler(request);
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* Server Component helper that throws redirect if not authenticated.
|
|
389
|
+
* Use in Server Components to protect pages.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```typescript
|
|
393
|
+
* // app/dashboard/page.tsx
|
|
394
|
+
* import { requireAuth } from '@authaz/next';
|
|
395
|
+
*
|
|
396
|
+
* export default async function DashboardPage() {
|
|
397
|
+
* await requireAuth(); // Redirects to login if not authenticated
|
|
398
|
+
*
|
|
399
|
+
* return <div>Dashboard</div>;
|
|
400
|
+
* }
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
const requireAuth = async (loginPath = "/api/auth/login") => {
|
|
404
|
+
const navigation = await import("next/navigation");
|
|
405
|
+
if (!await getAccessToken()) navigation.redirect(loginPath);
|
|
406
|
+
};
|
|
407
|
+
/**
|
|
408
|
+
* Server Component helper that returns user or redirects if not authenticated.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```typescript
|
|
412
|
+
* // app/profile/page.tsx
|
|
413
|
+
* import { requireUser } from '@authaz/next';
|
|
414
|
+
*
|
|
415
|
+
* const helpers = requireUser({
|
|
416
|
+
* authazDomain: 'https://authaz.com',
|
|
417
|
+
* apiKey: process.env.AUTHAZ_API_KEY!,
|
|
418
|
+
* });
|
|
419
|
+
*
|
|
420
|
+
* export default async function ProfilePage() {
|
|
421
|
+
* const user = await helpers.getOrRedirect();
|
|
422
|
+
* return <div>Hello {user.name}</div>;
|
|
423
|
+
* }
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
const requireUser = (config) => {
|
|
427
|
+
const authazDomain = config.authazDomain || "https://authaz.com";
|
|
428
|
+
const apiKey = config.apiKey || config.clientSecret;
|
|
429
|
+
const loginPath = config.loginPath || "/api/auth/login";
|
|
430
|
+
const getOrRedirect = async () => {
|
|
431
|
+
const navigation = await import("next/navigation");
|
|
432
|
+
const accessToken = (await getCookieStore()).get(COOKIE_NAMES.ACCESS_TOKEN)?.value;
|
|
433
|
+
if (!accessToken) navigation.redirect(loginPath);
|
|
434
|
+
const userinfo = await fetchUserinfo(authazDomain, accessToken, apiKey);
|
|
435
|
+
if (!userinfo) navigation.redirect(loginPath);
|
|
436
|
+
return mapUserinfoToUser(userinfo);
|
|
26
437
|
};
|
|
438
|
+
return { getOrRedirect };
|
|
27
439
|
};
|
|
28
440
|
|
|
29
441
|
//#endregion
|
|
30
|
-
export {
|
|
442
|
+
export { AuthazError, createAuthMiddleware, createAuthazHandler, createAuthazHelpers, getAccessToken, getRefreshToken, isAuthenticated, requireAuth, requireUser, withAuth };
|
package/package.json
CHANGED
|
@@ -1,52 +1,68 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"require": "./dist/index.js",
|
|
17
|
-
"types": "./dist/index.d.ts",
|
|
18
|
-
"development": "./src/index.ts"
|
|
19
|
-
}
|
|
2
|
+
"author": "@authaz",
|
|
3
|
+
"name": "@authaz/next",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "NextJS authaz SDK",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
20
16
|
},
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
17
|
+
"./*": "./dist/*"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Authaz/authaz-sdk-js.git"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"auth",
|
|
32
|
+
"authentication",
|
|
33
|
+
"authaz",
|
|
34
|
+
"sdk",
|
|
35
|
+
"react",
|
|
36
|
+
"nextjs",
|
|
37
|
+
"next"
|
|
38
|
+
],
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"next": ">=15",
|
|
41
|
+
"react": ">=17",
|
|
42
|
+
"@authaz/sdk": "^1.2.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@jest/globals": "30.2.0",
|
|
46
|
+
"@types/jest": "30.0.0",
|
|
47
|
+
"@types/node": "24.10.0",
|
|
48
|
+
"@types/react": "19.2.10",
|
|
49
|
+
"@types/react-dom": "19.2.3",
|
|
50
|
+
"jest": "30.2.0",
|
|
51
|
+
"next": "^15.5.11",
|
|
52
|
+
"react": "19.2.4",
|
|
53
|
+
"react-dom": "19.2.4",
|
|
54
|
+
"ts-jest": "29.4.5",
|
|
55
|
+
"tsdown": "0.15.12",
|
|
56
|
+
"typescript": "5.9.3"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsdown",
|
|
60
|
+
"test": "jest --config jest.config.cjs",
|
|
61
|
+
"test:watch": "jest --config jest.config.cjs --watch",
|
|
62
|
+
"test:coverage": "jest --config jest.config.cjs --coverage",
|
|
63
|
+
"release:patch": "pnpm version patch && git push origin main --follow-tags",
|
|
64
|
+
"release:minor": "pnpm version minor && git push origin main --follow-tags",
|
|
65
|
+
"release:major": "pnpm version major && git push origin main --follow-tags",
|
|
66
|
+
"release": "pnpm build && pnpm run release:patch"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
|
|
4
|
-
|
|
5
|
-
## [1.1.0] - 2024-01-XX
|
|
6
|
-
|
|
7
|
-
### ✨ Adicionado
|
|
8
|
-
- **Nova API de Configuração**: Introduzida configuração baseada em objeto para melhor organização
|
|
9
|
-
- **Validação de Configuração**: Validação automática de campos obrigatórios
|
|
10
|
-
- **Status Específicos**: Cada fluxo agora tem status específicos para diferentes cenários
|
|
11
|
-
- **Arquivo de Configuração**: Novo módulo `config.ts` para centralizar configurações
|
|
12
|
-
|
|
13
|
-
### 🔧 Melhorado
|
|
14
|
-
- **Nomenclatura Padronizada**:
|
|
15
|
-
- `SignupConfigureMfa` → `signupConfigureMfa`
|
|
16
|
-
- `SignupVerifyMfa` → `signupVerifyMfa`
|
|
17
|
-
- `acessTokenMfa` → `accessTokenMfa`
|
|
18
|
-
- **Estrutura de Respostas**: Status específicos para cada tipo de operação com cenários detalhados
|
|
19
|
-
- **Tipos de Usuário**: Unificação dos tipos de usuário com interface base `User`
|
|
20
|
-
- **URLs Dinâmicas**: Remoção de URLs hardcoded, agora configuráveis
|
|
21
|
-
|
|
22
|
-
### 🏗️ Refatorado
|
|
23
|
-
- **Construtor da ApiService**: Suporte para objeto de configuração + compatibilidade legacy
|
|
24
|
-
- **Tipos de Token**: Melhor organização e reutilização de tipos de token
|
|
25
|
-
- **Tipos de Erro**: Padronização de tipos de erro comuns
|
|
26
|
-
|
|
27
|
-
### 📚 Documentação
|
|
28
|
-
- **README Atualizado**: Exemplos de configuração moderna e legacy
|
|
29
|
-
- **Tipos Documentados**: Melhor documentação dos tipos disponíveis
|
|
30
|
-
- **Exemplos de Uso**: Adicionados exemplos com nova API de configuração
|
|
31
|
-
|
|
32
|
-
### ⚡ Compatibilidade
|
|
33
|
-
- **Backward Compatible**: Mantida compatibilidade com API legacy
|
|
34
|
-
- **Migração Suave**: Possibilidade de migrar gradualmente para nova API
|
|
35
|
-
|
|
36
|
-
## Exemplo de Migração
|
|
37
|
-
|
|
38
|
-
### Antes (Legacy)
|
|
39
|
-
```typescript
|
|
40
|
-
const client = new ApiService(
|
|
41
|
-
'https://api.authaz.com',
|
|
42
|
-
'client-id',
|
|
43
|
-
'client-secret',
|
|
44
|
-
'auth-pool-id',
|
|
45
|
-
'org-id',
|
|
46
|
-
'redirect-uri'
|
|
47
|
-
)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Depois (Moderno)
|
|
51
|
-
```typescript
|
|
52
|
-
const client = new ApiService({
|
|
53
|
-
baseUrl: 'https://api.authaz.com',
|
|
54
|
-
clientId: 'client-id',
|
|
55
|
-
clientSecret: 'client-secret',
|
|
56
|
-
authPoolId: 'auth-pool-id',
|
|
57
|
-
organizationId: 'org-id',
|
|
58
|
-
redirectUri: 'redirect-uri'
|
|
59
|
-
})
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Breaking Changes
|
|
63
|
-
|
|
64
|
-
Nenhuma breaking change nesta versão. Todas as mudanças mantêm compatibilidade com versões anteriores.
|