View Source UeberauthOrcid

Hex version CI HexDocs License: MIT

An Ueberauth strategy for authenticating an ORCID identity through the authorization-code flow and /oauth/userinfo. It supplies an Ueberauth.Auth result, not an account database, a session manager, a general ORCID API client, or an ID-token validator.

See the changelog, contributor/release guide, and security policy.

1. Install

In your application's mix.exs, add a normal runtime dependency:

defp deps do
  [
    {:ueberauth_orcid, "~> 0.3.0"}
  ]
end

Fetch dependencies with mix deps.get.

Do not add a new application module or manually add :ueberauth_orcid to extra_applications. Mix infers runtime applications from dependencies and starts them with your application. Keep this dependency at runtime (not runtime: false); applications that explicitly override Mix's :applications list must maintain that list themselves.

Supported toolchains

The locally checked pairs are:

RoleElixirErlang/OTP
Minimum compatibility1.14.525.3.2.21
Additional compatibility1.18.427.3.4
Canonical development1.20.427.3.4

The package declares Elixir ~> 1.14; the default TLS transport requires OTP 25.1 or newer. Use a supported Elixir/OTP pairing, not every combination of those versions. On Elixir 1.14, also add {:plug, "~> 1.19.5"} to your app's dependencies: Plug 1.20 requires Elixir 1.15, and an unlocked resolution does not automatically choose the older compatible branch. The app's lockfile, not this library's lockfile, determines its installed dependencies.

2. Register ORCID and configure runtime credentials

Follow ORCID's client registration guide, including the applicable Public API terms or Member API registration. Register the full callback URI, including its path; ORCID requires matching domains and separately registered subdomains. Registering only a host permits broader callback paths and is not recommended. See ORCID's redirect URI rules.

EnvironmentRegistration and accountsOAuth siteExample registered callback
ProductionProduction developer tools; production credentials/accountshttps://orcid.orghttps://app.example.org/auth/orcid/callback
Sandbox/localSandbox developer tools; separate sandbox credentials/accountshttps://sandbox.orcid.orghttp://localhost:4000/auth/orcid/callback

Production callbacks must use HTTPS. HTTP is for local sandbox testing only; your app must actually listen on the registered host/port. Keep sandbox identities, credentials, and stored tokens separate from production. Record API hosts such as pub.orcid.org and api.orcid.org are not the login site.

Put this in your application's config/runtime.exs. Supply the three environment variables from your deployment's secret/configuration mechanism, never source control. The provider name :orcid defines the default routes and returned auth.provider.

import Config

callback_url = System.fetch_env!("ORCID_REDIRECT_URI")

config :ueberauth, Ueberauth,
  providers: [
    orcid: {Ueberauth.Strategy.Orcid, [callback_url: callback_url]}
  ]

config :ueberauth, Ueberauth.Strategy.Orcid.OAuth,
  client_id: System.fetch_env!("ORCID_CLIENT_ID"),
  client_secret: System.fetch_env!("ORCID_CLIENT_SECRET"),
  redirect_uri: callback_url,
  site: "https://orcid.org"

config :oauth2, debug: false

For sandbox, change site in that same configuration to "https://sandbox.orcid.org" and supply sandbox values for all three variables. Relative authorization/token endpoints follow site; do not retain absolute production endpoint overrides when switching environments.

There is no implicit localhost callback. The explicit provider callback_url takes precedence over the OAuth client's redirect_uri, and the strategy sends that effective URI in both authorization and token exchange. Configure both to the same registered value as above. Direct OAuth helper calls also need an explicit effective redirect_uri for authorization/token exchange. Missing, blank, or wrong-type credentials raise an ArgumentError naming the key, not its value. Legacy {:system, "ENV_NAME"} credential values still work.

JSON contract

Jason is a runtime dependency and the default serializer; production does not need Credo or ExDoc to decode JSON. An existing override remains supported:

config :ueberauth, Ueberauth, json_library: Jason

A replacement must be a runtime module implementing encode!/1 and decode!/1 as expected by Ueberauth/OAuth2; decoded JSON objects must have string keys. Phoenix's own JSON configuration does not configure this client. This helper uses the global Ueberauth.json_library/0 selection. Known Jason/Poison parse errors are normalized; arbitrary serializer/programming errors still propagate.

3. Wire a browser callback

The following is a complete read-only Plug router, usable with your existing Plug-compatible HTTP server. It deliberately displays only the authenticated identity: it creates no account, persists no tokens, and establishes no logged-in account session. Visit / and follow the link to start; do not visit the callback directly. Place the module in your application's lib/ and configure your server to call MyApp.OrcidRouter on the registered port.

