Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,915 changes: 1,915 additions & 0 deletions examples/device-flow-cli/Cargo.lock

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions examples/device-flow-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "device-flow-cli"
version = "0.1.0"
edition = "2021"
description = "A CLI example demonstrating OAuth 2.0 Device Authorization Grant (RFC 8628) with AIP"

[[bin]]
name = "device-flow-cli"
path = "src/main.rs"

[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.41", features = ["macros", "rt", "rt-multi-thread"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
242 changes: 242 additions & 0 deletions examples/device-flow-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
# Device Flow CLI Example

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.

## πŸš€ Quick Start

### Prerequisites

1. **AIP server running** - Make sure AIP is running on `http://localhost:8080`
2. **Client management API enabled** - Set `ENABLE_CLIENT_API=true` in AIP (optional, for automatic registration)

### Setup

1. **Start AIP server:**
```bash
# From the AIP root directory
cd ../../..
ENABLE_CLIENT_API=true cargo run --bin aip
```

2. **Register the OAuth client:**
```bash
# From the device-flow-cli directory
./register-client.sh
```

This will automatically register a public OAuth client with the device code grant type.

### Run the Example

```bash
# From the device-flow-cli directory
cargo run

# Or with custom parameters
cargo run -- --aip-url http://localhost:8080 --client-id device-flow-cli-example --scope "atproto"
```

### Expected Output

The CLI will guide you through the complete device flow:

```
πŸš€ Starting OAuth 2.0 Device Authorization Grant flow
πŸ“‘ AIP Server: http://localhost:8080
πŸ†” Client ID: device-flow-cli-example

πŸ“± Device Authorization Required
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
πŸ“‹ User Code: ABCD-1234
🌐 Verification URL: http://localhost:8080/device
πŸ”— Quick Link: http://localhost:8080/device?user_code=ABCD-1234
⏰ Code expires in 600 seconds

🎯 Next Steps:
1. Open http://localhost:8080/device in your browser
2. Enter the user code: ABCD-1234
3. Complete the authentication process
4. Return here - the CLI will automatically detect completion

πŸ”„ Starting token polling (interval: 5s, timeout: 600s)
πŸ“‘ Polling attempt #1
⏳ Authorization still pending, waiting 5 seconds...
πŸ“‘ Polling attempt #2
πŸŽ‰ Access token obtained successfully!

βœ… Authentication Successful!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎫 Access Token: FdTkXbhf...yMj6aQ
⏰ Expires in: 3600 seconds
🏷️ Token Type: bearer
πŸ“‹ Granted Scope: atproto

πŸ” Testing Access Token...
βœ… Token is valid!
πŸ‘€ User: did:plc:abcd1234...

πŸŽ‰ Device flow complete! You can now use the access token to make authenticated API calls.
```

## πŸ”§ Configuration

### Command Line Arguments

| Argument | Description | Default |
|----------|-------------|---------|
| `--aip-url` | AIP server base URL | `http://localhost:8080` |
| `--client-id` | OAuth client ID | `device-flow-cli-example` |
| `--scope` | OAuth scope (optional) | None |

### Environment Variables

You can also use environment variables:

```bash
export AIP_BASE_URL="http://localhost:8080"
export CLIENT_ID="my-device-client"
export OAUTH_SCOPE="atproto"

cargo run
```

## πŸ”„ How It Works

The Device Authorization Grant follows these steps:

### 1. **Device Authorization Request**
```http
POST /oauth/device/authorization
Content-Type: application/x-www-form-urlencoded

client_id=device-flow-cli-example&scope=atproto
```

**Response:**
```json
{
"device_code": "GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "ABCD-1234",
"verification_uri": "http://localhost:8080/device",
"verification_uri_complete": "http://localhost:8080/device?user_code=ABCD-1234",
"expires_in": 600,
"interval": 5
}
```

### 2. **User Authorization**
- User opens `verification_uri` in browser
- Enters the `user_code`
- Completes ATProtocol OAuth authentication
- Authorizes the device

### 3. **Token Polling**
The CLI polls the token endpoint until authorization is complete:

```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS&client_id=device-flow-cli-example
```

**Pending Response:**
```json
{
"error": "authorization_pending"
}
```

**Success Response:**
```json
{
"access_token": "FdTkXbhfrQT_WS2cRFf-tX2TBWHlvPfrL-4XmyMj6aQ",
"token_type": "bearer",
"expires_in": 3600,
"scope": "atproto"
}
```

### 4. **Token Usage**
The CLI tests the access token by calling the session endpoint:

```http
GET /api/atprotocol/session
Authorization: Bearer FdTkXbhfrQT_WS2cRFf-tX2TBWHlvPfrL-4XmyMj6aQ
```

## 🎯 Key Features

- **πŸ“± User-friendly flow** - Clear instructions and progress indicators
- **πŸ”„ Automatic polling** - Handles token polling with proper intervals
- **⚠️ Error handling** - Comprehensive error handling for all failure modes
- **🎨 Rich output** - Colored output with emojis for better UX
- **πŸ” Token validation** - Tests the obtained token to ensure it works
- **βš™οΈ Configurable** - Support for custom AIP URLs, client IDs, and scopes

## πŸ› οΈ Error Handling

The CLI handles all standard OAuth 2.0 device flow errors:

- **`authorization_pending`** - User hasn't completed authorization yet
- **`slow_down`** - Polling too fast, increases interval automatically
- **`expired_token`** - Device code has expired
- **`access_denied`** - User denied the authorization
- **Network errors** - Connection failures, timeouts, etc.

## πŸ” Security Considerations

- **Public client** - This example uses a public OAuth client (no client secret)
- **Short-lived codes** - Device codes expire in 10 minutes by default
- **Rate limiting** - Respects polling intervals and slow-down responses
- **No token storage** - Tokens are only displayed, not persisted

## πŸ—οΈ Integration

To integrate this pattern into your own applications:

1. **Copy the core structs** - `DeviceAuthorizationRequest`, `TokenResponse`, etc.
2. **Implement the three steps** - authorization request, polling, token usage
3. **Handle errors gracefully** - Especially network and OAuth errors
4. **Store tokens securely** - Use secure storage for production applications

## πŸ“š References

- [RFC 8628: OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628)
- [AIP Documentation](../../../README.md)
- [OAuth 2.0 Security Best Practices](https://tools.ietf.org/html/draft-ietf-oauth-security-topics)

## πŸ› Troubleshooting

### Client Not Found
```
Device authorization request failed: 400 Bad Request - {"error":"invalid_client"}
```

**Solution:** Make sure your client is registered in AIP. Use the included registration script:

```bash
# From the device-flow-cli directory
./register-client.sh
```

Or manually register using the AIP client management API.

### Connection Refused
```
Failed to send device authorization request: Connection refused
```

**Solution:** Make sure AIP is running on the specified URL:

```bash
# Start AIP server
cargo run --bin aip
```

### Invalid Token
```
❌ Token test failed: Access token test failed: 500 Internal Server Error
```

**Solution:** This usually means the session is incomplete. Check AIP logs for more details.
73 changes: 73 additions & 0 deletions examples/device-flow-cli/register-client.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/bash
# Register OAuth client for device-flow-cli example

set -e

# Configuration
AIP_BASE_URL="${AIP_BASE_URL:-http://localhost:8080}"
CLIENT_NAME="Device Flow CLI Example"

echo "πŸš€ Registering OAuth client for device-flow-cli example"
echo "πŸ“‘ AIP Server: $AIP_BASE_URL"
echo

# Check if AIP is running
echo "πŸ” Checking if AIP server is running..."
if ! curl -s "$AIP_BASE_URL/.well-known/oauth-authorization-server" > /dev/null; then
echo "❌ Error: AIP server is not running at $AIP_BASE_URL"
echo " Please start AIP first:"
echo " cd ../../.. && cargo run --bin aip"
exit 1
fi
echo "βœ… AIP server is running"
echo

# Register the client
echo "πŸ“ Registering OAuth client..."
RESPONSE=$(curl -s -X POST "$AIP_BASE_URL/oauth/clients/register" \
-H "Content-Type: application/json" \
-d '{
"client_name": "'"$CLIENT_NAME"'",
"grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
"response_types": ["device_code"],
"token_endpoint_auth_method": "none",
"application_type": "native",
"software_id": "device-flow-cli-example",
"software_version": "0.1.0",
"scope": "atproto transition:generic"
}' \
-w "\nHTTP_STATUS:%{http_code}")

# Extract HTTP status
HTTP_STATUS=$(echo "$RESPONSE" | tail -n1 | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '$ d')

if [ "$HTTP_STATUS" -eq 201 ] || [ "$HTTP_STATUS" -eq 200 ]; then
echo "βœ… Client registered successfully!"
echo
echo "πŸ“‹ Client Details:"
echo "$BODY" | jq '.'
echo
# Extract the generated client_id
GENERATED_CLIENT_ID=$(echo "$BODY" | jq -r '.client_id')
echo
echo "🎯 You can now run the device flow example:"
echo " cargo run -- --client-id $GENERATED_CLIENT_ID"
elif [ "$HTTP_STATUS" -eq 409 ]; then
echo "ℹ️ Client already exists (HTTP 409)"
echo
echo "🎯 You can run the device flow example:"
echo " cargo run"
echo " # Note: You'll need the previously registered client ID"
elif [ "$HTTP_STATUS" -eq 404 ]; then
echo "❌ Client registration endpoint not found (HTTP 404)"
echo " This likely means client management API is disabled."
echo " Enable it by setting: ENABLE_CLIENT_API=true"
echo " Or manually register the client through AIP admin interface."
exit 1
else
echo "❌ Client registration failed (HTTP $HTTP_STATUS)"
echo "Response:"
echo "$BODY" | jq '.' 2>/dev/null || echo "$BODY"
exit 1
fi
Loading
Loading