CLI & native-app authentication¶
Give a command-line tool, desktop app, or any other native app a first-class sign-in: a browser login (authorization code + PKCE), a headless device login (RFC 8628), and a long-lived, silently-refreshed session — no secret baked into the binary.
What it is¶
A native app is a public OAuth client: it ships to end users, so it can't keep a client secret. HelixIAM authenticates it the way the major clouds do their CLIs (az, gcloud, aws sso):
| Flow | When to use | Grant |
|---|---|---|
| Browser | The machine has a browser (a developer laptop) | authorization_code + PKCE, loopback redirect (RFC 8252) |
| Device | Headless — SSH sessions, CI shells, servers | urn:ietf:params:oauth:grant-type:device_code (RFC 8628) |
| Service account | Automation / CI with no human | client_credentials |
All three mint a normal Helix access token carrying the caller's identity (sub) and roles (realm_access.roles / resource_access), so your API authorizes them exactly like any other token.
Stay signed in for 30 days
Native-app clients may hold a rotating refresh token, so your tool can renew the access token silently in the background. Used within the refresh window (30 days on the built-in helix-cli client) you stay signed in; leave it idle past the window and you simply sign in again. Rotation (a new refresh token on every renewal, the old one invalidated) keeps this OAuth 2.1-safe.
The built-in helix-cli client¶
Every realm is seeded with a public client for command-line tools, helix-cli ("HelixIAM CLI"): no
secret, PKCE required, grants authorization_code, refresh_token and
urn:ietf:params:oauth:grant-type:device_code, scopes openid profile email offline_access, loopback
redirect URIs http://127.0.0.1/callback, http://localhost/callback and http://127.0.0.1:8765/callback,
and a 30-day refresh-token lifespan with rotation. Use it for your own scripts and tools, or register your own
client the same way (below). HelixIAM does not ship a CLI binary; the flows below are plain
OAuth and work with any HTTP client.
Try the device flow with curl¶
The examples assume $HELIX_URL / $REALM from Authenticating to the API; no admin session is needed.
# 1. Ask for a device code
curl -s -X POST "$HELIX_URL/realms/$REALM/oauth2/device_authorization" \
-d client_id=helix-cli \
-d scope="profile offline_access"
{
"user_code": "XWFH-BFDW",
"device_code": "pbWxrYkGLCSfblhYD4dRAKWs…",
"verification_uri_complete": "http://localhost:8080/realms/master/activate?user_code=XWFH-BFDW",
"verification_uri": "http://localhost:8080/realms/master/activate",
"expires_in": 300
}
Show the user the user_code and the verification_uri (or open verification_uri_complete). They sign in
there and approve. Meanwhile, poll the token endpoint:
# 2. Poll until the user has approved
curl -s -X POST "$HELIX_URL/realms/$REALM/oauth2/token" \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d device_code="$DEVICE_CODE" \
-d client_id=helix-cli
{"error":"authorization_pending","error_uri":"https://datatracker.ietf.org/doc/html/rfc8628#section-3.5"}
Once approved, the same call returns a Bearer access token (expires_in 3599) and a refresh token, with
the approved scope profile offline_access. Renew silently with the refresh token:
# 3. Silent refresh — returns a new access token and a NEW refresh token
curl -s -X POST "$HELIX_URL/realms/$REALM/oauth2/token" \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN" \
-d client_id=helix-cli
Each refresh rotates the refresh token: reusing the previous one is refused with {"error":"invalid_grant"}.
Enable it for your own native app¶
The capability is not specific to helix-cli — any public client you register can use it. Two steps:
-
Register a public client and mark it as a native app by enabling the device grant alongside
authorization_codeandrefresh_token. In the console, on the client's Credentials tab, tick Authorization code (browser login), Refresh token and Device authorization (CLI / smart TV). Register a loopback redirect URI (http://127.0.0.1/callback) for the browser flow. Leave it public (no secret) so PKCE is enforced. -
Set the refresh-token lifespan you want (the idle window) on the same tab — Refresh token lifespan (seconds), e.g.
2592000for 30 days — and leave Reuse refresh tokens off so tokens rotate.
That's it. Your app now gets browser + device login and a rotating, long-lived session, like the built-in helix-cli client.
Why this needs a native-app marker
By default the OAuth2 authorization server Helix is built on (Spring Security 7) — following the OAuth 2.1 browser-app guidance — does not issue refresh tokens to public clients on the authorization-code grant, because a refresh token in browser JavaScript is a risk. Helix relaxes this only for native apps (identified by the device grant, per RFC 8252), where a rotating refresh token is safe and expected. Browser SPAs keep the strict, no-refresh behaviour.
How the flows work¶
Browser (authorization code + PKCE + loopback)¶
CLI Helix realm Browser
│ start loopback :<random> │
│ open ─────────────────────────────────────────────────────────────────► │
│ GET /realms/{realm}/oauth2/authorize?…code_challenge… │
│ sign in ◄───────────────────────────┤
│ ◄────────── 302 http://127.0.0.1:<port>/callback?code=…&state=… │
│ POST /oauth2/token (code + code_verifier, client_id, no secret) │
│ ◄────────── access_token (+ refresh_token for native apps) │
Device (RFC 8628)¶
CLI Helix realm
│ POST /oauth2/device_authorization (client_id) │
│ ◄────────── device_code, user_code, verification_uri, verification_uri_complete
│ show the user the code + URL │
│ POST /oauth2/token (grant_type=device_code, device_code, client_id) ──► │ authorization_pending…
│ … user approves at /activate … │
│ ◄────────── access_token (+ refresh_token) │
The device grant is OAuth2-only
The device flow does not accept the openid scope — asking for it returns invalid_scope (it issues access tokens, not ID tokens). Request scopes such as profile offline_access; the access token still carries the user's sub and roles.
Silent refresh¶
When the access token is near expiry the CLI renews it in the background:
CLI Helix realm
│ POST /oauth2/token (grant_type=refresh_token, refresh_token, client_id) │
│ ◄────────── new access_token + NEW refresh_token (old one invalidated) │
Each renewal issues a fresh refresh token and slides the idle window forward.
Related¶
- OIDC quickstart — the underlying authorization-code + PKCE flow.
- Workload identity — keyless machine tokens for Kubernetes / CI.
- Applications — register the client and its grants.