@tidecloak/nextjs 0.13.9 → 0.13.13
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 +31 -400
- package/dist/cjs/contexts/InternalTideCloakProvider.js +7 -3
- package/dist/cjs/contexts/TideCloakProvider.js +7 -3
- package/dist/cjs/index.js +31 -3
- package/dist/cjs/server/index.js +20 -2
- package/dist/cjs/server/routerMatcher.js +6 -2
- package/dist/cjs/server/tidecloakMiddleware.js +12 -9
- package/dist/cjs/server/tidecloakProxy.js +90 -0
- package/dist/cjs/server/tokenExchange.js +166 -0
- package/dist/esm/index.js +19 -1
- package/dist/esm/server/index.js +6 -0
- package/dist/esm/server/tidecloakProxy.js +87 -0
- package/dist/esm/server/tokenExchange.js +158 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/server/index.d.ts +5 -0
- package/dist/types/server/index.d.ts.map +1 -1
- package/dist/types/server/tidecloakProxy.d.ts +35 -0
- package/dist/types/server/tidecloakProxy.d.ts.map +1 -0
- package/dist/types/server/tokenExchange.d.ts +128 -0
- package/dist/types/server/tokenExchange.d.ts.map +1 -0
- package/package.json +13 -8
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTideCloakProxy = createTideCloakProxy;
|
|
4
|
+
const server_1 = require("next/server");
|
|
5
|
+
const verify_1 = require("@tidecloak/verify");
|
|
6
|
+
const routerMatcher_1 = require("./routerMatcher");
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
protectedRoutes: {},
|
|
9
|
+
onRequest: undefined,
|
|
10
|
+
onSuccess: undefined,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Returns a Next.js 16+ Proxy function enforcing TideCloak auth.
|
|
14
|
+
*
|
|
15
|
+
* This is the recommended approach for Next.js 16+. The proxy runs on Node.js runtime.
|
|
16
|
+
* For Edge runtime, use `createTideCloakMiddleware` instead (deprecated in Next.js 16).
|
|
17
|
+
*
|
|
18
|
+
* Example usage in your `proxy.ts`:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import tidecloakConfig from './tidecloakAdapter.json'
|
|
22
|
+
* import { createTideCloakProxy } from '@tidecloak/nextjs/server'
|
|
23
|
+
*
|
|
24
|
+
* export const proxy = createTideCloakProxy({
|
|
25
|
+
* config: tidecloakConfig,
|
|
26
|
+
* protectedRoutes: {
|
|
27
|
+
* '/admin/*': ['admin'],
|
|
28
|
+
* '/api/private/*': ['user']
|
|
29
|
+
* },
|
|
30
|
+
* onFailure: ({ token }, req) => NextResponse.redirect(new URL('/login', req.url)),
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* export const config = {
|
|
34
|
+
* matcher: ['/((?!_next|.*\\..*).*)'],
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
function createTideCloakProxy(opts) {
|
|
39
|
+
const settings = { ...DEFAULTS, ...opts };
|
|
40
|
+
// Prepare arrays of test functions for protected routes
|
|
41
|
+
const protectedTests = (0, routerMatcher_1.normalizeProtectedRoutes)(settings.protectedRoutes);
|
|
42
|
+
return async function proxy(req) {
|
|
43
|
+
var _a;
|
|
44
|
+
const path = req.nextUrl.pathname;
|
|
45
|
+
try {
|
|
46
|
+
// Extract the raw JWT from the specified cookie
|
|
47
|
+
const token = ((_a = req.cookies.get("kcToken")) === null || _a === void 0 ? void 0 : _a.value) || null;
|
|
48
|
+
// Allow custom logic before auth checks
|
|
49
|
+
if (settings.onRequest) {
|
|
50
|
+
const result = settings.onRequest({ token }, req);
|
|
51
|
+
if (result)
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
// Iterate protected routes; the first match enforces a role check
|
|
55
|
+
for (const { test, roles } of protectedTests) {
|
|
56
|
+
if (test(path, req)) {
|
|
57
|
+
// Verify signature, issuer, and presence of at least one allowed role
|
|
58
|
+
const payload = await (0, verify_1.verifyTideCloakToken)(settings.config, token, roles);
|
|
59
|
+
if (!payload) {
|
|
60
|
+
// Custom onFailure hook or default JSON response
|
|
61
|
+
if (settings.onFailure) {
|
|
62
|
+
const result = settings.onFailure({ token }, req);
|
|
63
|
+
if (result)
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
return server_1.NextResponse.json({ error: '[TideCloak Proxy] Access forbidden: invalid token' }, { status: 403 });
|
|
67
|
+
}
|
|
68
|
+
// Custom onSuccess hook if provided
|
|
69
|
+
if (settings.onSuccess) {
|
|
70
|
+
const result = settings.onSuccess({ payload }, req);
|
|
71
|
+
if (result)
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
// Token valid and role check passed
|
|
75
|
+
return server_1.NextResponse.next();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// No protected route matched; continue
|
|
79
|
+
return server_1.NextResponse.next();
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// Handle unexpected errors
|
|
83
|
+
if (settings.onError) {
|
|
84
|
+
return settings.onError(err, req);
|
|
85
|
+
}
|
|
86
|
+
console.error("[TideCloak Proxy] ", err);
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exchangeCodeForTokens = exchangeCodeForTokens;
|
|
4
|
+
exports.refreshAccessToken = refreshAccessToken;
|
|
5
|
+
exports.parseAuthCodeData = parseAuthCodeData;
|
|
6
|
+
exports.setSessionCookie = setSessionCookie;
|
|
7
|
+
exports.getSessionFromRequest = getSessionFromRequest;
|
|
8
|
+
exports.clearSessionCookie = clearSessionCookie;
|
|
9
|
+
/**
|
|
10
|
+
* Exchange an authorization code for tokens.
|
|
11
|
+
* Use this in your Next.js API route to complete the hybrid mode flow.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // app/api/auth/callback/route.ts
|
|
16
|
+
* import { exchangeCodeForTokens } from '@tidecloak/nextjs/server';
|
|
17
|
+
*
|
|
18
|
+
* export async function POST(req: NextRequest) {
|
|
19
|
+
* const body = await req.json();
|
|
20
|
+
* const authData = JSON.parse(body.accessToken);
|
|
21
|
+
*
|
|
22
|
+
* const result = await exchangeCodeForTokens({
|
|
23
|
+
* authServerUrl: process.env.TIDECLOAK_URL!,
|
|
24
|
+
* realm: process.env.TIDECLOAK_REALM!,
|
|
25
|
+
* clientId: process.env.TIDECLOAK_CLIENT_ID!,
|
|
26
|
+
* }, authData);
|
|
27
|
+
*
|
|
28
|
+
* if (!result.success) {
|
|
29
|
+
* return NextResponse.json({ error: result.error }, { status: 401 });
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* // Store tokens server-side and create session
|
|
33
|
+
* return NextResponse.json({ success: true });
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
async function exchangeCodeForTokens(config, authData) {
|
|
38
|
+
const tokenEndpoint = `${config.authServerUrl}/realms/${config.realm}/protocol/openid-connect/token`;
|
|
39
|
+
const params = new URLSearchParams({
|
|
40
|
+
grant_type: 'authorization_code',
|
|
41
|
+
client_id: config.clientId,
|
|
42
|
+
code: authData.code,
|
|
43
|
+
code_verifier: authData.code_verifier,
|
|
44
|
+
redirect_uri: authData.redirect_uri,
|
|
45
|
+
});
|
|
46
|
+
if (config.clientSecret) {
|
|
47
|
+
params.set('client_secret', config.clientSecret);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(tokenEndpoint, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
53
|
+
body: params,
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const errorText = await response.text();
|
|
57
|
+
return { success: false, error: `Token exchange failed: ${errorText}` };
|
|
58
|
+
}
|
|
59
|
+
const tokens = await response.json();
|
|
60
|
+
return { success: true, tokens };
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return { success: false, error: `Token exchange error: ${err.message}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Refresh an access token using a refresh token.
|
|
68
|
+
* Use this to get a new access token when the current one expires.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const result = await refreshAccessToken({
|
|
73
|
+
* authServerUrl: process.env.TIDECLOAK_URL!,
|
|
74
|
+
* realm: process.env.TIDECLOAK_REALM!,
|
|
75
|
+
* clientId: process.env.TIDECLOAK_CLIENT_ID!,
|
|
76
|
+
* }, session.tokens.refresh_token);
|
|
77
|
+
*
|
|
78
|
+
* if (result.success) {
|
|
79
|
+
* // Update stored tokens
|
|
80
|
+
* session.tokens = result.tokens;
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
async function refreshAccessToken(config, refreshToken) {
|
|
85
|
+
const tokenEndpoint = `${config.authServerUrl}/realms/${config.realm}/protocol/openid-connect/token`;
|
|
86
|
+
const params = new URLSearchParams({
|
|
87
|
+
grant_type: 'refresh_token',
|
|
88
|
+
client_id: config.clientId,
|
|
89
|
+
refresh_token: refreshToken,
|
|
90
|
+
});
|
|
91
|
+
if (config.clientSecret) {
|
|
92
|
+
params.set('client_secret', config.clientSecret);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(tokenEndpoint, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
98
|
+
body: params,
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const errorText = await response.text();
|
|
102
|
+
return { success: false, error: `Token refresh failed: ${errorText}` };
|
|
103
|
+
}
|
|
104
|
+
const tokens = await response.json();
|
|
105
|
+
return { success: true, tokens };
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return { success: false, error: `Token refresh error: ${err.message}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse the auth data from the client request body.
|
|
113
|
+
* The client sends the authorization code data as a JSON string in accessToken field.
|
|
114
|
+
*/
|
|
115
|
+
function parseAuthCodeData(requestBody) {
|
|
116
|
+
if (!requestBody.accessToken)
|
|
117
|
+
return null;
|
|
118
|
+
try {
|
|
119
|
+
const data = JSON.parse(requestBody.accessToken);
|
|
120
|
+
if (!data.code || !data.code_verifier || !data.redirect_uri) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
code: data.code,
|
|
125
|
+
code_verifier: data.code_verifier,
|
|
126
|
+
redirect_uri: data.redirect_uri,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Create a session cookie response helper.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* const response = NextResponse.json({ success: true });
|
|
139
|
+
* setSessionCookie(response, sessionId, { maxAge: 86400 });
|
|
140
|
+
* return response;
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
function setSessionCookie(response, sessionId, options = {}) {
|
|
144
|
+
const { name = 'session', maxAge = 86400, // 1 day
|
|
145
|
+
path = '/', secure = true, sameSite = 'lax', httpOnly = true, } = options;
|
|
146
|
+
response.cookies.set(name, sessionId, {
|
|
147
|
+
httpOnly,
|
|
148
|
+
secure,
|
|
149
|
+
sameSite,
|
|
150
|
+
path,
|
|
151
|
+
maxAge,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get session ID from request cookies
|
|
156
|
+
*/
|
|
157
|
+
function getSessionFromRequest(req, cookieName = 'session') {
|
|
158
|
+
var _a;
|
|
159
|
+
return ((_a = req.cookies.get(cookieName)) === null || _a === void 0 ? void 0 : _a.value) || null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Clear session cookie
|
|
163
|
+
*/
|
|
164
|
+
function clearSessionCookie(response, cookieName = 'session') {
|
|
165
|
+
response.cookies.delete(cookieName);
|
|
166
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useTideCloak, Authenticated, Unauthenticated, TideCloakContextProvider
|
|
2
|
+
import { useTideCloak, Authenticated, Unauthenticated, TideCloakContextProvider,
|
|
3
|
+
// Role-based guards
|
|
4
|
+
HasRealmRole, HasClientRole,
|
|
5
|
+
// Status components
|
|
6
|
+
Offline, Online, WasOffline, NeedsReauth, AuthLoading,
|
|
7
|
+
// Hybrid mode utilities
|
|
8
|
+
useAuthCallback, parseCallbackUrl, AuthCallback, SimpleAuthCallback,
|
|
9
|
+
// Re-export RequestEnclave for encryption
|
|
10
|
+
RequestEnclave } from '@tidecloak/react';
|
|
11
|
+
// Core exports
|
|
3
12
|
export { useTideCloak, Authenticated, Unauthenticated, TideCloakContextProvider };
|
|
13
|
+
// Role-based guard components
|
|
14
|
+
export { HasRealmRole, HasClientRole };
|
|
15
|
+
// Status components
|
|
16
|
+
export { Offline, Online, WasOffline, NeedsReauth, AuthLoading };
|
|
17
|
+
// Hybrid mode utilities
|
|
18
|
+
export { useAuthCallback, parseCallbackUrl, AuthCallback, SimpleAuthCallback };
|
|
19
|
+
// Encryption utility
|
|
20
|
+
export { RequestEnclave };
|
|
21
|
+
// Next.js specific provider
|
|
4
22
|
export { TideCloakProvider } from "./contexts/TideCloakProvider";
|
package/dist/esm/server/index.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
// Middleware (for Edge runtime - deprecated in Next.js 16+, but still supported)
|
|
1
2
|
export { createTideCloakMiddleware } from './tidecloakMiddleware';
|
|
3
|
+
// Proxy (for Next.js 16+ - Node.js runtime)
|
|
4
|
+
export { createTideCloakProxy } from './tidecloakProxy';
|
|
5
|
+
// Token verification
|
|
2
6
|
export { verifyTideCloakToken } from '@tidecloak/verify';
|
|
7
|
+
// Hybrid mode token exchange utilities
|
|
8
|
+
export { exchangeCodeForTokens, refreshAccessToken, parseAuthCodeData, setSessionCookie, getSessionFromRequest, clearSessionCookie } from './tokenExchange';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { verifyTideCloakToken } from '@tidecloak/verify';
|
|
3
|
+
import { normalizeProtectedRoutes } from './routerMatcher';
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
protectedRoutes: {},
|
|
6
|
+
onRequest: undefined,
|
|
7
|
+
onSuccess: undefined,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Returns a Next.js 16+ Proxy function enforcing TideCloak auth.
|
|
11
|
+
*
|
|
12
|
+
* This is the recommended approach for Next.js 16+. The proxy runs on Node.js runtime.
|
|
13
|
+
* For Edge runtime, use `createTideCloakMiddleware` instead (deprecated in Next.js 16).
|
|
14
|
+
*
|
|
15
|
+
* Example usage in your `proxy.ts`:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import tidecloakConfig from './tidecloakAdapter.json'
|
|
19
|
+
* import { createTideCloakProxy } from '@tidecloak/nextjs/server'
|
|
20
|
+
*
|
|
21
|
+
* export const proxy = createTideCloakProxy({
|
|
22
|
+
* config: tidecloakConfig,
|
|
23
|
+
* protectedRoutes: {
|
|
24
|
+
* '/admin/*': ['admin'],
|
|
25
|
+
* '/api/private/*': ['user']
|
|
26
|
+
* },
|
|
27
|
+
* onFailure: ({ token }, req) => NextResponse.redirect(new URL('/login', req.url)),
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* export const config = {
|
|
31
|
+
* matcher: ['/((?!_next|.*\\..*).*)'],
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function createTideCloakProxy(opts) {
|
|
36
|
+
const settings = { ...DEFAULTS, ...opts };
|
|
37
|
+
// Prepare arrays of test functions for protected routes
|
|
38
|
+
const protectedTests = normalizeProtectedRoutes(settings.protectedRoutes);
|
|
39
|
+
return async function proxy(req) {
|
|
40
|
+
var _a;
|
|
41
|
+
const path = req.nextUrl.pathname;
|
|
42
|
+
try {
|
|
43
|
+
// Extract the raw JWT from the specified cookie
|
|
44
|
+
const token = ((_a = req.cookies.get("kcToken")) === null || _a === void 0 ? void 0 : _a.value) || null;
|
|
45
|
+
// Allow custom logic before auth checks
|
|
46
|
+
if (settings.onRequest) {
|
|
47
|
+
const result = settings.onRequest({ token }, req);
|
|
48
|
+
if (result)
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
// Iterate protected routes; the first match enforces a role check
|
|
52
|
+
for (const { test, roles } of protectedTests) {
|
|
53
|
+
if (test(path, req)) {
|
|
54
|
+
// Verify signature, issuer, and presence of at least one allowed role
|
|
55
|
+
const payload = await verifyTideCloakToken(settings.config, token, roles);
|
|
56
|
+
if (!payload) {
|
|
57
|
+
// Custom onFailure hook or default JSON response
|
|
58
|
+
if (settings.onFailure) {
|
|
59
|
+
const result = settings.onFailure({ token }, req);
|
|
60
|
+
if (result)
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
return NextResponse.json({ error: '[TideCloak Proxy] Access forbidden: invalid token' }, { status: 403 });
|
|
64
|
+
}
|
|
65
|
+
// Custom onSuccess hook if provided
|
|
66
|
+
if (settings.onSuccess) {
|
|
67
|
+
const result = settings.onSuccess({ payload }, req);
|
|
68
|
+
if (result)
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
// Token valid and role check passed
|
|
72
|
+
return NextResponse.next();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// No protected route matched; continue
|
|
76
|
+
return NextResponse.next();
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
// Handle unexpected errors
|
|
80
|
+
if (settings.onError) {
|
|
81
|
+
return settings.onError(err, req);
|
|
82
|
+
}
|
|
83
|
+
console.error("[TideCloak Proxy] ", err);
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exchange an authorization code for tokens.
|
|
3
|
+
* Use this in your Next.js API route to complete the hybrid mode flow.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* // app/api/auth/callback/route.ts
|
|
8
|
+
* import { exchangeCodeForTokens } from '@tidecloak/nextjs/server';
|
|
9
|
+
*
|
|
10
|
+
* export async function POST(req: NextRequest) {
|
|
11
|
+
* const body = await req.json();
|
|
12
|
+
* const authData = JSON.parse(body.accessToken);
|
|
13
|
+
*
|
|
14
|
+
* const result = await exchangeCodeForTokens({
|
|
15
|
+
* authServerUrl: process.env.TIDECLOAK_URL!,
|
|
16
|
+
* realm: process.env.TIDECLOAK_REALM!,
|
|
17
|
+
* clientId: process.env.TIDECLOAK_CLIENT_ID!,
|
|
18
|
+
* }, authData);
|
|
19
|
+
*
|
|
20
|
+
* if (!result.success) {
|
|
21
|
+
* return NextResponse.json({ error: result.error }, { status: 401 });
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // Store tokens server-side and create session
|
|
25
|
+
* return NextResponse.json({ success: true });
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export async function exchangeCodeForTokens(config, authData) {
|
|
30
|
+
const tokenEndpoint = `${config.authServerUrl}/realms/${config.realm}/protocol/openid-connect/token`;
|
|
31
|
+
const params = new URLSearchParams({
|
|
32
|
+
grant_type: 'authorization_code',
|
|
33
|
+
client_id: config.clientId,
|
|
34
|
+
code: authData.code,
|
|
35
|
+
code_verifier: authData.code_verifier,
|
|
36
|
+
redirect_uri: authData.redirect_uri,
|
|
37
|
+
});
|
|
38
|
+
if (config.clientSecret) {
|
|
39
|
+
params.set('client_secret', config.clientSecret);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(tokenEndpoint, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
45
|
+
body: params,
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errorText = await response.text();
|
|
49
|
+
return { success: false, error: `Token exchange failed: ${errorText}` };
|
|
50
|
+
}
|
|
51
|
+
const tokens = await response.json();
|
|
52
|
+
return { success: true, tokens };
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return { success: false, error: `Token exchange error: ${err.message}` };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Refresh an access token using a refresh token.
|
|
60
|
+
* Use this to get a new access token when the current one expires.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const result = await refreshAccessToken({
|
|
65
|
+
* authServerUrl: process.env.TIDECLOAK_URL!,
|
|
66
|
+
* realm: process.env.TIDECLOAK_REALM!,
|
|
67
|
+
* clientId: process.env.TIDECLOAK_CLIENT_ID!,
|
|
68
|
+
* }, session.tokens.refresh_token);
|
|
69
|
+
*
|
|
70
|
+
* if (result.success) {
|
|
71
|
+
* // Update stored tokens
|
|
72
|
+
* session.tokens = result.tokens;
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export async function refreshAccessToken(config, refreshToken) {
|
|
77
|
+
const tokenEndpoint = `${config.authServerUrl}/realms/${config.realm}/protocol/openid-connect/token`;
|
|
78
|
+
const params = new URLSearchParams({
|
|
79
|
+
grant_type: 'refresh_token',
|
|
80
|
+
client_id: config.clientId,
|
|
81
|
+
refresh_token: refreshToken,
|
|
82
|
+
});
|
|
83
|
+
if (config.clientSecret) {
|
|
84
|
+
params.set('client_secret', config.clientSecret);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(tokenEndpoint, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
90
|
+
body: params,
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const errorText = await response.text();
|
|
94
|
+
return { success: false, error: `Token refresh failed: ${errorText}` };
|
|
95
|
+
}
|
|
96
|
+
const tokens = await response.json();
|
|
97
|
+
return { success: true, tokens };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return { success: false, error: `Token refresh error: ${err.message}` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse the auth data from the client request body.
|
|
105
|
+
* The client sends the authorization code data as a JSON string in accessToken field.
|
|
106
|
+
*/
|
|
107
|
+
export function parseAuthCodeData(requestBody) {
|
|
108
|
+
if (!requestBody.accessToken)
|
|
109
|
+
return null;
|
|
110
|
+
try {
|
|
111
|
+
const data = JSON.parse(requestBody.accessToken);
|
|
112
|
+
if (!data.code || !data.code_verifier || !data.redirect_uri) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
code: data.code,
|
|
117
|
+
code_verifier: data.code_verifier,
|
|
118
|
+
redirect_uri: data.redirect_uri,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Create a session cookie response helper.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const response = NextResponse.json({ success: true });
|
|
131
|
+
* setSessionCookie(response, sessionId, { maxAge: 86400 });
|
|
132
|
+
* return response;
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function setSessionCookie(response, sessionId, options = {}) {
|
|
136
|
+
const { name = 'session', maxAge = 86400, // 1 day
|
|
137
|
+
path = '/', secure = true, sameSite = 'lax', httpOnly = true, } = options;
|
|
138
|
+
response.cookies.set(name, sessionId, {
|
|
139
|
+
httpOnly,
|
|
140
|
+
secure,
|
|
141
|
+
sameSite,
|
|
142
|
+
path,
|
|
143
|
+
maxAge,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get session ID from request cookies
|
|
148
|
+
*/
|
|
149
|
+
export function getSessionFromRequest(req, cookieName = 'session') {
|
|
150
|
+
var _a;
|
|
151
|
+
return ((_a = req.cookies.get(cookieName)) === null || _a === void 0 ? void 0 : _a.value) || null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Clear session cookie
|
|
155
|
+
*/
|
|
156
|
+
export function clearSessionCookie(response, cookieName = 'session') {
|
|
157
|
+
response.cookies.delete(cookieName);
|
|
158
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { useTideCloak, Authenticated, Unauthenticated, TideCloakContextProvider } from '@tidecloak/react';
|
|
1
|
+
import { useTideCloak, Authenticated, Unauthenticated, TideCloakContextProvider, HasRealmRole, HasClientRole, Offline, Online, WasOffline, NeedsReauth, AuthLoading, useAuthCallback, parseCallbackUrl, AuthCallback, SimpleAuthCallback, RequestEnclave } from '@tidecloak/react';
|
|
2
2
|
export { useTideCloak, Authenticated, Unauthenticated, TideCloakContextProvider };
|
|
3
|
+
export { HasRealmRole, HasClientRole };
|
|
4
|
+
export { Offline, Online, WasOffline, NeedsReauth, AuthLoading };
|
|
5
|
+
export { useAuthCallback, parseCallbackUrl, AuthCallback, SimpleAuthCallback };
|
|
6
|
+
export { RequestEnclave };
|
|
7
|
+
export type { TideCloakContextValue, TideCloakContextProviderProps } from '@tidecloak/react';
|
|
8
|
+
export type { AuthCallbackState, UseAuthCallbackOptions, AuthCallbackProps } from '@tidecloak/react';
|
|
3
9
|
export { TideCloakProvider } from "./contexts/TideCloakProvider";
|
|
4
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,eAAe,EACf,wBAAwB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,eAAe,EACf,wBAAwB,EAExB,YAAY,EACZ,aAAa,EAEb,OAAO,EACP,MAAM,EACN,UAAU,EACV,WAAW,EACX,WAAW,EAEX,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAElB,cAAc,EACf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,YAAY,EACZ,aAAa,EACb,eAAe,EACf,wBAAwB,EACzB,CAAC;AAGF,OAAO,EACL,YAAY,EACZ,aAAa,EACd,CAAC;AAGF,OAAO,EACL,OAAO,EACP,MAAM,EACN,UAAU,EACV,WAAW,EACX,WAAW,EACZ,CAAC;AAGF,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EACnB,CAAC;AAGF,OAAO,EAAE,cAAc,EAAE,CAAC;AAG1B,YAAY,EAAE,qBAAqB,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAC7F,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrG,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA"}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { createTideCloakMiddleware } from './tidecloakMiddleware';
|
|
2
|
+
export type { TideMiddlewareOptions, TidecloakConfig } from './tidecloakMiddleware';
|
|
3
|
+
export { createTideCloakProxy } from './tidecloakProxy';
|
|
4
|
+
export type { TideProxyOptions } from './tidecloakProxy';
|
|
2
5
|
export { verifyTideCloakToken } from '@tidecloak/verify';
|
|
6
|
+
export { exchangeCodeForTokens, refreshAccessToken, parseAuthCodeData, setSessionCookie, getSessionFromRequest, clearSessionCookie } from './tokenExchange';
|
|
7
|
+
export type { TokenExchangeConfig, TokenResponse, AuthCodeData } from './tokenExchange';
|
|
3
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AACjE,YAAY,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAGnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvD,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAGxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAGxD,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,iBAAiB,CAAA;AAExB,YAAY,EACV,mBAAmB,EACnB,aAAa,EACb,YAAY,EACb,MAAM,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import type { TidecloakConfig, TideMiddlewareOptions } from './tidecloakMiddleware';
|
|
3
|
+
export type { TidecloakConfig, TideMiddlewareOptions };
|
|
4
|
+
/**
|
|
5
|
+
* Alias for TideMiddlewareOptions for use with the proxy convention
|
|
6
|
+
*/
|
|
7
|
+
export type TideProxyOptions = TideMiddlewareOptions;
|
|
8
|
+
/**
|
|
9
|
+
* Returns a Next.js 16+ Proxy function enforcing TideCloak auth.
|
|
10
|
+
*
|
|
11
|
+
* This is the recommended approach for Next.js 16+. The proxy runs on Node.js runtime.
|
|
12
|
+
* For Edge runtime, use `createTideCloakMiddleware` instead (deprecated in Next.js 16).
|
|
13
|
+
*
|
|
14
|
+
* Example usage in your `proxy.ts`:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import tidecloakConfig from './tidecloakAdapter.json'
|
|
18
|
+
* import { createTideCloakProxy } from '@tidecloak/nextjs/server'
|
|
19
|
+
*
|
|
20
|
+
* export const proxy = createTideCloakProxy({
|
|
21
|
+
* config: tidecloakConfig,
|
|
22
|
+
* protectedRoutes: {
|
|
23
|
+
* '/admin/*': ['admin'],
|
|
24
|
+
* '/api/private/*': ['user']
|
|
25
|
+
* },
|
|
26
|
+
* onFailure: ({ token }, req) => NextResponse.redirect(new URL('/login', req.url)),
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* export const config = {
|
|
30
|
+
* matcher: ['/((?!_next|.*\\..*).*)'],
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function createTideCloakProxy(opts: TideProxyOptions): (req: NextRequest) => Promise<NextResponse<unknown>>;
|
|
35
|
+
//# sourceMappingURL=tidecloakProxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tidecloakProxy.d.ts","sourceRoot":"","sources":["../../../src/server/tidecloakProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAMvD,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAGnF,YAAY,EAAE,eAAe,EAAE,qBAAqB,EAAE,CAAA;AAEtD;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,CAAA;AAQpD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,gBAAgB,IAM7B,KAAK,WAAW,oCAqD7C"}
|