Skip to content

Commit bb11de6

Browse files
committed
feat: implement OAuth 2.0 Device Authorization Grant (RFC 8628) (graze-social#42)
Implements the OAuth 2.0 Device Authorization Grant flow according to RFC 8628 for devices with limited input capabilities or lack of web browser access. Key features: - Device authorization endpoint (`/oauth/device_authorization`) - Device code token endpoint integration - User device verification flow with HTML templates - Internal device authorization client management - Support for both SQLite and PostgreSQL storage backends - Comprehensive test coverage for device flow scenarios Additional improvements: - Enhanced OAuth client registration with application metadata - SQLX offline mode support for both database backends - OAuth scope validation and configuration - Client management API enhancements - Integration with existing private_key_jwt authentication support
1 parent 32ab683 commit bb11de6

46 files changed

Lines changed: 4759 additions & 473 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/device-flow-cli/Cargo.lock

Lines changed: 1915 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "device-flow-cli"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "A CLI example demonstrating OAuth 2.0 Device Authorization Grant (RFC 8628) with AIP"
6+
7+
[[bin]]
8+
name = "device-flow-cli"
9+
path = "src/main.rs"
10+
11+
[dependencies]
12+
anyhow = "1.0"
13+
clap = { version = "4.5", features = ["derive"] }
14+
reqwest = { version = "0.12", features = ["json"] }
15+
serde = { version = "1.0", features = ["derive"] }
16+
serde_json = "1.0"
17+
tokio = { version = "1.41", features = ["macros", "rt", "rt-multi-thread"] }
18+
tracing = "0.1"
19+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

examples/device-flow-cli/README.md

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# Device Flow CLI Example
2+
3+
This CLI example demonstrates how to implement the **OAuth 2.0 Device Authorization Grant** (RFC 8628) flow with AIP. This flow is perfect for devices with limited input capabilities (like smart TVs, IoT devices, or CLI tools) that need to authenticate users.
4+
5+
## 🚀 Quick Start
6+
7+
### Prerequisites
8+
9+
1. **AIP server running** - Make sure AIP is running on `http://localhost:8080`
10+
2. **Client management API enabled** - Set `ENABLE_CLIENT_API=true` in AIP (optional, for automatic registration)
11+
12+
### Setup
13+
14+
1. **Start AIP server:**
15+
```bash
16+
# From the AIP root directory
17+
cd ../../..
18+
ENABLE_CLIENT_API=true cargo run --bin aip
19+
```
20+
21+
2. **Register the OAuth client:**
22+
```bash
23+
# From the device-flow-cli directory
24+
./register-client.sh
25+
```
26+
27+
This will automatically register a public OAuth client with the device code grant type.
28+
29+
### Run the Example
30+
31+
```bash
32+
# From the device-flow-cli directory
33+
cargo run
34+
35+
# Or with custom parameters
36+
cargo run -- --aip-url http://localhost:8080 --client-id device-flow-cli-example --scope "atproto"
37+
```
38+
39+
### Expected Output
40+
41+
The CLI will guide you through the complete device flow:
42+
43+
```
44+
🚀 Starting OAuth 2.0 Device Authorization Grant flow
45+
📡 AIP Server: http://localhost:8080
46+
🆔 Client ID: device-flow-cli-example
47+
48+
📱 Device Authorization Required
49+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50+
📋 User Code: ABCD-1234
51+
🌐 Verification URL: http://localhost:8080/device
52+
🔗 Quick Link: http://localhost:8080/device?user_code=ABCD-1234
53+
⏰ Code expires in 600 seconds
54+
55+
🎯 Next Steps:
56+
1. Open http://localhost:8080/device in your browser
57+
2. Enter the user code: ABCD-1234
58+
3. Complete the authentication process
59+
4. Return here - the CLI will automatically detect completion
60+
61+
🔄 Starting token polling (interval: 5s, timeout: 600s)
62+
📡 Polling attempt #1
63+
⏳ Authorization still pending, waiting 5 seconds...
64+
📡 Polling attempt #2
65+
🎉 Access token obtained successfully!
66+
67+
✅ Authentication Successful!
68+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
69+
🎫 Access Token: FdTkXbhf...yMj6aQ
70+
⏰ Expires in: 3600 seconds
71+
🏷️ Token Type: bearer
72+
📋 Granted Scope: atproto
73+
74+
🔍 Testing Access Token...
75+
✅ Token is valid!
76+
👤 User: did:plc:abcd1234...
77+
78+
🎉 Device flow complete! You can now use the access token to make authenticated API calls.
79+
```
80+
81+
## 🔧 Configuration
82+
83+
### Command Line Arguments
84+
85+
| Argument | Description | Default |
86+
|----------|-------------|---------|
87+
| `--aip-url` | AIP server base URL | `http://localhost:8080` |
88+
| `--client-id` | OAuth client ID | `device-flow-cli-example` |
89+
| `--scope` | OAuth scope (optional) | None |
90+
91+
### Environment Variables
92+
93+
You can also use environment variables:
94+
95+
```bash
96+
export AIP_BASE_URL="http://localhost:8080"
97+
export CLIENT_ID="my-device-client"
98+
export OAUTH_SCOPE="atproto"
99+
100+
cargo run
101+
```
102+
103+
## 🔄 How It Works
104+
105+
The Device Authorization Grant follows these steps:
106+
107+
### 1. **Device Authorization Request**
108+
```http
109+
POST /oauth/device/authorization
110+
Content-Type: application/x-www-form-urlencoded
111+
112+
client_id=device-flow-cli-example&scope=atproto
113+
```
114+
115+
**Response:**
116+
```json
117+
{
118+
"device_code": "GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS",
119+
"user_code": "ABCD-1234",
120+
"verification_uri": "http://localhost:8080/device",
121+
"verification_uri_complete": "http://localhost:8080/device?user_code=ABCD-1234",
122+
"expires_in": 600,
123+
"interval": 5
124+
}
125+
```
126+
127+
### 2. **User Authorization**
128+
- User opens `verification_uri` in browser
129+
- Enters the `user_code`
130+
- Completes ATProtocol OAuth authentication
131+
- Authorizes the device
132+
133+
### 3. **Token Polling**
134+
The CLI polls the token endpoint until authorization is complete:
135+
136+
```http
137+
POST /oauth/token
138+
Content-Type: application/x-www-form-urlencoded
139+
140+
grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS&client_id=device-flow-cli-example
141+
```
142+
143+
**Pending Response:**
144+
```json
145+
{
146+
"error": "authorization_pending"
147+
}
148+
```
149+
150+
**Success Response:**
151+
```json
152+
{
153+
"access_token": "FdTkXbhfrQT_WS2cRFf-tX2TBWHlvPfrL-4XmyMj6aQ",
154+
"token_type": "bearer",
155+
"expires_in": 3600,
156+
"scope": "atproto"
157+
}
158+
```
159+
160+
### 4. **Token Usage**
161+
The CLI tests the access token by calling the session endpoint:
162+
163+
```http
164+
GET /api/atprotocol/session
165+
Authorization: Bearer FdTkXbhfrQT_WS2cRFf-tX2TBWHlvPfrL-4XmyMj6aQ
166+
```
167+
168+
## 🎯 Key Features
169+
170+
- **📱 User-friendly flow** - Clear instructions and progress indicators
171+
- **🔄 Automatic polling** - Handles token polling with proper intervals
172+
- **⚠️ Error handling** - Comprehensive error handling for all failure modes
173+
- **🎨 Rich output** - Colored output with emojis for better UX
174+
- **🔍 Token validation** - Tests the obtained token to ensure it works
175+
- **⚙️ Configurable** - Support for custom AIP URLs, client IDs, and scopes
176+
177+
## 🛠️ Error Handling
178+
179+
The CLI handles all standard OAuth 2.0 device flow errors:
180+
181+
- **`authorization_pending`** - User hasn't completed authorization yet
182+
- **`slow_down`** - Polling too fast, increases interval automatically
183+
- **`expired_token`** - Device code has expired
184+
- **`access_denied`** - User denied the authorization
185+
- **Network errors** - Connection failures, timeouts, etc.
186+
187+
## 🔐 Security Considerations
188+
189+
- **Public client** - This example uses a public OAuth client (no client secret)
190+
- **Short-lived codes** - Device codes expire in 10 minutes by default
191+
- **Rate limiting** - Respects polling intervals and slow-down responses
192+
- **No token storage** - Tokens are only displayed, not persisted
193+
194+
## 🏗️ Integration
195+
196+
To integrate this pattern into your own applications:
197+
198+
1. **Copy the core structs** - `DeviceAuthorizationRequest`, `TokenResponse`, etc.
199+
2. **Implement the three steps** - authorization request, polling, token usage
200+
3. **Handle errors gracefully** - Especially network and OAuth errors
201+
4. **Store tokens securely** - Use secure storage for production applications
202+
203+
## 📚 References
204+
205+
- [RFC 8628: OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628)
206+
- [AIP Documentation](../../../README.md)
207+
- [OAuth 2.0 Security Best Practices](https://tools.ietf.org/html/draft-ietf-oauth-security-topics)
208+
209+
## 🐛 Troubleshooting
210+
211+
### Client Not Found
212+
```
213+
Device authorization request failed: 400 Bad Request - {"error":"invalid_client"}
214+
```
215+
216+
**Solution:** Make sure your client is registered in AIP. Use the included registration script:
217+
218+
```bash
219+
# From the device-flow-cli directory
220+
./register-client.sh
221+
```
222+
223+
Or manually register using the AIP client management API.
224+
225+
### Connection Refused
226+
```
227+
Failed to send device authorization request: Connection refused
228+
```
229+
230+
**Solution:** Make sure AIP is running on the specified URL:
231+
232+
```bash
233+
# Start AIP server
234+
cargo run --bin aip
235+
```
236+
237+
### Invalid Token
238+
```
239+
❌ Token test failed: Access token test failed: 500 Internal Server Error
240+
```
241+
242+
**Solution:** This usually means the session is incomplete. Check AIP logs for more details.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/bin/bash
2+
# Register OAuth client for device-flow-cli example
3+
4+
set -e
5+
6+
# Configuration
7+
AIP_BASE_URL="${AIP_BASE_URL:-http://localhost:8080}"
8+
CLIENT_NAME="Device Flow CLI Example"
9+
10+
echo "🚀 Registering OAuth client for device-flow-cli example"
11+
echo "📡 AIP Server: $AIP_BASE_URL"
12+
echo
13+
14+
# Check if AIP is running
15+
echo "🔍 Checking if AIP server is running..."
16+
if ! curl -s "$AIP_BASE_URL/.well-known/oauth-authorization-server" > /dev/null; then
17+
echo "❌ Error: AIP server is not running at $AIP_BASE_URL"
18+
echo " Please start AIP first:"
19+
echo " cd ../../.. && cargo run --bin aip"
20+
exit 1
21+
fi
22+
echo "✅ AIP server is running"
23+
echo
24+
25+
# Register the client
26+
echo "📝 Registering OAuth client..."
27+
RESPONSE=$(curl -s -X POST "$AIP_BASE_URL/oauth/clients/register" \
28+
-H "Content-Type: application/json" \
29+
-d '{
30+
"client_name": "'"$CLIENT_NAME"'",
31+
"grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
32+
"response_types": ["device_code"],
33+
"token_endpoint_auth_method": "none",
34+
"application_type": "native",
35+
"software_id": "device-flow-cli-example",
36+
"software_version": "0.1.0",
37+
"scope": "atproto transition:generic"
38+
}' \
39+
-w "\nHTTP_STATUS:%{http_code}")
40+
41+
# Extract HTTP status
42+
HTTP_STATUS=$(echo "$RESPONSE" | tail -n1 | cut -d: -f2)
43+
BODY=$(echo "$RESPONSE" | sed '$ d')
44+
45+
if [ "$HTTP_STATUS" -eq 201 ] || [ "$HTTP_STATUS" -eq 200 ]; then
46+
echo "✅ Client registered successfully!"
47+
echo
48+
echo "📋 Client Details:"
49+
echo "$BODY" | jq '.'
50+
echo
51+
# Extract the generated client_id
52+
GENERATED_CLIENT_ID=$(echo "$BODY" | jq -r '.client_id')
53+
echo
54+
echo "🎯 You can now run the device flow example:"
55+
echo " cargo run -- --client-id $GENERATED_CLIENT_ID"
56+
elif [ "$HTTP_STATUS" -eq 409 ]; then
57+
echo "ℹ️ Client already exists (HTTP 409)"
58+
echo
59+
echo "🎯 You can run the device flow example:"
60+
echo " cargo run"
61+
echo " # Note: You'll need the previously registered client ID"
62+
elif [ "$HTTP_STATUS" -eq 404 ]; then
63+
echo "❌ Client registration endpoint not found (HTTP 404)"
64+
echo " This likely means client management API is disabled."
65+
echo " Enable it by setting: ENABLE_CLIENT_API=true"
66+
echo " Or manually register the client through AIP admin interface."
67+
exit 1
68+
else
69+
echo "❌ Client registration failed (HTTP $HTTP_STATUS)"
70+
echo "Response:"
71+
echo "$BODY" | jq '.' 2>/dev/null || echo "$BODY"
72+
exit 1
73+
fi

0 commit comments

Comments
 (0)