<!-- Source: https://docs.geopera.com/api-reference/sdks/cli/authentication · Markdown for LLMs -->

# CLI authentication & login

The `geopera` CLI signs in with the OAuth 2.0 device flow by default, or with a `gpra_` API key for headless use, stores credentials in a per-profile file, and chooses automatically between an `Authorization: Bearer` token and an `X-API-Key` header on every request.

## How the CLI handles auth

The CLI is a thin auth-and-dispatch shell over the published [`geopera` SDK](/api-reference/sdks). The only CLI-resident logic is authentication: the device flow, token refresh and rotation, and choosing between a `Bearer` token and an `X-API-Key` header. Everything else is reached through structured commands that mirror each [operation](/api-reference/operations) — for example `geopera catalog search` or `geopera orders archive place`. For the underlying token model — session tokens, minted `gpra_` API keys, and how the API validates them — see [Authentication](/api-reference/authentication).

There are exactly five auth-related commands, and they are the only commands the CLI implements itself:

| Command          | What it does                                                                 |
| ---------------- | ---------------------------------------------------------------------------- |
| `geopera login`  | Sign in via the device flow, or store an API key with `--api-key`.           |
| `geopera logout` | Clear the active profile's stored credentials (and revoke an OAuth session). |
| `geopera whoami` | Resolve the active profile and print the authenticated principal.            |

(`login`, `logout`, and `whoami` are the three you will use daily; every other `geopera <resource> <action>` command is a structured wrapper over an operation, and the hidden `geopera op` is the raw escape hatch.)

## Interactive login (default)

Running `geopera login` with no flags starts the **OAuth 2.0 Device Authorization Grant** ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)) with PKCE. This is the right choice on a workstation where a browser is available.

```bash
geopera login
```

The flow is:

1. The CLI generates a PKCE verifier and an `S256` challenge, then calls the device-authorization endpoint as a public native client (client id `geopera-cli`, no secret).
2. It prints a verification URL and a short **user code**, and — unless `--no-browser` is set — opens the URL in your default browser. The CLI prefers the server's `verification_uri_complete` (the URL with the code already embedded) and falls back to the bare `verification_uri`.
3. You confirm the code in the browser. Meanwhile the CLI polls the token endpoint on the server-supplied `interval` (default 5 seconds), backing off by 5 seconds whenever the server replies `slow_down`, and treating `authorization_pending` as "keep waiting".
4. On success it stores the access and refresh tokens, then confirms with a `userinfo` round-trip and prints the resolved principal.

Typical output:

```bash
  To sign in, visit:
    https://api.geopera.com/device?user_code=WDJB-MJHT

  And confirm this code:
    WDJB-MJHT

Logged in as 3f6c1b2a-... (profile 'default').
```

While it polls, a small spinner renders on stderr (only when stderr is a TTY), so the spinner never pollutes piped or redirected output.

### Login options

| Option             | Default                   | Description                                                                                                            |
| ------------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--no-browser`     | off                       | Do not auto-open the verification URL. Print the code and URL only.                                                    |
| `--scope <scope>`  | `openid profile`          | OAuth scope to request during the device flow.                                                                         |
| `--api-key <key>`  | —                         | Skip the device flow and store an API key (see [Headless login](#headless-login-api-key)). Use `-` to read from stdin. |
| `--api-url <url>`  | `https://api.geopera.com` | API base URL override (env: `GEOPERA_API_URL`).                                                                        |
| `--profile <name>` | `default`                 | Stored identity to write to (env: `GEOPERA_PROFILE`).                                                                  |

Use `--no-browser` on a remote box where the browser you want is on a different machine — copy the URL across yourself:

```bash
geopera login --no-browser
```

Request a different scope set with `--scope` (space-separated, quoted):

```bash
geopera login --scope "openid profile orders:write"
```

The scope you request is stored alongside the token and reported by `geopera whoami`. For the full list of what each scope grants, see [Scopes](/api-reference/scopes).

### Device-flow timing and termination

The CLI honours the three timing fields the server returns from the device-authorization step:

