@tidecloak/react 0.13.11 → 0.13.14
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 +18 -298
- package/dist/cjs/components/AuthCallback.js +90 -0
- package/dist/cjs/contexts/TideCloakContextProvider.js +534 -73
- package/dist/cjs/hooks/useAuthCallback.js +161 -0
- package/dist/cjs/index.js +81 -2
- package/dist/esm/components/AuthCallback.js +90 -0
- package/dist/esm/contexts/TideCloakContextProvider.js +534 -73
- package/dist/esm/hooks/useAuthCallback.js +161 -0
- package/dist/esm/index.js +81 -2
- package/dist/types/components/AuthCallback.d.ts +90 -0
- package/dist/types/components/AuthCallback.d.ts.map +1 -0
- package/dist/types/contexts/TideCloakContextProvider.d.ts +92 -6
- package/dist/types/contexts/TideCloakContextProvider.d.ts.map +1 -1
- package/dist/types/hooks/useAuthCallback.d.ts +80 -0
- package/dist/types/hooks/useAuthCallback.d.ts.map +1 -0
- package/dist/types/index.d.ts +63 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,316 +1,36 @@
|
|
|
1
1
|
# TideCloak React SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
>
|
|
5
|
-
> If you're new to TideCloak, the fastest way to get started with React + Vite is to follow our official Getting Started guide:
|
|
6
|
-
> [https://github.com/tide-foundation/tidecloak-gettingstarted](https://github.com/tide-foundation/tidecloak-gettingstarted)
|
|
7
|
-
>
|
|
8
|
-
> ---
|
|
9
|
-
|
|
10
|
-
Secure your React app with TideCloak: authentication, session management, data encryption, and role-based access.
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## 1. Prerequisites
|
|
15
|
-
|
|
16
|
-
Before you begin, ensure you have the following:
|
|
17
|
-
|
|
18
|
-
* **React 18** or later
|
|
19
|
-
* **Node.js ≥18.17.0**
|
|
20
|
-
* A [running](https://github.com/tide-foundation/tidecloak-gettingstarted) TideCloak server you have admin control over
|
|
21
|
-
* IGA enabled realm
|
|
22
|
-
* A registered client in your realm with default user contexts approved and committed
|
|
23
|
-
* A valid Tidecloak adapter JSON file (e.g., `tidecloakAdapter.json`)
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## 2. Install `@tidecloak/react`
|
|
28
|
-
|
|
29
|
-
Add the TideCloak React SDK to your project:
|
|
3
|
+
Add TideCloak authentication to your React app.
|
|
30
4
|
|
|
31
5
|
```bash
|
|
32
6
|
npm install @tidecloak/react
|
|
33
|
-
# or
|
|
34
|
-
yarn add @tidecloak/react
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
This bundle provides:
|
|
38
|
-
|
|
39
|
-
* `<TideCloakContextProvider>` - application-level context
|
|
40
|
-
* `useTideCloak()` hook - access tokens and auth actions
|
|
41
|
-
* `<Authenticated>` / `<Unauthenticated>` - UI guards
|
|
42
|
-
* `doEncrypt()` / `doDecrypt()` - tag-based encryption/decryption
|
|
43
|
-
|
|
44
|
-
> **Note:** Installing this package automatically adds a `silent-check-sso.html` file to your `public` directory. This file is required for silent SSO checks; if it doesn't exist, create it manually at `public/silent-check-sso.html` with the following content, otherwise the app will break:
|
|
45
|
-
>
|
|
46
|
-
> ```html
|
|
47
|
-
> <html>
|
|
48
|
-
> <body>
|
|
49
|
-
> <script>parent.postMessage(location.href, location.origin)</script>
|
|
50
|
-
> </body>
|
|
51
|
-
> </html>
|
|
52
|
-
> ```
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 3. Initialize the Provider
|
|
57
|
-
|
|
58
|
-
Wrap your app's root with `<TideCloakContextProvider>` to enable authentication context throughout the component tree.
|
|
59
|
-
|
|
60
|
-
If you're using React Router, your setup might look like this:
|
|
61
|
-
|
|
62
|
-
**File:** `src/App.tsx`
|
|
63
|
-
|
|
64
|
-
```tsx
|
|
65
|
-
import React from 'react';
|
|
66
|
-
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
67
|
-
import { TideCloakContextProvider } from '@tidecloak/react';
|
|
68
|
-
import adapter from '../tidecloakAdapter.json';
|
|
69
|
-
import Home from './pages/Home';
|
|
70
|
-
import RedirectPage from './pages/auth/RedirectPage';
|
|
71
|
-
|
|
72
|
-
export default function App() {
|
|
73
|
-
return (
|
|
74
|
-
<TideCloakContextProvider config={adapter}>
|
|
75
|
-
<BrowserRouter>
|
|
76
|
-
<Routes>
|
|
77
|
-
<Route path="/" element={<Home />} />
|
|
78
|
-
<Route path="/auth/redirect" element={<RedirectPage />} />
|
|
79
|
-
{/* Add additional routes here */}
|
|
80
|
-
</Routes>
|
|
81
|
-
</BrowserRouter>
|
|
82
|
-
</TideCloakContextProvider>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
> ⚠️ If you don't define a route at `/auth/redirect`, and you're using the default `redirectUri`, your app **will break after login/logout**. Either create this route or override `redirectUri` in the provider config.
|
|
88
|
-
>
|
|
89
|
-
> If you override the `redirectUri`, you **must** ensure that the custom path exists in your router. Otherwise, the app will redirect to a non-existent route and fail.
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## 4. Redirect URI Handling
|
|
94
|
-
|
|
95
|
-
TideCloak supports an optional `redirectUri` config field. This defines where the user is sent after login/logout.
|
|
96
|
-
|
|
97
|
-
If omitted, it defaults to:
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
`${window.location.origin}/auth/redirect`
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
> Example: If your app runs at `http://localhost:3000`, then by default the redirect path is `http://localhost:3000/auth/redirect`.
|
|
104
|
-
|
|
105
|
-
You must **create this route** if you use the default, or explicitly override it:
|
|
106
|
-
|
|
107
|
-
```tsx
|
|
108
|
-
<TideCloakContextProvider config={{ ...adapter, redirectUri: 'https://yourapp.com/auth/callback' }}>
|
|
109
|
-
<YourApp />
|
|
110
|
-
</TideCloakContextProvider>
|
|
111
7
|
```
|
|
112
8
|
|
|
113
|
-
> ⚠️ If you override the `redirectUri`, make sure the specified path exists in your app. Missing this route will cause failed redirects.
|
|
114
|
-
|
|
115
|
-
### Example: Redirect Handling Page
|
|
116
|
-
|
|
117
|
-
**File:** `src/pages/auth/RedirectPage.tsx`
|
|
118
|
-
|
|
119
|
-
```tsx
|
|
120
|
-
import { useEffect } from 'react';
|
|
121
|
-
import { useNavigate } from 'react-router-dom';
|
|
122
|
-
import { useTideCloak } from '@tidecloak/react';
|
|
123
|
-
|
|
124
|
-
export default function RedirectPage() {
|
|
125
|
-
const { authenticated, isInitializing, logout } = useTideCloak();
|
|
126
|
-
const navigate = useNavigate();
|
|
127
|
-
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
const params = new URLSearchParams(window.location.search);
|
|
130
|
-
if (params.get("auth") === "failed") {
|
|
131
|
-
sessionStorage.setItem("tokenExpired", "true");
|
|
132
|
-
logout();
|
|
133
|
-
}
|
|
134
|
-
}, []);
|
|
135
|
-
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
if (!isInitializing) {
|
|
138
|
-
navigate(authenticated ? '/home' : '/');
|
|
139
|
-
}
|
|
140
|
-
}, [authenticated, isInitializing, navigate]);
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
<div style={{
|
|
144
|
-
minHeight: '100vh',
|
|
145
|
-
display: 'flex',
|
|
146
|
-
alignItems: 'center',
|
|
147
|
-
justifyContent: 'center',
|
|
148
|
-
fontSize: '1rem',
|
|
149
|
-
color: '#555',
|
|
150
|
-
}}>
|
|
151
|
-
<p>Waiting for authentication...</p>
|
|
152
|
-
</div>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
**Description:** This page helps finalize the login or logout flow, and also reacts to token expiration events that may have triggered a redirect. It's required if you're using the default `redirectUri`. If you override the redirect URI, the file is optional-but the **route** for the redirect **must** still exist in your app.
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## 5. Using the `useTideCloak` Hook
|
|
162
|
-
|
|
163
|
-
Use this hook anywhere in your component tree to manage authentication:
|
|
164
|
-
|
|
165
|
-
```tsx
|
|
166
|
-
import { useTideCloak } from '@tidecloak/react';
|
|
167
|
-
|
|
168
|
-
function Header() {
|
|
169
|
-
const {
|
|
170
|
-
authenticated,
|
|
171
|
-
login,
|
|
172
|
-
logout,
|
|
173
|
-
token,
|
|
174
|
-
tokenExp,
|
|
175
|
-
refreshToken,
|
|
176
|
-
getValueFromToken,
|
|
177
|
-
getValueFromIdToken,
|
|
178
|
-
hasRealmRole,
|
|
179
|
-
hasClientRole,
|
|
180
|
-
doEncrypt,
|
|
181
|
-
doDecrypt,
|
|
182
|
-
} = useTideCloak();
|
|
183
|
-
|
|
184
|
-
return (
|
|
185
|
-
<header>
|
|
186
|
-
{authenticated ? (
|
|
187
|
-
<>
|
|
188
|
-
<span>Logged in</span>
|
|
189
|
-
<button onClick={logout}>Log Out</button>
|
|
190
|
-
</>
|
|
191
|
-
) : (
|
|
192
|
-
<button onClick={login}>Log In</button>
|
|
193
|
-
)}
|
|
194
|
-
{token && (
|
|
195
|
-
<small>Expires at {new Date(tokenExp * 1000).toLocaleTimeString()}</small>
|
|
196
|
-
)}
|
|
197
|
-
</header>
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
| Name | Type | Description |
|
|
203
|
-
| ------------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------- |
|
|
204
|
-
| `authenticated` | `boolean` | Whether the user is logged in. |
|
|
205
|
-
| `login()` / `logout()` | `() => void` | Trigger the login or logout flows. |
|
|
206
|
-
| `token`, `tokenExp` | `string`, `number` | Access token and its expiration timestamp. |
|
|
207
|
-
| Automatic token refresh | built-in | Tokens refresh silently on expiration-no manual setup needed. |
|
|
208
|
-
| `refreshToken()` | `() => Promise<boolean>` | Force a silent token renewal. |
|
|
209
|
-
| `getValueFromToken(key)` | `(key: string) => any` | Read a custom claim from the access token. |
|
|
210
|
-
| `getValueFromIdToken(key)` | `(key: string) => any` | Read a custom claim from the ID token. |
|
|
211
|
-
| `hasRealmRole(role)` | `(role: string) => boolean` | Check a realm-level role. |
|
|
212
|
-
| `hasClientRole(role, client?)` | `(role: string, client?: string) => boolean` | Check a client-level role; defaults to your app's client ID if omitted. |
|
|
213
|
-
| `doEncrypt(data)` / `doDecrypt(data)` | `(data: any) => Promise<any>` | Encrypt or decrypt payloads via TideCloak's built-in service. |
|
|
214
|
-
|
|
215
9
|
---
|
|
216
10
|
|
|
217
|
-
##
|
|
218
|
-
|
|
219
|
-
Use these components to show or hide content based on authentication state:
|
|
11
|
+
## Choose Your Mode
|
|
220
12
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<>
|
|
227
|
-
<Authenticated>
|
|
228
|
-
<h1>Dashboard</h1>
|
|
229
|
-
{/* Protected widgets */}
|
|
230
|
-
</Authenticated>
|
|
231
|
-
|
|
232
|
-
<Unauthenticated>
|
|
233
|
-
<p>Please log in to access the dashboard.</p>
|
|
234
|
-
</Unauthenticated>
|
|
235
|
-
</>
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
* `<Authenticated>`: renders children only when `authenticated === true`
|
|
241
|
-
* `<Unauthenticated>`: renders children only when `authenticated === false`
|
|
13
|
+
| I'm building... | Use this mode |
|
|
14
|
+
|-----------------|---------------|
|
|
15
|
+
| A React web app or SPA | [Front-channel](docs/FRONT_CHANNEL.md) |
|
|
16
|
+
| A secure app where tokens should stay on my server | [Hybrid/BFF](docs/HYBRID_MODE.md) |
|
|
17
|
+
| An Electron, Tauri, or React Native app | [Native](docs/NATIVE_MODE.md) |
|
|
242
18
|
|
|
243
19
|
---
|
|
244
20
|
|
|
245
|
-
##
|
|
21
|
+
## Quick Comparison
|
|
246
22
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
]);
|
|
254
|
-
|
|
255
|
-
// Decrypt:
|
|
256
|
-
const decryptedArray = await doDecrypt([
|
|
257
|
-
{ encrypted: encryptedArray[0], tags: ['email'] },
|
|
258
|
-
]);
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
> **Important:** The `data` property **must** be either a string or a `Uint8Array` (raw bytes).\
|
|
263
|
-
> When you encrypt a string, decryption returns a string.\
|
|
264
|
-
> When you encrypt a `Uint8Array`, decryption returns a `Uint8Array`.
|
|
265
|
-
>
|
|
266
|
-
|
|
267
|
-
### Valid example:
|
|
268
|
-
>
|
|
269
|
-
> ```ts
|
|
270
|
-
> // Before testing below, ensure you've set up the necessary roles:
|
|
271
|
-
> const multi_encrypted_addresses = await doEncrypt([
|
|
272
|
-
> {
|
|
273
|
-
> data: "10 Smith Street",
|
|
274
|
-
> tags: ["street"]
|
|
275
|
-
> },
|
|
276
|
-
> {
|
|
277
|
-
> data: "Southport",
|
|
278
|
-
> tags: ["suburb"]
|
|
279
|
-
> },
|
|
280
|
-
> {
|
|
281
|
-
> data: "20 James Street - Burleigh Heads",
|
|
282
|
-
> tags: ["street", "suburb"]
|
|
283
|
-
> }
|
|
284
|
-
> ]);
|
|
285
|
-
> ```
|
|
286
|
-
>
|
|
287
|
-
|
|
288
|
-
### Invalid (will fail):
|
|
289
|
-
>
|
|
290
|
-
> ```ts
|
|
291
|
-
> // Prepare data for encryption
|
|
292
|
-
> const dataToEncrypt = {
|
|
293
|
-
> title: noteData.title,
|
|
294
|
-
> content: noteData.content
|
|
295
|
-
> };
|
|
296
|
-
>
|
|
297
|
-
> // Encrypt the note data using TideCloak (this will error)
|
|
298
|
-
> const encryptedArray = await doEncrypt([{ data: dataToEncrypt, tags: ['note'] }]);
|
|
299
|
-
> ```
|
|
300
|
-
|
|
301
|
-
> **Permissions:** Encryption requires `_tide_<tag>.selfencrypt`; decryption requires `_tide_<tag>.selfdecrypt`.
|
|
302
|
-
>
|
|
303
|
-
> **Order guarantee:** Output preserves input order.
|
|
304
|
-
>
|
|
23
|
+
| | Front-channel | Hybrid/BFF | Native |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| Tokens stored in | Browser | Server | App (secure storage) |
|
|
26
|
+
| Best for | Web apps | High-security apps | Desktop/mobile apps |
|
|
27
|
+
| Setup complexity | Easy | Medium | Medium |
|
|
28
|
+
| Works offline | No | No | Yes |
|
|
305
29
|
|
|
306
30
|
---
|
|
307
31
|
|
|
308
|
-
##
|
|
32
|
+
## Requirements
|
|
309
33
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
* **Role-Based UI**: combine `hasRealmRole`, `hasClientRole`, and guard components
|
|
314
|
-
* **Lazy Initialization**: optionally wrap `<TideCloakContextProvider>` around only protected routes
|
|
315
|
-
|
|
316
|
-
---
|
|
34
|
+
- React 18+
|
|
35
|
+
- A TideCloak server ([setup guide](https://github.com/tide-foundation/tidecloak-gettingstarted))
|
|
36
|
+
- A registered client in your TideCloak realm
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useAuthCallback } from '../hooks/useAuthCallback';
|
|
3
|
+
/**
|
|
4
|
+
* Component for handling OAuth callback pages in hybrid mode.
|
|
5
|
+
*
|
|
6
|
+
* Drop this component into your callback route and it handles everything:
|
|
7
|
+
* - Detecting if this is a callback page
|
|
8
|
+
* - Processing the authorization code
|
|
9
|
+
* - Exchanging tokens with your backend
|
|
10
|
+
* - Calling onSuccess/onError callbacks
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // Simple usage with navigation
|
|
15
|
+
* function OAuthCallbackPage() {
|
|
16
|
+
* const navigate = useNavigate();
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <AuthCallback
|
|
20
|
+
* onSuccess={(returnUrl) => navigate(returnUrl || '/')}
|
|
21
|
+
* onError={(error) => console.error(error)}
|
|
22
|
+
* loadingComponent={<Spinner />}
|
|
23
|
+
* errorComponent={({ error }) => <Alert status="error">{error.message}</Alert>}
|
|
24
|
+
* />
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* // Or in routes directly
|
|
29
|
+
* <Route path="/auth/callback" element={
|
|
30
|
+
* <AuthCallback
|
|
31
|
+
* onSuccess={(url) => window.location.assign(url || '/')}
|
|
32
|
+
* loadingComponent={<LoadingScreen />}
|
|
33
|
+
* />
|
|
34
|
+
* } />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function AuthCallback({ loadingComponent = null, errorComponent, successComponent, children, ...options }) {
|
|
38
|
+
const { isCallback, isProcessing, isSuccess, error, returnUrl } = useAuthCallback(options);
|
|
39
|
+
// Not a callback page - render children or nothing
|
|
40
|
+
if (!isCallback) {
|
|
41
|
+
return children !== null && children !== void 0 ? children : null;
|
|
42
|
+
}
|
|
43
|
+
// Processing
|
|
44
|
+
if (isProcessing) {
|
|
45
|
+
return loadingComponent;
|
|
46
|
+
}
|
|
47
|
+
// Error
|
|
48
|
+
if (error) {
|
|
49
|
+
if (errorComponent) {
|
|
50
|
+
if (typeof errorComponent === 'function') {
|
|
51
|
+
const ErrorComponent = errorComponent;
|
|
52
|
+
return _jsx(ErrorComponent, { error: error });
|
|
53
|
+
}
|
|
54
|
+
return errorComponent;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// Success
|
|
59
|
+
if (isSuccess) {
|
|
60
|
+
if (successComponent) {
|
|
61
|
+
if (typeof successComponent === 'function') {
|
|
62
|
+
const SuccessComponent = successComponent;
|
|
63
|
+
return _jsx(SuccessComponent, { returnUrl: returnUrl });
|
|
64
|
+
}
|
|
65
|
+
return successComponent;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Simple callback page that auto-redirects on success.
|
|
73
|
+
* Use this when you just want the callback handled with minimal UI.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* <Route path="/auth/callback" element={
|
|
78
|
+
* <SimpleAuthCallback
|
|
79
|
+
* defaultRedirect="/"
|
|
80
|
+
* loginPage="/login"
|
|
81
|
+
* loadingComponent={<FullPageSpinner />}
|
|
82
|
+
* />
|
|
83
|
+
* } />
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function SimpleAuthCallback({ defaultRedirect = '/', loginPage = '/login', loadingComponent, errorComponent, }) {
|
|
87
|
+
return (_jsx(AuthCallback, { onSuccess: (returnUrl) => {
|
|
88
|
+
window.location.assign(returnUrl || defaultRedirect);
|
|
89
|
+
}, onMissingVerifierRedirectTo: loginPage, loadingComponent: loadingComponent, errorComponent: errorComponent }));
|
|
90
|
+
}
|