@basictech/react 0.7.0-beta.2 → 0.7.0-beta.4

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.
@@ -0,0 +1,2009 @@
1
+ # Authentication Implementation Guide
2
+
3
+ This guide provides comprehensive REST API specifications and requirements for implementing authentication with Basic PDS Server.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [PDS Authentication](#pds-authentication)
10
+ - [Account Registration](#account-registration)
11
+ - [Account Login](#account-login)
12
+ - [OAuth Authorization](#oauth-authorization)
13
+ - [Session Management](#session-management)
14
+ - [Password Management](#password-management)
15
+ - [Email Verification](#email-verification)
16
+ - [Username Availability](#username-availability)
17
+ - [DID Resolution](#did-resolution)
18
+ 2. [OAuth2 Implementation](#oauth2-implementation)
19
+ - [Discovery & Configuration](#discovery--configuration)
20
+ - [Scopes](#scopes)
21
+ - [Authorization Code Flow](#authorization-code-flow)
22
+ - [Token Management](#token-management)
23
+ - [User Information](#user-information)
24
+ 3. [Scopes & Authorization](#scopes--authorization)
25
+ - [Scope System](#scope-system)
26
+ - [Available Scopes Reference](#available-scopes-reference)
27
+ - [Action Implications](#action-implications)
28
+ - [Admin Scope Behavior](#admin-scope-behavior)
29
+ - [Authorization Errors](#authorization-errors)
30
+ - [Best Practices](#best-practices)
31
+ 4. [PKCE Extension](#pkce-extension-optional)
32
+ 5. [DPoP Extension](#dpop-extension-optional)
33
+ 6. [Security Requirements](#security-requirements)
34
+
35
+ ---
36
+
37
+ ## PDS Authentication
38
+
39
+ PDS authentication provides username/password-based authentication with email verification, password reset, and decentralized identifiers (DIDs). This is meant for clients of the PDS server, usually a front-end.
40
+
41
+ ---
42
+
43
+ ### Account Registration
44
+
45
+ **Endpoint:** `POST /auth/signup`
46
+
47
+ Create a new user account with username and password.
48
+
49
+ #### Request
50
+
51
+ ```http
52
+ POST /auth/signup HTTP/1.1
53
+ Content-Type: application/json
54
+
55
+ {
56
+ "type": "password",
57
+ "username": "john_doe",
58
+ "password": "securepassword123",
59
+ "email": "john@example.com",
60
+ "name": "John Doe"
61
+ }
62
+ ```
63
+
64
+ #### Request Body Parameters
65
+
66
+ | Parameter | Type | Required | Description |
67
+ |-----------|------|----------|-------------|
68
+ | `type` | string | Yes | Authentication type. Must be `"password"` |
69
+ | `username` | string | Yes | Unique username (alphanumeric, underscores) |
70
+ | `password` | string | Yes | User password (minimum 3 characters) |
71
+ | `email` | string | No | User email address (for verification) |
72
+ | `name` | string | No | User's display name |
73
+
74
+ #### Response
75
+
76
+ **Success (200):**
77
+ ```json
78
+ {
79
+ "data": {
80
+ "id": "acc_12345",
81
+ "username": "john_doe",
82
+ "email": "john@example.com",
83
+ "name": "John Doe",
84
+ "created_at": "2025-09-29T10:00:00Z"
85
+ }
86
+ }
87
+ ```
88
+
89
+ **Error (400):**
90
+ ```json
91
+ {
92
+ "error": "Email already verified",
93
+ "message": "This email address is already verified with another account"
94
+ }
95
+ ```
96
+
97
+ #### Email Verification
98
+
99
+ If an email is provided during signup, a verification email is automatically sent containing a verification link:
100
+ ```
101
+ https://your-app.com/login?verify_email_token={token}&email={email}
102
+ ```
103
+
104
+ The verification token is valid for **24 hours**.
105
+
106
+ ---
107
+
108
+ ### Account Login
109
+
110
+ **Endpoint:** `POST /auth/login`
111
+
112
+ Authenticate with username and password to receive access and refresh tokens.
113
+
114
+ #### Request
115
+
116
+ ```http
117
+ POST /auth/login HTTP/1.1
118
+ Content-Type: application/json
119
+
120
+ {
121
+ "username": "john_doe",
122
+ "password": "securepassword123"
123
+ }
124
+ ```
125
+
126
+ #### Request Body Parameters
127
+
128
+ | Parameter | Type | Required | Description |
129
+ |-----------|------|----------|-------------|
130
+ | `username` | string | Yes | User's username or email |
131
+ | `password` | string | Yes | User's password |
132
+
133
+ #### Response
134
+
135
+ **Success (200):**
136
+ ```json
137
+ {
138
+ "auth": {
139
+ "account_id": "acc_12345",
140
+ "ok": true
141
+ },
142
+ "token": {
143
+ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
144
+ "token_type": "Bearer",
145
+ "expires_in": 60,
146
+ "refresh_token": "3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef123456..."
147
+ }
148
+ }
149
+ ```
150
+
151
+ **Error (401):**
152
+ ```json
153
+ {
154
+ "error": "Unauthorized - Invalid credentials"
155
+ }
156
+ ```
157
+
158
+ #### Token Details
159
+
160
+ - **Access Token**: JWT signed with RS256, valid for 1 minute
161
+ - **Refresh Token**: Cryptographically random opaque token stored in database, valid for 30 days
162
+ - **Token Type**: Always `"Bearer"`
163
+
164
+ The access token payload contains:
165
+ ```json
166
+ {
167
+ "userId": "acc_12345",
168
+ "clientId": "self",
169
+ "scope": "admin",
170
+ "sid": "sess_abc123",
171
+ "iat": 1727606400,
172
+ "exp": 1727606460
173
+ }
174
+ ```
175
+
176
+ **Token Claims:**
177
+ - `userId` - Account ID
178
+ - `clientId` - Always `"self"` for login
179
+ - `scope` - Always `"admin"` for login (grants full account access)
180
+ - `sid` - Session ID
181
+ - `iat` - Issued at timestamp
182
+ - `exp` - Expiration timestamp
183
+
184
+ **The `admin` scope:**
185
+ - Grants access to all account features
186
+ - Can view/manage all sessions
187
+ - Can access data from all connected apps
188
+ - Only available via login (not OAuth apps)
189
+
190
+ ---
191
+
192
+ ### OAuth Authorization
193
+
194
+ **Endpoint:** `POST /auth/authorize`
195
+
196
+ After a user logs in with PDS authentication, they can authorize OAuth applications to access their account.
197
+
198
+ #### Request
199
+
200
+ ```http
201
+ POST /auth/authorize HTTP/1.1
202
+ Host: auth.example.com
203
+ Authorization: Bearer {user_access_token}
204
+ Content-Type: application/json
205
+
206
+ {
207
+ "client_id": "app_123",
208
+ "redirect_uri": "https://app.example.com/callback",
209
+ "scope": "profile email",
210
+ "state": "xyz123",
211
+ "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
212
+ "code_challenge_method": "S256"
213
+ }
214
+ ```
215
+
216
+ #### Request Headers
217
+
218
+ | Header | Required | Description |
219
+ |--------|----------|-------------|
220
+ | `Authorization` | Yes | Bearer token with `admin` scope (from login) |
221
+
222
+ **Note:** Only users logged in via `/auth/login` can authorize apps (have admin scope)
223
+
224
+ #### Request Body Parameters
225
+
226
+ | Parameter | Type | Required | Description |
227
+ |-----------|------|----------|-------------|
228
+ | `client_id` | string | Yes | Application identifier |
229
+ | `redirect_uri` | string | Yes | Callback URI |
230
+ | `scope` | string | Yes | Requested scopes |
231
+ | `state` | string | No | State parameter from initial request |
232
+ | `code_challenge` | string | No | PKCE code challenge |
233
+ | `code_challenge_method` | string | No | Must be `"S256"` if challenge provided |
234
+
235
+ #### Response
236
+
237
+ **Success (200):**
238
+ ```json
239
+ {
240
+ "code": "auth_abc123def456",
241
+ "redirect": "https://app.example.com/callback?code=auth_abc123def456&state=xyz123",
242
+ "connect": {
243
+ "id": "conn_789",
244
+ "account_id": "acc_12345",
245
+ "project_id": "app_123",
246
+ "status": "connected",
247
+ "scope": "profile email"
248
+ },
249
+ "pkce_supported": true
250
+ }
251
+ ```
252
+
253
+ **Error (401):**
254
+ ```json
255
+ {
256
+ "error": "access_denied",
257
+ "error_description": "User authentication failed"
258
+ }
259
+ ```
260
+
261
+ #### Authorization Code Properties
262
+
263
+ - **Format**: Opaque string (e.g., `abc123def456`)
264
+ - **Validity**: Single use only
265
+ - **Expiration**: Short-lived (typically 10 minutes)
266
+ - **Storage**: Includes redirect_uri, scope, PKCE challenge if provided
267
+
268
+ ---
269
+
270
+ ### Session Management
271
+
272
+ Sessions track authenticated devices and provide security features like device management and refresh token family tracking.
273
+
274
+ #### Session Creation
275
+
276
+ Sessions are automatically created during:
277
+ 1. **PDS Login** (`POST /auth/login`)
278
+ 2. **OAuth2 Token Exchange** (`POST /auth/token` with authorization code)
279
+
280
+ Each session includes:
281
+ - **Session ID**: Unique identifier
282
+ - **Device Instance ID**: Unique device identifier
283
+ - **User Agent**: Browser/application information
284
+ - **Platform**: Device platform (Web, Mobile, etc.)
285
+ - **IP Address**: Connection IP
286
+ - **Last Seen**: Timestamp of last activity
287
+
288
+ #### Session Properties
289
+
290
+ ```json
291
+ {
292
+ "id": "sess_abc123",
293
+ "account_connection_id": "conn_789",
294
+ "account_id": "acc_12345",
295
+ "project_id": "app_123",
296
+ "device_instance_id": "device_1727606400_abc",
297
+ "label": "OAuth Client Session",
298
+ "user_agent": "Mozilla/5.0...",
299
+ "platform": "Web",
300
+ "ip_inet": "192.168.1.1",
301
+ "last_seen_at": "2025-09-29T10:30:00Z",
302
+ "created_at": "2025-09-29T10:00:00Z"
303
+ }
304
+ ```
305
+
306
+ #### Refresh Token Chain
307
+
308
+ Each session maintains a **refresh token family**:
309
+ - All refresh tokens in a session belong to the same family
310
+ - Token rotation creates new tokens in the same family
311
+ - Reuse detection revokes the entire family
312
+ - Session termination revokes all family tokens
313
+
314
+ ---
315
+
316
+ ### Password Management
317
+
318
+ #### Request Password Reset
319
+
320
+ **Endpoint:** `POST /auth/forgot-password`
321
+
322
+ Request a password reset link to be sent via email.
323
+
324
+ #### Request
325
+
326
+ ```http
327
+ POST /auth/forgot-password HTTP/1.1
328
+ Content-Type: application/json
329
+
330
+ {
331
+ "identifier": "john@example.com"
332
+ }
333
+ ```
334
+
335
+ #### Request Body Parameters
336
+
337
+ | Parameter | Type | Required | Description |
338
+ |-----------|------|----------|-------------|
339
+ | `identifier` | string | Yes | User's email or username |
340
+
341
+ #### Response
342
+
343
+ **Success (200):**
344
+ ```json
345
+ {
346
+ "success": true,
347
+ "message": "If an account with that email/username exists, a password reset link has been sent"
348
+ }
349
+ ```
350
+
351
+ **Note:** For security, the response is always the same whether the account exists or not.
352
+
353
+ #### Reset Token
354
+
355
+ The reset link sent via email has the format:
356
+ ```
357
+ https://your-app.com/login?reset_token={token}&email={email}
358
+ ```
359
+
360
+ The reset token is valid for **15 minutes**.
361
+
362
+ ---
363
+
364
+ #### Reset Password with Token
365
+
366
+ **Endpoint:** `POST /auth/reset-password`
367
+
368
+ Reset the password using a valid reset token.
369
+
370
+ #### Request
371
+
372
+ ```http
373
+ POST /auth/reset-password HTTP/1.1
374
+ Content-Type: application/json
375
+
376
+ {
377
+ "reset_token": "abc123def456...",
378
+ "password": "newSecurePassword456"
379
+ }
380
+ ```
381
+
382
+ #### Request Body Parameters
383
+
384
+ | Parameter | Type | Required | Description |
385
+ |-----------|------|----------|-------------|
386
+ | `reset_token` | string | Yes | Valid reset token from email |
387
+ | `password` | string | Yes | New password (minimum 3 characters) |
388
+
389
+ #### Response
390
+
391
+ **Success (200):**
392
+ ```json
393
+ {
394
+ "success": true,
395
+ "message": "Password has been successfully reset"
396
+ }
397
+ ```
398
+
399
+ **Error (400):**
400
+ ```json
401
+ {
402
+ "error": "Invalid or expired reset token"
403
+ }
404
+ ```
405
+
406
+ ---
407
+
408
+ #### Verify Reset Token
409
+
410
+ **Endpoint:** `POST /auth/verify-reset-token`
411
+
412
+ Check if a password reset token is valid before showing the reset form.
413
+
414
+ #### Request
415
+
416
+ ```http
417
+ POST /auth/verify-reset-token HTTP/1.1
418
+ Content-Type: application/json
419
+
420
+ {
421
+ "reset_token": "abc123def456..."
422
+ }
423
+ ```
424
+
425
+ #### Response
426
+
427
+ ```json
428
+ {
429
+ "valid": true
430
+ }
431
+ ```
432
+
433
+ ---
434
+
435
+ ### Email Verification
436
+
437
+ #### Send Verification Email
438
+
439
+ **Endpoint:** `POST /auth/verify-email`
440
+
441
+ Request a verification email to be sent.
442
+
443
+ #### Request
444
+
445
+ ```http
446
+ POST /auth/verify-email HTTP/1.1
447
+ Content-Type: application/json
448
+
449
+ {
450
+ "email": "john@example.com"
451
+ }
452
+ ```
453
+
454
+ #### Response
455
+
456
+ **Success (200):**
457
+ ```json
458
+ {
459
+ "success": true,
460
+ "message": "If an account with that email exists, a verification link has been sent",
461
+ "verification_link": "https://your-app.com/login?verify_email_token=..."
462
+ }
463
+ ```
464
+
465
+ **Already Verified (200):**
466
+ ```json
467
+ {
468
+ "success": true
469
+ }
470
+ ```
471
+
472
+ ---
473
+
474
+ #### Verify Email with Token
475
+
476
+ **Endpoint:** `POST /auth/verify-email-token`
477
+
478
+ Complete email verification using the token from the verification link.
479
+
480
+ #### Request
481
+
482
+ ```http
483
+ POST /auth/verify-email-token HTTP/1.1
484
+ Content-Type: application/json
485
+
486
+ {
487
+ "token": "abc123def456..."
488
+ }
489
+ ```
490
+
491
+ #### Response
492
+
493
+ **Success (200):**
494
+ ```json
495
+ {
496
+ "success": true,
497
+ "message": "Email has been successfully verified"
498
+ }
499
+ ```
500
+
501
+ **Error (400):**
502
+ ```json
503
+ {
504
+ "error": "Invalid or expired verification token"
505
+ }
506
+ ```
507
+
508
+ ---
509
+
510
+ ### Username Availability
511
+
512
+ **Endpoint:** `GET /auth/check-username`
513
+
514
+ Check if a username is available for registration.
515
+
516
+ #### Request
517
+
518
+ ```http
519
+ GET /auth/check-username?username=john_doe HTTP/1.1
520
+ ```
521
+
522
+ #### Query Parameters
523
+
524
+ | Parameter | Type | Required | Description |
525
+ |-----------|------|----------|-------------|
526
+ | `username` | string | Yes | Username to check |
527
+
528
+ #### Response
529
+
530
+ ```json
531
+ {
532
+ "available": true
533
+ }
534
+ ```
535
+
536
+ ---
537
+
538
+ ### DID Resolution
539
+
540
+ **Endpoint:** `GET /auth/resolve`
541
+
542
+ Resolve a decentralized identifier (DID) to its document. Further DID support coming soon.
543
+
544
+ #### Request
545
+
546
+ ```http
547
+ GET /auth/resolve?did=did:pds:base64url_encoded_data HTTP/1.1
548
+ ```
549
+
550
+ #### Query Parameters
551
+
552
+ | Parameter | Type | Required | Description |
553
+ |-----------|------|----------|-------------|
554
+ | `did` | string | Yes | DID to resolve (format: `did:pds:{base64url}`) |
555
+
556
+ #### Response
557
+
558
+ ```json
559
+ {
560
+ "did": "decoded_did_data"
561
+ }
562
+ ```
563
+
564
+ #### DID Format
565
+
566
+ DIDs follow the format: `did:pds:{base64url_encoded_data}`
567
+
568
+ - Must start with `did:`
569
+ - Method must be `pds`
570
+ - Body is base64url encoded data
571
+
572
+ ---
573
+
574
+ ## OAuth2 Implementation
575
+
576
+ OAuth2 endpoints are meant for client applications who want to connect to a user's PDS.
577
+ OAuth2 provides secure delegated access using the **Authorization Code Flow** with optional PKCE support. This implementation follows RFC 6749 (OAuth 2.0) and RFC 6750 (Bearer Token).
578
+
579
+ ### Discovery & Configuration
580
+
581
+ #### OpenID Connect Discovery
582
+
583
+ **Endpoint:** `GET /.well-known/openid-configuration`
584
+
585
+ Provides metadata about the OAuth2/OIDC server capabilities.
586
+
587
+ #### Request
588
+
589
+ ```http
590
+ GET /.well-known/openid-configuration HTTP/1.1
591
+ Host: auth.example.com
592
+ ```
593
+
594
+ #### Response
595
+
596
+ ```json
597
+ {
598
+ "issuer": "https://auth.example.com",
599
+ "authorization_endpoint": "https://auth.example.com/auth/authorize",
600
+ "token_endpoint": "https://auth.example.com/auth/token",
601
+ "userinfo_endpoint": "https://auth.example.com/auth/userinfo",
602
+ "jwks_uri": "https://auth.example.com/auth/.well-known/jwks.json",
603
+ "response_types_supported": ["code"],
604
+ "grant_types_supported": ["authorization_code", "refresh_token"],
605
+ "subject_types_supported": ["public"],
606
+ "id_token_signing_alg_values_supported": ["RS256"],
607
+ "scopes_supported": [
608
+ "openid:read",
609
+ "profile:read",
610
+ "email:read",
611
+ "account:read",
612
+ "account:write",
613
+ "account:update",
614
+ "account:app:read",
615
+ "account:app:write",
616
+ "account:app:delete",
617
+ "account:session:read",
618
+ "account:session:write",
619
+ "account:session:update",
620
+ "account:session:delete",
621
+ "app:db:read",
622
+ "app:db:write",
623
+ "app:db:update",
624
+ "app:db:delete",
625
+ "admin"
626
+ ],
627
+ "token_endpoint_auth_methods_supported": [
628
+ "client_secret_post",
629
+ "client_secret_basic",
630
+ "none"
631
+ ],
632
+ "code_challenge_methods_supported": ["S256"],
633
+ "claims_supported": [
634
+ "sub",
635
+ "iss",
636
+ "aud",
637
+ "exp",
638
+ "iat",
639
+ "email",
640
+ "email_verified",
641
+ "name",
642
+ "given_name",
643
+ "family_name",
644
+ "picture"
645
+ ]
646
+ }
647
+ ```
648
+
649
+ ---
650
+
651
+ #### JWKS Endpoint
652
+
653
+ **Endpoint:** `GET /.well-known/jwks.json`
654
+
655
+ Provides the public keys for JWT verification in JSON Web Key Set (JWKS) format.
656
+
657
+ #### Request
658
+
659
+ ```http
660
+ GET /.well-known/jwks.json HTTP/1.1
661
+ Host: auth.example.com
662
+ ```
663
+
664
+ #### Response
665
+
666
+ ```json
667
+ {
668
+ "keys": [
669
+ {
670
+ "kty": "RSA",
671
+ "use": "sig",
672
+ "kid": "1",
673
+ "n": "xGOr-H7A...",
674
+ "e": "AQAB",
675
+ "alg": "RS256"
676
+ }
677
+ ]
678
+ }
679
+ ```
680
+
681
+ **Response Headers:**
682
+ ```
683
+ Content-Type: application/json
684
+ Cache-Control: public, max-age=3600
685
+ Access-Control-Allow-Origin: *
686
+ ```
687
+
688
+ ---
689
+
690
+ ### Authorization Code Flow
691
+
692
+ The authorization code flow is a three-step process:
693
+ 1. Client redirects user to authorization endpoint
694
+ 2. User authenticates and authorizes the application
695
+ 3. Client exchanges authorization code for access token
696
+
697
+ #### Step 1: Authorization Request
698
+
699
+ **Endpoint:** `GET /auth/authorize`
700
+
701
+ Initiate the OAuth2 authorization flow.
702
+
703
+ #### Request
704
+
705
+ ```http
706
+ GET /auth/authorize?client_id=app_123&redirect_uri=https://app.example.com/callback&response_type=code&scope=profile%20email&state=xyz123 HTTP/1.1
707
+ Host: auth.example.com
708
+ ```
709
+
710
+ #### Query Parameters
711
+
712
+ | Parameter | Type | Required | Description |
713
+ |-----------|------|----------|-------------|
714
+ | `client_id` | string | Yes | Application/project identifier |
715
+ | `redirect_uri` | string | Yes | URI to redirect after authorization |
716
+ | `response_type` | string | Yes | Must be `"code"` |
717
+ | `scope` | string | No | Space-separated list of scopes (default: `"profile"`) |
718
+ | `state` | string | Yes | CSRF protection token (opaque value) |
719
+ | `code_challenge` | string | No | PKCE code challenge (see PKCE section) |
720
+ | `code_challenge_method` | string | No | Must be `"S256"` if PKCE is used |
721
+
722
+ #### Redirect URI Validation
723
+
724
+ The `redirect_uri` must:
725
+ - Be a valid URL format
726
+ - Match the registered redirect URI in the project profile (exact match)
727
+ - If no redirect URI is registered, fallback rules apply:
728
+ - Localhost URIs (`localhost`, `127.0.0.1`) are allowed
729
+ - URIs matching the project's website hostname are allowed
730
+
731
+ #### Response
732
+
733
+ The server redirects the user to the authorization UI:
734
+
735
+ ```http
736
+ HTTP/1.1 302 Found
737
+ Location: https://basic.id/authorize?client_id=app_123&redirect_uri=https://app.example.com/callback&response_type=code&scope=profile%20email&state=xyz123
738
+ ```
739
+
740
+ #### Error Handling
741
+
742
+ If validation fails, the server responds based on the error:
743
+
744
+ **Invalid redirect_uri (400):**
745
+ ```json
746
+ {
747
+ "error": "invalid_request",
748
+ "error_description": "Invalid redirect_uri: does not match registered value"
749
+ }
750
+ ```
751
+
752
+ **For other errors, redirect to client:**
753
+ ```http
754
+ HTTP/1.1 302 Found
755
+ Location: https://app.example.com/callback?error=invalid_request&error_description=Missing+client_id&state=xyz123
756
+ ```
757
+
758
+ ---
759
+
760
+ #### Step 2: Token Exchange
761
+
762
+ **Endpoint:** `POST /auth/token`
763
+
764
+ Exchange authorization code for access and refresh tokens.
765
+
766
+ #### Request
767
+
768
+ ```http
769
+ POST /auth/token HTTP/1.1
770
+ Host: auth.example.com
771
+ Content-Type: application/json
772
+
773
+ {
774
+ "grant_type": "authorization_code",
775
+ "code": "auth_abc123def456",
776
+ "redirect_uri": "https://app.example.com/callback",
777
+ "client_id": "app_123",
778
+ "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
779
+ }
780
+ ```
781
+
782
+ #### Request Body Parameters (JSON)
783
+
784
+ | Parameter | Type | Required | Description |
785
+ |-----------|------|----------|-------------|
786
+ | `grant_type` | string | Yes | Must be `"authorization_code"` |
787
+ | `code` | string | Yes | Authorization code from previous step |
788
+ | `redirect_uri` | string | Yes* | Must match the authorization request |
789
+ | `client_id` | string | No | Application identifier (validated if provided) |
790
+ | `code_verifier` | string | No** | PKCE code verifier |
791
+
792
+ \* Required if `redirect_uri` was used in authorization request
793
+ \** Required if PKCE code challenge was used
794
+
795
+ #### Response
796
+
797
+ **Success (200):**
798
+ ```json
799
+ {
800
+ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
801
+ "token_type": "Bearer",
802
+ "expires_in": 60,
803
+ "refresh_token": "789abc123def456789abc123def456789abc456def789abc123def456...",
804
+ "scope": "profile email"
805
+ }
806
+ ```
807
+
808
+ **Error (400):**
809
+ ```json
810
+ {
811
+ "error": "invalid_grant",
812
+ "error_description": "Invalid or expired authorization code"
813
+ }
814
+ ```
815
+
816
+ #### Token Properties
817
+
818
+ **Access Token (JWT):**
819
+ - Algorithm: RS256
820
+ - Expiration: 60 seconds (1 minute)
821
+ - Claims:
822
+ ```json
823
+ {
824
+ "clientId": "app_123",
825
+ "userId": "acc_12345",
826
+ "scope": "profile email",
827
+ "iat": 1727606400,
828
+ "exp": 1727610000
829
+ }
830
+ ```
831
+
832
+ **Refresh Token:**
833
+ - Format: Opaque cryptographic token
834
+ - Storage: Database with hashed value
835
+ - Expiration: 30 days
836
+ - Properties: Linked to session, supports rotation
837
+
838
+ ---
839
+
840
+ ### Token Management
841
+
842
+ #### Refresh Access Token
843
+
844
+ **Endpoint:** `POST /auth/token`
845
+
846
+ Obtain a new access token using a refresh token.
847
+
848
+ #### Request
849
+
850
+ ```http
851
+ POST /auth/token HTTP/1.1
852
+ Host: auth.example.com
853
+ Content-Type: application/json
854
+
855
+ {
856
+ "grant_type": "refresh_token",
857
+ "refresh_token": "789abc123def456789abc123def456789abc456def789abc123def456...",
858
+ "client_id": "app_123"
859
+ }
860
+ ```
861
+
862
+ #### Request Body Parameters (JSON)
863
+
864
+ | Parameter | Type | Required | Description |
865
+ |-----------|------|----------|-------------|
866
+ | `grant_type` | string | Yes | Must be `"refresh_token"` |
867
+ | `refresh_token` | string | Yes | Valid refresh token |
868
+ | `client_id` | string | No | Application identifier (validated if provided) |
869
+
870
+ #### Response
871
+
872
+ **Success (200):**
873
+ ```json
874
+ {
875
+ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
876
+ "token_type": "Bearer",
877
+ "expires_in": 60,
878
+ "refresh_token": "456def789abc123def456789abc123def456abc123def456789abc...",
879
+ "scope": "profile email"
880
+ }
881
+ ```
882
+
883
+ **Error (400):**
884
+ ```json
885
+ {
886
+ "error": "invalid_grant",
887
+ "error_description": "Invalid or expired refresh token"
888
+ }
889
+ ```
890
+
891
+ #### Refresh Token Rotation
892
+
893
+ The server implements **automatic refresh token rotation** for security:
894
+
895
+ 1. Each refresh token use generates a new refresh token
896
+ 2. The old refresh token is marked as "used" but kept in grace period
897
+ 3. **Grace Period**: 60 seconds window where old token can be reused
898
+ - Prevents issues with network retries
899
+ - Multiple requests return the same new token
900
+ 4. **Reuse Detection**: If a revoked token is used → entire chain is revoked
901
+
902
+ #### Validation Checks
903
+
904
+ Before issuing new tokens, the server validates:
905
+ - Refresh token is active and not expired
906
+ - Account connection is still active (`status = 'connected'`)
907
+ - Client ID matches (if provided)
908
+ - Token hasn't been revoked
909
+
910
+ ---
911
+
912
+ ### User Information
913
+
914
+ **Endpoint:** `GET /auth/userinfo`
915
+
916
+ Retrieve user information using an access token (OpenID Connect UserInfo endpoint).
917
+
918
+ #### Request
919
+
920
+ ```http
921
+ GET /auth/userinfo HTTP/1.1
922
+ Host: auth.example.com
923
+ Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
924
+ ```
925
+
926
+ #### Request Headers
927
+
928
+ | Header | Required | Description |
929
+ |--------|----------|-------------|
930
+ | `Authorization` | Yes | Bearer token with `profile` or `admin` scope |
931
+
932
+ #### Response
933
+
934
+ **Success (200):**
935
+ ```json
936
+ {
937
+ "sub": "acc_12345",
938
+ "id": "acc_12345",
939
+ "name": "John Doe",
940
+ "email": "john@example.com",
941
+ "username": "john_doe"
942
+ }
943
+ ```
944
+
945
+ **Error (401):**
946
+ ```json
947
+ {
948
+ "error": "invalid_request",
949
+ "error_description": "Invalid or expired access token"
950
+ }
951
+ ```
952
+
953
+ **Error (403):**
954
+ ```json
955
+ {
956
+ "error": "invalid_scope",
957
+ "error_description": "Requires 'profile:read' permission. No grant found for 'profile:read'..."
958
+ }
959
+ ```
960
+
961
+ #### Scope Requirements
962
+
963
+ The access token must have:
964
+ - `profile:read` scope - Standard user information access
965
+ - OR `admin` scope - Full access (includes all scopes)
966
+
967
+ ---
968
+
969
+ ## Scopes & Authorization
970
+
971
+ ### Scope System
972
+
973
+ The server uses a hierarchical scope-based authorization system for fine-grained permission control.
974
+
975
+ **Scope Format:** `resource:child:action`
976
+
977
+ **Examples:**
978
+ - `account:read` or `account` - Read account information (read is default)
979
+ - `app:db:write` - Write to app database
980
+ - `account:session:delete` - Delete sessions
981
+ - `admin` - Super scope (grants everything)
982
+
983
+ **Default Action:**
984
+ - If no action is specified, defaults to `read`
985
+ - `profile` = `profile:read`
986
+ - `email` = `email:read`
987
+ - `account` = `account:read`
988
+
989
+ **Key Principles:**
990
+ - ✅ **Hierarchical:** Resources can have children (`account:app:read`)
991
+ - ✅ **Action Implications:** Higher actions grant lower ones
992
+ - ✅ **Default Action:** Omitted action defaults to `read`
993
+ - ✅ **Validated:** Invalid scopes rejected at creation
994
+ - ✅ **Clear Errors:** Detailed permission messages
995
+
996
+ ---
997
+
998
+ ### Available Scopes Reference
999
+
1000
+ #### OpenID Connect Scopes
1001
+
1002
+ | Scope | Description |
1003
+ |-------|-------------|
1004
+ | `profile:read` | User profile information (name, username) |
1005
+ | `email:read` | User's email address |
1006
+ | `openid:read` | OpenID Connect authentication |
1007
+
1008
+ #### Account Management
1009
+
1010
+ | Scope | Description | Implied Actions |
1011
+ |-------|-------------|-----------------|
1012
+ | `account:read` | View account details | - |
1013
+ | `account:write` | Full write access | read, create, update, delete |
1014
+ | `account:update` | Update account information | - |
1015
+ | `account:admin` | Full account control | write + all sub-actions |
1016
+
1017
+ #### Connected Apps
1018
+
1019
+ | Scope | Description | Implied Actions |
1020
+ |-------|-------------|-----------------|
1021
+ | `account:app:read` | List connected applications | - |
1022
+ | `account:app:write` | Full app management | read, create, update, delete |
1023
+ | `account:app:update` | Update app connections | - |
1024
+ | `account:app:delete` | Disconnect applications | - |
1025
+ | `account:app:admin` | Full app control | write + all sub-actions |
1026
+
1027
+ #### Session Management
1028
+
1029
+ | Scope | Description | Implied Actions |
1030
+ |-------|-------------|-----------------|
1031
+ | `account:session:read` | View active device sessions | - |
1032
+ | `account:session:write` | Full session management | read, create, update, delete |
1033
+ | `account:session:update` | Update session metadata | - |
1034
+ | `account:session:delete` | Revoke sessions | - |
1035
+ | `account:session:admin` | Full session control | write + all sub-actions |
1036
+
1037
+ #### App Database
1038
+
1039
+ | Scope | Description | Implied Actions |
1040
+ |-------|-------------|-----------------|
1041
+ | `app:db:read` | Query database tables | - |
1042
+ | `app:db:write` | Full database access | read, create, update, delete |
1043
+ | `app:db:create` | Create new records | - |
1044
+ | `app:db:update` | Update existing records | - |
1045
+ | `app:db:delete` | Delete records | - |
1046
+ | `app:db:admin` | Full database control | write + all sub-actions |
1047
+
1048
+ #### App Profiles
1049
+
1050
+ | Scope | Description | Implied Actions |
1051
+ |-------|-------------|-----------------|
1052
+ | `app:profile:read` | View user profiles in app | - |
1053
+ | `app:profile:write` | Full profile access | read, create, update, delete |
1054
+ | `app:profile:update` | Update user profiles | - |
1055
+ | `app:profile:delete` | Delete user profiles | - |
1056
+ | `app:profile:admin` | Full profile control | write + all sub-actions |
1057
+
1058
+ ---
1059
+
1060
+ ### Action Implications
1061
+
1062
+ **Automatic Permission Grants:**
1063
+
1064
+ ```
1065
+ admin → write → read, create, update, delete
1066
+ ```
1067
+
1068
+ **How It Works:**
1069
+ When you have a higher-level action, you automatically get all lower-level actions.
1070
+
1071
+ **Example 1: Database Write**
1072
+ ```
1073
+ Granted: app:db:write
1074
+ Automatically includes:
1075
+ ✅ app:db:read
1076
+ ✅ app:db:create
1077
+ ✅ app:db:update
1078
+ ✅ app:db:delete
1079
+ ```
1080
+
1081
+ **Example 2: Admin Scope**
1082
+ ```
1083
+ Granted: admin
1084
+ Automatically includes:
1085
+ ✅ Every scope in the system
1086
+ ✅ Bypasses all restrictions
1087
+ ```
1088
+
1089
+ **Best Practice:** Request the highest action you need, not every individual permission.
1090
+
1091
+ ---
1092
+
1093
+ ### Admin Scope Behavior
1094
+
1095
+ **The `admin` scope is special:**
1096
+
1097
+ **Availability:**
1098
+ - ✅ Only via `/auth/login` (client_id='self')
1099
+ - ❌ OAuth apps cannot request it
1100
+
1101
+ **Permissions:**
1102
+ - ✅ Grants ALL scopes
1103
+ - ✅ Bypasses ownership checks
1104
+ - ✅ Access to all apps' data
1105
+ - ✅ Full account administration
1106
+
1107
+ **Use Cases:**
1108
+ - User managing their own account
1109
+ - Viewing data across all connected apps
1110
+ - Account administration and debugging
1111
+
1112
+ **Security:**
1113
+ ```http
1114
+ Third-party app requests admin:
1115
+ GET /auth/authorize?scope=admin&client_id=my-app
1116
+ → ERROR: "Admin scopes can only be granted to the self client"
1117
+ ```
1118
+
1119
+ ---
1120
+
1121
+ ### Authorization Errors
1122
+
1123
+ #### 1. Insufficient Scope (403)
1124
+
1125
+ **Cause:** Missing required scope
1126
+
1127
+ **Response:**
1128
+ ```json
1129
+ {
1130
+ "error": "insufficient_permissions",
1131
+ "message": "Requires 'app:db:write' permission",
1132
+ "required": "app:db:write",
1133
+ "reason": "No grant found for 'app:db:write'. Available: profile:read"
1134
+ }
1135
+ ```
1136
+
1137
+ **Solution:** Request additional scopes via incremental authorization
1138
+
1139
+ ---
1140
+
1141
+ #### 2. Resource Ownership (403)
1142
+
1143
+ **Cause:** Accessing another app's data
1144
+
1145
+ **Response:**
1146
+ ```json
1147
+ {
1148
+ "error": "forbidden",
1149
+ "message": "Cannot access data for a different application",
1150
+ "type": "ownership",
1151
+ "hint": "Your access token is scoped to a different application."
1152
+ }
1153
+ ```
1154
+
1155
+ **Solution:** Use correct project_id or get admin scope
1156
+
1157
+ ---
1158
+
1159
+ #### 3. Invalid Scope (400)
1160
+
1161
+ **Cause:** Requesting non-existent scope
1162
+
1163
+ **Response:**
1164
+ ```json
1165
+ {
1166
+ "error": "invalid_scope",
1167
+ "error_description": "Invalid scopes: unknown:scope"
1168
+ }
1169
+ ```
1170
+
1171
+ **Solution:** Use valid scopes from reference
1172
+
1173
+ ---
1174
+
1175
+ ### Best Practices
1176
+
1177
+ **1. Request Minimum Scopes**
1178
+ ```javascript
1179
+ // ✅ Good
1180
+ scope: 'profile:read app:db:write'
1181
+
1182
+ // ❌ Bad (redundant)
1183
+ scope: 'profile:read app:db:read app:db:write app:db:create app:db:update'
1184
+ ```
1185
+
1186
+ **2. Handle Errors Gracefully**
1187
+ ```javascript
1188
+ if (response.status === 403) {
1189
+ const error = await response.json();
1190
+ if (error.required) {
1191
+ // Guide user to re-authorize with needed scope
1192
+ requestAdditionalScope(error.required);
1193
+ }
1194
+ }
1195
+ ```
1196
+
1197
+ **3. Use Incremental Authorization**
1198
+ ```javascript
1199
+ // Start with basic scopes
1200
+ initialScopes: 'profile:read'
1201
+
1202
+ // Request more later when needed
1203
+ additionalScopes: 'app:db:write'
1204
+ // Result: User has both scopes
1205
+ ```
1206
+
1207
+ **4. Test with Real Scopes**
1208
+ - Don't use admin in development
1209
+ - Test with actual OAuth scopes
1210
+ - Validate error handling
1211
+
1212
+ ---
1213
+
1214
+ ### Scope Format Rules
1215
+
1216
+ **Valid Formats:**
1217
+ - Full format: `resource:action` (e.g., `profile:read`)
1218
+ - Shorthand: `resource` (e.g., `profile` - defaults to `read`)
1219
+ - Hierarchical: `resource:child:action` (e.g., `account:app:read`)
1220
+
1221
+ **Examples:**
1222
+ ```
1223
+ ✅ Valid:
1224
+ - profile (defaults to profile:read)
1225
+ - email (defaults to email:read)
1226
+ - account:read
1227
+ - app:db:write
1228
+ - account:session:delete
1229
+
1230
+ ❌ Invalid:
1231
+ - account.read (wrong separator)
1232
+ - app/db (wrong separator)
1233
+ - app::read (consecutive colons)
1234
+ ```
1235
+
1236
+ **OAuth Standard Compatibility:**
1237
+ - OpenID Connect scopes: `profile`, `email`, `openid` (read action implied)
1238
+ - Custom scopes: Use full format or shorthand for read
1239
+
1240
+ ---
1241
+
1242
+ ## PKCE Extension (Optional)
1243
+
1244
+ **Proof Key for Code Exchange (PKCE)** enhances security for public clients that cannot securely store client secrets. This follows RFC 7636.
1245
+
1246
+ ### PKCE Flow Overview
1247
+
1248
+ 1. Client generates code verifier (random string)
1249
+ 2. Client creates code challenge (SHA256 hash of verifier)
1250
+ 3. Authorization request includes code challenge
1251
+ 4. Token request includes code verifier
1252
+ 5. Server validates: `SHA256(code_verifier) === code_challenge`
1253
+
1254
+ ### Implementation Steps
1255
+
1256
+ #### Step 1: Generate Code Verifier
1257
+
1258
+ Create a cryptographically random string:
1259
+
1260
+ ```javascript
1261
+ // Generate 32-byte random string
1262
+ const verifier = base64url(randomBytes(32))
1263
+ // Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
1264
+ ```
1265
+
1266
+ **Requirements:**
1267
+ - Length: 43-128 characters
1268
+ - Character set: `[A-Z]`, `[a-z]`, `[0-9]`, `-`, `.`, `_`, `~`
1269
+ - Encoding: Base64url without padding
1270
+
1271
+ ---
1272
+
1273
+ #### Step 2: Generate Code Challenge
1274
+
1275
+ Create SHA256 hash of the code verifier:
1276
+
1277
+ ```javascript
1278
+ const challenge = base64url(sha256(verifier))
1279
+ // Example: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
1280
+ ```
1281
+
1282
+ **Algorithm:** `S256` (SHA256) - Only supported method
1283
+
1284
+ ---
1285
+
1286
+ #### Step 3: Authorization Request with PKCE
1287
+
1288
+ ```http
1289
+ GET /auth/authorize?client_id=app_123&redirect_uri=https://app.example.com/callback&response_type=code&state=xyz123&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256 HTTP/1.1
1290
+ ```
1291
+
1292
+ **Additional Parameters:**
1293
+
1294
+ | Parameter | Value | Description |
1295
+ |-----------|-------|-------------|
1296
+ | `code_challenge` | Base64url string | SHA256 hash of verifier |
1297
+ | `code_challenge_method` | `"S256"` | Always use SHA256 |
1298
+
1299
+ ---
1300
+
1301
+ #### Step 4: Token Exchange with PKCE
1302
+
1303
+ ```http
1304
+ POST /auth/token HTTP/1.1
1305
+ Content-Type: application/json
1306
+
1307
+ {
1308
+ "grant_type": "authorization_code",
1309
+ "code": "auth_abc123",
1310
+ "redirect_uri": "https://app.example.com/callback",
1311
+ "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
1312
+ }
1313
+ ```
1314
+
1315
+ **Additional Parameter:**
1316
+
1317
+ | Parameter | Value | Description |
1318
+ |-----------|-------|-------------|
1319
+ | `code_verifier` | Original random string | Used to verify challenge |
1320
+
1321
+ ---
1322
+
1323
+ ### PKCE Validation
1324
+
1325
+ The server validates PKCE as follows:
1326
+
1327
+ 1. **If code challenge was provided in authorization:**
1328
+ - `code_verifier` is **required** in token request
1329
+ - Server computes: `SHA256(code_verifier)`
1330
+ - Must match stored `code_challenge`
1331
+ - Mismatch → `400 invalid_grant`
1332
+
1333
+ 2. **If no code challenge in authorization:**
1334
+ - PKCE is optional
1335
+ - `code_verifier` is ignored if provided
1336
+
1337
+ ### Error Responses
1338
+
1339
+ **Missing code_verifier:**
1340
+ ```json
1341
+ {
1342
+ "error": "invalid_request",
1343
+ "error_description": "Missing required parameter: code_verifier (PKCE validation required)"
1344
+ }
1345
+ ```
1346
+
1347
+ **Invalid code_verifier:**
1348
+ ```json
1349
+ {
1350
+ "error": "invalid_grant",
1351
+ "error_description": "Invalid code_verifier - PKCE validation failed"
1352
+ }
1353
+ ```
1354
+
1355
+ **Unsupported challenge method:**
1356
+ ```json
1357
+ {
1358
+ "error": "invalid_request",
1359
+ "error_description": "Invalid code_challenge_method. Only S256 is supported"
1360
+ }
1361
+ ```
1362
+
1363
+ ---
1364
+
1365
+ ## DPoP Extension (Optional)
1366
+
1367
+ **Demonstrating Proof-of-Possession (DPoP)** binds tokens to specific clients using public key cryptography. This follows [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449).
1368
+
1369
+ ### DPoP Overview
1370
+
1371
+ DPoP prevents token theft by binding tokens to a client's private key:
1372
+ 1. Client generates key pair
1373
+ 2. Client creates DPoP proof (signed JWT)
1374
+ 3. Server binds token to key thumbprint
1375
+ 4. All requests require valid DPoP proof
1376
+
1377
+ ### DPoP Proof Format
1378
+
1379
+ A DPoP proof is a JWT with specific claims:
1380
+
1381
+ ```json
1382
+ {
1383
+ "typ": "dpop+jwt",
1384
+ "alg": "ES256",
1385
+ "jwk": {
1386
+ "kty": "EC",
1387
+ "crv": "P-256",
1388
+ "x": "...",
1389
+ "y": "..."
1390
+ }
1391
+ }
1392
+ ```
1393
+
1394
+ **Payload:**
1395
+ ```json
1396
+ {
1397
+ "jti": "unique-request-id",
1398
+ "htm": "POST",
1399
+ "htu": "https://auth.example.com/auth/token",
1400
+ "iat": 1727606400,
1401
+ "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
1402
+ }
1403
+ ```
1404
+
1405
+ ### DPoP Claims
1406
+
1407
+ | Claim | Required | Description |
1408
+ |-------|----------|-------------|
1409
+ | `typ` | Yes | Must be `"dpop+jwt"` |
1410
+ | `alg` | Yes | Signing algorithm (ES256, RS256) |
1411
+ | `jwk` | Yes | Public key in JWK format |
1412
+ | `jti` | Yes | Unique identifier (prevents replay) |
1413
+ | `htm` | Yes | HTTP method (uppercase) |
1414
+ | `htu` | Yes | HTTP URI (without query/fragment) |
1415
+ | `iat` | Yes | Issued at timestamp |
1416
+ | `ath` | No | Hash of access token (for resource requests) |
1417
+
1418
+ ### Implementation Steps
1419
+
1420
+ #### Step 1: Generate Key Pair
1421
+
1422
+ ```javascript
1423
+ // Generate ES256 key pair
1424
+ const keyPair = await crypto.subtle.generateKey(
1425
+ { name: "ECDSA", namedCurve: "P-256" },
1426
+ true,
1427
+ ["sign", "verify"]
1428
+ )
1429
+
1430
+ // Export public key as JWK
1431
+ const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey)
1432
+ ```
1433
+
1434
+ ---
1435
+
1436
+ #### Step 2: Create DPoP Proof
1437
+
1438
+ ```javascript
1439
+ // Create proof header
1440
+ const header = {
1441
+ typ: "dpop+jwt",
1442
+ alg: "ES256",
1443
+ jwk: publicJwk
1444
+ }
1445
+
1446
+ // Create proof payload
1447
+ const payload = {
1448
+ jti: crypto.randomUUID(),
1449
+ htm: "POST",
1450
+ htu: "https://auth.example.com/auth/token",
1451
+ iat: Math.floor(Date.now() / 1000)
1452
+ }
1453
+
1454
+ // Sign JWT
1455
+ const dpopProof = await signJWT(header, payload, privateKey)
1456
+ ```
1457
+
1458
+ ---
1459
+
1460
+ #### Step 3: Token Request with DPoP
1461
+
1462
+ ```http
1463
+ POST /auth/token HTTP/1.1
1464
+ Host: auth.example.com
1465
+ Content-Type: application/json
1466
+ DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6Ik...
1467
+
1468
+ {
1469
+ "grant_type": "authorization_code",
1470
+ "code": "auth_abc123",
1471
+ "redirect_uri": "https://app.example.com/callback"
1472
+ }
1473
+ ```
1474
+
1475
+ **Headers:**
1476
+ ```
1477
+ Content-Type: application/json
1478
+ DPoP: {dpop_proof_jwt}
1479
+ ```
1480
+
1481
+ ---
1482
+
1483
+ #### Step 4: DPoP-Bound Token Response
1484
+
1485
+ ```json
1486
+ {
1487
+ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
1488
+ "token_type": "DPoP",
1489
+ "expires_in": 60,
1490
+ "refresh_token": "xyz789abc123def456789abc123def456789def456abc123def789abc...",
1491
+ "scope": "profile email"
1492
+ }
1493
+ ```
1494
+
1495
+ **Key Differences:**
1496
+ - `token_type` is `"DPoP"` (not `"Bearer"`)
1497
+ - Access token contains `cnf` claim with key thumbprint:
1498
+ ```json
1499
+ {
1500
+ "cnf": {
1501
+ "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
1502
+ }
1503
+ }
1504
+ ```
1505
+
1506
+ ---
1507
+
1508
+ #### Step 5: Resource Request with DPoP
1509
+
1510
+ ```http
1511
+ GET /account/profile HTTP/1.1
1512
+ Host: api.example.com
1513
+ Authorization: DPoP eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...
1514
+ DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6Ik...
1515
+ ```
1516
+
1517
+ **DPoP Proof includes `ath` claim:**
1518
+ ```json
1519
+ {
1520
+ "jti": "unique-request-id-2",
1521
+ "htm": "GET",
1522
+ "htu": "https://api.example.com/account/profile",
1523
+ "iat": 1727606500,
1524
+ "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
1525
+ }
1526
+ ```
1527
+
1528
+ `ath` = Base64url(SHA256(access_token))
1529
+
1530
+ ---
1531
+
1532
+ ### DPoP Validation
1533
+
1534
+ The server validates DPoP proofs:
1535
+
1536
+ 1. **Signature Validation:**
1537
+ - Verify JWT signature using `jwk` in header
1538
+ - Algorithm must match `alg` claim
1539
+
1540
+ 2. **Claim Validation:**
1541
+ - `typ` must be `"dpop+jwt"`
1542
+ - `htm` must match HTTP method
1543
+ - `htu` must match request URI (scheme + authority + path)
1544
+ - `iat` must be recent (within acceptable skew)
1545
+
1546
+ 3. **Token Binding:**
1547
+ - Compute `jkt` = Base64url(SHA256(JWK))
1548
+ - Must match `cnf.jkt` in access token
1549
+
1550
+ 4. **Replay Protection:**
1551
+ - `jti` must be unique
1552
+ - Store recent `jti` values (cache)
1553
+
1554
+ ### Error Responses
1555
+
1556
+ **Invalid DPoP Proof:**
1557
+ ```
1558
+ HTTP/1.1 401 Unauthorized
1559
+ WWW-Authenticate: DPoP error="invalid_dpop_proof"
1560
+ ```
1561
+
1562
+ **Missing DPoP Proof:**
1563
+ ```
1564
+ HTTP/1.1 401 Unauthorized
1565
+ WWW-Authenticate: DPoP error="invalid_token", error_description="DPoP proof required"
1566
+ ```
1567
+
1568
+ ---
1569
+
1570
+ ## Security Requirements
1571
+
1572
+ ### Token Security
1573
+
1574
+ #### Access Token
1575
+ - **Format:** JWT with RS256 signature
1576
+ - **Lifetime:** 1 minute (60 seconds)
1577
+ - **Transmission:** HTTPS only, in Authorization header
1578
+
1579
+ #### Token Validation with JWKS
1580
+
1581
+ To verify access tokens, clients should use the public keys provided by the JWKS endpoint.
1582
+
1583
+ **Step 1: Fetch JWKS**
1584
+
1585
+ ```javascript
1586
+ // Fetch and cache the JWKS
1587
+ async function getJWKS() {
1588
+ const response = await fetch('https://auth.example.com/auth/.well-known/jwks.json');
1589
+ const jwks = await response.json();
1590
+ return jwks;
1591
+ }
1592
+ ```
1593
+
1594
+ **Step 2: Verify JWT Signature**
1595
+
1596
+ Using the `jose` library (recommended):
1597
+
1598
+ ```javascript
1599
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
1600
+
1601
+ // Create JWKS instance (cache this)
1602
+ const JWKS = createRemoteJWKSet(
1603
+ new URL('https://auth.example.com/auth/.well-known/jwks.json')
1604
+ );
1605
+
1606
+ // Verify token
1607
+ async function verifyAccessToken(token) {
1608
+ try {
1609
+ const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
1610
+ algorithms: ['RS256'],
1611
+ issuer: 'https://auth.example.com', // Optional: validate issuer
1612
+ });
1613
+
1614
+ // Token is valid
1615
+ return {
1616
+ valid: true,
1617
+ userId: payload.userId,
1618
+ clientId: payload.clientId,
1619
+ scope: payload.scope,
1620
+ exp: payload.exp
1621
+ };
1622
+ } catch (error) {
1623
+ // Token is invalid or expired
1624
+ return {
1625
+ valid: false,
1626
+ error: error.message
1627
+ };
1628
+ }
1629
+ }
1630
+ ```
1631
+
1632
+ **Step 3: Validate Claims**
1633
+
1634
+ ```javascript
1635
+ async function validateToken(token) {
1636
+ // Verify signature and decode
1637
+ const result = await verifyAccessToken(token);
1638
+
1639
+ if (!result.valid) {
1640
+ throw new Error('Invalid token: ' + result.error);
1641
+ }
1642
+
1643
+ // Additional claim validation
1644
+ const now = Math.floor(Date.now() / 1000);
1645
+
1646
+ // Check expiration
1647
+ if (result.exp < now) {
1648
+ throw new Error('Token expired');
1649
+ }
1650
+
1651
+ // Check scope (recommended approach)
1652
+ const tokenScopes = result.scope.split(',').map(s => s.trim());
1653
+
1654
+ // Check for required scope or admin (which grants everything)
1655
+ const hasRequiredScope = tokenScopes.includes('profile:read') || tokenScopes.includes('admin');
1656
+
1657
+ if (!hasRequiredScope) {
1658
+ throw new Error('Insufficient scope: requires profile:read');
1659
+ }
1660
+
1661
+ return result;
1662
+ }
1663
+ ```
1664
+
1665
+ **Best Practices:**
1666
+
1667
+ 1. **Cache JWKS**: Cache the JWKS response for at least 1 hour (check `Cache-Control` header)
1668
+ 2. **Key Rotation**: Support multiple keys in JWKS for seamless rotation
1669
+ 3. **Algorithm Validation**: Always specify `algorithms: ['RS256']` to prevent algorithm confusion attacks
1670
+ 4. **Clock Skew**: Allow 60-second clock skew for `exp` and `iat` validation
1671
+ 5. **Error Handling**: Distinguish between expired, invalid signature, and malformed tokens
1672
+
1673
+ **Manual Verification (without library):**
1674
+
1675
+ ```javascript
1676
+ import crypto from 'crypto';
1677
+
1678
+ async function manualVerifyToken(token) {
1679
+ // Split JWT into parts
1680
+ const [headerB64, payloadB64, signatureB64] = token.split('.');
1681
+
1682
+ // Decode header and payload
1683
+ const header = JSON.parse(Buffer.from(headerB64, 'base64url').toString());
1684
+ const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
1685
+
1686
+ // Fetch JWKS and find matching key
1687
+ const jwks = await getJWKS();
1688
+ const key = jwks.keys.find(k => k.kid === header.kid);
1689
+
1690
+ if (!key) {
1691
+ throw new Error('Key not found in JWKS');
1692
+ }
1693
+
1694
+ // Convert JWK to PEM format (requires jwk-to-pem library)
1695
+ const publicKey = jwkToPem(key);
1696
+
1697
+ // Verify signature
1698
+ const verify = crypto.createVerify('RSA-SHA256');
1699
+ verify.update(`${headerB64}.${payloadB64}`);
1700
+
1701
+ const signature = Buffer.from(signatureB64, 'base64url');
1702
+ const isValid = verify.verify(publicKey, signature);
1703
+
1704
+ if (!isValid) {
1705
+ throw new Error('Invalid signature');
1706
+ }
1707
+
1708
+ // Validate claims
1709
+ const now = Math.floor(Date.now() / 1000);
1710
+ if (payload.exp < now) {
1711
+ throw new Error('Token expired');
1712
+ }
1713
+
1714
+ return payload;
1715
+ }
1716
+ ```
1717
+
1718
+ **Token Validation Response:**
1719
+
1720
+ ```javascript
1721
+ // Valid token
1722
+ {
1723
+ "valid": true,
1724
+ "userId": "acc_12345",
1725
+ "clientId": "app_123",
1726
+ "scope": "profile email",
1727
+ "exp": 1727606460
1728
+ }
1729
+
1730
+ // Invalid token
1731
+ {
1732
+ "valid": false,
1733
+ "error": "signature verification failed"
1734
+ }
1735
+ ```
1736
+
1737
+ #### Refresh Token
1738
+ - **Format:** Cryptographically random string (256-bit)
1739
+ - **Storage:** Database with SHA256 hash
1740
+ - **Lifetime:** 30 days
1741
+ - **Rotation:** Automatic on each use
1742
+ - **Grace Period:** 60 seconds for network retries
1743
+ - **Reuse Detection:** Entire chain revoked on reuse
1744
+
1745
+ ---
1746
+
1747
+ ### CSRF Protection
1748
+
1749
+ **State Parameter:**
1750
+ - Required in authorization flow
1751
+ - Minimum 128-bit entropy
1752
+ - Single-use (bound to session)
1753
+ - Verified on callback
1754
+
1755
+ **Implementation:**
1756
+ ```javascript
1757
+ // Generate state
1758
+ const state = base64url(randomBytes(32))
1759
+ // Store in session/cookie
1760
+ session.oauthState = state
1761
+ // Include in authorization URL
1762
+ const authUrl = `...&state=${state}`
1763
+ // Verify on callback
1764
+ if (callbackState !== session.oauthState) {
1765
+ throw new Error('Invalid state')
1766
+ }
1767
+ ```
1768
+
1769
+ ---
1770
+
1771
+ ### Redirect URI Security
1772
+
1773
+ **Validation Rules:**
1774
+ 1. **Exact Match:** Must match registered URI
1775
+ 2. **No Wildcards:** Wildcards not supported
1776
+ 3. **HTTPS Only:** In production (HTTP allowed for localhost)
1777
+ 4. **No Fragments:** Fragment identifiers not allowed
1778
+ 5. **Validation Timing:** Before showing authorization UI
1779
+
1780
+ **Error Handling:**
1781
+ - Invalid redirect_uri → Do not redirect (return 400)
1782
+ - Other errors → Redirect with error parameter
1783
+
1784
+ ---
1785
+
1786
+ ### Password Requirements
1787
+
1788
+ **Minimum Requirements:**
1789
+ - Length: 3 characters (update this for production)
1790
+ - Storage: Hashed with bcrypt (cost factor 10)
1791
+ - Transmission: HTTPS only
1792
+ - Reset tokens: 15-minute expiration
1793
+ - Verification tokens: 24-hour expiration
1794
+
1795
+ ---
1796
+
1797
+
1798
+ ### Error Handling
1799
+
1800
+ **Error Response Format (OAuth2):**
1801
+ ```json
1802
+ {
1803
+ "error": "invalid_request",
1804
+ "error_description": "Missing required parameter: client_id",
1805
+ "error_uri": "https://docs.example.com/errors/invalid_request"
1806
+ }
1807
+ ```
1808
+
1809
+ **Standard Error Codes:**
1810
+ - `invalid_request` - Malformed request
1811
+ - `invalid_client` - Invalid client credentials
1812
+ - `invalid_grant` - Invalid/expired code or token
1813
+ - `unauthorized_client` - Client not authorized
1814
+ - `unsupported_grant_type` - Grant type not supported
1815
+ - `invalid_scope` - Requested scope invalid
1816
+ - `access_denied` - User denied authorization
1817
+ - `server_error` - Internal server error
1818
+
1819
+ **Error Response Rules:**
1820
+ - Never leak sensitive information
1821
+ - Log detailed errors server-side
1822
+ - Return generic errors to client
1823
+ - Include `state` parameter in redirects
1824
+
1825
+ ---
1826
+
1827
+ ## Complete Flow Examples
1828
+
1829
+ ### Example 1: Simple Web App Login
1830
+
1831
+ **Step 1 - User clicks "Login":**
1832
+ ```javascript
1833
+ const authUrl = new URL('https://auth.example.com/auth/authorize')
1834
+ authUrl.searchParams.set('client_id', 'webapp_123')
1835
+ authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback')
1836
+ authUrl.searchParams.set('response_type', 'code')
1837
+ authUrl.searchParams.set('scope', 'profile email')
1838
+ authUrl.searchParams.set('state', generateState())
1839
+
1840
+ window.location.href = authUrl.toString()
1841
+ ```
1842
+
1843
+ **Step 2 - User authenticates and authorizes**
1844
+
1845
+ **Step 3 - Callback receives code:**
1846
+ ```javascript
1847
+ // https://myapp.com/callback?code=auth_abc123&state=xyz789
1848
+
1849
+ // Validate state
1850
+ if (params.state !== session.state) throw new Error('Invalid state')
1851
+
1852
+ // Exchange code for tokens
1853
+ const response = await fetch('https://auth.example.com/auth/token', {
1854
+ method: 'POST',
1855
+ headers: { 'Content-Type': 'application/json' },
1856
+ body: JSON.stringify({
1857
+ grant_type: 'authorization_code',
1858
+ code: params.code,
1859
+ redirect_uri: 'https://myapp.com/callback'
1860
+ })
1861
+ })
1862
+
1863
+ const tokens = await response.json()
1864
+ // Store tokens securely
1865
+ session.accessToken = tokens.access_token
1866
+ session.refreshToken = tokens.refresh_token
1867
+ ```
1868
+
1869
+ **Step 4 - Use access token:**
1870
+ ```javascript
1871
+ const userInfo = await fetch('https://auth.example.com/auth/userinfo', {
1872
+ headers: {
1873
+ 'Authorization': `Bearer ${session.accessToken}`
1874
+ }
1875
+ })
1876
+ ```
1877
+
1878
+ ---
1879
+
1880
+ ### Example 2: Mobile App with PKCE
1881
+
1882
+ **Step 1 - Generate PKCE values:**
1883
+ ```javascript
1884
+ // Generate code verifier
1885
+ const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)))
1886
+
1887
+ // Generate code challenge
1888
+ const encoder = new TextEncoder()
1889
+ const data = encoder.encode(verifier)
1890
+ const hash = await crypto.subtle.digest('SHA-256', data)
1891
+ const challenge = base64url(new Uint8Array(hash))
1892
+
1893
+ // Store verifier for later
1894
+ storage.set('pkce_verifier', verifier)
1895
+ ```
1896
+
1897
+ **Step 2 - Authorization request:**
1898
+ ```javascript
1899
+ const authUrl = new URL('https://auth.example.com/auth/authorize')
1900
+ authUrl.searchParams.set('client_id', 'mobile_app_456')
1901
+ authUrl.searchParams.set('redirect_uri', 'myapp://callback')
1902
+ authUrl.searchParams.set('response_type', 'code')
1903
+ authUrl.searchParams.set('scope', 'profile email')
1904
+ authUrl.searchParams.set('state', generateState())
1905
+ authUrl.searchParams.set('code_challenge', challenge)
1906
+ authUrl.searchParams.set('code_challenge_method', 'S256')
1907
+
1908
+ // Open browser
1909
+ openBrowser(authUrl.toString())
1910
+ ```
1911
+
1912
+ **Step 3 - Token exchange with verifier:**
1913
+ ```javascript
1914
+ // Deep link: myapp://callback?code=auth_abc123&state=xyz789
1915
+
1916
+ const response = await fetch('https://auth.example.com/auth/token', {
1917
+ method: 'POST',
1918
+ headers: { 'Content-Type': 'application/json' },
1919
+ body: JSON.stringify({
1920
+ grant_type: 'authorization_code',
1921
+ code: params.code,
1922
+ redirect_uri: 'myapp://callback',
1923
+ code_verifier: storage.get('pkce_verifier')
1924
+ })
1925
+ })
1926
+
1927
+ const tokens = await response.json()
1928
+ ```
1929
+
1930
+ ---
1931
+
1932
+ ### Example 3: Token Refresh
1933
+
1934
+ **Automatic token refresh:**
1935
+ ```javascript
1936
+ async function getValidAccessToken() {
1937
+ // Check if current token is expired
1938
+ const decodedToken = jwt.decode(session.accessToken)
1939
+ const expiresAt = decodedToken.exp * 1000
1940
+ const now = Date.now()
1941
+
1942
+ // Refresh if token expires in less than 5 minutes
1943
+ if (expiresAt - now < 5 * 60 * 1000) {
1944
+ const response = await fetch('https://auth.example.com/auth/token', {
1945
+ method: 'POST',
1946
+ headers: { 'Content-Type': 'application/json' },
1947
+ body: JSON.stringify({
1948
+ grant_type: 'refresh_token',
1949
+ refresh_token: session.refreshToken
1950
+ })
1951
+ })
1952
+
1953
+ if (!response.ok) {
1954
+ // Refresh token is invalid, need to re-authenticate
1955
+ redirectToLogin()
1956
+ return null
1957
+ }
1958
+
1959
+ const tokens = await response.json()
1960
+ session.accessToken = tokens.access_token
1961
+ session.refreshToken = tokens.refresh_token
1962
+ }
1963
+
1964
+ return session.accessToken
1965
+ }
1966
+
1967
+ // Use in API calls
1968
+ async function apiRequest(url) {
1969
+ const token = await getValidAccessToken()
1970
+ return fetch(url, {
1971
+ headers: { 'Authorization': `Bearer ${token}` }
1972
+ })
1973
+ }
1974
+ ```
1975
+
1976
+ ---
1977
+
1978
+ ## Additional Resources
1979
+
1980
+ ### RFCs and Standards
1981
+
1982
+ - [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) - OAuth 2.0 Authorization Framework
1983
+ - [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750) - OAuth 2.0 Bearer Token Usage
1984
+ - [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) - Proof Key for Code Exchange (PKCE)
1985
+ - [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) - Token Introspection
1986
+ - [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) - OAuth 2.0 Demonstrating Proof-of-Possession (DPoP)
1987
+ - [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
1988
+ - [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html)
1989
+
1990
+ ### Security Best Practices
1991
+
1992
+ - [OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)
1993
+ - [OAuth 2.0 for Browser-Based Apps](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps)
1994
+ - [OAuth 2.0 for Native Apps](https://datatracker.ietf.org/doc/html/rfc8252)
1995
+
1996
+ ---
1997
+
1998
+ ## Support
1999
+
2000
+ For implementation questions or issues:
2001
+ - Review the Swagger documentation at `/docs`
2002
+ - OpenAPI available at /docs/json
2003
+
2004
+ ---
2005
+
2006
+ ---
2007
+
2008
+ **Last Updated:** October 1, 2025
2009
+ **Version:** 2.0.0 - With Scope-Based Authorization