| Field                       | Meaning                        | CLI behaviour                                                            |
| --------------------------- | ------------------------------ | ------------------------------------------------------------------------ |
| `interval`                  | Seconds to wait between polls  | Sleeps this long between token polls (default 5 if omitted).             |
| `expires_in`                | Lifetime of the device request | Used as the polling deadline (default 600, about 10 minutes).            |
| `verification_uri_complete` | URL with the code pre-filled   | Opened in the browser and shown first; falls back to `verification_uri`. |

The poll loop ends in one of four ways:

- **Success** — the token endpoint returns an access/refresh token pair; the CLI stores it and prints `Logged in as ...`.
- **Denied** — you reject the request in the browser; the CLI reports `Login was denied in the browser.` and exits non-zero.
- **Expired** — the device request lifetime elapses before you confirm; the server returns `expired_token`, or the CLI hits its own deadline and reports `Login timed out before authorization completed.` Rerun `geopera login`.
- **Unsupported** — if the device-authorization endpoint itself is unavailable, the CLI reports `Device authorization failed. The server may not support the device flow yet` and suggests `geopera login --api-key <key>`.

## Headless login (API key)

For CI, servers, and other non-interactive contexts, skip the device flow and store a minted API key with `--api-key`. Geopera API keys start with the `gpra_` prefix, and the key itself is the credential — there is no token exchange, OIDC handshake, or refresh step for API keys.

```bash
geopera login --api-key gpra_xxxxxxxxxxxxxxxxxxxxxxxx
```

To keep the key out of your shell history, pass `-` and feed it on stdin:

```bash
echo "$GEOPERA_API_KEY" | geopera login --api-key -
```

The CLI validates the key with a `userinfo` call before storing it. If validation succeeds it prints the resolved principal id:

```bash
Logged in as svc_8a1c... (API key, profile 'default').
```

If validation fails the key is **not** saved and the command exits non-zero:

```bash
Error: API key validation failed: ...
```

An empty value (for example an empty stdin pipe) is rejected before any network call with `Error: No API key provided.`

A key without the `gpra_` prefix still works, but the CLI prints a note to stderr that new Geopera keys are expected to start with `gpra_`:

```bash
Note: this key has no 'gpra_' prefix. It will still work, but new Geopera keys are expected to start with 'gpra_'.
```

To learn how to mint keys and which [scopes](/api-reference/scopes) they carry, see [Authentication](/api-reference/authentication).

### How keys are sent

API keys are sent in the **`X-API-Key`** header with **no prefix** — not as a `Bearer` token. The CLI selects the header automatically based on how you logged in:

| Credential type                       | Header sent                            |
| ------------------------------------- | -------------------------------------- |
| OAuth (device flow / browser sign-in) | `Authorization: Bearer <access_token>` |
| API key (`gpra_...`)                  | `X-API-Key: gpra_...`                  |

This selection is made per request from the stored credential `type`, so you never set the header yourself.

## Ephemeral tokens without storing

To use a token for a single invocation without writing it to the credential file — handy in CI — set `GEOPERA_API_TOKEN`. The CLI inspects the value:

- A value starting with `gpra_` is treated as an **API key** and sent as `X-API-Key`.
- Any other value is treated as an opaque **session token** and sent as `Authorization: Bearer ...`.

```bash
export GEOPERA_API_TOKEN="gpra_xxxxxxxxxxxxxxxxxxxxxxxx"
geopera whoami
```

This override takes precedence over any stored profile, is never persisted, and never refreshes (the CLI has no refresh token for it, so it is used verbatim). It is the cleanest way to run the CLI on a host that should hold no long-lived state on disk.

## Inspecting the current principal

`geopera whoami` resolves the active profile (refreshing the OAuth token first if needed) and prints the authenticated principal, org, and scopes from a live `userinfo` call.

```bash
geopera whoami
```

```bash
sub:             3f6c1b2a-9a4e-4c0f-8b21-1d2e3f4a5b6c
principal_type:  user
org_id:          org_8a1c...
scope:           openid profile
api_url:         https://api.geopera.com
profile:         default
```

The fields map directly onto the `userinfo` payload:

