Skip to content

Commit f2acdbe

Browse files
authored
feat: add robust OAuth2 refresh token support and management (#14)
* feat: add robust OAuth2 refresh token support and management - Add comprehensive refresh token support (RFC 6749) with both fixed (multi-device friendly) and rotation (high security) modes - Implement refresh token issuance during device code exchange and support refresh token grant type at the token endpoint - Update documentation: clarify architecture, describe refresh token flow, add new environment variables and endpoint details - Extend CLI client to persist tokens, support auto-refresh on expiration, and demo seamless re-authentication - Add settings for refresh token expiration, feature enabling/disabling, and mode selection via environment variables - Enhance token model with category, status, tracking fields, and management utilities (active/disabled/revoked states) - Add new token provider methods for refresh token issuance, validation, and rotation (both local and HTTP API) - Refactor service logic to handle access/refresh tokens in transactional manner, enforce scope, and support rotation - Provide new store/database utilities for token status management and queries by category - Update and extend tests for device code flow to cover dual token issuance and user token queries - Improve .gitignore and example env to exclude new token files - Add references and improve documentation for refresh token concepts and standards Signed-off-by: appleboy <appleboy.tw@gmail.com> * refactor: standardize token type usage and centralize JWT handling - Replace hardcoded token provider mode strings with constants from config for improved maintainability - Refactor local token provider to centralize JWT creation, reducing code duplication - Introduce a helper function for HTTP API token validation, simplifying ValidateToken and ValidateRefreshToken logic - Set token type using a constant rather than a string literal to avoid typos - Define a TokenTypeBearer constant for standardized usage across the token logic Signed-off-by: appleboy <appleboy.tw@gmail.com> --------- Signed-off-by: appleboy <appleboy.tw@gmail.com>
1 parent 55b5372 commit f2acdbe

File tree

15 files changed

+1326
-171
lines changed

15 files changed

+1326
-171
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,8 @@ Thumbs.db
3030
.env.local
3131
bin
3232
.claude
33-
coverage.txt
33+
coverage.txt
34+
35+
# Tokens
36+
*.authgate-tokens.json
37+
.authgate-tokens.json

CLAUDE.md

Lines changed: 94 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ docker build -f docker/Dockerfile -t authgate .
3535

3636
## Architecture
3737

38-
**Device Authorization Flow**:
38+
**Device Authorization Flow (with Refresh Tokens)**:
3939

4040
1. CLI calls `POST /oauth/device/code` with client_id → receives device_code + user_code + verification_uri
4141
2. User visits verification_uri (`/device`) in browser, must login first if not authenticated
4242
3. User submits user_code via `POST /device/verify` → device code marked as authorized
43-
4. CLI polls `POST /oauth/token` with device_code every 5s → receives JWT when authorized
43+
4. CLI polls `POST /oauth/token` with device_code every 5s → receives access_token + refresh_token when authorized
44+
5. CLI uses access_token for API calls (expires in 1 hour)
45+
6. When access_token expires, CLI calls `POST /oauth/token` with `grant_type=refresh_token` → receives new access_token (fixed mode) or new access_token + refresh_token (rotation mode)
4446

4547
**Layers** (dependency injection pattern):
4648

@@ -102,6 +104,62 @@ docker build -f docker/Dockerfile -t authgate .
102104
- **API Contract**: HTTPTokenProvider expects `/generate` and `/validate` endpoints with specific JSON format
103105
- **Key Benefit**: Centralized token services, advanced key management, compliance requirements while maintaining local token management
104106

107+
**Refresh Token Architecture**:
108+
109+
AuthGate supports refresh tokens following RFC 6749 with configurable rotation modes for different security requirements.
110+
111+
- **Key Features**:
112+
113+
- **Dual Modes**: Fixed (reusable) vs Rotation (one-time use) refresh tokens
114+
- **Unified Storage**: Both access and refresh tokens stored in `AccessToken` table with `token_category` field
115+
- **Token Family Tracking**: `parent_token_id` links tokens for audit trails and revocation
116+
- **Status Management**: Tokens can be `active`, `disabled`, or `revoked`
117+
- **Configurable Expiration**: `REFRESH_TOKEN_EXPIRATION` env var (default: 720h = 30 days)
118+
- **Provider Support**: Both LocalTokenProvider and HTTPTokenProvider support refresh operations
119+
120+
- **Fixed Mode (Default - Multi-Device Friendly)**:
121+
122+
1. Device code exchange returns `access_token` + `refresh_token`
123+
2. When access token expires, client POSTs to `/oauth/token` with `grant_type=refresh_token`
124+
3. Server returns new `access_token` only (refresh token remains unchanged and reusable)
125+
4. Process repeats until refresh token expires or is manually disabled/revoked
126+
5. Each device/application gets its own refresh token that doesn't affect others
127+
6. Users can manage all tokens (disable/enable/revoke) via backend UI
128+
7. LastUsedAt field tracks activity for identifying inactive sessions
129+
130+
- **Rotation Mode (Optional - High Security)**:
131+
132+
1. Same as fixed mode, but step 3 returns both new `access_token` + new `refresh_token`
133+
2. Old refresh token is automatically revoked (status set to 'revoked') after each use
134+
3. Prevents token replay attacks
135+
4. Requires clients to update stored refresh token after each use
136+
5. Enable via `ENABLE_TOKEN_ROTATION=true`
137+
138+
- **Token Management**:
139+
140+
- **Status Field**: `active` (usable) / `disabled` (temporarily blocked, can re-enable) / `revoked` (permanently blocked)
141+
- **Independent Revocation**: Revoking refresh token doesn't affect existing access tokens
142+
- **Family Tracking**: ParentTokenID enables audit trails and selective revocation
143+
- **Scope Validation**: Refresh requests cannot escalate privileges beyond original grant
144+
145+
- **Environment Variables**:
146+
147+
- `REFRESH_TOKEN_EXPIRATION=720h` - Refresh token lifetime (default: 30 days)
148+
- `ENABLE_REFRESH_TOKENS=true` - Feature flag (default: enabled)
149+
- `ENABLE_TOKEN_ROTATION=false` - Enable rotation mode (default: disabled, uses fixed mode)
150+
151+
- **Grant Type Support**:
152+
153+
- `urn:ietf:params:oauth:grant-type:device_code` - Device authorization flow (returns access + refresh)
154+
- `refresh_token` - RFC 6749 refresh token grant (returns new tokens)
155+
156+
- **Security Considerations**:
157+
- Refresh tokens validated by type claim (`"type": "refresh"` in JWT)
158+
- Refresh tokens cannot be used as access tokens (separate validation logic)
159+
- Client ID verification prevents cross-client token usage
160+
- Token family tracking enables detection of suspicious patterns
161+
- Optional rotation mode for high-security scenarios
162+
105163
**Key Implementation Details**:
106164

107165
- Device codes expire after 30min (configurable via Config.DeviceCodeExpiration)
@@ -116,8 +174,11 @@ docker build -f docker/Dockerfile -t authgate .
116174

117175
- `GET /health` - Health check with database connection test
118176
- `POST /oauth/device/code` - CLI requests device+user codes (accepts form or JSON)
119-
- `POST /oauth/token` - CLI polls for JWT (grant_type=urn:ietf:params:oauth:grant-type:device_code)
177+
- `POST /oauth/token` - Token endpoint supporting multiple grant types:
178+
- `grant_type=urn:ietf:params:oauth:grant-type:device_code` - Device authorization flow (returns access + refresh tokens)
179+
- `grant_type=refresh_token` - Refresh token grant (RFC 6749) - returns new access token (fixed mode) or new access + refresh tokens (rotation mode)
120180
- `GET /oauth/tokeninfo` - Verify JWT validity
181+
- `POST /oauth/revoke` - Revoke tokens (RFC 7009)
121182
- `GET /device` - User authorization page (protected, requires login)
122183
- `POST /device/verify` - User submits code to authorize device (protected)
123184
- `GET|POST /login` - User authentication
@@ -127,22 +188,25 @@ docker build -f docker/Dockerfile -t authgate .
127188

128189
## Environment Variables
129190

130-
| Variable | Default | Description |
131-
| ----------------------------- | ----------------------- | ------------------------------------------------------------ |
132-
| SERVER_ADDR | :8080 | Listen address |
133-
| BASE_URL | `http://localhost:8080` | Public URL for verification_uri |
134-
| JWT_SECRET | (default) | JWT signing key (used when TOKEN_PROVIDER_MODE=local) |
135-
| SESSION_SECRET | (default) | Cookie encryption key |
136-
| DATABASE_DRIVER | sqlite | Database driver ("sqlite" or "postgres") |
137-
| DATABASE_DSN | oauth.db | Connection string (path for SQLite, DSN for PostgreSQL) |
138-
| **AUTH_MODE** | local | Authentication mode: `local` or `http_api` |
139-
| HTTP_API_URL | (none) | External auth API endpoint (required when AUTH_MODE=http_api)|
140-
| HTTP_API_TIMEOUT | 10s | HTTP API request timeout |
141-
| HTTP_API_INSECURE_SKIP_VERIFY | false | Skip TLS verification (dev/testing only) |
142-
| **TOKEN_PROVIDER_MODE** | local | Token provider mode: `local` or `http_api` |
143-
| TOKEN_API_URL | (none) | External token API endpoint (required when TOKEN_PROVIDER_MODE=http_api) |
144-
| TOKEN_API_TIMEOUT | 10s | Token API request timeout |
145-
| TOKEN_API_INSECURE_SKIP_VERIFY| false | Skip TLS verification for token API (dev/testing only) |
191+
| Variable | Default | Description |
192+
| ------------------------------ | ----------------------- | ------------------------------------------------------------------------ |
193+
| SERVER_ADDR | :8080 | Listen address |
194+
| BASE_URL | `http://localhost:8080` | Public URL for verification_uri |
195+
| JWT_SECRET | (default) | JWT signing key (used when TOKEN_PROVIDER_MODE=local) |
196+
| SESSION_SECRET | (default) | Cookie encryption key |
197+
| DATABASE_DRIVER | sqlite | Database driver ("sqlite" or "postgres") |
198+
| DATABASE_DSN | oauth.db | Connection string (path for SQLite, DSN for PostgreSQL) |
199+
| **AUTH_MODE** | local | Authentication mode: `local` or `http_api` |
200+
| HTTP_API_URL | (none) | External auth API endpoint (required when AUTH_MODE=http_api) |
201+
| HTTP_API_TIMEOUT | 10s | HTTP API request timeout |
202+
| HTTP_API_INSECURE_SKIP_VERIFY | false | Skip TLS verification (dev/testing only) |
203+
| **TOKEN_PROVIDER_MODE** | local | Token provider mode: `local` or `http_api` |
204+
| TOKEN_API_URL | (none) | External token API endpoint (required when TOKEN_PROVIDER_MODE=http_api) |
205+
| TOKEN_API_TIMEOUT | 10s | Token API request timeout |
206+
| TOKEN_API_INSECURE_SKIP_VERIFY | false | Skip TLS verification for token API (dev/testing only) |
207+
| **REFRESH_TOKEN_EXPIRATION** | 720h | Refresh token lifetime (default: 30 days) |
208+
| **ENABLE_REFRESH_TOKENS** | true | Feature flag to enable/disable refresh tokens |
209+
| **ENABLE_TOKEN_ROTATION** | false | Enable rotation mode (default: false, uses fixed mode) |
146210

147211
## Default Test Data
148212

@@ -260,6 +324,7 @@ TOKEN_API_INSECURE_SKIP_VERIFY=false
260324
**Token Generation Endpoint:** `POST {TOKEN_API_URL}/generate`
261325

262326
Request:
327+
263328
```json
264329
{
265330
"user_id": "user-uuid",
@@ -270,6 +335,7 @@ Request:
270335
```
271336

272337
Response (Success):
338+
273339
```json
274340
{
275341
"success": true,
@@ -283,6 +349,7 @@ Response (Success):
283349
```
284350

285351
Response (Error):
352+
286353
```json
287354
{
288355
"success": false,
@@ -293,13 +360,15 @@ Response (Error):
293360
**Token Validation Endpoint:** `POST {TOKEN_API_URL}/validate`
294361

295362
Request:
363+
296364
```json
297365
{
298366
"token": "eyJhbGc..."
299367
}
300368
```
301369

302370
Response (Valid):
371+
303372
```json
304373
{
305374
"valid": true,
@@ -314,6 +383,7 @@ Response (Valid):
314383
```
315384

316385
Response (Invalid):
386+
317387
```json
318388
{
319389
"valid": false,
@@ -324,6 +394,7 @@ Response (Invalid):
324394
**Response Requirements:**
325395

326396
Generation Response:
397+
327398
- `success` (required): Boolean indicating generation result
328399
- `access_token` (required when success=true): Non-empty JWT string
329400
- `token_type` (optional): Token type, defaults to "Bearer"
@@ -332,6 +403,7 @@ Generation Response:
332403
- `message` (optional): Error message when success=false
333404

334405
Validation Response:
406+
335407
- `valid` (required): Boolean indicating validation result
336408
- `user_id` (required when valid=true): User identifier from token
337409
- `client_id` (required when valid=true): Client identifier from token
@@ -361,18 +433,21 @@ JWT_SECRET=your-256-bit-secret-change-in-production
361433
### Token Provider Benefits
362434

363435
**Local Mode:**
436+
364437
- Simple setup, no external dependencies
365438
- Fast token operations
366439
- Self-contained deployment
367440

368441
**HTTP API Mode:**
442+
369443
- Centralized token services across multiple applications
370444
- Advanced key management and rotation
371445
- Custom signing algorithms (RS256, ES256)
372446
- Compliance requirements for token generation
373447
- Integration with existing IAM/PKI systems
374448

375449
**Why Local Storage is Retained:**
450+
376451
- Revocation: Users can revoke tokens via `/account/sessions` or `/oauth/revoke`
377452
- Management: Users can list active sessions
378453
- Auditing: Track when and for which clients tokens were issued

0 commit comments

Comments
 (0)