People and services act on the MCS through the API (and the web editor), which needs to know who they are: to trace who launched a run or confirmed a hazardous telecommand, and to check that a supervisor is a supervisor. Stellar Control accepts two kinds of identity:
- an OIDC token, a JWT issued by an identity provider and checked by its signature;
- a declared identity, a name and roles the caller states, trusted without proof.
The environment of the target decides which one is enough, so that a bench in AIT runs without any identity provider while operations in orbit require tokens.
Presenting an identity#
| Way | HTTP | CLI | Web console |
|---|---|---|---|
| OIDC token | Authorization: Bearer <jwt> | stellar login, or --token / STELLAR_TOKEN | Sign-in at the provider, or the OIDC token field of the identity panel |
| Declared identity | X-Stellar-User: <name> | --as (default $USER, else operator) | Operator (declared) field |
| Declared roles | X-Stellar-Role: operator,supervisor | --role or STELLAR_ROLE | Roles (declared) field |
When a request carries a token, the token decides: a refused token is an error, never a
fallback to the declared identity. The CLI always sends X-Stellar-User, and a token too when
one is given. The web console keeps these settings in the local storage of the browser.
What each environment accepts#
The rule is set by the human_orchestration of the environment (see
Environments and Policies):
| Environment | Accepted | Refused with |
|---|---|---|
human_orchestration: off | Anyone, anonymous included | — |
identity: declared | A declared identity, or a checked token | 401 api::anonymous without identity |
identity: jwt (the default) | A checked token only | 401 auth::jwt-required for a declared identity, 401 api::anonymous without identity |
The rule applies, in the environment of the run, to its launch, to answers to its questions and decisions, and to its suspension, resumption and abortion; also to the creation of a schedule and to its validation by a supervisor.
Some requests need an identity whatever the environment: commands on alarms (acknowledge, shelve,
unshelve), answers and commands on runs (400 api::anonymous without one). Pass feeders are
recognised by their identity, token or declared, against passes.feeders (see
Writing a Pass Feeder).
Roles#
Roles are free strings carried by the token or declared. The MCS gives meaning to two of them:
| Role | Where |
|---|---|
operator | Confirms hazardous telecommands, first (hazardous_confirmation) |
supervisor | Confirms hazardous telecommands after the operator; validates the plan of a schedule (POST /v1/schedules/{id}/validate, 403 schedule::not-supervisor otherwise) |
An environment lists the roles that confirm a hazardous telecommand, always starting with
operator. In in_orbit, the operator and the supervisor must be two distinct authenticated
people: the executor requires a different sub. See
Hazardous Confirmations.
OIDC tokens#
The MCS checks tokens against the provider's key set, read from its jwks_uri or from a file.
auth:
jwks_url: https://idp.example.org/realms/ops/protocol/openid-connect/certs
# or jwks_file: /etc/stellar/jwks.json # the file the jwks_uri of the provider serves
issuer: https://idp.example.org/realms/ops
audience: stellar-mcs
roles_claim: realm_access.rolesWith auth.jwks_url, the services read the key set at start (a provider out of reach stops them)
and again every hour, so that keys rotated by the provider are taken; a failed read keeps the
keys already known and is logged.
A token is accepted when:
- it is a JWT (three base64url parts);
- its signature verifies with a key of the set: RS256, RS384 or RS512 (RSA key),
ES256 (EC key on P-256), ES384 (EC key on P-384, that of Logto) or EdDSA (OKP key
on Ed25519); when both the token and the key carry a
kid, they must match; expis present and not past, andnbf, when present, is reached, both with 30 s of leeway;issequalsauth.issuer, when it is set;audequalsauth.audienceor, as a list, contains it, when it is set;subis present and not empty.
The roles are read at auth.roles_claim, a dotted path in the claims (roles by default,
realm_access.roles for Keycloak): a list of strings, a string of roles separated by spaces (the
scope of an OAuth access token), or no roles when absent. With auth.roles_prefix, only the
roles with that prefix are those of the MCS, the prefix taken off: a provider shared with other
applications keeps its own. With roles_claim: scope and roles_prefix: "stellar:", the scope
openid stellar:operator billing:read gives the role operator.
The API, the executor and the editor load the same key set. Without auth.jwks_url or
auth.jwks_file, tokens are refused (401 auth::no-provider) and only declared identities work;
the API logs a warning at start.
A token for everything#
With auth.required: true, the API refuses every request without a checked token, whatever its
environment: declared identities and anonymous requests get 401 auth::token-required. Only
GET /v1/auth/config (how to sign in) and GET /v1/openapi.json stay open, and the files of the
web console. A browser cannot give a header to a WebSocket: its token is then the token
parameter of its URL. The cells of the online trial run so, each open to the members of its
organization only.
Signing in the web console#
With auth.issuer and auth.client_id, the web console signs its users in by itself. It asks
GET /v1/auth/config (open to anyone), which names the provider, the client of the console and,
for Logto, the organization of the cell and the scopes to ask:
auth:
issuer: https://auth.example.org/oidc
jwks_url: https://auth.example.org/oidc/jwks
audience: urn:logto:organization:<organization> # the tokens of that organization only
roles_claim: scope
roles_prefix: "stellar:"
required: true
client_id: <the single-page application of the consoles>
organization: <organization>The console is a public client (authorization code with PKCE, S256), coming back to the root of
its own address, which must be among the redirect URIs of the client. It asks offline_access,
the organizations (urn:logto:scope:organizations) and stellar:operator stellar:supervisor,
then exchanges its refresh token for the token of the organization (organization_id): the one
the API takes, its roles the permissions of the user in that organization. That token stays in
memory, renewed a minute before it expires; the refresh token stays in the tab
(sessionStorage), never in the URL nor kept by the browser beyond the tab. The WebSockets carry
it as their token parameter. Sign out signs out at the provider too. A token of another
organization is refused by the audience (401 auth::invalid-token).
Without a provider named, the console works as before: a declared identity or a pasted token.
Signing in the CLI: stellar login#
With auth.cli_client_id too, the CLI signs in by the device authorization grant (RFC 8628),
which needs no browser on the machine of the CLI:
stellar login --api https://ada-1.control.example.org
# Open https://auth.example.org/oidc/device?user_code=WXYZ-1234
# and check that it shows the code WXYZ-1234
stellar run runs/first-light.yaml --api https://ada-1.control.example.org
stellar logout --api https://ada-1.control.example.org- The client is a public native application with device flow (
customClientMetadata.isDeviceFlowat Logto), named inauth.cli_client_idand given byGET /v1/auth/config. One serves every cell: the token of the organization keeps them apart. - The token. The CLI asks the same scopes as the console (
offline_access, the organizations, the roles of the MCS), polls the provider while the user approves, then exchanges the refresh token for the token of the organization of the cell. - The session is kept per API URL in
~/.config/stellar/credentials($XDG_CONFIG_HOME, orSTELLAR_CREDENTIALS), created readable by its owner only (0600). Every command to that API uses it when no--tokenis given, renewed a minute before it expires with the refresh token, which the provider may rotate. A longstellar watchtaken up again after a cut gets a renewed token. stellar logoutforgets the session of the API.
For a machine (CI), pass a token with --token or STELLAR_TOKEN.
Errors#
| Code | Status | Why |
|---|---|---|
auth::invalid-token | 401 | Not a JWT, unknown key or algorithm, invalid signature, expired, not valid yet, wrong issuer or audience, no subject (the message says which) |
auth::no-provider | 401 | A token, but no auth.jwks_url nor auth.jwks_file |
auth::jwt-required | 401 | The environment requires a token |
auth::token-required | 401 | auth.required: the API requires a token for every request |
api::anonymous | 400 or 401 | No identity where one is needed |
Answers checked twice#
An answer to a question of a run goes from the API to the executor with the identity, its roles
and, for a checked identity, the token itself. With identity: jwt, the executor verifies the
token again (signature, expiry, sub equal to the author, roles of the token) before taking the
answer. An answer that does not fit (wrong role, a person who already confirmed the step, a token
refused) is logged in the run as answer_refused, with the author and the reason, and the run
keeps waiting.
Development identity provider#
Benches and tests often have no identity provider. The CLI makes one, with an Ed25519 key:
stellar auth dev-key dev-idp.seed --jwks jwks.json # a new key; its JWK set for auth.jwks_file
stellar auth dev-token --key dev-idp.seed --sub alice --role operator
stellar auth dev-token --key dev-idp.seed --sub bob --role operator --role supervisor \
--issuer https://dev.local --audience stellar-mcs --ttl 3600dev-key <seed>writes a new seed (hexadecimal, readable by its owner only) and prints the JWK set (key idstellar-dev, algorithm EdDSA), or writes it to--jwks.dev-tokenprints a token signed with the key, withsub,roles,iat,exp(now plus--ttl, 8 h by default) and, when given,issandaud.
Development tokens carry their roles in the roles claim: keep auth.roles_claim: roles with
them.
export STELLAR_TOKEN=$(stellar auth dev-token --key dev-idp.seed --sub alice --role operator)
stellar run request.yamlThe web editor#
The web editor identifies its users the same way, token first:
- a token (
Authorization: Bearer) is checked against the key set of the provider; - a declared identity (
X-Stellar-User) is accepted unlesseditor.declared_identity: false, in which case it is refused with401 auth::token-required; - without either,
401 auth::anonymous.
A WebSocket opened by a browser cannot carry headers: the editor then reads the identity from the
token or user parameter of its query. Only the owner of a draft may change it; its commits are
authored by the user.
Services and NATS#
Services do not use OIDC: the services of the core connect to NATS with credentials given by the deployment, and drivers, transports, gateways and connectors with the JWT the reconciler issues them. People who want to follow runs and alarms directly on NATS exchange their token for short-lived NATS credentials. See NATS Accounts and Credentials.