Skip to content

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:

  1. Register a public client and mark it as a native app by enabling the device grant alongside authorization_code and refresh_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.

  2. Set the refresh-token lifespan you want (the idle window) on the same tab — Refresh token lifespan (seconds), e.g. 2592000 for 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.