0
# Protected Resource Access
1
2
Access protected resources and UserInfo endpoint with proper access token handling, including DPoP support for sender-constrained tokens.
3
4
## Capabilities
5
6
### Fetch UserInfo
7
8
Retrieve user information from the OpenID Connect UserInfo endpoint.
9
10
```typescript { .api }
11
/**
12
* Fetch UserInfo from authorization server
13
* @param config - Configuration instance
14
* @param accessToken - Access token for authorization
15
* @param expectedSubject - Expected subject claim or skipSubjectCheck
16
* @param options - Request options (DPoP, etc.)
17
* @returns Promise resolving to UserInfo response
18
*/
19
function fetchUserInfo(
20
config: Configuration,
21
accessToken: string,
22
expectedSubject: string | typeof skipSubjectCheck,
23
options?: DPoPOptions
24
): Promise<UserInfoResponse>;
25
```
26
27
**Usage Examples:**
28
29
```typescript
30
import * as client from "openid-client";
31
32
// Basic UserInfo request
33
const tokens = await client.authorizationCodeGrant(config, currentUrl, checks);
34
const userInfo = await client.fetchUserInfo(
35
config,
36
tokens.access_token,
37
tokens.claims()?.sub // expected subject from ID Token
38
);
39
40
console.log("User Info:", userInfo);
41
console.log("Email:", userInfo.email);
42
console.log("Name:", userInfo.name);
43
44
// Skip subject validation (not recommended)
45
const userInfo = await client.fetchUserInfo(
46
config,
47
tokens.access_token,
48
client.skipSubjectCheck
49
);
50
51
// With DPoP for sender-constrained tokens
52
const dpopKeyPair = await client.randomDPoPKeyPair();
53
const dpopHandle = client.getDPoPHandle(config, dpopKeyPair);
54
55
const tokens = await client.authorizationCodeGrant(
56
config,
57
currentUrl,
58
checks,
59
undefined,
60
{ DPoP: dpopHandle }
61
);
62
63
// Use same DPoP handle for UserInfo request
64
const userInfo = await client.fetchUserInfo(
65
config,
66
tokens.access_token,
67
tokens.claims()?.sub,
68
{ DPoP: dpopHandle }
69
);
70
```
71
72
### Fetch Protected Resource
73
74
Make authenticated requests to any protected resource endpoint.
75
76
```typescript { .api }
77
/**
78
* Make authenticated request to protected resource
79
* @param config - Configuration instance
80
* @param accessToken - Access token for authorization
81
* @param url - Target URL for the request
82
* @param method - HTTP method
83
* @param body - Request body (optional)
84
* @param headers - Additional headers (optional)
85
* @param options - Request options (DPoP, etc.)
86
* @returns Promise resolving to Response object
87
*/
88
function fetchProtectedResource(
89
config: Configuration,
90
accessToken: string,
91
url: URL,
92
method: string,
93
body?: FetchBody,
94
headers?: Headers,
95
options?: DPoPOptions
96
): Promise<Response>;
97
```
98
99
**Usage Examples:**
100
101
```typescript
102
import * as client from "openid-client";
103
104
// GET request to API endpoint
105
const response = await client.fetchProtectedResource(
106
config,
107
accessToken,
108
new URL("https://api.example.com/users/me"),
109
"GET"
110
);
111
112
if (response.ok) {
113
const userData = await response.json();
114
console.log("User data:", userData);
115
} else {
116
console.log("Request failed:", response.status, response.statusText);
117
}
118
119
// POST request with JSON body
120
const postResponse = await client.fetchProtectedResource(
121
config,
122
accessToken,
123
new URL("https://api.example.com/users"),
124
"POST",
125
JSON.stringify({
126
name: "John Doe",
127
email: "john@example.com"
128
}),
129
new Headers({
130
"Content-Type": "application/json"
131
})
132
);
133
134
// PUT request with form data
135
const formData = new URLSearchParams();
136
formData.append("name", "Jane Doe");
137
formData.append("department", "Engineering");
138
139
const putResponse = await client.fetchProtectedResource(
140
config,
141
accessToken,
142
new URL("https://api.example.com/users/123"),
143
"PUT",
144
formData,
145
new Headers({
146
"Content-Type": "application/x-www-form-urlencoded"
147
})
148
);
149
150
// DELETE request
151
const deleteResponse = await client.fetchProtectedResource(
152
config,
153
accessToken,
154
new URL("https://api.example.com/users/123"),
155
"DELETE"
156
);
157
158
// With DPoP for sender-constrained access tokens
159
const response = await client.fetchProtectedResource(
160
config,
161
dpopConstrainedToken,
162
new URL("https://api.example.com/sensitive-data"),
163
"GET",
164
undefined,
165
undefined,
166
{ DPoP: dpopHandle }
167
);
168
```
169
170
## Advanced Usage Patterns
171
172
### API Client Wrapper
173
174
Create a reusable API client with automatic token handling:
175
176
```typescript
177
import * as client from "openid-client";
178
179
class APIClient {
180
constructor(
181
private config: client.Configuration,
182
private tokenManager: TokenManager
183
) {}
184
185
async get<T = any>(endpoint: string): Promise<T> {
186
const token = await this.tokenManager.getValidAccessToken();
187
const response = await client.fetchProtectedResource(
188
this.config,
189
token,
190
new URL(endpoint),
191
"GET"
192
);
193
194
if (!response.ok) {
195
throw new Error(`API request failed: ${response.status}`);
196
}
197
198
return response.json();
199
}
200
201
async post<T = any>(endpoint: string, data: any): Promise<T> {
202
const token = await this.tokenManager.getValidAccessToken();
203
const response = await client.fetchProtectedResource(
204
this.config,
205
token,
206
new URL(endpoint),
207
"POST",
208
JSON.stringify(data),
209
new Headers({ "Content-Type": "application/json" })
210
);
211
212
if (!response.ok) {
213
throw new Error(`API request failed: ${response.status}`);
214
}
215
216
return response.json();
217
}
218
219
async put<T = any>(endpoint: string, data: any): Promise<T> {
220
const token = await this.tokenManager.getValidAccessToken();
221
const response = await client.fetchProtectedResource(
222
this.config,
223
token,
224
new URL(endpoint),
225
"PUT",
226
JSON.stringify(data),
227
new Headers({ "Content-Type": "application/json" })
228
);
229
230
if (!response.ok) {
231
throw new Error(`API request failed: ${response.status}`);
232
}
233
234
return response.json();
235
}
236
237
async delete(endpoint: string): Promise<void> {
238
const token = await this.tokenManager.getValidAccessToken();
239
const response = await client.fetchProtectedResource(
240
this.config,
241
token,
242
new URL(endpoint),
243
"DELETE"
244
);
245
246
if (!response.ok) {
247
throw new Error(`API request failed: ${response.status}`);
248
}
249
}
250
}
251
252
// Usage
253
const apiClient = new APIClient(config, tokenManager);
254
255
const user = await apiClient.get<UserProfile>("https://api.example.com/user");
256
const newPost = await apiClient.post<Post>("https://api.example.com/posts", {
257
title: "Hello World",
258
content: "This is my first post"
259
});
260
```
261
262
### Error Handling and Retry Logic
263
264
Handle common API errors and implement retry logic:
265
266
```typescript
267
import * as client from "openid-client";
268
269
class ProtectedResourceClient {
270
constructor(
271
private config: client.Configuration,
272
private tokenManager: TokenManager
273
) {}
274
275
async request(
276
url: URL,
277
method: string,
278
body?: client.FetchBody,
279
headers?: Headers,
280
retries = 1
281
): Promise<Response> {
282
for (let attempt = 0; attempt <= retries; attempt++) {
283
try {
284
const token = await this.tokenManager.getValidAccessToken();
285
const response = await client.fetchProtectedResource(
286
this.config,
287
token,
288
url,
289
method,
290
body,
291
headers
292
);
293
294
// Handle token expiration
295
if (response.status === 401) {
296
const wwwAuth = response.headers.get("WWW-Authenticate");
297
if (wwwAuth?.includes("invalid_token") && attempt < retries) {
298
// Try refreshing token and retry
299
await this.tokenManager.refreshTokens();
300
continue;
301
}
302
}
303
304
// Handle rate limiting
305
if (response.status === 429 && attempt < retries) {
306
const retryAfter = response.headers.get("Retry-After");
307
const delay = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
308
await new Promise(resolve => setTimeout(resolve, delay));
309
continue;
310
}
311
312
return response;
313
} catch (error) {
314
if (attempt === retries) {
315
throw error;
316
}
317
318
// Exponential backoff for network errors
319
const delay = Math.pow(2, attempt) * 1000;
320
await new Promise(resolve => setTimeout(resolve, delay));
321
}
322
}
323
324
throw new Error("Max retries exceeded");
325
}
326
}
327
```
328
329
### File Upload with Protected Resources
330
331
Handle file uploads to protected endpoints:
332
333
```typescript
334
import * as client from "openid-client";
335
336
async function uploadFile(
337
config: client.Configuration,
338
accessToken: string,
339
file: File,
340
uploadUrl: string
341
): Promise<any> {
342
const formData = new FormData();
343
formData.append("file", file);
344
formData.append("filename", file.name);
345
346
const response = await client.fetchProtectedResource(
347
config,
348
accessToken,
349
new URL(uploadUrl),
350
"POST",
351
formData
352
// Note: Don't set Content-Type header for FormData - browser will set it with boundary
353
);
354
355
if (!response.ok) {
356
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
357
}
358
359
return response.json();
360
}
361
362
// Usage
363
const fileInput = document.getElementById("file") as HTMLInputElement;
364
const file = fileInput.files?.[0];
365
366
if (file) {
367
try {
368
const result = await uploadFile(
369
config,
370
accessToken,
371
file,
372
"https://api.example.com/upload"
373
);
374
console.log("Upload successful:", result);
375
} catch (error) {
376
console.error("Upload failed:", error);
377
}
378
}
379
```
380
381
## Response Types
382
383
```typescript { .api }
384
interface UserInfoResponse {
385
/** Subject identifier */
386
sub: string;
387
388
/** End-user's full name */
389
name?: string;
390
391
/** Given name(s) or first name(s) */
392
given_name?: string;
393
394
/** Surname(s) or last name(s) */
395
family_name?: string;
396
397
/** Middle name(s) */
398
middle_name?: string;
399
400
/** Casual name */
401
nickname?: string;
402
403
/** Shorthand name */
404
preferred_username?: string;
405
406
/** Profile page URL */
407
profile?: string;
408
409
/** Profile picture URL */
410
picture?: string;
411
412
/** Web page or blog URL */
413
website?: string;
414
415
/** Preferred e-mail address */
416
email?: string;
417
418
/** True if email has been verified */
419
email_verified?: boolean;
420
421
/** Gender */
422
gender?: string;
423
424
/** Birthdate (YYYY-MM-DD format) */
425
birthdate?: string;
426
427
/** Time zone */
428
zoneinfo?: string;
429
430
/** Locale */
431
locale?: string;
432
433
/** Preferred telephone number */
434
phone_number?: string;
435
436
/** True if phone number has been verified */
437
phone_number_verified?: boolean;
438
439
/** Preferred postal address */
440
address?: UserInfoAddress;
441
442
/** Time the information was last updated */
443
updated_at?: number;
444
445
// Additional claims may be present
446
[key: string]: any;
447
}
448
449
interface UserInfoAddress {
450
/** Full mailing address */
451
formatted?: string;
452
453
/** Full street address */
454
street_address?: string;
455
456
/** City or locality */
457
locality?: string;
458
459
/** State, province, prefecture, or region */
460
region?: string;
461
462
/** Zip code or postal code */
463
postal_code?: string;
464
465
/** Country name */
466
country?: string;
467
}
468
469
type FetchBody = ArrayBuffer | null | ReadableStream | string | Uint8Array | undefined | URLSearchParams;
470
```
471
472
## Security Override Symbols
473
474
```typescript { .api }
475
/**
476
* Skip subject validation in UserInfo requests
477
* WARNING: Use only when you understand the security implications
478
*/
479
declare const skipSubjectCheck: unique symbol;
480
```
481
482
## DPoP Integration
483
484
For sender-constrained access tokens:
485
486
```typescript { .api }
487
interface DPoPOptions {
488
/** DPoP handle for proof-of-possession */
489
DPoP?: DPoPHandle;
490
}
491
```
492
493
**Complete DPoP Example:**
494
495
```typescript
496
import * as client from "openid-client";
497
498
// Create DPoP key pair and handle
499
const dpopKeyPair = await client.randomDPoPKeyPair();
500
const dpopHandle = client.getDPoPHandle(config, dpopKeyPair);
501
502
// Get sender-constrained tokens
503
const tokens = await client.authorizationCodeGrant(
504
config,
505
currentUrl,
506
checks,
507
undefined,
508
{ DPoP: dpopHandle }
509
);
510
511
// Verify token is sender-constrained
512
if (tokens.token_type === "dpop") {
513
console.log("Access token is sender-constrained with DPoP");
514
}
515
516
// Use with UserInfo
517
const userInfo = await client.fetchUserInfo(
518
config,
519
tokens.access_token,
520
tokens.claims()?.sub,
521
{ DPoP: dpopHandle }
522
);
523
524
// Use with protected resources
525
const apiResponse = await client.fetchProtectedResource(
526
config,
527
tokens.access_token,
528
new URL("https://api.example.com/data"),
529
"GET",
530
undefined,
531
undefined,
532
{ DPoP: dpopHandle }
533
);
534
```