CookbookTokens & OAuth2
CLI / device authorization
Authenticate a headless CLI using device authorization grant
For CLIs (your dev tool, an admin script, a server with no browser), the Device Authorization Grant (RFC 8628) is the right pattern.
Flow
1. CLI: POST /oauth2/device → receive device_code + user_code + verification URL.
2. CLI: display "Open <URL>, enter code <user_code>".
3. User: opens URL in browser on phone/laptop, enters code.
4. User: authenticates via Olympus, approves consent.
5. CLI: meanwhile polls /oauth2/token with device_code until user completes.
6. CLI: receives access_token + refresh_token. Stores locally.Olympus support
Hydra supports the device authorization grant. Enable in hydra.yml:
oauth2:
device_authorization:
enabled: true
token_polling_interval: 5s
request_lifespan: 15mClient registration
In Athena → OAuth2 Clients:
- Type: Public (no secret).
- Grants:
urn:ietf:params:oauth:grant-type:device_code,refresh_token. - Scope: as needed.
CLI implementation (TypeScript)
async function deviceLogin() {
// 1. Request device code
const start = await fetch(`https://ciam.your-domain/oauth2/device/auth`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
scope: "openid profile email offline_access",
}),
}).then((r) => r.json());
console.log(`Open this URL in your browser:\n ${start.verification_uri_complete}`);
console.log(`Or visit ${start.verification_uri} and enter code: ${start.user_code}`);
// 2. Poll
const start_time = Date.now();
while (Date.now() - start_time < start.expires_in * 1000) {
await new Promise((r) => setTimeout(r, start.interval * 1000));
const tokenRes = await fetch(`https://ciam.your-domain/oauth2/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: start.device_code,
client_id: CLIENT_ID,
}),
});
if (tokenRes.ok) {
const tokens = await tokenRes.json();
saveTokensLocally(tokens);
console.log("Logged in!");
return tokens;
}
const error = await tokenRes.json();
if (error.error === "authorization_pending") continue; // Still waiting
if (error.error === "slow_down") { await new Promise((r) => setTimeout(r, 5000)); continue; }
throw new Error(error.error);
}
throw new Error("Login timed out");
}Token storage
Pick a location based on the user's OS:
- macOS:
~/Library/Application Support/<your-app>/credentials.jsonwith 600 permissions. - Linux:
~/.config/<your-app>/credentials.json. - Windows:
%APPDATA%\<your-app>\credentials.json.
Or use the OS keychain via libraries like keytar.
Refresh
Same as web flow, periodically refresh before expiry.
Use cases
octl deployflow if it ever needs to call Olympus APIs.- Custom CLIs your operators use.
- CI scripts that need to call Athena API on behalf of a real user (rare; usually M2M is better).
Related
- Integrate, OAuth2 PKCE, alternative for CLIs.
- Integrate, OAuth2 client credentials, for fully unattended use.
- Cookbook, Validate access token (Node)