Two authorities, two credentials.
AMLService is a pure OIDC relying party — it stores no user accounts and no passwords. There are two kinds of caller, each with its own credential. The tenant is always derived from the credential, never sent in a request body.
| Caller | Authenticates with | Can do |
|---|---|---|
| Admin (you / the platform) | OIDC bearer token (admin role, aud: aml-api) | Onboard tenants, issue/revoke API keys, manage providers, risk profiles & quotas. |
| Tenant (your app / customer) | API key (X-Api-Key) | Screen subjects, bulk-screen, read usage, manage monitored subjects. |
The dual-authority model
Screening & usage endpoints accept either an OIDC bearer token or a tenant
API key — both resolve to a tenant and a role. Onboarding & key/quota management are
admin-only (OIDC bearer, admin role). Roles map to policies:
| Policy | Allowed roles | Example endpoints |
|---|---|---|
Screening.Read | compliance_analyst, admin, auditor | GET /v1/screenings/{id}, GET /v1/usage |
Screening.Write | compliance_analyst, admin | POST /v1/screenings/check |
Admin | admin | POST /v1/tenants, quota & key management |
https://aml-identity.dloizides.com — PROOViD's own
OpenIddict IdP. The service is pluggable: switching IdPs is a config change
(Identity__Authority + Audience), no code change.Machine-to-machine — client_credentials
For automation: a service account exchanges its client secret for an admin token.
TOKEN=$(curl -s \
-X POST https://aml-identity.dloizides.com/connect/token \
-d grant_type=client_credentials \
-d client_id=aml-admin-cli \
--data-urlencode "client_secret=$AML_ADMIN_CLI_SECRET" \
-d scope="aml-api roles" \
| jq -r .access_token)
The access token is a signed JWT carrying the AML claims contract: sub,
roles: ["admin"], tenant_id, aud: "aml-api". AMLService validates it
via the IdP's JWKS (/.well-known/openid-configuration → jwks_uri).
Human admin — hosted login (authorization_code + PKCE)
A person signs in through the IdP's hosted login UI instead of holding a client secret.
The public PKCE client is aml-admin-console.
1. Redirect the browser to:
https://aml-identity.dloizides.com/connect/authorize
?client_id=aml-admin-console
&response_type=code
&redirect_uri=<your-registered-redirect-uri>
&scope=openid profile email roles aml-api offline_access
&code_challenge=<S256(code_verifier)>
&code_challenge_method=S256
2. The IdP shows its hosted login page; the admin authenticates.
3. The IdP redirects back with ?code=...
4. Exchange the code (POST /connect/token, grant_type=authorization_code,
code, redirect_uri, client_id, code_verifier) for an access_token.
Tenant API key — X-Api-Key
Tenants authenticate with the API key returned (once) at onboarding. Send it as the
X-Api-Key header, or as a bearer token — both are accepted on tenant-scoped endpoints.
# Either header works on screening / usage endpoints
curl https://aml-screening.dloizides.com/v1/usage \
-H "X-Api-Key: aml_live_7Qk2yP9sR4tU6vW8xZ1aB3cD5eF7gH9"
curl https://aml-screening.dloizides.com/v1/usage \
-H "Authorization: Bearer aml_live_7Qk2yP9sR4tU6vW8xZ1aB3cD5eF7gH9"
POST /v1/tenants/{id}/api-keys?label=ci)
and revoking the old one (DELETE /v1/tenants/{id}/api-keys/{keyId}) — both admin-only.Onboarding returns the credentials
A single admin call provisions everything. The apiKey and
webhookSigningSecret are returned once — store them immediately.
curl -X POST https://aml-screening.dloizides.com/v1/tenants \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Compliance", "slug": "acme",
"contactEmail": "ops@acme.example", "plan": "standard",
"enabledLists": ["UN","EU","OFAC","UK"], "webhookUrl": "https://acme.example/aml-hooks" }'