@nihplod/sso-sdk 1.0.0 → 1.1.0
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 +374 -360
- package/dist/index.d.mts +99 -12
- package/dist/index.d.ts +99 -12
- package/dist/index.js +413 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +408 -74
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +17 -2
- package/dist/next/index.d.ts +17 -2
- package/dist/next/index.js +276 -45
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +274 -45
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +71 -8
- package/dist/react/index.d.ts +71 -8
- package/dist/react/index.js +476 -111
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +476 -111
- package/dist/react/index.mjs.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,360 +1,374 @@
|
|
|
1
|
-
# @nihplod/sso-sdk
|
|
2
|
-
|
|
3
|
-
NIHPLOD SSO
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @nihplod/sso-sdk
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { SsoClient } from "@nihplod/sso-sdk";
|
|
15
|
-
|
|
16
|
-
const sso = new SsoClient({
|
|
17
|
-
clientId: "your-client-id",
|
|
18
|
-
redirectUri: "https://yourapp.com/callback",
|
|
19
|
-
ssoBaseUrl: "https://nihplod.cn",
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// 1.
|
|
23
|
-
await sso.login();
|
|
24
|
-
|
|
25
|
-
// 2.
|
|
26
|
-
const token = await sso.handleCallback(window.location.href);
|
|
27
|
-
|
|
28
|
-
// 3.
|
|
29
|
-
const user = await sso.getUserInfo();
|
|
30
|
-
|
|
31
|
-
// 4.
|
|
32
|
-
await sso.logout();
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## API
|
|
38
|
-
|
|
39
|
-
### `new SsoClient(config)`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
|
44
|
-
|------|------|------|------|
|
|
45
|
-
| `clientId` | `string` | ✅ | OAuth Client ID |
|
|
46
|
-
| `redirectUri` | `string` | ✅ |
|
|
47
|
-
| `ssoBaseUrl` | `string` | ✅ | SSO
|
|
48
|
-
| `scopes` | `string` | ❌ |
|
|
49
|
-
| `clientSecret` | `string` | ❌ |
|
|
50
|
-
|
|
51
|
-
### `sso.login(returnUrl?)`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|------|------|------|
|
|
57
|
-
| `returnUrl` | `string` |
|
|
58
|
-
|
|
59
|
-
### `sso.getLoginUrl(returnUrl?)`
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
### `sso.handleCallback(callbackUrl)`
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
|
68
|
-
|------|------|------|
|
|
69
|
-
| `callbackUrl` | `string` |
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
### `sso.refreshToken()`
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
### `sso.getUserInfo()`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
interface SsoUser {
|
|
87
|
-
sub: string;
|
|
88
|
-
nickname?: string;
|
|
89
|
-
avatar?: string;
|
|
90
|
-
phone?: string; //
|
|
91
|
-
membership_level?: string;
|
|
92
|
-
total_points?: number;
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### `sso.getAccessToken()`
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
### `sso.isAuthenticated()`
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
### `sso.logout(redirectToSso?)`
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
|
113
|
-
|------|------|------|------|
|
|
114
|
-
| `redirectToSso` | `boolean` | `false` |
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
<a
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
1
|
+
# @nihplod/sso-sdk
|
|
2
|
+
|
|
3
|
+
NIHPLOD SSO client SDK — OAuth 2.0 Authorization Code + PKCE wrapper for sub-projects.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nihplod/sso-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { SsoClient } from "@nihplod/sso-sdk";
|
|
15
|
+
|
|
16
|
+
const sso = new SsoClient({
|
|
17
|
+
clientId: "your-client-id",
|
|
18
|
+
redirectUri: "https://yourapp.com/callback",
|
|
19
|
+
ssoBaseUrl: "https://nihplod.cn",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 1. Initiate login
|
|
23
|
+
await sso.login();
|
|
24
|
+
|
|
25
|
+
// 2. Handle callback (inside the callback page)
|
|
26
|
+
const token = await sso.handleCallback(window.location.href);
|
|
27
|
+
|
|
28
|
+
// 3. Get user info
|
|
29
|
+
const user = await sso.getUserInfo();
|
|
30
|
+
|
|
31
|
+
// 4. Logout
|
|
32
|
+
await sso.logout();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## API Reference
|
|
38
|
+
|
|
39
|
+
### `new SsoClient(config)`
|
|
40
|
+
|
|
41
|
+
Create an SSO client instance.
|
|
42
|
+
|
|
43
|
+
| Parameter | Type | Required | Description |
|
|
44
|
+
|------|------|------|------|
|
|
45
|
+
| `clientId` | `string` | ✅ | OAuth Client ID |
|
|
46
|
+
| `redirectUri` | `string` | ✅ | Callback URL, must match the one registered |
|
|
47
|
+
| `ssoBaseUrl` | `string` | ✅ | SSO provider base URL, e.g. `https://nihplod.cn` |
|
|
48
|
+
| `scopes` | `string` | ❌ | Space-separated scopes, default `"openid profile"` |
|
|
49
|
+
| `clientSecret` | `string` | ❌ | **Only for Confidential Clients**. Do NOT pass this in browser SPA (Public Client) to avoid leaking secrets. BFF / Next.js Route Handlers may pass it. |
|
|
50
|
+
|
|
51
|
+
### `sso.login(returnUrl?)`
|
|
52
|
+
|
|
53
|
+
Initiate SSO login. Generates PKCE parameters and redirects to the SSO login page.
|
|
54
|
+
|
|
55
|
+
| Parameter | Type | Description |
|
|
56
|
+
|------|------|------|
|
|
57
|
+
| `returnUrl` | `string` | Optional URL to return to after login |
|
|
58
|
+
|
|
59
|
+
### `sso.getLoginUrl(returnUrl?)`
|
|
60
|
+
|
|
61
|
+
Build the login URL string without redirecting. Returns `Promise<string>`.
|
|
62
|
+
|
|
63
|
+
### `sso.handleCallback(callbackUrl)`
|
|
64
|
+
|
|
65
|
+
Handle the OAuth callback. Parses `code` and `state` from the URL, validates state, and exchanges the code for tokens.
|
|
66
|
+
|
|
67
|
+
| Parameter | Type | Description |
|
|
68
|
+
|------|------|------|
|
|
69
|
+
| `callbackUrl` | `string` | Full callback URL (`window.location.href`) |
|
|
70
|
+
|
|
71
|
+
Returns `Promise<TokenData>`.
|
|
72
|
+
|
|
73
|
+
### `sso.refreshToken()`
|
|
74
|
+
|
|
75
|
+
Refresh the access_token using the refresh_token. Uses a mutex to prevent concurrent refresh requests.
|
|
76
|
+
|
|
77
|
+
Returns `Promise<TokenData>`.
|
|
78
|
+
|
|
79
|
+
### `sso.getUserInfo()`
|
|
80
|
+
|
|
81
|
+
Fetch current user info. Automatically refreshes the access_token if expired.
|
|
82
|
+
|
|
83
|
+
Returns `Promise<SsoUser>`.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface SsoUser {
|
|
87
|
+
sub: string;
|
|
88
|
+
nickname?: string;
|
|
89
|
+
avatar?: string;
|
|
90
|
+
phone?: string; // Masked phone number
|
|
91
|
+
membership_level?: string;
|
|
92
|
+
total_points?: number;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `sso.getAccessToken()`
|
|
97
|
+
|
|
98
|
+
Get the current valid access_token. Automatically refreshes if expired.
|
|
99
|
+
|
|
100
|
+
Returns `Promise<string | null>`.
|
|
101
|
+
|
|
102
|
+
### `sso.isAuthenticated()`
|
|
103
|
+
|
|
104
|
+
Check whether the user is authenticated (local check only, no network request).
|
|
105
|
+
|
|
106
|
+
Returns `boolean`.
|
|
107
|
+
|
|
108
|
+
### `sso.logout(redirectToSso?)`
|
|
109
|
+
|
|
110
|
+
Clear local token data and attempt to revoke the server-side refresh_token.
|
|
111
|
+
|
|
112
|
+
| Parameter | Type | Default | Description |
|
|
113
|
+
|------|------|------|------|
|
|
114
|
+
| `redirectToSso` | `boolean` | `false` | Whether to redirect to the SSO logout page (OIDC RP-Initiated Logout) |
|
|
115
|
+
|
|
116
|
+
When `redirectToSso=true`, the user is redirected to `/logout?client_id=...&post_logout_redirect_uri=...`. The main site clears the session and then returns to the sub-project callback address.
|
|
117
|
+
|
|
118
|
+
### `sso.getDiscovery()`
|
|
119
|
+
|
|
120
|
+
Fetch the OIDC Discovery document.
|
|
121
|
+
|
|
122
|
+
Returns `Promise<OidcDiscovery>`.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## TypeScript Types
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// SsoClient config
|
|
130
|
+
interface SsoClientConfig {
|
|
131
|
+
clientId: string;
|
|
132
|
+
redirectUri: string;
|
|
133
|
+
ssoBaseUrl: string;
|
|
134
|
+
scopes?: string;
|
|
135
|
+
clientSecret?: string; // Only for Confidential Clients
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Token data
|
|
139
|
+
interface TokenData {
|
|
140
|
+
access_token: string;
|
|
141
|
+
token_type: string;
|
|
142
|
+
expires_in: number;
|
|
143
|
+
refresh_token: string;
|
|
144
|
+
id_token?: string;
|
|
145
|
+
issued_at: number;
|
|
146
|
+
expires_at: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// User info
|
|
150
|
+
interface SsoUser {
|
|
151
|
+
sub: string;
|
|
152
|
+
nickname?: string;
|
|
153
|
+
avatar?: string;
|
|
154
|
+
phone?: string;
|
|
155
|
+
membership_level?: string;
|
|
156
|
+
total_points?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// OIDC Discovery
|
|
160
|
+
interface OidcDiscovery {
|
|
161
|
+
issuer: string;
|
|
162
|
+
authorization_endpoint: string;
|
|
163
|
+
token_endpoint: string;
|
|
164
|
+
userinfo_endpoint: string;
|
|
165
|
+
jwks_uri: string;
|
|
166
|
+
introspection_endpoint: string;
|
|
167
|
+
revocation_endpoint?: string;
|
|
168
|
+
end_session_endpoint?: string;
|
|
169
|
+
scopes_supported: string[];
|
|
170
|
+
response_types_supported: string[];
|
|
171
|
+
grant_types_supported: string[];
|
|
172
|
+
code_challenge_methods_supported: string[];
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## React Bindings
|
|
179
|
+
|
|
180
|
+
### `<SsoProvider>`
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
import { SsoProvider } from "@nihplod/sso-sdk/react";
|
|
184
|
+
|
|
185
|
+
<SsoProvider
|
|
186
|
+
config={{
|
|
187
|
+
clientId: "...",
|
|
188
|
+
redirectUri: "...",
|
|
189
|
+
ssoBaseUrl: "...",
|
|
190
|
+
scopes: "openid profile",
|
|
191
|
+
}}
|
|
192
|
+
refreshThreshold={60} // Auto-refresh 60s before expiry
|
|
193
|
+
>
|
|
194
|
+
<App />
|
|
195
|
+
</SsoProvider>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `useSso()`
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { useSso } from "@nihplod/sso-sdk/react";
|
|
202
|
+
|
|
203
|
+
const {
|
|
204
|
+
user, // SsoUser | null
|
|
205
|
+
isAuthenticated, // boolean
|
|
206
|
+
isLoading, // boolean
|
|
207
|
+
login, // (returnUrl?: string) => Promise<void>
|
|
208
|
+
logout, // (redirectToSso?: boolean) => Promise<void>
|
|
209
|
+
refreshUser, // () => Promise<void>
|
|
210
|
+
getAccessToken, // () => Promise<string | null>
|
|
211
|
+
client, // SsoClient instance
|
|
212
|
+
} = useSso();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `<RequireAuth>`
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { RequireAuth } from "@nihplod/sso-sdk/react";
|
|
219
|
+
|
|
220
|
+
<RequireAuth>
|
|
221
|
+
<ProtectedContent />
|
|
222
|
+
</RequireAuth>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `withAuth(Component)`
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import { withAuth } from "@nihplod/sso-sdk/react";
|
|
229
|
+
|
|
230
|
+
function DashboardPage() { return <div>Dashboard</div>; }
|
|
231
|
+
export default withAuth(DashboardPage);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### `<CallbackPage>`
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
import { CallbackPage } from "@nihplod/sso-sdk/react";
|
|
238
|
+
|
|
239
|
+
// Render this component in the callback route
|
|
240
|
+
export default function AuthCallback() {
|
|
241
|
+
return <CallbackPage />;
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Next.js Bindings
|
|
248
|
+
|
|
249
|
+
For Next.js, the recommended approach is **Middleware + Route Handler** BFF pattern. Tokens are stored in `httpOnly` cookies so JavaScript cannot read them, providing the highest security.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// src/middleware.ts
|
|
253
|
+
import { createSsoMiddleware } from "@nihplod/sso-sdk/next";
|
|
254
|
+
|
|
255
|
+
export const middleware = createSsoMiddleware({
|
|
256
|
+
clientId: "...",
|
|
257
|
+
ssoBaseUrl: "https://nihplod.cn",
|
|
258
|
+
redirectUri: "https://yourapp.com/api/auth/callback",
|
|
259
|
+
scopes: "openid profile",
|
|
260
|
+
publicPaths: ["/", "/public"],
|
|
261
|
+
// Confidential Client (BFF) can pass clientSecret
|
|
262
|
+
// clientSecret: process.env.SSO_CLIENT_SECRET,
|
|
263
|
+
// validateSsoCookie defaults to true: the middleware calls the introspection
|
|
264
|
+
// endpoint to verify the SSO session cookie. Set to false only if you accept
|
|
265
|
+
// "cookie exists = logged in" semantics (lowest latency, but may pass revoked
|
|
266
|
+
// sessions). Either way, the middleware is only a UX gate — always re-verify
|
|
267
|
+
// tokens in Route Handlers / Server Components before serving sensitive data.
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
export const config = {
|
|
271
|
+
matcher: ["/((?!_next|favicon.ico).*)"],
|
|
272
|
+
};
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// src/app/api/auth/callback/route.ts
|
|
277
|
+
import { createCallbackRouteHandler } from "@nihplod/sso-sdk/next";
|
|
278
|
+
|
|
279
|
+
export const GET = createCallbackRouteHandler({
|
|
280
|
+
clientId: "...",
|
|
281
|
+
ssoBaseUrl: "https://nihplod.cn",
|
|
282
|
+
redirectUri: "https://yourapp.com/api/auth/callback",
|
|
283
|
+
defaultReturnPath: "/dashboard",
|
|
284
|
+
// Same as middleware; Confidential Client can pass clientSecret
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// src/app/api/auth/logout/route.ts
|
|
290
|
+
import { createLogoutRouteHandler } from "@nihplod/sso-sdk/next";
|
|
291
|
+
|
|
292
|
+
const handler = createLogoutRouteHandler({
|
|
293
|
+
clientId: "...",
|
|
294
|
+
ssoBaseUrl: "https://nihplod.cn",
|
|
295
|
+
redirectUri: "https://yourapp.com/api/auth/callback",
|
|
296
|
+
postLogoutRedirectUri: "https://yourapp.com/",
|
|
297
|
+
redirectToSso: true,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Prefer POST to trigger logout (prevents logout CSRF via cross-site GET);
|
|
301
|
+
// GET is kept for compatibility with plain <a> navigations.
|
|
302
|
+
export const GET = handler;
|
|
303
|
+
export const POST = handler;
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Trigger the logout endpoint with a POST request (recommended):
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
<button onClick={() => fetch("/api/auth/logout", { method: "POST" }).then(() => location.assign("/"))}>
|
|
310
|
+
Logout
|
|
311
|
+
</button>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
A plain `<a href="/api/auth/logout">` still works via GET, but note it can be triggered cross-site (logout CSRF).
|
|
315
|
+
|
|
316
|
+
### Cookie Configuration
|
|
317
|
+
|
|
318
|
+
Default cookie names:
|
|
319
|
+
|
|
320
|
+
| Cookie | Default Name | Description |
|
|
321
|
+
|--------|--------------|-------------|
|
|
322
|
+
| access_token | `__Host-nihplod_sso_at` | Requires Secure + Path=/ + no Domain |
|
|
323
|
+
| refresh_token | `__Host-nihplod_sso_rt` | Requires Secure + Path=/ + no Domain |
|
|
324
|
+
| state | `__Host-nihplod_sso_state` | Requires Secure + Path=/ + no Domain |
|
|
325
|
+
| return_url | `__Host-nihplod_sso_return` | Requires Secure + Path=/ + no Domain |
|
|
326
|
+
| verifier | `__Secure-nihplod_sso_verifier` | Requires Secure + no Domain; Path is the callback path, therefore uses `__Secure-` prefix |
|
|
327
|
+
|
|
328
|
+
> For local development with `http://localhost`, the browser will reject `Secure` cookies. You may disable `secure` locally, but HTTPS is mandatory in production.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Security Recommendations and Token Storage
|
|
333
|
+
|
|
334
|
+
By default, the SDK stores tokens in **memory**. Public Clients (SPA / mobile / desktop) should **never** write the `refresh_token` to `localStorage` to prevent XSS from stealing long-lived credentials.
|
|
335
|
+
|
|
336
|
+
Transient OAuth data (PKCE `code_verifier`, `state`, `returnUrl`, popup nonce) is stored separately in **sessionStorage**, because it must survive the full-page redirect to the SSO center and back; it is cleared automatically when the tab closes. In SSR environments without `sessionStorage`, it falls back to an in-memory map.
|
|
337
|
+
|
|
338
|
+
If the sub-project is a **Next.js BFF / Confidential Client**, you can store tokens in `localStorage` for multi-tab sharing:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { setTokenStorage, createSecureStorage } from "@nihplod/sso-sdk";
|
|
342
|
+
|
|
343
|
+
setTokenStorage(createSecureStorage({ persist: true }));
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
In production, it is more secure to keep the refresh token in a Service Worker or HTTP-only cookie, exposing only the short-lived access token to the frontend.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Utility Functions
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import {
|
|
354
|
+
generateCodeVerifier,
|
|
355
|
+
generateCodeChallenge,
|
|
356
|
+
generateState,
|
|
357
|
+
setTokenStorage,
|
|
358
|
+
createSecureStorage,
|
|
359
|
+
getTokenData,
|
|
360
|
+
saveTokenData,
|
|
361
|
+
removeTokenData,
|
|
362
|
+
clearAllSsoData,
|
|
363
|
+
} from "@nihplod/sso-sdk";
|
|
364
|
+
|
|
365
|
+
// PKCE
|
|
366
|
+
const verifier = generateCodeVerifier(64);
|
|
367
|
+
const challenge = await generateCodeChallenge(verifier);
|
|
368
|
+
|
|
369
|
+
// State
|
|
370
|
+
const state = generateState();
|
|
371
|
+
|
|
372
|
+
// Custom token storage (default is memory storage)
|
|
373
|
+
setTokenStorage(createSecureStorage({ persist: false }));
|
|
374
|
+
```
|