Skip to content

Auth & Identity

Aperture ships with a complete built-in auth system covering interactive users, machine-to-machine service accounts, and API keys. All of it is swappable through the CredentialValidator SPI.

Built-in auth endpoints

MethodPathDescription
POST/auth/loginUsername + password → JWT + refresh token
POST/auth/refreshRotate refresh token → new JWT
POST/auth/logoutRevoke refresh token family
GET/auth/meCurrent user info
PATCH/auth/meUpdate own profile (user-editable metadata only)
POST/auth/change-passwordChange password (required when forcePasswordChange: true)
POST/auth/tokenService account client_id + client_secret → access token

JWT login

bash
POST /auth/login
Content-Type: application/json

{
  "username": "accountant@acme.com",
  "password": "Accountant123!"
}

Response:

json
{
  "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}

Use the token as Authorization: Bearer <accessToken> on all API requests.

Token claims

The JWT carries everything Aperture needs to enforce tenancy, RBAC, and ABAC:

ClaimTypeDescription
substringUser ID (UUID)
tenantIdstringTenant the user belongs to (absent for SuperAdmin)
rolesstring[]Domain roles assigned to the user (e.g., ["Accountant"])
profileobjectUser-editable profile metadata (display name, picture, etc.)
securityAttributesobjectAdmin-assigned security claims used in ABAC (department, region, etc.)
accountKindstringUSER, SERVICE_ACCOUNT, or PERSONAL_API_KEY
isSuperAdminbooleanTrue if this is the platform super-administrator
isTenantAdminbooleanTrue if this user is a tenant administrator
scopestring[]Present only when FORCE_CHANGE is active (password change required)

Security attributes are admin-assigned key-value pairs stored in a JSONB column. ABAC policies compare against them using SpEL expressions (#user.securityAttributes['department'] == 'finance'). Ordinary users cannot modify security attributes; only the profile field is user-editable.

Profile vs. Security Attributes:

  • profile: free-form user-editable metadata (display name, preferences, timezone, etc.). Users can update their own profile via PATCH /auth/me.
  • securityAttributes: admin-controlled security claims (department, region, clearance level, etc.) used for ABAC policies. Ordinary users cannot modify security attributes.

Token refresh

bash
POST /auth/refresh
Content-Type: application/json

{ "refreshToken": "eyJhbGciOiJIUzI1NiJ9..." }

Returns a new accessToken + rotated refreshToken. The old refresh token is revoked. Logout revokes the entire token family.

Forced password change

When a user account is created by an admin or seeder with a temporary password, the login response contains "forcePasswordChange": true and the token has a limited scope that blocks all API calls except /auth/change-password. The user must change their password before accessing anything else.

Service accounts

Service accounts are machine-to-machine identities. They authenticate with a client_id + client_secret pair instead of a username and password:

bash
POST /auth/token
Content-Type: application/json

{
  "clientId": "my-service-client",
  "clientSecret": "secret-shown-once"
}

The client_secret is shown only once at creation time and stored as a hash. Service accounts carry a tenantId and roles just like user accounts but are flagged as isServiceAccount: true in the principal.

API keys

API keys are an alternative credential for machine or integration use. They are passed via the X-API-Key header:

bash
curl http://localhost:8080/api/v1/invoices \
  -H "X-API-Key: ak_..."

Key values are shown only once at creation. Storage is a hash. Listing keys shows their ID, name, status, and expiry, but never the key value itself.

Personal API keys

Users can issue their own API keys (delegated from their own identity) via:

bash
POST /auth/me/api-keys
Authorization: Bearer <user-token>
Content-Type: application/json

{
  "name": "CI pipeline key",
  "domainRoles": ["Accountant"],      # subset of caller's roles
  "securityAttributes": { "department": "finance" },  # subset of caller's attributes
  "expiresAt": "2027-01-01T00:00:00Z"
}

Keys must be a delegation of the calling user's identity, so they cannot carry roles or attributes the user does not currently hold. The system enforces this at creation time and again on each use (fail-closed: if the user loses a role or attribute, existing keys that carried it are immediately rejected).

List own keys: GET /auth/me/api-keys
Disable own key: POST /auth/me/api-keys/{keyId}/disable

Tenant settings control whether personal API keys are enabled and whether non-expiring keys are allowed:

GET  /manage/tenants/{id}/settings/personal-api-keys   # read settings (TenantAdmin or SuperAdmin)
PUT  /manage/tenants/{id}/settings/personal-api-keys   # update settings (TenantAdmin or SuperAdmin)
GET  /manage/settings/personal-api-keys                # global defaults (SuperAdmin only)
PUT  /manage/settings/personal-api-keys                # update global defaults (SuperAdmin only)

Tenant settings override global defaults. Both default to enabled: true, allowNonExpiring: false.

Admin revocation (tenant-wide): POST /manage/tenants/{id}/personal-api-keys/{keyId}/revoke

Identity administration

SuperAdmin and TenantAdmin are platform authorities, not domain roles. They are stored as boolean flags on the principal (superAdmin, tenantAdmin) and in dedicated database tables (aperture_tenant_admins), not in the domain roles catalog. They cannot be declared in RoleDefinition manifests or assigned as RBAC roles.

Tenant management requires the SuperAdmin authority. User management within a tenant requires SuperAdmin or TenantAdmin for the same tenant.

MethodPathRequired authority
POST/manage/tenantsSuperAdmin
GET/manage/tenantsSuperAdmin
GET/manage/tenants/{id}SuperAdmin
PATCH/manage/tenants/{id}SuperAdmin
DELETE/manage/tenants/{id}SuperAdmin
POST/manage/tenants/{id}/usersSuperAdmin or same-tenant TenantAdmin
GET/manage/tenants/{id}/usersSuperAdmin or same-tenant TenantAdmin
PATCH/manage/tenants/{id}/users/{uid}SuperAdmin or same-tenant TenantAdmin
DELETE/manage/tenants/{id}/users/{uid}SuperAdmin or same-tenant TenantAdmin
PUT/manage/tenants/{id}/users/{uid}/rolesSuperAdmin or same-tenant TenantAdmin
POST/manage/tenants/{id}/tenant-admins/{uid}SuperAdmin; grants TenantAdmin authority
DELETE/manage/tenants/{id}/tenant-admins/{uid}SuperAdmin; revokes TenantAdmin authority
POST/manage/tenants/{id}/invitesSuperAdmin or same-tenant TenantAdmin
GET/manage/tenants/{id}/invitesSuperAdmin or same-tenant TenantAdmin
DELETE/manage/tenants/{id}/invites/{inviteId}SuperAdmin or same-tenant TenantAdmin
POST/manage/tenants/{id}/service-accountsSuperAdmin or same-tenant TenantAdmin
GET/manage/tenants/{id}/service-accountsSuperAdmin or same-tenant TenantAdmin
POST/manage/tenants/{id}/service-accounts/{accountId}/disableSuperAdmin or same-tenant TenantAdmin
POST/manage/tenants/{id}/service-accounts/{accountId}/rotate-secretSuperAdmin or same-tenant TenantAdmin
GET/manage/tenants/{id}/personal-api-keysSuperAdmin or same-tenant TenantAdmin
POST/manage/tenants/{id}/personal-api-keys/{keyId}/revokeSuperAdmin or same-tenant TenantAdmin
GET/manage/settings/personal-api-keysSuperAdmin
PUT/manage/settings/personal-api-keysSuperAdmin

The invite flow

  1. Admin calls POST /manage/tenants/{id}/invites with the invitee's email, intended username, and roles
  2. Aperture generates a secure, expiring invite token
  3. The invitee redeems the token via POST /auth/accept-invite with the token, their chosen username, and new password
  4. The account is created with the assigned roles; the invite token is consumed

Swapping auth providers with the CredentialValidator SPI

The CredentialValidator interface is the only boundary between Aperture and the identity system:

java
public interface CredentialValidator {
    ValidationResult validate(HttpServletRequest req);
}

validate receives the full HTTP request and returns either a ValidationResult with an AperturePrincipal (success) or a rejection. The AperturePrincipal carries everything Aperture needs downstream.

java
public record AperturePrincipal(
    String userId,
    String tenantId,
    Set<String> roles,            // domain roles only; no platform authority strings
    PrincipalKind kind,           // USER, SERVICE_ACCOUNT, or PERSONAL_API_KEY
    Map<String, Object> profile,
    Map<String, Object> securityAttributes,
    Set<String> scopes,           // e.g. {"FORCE_CHANGE"}
    boolean superAdmin,
    boolean tenantAdmin
) implements java.security.Principal {}

To swap auth providers: declare a Spring bean that implements CredentialValidator. The built-in SimpleCredentialValidator is annotated @ConditionalOnMissingBean(CredentialValidator.class), so declaring your own bean disables the entire JWT/API-key infrastructure in aperture-simple-auth.

Tenancy, RBAC, ABAC, hooks, and audit all read from the AperturePrincipal and are unaffected by the auth provider swap.

See the Keycloak Integration example for a complete worked implementation.