Packages

An Elixir client for the Auth0 Management and Authentication APIs, built on Req.

Current section

Files

Jump to
auth0_client ROADMAP.md
Raw

ROADMAP.md

# Roadmap
Where this library stands against the Auth0 API, and what to build next.
Endpoint counts come from Auth0's [OpenAPI 3.1 document](https://auth0.com/docs/api/management/openapi.json)
for the Management API, enumerated programmatically, and from the
[Authentication API reference](https://auth0.com/docs/api/authentication) page by page. Audited 2026-07-21.
Two things that look like evidence and are not:
- **A doc URL returning HTTP 200 does not prove an endpoint exists.** Retired resources still render the
generic Management API index. Absence from the OpenAPI document and from the resource nav is the reliable
signal.
- **The OpenAPI `deprecated` flag is unreliable.** Every `guardian` endpoint reads `deprecated: false`,
including five whose own description begins "This endpoint has been deprecated." Read the description
text, and check the
[product lifecycle page](https://auth0.com/docs/troubleshoot/product-lifecycle/deprecations-and-migrations);
the flag is at best a weak positive signal.
## Coverage at a glance
| API | Implemented | Total | |
|---|---|---|---|
| Management v2 | 253 | 451 | ~56% |
| Authentication | 21 | ~30 | plus 11 of ~11 `/oauth/token` grants |
Management resource groups: 22 fully covered, 3 partial, 24 with no coverage at all.
- **Full:** `client-grants`, `clients`, `connections`, `connections-directory-provisionings`,
`connections-scim-configurations`, `custom-domains`, `device-credentials`, `emails`, `grants`, `groups`,
`jobs`, `keys`, `logs`, `refresh-tokens`, `resource-servers`, `roles`, `sessions`, `tenants`, `tickets`,
`user-blocks`, `users`, `users-by-email`
- **Partial:** `organizations` 37/42, `guardian` 30/36, `actions` 14/24 — all three partial by the
decisions recorded in §7, not for want of work
The `rules` group (5 operations) was removed rather than fixed — see §3 — so the count went 55 → 50 before
RBAC took it to 73. `blacklists` was never in the OpenAPI document, so removing it changed no count.
No implemented endpoint has a wrong path or HTTP method, and as of the correctness pass none has a wrong
parameter shape either.
---
## 1. Foundation
- [x] Per-request headers — `Utils.request_headers/2` merges caller-supplied headers over the defaults,
case-insensitively, so a per-call `Authorization` replaces the default rather than being sent
alongside it. `do_get/3` and `do_post/3` expose it
- [x] Omit nil-valued optional parameters instead of sending explicit JSON `null`. **Scoped deliberately**
to payloads this library constructs with optional defaults (`Token.auth_code/4`,
`auth_code_pkce/4`). A blanket transform in `do_post`/`do_patch` would break Management PATCH calls,
where an explicit `null` is how you *clear* a field
- [x] Form-encoded bodies where Auth0 documents them. `do_post_form/3` sends `/oauth/token` and
`/oauth/revoke` as `application/x-www-form-urlencoded`, per RFC 6749 and Auth0's docs.
`/dbconnections/*` stays JSON — see the encoding rule in §7
- [x] URL-only builders for endpoints that are browser redirects rather than calls the library should
make — `Auth0Client.Authentication.Url`
- [x] Multipart uploads — `do_post_multipart/2`, for `POST /jobs/users-imports`, which Auth0 accepts in no
other encoding. Needs `Utils.drop_header/2` rather than `merge_headers/2`: Req sets the multipart
content-type with `put_new_header`, so the default `application/json` would win and the boundary
would never be sent
- [x] `do_delete/3` with per-request headers, for `DELETE /mfa/authenticators/{id}`
- [x] **Every verb takes extra headers.** They had been added one verb at a time as endpoints demanded
them, which is how `do_delete` came to lack them until `/mfa/authenticators` needed one. All eight now
share the shape `(path, payload_or_params \\ default, extra_headers \\ [])`, and a test asserts each
one both passes the header through and lets it replace the management token rather than duplicate it.
Collapsing the `is_map`/`is_list` clause pairs into `Enum.into/2` and one `keyword_to_object/1` helper
shrank the code generated into all fifteen API modules
**Not unified: list bodies.** The original note here proposed making keyword-param handling uniform
too. That would have been wrong. A list body is genuinely ambiguous — a keyword list meant as a JSON
object, or a real JSON array — and the split is load-bearing, not accidental: `do_post` and
`do_patch` resolve it as a keyword list, while `do_put` leaves it alone because it is how
`User.replace_authentication_methods/2` and `Guardian.update_policies/1` send arrays. Unifying them
raises `ArgumentError` on both. The asymmetry is now named by `keyword_to_object/1`, commented where
it matters, and pinned by tests
## 2. Known-broken
- [x] **`Authentication.userinfo/1` sent the token in the query string.** Now sent as
`Authorization: Bearer`, the only form Auth0 documents, which also keeps it out of access logs.
[Docs](https://auth0.com/docs/api/authentication/user-profile/get-user-info)
- [x] **`Token.auth_code_pkce/5` required `client_secret`.** Now `auth_code_pkce/4`:
`(client_id, code, code_verifier, opts)`, where `opts` may carry `:redirect_uri` and, for a
confidential client, `:client_secret`.
[Docs](https://auth0.com/docs/api/authentication/authorization-code-flow-with-pkce/get-token-pkce)
- [x] **`search_engine=v3` leaked onto every User call.** Now applied in `User.all/1` only, via
`Map.put_new/3`, so an explicit `search_engine` still wins
- [x] **`Utils.fetch_mgmt_token/0` raised on any non-success response.** `mgmt_token/0` now returns
`{:ok, token} | {:error, reason}`, header construction is fallible, and `do_request` short-circuits.
A management call against a rate-limited tenant returns `{:error, %{reason: :token_fetch_failed,
status: 429, body: _}}` and never reaches the endpoint
## 3. Removals
This library does not support endpoints Auth0 has deprecated or retired.
- [x] **`Auth0Client.Management.Rule`** — deprecated 2023-05-16, read-only since 2024-11-18, end-of-life
2026-11-18. `Action` is the replacement (§5)
[Lifecycle](https://auth0.com/docs/troubleshoot/product-lifecycle/deprecations-and-migrations)
- [x] **The whole `Auth0Client.Authentication.Login` module**`database/6` (`POST /oauth/ro`, deprecated
2017-07-08) and `social/4` (`POST /oauth/access_token`) were its only two functions. Replacements are
the Resource Owner Password grant and `GET /authorize` respectively, both in §4
- [x] **`Authentication.tokeninfo/1`** (`POST /tokeninfo`) — superseded by `GET /userinfo`
- [x] **`Auth0Client.Management.Blacklist`**`/blacklists/tokens` has no entry in the OpenAPI document and no
page in the docs nav
- [x] **The `:v2_search` config flag** — search v2 was retired 2019-06-30, so the flag selected an engine
that no longer exists
## 4. Authentication API
Implemented: `/dbconnections/signup`, `/dbconnections/change_password`, `/userinfo`, `/authorize`,
`/v2/logout`, `/oidc/logout`, `/oauth/revoke`, `/oauth/par`, `/oauth/device/code`,
`/oauth/global-token-revocation`, `/passwordless/start`, the four `/mfa/*` endpoints,
`/.well-known/jwks.json`, `/.well-known/openid-configuration`, and `/oauth/token` with the
`authorization_code`, PKCE, `client_credentials`, `refresh_token`, `password` / `password-realm`,
`mfa-otp`, `mfa-oob`, `mfa-recovery-code`, `passwordless/otp`, `device_code` and native-social
`token-exchange` grants.
**P0 — done. The authorization-code flow is complete end to end.**
- [x] `refresh_token` grant — `Token.refresh/3`
[docs](https://auth0.com/docs/api/authentication/refresh-token/refresh-token)
- [x] Resource Owner Password grant — `Token.password/4`, switching to `password-realm` when given a
`:realm`. Replaces the removed `Login.database/6`
[docs](https://auth0.com/docs/api/authentication/resource-owner-password-flow/get-token)
- [x] `POST /oauth/revoke``Token.revoke/3`, normalising Auth0's empty 200 body to `:ok`
[docs](https://auth0.com/docs/api/authentication/revoke-refresh-token/revoke-refresh-token)
- [x] `GET /authorize` URL builder — `Url.authorize/2`
[docs](https://auth0.com/docs/api/authentication/authorization-code-flow/authorize-application)
- [x] Logout URL builders — `Url.logout/1` (`/v2/logout`) and `Url.oidc_logout/1` (`/oidc/logout`)
**P1 — done. An MFA login can now be completed end to end.**
- [x] Passwordless — `Authentication.passwordless_start/1` and `Token.passwordless/5`
- [x] MFA — `Auth0Client.Authentication.Mfa` (`challenge/1`, `associate/2`, `authenticators/1`,
`delete_authenticator/2`) plus `Token.verify_otp/4`, `verify_oob/4` and `verify_recovery_code/4`.
Closes the gap where `Token.password/4` reported `mfa_required` and nothing could act on it
- [x] JWKS and OIDC discovery — `Authentication.jwks/0` and `openid_configuration/0`
**P2 — done. Every documented `/oauth/token` grant is now reachable.**
- [x] Device Authorization Flow — `Authentication.device_code/2` starts it and `Token.device/3` exchanges,
mirroring the `passwordless_start/1` + `Token.passwordless/5` split. The flow is a polling loop whose
`authorization_pending` and `slow_down` responses are **errors that mean keep going**, so the `@doc`
and the guide both show the loop rather than the call
[docs](https://auth0.com/docs/api/authentication/device-authorization-flow/authorize-device)
- [x] Pushed Authorization Requests — `Authentication.pushed_authorization_request/2`, answering `201` with
a `request_uri` that feeds the existing `Url.authorize/2`. Auth0's prose for this endpoint calls the
field `redirect_uri`; its own response schema and RFC 9126 both say `request_uri`, which is what the
API returns — recorded so nobody "corrects" it back
- [x] Native social token exchange — `Token.native_social/4`. `user_profile` is a nested object in a
form-encoded body, which `URI.encode_query/1` cannot represent, so the function JSON-encodes that one
value. Done in the function rather than in `post_form/3`, where it would silently encode fields Auth0
expects flat
- [x] Global token revocation — `Authentication.global_token_revocation/3`. Revokes sessions and refresh
tokens across every application but **not access tokens**, which expire on their own
- [ ] Back-channel login (CIBA), passkeys, dynamic client registration, SAML/WS-Fed
**Smaller drift, same area**
Done. Optional arguments moved into an `opts` map, as decided.
- [x] `change_password/4` takes `opts`, carrying `organization`
- [x] `signup/4` is now `(client_id, password, connection, opts)` with `email` in `opts`, so phone- and
username-based connections need no dummy address. **The arity did not change**, so an unmigrated
call would have been silently misread — the `when is_map(opts)` guard turns it into a
`FunctionClauseError` instead, and a test pins that
- [x] `client_credentials/4` takes `opts`, carrying `organization`, `client_assertion` and
`client_assertion_type`. Pass `nil` for `client_secret` when using Private Key JWT; nil values are
pruned, so it is omitted
- [x] `auth0-forwarded-for` — supported on `Token.password/4` via `:forwarded_for`, so brute-force
protection sees the end user's address rather than the server's
## 5. Management API
Ordered by value. Counts are endpoints in each group.
- [x] **`Role` (13 of 13)**`Auth0Client.Management.Role`. CRUD, permissions, users and groups. The group is
fully covered
- [x] **`User` RBAC**`assign_roles/2`, `remove_roles/2`, the permissions endpoints, `organizations/2`,
and the `effective-*` lookups that resolve access inherited through groups
- [x] **Complete `User` (39 of 39)** — authentication methods, sessions and refresh-token revocation,
`revoke_access/2`, groups, connected accounts and risk-assessment clearing. The group is fully covered
- [x] **Complete `Job` (5 of 5)**`users_imports/3`, `users_exports/1`, `get/1` and `errors/1`. Bulk
migration works in both directions; import required multipart support, added in §1
- [x] **`Organization` (37 of 42)**`Auth0Client.Management.Organization`. CRUD, members, member roles,
invitations, connections, client grants, discovery domains and groups. The five `/enabled_connections`
endpoints are deliberately not wrapped — see §7
- [x] **`Guardian` (30 of 36)**`Auth0Client.Management.Guardian`. Factors, policies, enrollment tickets, and
phone, push and Duo provider configuration. Pairs with `User.enrollments/1` and
`User.delete_authenticators/1`, which previously had no configuration side. The six
`/guardian/factors/sms/*` endpoints are deprecated and not wrapped — see §7
- [x] **`Grant` (3) + `UsersByEmail` (1) + `Tenant` (2)**`Auth0Client.Management.Grant` and
`Auth0Client.Management.Tenant`. `users-by-email` is `User.by_email/2` rather than a one-function module,
since that is where a reader looks for it; the group is still fully covered
- [x] **`Action` (14 of 24)**`Auth0Client.Management.Action`. Actions CRUD, deploy, test, versions and
rollback, executions, triggers and bindings. Closes the gap left by removing `Rule` in §3, ahead of
that end-of-life date. Actions themselves are current and actively developed — none of their
endpoints is deprecated. The 10 `/actions/modules` endpoints are deferred — see §7
- [x] **`CustomDomain` (9) + `Key` (14)**`Auth0Client.Management.CustomDomain` and
`Auth0Client.Management.Key`. Domain registration, DNS verification and default promotion; signing-key
rotation and revocation, encryption keys including bring-your-own-key, and custom signing keys.
Both groups fully covered
- [x] **Complete `Client` (14) + `ClientGrant` (6)** — credentials, CIMD registration and connections on
`Client`; `get/1` and `organizations/2` on `ClientGrant`. Pairs with the §4 `client_credentials/4`
fix so Private Key JWT works end to end rather than half-built. Both groups fully covered
- [x] **Complete `Connection` (28)**`Auth0Client.Management.Connection` gains per-client enablement,
connection keys and the AD/LDAP status check; `Connection.Scim` (9) and
`Connection.DirectoryProvisioning` (9) cover inbound and outbound provisioning, including the two
tenant-level listings that sit under their own OpenAPI tags. Three groups fully covered, and the last
core resource that was partial for want of work
- [x] **`Group` (7) + `Session` (4) + `RefreshToken` (5)** — one theme: make addressable the things `User`
and `Role` already referenced. `groups` had seven endpoints and no module at all, while
`User.effective_role_source_groups/2` named groups as the reason a user held a role and nothing could
open one; `User.sessions/2` and `refresh_tokens/2` listed objects that could only be destroyed all at
once. Three groups fully covered
**§5's ordered list is complete.** What remains is the next tier below, which has never been ranked
against itself.
**Next tier:** `EmailTemplate` (4), `LogStream` (5), `AttackProtection` (12) + `anomaly` (2), `Prompt` (10),
`Branding` (23), `NetworkAcl` (6), `Flow` (13), `Form` (5), `stats` (2), `self-service-profiles` (9),
`event-streams` (10), `connection-profiles` (7), `user-attribute-profiles` (7),
`token-exchange-profiles` (5).
**Will not support:** `hooks` (9) and `rules-configs` (3) — both legacy, superseded by Actions.
**Deprioritized as niche:** `experimentation` (23), `verifiable-credentials`, `supplemental-signals`,
`rate-limit-policies`, `risk-assessments`.
`GET /events` is Server-Sent Events and likely needs transport work in `lib/auth0_client/api.ex` rather than just
a new module.
## 6. Caveats in endpoints that already work
These are correct but incomplete — worth folding into whichever module you touch next.
- [x] `ResourceServer.all/1` takes params, so `identifiers` and pagination are reachable
- [x] `Log.search/1` documents the 100-record cap, the 1,000-result ceiling and the deprecated
`include_totals`
- [x] `User.all/1` — v3 search semantics and the 1,000-record cap are now documented on the function, with
a pointer to `Job.users_exports/1` for anything larger. `get/2` also accepts a keyword list now,
matching `all/1`
- [x] `Client.all/1` documents that `q` needs checkpoint pagination and carries reduced rate limits
- [x] `Connection.all/1` documents checkpoint pagination past 1,000 connections
- [x] `Connection.update/2` — the invalid example is replaced, and the immutability of `name` and
`strategy` documented. The `enabled_clients` replacement it pointed at now exists:
`Connection.update_clients/2`, which changes one client at a time rather than replacing the list and
racing anyone else editing the connection
- [x] `UserBlock.get/2` and `get_user_block/2` accept params, so `consider_brute_force_enablement` is
reachable
- [x] `EmailProvider.configure/1` — the example includes the required `credentials`
- [x] `User.enrollments/1` — the `@doc` claimed "all Guardain enrollments"; corrected to say Auth0 returns
only the first confirmed enrollment
- [x] `Job.send_verification_email/1` — the empty-body default is gone, so a call without `user_id` fails
to compile rather than at runtime
- [x] `Ticket.email_verification/2` — the override is documented rather than changed, since callers may
rely on it
- [x] `build_url/2` appends `?` only when there is a query string. Fixing it surfaced a latent bug:
`query_string/1` used `Map.merge`, so calling the public `build_url/2` with a keyword list raised
`BadMapError` — it normalises now
## 7. Reliability and token handling
Raised by a downstream dependency audit of this library (2026-07-21) and still open. None are API-parity
issues; they are the things that bite under load or partial failure.
- [x] **`Parser` discarded response headers**, making `Retry-After` unreachable. Failures are now a single
`Auth0Client.Error` struct carrying `reason`, `status`, `body` and `headers`, with
`Auth0Client.Error.retry_after/1` to read the backoff. This also collapsed the two error shapes into one,
which was itself a source of `FunctionClauseError` in a consuming app
- [x] **Token expiry had no skew margin, no single-flight, and no invalidation on 401.** `TokenState` is now
a GenServer that owns the token: one fetch in flight at a time with all callers waiting on it,
renewal `Config.token_refresh_skew/0` seconds before expiry (60 by default), and a 401 on a
Management call evicts the token and replays the request once
- [x] **`TokenState` exposed the management JWT** through a public `get_all/0`. The key-value API is gone —
only `fetch/0` and `invalidate/0` remain — and an `Inspect` implementation redacts the token so it
cannot reach a crash dump or a stray `inspect`
- [x] **`http_opts` was an unvalidated pass-through** to `Req.request/1`, so a caller could disable
certificate verification through it — `connect_options: [transport_opts: [verify: :verify_none]]`
and send the management client secret over an unauthenticated connection with nothing flagging it.
`Config.http_opts/0` now refuses that setting, naming both the correct fix (`cacerts`/`cacertfile`)
and the deliberate opt-out. See the settled note below for the scoping
### Settled, recorded so it is not re-litigated
- **Every 2xx is a success, and a 2xx with an empty body means `:ok`.** Auth0 answers equivalent mutations
with 200, 201 or 204 depending on the endpoint, and accepted-but-asynchronous ones — deploying an action
— with 202. `Parser` treats the whole 200..299 range as success and collapses the empty-bodied ones to
`:ok`, so callers never get a meaningless `{:ok, ""}` and no endpoint needs its own special case.
- **`/actions/modules` is deferred, not rejected.** The 10 endpoints share reusable code between actions,
which is orthogonal to replacing a Rule. This is why `actions` reads 14/24.
- **`/guardian/factors/sms/*` is not wrapped.** Five of the six say "This endpoint has been deprecated" in
their own description and name a `/guardian/factors/phone/*` replacement taking the same payload; the
sixth says a new phone endpoint with the same payload exists. Applying §3's rule, they are excluded —
which is why `guardian` reads 30/36. Note they all read `deprecated: false` in the OpenAPI document,
which is what prompted the methodology correction at the top of this file.
- **`/organizations/{id}/enabled_connections` is deliberately not wrapped.** Auth0 ships it alongside
`/organizations/{id}/connections` with the same semantics; neither is formally deprecated, but
`/connections` takes a superset of the body (`organization_connection_name`, `organization_access_level`,
`is_enabled`). Wrapping both would ship two near-identical function families and make callers guess.
This is why `organizations` reads 37/42 rather than fully covered — it is a choice, not a gap.
- **Some endpoints take a request body on `DELETE`.** Unusual, but documented — the RBAC remove operations
work this way. `do_delete_body/3` exists for exactly those; `do_delete/3` still means query params.
- **`do_put` sends a list as a JSON array; `do_post` and `do_patch` read a *keyword* list as an object.**
Not an oversight — a list body is ambiguous and each verb resolves it the way its callers need.
`User.replace_authentication_methods/2` and `Guardian.update_policies/1` depend on the PUT behaviour, and
converting a list there raises `ArgumentError`. Do not "unify" these either.
Refined once, when `Connection.update_clients/2` needed to PATCH a JSON array: `keyword_to_object/1`
now tests `Keyword.keyword?/1` rather than `is_list/1`, so a non-keyword list passes through to POST and
PATCH as an array instead of raising `ArgumentError` on `Enum.into/2`. That is not the unification this
note forbids — keyword lists still become objects, `do_put` is untouched, and the old behaviour on a
non-keyword list was a crash, so nothing could depend on it.
- **Early Access is not a reason to skip an endpoint; deprecated is.** §3's rule is about endpoints Auth0
is retiring, not ones it is still introducing. `x-release-lifecycle: EA` appears on 37 operations, and
this library already shipped several before the flag was ever examined — `Organization.connections/2`
among them — so excluding `GET /refresh-tokens` and `POST /refresh-tokens/revoke` would have invented a
policy mid-stream and left `refresh-tokens` partial for no stated reason. EA endpoints are wrapped, with
the lifecycle noted in the `@doc` so a caller can weigh it. The 23 `beta` operations are a separate
question, and all of them currently fall in groups deprioritized as niche.
- **Request encoding is decided per endpoint, from that endpoint's own docs — not by a house convention.**
`/oauth/token`, `/oauth/revoke`, `/oauth/par` and `/oauth/device/code` are form-encoded, as RFC 6749 and
Auth0 both specify. `/dbconnections/*` is JSON: it carries `user_metadata`, typed `object` in the docs,
and form encoding has no representation for a nested object — `URI.encode_query/1` raises on one.
`/jobs/users-imports` is `multipart/form-data`, which Auth0 accepts in no other encoding. Three
encodings, each chosen per endpoint. Do not "unify" these.
**Not even a shared path prefix is evidence.** `/oauth/global-token-revocation` takes JSON while every
other `/oauth/*` endpoint here takes a form. And where a form-encoded endpoint genuinely needs a nested
object — `user_profile` on the native-social grant — Auth0's answer is JSON inside one string field, so
`Token.native_social/4` encodes that single value itself. That transform belongs in the function, not in
`post_form/3`, which would encode fields Auth0 expects flat.
- **TLS verification is enforced.** Re-verified on the Req/Finch path after the migration: expired,
hostname-mismatch and self-signed certificates are all rejected, a valid host succeeds. Trust now comes
from OTP's system CA store via Mint rather than `certifi` via hackney, so the pre-migration evidence chain
no longer applies — this is the current one.
**And configuration can no longer switch it off by accident.** Three choices worth keeping:
- **Refused by default, with a loudly-named opt-in**`dangerously_disable_tls_verification: true`,
which also logs a warning at boot. A hard block with no escape hatch strands anyone behind a corporate
MITM appliance, and the realistic outcome there is a fork or a version pin, not a fixed trust store.
Typing those words is deliberate in a way that copying a config snippet is not.
- **Only `verify: :verify_none` is rejected.** `verify_fun` can weaken verification but is also how
pinning is done, and `cacerts`/`cacertfile` are the *correct* fix for a private CA — rejecting either
would push people toward the very setting this exists to stop. The error message names `cacertfile`
for that reason, since people reach for `:verify_none` mostly in ignorance of the alternative.
- **Validated on every read of `Auth0Client.Config.http_opts/0`, not once at boot**, so a runtime
`Application.put_env/3` cannot slip past. The traversal is two `Keyword.get/2` calls on a list that is
usually empty. `connect_options → transport_opts` is the only TLS path Req exposes, so there is one
spelling to check rather than several.
One hole remains and is documented rather than closed: `http_opts: [finch: MyPool]` uses a pool started
elsewhere, whose TLS settings this library never sees.
- **The `httpoison ~> 2.2` resolver constraint is gone.** It previously blocked consumers from moving to
httpoison 3.0 / hackney 4.0. Dropping HTTPoison removes hackney from this library's dependency tree
entirely, along with the EOL 1.x advisories it carried
- **Error bodies are now decoded maps** rather than raw JSON strings. Consumers that called `Jason.decode!/1`
on `{:error, body, status}` themselves must drop that call — it will raise on an already-decoded map