Olympus Docs
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: 15m

Client 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.json with 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 deploy flow 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).

On this page