Supply SECRET_KEY_BASE as a cryptographically random secret of at least 64 bytes (the existing Phoenix endpoint secret is suitable). Configure TLS and host validation at your endpoint/reverse proxy before this router. In production, redirect HTTP to HTTPS before handling authentication; normalize the connection's scheme only from a trusted proxy. The conditional cookie flag below permits local HTTP sandbox development, not production HTTP.

defmodule MyApp.OrcidRouter do
  use Plug.Router

  plug :browser_session
  plug Plug.CSRFProtection
  plug :authenticate
  plug :match
  plug :dispatch

  get "/" do
    conn
    |> put_resp_content_type("text/html")
    |> send_resp(200, ~s(<a href="/auth/orcid">Authenticate with ORCID</a>))
  end

  get "/auth/orcid/callback" do
    case conn.assigns do
      %{ueberauth_failure: %Ueberauth.Failure{}} ->
        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(401, "ORCID authentication failed. Start again from /.")

      %{ueberauth_auth: %Ueberauth.Auth{provider: :orcid, uid: uid}} ->
        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(200, "Authenticated ORCID iD: #{uid}\n")

      _ ->
        send_resp(conn, 400, "No authentication result")
    end
  end

  match _ do
    send_resp(conn, 404, "Not found")
  end

  defp browser_session(conn, _opts) do
    conn = %{conn | secret_key_base: System.fetch_env!("SECRET_KEY_BASE")}

    opts = Plug.Session.init(
      store: :cookie,
      key: "_my_app_session",
      signing_salt: "orcid-browser-session",
      same_site: "Lax",
      http_only: true,
      secure: conn.scheme == :https
    )

    conn
    |> Plug.Session.call(opts)
    |> fetch_session()
    |> put_resp_header("cache-control", "no-store")
    |> put_resp_header("referrer-policy", "no-referrer")
  end

  defp authenticate(conn, _opts) do
    conn = fetch_query_params(conn)

    if conn.request_path == "/auth/orcid" and Map.has_key?(conn.query_params, "scope") do
      conn |> send_resp(400, "Scope overrides are not enabled") |> halt()
    else
      Ueberauth.call(conn, Ueberauth.init([]))
    end
  end
end

Ueberauth intercepts GET /auth/orcid and halts with an ORCID redirect; the callback continues to the success/failure branches above. Runtime initialization in authenticate/2 ensures the providers from runtime.exs are available. The request guard is this example's fixed-scope policy, not a library default. The module requires only the existing Plug/Ueberauth dependencies; it is also callable through Plug.Test without creating a Phoenix project.

For an existing Phoenix app, use its endpoint's Plug.Session and browser pipeline (fetch_session, protect_from_forgery, secure browser headers), and route both GET /auth/orcid and GET /auth/orcid/callback to an auth controller. Run Ueberauth before those actions and handle its two assignments exactly as above; do not run a second session stack or require an already logged-in user on the login/callback routes. Preserve a request-scope policy before Ueberauth.

State, cookies, and trusted URLs

Ueberauth's OAuth state check is distinct from your app's ordinary form CSRF protection. It uses the ueberauth.state_param cookie (SameSite Lax in Ueberauth 0.10.8), requires the matching callback state, and needs the session plug installed. Keep both protections enabled. Do not disable state checking to fix a cookie problem. The state cookie is separate from your session cookie; setting session attributes does not reconfigure it. Use a top-level GET callback, the same browser and host throughout, HTTPS in production, and trusted proxy normalization. Starting a second flow in another tab can replace the state cookie.

Prefer an explicit callback_url. Without one, Ueberauth 0.10.8's URL helper consults x-forwarded-proto, x-forwarded-host, and Host headers as well as the connection. It does not establish proxy trust or validate allowed hosts for you. Strip untrusted forwarding headers at the edge, allowlist hosts, and normalize scheme/port before authentication. Never rely on arbitrary client-supplied headers to select a trusted callback. callback_url changes the URI sent to ORCID; its path must still match your app's route and configured callback_path.

Options and scope policy

Provider options go beside callback_url in the providers entry:

Strategy optionDefaultMeaning
uid_field:subUserinfo claim used as UID. A nonblank string sub is always required; any selected alternate claim must also be nonblank. Keep the default for account identity.
default_scope"openid email profile"Requested scope when the request has no scope parameter.
send_redirect_uritrueSend Ueberauth's effective callback for both exchanges. With false, the client's explicit redirect_uri is used instead; it does not make redirects optional.
oauth2_moduleUeberauth.Strategy.Orcid.OAuthImplementation for authorization, non-raising token exchange, and userinfo retrieval; see migration below.

Requested scopes are not proof of granted permissions. The library preserves openid email profile for compatibility. Production and sandbox discovery (sandbox) advertise openid, not email or profile, and list no email claim. This is not evidence that ORCID rejects the additional strings: their acceptance has not been verified in a live flow for this candidate. They do not establish email access.

