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