| Line             | Source field             | Notes                                                            |
| ---------------- | ------------------------ | ---------------------------------------------------------------- |
| `sub`            | `sub`                    | The principal's stable subject id.                               |
| `principal_type` | `geopera_principal_type` | `user` for a person, or a service-principal type for an API key. |
| `org_id`         | `geopera_org_id`         | The organisation the principal belongs to.                       |
| `scope`          | `scope`                  | Scopes actually granted on this credential.                      |
| `api_url`        | resolved at call time    | The base URL the request went to.                                |
| `profile`        | resolved at call time    | The active profile name.                                         |

Add `--json` to print the raw `userinfo` payload instead of the formatted summary — useful for scripting:

```bash
geopera whoami --json
```

```json
{
	"sub": "3f6c1b2a-9a4e-4c0f-8b21-1d2e3f4a5b6c",
	"geopera_principal_type": "user",
	"geopera_org_id": "org_8a1c...",
	"scope": "openid profile"
}
```

`whoami` accepts `--profile` and `--api-url` like the other commands, so you can check a non-active identity without switching:

```bash
geopera whoami --profile staging
```

If there are no credentials for the active profile, `whoami` exits non-zero with `Not logged in (profile '...'). Run 'geopera login' first.`

## Token refresh and rotation

OAuth access tokens refresh automatically, so you rarely need to think about it. There are two paths:

- **Proactive** — before each command, if the stored access token is missing an expiry or is within **30 seconds** of expiring, the CLI exchanges the refresh token for a fresh access token first, then runs the command.
- **Reactive** — if a request returns `401` anyway (for example, the token died between the proactive check and the call, or was revoked server-side), the CLI refreshes **once** and retries the request. If the retry also fails, the error is surfaced.

Both paths use **refresh-token rotation**: a new refresh token is issued on every use, and the CLI persists both the new access token and the new refresh token atomically. If the response omits a new refresh token, the CLI keeps the previous one as a fallback.

If the refresh token has been revoked or is missing, the CLI reports that your session expired and asks you to run `geopera login` again:

```bash
Error: Token refresh failed (your session may have been revoked). Run 'geopera login' to sign in again.
```

API keys do not refresh — a `401` on an API-key credential is terminal. The CLI does not retry; it reports:

```bash
Error: API key rejected (401). Check the key or create a new one.
```

## Profiles

Every credential lives under a named **profile**, letting you keep separate identities (for example production and staging, or a personal login and a service key) side by side. The active profile is resolved with this precedence, highest first:

1. the `--profile` flag
2. the `GEOPERA_PROFILE` environment variable
3. the built-in default, `default`

Each profile also carries its own `api_url`, resolved independently:

1. the `--api-url` flag
2. the `GEOPERA_API_URL` environment variable
3. the `api_url` stored in the active profile
4. the built-in default, `https://api.geopera.com`

Sign two profiles in against different endpoints:

```bash
geopera login --profile default
geopera login --profile staging --api-url https://staging.api.geopera.com --api-key -
```

Then target either one per command:

```bash
geopera whoami --profile staging
GEOPERA_PROFILE=staging geopera orders list
```

Because each profile remembers its `api_url`, a later `geopera login --profile staging` reuses the staging endpoint without you re-specifying `--api-url`.

## Where credentials are stored

Credentials live in a single JSON file at `~/.config/geopera/credentials.json`. The directory is created with mode `0700` and the file is written atomically with mode `0600` (the temp file is opened `0600` from the start), so secrets are never briefly world-readable. Writes are atomic via a temp-file-then-rename, so a crash mid-write cannot corrupt the store or leak a partial secret.

Each top-level key is a **profile** holding an `api_url` and an `auth` block. OAuth and API-key profiles have different `auth` shapes:

```json
{
	"default": {
		"api_url": "https://api.geopera.com",
		"auth": {
			"type": "oauth",
			"access_token": "...",
			"refresh_token": "...",
			"expires_at": 1750000000,
			"scope": "openid profile",
			"issuer": "https://api.geopera.com"
		}
	},
	"staging": {
		"api_url": "https://staging.api.geopera.com",
		"auth": { "type": "api_key", "api_key": "gpra_..." }
	}
}
```