ORCID's OpenID documentation documents openid as granting access to userinfo. A new integration may explicitly choose default_scope: "openid" for that documented flow; this is an application choice, not a changed library default. Verify it in sandbox before rollout. Do not substitute /authenticate alone: this strategy retrieves identity from userinfo, not the token response's orcid field. Member API permissions are a separate integration concern, not implied by login.

Without the example's guard, conn.params["scope"] overrides default_scope (including an empty string). The library does not allowlist it. If your app needs reauthorization, validate a server-owned set of allowed scopes before Ueberauth; do not pass arbitrary user input or interpret requested scopes as authorization.

Common Ueberauth settings

These are inherited from Ueberauth, not additional ORCID features:

SettingDefault / behavior
base_pathGlobal/plug option, "/auth".
request_pathProvider option, "/auth/orcid" for this registration; absolute path. Request method is fixed to GET in Ueberauth 0.10.8; there is no request_methods option.
callback_pathProvider option, "/auth/orcid/callback"; absolute path.
callback_methodsProvider option, ["GET"]; changing it also requires matching host routes/provider behavior and cookie/CSRF handling. Keep GET for this flow.
callback_urlUnset; explicit full URI recommended instead of header-derived URL.
callback_scheme, callback_portUnset; override generated callback components when no explicit URL is supplied.
request_scheme, request_portUnset; affect Ueberauth's request-URL helper, not ORCID endpoint selection.
callback_paramsNo parameters copied by default; allowlisted request parameter names included in a generated callback URL. Avoid for this fixed registered callback.
providers, otp_appPlug options for provider filtering/application configuration; see upstream documentation. OAuth credentials and this helper's serializer still use the global configuration shown above.

allow_private_emails is not a supported option and has no effect. There is no ORCID provider option that grants private-email access or replaces state security.

OAuth client and HTTP options

Configure Ueberauth.Strategy.Orcid.OAuth under application :ueberauth:

OptionDefault / behavior
client_id, client_secretRequired nonblank strings, or legacy environment tuples.
redirect_uriNo usable default; required when the strategy/provider does not supply one.
site"https://orcid.org". Userinfo is fetched at /oauth/userinfo on this site.
authorize_url"/oauth/authorize"; relative or explicit absolute URL.
token_url"/oauth/token"; relative or explicit absolute URL.
headers[{"user-agent", "ueberauth-orcid"}]. Never add credentials-bearing logging.
paramsOAuth2 client parameter map, empty by default; flow parameters take precedence.
request_optsHTTP adapter options; Httpc defaults below.

The helper uses its own OAuth2 strategy and POST token exchange; generic OAuth2.Client fields are not promises of extra ORCID features (for example, token_method does not switch this helper away from POST). See the OAuth2 client API for underlying client structures. client/1 merges per-call client options over application configuration over library defaults.

Token requests use form-only client_secret_post, as advertised by ORCID discovery, not redundant Basic authentication. Userinfo uses a Bearer token, without adding the client secret to resource parameters.

Result and application responsibilities

On success, conn.assigns.ueberauth_auth contains:

FieldContract
provider, uid:orcid for the registration above; the selected userinfo claim, normally the bare ORCID sub, unchanged. No email/name fallback or new checksum restriction.
info.nameValid trimmed given/family names joined, falling back to the credit-name claim name; nil if absent/invalid.
info.nicknameValid trimmed credit name (name), otherwise nil.
info.emailAlways nil, even if raw userinfo contains an email. No private/verified-email claim.
credentials.scopesWhitespace-separated granted scopes from the token response; [] when missing/empty. Commas are not delimiters.
credentials.token, token_typeValidated access token and Bearer type from the helper.
credentials.refresh_tokenOptional; do not require or invent it.
credentials.expires_at, expiresOptional expiration timestamp; expires is false when no timestamp exists.
extra.raw_info.user, extra.raw_info.tokenRaw userinfo and token retained for compatibility; sensitive, not safe diagnostics.

Successful userinfo must be an HTTP 200 JSON object with a nonblank sub and selected UID claim. Optional profile omissions do not fail authentication. Temporary :orcid_user and :orcid_token connection-private fields are cleaned up, but this does not remove secrets from the returned auth struct.

The read-only callback above is the integration point for your own account policy. Link an account using the authenticated provider/UID in the correct production/sandbox namespace, never a supplied ORCID string, display name, or unverified email. Linking an additional identity should require an authenticated local account and confirmation; enforce uniqueness and handle conflicts rather than silently merging accounts. After an intentional local login, rotate the session identifier and store only the application's account/session identifier, not the full auth struct. Store tokens only if your application needs them, with access controls and encryption at rest; a signed cookie is not encryption.

The strategy uses an OIDC userinfo endpoint but does not validate an ID token's signature, issuer, audience, expiry, or nonce. Do not treat a raw id_token as a verified assertion or infer MFA from it. Local logout means clearing your own session; it does not revoke ORCID tokens, remove consent, or end the ORCID session.

