or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-security.mdauthorization-flows.mdclient-authentication.mdconfiguration.mdgrant-types.mdindex.mdpassport-integration.mdprotected-resources.mdtoken-management.md

protected-resources.mddocs/

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

```