Admin API

Admin endpoints require a JWT access token with the authagonal-admin scope (configurable via AdminApi:Scope).

All endpoints are under /api/v1/.

Bootstrapping the first admin token

Every /api/v1/* endpoint requires a bearer token carrying the admin scope, but the admin API itself (and dynamic client registration) refuses to create or update any client holding that scope (403 forbidden_scope), so a runtime-created client can never escalate to admin. The only way to mint an admin token is a config-seeded client: entries in the Clients: configuration section are upserted at startup by ClientSeedService, and config is trusted, the forbidden-scope guard applies only to the runtime APIs.

Seed a client_credentials client with the admin scope in appsettings.json (or the equivalent environment variables / secret store):

{
  "Clients": [
    {
      "Id": "admin-cli",
      "Name": "Admin CLI",
      "ClientSecret": "a-long-random-secret",
      "GrantTypes": ["client_credentials"],
      "Scopes": ["authagonal-admin"]
    }
  ]
}

(ClientSecret is hashed at startup; supply SecretHashes instead if you prefer to keep only a pre-hashed value in config. ClientId/ClientName/AllowedGrantTypes/AllowedScopes are accepted as aliases for Id/Name/GrantTypes/Scopes.)

Then exchange the credentials for a token at the standard token endpoint:

curl -X POST https://auth.example.com/connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=admin-cli" \
  -d "client_secret=a-long-random-secret" \
  -d "scope=authagonal-admin"
{ "access_token": "eyJhbGci...", "token_type": "Bearer", "expires_in": 1800, "scope": "authagonal-admin" }

The client_credentials grant validates the requested scope against the client’s AllowedScopes, since the seeded client holds authagonal-admin, the token is issued. Use it as Authorization: Bearer {access_token} on every admin call:

curl https://auth.example.com/api/v1/clients -H "Authorization: Bearer eyJhbGci..."

Keep the seeded client’s secret in your deployment’s secret store; rotating it is a config change + restart.

Users

Get User

GET /api/v1/profile/{userId}

Returns user details including external login links.

User Exists

GET /api/v1/profile/{userId}/exists

Returns 204 if the user exists, 404 otherwise (a cheap existence probe, no body).

Register User

POST /api/v1/profile/
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "SecurePass1!",
  "firstName": "Jane",
  "lastName": "Doe"
}

Creates a user and sends a verification email. Returns 409 user_exists if the email is already taken.

Optional admin-only fields: userId (caller-supplied id, 409 user_id_in_use on collision), emailConfirmed (create the user already verified, skipping the verification email), companyName, organizationId, phone, locale, and customAttributes (a string map persisted on the user and forwarded to provisioning targets).

Update User

PUT /api/v1/profile/
Content-Type: application/json

{
  "userId": "user-id",
  "firstName": "Jane",
  "lastName": "Smith",
  "organizationId": "new-org-id"
}

userId is required; every other field is optional, only provided fields are updated. Changing organizationId triggers:

Delete User

DELETE /api/v1/profile/{userId}

Deletes the user, revokes all grants, and deprovisions from all downstream apps (best-effort).

Confirm Email

POST /api/v1/profile/confirm-email?token={token}

Send Verification Email

POST /api/v1/profile/{userId}/send-verification-email
POST /api/v1/profile/{userId}/identities
Content-Type: application/json

{
  "provider": "saml:acme-azure",
  "providerKey": "external-user-id",
  "displayName": "Acme Corp Azure AD"
}
DELETE /api/v1/profile/{userId}/identities/{provider}/{externalUserId}

MFA Management

Get MFA Status

GET /api/v1/profile/{userId}/mfa

Returns MFA status and enrolled methods for a user.

Reset All MFA

DELETE /api/v1/profile/{userId}/mfa

Removes all MFA credentials and sets MfaEnabled=false. The user will need to re-enroll if required.

Remove Specific MFA Credential

DELETE /api/v1/profile/{userId}/mfa/{credentialId}

Removes a specific MFA credential (e.g., a lost authenticator). If the last primary method is removed, MFA is disabled.

SSO Providers

SAML Providers

POST   /api/v1/saml/connections                    # Create
GET    /api/v1/saml/connections/{connectionId}     # Get one
PUT    /api/v1/saml/connections/{connectionId}     # Update (partial — only supplied fields change)
DELETE /api/v1/saml/connections/{connectionId}     # Delete

Create requires connectionName, entityId, and exactly one of metadataLocation (a metadata URL) or metadataXml (pasted IdP metadata, for IdPs without a metadata URL, it is parse-validated and condensed at save). Optional: nameIdFormat (omit for the emailAddress default, "none" to omit NameIDPolicy, recommended for ADFS, or a NameID format URN), signAuthnRequests, iconUrl, allowedDomains, disableJitProvisioning. Every connection gets a server-generated SP keypair; it is never returned by the API. See SAML for details.

OIDC Providers

POST   /api/v1/oidc/connections                    # Create
GET    /api/v1/oidc/connections/{connectionId}     # Get one
DELETE /api/v1/oidc/connections/{connectionId}     # Delete

Create requires connectionName, metadataLocation, clientId, clientSecret, redirectUrl. Optional: iconUrl, allowedDomains, passthroughParams. The client secret is protected at rest and never returned. See OIDC Federation.

SSO Domains

GET    /api/v1/sso/domains                 # List all

Clients

Manage OAuth clients at runtime. All routes require the IdentityAdmin policy (the admin scope).

GET    /api/v1/clients              # List all clients
GET    /api/v1/clients/{clientId}   # Get one client
POST   /api/v1/clients              # Create a client
PUT    /api/v1/clients/{clientId}   # Update a client
DELETE /api/v1/clients/{clientId}   # Delete a client

Create / Update Client

POST /api/v1/clients
Content-Type: application/json

{
  "clientId": "my-app",
  "clientName": "My Application",
  "allowedGrantTypes": ["authorization_code"],
  "redirectUris": ["https://app.example.com/callback"],
  "allowedScopes": ["openid", "profile", "email"]
}

POST returns 409 if the client already exists. PUT updates an existing client (404 if not found); on update, only newly-added scopes are escalation-checked.

Notes:

Scopes

Manage custom OAuth scopes at runtime. See OAuth Scopes for the full scope model.

GET    /api/v1/scopes           # List all scopes
GET    /api/v1/scopes/{name}    # Get one scope
POST   /api/v1/scopes           # Create a scope
PUT    /api/v1/scopes/{name}    # Update a scope (only supplied fields change)
DELETE /api/v1/scopes/{name}    # Delete a scope
POST /api/v1/scopes
Content-Type: application/json

{
  "name": "billing.read",
  "displayName": "Billing — read-only",
  "description": "View invoices and payment history",
  "userClaims": ["billing_plan"]
}

Returns 201 on create (409 if the scope already exists), the scope JSON on get/update, and 204 on delete.

Provisioning Apps

Manage downstream provisioning targets at runtime. All routes require the IdentityAdmin policy.

GET    /api/v1/provisioning/apps               # List apps (also returns the configured limit)
POST   /api/v1/provisioning/apps               # Create an app
PUT    /api/v1/provisioning/apps/{appId}       # Update an app
DELETE /api/v1/provisioning/apps/{appId}       # Delete an app
POST   /api/v1/provisioning/apps/{appId}/test  # Send a test /try call to the app's callback

Create / Update Provisioning App

POST /api/v1/provisioning/apps
Content-Type: application/json

{
  "name": "Backend",
  "callbackUrl": "https://api.example.com/provisioning",
  "apiKey": "secret-api-key",
  "tryTimeoutSeconds": 30
}

Test a Provisioning App

POST /api/v1/provisioning/apps/{appId}/test

Sends a synthetic POST {callbackUrl}/try with a sample payload (and the app’s API key as a bearer token if set) and returns { success, statusCode, body } so you can verify connectivity from the admin UI.

Roles

List Roles

GET /api/v1/roles

Get Role

GET /api/v1/roles/{roleId}

Create Role

POST /api/v1/roles
Content-Type: application/json

{
  "name": "admin",
  "description": "Administrator role"
}

Update Role

PUT /api/v1/roles/{roleId}
Content-Type: application/json

{
  "name": "admin",
  "description": "Updated description"
}

Delete Role

DELETE /api/v1/roles/{roleId}

Assign Role to User

POST /api/v1/roles/assign
Content-Type: application/json

{
  "userId": "user-id",
  "roleName": "admin"
}

Assignment is by role name, not role id. Returns the user’s updated role list.

Unassign Role from User

POST /api/v1/roles/unassign
Content-Type: application/json

{
  "userId": "user-id",
  "roleName": "admin"
}

Get User’s Roles

GET /api/v1/roles/user/{userId}

SCIM Tokens

Generate Token

POST /api/v1/scim/tokens
Content-Type: application/json

{
  "clientId": "client-id",
  "description": "Entra provisioning",
  "expiresInDays": 365
}

description and expiresInDays are optional (omit expiresInDays for a non-expiring token). Returns the raw token once. Store it securely, it cannot be retrieved again.

List Tokens

GET /api/v1/scim/tokens?clientId=client-id

Returns token metadata (ID, created date) without the raw token value.

Revoke Token

DELETE /api/v1/scim/tokens/{tokenId}?clientId=client-id

Tokens

Impersonate User

POST /api/v1/token?clientId=client-id&userId=user-id&scopes=openid%20profile

Issues tokens (access, refresh, and, when openid is requested, id token) on behalf of a user without requiring their credentials. Useful for testing and support. Parameters are passed as query strings.

Query parameter Required Description
clientId Yes The client the tokens are issued for. Token lifetimes come from this client’s configuration.
userId Yes The user to impersonate.
scopes No Space-separated list of scopes (URL-encode the spaces). Defaults to the client’s AllowedScopes when omitted.

Restrictions:

The response is a standard token response with access_token, refresh_token, optional id_token, expires_in, and the granted scope (space-separated).