Credentials
Managing password, API key, scoped access-token, and certificate credentials for entities.
Credentials are attached to entities. An entity may have multiple credentials of different kinds.
Supported credential kinds:
- password;
- access_token (API keys and scoped access tokens);
- shared_key;
- certificate.
This page covers passwords, API keys, and scoped access tokens. See Certificates for mTLS certificate credentials, CA files, CRL, OCSP, and runtime lookup.
Password credentials
Used for the /auth/login endpoint. The password is hashed with argon2 before storage; the plaintext is never persisted.
Login returns a JWT and session reference:
If ATOM_SELF_REGISTRATION_ENABLED=true, a human can self-register into an
existing global account without selecting a tenant. Password signup stores the
normalized email as the login identifier and requires email verification before
normal login:
Verification and resend endpoints are public:
ATOM_ALLOW_UNVERIFIED_EMAIL_LOGIN=true allows local development login
before email verification, but inactive and suspended entities are still
rejected.
API keys
API keys are long-lived credentials suited for device and service authentication where interactive login is not practical.
Atom stores API keys and scoped access tokens as access_token credentials. The product difference is scope:
| Product label | Creator | Authority |
|---|---|---|
| API key | Credential administrator | Unscoped; authenticates as the entity with its live grants |
| Scoped access token | Token owner | Capped by a permission ceiling and can never exceed the owner's live grants |
Format
The credential ID is embedded in the key for O(1) lookup. The server decodes the ID from the prefix, fetches that specific credential row, then verifies the argon2-hashed secret.
Creating an API key
The dedicated createApiKey mutation has been removed. All tokens are minted
through a single surface — createAccessToken. To create an
unscoped API key (the owner's full live grants, no ceiling), pass scoped: false
with an empty permissions list; omit scoped (or set it true) for a ceiling-capped
scoped token.
An unscoped token carries the owner's full authority, so minting one requires
credential-management authority over the owner — the same gate as any other
credential operation. For provisioned service access, an administrator mints an
unscoped token for the service entity (delegated createAccessToken with
subjectId + scoped: false):
The full token is returned exactly once and cannot be recovered. Prefer a
scoped token (a permission ceiling) whenever the consumer does not need owner-wide
surfaces such as authorizedObjectIds, which scoped tokens cannot call.
Using an API key
Pass it as a Bearer token — the service detects the atom_ prefix and routes to API key verification:
API key authentication does not create a session and does not return a JWT.
Scoped access tokens
A scoped access token is a least-privilege bearer credential for CLI, API, and
automation use. It uses the same atom_<id>_<secret> format as any other
access_token credential, but the credential row is marked scoped and carries a
permission ceiling.
Effective access
The ceiling is an allow-list that can only narrow the owner. Consequences:
- A token can never grant anything the owner does not currently hold. Grant the owner a role after minting and the token still only sees what its ceiling allows.
- Lose the owner's role or Direct Policy and the token loses that access on the next request — state is re-evaluated live, never baked into the token.
- An empty ceiling is closed: it permits nothing (it is never read as "all"). Revoking, expiring, or emptying the ceiling denies the next request.
- An owner-level
deny(role block or Direct Policy) still overrides any ceiling allow.
Creating a scoped access token
Self-service — the signed-in entity mints a token for itself:
Read-only over all resources:
The full token is returned exactly once at creation. It is never stored in plaintext and cannot be recovered — store it immediately.
At least one permission is required; an empty permissions list is rejected.
Delegated minting (for services)
Pass subjectId to mint a token owned by another subject — the provisioning
path for services and workloads:
Rules:
- Omit
subjectId, or set it to yourself, for self-service. - A different
subjectIdis a delegated mint and requires an unscoped caller holdingmanageon the target subject (or its tenant) — the same gate as any credential-management operation. A scoped token can never mint delegated tokens. - The ceiling is not validated against the target's grants at mint time.
Effective access stays
target live grants ∩ ceiling, evaluated per request, so a delegated token can never exceed the target even if the ceiling names more.
Permission scope modes
Each ceiling entry is one allow-list row: one or more actions, a scopeMode, its
matching scope reference, and optional conditions.
scopeMode | Required fields | Optional | Matches |
|---|---|---|---|
platform | — | — | every platform-scoped object |
tenant | tenantId | — | one tenant |
object_kind | objectKind | tenantId | all objects of a kind (optionally within one tenant) |
object_type | objectKind, objectType | tenantId | one namespaced sub-type (optionally within one tenant) |
object | objectId | — | one exact object |
objectType must be the full namespaced value (entity:device,
resource:channel) matching objectKind — a bare sub-kind (device) is rejected.
When tenantId is set on an object_kind/object_type entry, matches are confined
to that tenant; omit it for a tenant-agnostic ceiling.
Examples:
Conditional (ABAC) entries
An entry may carry conditions evaluated against request context, exactly like a
permission block:
Conditions are applied where a full object decision exists (authzCheck, object
reads). Coarse control-plane gates run without object context and fail closed on
conditional entries — a conditional ceiling entry never satisfies a coarse gate, so a
token whose only matching allow is conditional cannot pass an administrative
precondition. Use an unconditional entry when a coarse gate must pass.
Using a scoped access token
Pass it as a Bearer token — the atom_ prefix routes to credential verification:
Authorization semantics:
authzCheck,authzBulkCheck(per-object), gRPCAuthzService.Check, object reads, andauthzExplainapply the ceiling when the checked subject is the token owner. When the owner is allowed but the ceiling omits the action or scope,authzExplainreturnsdenied by access token permission ceiling.- A delegated check about a different subject is not altered by the caller's ceiling. The caller's right to ask is still capped by its own ceiling.
- Broad authorized-listing surfaces are not yet ceiling-aware and fail closed for
scoped tokens (a
bad_request). Use per-objectauthzCheckor direct object reads.
Listing, replacing, and revoking
Replace the entire ceiling (owner-only; requires at least one permission):
accessTokens, replaceAccessTokenPermissions, and revokeAccessToken are
owner-scoped — they act only on the caller's own tokens. To manage a delegated
token you minted for another subject, revoke it as an administrator with
revokeCredential (manage on the target).
Self-escalation guardrails
A scoped token must never widen itself or create a broader sibling. These operations require an unscoped session or credential — the token itself is refused even when its ceiling would grant the underlying capability:
- create a scoped access token (self or delegated);
- replace an access token's permissions;
- revoke an access token or any credential (
revokeCredential); - create, rotate, reveal, or revoke credentials through credential-management APIs.
Audit
Access-token lifecycle changes are recorded as credential events:
| Operation | Event |
|---|---|
| create (self or delegated) | credential.create |
| replace permissions | credential.update |
| revoke | credential.revoke |
Delegated creates record the target as entity_id and set delegated: true in the
audit detail.
Listing credentials
Returns metadata only — no secret hashes, no plaintext secrets.
Revoking a credential
The credential status is set to revoked. Revoked credentials are rejected immediately on the next request — no token reissuance is required because API keys are verified live against the database on each request.