idenplane-sdk 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 +649 -0
- package/package.json +94 -0
package/README.md
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://idenplane.com/logo.svg" alt="Idenplane" width="60" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h2 align="center">idenplane-sdk</h2>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Official client SDK for <a href="https://idenplane.com">Idenplane</a></strong><br />
|
|
9
|
+
<sub>Zero-dependency TypeScript SDK with OAuth 2.0 PKCE, token management, React bindings, and Next.js support.</sub>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://img.shields.io/badge/version-1.0.0-blue" alt="v1.0.0" />
|
|
14
|
+
<img src="https://img.shields.io/badge/bundle-~5KB_gzipped-green" alt="bundle size" />
|
|
15
|
+
<img src="https://img.shields.io/badge/TypeScript-first-blue" alt="TypeScript" />
|
|
16
|
+
<img src="https://img.shields.io/badge/React-18%2B-61dafb" alt="React 18+" />
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install idenplane-sdk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Vanilla JavaScript / TypeScript
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { IdenplaneClient } from 'idenplane-sdk';
|
|
35
|
+
|
|
36
|
+
const idenplane = new IdenplaneClient({
|
|
37
|
+
url: 'http://localhost:3000',
|
|
38
|
+
realm: 'my-realm',
|
|
39
|
+
clientId: 'my-app',
|
|
40
|
+
redirectUri: 'http://localhost:5173/callback',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Initialize (restores existing session if any)
|
|
44
|
+
await idenplane.init();
|
|
45
|
+
|
|
46
|
+
if (!idenplane.isAuthenticated()) {
|
|
47
|
+
// Redirects to Idenplane login page
|
|
48
|
+
await idenplane.login();
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
On your callback page:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const idenplane = new IdenplaneClient({ /* same config */ });
|
|
56
|
+
const success = await idenplane.handleCallback();
|
|
57
|
+
if (success) {
|
|
58
|
+
const user = idenplane.getUserInfo();
|
|
59
|
+
console.log(`Welcome, ${user?.name}!`);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### React
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { AuthProvider, useAuth, useUser, usePermissions } from 'idenplane-sdk/react';
|
|
67
|
+
|
|
68
|
+
function App() {
|
|
69
|
+
return (
|
|
70
|
+
<AuthProvider
|
|
71
|
+
serverUrl="http://localhost:3000"
|
|
72
|
+
realm="my-realm"
|
|
73
|
+
clientId="my-app"
|
|
74
|
+
redirectUri="http://localhost:5173/callback"
|
|
75
|
+
onLogin={(tokens) => console.log('Logged in!')}
|
|
76
|
+
onLogout={() => console.log('Logged out!')}
|
|
77
|
+
onError={(err) => console.error('Auth error:', err)}
|
|
78
|
+
>
|
|
79
|
+
<Main />
|
|
80
|
+
</AuthProvider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function Main() {
|
|
85
|
+
const { isAuthenticated, isLoading, login, logout, getToken } = useAuth();
|
|
86
|
+
const user = useUser();
|
|
87
|
+
const { hasRole, hasPermission, roles } = usePermissions();
|
|
88
|
+
|
|
89
|
+
if (isLoading) return <div>Loading...</div>;
|
|
90
|
+
|
|
91
|
+
if (!isAuthenticated) {
|
|
92
|
+
return <button onClick={() => login()}>Sign In</button>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
<p>Welcome, {user?.name}!</p>
|
|
98
|
+
<p>Token: {getToken()}</p>
|
|
99
|
+
{hasRole('admin') && <p>You are an admin.</p>}
|
|
100
|
+
{hasPermission('read:reports') && <p>You can read reports.</p>}
|
|
101
|
+
<p>Your roles: {roles.join(', ')}</p>
|
|
102
|
+
<button onClick={() => logout()}>Sign Out</button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### AuthProvider with pre-built client
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { IdenplaneClient } from 'idenplane-sdk';
|
|
112
|
+
import { AuthProvider } from 'idenplane-sdk/react';
|
|
113
|
+
|
|
114
|
+
const client = new IdenplaneClient({
|
|
115
|
+
url: 'http://localhost:3000',
|
|
116
|
+
realm: 'my-realm',
|
|
117
|
+
clientId: 'my-app',
|
|
118
|
+
redirectUri: 'http://localhost:5173/callback',
|
|
119
|
+
refreshStrategy: 'rotation', // 'rotation' | 'silent' | 'eager'
|
|
120
|
+
storage: 'sessionStorage', // 'sessionStorage' | 'localStorage' | 'memory'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function App() {
|
|
124
|
+
return (
|
|
125
|
+
<AuthProvider client={client}>
|
|
126
|
+
<Main />
|
|
127
|
+
</AuthProvider>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### ProtectedRoute
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { AuthProvider, ProtectedRoute } from 'idenplane-sdk/react';
|
|
136
|
+
import { useNavigate } from 'react-router-dom';
|
|
137
|
+
|
|
138
|
+
function AdminPage() {
|
|
139
|
+
const navigate = useNavigate();
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<ProtectedRoute
|
|
143
|
+
roles={['admin']}
|
|
144
|
+
fallback={<div>Loading...</div>}
|
|
145
|
+
onUnauthorized={() => {
|
|
146
|
+
navigate('/login');
|
|
147
|
+
return null;
|
|
148
|
+
}}
|
|
149
|
+
onForbidden={() => <div>Access denied. Admin role required.</div>}
|
|
150
|
+
>
|
|
151
|
+
<AdminDashboard />
|
|
152
|
+
</ProtectedRoute>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Token Refresh Strategies
|
|
160
|
+
|
|
161
|
+
Configure how the SDK refreshes tokens via the `refreshStrategy` option:
|
|
162
|
+
|
|
163
|
+
| Strategy | Description |
|
|
164
|
+
|----------|-------------|
|
|
165
|
+
| `rotation` | *(default)* Uses refresh token grant to get a new access token |
|
|
166
|
+
| `eager` | Like `rotation`, but refreshes proactively at 2× the `refreshBuffer` |
|
|
167
|
+
| `silent` | Uses a hidden iframe with `prompt=none` for silent re-auth |
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const client = new IdenplaneClient({
|
|
171
|
+
url: 'http://localhost:3000',
|
|
172
|
+
realm: 'my-realm',
|
|
173
|
+
clientId: 'my-app',
|
|
174
|
+
redirectUri: 'http://localhost:5173/callback',
|
|
175
|
+
refreshStrategy: 'eager', // refresh 60 seconds before expiry (2× default 30s)
|
|
176
|
+
refreshBuffer: 30, // seconds before expiry to trigger refresh
|
|
177
|
+
autoRefresh: true, // enable automatic refresh (default: true)
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
For the `silent` strategy, your app's redirect URI page must post a message back to the parent window:
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<!-- /callback page for silent refresh -->
|
|
185
|
+
<script>
|
|
186
|
+
const params = new URLSearchParams(window.location.search);
|
|
187
|
+
window.parent.postMessage({
|
|
188
|
+
type: 'idenplane:silent_callback',
|
|
189
|
+
code: params.get('code'),
|
|
190
|
+
state: params.get('state'),
|
|
191
|
+
error: params.get('error'),
|
|
192
|
+
}, window.location.origin);
|
|
193
|
+
</script>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Event System
|
|
199
|
+
|
|
200
|
+
Subscribe to SDK events to react to authentication state changes:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const client = new IdenplaneClient({ ... });
|
|
204
|
+
|
|
205
|
+
// Subscribe — returns an unsubscribe function
|
|
206
|
+
const unsubscribe = client.on('login', (tokens) => {
|
|
207
|
+
console.log('User logged in, token:', tokens.access_token);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
client.on('logout', () => {
|
|
211
|
+
console.log('User logged out');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
client.on('tokenRefresh', (tokens) => {
|
|
215
|
+
console.log('Token refreshed silently');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
client.on('error', (error) => {
|
|
219
|
+
console.error('Auth error:', error.message);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
client.on('ready', (isAuthenticated) => {
|
|
223
|
+
console.log('Client initialized, authenticated:', isAuthenticated);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Unsubscribe when done
|
|
227
|
+
unsubscribe();
|
|
228
|
+
// or
|
|
229
|
+
client.off('login', myHandler);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Alternatively, pass callbacks directly in the config:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const client = new IdenplaneClient({
|
|
236
|
+
url: 'http://localhost:3000',
|
|
237
|
+
realm: 'my-realm',
|
|
238
|
+
clientId: 'my-app',
|
|
239
|
+
redirectUri: '/callback',
|
|
240
|
+
onLogin: (tokens) => saveSession(tokens),
|
|
241
|
+
onLogout: () => clearSession(),
|
|
242
|
+
onError: (err) => reportError(err),
|
|
243
|
+
onTokenRefresh: (tokens) => updateSession(tokens),
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Next.js Integration
|
|
250
|
+
|
|
251
|
+
### Server-side authentication in API routes
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// pages/api/profile.ts (or app/api/profile/route.ts)
|
|
255
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
256
|
+
import { getServerSideAuth } from 'idenplane-sdk/server';
|
|
257
|
+
|
|
258
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
259
|
+
const { user, isAuthenticated } = await getServerSideAuth(req, {
|
|
260
|
+
issuerUrl: 'http://localhost:3000',
|
|
261
|
+
realm: 'my-realm',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (!isAuthenticated) {
|
|
265
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
res.json({
|
|
269
|
+
id: user!.sub,
|
|
270
|
+
username: user!.preferred_username,
|
|
271
|
+
email: user!.email,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### getServerSideProps
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { getServerSideAuth } from 'idenplane-sdk/server';
|
|
280
|
+
|
|
281
|
+
export const getServerSideProps = async ({ req }) => {
|
|
282
|
+
const { user, isAuthenticated } = await getServerSideAuth(req, {
|
|
283
|
+
issuerUrl: 'http://localhost:3000',
|
|
284
|
+
realm: 'my-realm',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (!isAuthenticated) {
|
|
288
|
+
return { redirect: { destination: '/login', permanent: false } };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
props: {
|
|
293
|
+
user: { sub: user!.sub, name: user!.name, email: user!.email },
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Next.js Middleware
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// middleware.ts
|
|
303
|
+
import { NextResponse } from 'next/server';
|
|
304
|
+
import type { NextRequest } from 'next/server';
|
|
305
|
+
import { createNextMiddleware } from 'idenplane-sdk/server';
|
|
306
|
+
|
|
307
|
+
const authMiddleware = createNextMiddleware({
|
|
308
|
+
issuerUrl: 'http://localhost:3000',
|
|
309
|
+
realm: 'my-realm',
|
|
310
|
+
protectedPaths: ['/dashboard', '/api/protected'],
|
|
311
|
+
loginPath: '/login',
|
|
312
|
+
forbiddenPath: '/403',
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
export async function middleware(request: NextRequest) {
|
|
316
|
+
const result = await authMiddleware(request as any);
|
|
317
|
+
|
|
318
|
+
if (result?.redirect) {
|
|
319
|
+
return NextResponse.redirect(new URL(result.redirect, request.url));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return NextResponse.next();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export const config = {
|
|
326
|
+
matcher: ['/((?!_next|public|favicon.ico).*)'],
|
|
327
|
+
};
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## SSR Compatibility
|
|
333
|
+
|
|
334
|
+
The SDK is fully SSR-safe. When `window` is undefined (e.g., in Node.js / Next.js):
|
|
335
|
+
- Storage automatically falls back to `MemoryStorage`
|
|
336
|
+
- `login()` and `logout()` browser redirects throw a safe error
|
|
337
|
+
- `init()` and token operations work without errors
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
// This works safely in both browser and SSR environments
|
|
341
|
+
const client = new IdenplaneClient({
|
|
342
|
+
url: 'http://localhost:3000',
|
|
343
|
+
realm: 'my-realm',
|
|
344
|
+
clientId: 'my-app',
|
|
345
|
+
redirectUri: '/callback',
|
|
346
|
+
storage: 'memory', // explicit for SSR, or falls back automatically
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## API Reference
|
|
353
|
+
|
|
354
|
+
### `IdenplaneClient`
|
|
355
|
+
|
|
356
|
+
#### Constructor
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
new IdenplaneClient(config: IdenplaneConfig)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
| Option | Type | Default | Description |
|
|
363
|
+
|--------|------|---------|-------------|
|
|
364
|
+
| `url` | `string` | *required* | Idenplane server URL |
|
|
365
|
+
| `realm` | `string` | *required* | Realm name |
|
|
366
|
+
| `clientId` | `string` | *required* | OAuth2 client ID (PUBLIC client) |
|
|
367
|
+
| `redirectUri` | `string` | *required* | Callback URL after login |
|
|
368
|
+
| `scopes` | `string[]` | `['openid', 'profile', 'email']` | OAuth2 scopes to request |
|
|
369
|
+
| `storage` | `'sessionStorage' \| 'localStorage' \| 'memory'` | `'sessionStorage'` | Where to persist tokens |
|
|
370
|
+
| `autoRefresh` | `boolean` | `true` | Automatically refresh tokens before expiry |
|
|
371
|
+
| `refreshBuffer` | `number` | `30` | Seconds before expiry to trigger refresh |
|
|
372
|
+
| `refreshStrategy` | `'rotation' \| 'silent' \| 'eager'` | `'rotation'` | Token refresh strategy |
|
|
373
|
+
| `postLogoutRedirectUri` | `string` | — | URL to redirect after logout |
|
|
374
|
+
| `onLogin` | `(tokens: TokenResponse) => void` | — | Called on successful login |
|
|
375
|
+
| `onLogout` | `() => void` | — | Called on logout |
|
|
376
|
+
| `onError` | `(error: Error) => void` | — | Called on error |
|
|
377
|
+
| `onTokenRefresh` | `(tokens: TokenResponse) => void` | — | Called on token refresh |
|
|
378
|
+
|
|
379
|
+
#### Methods
|
|
380
|
+
|
|
381
|
+
| Method | Returns | Description |
|
|
382
|
+
|--------|---------|-------------|
|
|
383
|
+
| `init()` | `Promise<boolean>` | Initialize client, restore session. Returns `true` if authenticated |
|
|
384
|
+
| `login(options?)` | `Promise<void>` | Redirect to Idenplane login page |
|
|
385
|
+
| `handleCallback(url?)` | `Promise<boolean>` | Exchange authorization code for tokens |
|
|
386
|
+
| `logout()` | `Promise<void>` | Clear tokens and call server logout endpoint |
|
|
387
|
+
| `getAccessToken()` | `string \| null` | Current access token (null if expired) |
|
|
388
|
+
| `getTokenClaims()` | `TokenClaims \| null` | Parsed access token JWT payload |
|
|
389
|
+
| `getIdTokenClaims()` | `TokenClaims \| null` | Parsed ID token JWT payload |
|
|
390
|
+
| `isAuthenticated()` | `boolean` | Whether user has a valid, non-expired access token |
|
|
391
|
+
| `fetchUserInfo()` | `Promise<UserInfo \| null>` | Fetch user info from the server UserInfo endpoint |
|
|
392
|
+
| `getUserInfo()` | `UserInfo \| null` | Cached user info (from ID token or last fetch) |
|
|
393
|
+
| `hasRealmRole(role)` | `boolean` | Check if user has a realm-level role |
|
|
394
|
+
| `hasClientRole(clientId, role)` | `boolean` | Check if user has a client-level role |
|
|
395
|
+
| `hasPermission(permission)` | `boolean` | Check realm or default client role |
|
|
396
|
+
| `getRealmRoles()` | `string[]` | All realm roles for the current user |
|
|
397
|
+
| `getClientRoles(clientId)` | `string[]` | All client roles for a specific client |
|
|
398
|
+
| `refreshTokens()` | `Promise<TokenResponse>` | Manually trigger a token refresh |
|
|
399
|
+
| `on(event, handler)` | `() => void` | Subscribe to SDK events — returns unsubscribe function |
|
|
400
|
+
| `off(event, handler)` | `void` | Unsubscribe from SDK events |
|
|
401
|
+
|
|
402
|
+
#### Events
|
|
403
|
+
|
|
404
|
+
| Event | Payload | Fires When |
|
|
405
|
+
|-------|---------|------------|
|
|
406
|
+
| `login` | `TokenResponse` | After successful login or callback |
|
|
407
|
+
| `logout` | — | After logout completes |
|
|
408
|
+
| `tokenRefresh` | `TokenResponse` | After a silent token refresh |
|
|
409
|
+
| `error` | `Error` | On any authentication error |
|
|
410
|
+
| `ready` | `boolean` | After `init()` completes (`true` if authenticated) |
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### React
|
|
415
|
+
|
|
416
|
+
Import from `idenplane-sdk/react`.
|
|
417
|
+
|
|
418
|
+
#### `<AuthProvider>`
|
|
419
|
+
|
|
420
|
+
| Prop | Type | Description |
|
|
421
|
+
|------|------|-------------|
|
|
422
|
+
| `client` | `IdenplaneClient` | Pre-built client (mutually exclusive with inline config props) |
|
|
423
|
+
| `serverUrl` | `string` | Idenplane server URL (alternative to `client`) |
|
|
424
|
+
| `realm` | `string` | Realm name (alternative to `client`) |
|
|
425
|
+
| `clientId` | `string` | OAuth2 client ID (alternative to `client`) |
|
|
426
|
+
| `redirectUri` | `string` | Redirect URI (alternative to `client`) |
|
|
427
|
+
| `scope` | `string[]` | OAuth2 scopes |
|
|
428
|
+
| `autoHandleCallback` | `boolean` | Auto-handle `/callback` redirect (default: `true`) |
|
|
429
|
+
| `onReady` | `(authenticated: boolean) => void` | Called when initialization is complete |
|
|
430
|
+
| `onLogin` | `(tokens: TokenResponse) => void` | Called on login |
|
|
431
|
+
| `onLogout` | `() => void` | Called on logout |
|
|
432
|
+
| `onError` | `(error: Error) => void` | Called on error |
|
|
433
|
+
| `onTokenRefresh` | `(tokens: TokenResponse) => void` | Called on token refresh |
|
|
434
|
+
|
|
435
|
+
#### `useAuth()`
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
const { isAuthenticated, isLoading, login, logout, getToken, user, client } = useAuth();
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
| Property | Type | Description |
|
|
442
|
+
|----------|------|-------------|
|
|
443
|
+
| `isAuthenticated` | `boolean` | Whether the user is logged in |
|
|
444
|
+
| `isLoading` | `boolean` | `true` during initialization |
|
|
445
|
+
| `login` | `(options?) => Promise<void>` | Trigger login redirect |
|
|
446
|
+
| `logout` | `() => Promise<void>` | Trigger logout |
|
|
447
|
+
| `getToken` | `() => string \| null` | Get current access token |
|
|
448
|
+
| `user` | `UserInfo \| null` | Current user profile |
|
|
449
|
+
| `client` | `IdenplaneClient` | Underlying SDK client instance |
|
|
450
|
+
|
|
451
|
+
#### `useUser()`
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
const user = useUser();
|
|
455
|
+
// user?.sub, user?.name, user?.email, user?.preferred_username, etc.
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Returns the current user's profile information, or `null` if not authenticated.
|
|
459
|
+
|
|
460
|
+
#### `usePermissions()`
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const { hasRole, hasPermission, roles } = usePermissions();
|
|
464
|
+
|
|
465
|
+
hasRole('admin'); // boolean — check realm role
|
|
466
|
+
hasPermission('read:reports'); // boolean — check realm or client role
|
|
467
|
+
roles; // string[] — all realm roles
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
#### `<ProtectedRoute>`
|
|
471
|
+
|
|
472
|
+
| Prop | Type | Description |
|
|
473
|
+
|------|------|-------------|
|
|
474
|
+
| `roles` | `string[]` | Required realm roles (optional) |
|
|
475
|
+
| `fallback` | `ReactNode` | Shown while loading |
|
|
476
|
+
| `onUnauthorized` | `() => ReactNode \| null` | Called when not authenticated |
|
|
477
|
+
| `onForbidden` | `() => ReactNode \| null` | Called when lacking required roles |
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
### Server (`idenplane-sdk/server`)
|
|
482
|
+
|
|
483
|
+
#### `verifyToken(token, config)`
|
|
484
|
+
|
|
485
|
+
Verifies a JWT access token using the server's JWKS endpoint.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
const payload = await verifyToken(accessToken, {
|
|
489
|
+
issuerUrl: 'http://localhost:3000',
|
|
490
|
+
realm: 'my-realm',
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### `getServerSideAuth(req, config)`
|
|
495
|
+
|
|
496
|
+
Helper for Next.js API routes and `getServerSideProps`.
|
|
497
|
+
|
|
498
|
+
#### `createNextMiddleware(config)`
|
|
499
|
+
|
|
500
|
+
Factory for Next.js middleware with route protection.
|
|
501
|
+
|
|
502
|
+
#### `createIdenplaneMiddleware(config)`
|
|
503
|
+
|
|
504
|
+
Express middleware for token validation.
|
|
505
|
+
|
|
506
|
+
#### `createIdenplaneGuard(config)`
|
|
507
|
+
|
|
508
|
+
NestJS guard factory for token validation.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Backend Integration
|
|
513
|
+
|
|
514
|
+
### Express
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import express from 'express';
|
|
518
|
+
import { createIdenplaneMiddleware } from 'idenplane-sdk/server';
|
|
519
|
+
|
|
520
|
+
const app = express();
|
|
521
|
+
const idenplane = createIdenplaneMiddleware({
|
|
522
|
+
issuerUrl: 'http://localhost:3000',
|
|
523
|
+
realm: 'my-realm',
|
|
524
|
+
requiredRoles: ['user'], // optional
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
app.get('/api/profile', idenplane, (req: any, res) => {
|
|
528
|
+
res.json(req.user); // IdenplaneTokenPayload
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### NestJS
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { createIdenplaneGuard } from 'idenplane-sdk/server';
|
|
536
|
+
|
|
537
|
+
const IdenplaneGuard = createIdenplaneGuard({
|
|
538
|
+
issuerUrl: 'http://localhost:3000',
|
|
539
|
+
realm: 'my-realm',
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
@Controller('api')
|
|
543
|
+
export class AppController {
|
|
544
|
+
@Get('profile')
|
|
545
|
+
@UseGuards(IdenplaneGuard)
|
|
546
|
+
getProfile(@Req() req) {
|
|
547
|
+
return req.user;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Attaching Tokens to API Requests
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
// With fetch
|
|
558
|
+
const res = await fetch('/api/profile', {
|
|
559
|
+
headers: {
|
|
560
|
+
Authorization: `Bearer ${idenplane.getAccessToken()}`,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// With axios interceptor
|
|
565
|
+
api.interceptors.request.use((config) => {
|
|
566
|
+
const token = idenplane.getAccessToken();
|
|
567
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
568
|
+
return config;
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## Full SPA Callback Flow (React Router)
|
|
575
|
+
|
|
576
|
+
```tsx
|
|
577
|
+
// main.tsx
|
|
578
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
579
|
+
import { AuthProvider, ProtectedRoute } from 'idenplane-sdk/react';
|
|
580
|
+
|
|
581
|
+
function App() {
|
|
582
|
+
return (
|
|
583
|
+
<AuthProvider
|
|
584
|
+
serverUrl="http://localhost:3000"
|
|
585
|
+
realm="my-realm"
|
|
586
|
+
clientId="my-app"
|
|
587
|
+
redirectUri="http://localhost:5173/callback"
|
|
588
|
+
>
|
|
589
|
+
<BrowserRouter>
|
|
590
|
+
<Routes>
|
|
591
|
+
<Route path="/" element={<Home />} />
|
|
592
|
+
<Route
|
|
593
|
+
path="/dashboard"
|
|
594
|
+
element={
|
|
595
|
+
<ProtectedRoute
|
|
596
|
+
onUnauthorized={() => { window.location.href = '/login'; return null; }}
|
|
597
|
+
>
|
|
598
|
+
<Dashboard />
|
|
599
|
+
</ProtectedRoute>
|
|
600
|
+
}
|
|
601
|
+
/>
|
|
602
|
+
<Route path="/login" element={<Login />} />
|
|
603
|
+
</Routes>
|
|
604
|
+
</BrowserRouter>
|
|
605
|
+
</AuthProvider>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Error Handling
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
idenplane.on('error', (error) => {
|
|
616
|
+
console.error('Auth error:', error.message);
|
|
617
|
+
|
|
618
|
+
// Common errors:
|
|
619
|
+
// - "No refresh token available" — user needs to re-login
|
|
620
|
+
// - "State mismatch — possible CSRF attack"
|
|
621
|
+
// - "Missing PKCE verifier"
|
|
622
|
+
// - "token_exchange_failed" — authorization code invalid or expired
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## Idenplane Client Setup
|
|
629
|
+
|
|
630
|
+
For the SDK to work, register a **PUBLIC** client in Idenplane:
|
|
631
|
+
|
|
632
|
+
1. Open the Idenplane Admin Console at `/console`
|
|
633
|
+
2. Navigate to your realm > **Clients** > **Create**
|
|
634
|
+
3. Set **Client Type** to `PUBLIC`
|
|
635
|
+
4. Add your app's URL to **Redirect URIs** (e.g., `http://localhost:5173/callback`)
|
|
636
|
+
5. Add your app's origin to **Web Origins** (e.g., `http://localhost:5173`)
|
|
637
|
+
6. Enable the `authorization_code` and `refresh_token` grant types
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## License
|
|
642
|
+
|
|
643
|
+
MIT
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
<p align="center">
|
|
648
|
+
Part of <a href="https://idenplane.com">Idenplane</a> — Open-source Identity & Access Management
|
|
649
|
+
</p>
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "idenplane-sdk",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Client SDK for Idenplane Identity and Access Management Server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./react": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/react.d.ts",
|
|
23
|
+
"default": "./dist/react.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/react.d.cts",
|
|
27
|
+
"default": "./dist/react.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./server": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/server.d.ts",
|
|
33
|
+
"default": "./dist/server.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/server.d.cts",
|
|
37
|
+
"default": "./dist/server.cjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"jose": ">=5.0.0",
|
|
43
|
+
"react": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"react": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"jose": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:watch": "vitest"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/react": "^19.0.0",
|
|
62
|
+
"jose": "^6.1.3",
|
|
63
|
+
"react": "^19.0.0",
|
|
64
|
+
"tsup": "^8.4.0",
|
|
65
|
+
"typescript": "^5.7.0",
|
|
66
|
+
"vitest": "^4.1.0"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist",
|
|
70
|
+
"README.md"
|
|
71
|
+
],
|
|
72
|
+
"keywords": [
|
|
73
|
+
"idenplane",
|
|
74
|
+
"oauth2",
|
|
75
|
+
"oidc",
|
|
76
|
+
"openid-connect",
|
|
77
|
+
"authentication",
|
|
78
|
+
"identity",
|
|
79
|
+
"pkce",
|
|
80
|
+
"react"
|
|
81
|
+
],
|
|
82
|
+
"license": "MIT",
|
|
83
|
+
"repository": {
|
|
84
|
+
"type": "git",
|
|
85
|
+
"url": "https://github.com/idenplane/idenplane.git",
|
|
86
|
+
"directory": "packages/idenplane-js"
|
|
87
|
+
},
|
|
88
|
+
"publishConfig": {
|
|
89
|
+
"access": "public"
|
|
90
|
+
},
|
|
91
|
+
"overrides": {
|
|
92
|
+
"esbuild": ">=0.28.1"
|
|
93
|
+
}
|
|
94
|
+
}
|