@tidecloak/create-nextjs 0.13.30 → 0.13.31
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 +9 -9
- package/init/realm.json +6 -6
- package/package.json +1 -1
- package/template-js-app/app/home/page.jsx +75 -1
- package/template-js-app/init/realm.json +6 -6
- package/template-js-app/init/tcinit.sh +1 -1
- package/template-js-app/middleware.js +7 -3
- package/template-js-app/package.json +1 -1
- package/template-ts-app/app/api/protected/route.ts +1 -1
- package/template-ts-app/app/home/page.tsx +76 -2
- package/template-ts-app/app/page.tsx +1 -1
- package/template-ts-app/init/realm.json +6 -6
- package/template-ts-app/init/tcinit.sh +1 -1
- package/template-ts-app/middleware.ts +40 -44
- package/template-ts-app/next.config.js +5 -0
- package/template-ts-app/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,23 +38,22 @@ npm init @tidecloak/nextjs@latest my-app
|
|
|
38
38
|
|
|
39
39
|
```
|
|
40
40
|
my-app/
|
|
41
|
-
├── app
|
|
42
|
-
|
|
|
41
|
+
├── app/
|
|
42
|
+
| ├── api/
|
|
43
43
|
| │ └── protected/
|
|
44
44
|
| │ └── route.js <- A protected API on your NextJS server that verifies the user's access token
|
|
45
45
|
| ├── auth/
|
|
46
46
|
| │ └── redirect/
|
|
47
47
|
| │ └── page.jsx <- A dedicated page to redirect the user back to once authentication is complete
|
|
48
48
|
| ├── home/
|
|
49
|
-
| | └── page.jsx <- Your home page the user goes to once
|
|
50
|
-
| ├── public/
|
|
51
|
-
│ | └── silent-check-sso.html
|
|
49
|
+
| | └── page.jsx <- Your home page the user goes to once authenticated
|
|
52
50
|
| ├── layout.jsx <- Entry point of your app before the user sees any actual pages
|
|
53
51
|
| └── page.jsx <- Your login page the user is brought to when they need to authenticate
|
|
54
|
-
|
|
52
|
+
├── public/
|
|
53
|
+
│ └── silent-check-sso.html <- Silent SSO check page served at the site root
|
|
55
54
|
├── tidecloak.json <- Where your Tidecloak configuration sits
|
|
56
|
-
├── next.config.
|
|
57
|
-
├── middleware.js <- Run on each page navigation - this is where the
|
|
55
|
+
├── next.config.js
|
|
56
|
+
├── middleware.js <- Run on each page navigation - this is where the Tidecloak token is verified
|
|
58
57
|
└── package.json
|
|
59
58
|
```
|
|
60
59
|
|
|
@@ -204,7 +203,8 @@ TideCloak provides server-side route protection for both the **Pages Router** an
|
|
|
204
203
|
#### Options
|
|
205
204
|
|
|
206
205
|
* **`config`** (`TidecloakConfig`): Your Tidecloak adapter JSON (downloaded from your TideCloak client settings).
|
|
207
|
-
* **`protectedRoutes`** (`ProtectedRoutesMap`): Map of path patterns to arrays of required roles.
|
|
206
|
+
* **`protectedRoutes`** (`ProtectedRoutesMap`): Map of path patterns to arrays of required roles. A trailing `/*` glob (e.g. `"/admin/*"`) also matches the bare base path (`/admin`).
|
|
207
|
+
* **`cookieName`** (`string`, default `"kcToken"`): Name of the cookie that holds the access token.
|
|
208
208
|
* **`onRequest`**<br>`(ctx: { token: string | null }, req: NextRequest) => NextResponse | void`<br>Hook before auth logic; can short-circuit by returning a `NextResponse`.
|
|
209
209
|
* **`onSuccess`**<br>`(ctx: { payload: Record<string, any> }, req: NextRequest) => NextResponse | void`<br>Hook after successful auth & role checks; override the response by returning one.
|
|
210
210
|
* **`onFailure`**<br>`(ctx: { token: string | null }, req: NextRequest) => NextResponse | void`<br>Hook when auth or role check fails; return a `NextResponse` to override.
|
package/init/realm.json
CHANGED
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"description": "Standard application user"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"name": "
|
|
16
|
-
"description": "Tide E2EE self-encrypt
|
|
15
|
+
"name": "_tide_message.selfencrypt",
|
|
16
|
+
"description": "Tide E2EE self-encrypt message data"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
"name": "
|
|
20
|
-
"description": "Tide E2EE self-decrypt
|
|
19
|
+
"name": "_tide_message.selfdecrypt",
|
|
20
|
+
"description": "Tide E2EE self-decrypt message data"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "default-roles-nextjs-test",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"composite": true,
|
|
26
26
|
"composites": {
|
|
27
27
|
"realm": [
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"_tide_message.selfencrypt",
|
|
29
|
+
"_tide_message.selfdecrypt",
|
|
30
30
|
"appUser"
|
|
31
31
|
]
|
|
32
32
|
}
|
package/package.json
CHANGED
|
@@ -5,19 +5,40 @@ import { useTideCloak } from '@tidecloak/nextjs'
|
|
|
5
5
|
import tcConfig from "../../tidecloak.json"
|
|
6
6
|
|
|
7
7
|
export default function HomePage() {
|
|
8
|
-
const { logout, getValueFromIdToken, hasRealmRole, token } = useTideCloak()
|
|
8
|
+
const { logout, getValueFromIdToken, hasRealmRole, token, doEncrypt, doDecrypt } = useTideCloak()
|
|
9
9
|
|
|
10
10
|
const [username, setUsername] = useState("")
|
|
11
11
|
const [hasDefaultRole, setHasDefaultRole] = useState(false)
|
|
12
12
|
const [verifyResult, setVerifyResult] = useState(null)
|
|
13
13
|
const [verifying, setVerifying] = useState(false)
|
|
14
14
|
|
|
15
|
+
// Self encrypt/decrypt: data is bound to THIS user's identity — only they can
|
|
16
|
+
// decrypt it. The "message" tag matches the _tide_message.selfencrypt/.selfdecrypt
|
|
17
|
+
// roles granted to every user in init/realm.json.
|
|
18
|
+
const TAG = "message"
|
|
19
|
+
const [text, setText] = useState("") // always the decrypted value, editable
|
|
20
|
+
const [busy, setBusy] = useState(false)
|
|
21
|
+
const [status, setStatus] = useState("")
|
|
22
|
+
const [cryptoErr, setCryptoErr] = useState("")
|
|
23
|
+
|
|
24
|
+
// localStorage key for the saved note, namespaced per user (vuid).
|
|
25
|
+
const storageKey = () => `tide-note:${getValueFromIdToken("vuid")}`
|
|
26
|
+
|
|
15
27
|
useEffect(() => {
|
|
16
28
|
if (token) {
|
|
17
29
|
const name = getValueFromIdToken("preferred_username")
|
|
18
30
|
const defaultRole = hasRealmRole(`default-roles-${tcConfig["realm"]}`)
|
|
19
31
|
setUsername(name);
|
|
20
32
|
setHasDefaultRole(defaultRole)
|
|
33
|
+
|
|
34
|
+
// Restore the saved note. Only the CIPHERTEXT is persisted; we decrypt it
|
|
35
|
+
// client-side here so the field shows plaintext when you log back in.
|
|
36
|
+
const stored = typeof window !== "undefined" ? localStorage.getItem(storageKey()) : null
|
|
37
|
+
if (stored) {
|
|
38
|
+
doDecrypt([{ encrypted: stored, tags: [TAG] }])
|
|
39
|
+
.then((res) => setText(String(res[0])))
|
|
40
|
+
.catch(() => {})
|
|
41
|
+
}
|
|
21
42
|
}
|
|
22
43
|
|
|
23
44
|
}, [token])
|
|
@@ -48,6 +69,25 @@ export default function HomePage() {
|
|
|
48
69
|
}
|
|
49
70
|
}, [token])
|
|
50
71
|
|
|
72
|
+
// Submit = encrypt the current value, persist the ciphertext, then decrypt it
|
|
73
|
+
// straight back so the field keeps showing plaintext. We store only the
|
|
74
|
+
// ciphertext (here in localStorage; in a real app, on your server) — it's
|
|
75
|
+
// decrypted again when you log back in.
|
|
76
|
+
const onSubmit = useCallback(async () => {
|
|
77
|
+
setBusy(true); setCryptoErr(""); setStatus("")
|
|
78
|
+
try {
|
|
79
|
+
const [ct] = await doEncrypt([{ data: text, tags: [TAG] }])
|
|
80
|
+
if (typeof window !== "undefined") localStorage.setItem(storageKey(), ct)
|
|
81
|
+
const [pt] = await doDecrypt([{ encrypted: ct, tags: [TAG] }])
|
|
82
|
+
setText(String(pt))
|
|
83
|
+
setStatus("Message successfully stored")
|
|
84
|
+
} catch (err) {
|
|
85
|
+
setCryptoErr(err.message || "Failed")
|
|
86
|
+
} finally {
|
|
87
|
+
setBusy(false)
|
|
88
|
+
}
|
|
89
|
+
}, [text, doEncrypt, doDecrypt])
|
|
90
|
+
|
|
51
91
|
return (
|
|
52
92
|
<div style={containerStyle}>
|
|
53
93
|
<div style={cardStyle}>
|
|
@@ -75,11 +115,45 @@ export default function HomePage() {
|
|
|
75
115
|
{verifyResult}
|
|
76
116
|
</p>
|
|
77
117
|
)}
|
|
118
|
+
|
|
119
|
+
{/* ── Encrypted note: always shown decrypted; Submit re-encrypts then decrypts ── */}
|
|
120
|
+
<div style={{ marginTop: '1.5rem', borderTop: '1px solid #eee', paddingTop: '1rem', textAlign: 'left' }}>
|
|
121
|
+
<h2 style={{ fontSize: '1.1rem', margin: '0 0 0.25rem' }}>Your encrypted note</h2>
|
|
122
|
+
<p style={{ margin: '0 0 0.5rem', color: '#777', fontSize: '0.85rem' }}>
|
|
123
|
+
This is an encrypted textbox under your own identity — only you can decrypt it.
|
|
124
|
+
</p>
|
|
125
|
+
|
|
126
|
+
<textarea
|
|
127
|
+
value={text}
|
|
128
|
+
onChange={(e) => setText(e.target.value)}
|
|
129
|
+
placeholder="Type your note…"
|
|
130
|
+
style={textareaStyle}
|
|
131
|
+
/>
|
|
132
|
+
<button onClick={onSubmit} style={buttonStyle} disabled={busy}>
|
|
133
|
+
{busy ? 'Submitting…' : 'Submit'}
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
{status && <p style={{ color: 'green', marginTop: '0.5rem', fontSize: '0.85rem' }}>{status}</p>}
|
|
137
|
+
|
|
138
|
+
{cryptoErr && <p style={{ color: 'red', marginTop: '0.5rem' }}>{cryptoErr}</p>}
|
|
139
|
+
</div>
|
|
78
140
|
</div>
|
|
79
141
|
</div>
|
|
80
142
|
)
|
|
81
143
|
}
|
|
82
144
|
|
|
145
|
+
const textareaStyle = {
|
|
146
|
+
width: '100%',
|
|
147
|
+
minHeight: '64px',
|
|
148
|
+
padding: '0.5rem',
|
|
149
|
+
borderRadius: '4px',
|
|
150
|
+
border: '1px solid #ccc',
|
|
151
|
+
boxSizing: 'border-box',
|
|
152
|
+
fontFamily: 'inherit',
|
|
153
|
+
fontSize: '0.9rem',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
83
157
|
const containerStyle = {
|
|
84
158
|
minHeight: '100vh',
|
|
85
159
|
display: 'flex',
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"description": "Standard application user"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"name": "
|
|
16
|
-
"description": "Tide E2EE self-encrypt
|
|
15
|
+
"name": "_tide_message.selfencrypt",
|
|
16
|
+
"description": "Tide E2EE self-encrypt message data"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
"name": "
|
|
20
|
-
"description": "Tide E2EE self-decrypt
|
|
19
|
+
"name": "_tide_message.selfdecrypt",
|
|
20
|
+
"description": "Tide E2EE self-decrypt message data"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "default-roles-nextjs-test",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"composite": true,
|
|
26
26
|
"composites": {
|
|
27
27
|
"realm": [
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"_tide_message.selfencrypt",
|
|
29
|
+
"_tide_message.selfdecrypt",
|
|
30
30
|
"appUser"
|
|
31
31
|
]
|
|
32
32
|
}
|
|
@@ -135,7 +135,7 @@ echo "🔐 Initializing Tide realm + IGA..."
|
|
|
135
135
|
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/vendorResources/setUpTideRealm" \
|
|
136
136
|
-H "Authorization: Bearer ${TOKEN}" \
|
|
137
137
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
138
|
-
--data-urlencode "email
|
|
138
|
+
--data-urlencode "email=${SUBSCRIPTION_EMAIL:-test@demo.org}" >/dev/null
|
|
139
139
|
|
|
140
140
|
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/tide-admin/toggle-iga" \
|
|
141
141
|
-H "Authorization: Bearer ${TOKEN}" \
|
|
@@ -7,6 +7,9 @@ import tcConfig from "./tidecloak.json";
|
|
|
7
7
|
export default createTideCloakMiddleware({
|
|
8
8
|
config: tcConfig,
|
|
9
9
|
protectedRoutes:{
|
|
10
|
+
// "offline_access" is granted to every authenticated user, so this protects
|
|
11
|
+
// the route for "any logged-in user". Swap it for a real realm/client role
|
|
12
|
+
// (e.g. "appUser") to demonstrate role-based access control.
|
|
10
13
|
"/protected": ["offline_access"]
|
|
11
14
|
},
|
|
12
15
|
onFailure: (ctx, req) => {
|
|
@@ -19,13 +22,14 @@ export default createTideCloakMiddleware({
|
|
|
19
22
|
onSuccess: (ctx, req) => {
|
|
20
23
|
return NextResponse.next();
|
|
21
24
|
},
|
|
22
|
-
|
|
25
|
+
// Note: onError receives (err, req) - the error is the first argument.
|
|
26
|
+
onError: (err, req) => {
|
|
23
27
|
console.error("[Middleware] ", err);
|
|
24
28
|
return NextResponse.redirect(new URL("/auth/redirect", req.url));
|
|
25
29
|
}
|
|
26
30
|
})
|
|
27
31
|
|
|
28
|
-
//Which routes the middleware should run on:
|
|
32
|
+
//Which routes the middleware should run on (include the bare path and subpaths):
|
|
29
33
|
export const config = {
|
|
30
|
-
matcher: ["/protected/:path*"],
|
|
34
|
+
matcher: ["/protected", "/protected/:path*"],
|
|
31
35
|
};
|
|
@@ -16,7 +16,7 @@ export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
|
16
16
|
const token = authHeader.split(' ')[1]
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
|
-
const user = await verifyTideCloakToken(tcConfig, token, [ALLOWED_ROLE])
|
|
19
|
+
const user = await verifyTideCloakToken(tcConfig, token, [ALLOWED_ROLE]) as Record<string, any>
|
|
20
20
|
|
|
21
21
|
if (!user) {
|
|
22
22
|
return NextResponse.json(
|
|
@@ -6,19 +6,40 @@ import tcConfig from "../../tidecloak.json"
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
export default function HomePage() {
|
|
9
|
-
const { logout, getValueFromIdToken, hasRealmRole, token } = useTideCloak()
|
|
9
|
+
const { logout, getValueFromIdToken, hasRealmRole, token, doEncrypt, doDecrypt } = useTideCloak()
|
|
10
10
|
|
|
11
11
|
const [username, setUsername] = useState("")
|
|
12
12
|
const [hasDefaultRole, setHasDefaultRole] = useState(false)
|
|
13
13
|
const [verifyResult, setVerifyResult] = useState<string | null>(null)
|
|
14
14
|
const [verifying, setVerifying] = useState(false)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Self encrypt/decrypt: data is bound to THIS user's identity — only they can
|
|
17
|
+
// decrypt it. The "message" tag matches the _tide_message.selfencrypt/.selfdecrypt
|
|
18
|
+
// roles granted to every user in init/realm.json.
|
|
19
|
+
const TAG = "message"
|
|
20
|
+
const [text, setText] = useState("") // always the decrypted value, editable
|
|
21
|
+
const [busy, setBusy] = useState(false)
|
|
22
|
+
const [status, setStatus] = useState("")
|
|
23
|
+
const [cryptoErr, setCryptoErr] = useState("")
|
|
24
|
+
|
|
25
|
+
// localStorage key for the saved note, namespaced per user (vuid).
|
|
26
|
+
const storageKey = () => `tide-note:${getValueFromIdToken("vuid")}`
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
17
29
|
if (token) {
|
|
18
30
|
const name = getValueFromIdToken("preferred_username")
|
|
19
31
|
const defaultRole = hasRealmRole(`default-roles-${tcConfig["realm"]}`)
|
|
20
32
|
setUsername(name);
|
|
21
33
|
setHasDefaultRole(defaultRole)
|
|
34
|
+
|
|
35
|
+
// Restore the saved note. Only the CIPHERTEXT is persisted; we decrypt it
|
|
36
|
+
// client-side here so the field shows plaintext when you log back in.
|
|
37
|
+
const stored = typeof window !== "undefined" ? localStorage.getItem(storageKey()) : null
|
|
38
|
+
if (stored) {
|
|
39
|
+
doDecrypt([{ encrypted: stored, tags: [TAG] }])
|
|
40
|
+
.then((res) => setText(String(res[0])))
|
|
41
|
+
.catch(() => {})
|
|
42
|
+
}
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
}, [token])
|
|
@@ -50,6 +71,25 @@ export default function HomePage() {
|
|
|
50
71
|
}
|
|
51
72
|
}, [token])
|
|
52
73
|
|
|
74
|
+
// Submit = encrypt the current value, persist the ciphertext, then decrypt it
|
|
75
|
+
// straight back so the field keeps showing plaintext. We store only the
|
|
76
|
+
// ciphertext (here in localStorage; in a real app, on your server) — it's
|
|
77
|
+
// decrypted again when you log back in.
|
|
78
|
+
const onSubmit = useCallback(async () => {
|
|
79
|
+
setBusy(true); setCryptoErr(""); setStatus("")
|
|
80
|
+
try {
|
|
81
|
+
const [ct] = await doEncrypt([{ data: text, tags: [TAG] }])
|
|
82
|
+
if (typeof window !== "undefined") localStorage.setItem(storageKey(), ct)
|
|
83
|
+
const [pt] = await doDecrypt([{ encrypted: ct, tags: [TAG] }])
|
|
84
|
+
setText(String(pt))
|
|
85
|
+
setStatus("Message successfully stored")
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
setCryptoErr(err.message || "Failed")
|
|
88
|
+
} finally {
|
|
89
|
+
setBusy(false)
|
|
90
|
+
}
|
|
91
|
+
}, [text, doEncrypt, doDecrypt])
|
|
92
|
+
|
|
53
93
|
return (
|
|
54
94
|
<div style={containerStyle}>
|
|
55
95
|
<div style={cardStyle}>
|
|
@@ -75,11 +115,45 @@ export default function HomePage() {
|
|
|
75
115
|
{verifyResult}
|
|
76
116
|
</p>
|
|
77
117
|
)}
|
|
118
|
+
|
|
119
|
+
{/* ── Encrypted note: always shown decrypted; Submit re-encrypts then decrypts ── */}
|
|
120
|
+
<div style={{ marginTop: '1.5rem', borderTop: '1px solid #eee', paddingTop: '1rem', textAlign: 'left' }}>
|
|
121
|
+
<h2 style={{ fontSize: '1.1rem', margin: '0 0 0.25rem' }}>Your encrypted note</h2>
|
|
122
|
+
<p style={{ margin: '0 0 0.5rem', color: '#777', fontSize: '0.85rem' }}>
|
|
123
|
+
This is an encrypted textbox under your own identity — only you can decrypt it.
|
|
124
|
+
</p>
|
|
125
|
+
|
|
126
|
+
<textarea
|
|
127
|
+
value={text}
|
|
128
|
+
onChange={(e) => setText(e.target.value)}
|
|
129
|
+
placeholder="Type your note…"
|
|
130
|
+
style={textareaStyle}
|
|
131
|
+
/>
|
|
132
|
+
<button onClick={onSubmit} style={buttonStyle} disabled={busy}>
|
|
133
|
+
{busy ? 'Submitting…' : 'Submit'}
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
{status && <p style={{ color: 'green', marginTop: '0.5rem', fontSize: '0.85rem' }}>{status}</p>}
|
|
137
|
+
|
|
138
|
+
{cryptoErr && <p style={{ color: 'red', marginTop: '0.5rem' }}>{cryptoErr}</p>}
|
|
139
|
+
</div>
|
|
78
140
|
</div>
|
|
79
141
|
</div>
|
|
80
142
|
)
|
|
81
143
|
}
|
|
82
144
|
|
|
145
|
+
const textareaStyle: React.CSSProperties = {
|
|
146
|
+
width: '100%',
|
|
147
|
+
minHeight: '64px',
|
|
148
|
+
padding: '0.5rem',
|
|
149
|
+
borderRadius: '4px',
|
|
150
|
+
border: '1px solid #ccc',
|
|
151
|
+
boxSizing: 'border-box',
|
|
152
|
+
fontFamily: 'inherit',
|
|
153
|
+
fontSize: '0.9rem',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
83
157
|
const containerStyle: React.CSSProperties = {
|
|
84
158
|
minHeight: '100vh',
|
|
85
159
|
display: 'flex',
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"description": "Standard application user"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"name": "
|
|
16
|
-
"description": "Tide E2EE self-encrypt
|
|
15
|
+
"name": "_tide_message.selfencrypt",
|
|
16
|
+
"description": "Tide E2EE self-encrypt message data"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
"name": "
|
|
20
|
-
"description": "Tide E2EE self-decrypt
|
|
19
|
+
"name": "_tide_message.selfdecrypt",
|
|
20
|
+
"description": "Tide E2EE self-decrypt message data"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "default-roles-nextjs-test",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"composite": true,
|
|
26
26
|
"composites": {
|
|
27
27
|
"realm": [
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"_tide_message.selfencrypt",
|
|
29
|
+
"_tide_message.selfdecrypt",
|
|
30
30
|
"appUser"
|
|
31
31
|
]
|
|
32
32
|
}
|
|
@@ -135,7 +135,7 @@ echo "🔐 Initializing Tide realm + IGA..."
|
|
|
135
135
|
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/vendorResources/setUpTideRealm" \
|
|
136
136
|
-H "Authorization: Bearer ${TOKEN}" \
|
|
137
137
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
138
|
-
--data-urlencode "email
|
|
138
|
+
--data-urlencode "email=${SUBSCRIPTION_EMAIL:-test@demo.org}" >/dev/null
|
|
139
139
|
|
|
140
140
|
curl -s -X POST "${TIDECLOAK_LOCAL_URL}/admin/realms/${REALM_NAME}/tide-admin/toggle-iga" \
|
|
141
141
|
-H "Authorization: Bearer ${TOKEN}" \
|
|
@@ -1,44 +1,40 @@
|
|
|
1
|
-
// an example nextJS middleware router that does server-side validation on all traffic to secure pages
|
|
2
|
-
import type { NextRequest } from "next/server";
|
|
3
|
-
import { NextResponse } from "next/server";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
onError: (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Tell Next.js which paths to apply this middleware to
|
|
42
|
-
export const config = {
|
|
43
|
-
matcher: ["/protected/:path*"],
|
|
44
|
-
};
|
|
1
|
+
// an example nextJS middleware router that does server-side validation on all traffic to secure pages
|
|
2
|
+
import type { NextRequest } from "next/server";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
import { createTideCloakMiddleware } from "@tidecloak/nextjs/server";
|
|
5
|
+
import tcConfig from "./tidecloak.json";
|
|
6
|
+
|
|
7
|
+
export default createTideCloakMiddleware({
|
|
8
|
+
config: tcConfig,
|
|
9
|
+
protectedRoutes: {
|
|
10
|
+
// "offline_access" is granted to every authenticated user, so this protects
|
|
11
|
+
// the route for "any logged-in user". Swap it for a real realm/client role
|
|
12
|
+
// (e.g. "appUser") to demonstrate role-based access control.
|
|
13
|
+
"/protected": ["offline_access"],
|
|
14
|
+
},
|
|
15
|
+
onFailure: (ctx: { token: string | null }, req: NextRequest) => {
|
|
16
|
+
console.debug("Token verification failed", {
|
|
17
|
+
path: req.nextUrl.pathname,
|
|
18
|
+
ctx,
|
|
19
|
+
});
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: "Access forbidden: invalid token" },
|
|
22
|
+
{ status: 403 }
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
onSuccess: (ctx: { payload: Record<string, any> }, req: NextRequest) => {
|
|
26
|
+
return NextResponse.next();
|
|
27
|
+
},
|
|
28
|
+
// Note: onError receives (err, req) - the error is the first argument.
|
|
29
|
+
onError: (err: unknown, req: NextRequest) => {
|
|
30
|
+
console.error("[Middleware] error verifying token for", req.nextUrl.pathname, err);
|
|
31
|
+
// if something unexpected happens, redirect to your auth flow
|
|
32
|
+
const redirectUrl = new URL("/auth/redirect", req.url);
|
|
33
|
+
return NextResponse.redirect(redirectUrl);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Tell Next.js which paths to apply this middleware to (bare path and subpaths)
|
|
38
|
+
export const config = {
|
|
39
|
+
matcher: ["/protected", "/protected/:path*"],
|
|
40
|
+
};
|