Device Authorization Flow (RFC 8628)
Authenticate desktop apps, CLI tools, and devices without browser redirect capability using Sigma Identity.
When to Use
- Desktop apps (Electron, Tauri) that need cross-origin auth
- CLI tools that can't open browser windows reliably
- TV/IoT apps with limited input capability
- Any app where OAuth redirect flow is impractical
Flow Overview
1. App requests device code from Sigma
2. User visits verification URL in browser
3. User enters code and approves
4. App polls for token (receives access_token when approved)
5. App fetches user info with Bearer token
Endpoints
All endpoints are on https://auth.sigmaidentity.com:
| Endpoint | Method | Purpose |
|---|---|---|
/api/auth/device/code |
POST | Get device_code and user_code |
/api/auth/device/token |
POST | Poll for access_token |
/api/auth/oauth2/userinfo |
GET | Fetch user info with Bearer token |
Implementation
Step 1: Request Device Code
const authUrl = "https://auth.sigmaidentity.com";
const response = await fetch(`${authUrl}/api/auth/device/code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: "your-app-name",
scope: "openid profile",
}),
});
const deviceAuth = await response.json();
// {
// device_code: "abc123...",
// user_code: "ABCD-1234",
// verification_uri: "https://auth.sigmaidentity.com/device",
// verification_uri_complete: "https://auth.sigmaidentity.com/device?code=ABCD-1234",
// expires_in: 900,
// interval: 5
// }
Step 2: Show User Code & Open Browser
Display the user code prominently and open the verification URL:
// Display to user
console.log(`Enter code: ${deviceAuth.user_code}`);
console.log(`Visit: ${deviceAuth.verification_uri}`);
// Or open browser directly with pre-filled code
openBrowser(deviceAuth.verification_uri_complete);
Step 3: Poll for Token
Poll at the specified interval until user approves:
async function pollForToken(deviceCode: string, interval: number): Promise<string> {
const pollInterval = interval * 1000;
while (true) {
await new Promise(r => setTimeout(r, pollInterval));
const response = await fetch(`${authUrl}/api/auth/device/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: deviceCode,
client_id: "your-app-name",
}),
});
const data = await response.json();
if (data.access_token) {
return data.access_token;
}
// Handle error cases per RFC 8628
switch (data.error) {
case "authorization_pending":
continue; // Keep polling
case "slow_down":
pollInterval += 5000; // Increase interval
continue;
case "expired_token":
throw new Error("Code expired - please restart");
case "access_denied":
throw new Error("User denied authorization");
default:
throw new Error(data.error || "Unknown error");
}
}
}
Step 4: Fetch User Info
Use the access token to get user details:
const userInfoRes = await fetch(`${authUrl}/api/auth/oauth2/userinfo`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const userInfo = await userInfoRes.json();
// {
// sub: "user-id-123",
// name: "satoshi",
// email: "user@example.com",
// picture: "https://...",
// bap_id: "bap-identity-key", // Sigma-specific
// pubkey: "03abc..." // Sigma-specific
// }
User Info Response Fields
Standard OIDC claims:
sub- User ID (use as userId)name- Display nameemail- Email addresspicture- Avatar URL
Sigma-specific claims:
bap_id- BAP identity key (use as bapId)pubkey- User's public keybap- Full BAP profile (JSON string)
CORS Configuration
For cross-origin requests from localhost or desktop apps, these endpoints allow any origin:
/api/auth/device/code/api/auth/device/token/api/auth/oauth2/userinfo
Security is enforced via the access token, not origin validation.
Error Handling
| Error | Meaning | Action |
|---|---|---|
authorization_pending |
User hasn't approved yet | Continue polling |
slow_down |
Polling too fast | Increase interval by 5s |
expired_token |
Code expired (15 min default) | Restart flow |
access_denied |
User denied request | Show error to user |
Complete Example
async function deviceAuth() {
const authUrl = "https://auth.sigmaidentity.com";
// 1. Get device code
const codeRes = await fetch(`${authUrl}/api/auth/device/code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ client_id: "my-app", scope: "openid profile" }),
});
const deviceAuth = await codeRes.json();
// 2. Show code to user
console.log(`Code: ${deviceAuth.user_code}`);
openBrowser(deviceAuth.verification_uri_complete);
// 3. Poll for token
const accessToken = await pollForToken(
deviceAuth.device_code,
deviceAuth.interval
);
// 4. Fetch user info
const userRes = await fetch(`${authUrl}/api/auth/oauth2/userinfo`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const user = await userRes.json();
return {
userId: user.sub,
bapId: user.bap_id,
name: user.name,
image: user.picture,
};
}
Security Considerations
- Device codes expire after 15 minutes (configurable)
- Polling is rate-limited (5s default interval)
- Access tokens are short-lived (1 hour default)
- No client_secret required - security comes from user approval
- User must explicitly approve on auth.sigmaidentity.com
Tauri/Desktop Integration
For Tauri apps, use the shell plugin to open the browser:
// In Tauri command
use tauri_plugin_shell::ShellExt;
app.shell().open(verification_url, None)?;
Or from JavaScript:
import { open } from "@tauri-apps/plugin-shell";
await open(deviceAuth.verification_uri_complete);