# 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.

!!! tip "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](../getting-started/api-authentication.md); no admin session is needed.

```bash
# 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"
```
```json
{
  "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:

```bash
# 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
```
```json
{"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:

```bash
# 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.

!!! note "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)

```text
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)

```text
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)                               │
```

!!! warning "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:

```text
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](oidc-quickstart.md) — the underlying authorization-code + PKCE flow.
- [Workload identity](workload-identity.md) — keyless machine tokens for Kubernetes / CI.
- [Applications](../manage/applications.md) — register the client and its grants.
