Where this library stands against the Auth0 API, and what to build next.

Endpoint counts come from Auth0's OpenAPI 3.1 document for the Management API, enumerated programmatically, and from the Authentication API reference 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; the flag is at best a weak positive signal.

Coverage at a glance

APIImplementedTotal
Management v2253451~56%
Authentication21~30plus 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 moduledatabase/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/revokeToken.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 RBACassign_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-indangerously_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