Upgrading a custom OAuth module

oauth2_module now covers the entire flow. Every configured module must export:

  • authorize_url!(params, client_options) returning an authorization URL.
  • get_token(params, options) returning {:ok, %OAuth2.AccessToken{}} or {:error, reason}; not a client struct or a bare token.
  • get(token, url, headers, options) returning {:ok, %OAuth2.Response{}} or {:error, reason}. A successful userinfo response must satisfy the JSON/status/ identity contract above.

Here is a complete pass-through module; retain your intentional authorization customizations while adding both non-raising operations:

defmodule MyApp.OrcidOAuth do
  defdelegate authorize_url!(params, options), to: Ueberauth.Strategy.Orcid.OAuth
  defdelegate get_token(params, options), to: Ueberauth.Strategy.Orcid.OAuth
  defdelegate get(token, url, headers, options), to: Ueberauth.Strategy.Orcid.OAuth
end

Set oauth2_module: MyApp.OrcidOAuth in the provider options only when you need that customization. Earlier partial wrappers exporting only authorization and get_token! must migrate before upgrading; there is no partial-module fallback. A custom module is trusted code responsible for safe transport and result shapes.

The default helper's public get_token!/2 remains available to direct callers, returning a token or raising a sanitized OAuth2.Error. Both token helpers accept top-level headers: (HTTP headers), options: (HTTP adapter options), and client_options: (client overrides). Authorization headers are removed and form content type enforced for token exchange. get/4 takes HTTP options as its fourth argument. Expected provider failures return tuples; configuration/programming errors are not blanket-rescued. Authorization-code exchange is never automatically retried: codes are one-use credentials.

Transport, logging, and troubleshooting

The default Tesla.Adapter.Httpc transport verifies the certificate chain and HTTPS hostname using OTP's native TLS helper and operating-system CA store. Install CA certificates in deployment images; do not disable verification to work around a missing store. Defaults are a 5-second connection timeout and a 15-second request timeout. Merge timeout overrides into your OAuth configuration:

config :ueberauth, Ueberauth.Strategy.Orcid.OAuth,
  request_opts: [connect_timeout: 5_000, timeout: 15_000]

Explicit Httpc adapter options are merged with client request_opts; client options win, then per-request options win. An explicit ssl: list replaces the default TLS options: custom trust stores must retain verify: :verify_peer and hostname verification. Other adapters are not hardened by this helper; configure their own TLS and timeout options. Default redirects remain disabled. Do not enable automatic redirects for credential-bearing requests.

Keep OAuth2 debug logging off and avoid HTTP payload-logging middleware. Redact codes, state, cookies, client secrets, tokens, and raw auth/provider data from application, proxy, and error-reporting logs; filtering controller parameters alone does not remove query strings from access logs. Never log the whole auth struct or include real credentials/private records in bug reports.

Expected failures assign ueberauth_failure, not a successful auth result. Inspect failure.errors[*].message_key for categories, not raw provider bodies:

Symptom/categoryCheck or response
csrf_attackStart again from the request route. Check the browser's state cookie, matching host, session plug, SameSite behavior, trusted HTTPS proxy settings, and overlapping tabs. Never bypass state validation.
ORCID redirect mismatchCompare the fully registered callback with both configured URIs, route path, scheme, host, and port; check production/sandbox credentials are not mixed.
access_deniedConsent was declined; offer a fresh login without treating it as an application crash.
provider_errorORCID returned another authorization error; offer a fresh attempt and check provider availability/configuration without reflecting its description.
missing_codeNo nonblank authorization code arrived; begin a new authorization flow.
token_exchange_failedInvalid/expired/reused code, credentials/redirect mismatch, rejected/malformed token response, or transport failure. Start a fresh flow; do not retry the code automatically.
invalid_tokenA custom module returned a token that fails strategy validation. Invalid default-helper token responses appear as token_exchange_failed.
userinfo_failedCheck granted openid scope and provider status; includes non-200 responses, redirects, malformed JSON/content type, and transport failures.
invalid_identityUserinfo lacks a nonblank sub or selected UID claim. Do not fall back to names/email.
Missing name/email/refresh tokenThese are optional; handle nil rather than rejecting or inventing profile data. Email is never supplied by info.email.
Configuration ArgumentErrorCheck the named key and runtime environment without printing its value.
TLS/timeouts or provider outagesCheck CA installation, hostname, network/proxy configuration, and ORCID availability. Preserve verification and use bounded timeouts.

Failure descriptions are sanitized: provider descriptions, response bodies, and credentials are not copied into these strategy failures. See CONTRIBUTING.md for maintenance and sandbox procedures and SECURITY.md for private vulnerability reporting.