| Field           | Profiles | Meaning                                                            |
| --------------- | -------- | ------------------------------------------------------------------ |
| `type`          | both     | `oauth` or `api_key`; selects the auth header at request time.     |
| `access_token`  | oauth    | The bearer access token.                                           |
| `refresh_token` | oauth    | Rotated on every refresh; used to mint new access tokens.          |
| `expires_at`    | oauth    | Unix epoch seconds; drives the proactive 30-second refresh window. |
| `scope`         | oauth    | Scopes granted at login.                                           |
| `issuer`        | oauth    | The API base URL the token was issued by.                          |
| `api_key`       | api_key  | The `gpra_` key, sent verbatim as `X-API-Key`.                     |

You do not edit this file by hand in normal use — `login` and `logout` manage it for you — but its layout is documented so you can audit or back it up.

## Logging out

`geopera logout` clears the active profile's stored `auth` block. For an OAuth session it also makes a best-effort RP-initiated logout call to revoke the session server-side (failures there are ignored, since the local credential is being cleared regardless). The profile's `api_url` is kept, so a later `geopera login` reuses the same endpoint without you re-specifying `--api-url`.

```bash
geopera logout
```

```bash
Logged out (profile 'default').
```

Target a specific profile with `--profile`:

```bash
geopera logout --profile staging
```

If there was nothing to clear:

```bash
No stored credentials for profile 'default'.
```

Logging out clears only the named profile; other profiles in the store are left untouched. `GEOPERA_API_TOKEN` is unaffected by `logout` — it is never stored, so there is nothing to clear.

## Worked example: CI pipeline

Authenticate non-interactively, confirm the principal, then run a command — all without touching a browser or writing long-lived state to disk.

```bash
#!/usr/bin/env bash
set -euo pipefail

# Inject the key from the CI secret store; never echo it.
echo "$GEOPERA_API_KEY" | geopera login --api-key -

# Confirm we authenticated as the expected service principal.
geopera whoami

# Run any operation as a structured command. Scalar fields are flags;
# complex bodies use --json '<json>' / @file.json / - (stdin).
geopera orders list
geopera catalog search --collections sentinel-2-l2a --limit 10
```

For a fully stateless run, drop the `login` step and pass the key through the environment instead — nothing is written to `~/.config/geopera`:

```bash
GEOPERA_API_TOKEN="$GEOPERA_API_KEY" geopera orders list
```

If you need to drive an operation that is not exposed as a named command — for example one generated dynamically in a script — the low-level escape hatch still works: `geopera op <operation_id> '<json>'`, such as `geopera op orders.list '{}'`. The body can also come from a file (`--file body.json`) or stdin (`-`).

## Troubleshooting

| Symptom                                                                       | Cause and fix                                                                                  |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `Not logged in (profile '...'). Run 'geopera login' first.`                   | No stored credentials for the active profile. Run `geopera login`, or set `GEOPERA_API_TOKEN`. |
| `Token refresh failed (your session may have been revoked).`                  | The refresh token was revoked or expired. Run `geopera login` again.                           |
| `Session expired and no refresh token is stored.`                             | The stored OAuth entry has no refresh token. Run `geopera login` again.                        |
| `API key rejected (401).`                                                     | The API key is invalid or revoked. Check it or mint a new one.                                 |
| `API key validation failed: ...`                                              | The key failed its `userinfo` check at login and was not saved. Verify the key value.          |
| `No API key provided.`                                                        | `--api-key -` got an empty stdin, or `--api-key ""`. Supply a real key.                        |
| `Device authorization failed. The server may not support the device flow yet` | Fall back to `geopera login --api-key <key>`.                                                  |
| `Login was denied in the browser.`                                            | You rejected the request. Rerun `geopera login` and approve it.                                |
| `Login timed out before authorization completed.`                             | You did not confirm the code in time (~10 minutes). Rerun `geopera login`.                     |

## Related

- [Authentication](/api-reference/authentication) — the token model: session tokens, `gpra_` API keys, and how the API validates them.
- [Scopes](/api-reference/scopes) — what a credential is allowed to do.
- [Errors](/api-reference/errors) — the problem+json error shape returned by operations.
- [Operations](/api-reference/operations) — the structured commands the CLI exposes, and the `POST /v1/op/{operation_id}` model they dispatch to.
