Skip to content

Accounts, Users & Access

Your organization is a tenant on CORE-M — your own isolated slice of the platform. Every request you and your team make carries an identity that belongs to your tenant, and CORE-M answers two questions on each call: who are you (authentication) and are you allowed to do this (authorization). Your tenant’s data is automatically scoped to you and never visible to any other tenant.

This page explains everything you manage for your own tenant: users and the role model, group-based page permissions, the JWT lifecycle, login rate limiting, API keys, SSO configuration, and how isolation keeps your data separate.

Your tenant is your top-level isolation boundary. When your tenant is created you get:

  • A unique tenant_id that scopes every piece of your data.
  • An initial admin user (role tenant_admin), with the password stored bcrypt-hashed.
  • An API key for programmatic access, returned once.
  • A default group “All Users” with every page permission (dashboard, devices, telemetry, rules, anchors). Your initial admin is not added to it — admins bypass group checks via their role.

Tenant names are unique; a duplicate name is rejected with ALREADY_EXISTS.

Each user in your tenant has exactly one role. Roles define administrative scope and are coarse-grained; fine-grained access for standard users comes from groups.

RoleScopeWhat it grants
platform_adminSystem-wideReserved for the Kronox Data platform team that operates CORE-M. Not assignable by your tenant.
tenant_adminYour tenantFull access within your tenant: users, groups, devices, rules, API keys, and settings. Bypasses group permissions inside your tenant.
memberYour tenantStandard user. Access is controlled entirely by group memberships. No access to /manage/* or /admin/*.

Groups are tenant-scoped collections of users with a defined set of page permissions. A user can belong to multiple groups, and their effective permissions are the deduplicated union of all their groups’ permissions.

Available page permissions:

PermissionGrants access to
dashboardDashboard overview
devicesView and manage devices
telemetryTelemetry dashboards and charts
rulesView and manage rules
anchorsBlockchain proof data on device detail pages

Permission rules in practice:

  • A member with no groups has no access at all and sees: “Your account has no permissions assigned. Contact your administrator.”
  • A member who tries an action outside their permissions is rejected with PERMISSION_DENIED and “missing permission: rules” (or the relevant page).
  • Changing a group’s permissions does not rewrite live tokens. Existing JWTs go stale and pick up the new permissions on the next token refresh.
  • Deleting a group removes all its members and recalculates their effective permissions, which may drop access to some pages.

Users authenticate with email and password. Passwords are bcrypt-hashed at rest. Login responses are timing-safe and never reveal whether the email or the password was wrong — a nonexistent user is indistinguishable from a wrong password.

A successful login issues two tokens, both Ed25519-signed:

  • An access token, 15-minute expiry, presented on every API call.
  • A refresh token, 7-day expiry, used only to mint new access tokens.
flowchart TD
  login([Login: email + password]) -->|valid| issue["Issue access (15 min)<br/>+ refresh (7 day)"]
  login -->|invalid| fail["UNAUTHENTICATED<br/>(timing-safe, no field hint)"]
  issue --> use["Call API with access token"]
  use -->|access valid| ok([Request authorized])
  use -->|access expired| refresh{Refresh token<br/>still valid?}
  refresh -->|yes| rotate["Issue new access token<br/>+ rotate refresh token<br/>(old refresh invalidated)"]
  rotate --> use
  refresh -->|no| relogin([Redirect to login])

Refresh is rotating: each RefreshToken call invalidates the old refresh token and issues a fresh one alongside the new access token. An expired refresh token is rejected with UNAUTHENTICATED, forcing a fresh login.

The access token is signed with Ed25519 using the key from the AUTH_SIGNING_KEY environment variable. Downstream services verify the signature and read the claims — they do not call back to the auth service per request.

A member in tenant T1, in groups “Engineering” (devices, telemetry) and “Monitoring” (dashboard, rules), receives an access token whose payload looks like this:

{
"sub": "9f2c1e44-1b3a-4c8e-9d77-6a0b2f5e1c90",
"tid": "1a2b3c4d-0000-4e5f-8a9b-112233445566",
"role": "member",
"groups": [
"grp-engineering-001",
"grp-monitoring-002"
],
"permissions": [
"dashboard",
"devices",
"rules",
"telemetry"
],
"iat": 1747396800,
"exp": 1747397700
}
ClaimMeaning
subUser ID
tidTenant ID — the isolation key for all downstream queries
roleOne of platform_admin, tenant_admin, member
groupsGroup IDs the user belongs to
permissionsDeduplicated union of all group page permissions
iat / expIssued-at and expiry (15-minute window for access tokens)

To blunt credential-stuffing and brute force, failed logins are rate-limited on two independent dimensions, both with a 900-second (15-minute) TTL:

DimensionThresholdAerospike keyTTL
Per email5 failed attempts in 15 min → block the 6thloginrl:{email_hash}900s
Per source IP20 failed attempts in 15 min → block the 21stloginrl:ip:{ip}900s

A rate-limited request is rejected with RESOURCE_EXHAUSTED and “too many login attempts, try again later”. The response does not reveal whether the account exists, and corem_auth_login_rate_limited_total is incremented. Because the lockout state lives in an Aerospike key with a TTL, the limit clears itself once the window expires — there is no manual unlock step.

API keys authenticate device-to-platform and programmatic calls into your tenant.

  • Format: prefix sk_live_ followed by 32 cryptographically random bytes, URL-safe encoded.
  • Storage: only the hash is stored — never the raw key.
  • Display: the raw key is returned exactly once at creation. There is no way to recover it later; lose it and you rotate.

Presenting a key as Authorization: Bearer sk_live_… lets CORE-M resolve your tenant_id (and, for device keys, the device_id) and scope the request to your tenant. A revoked key is rejected with UNAUTHENTICATED.

CORE-M stores and validates SSO provider configuration for your tenant for SAML 2.0 and OIDC.

FieldSAML 2.0OIDC
Protocolsamloidc
IdP metadata / discoveryMetadata XML URL or uploaded XMLDiscovery URL (.well-known/openid-configuration)
Entity ID / Client IDSP Entity IDClient ID
Client SecretN/AClient Secret (encrypted at rest)
EnabledWhether the stored config is enabled for future integrationSame

A configuration test checks that the IdP metadata or OIDC discovery endpoint is reachable and valid and reports discovered attributes. The test never creates or updates CORE-M users.

Isolation is enforced on every request, not as a feature of any one screen:

  1. A request arrives with a JWT or API key.

  2. CORE-M extracts your tenant_id (from the tid claim or the API key) and attaches it to the request.

  3. Every read and write is scoped to that tenant_id, so a request made under your tenant can never address another tenant’s data.

  4. Any cross-tenant access is rejected with PERMISSION_DENIED and logged for audit.

This is why the tid claim matters so much: it is the single value that pins an entire request to your tenant’s slice of the platform.