OpenIDConnect (OpenID Connect v1.0.0)

View Source

OpenID Connect client library for Elixir.

This library provides a complete implementation of the OpenID Connect authentication protocol, which is built on top of OAuth 2.0. It handles the complete authentication flow including:

  • Generating authorization URIs for redirecting users to identity providers
  • Fetching tokens from the token endpoint
  • Verifying JWT ID tokens using provider JWKs (JSON Web Keys)
  • Fetching user information from the userinfo endpoint
  • Supporting provider logout via end session endpoints

Supported Authentication Flows

The library supports the standard OpenID Connect authentication flows:

  • Authorization Code Flow (most common for web applications)
  • Implicit Flow (less secure, used for JavaScript applications)
  • Hybrid Flow (combines aspects of both)

Supported Identity Providers

This library works with any OpenID Connect compliant provider, including:

  • Google
  • Microsoft Azure AD
  • Auth0
  • Okta
  • Amazon Cognito
  • Keycloak
  • OneLogin
  • HashiCorp Vault
  • And many others

Basic Usage

# Step 1: Configure the provider
google_config = %{
  discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration",
  client_id: "YOUR_CLIENT_ID",
  client_secret: "YOUR_CLIENT_SECRET",
  response_type: "code",
  scope: "openid email profile"
}

# Step 2: Generate the authorization URI (redirect the user to this URI)
{:ok, uri} = OpenIDConnect.authorization_uri(
  google_config,
  "https://example.com/auth/callback",
  %{state: state_token}
)

# Step 3: Exchange the authorization code for tokens
{:ok, tokens} = OpenIDConnect.fetch_tokens(
  google_config,
  %{
    code: auth_code,
    redirect_uri: "https://example.com/auth/callback"
  }
)

# Step 4: Verify the ID token
{:ok, claims} = OpenIDConnect.verify(google_config, tokens["id_token"])

# Optional: Fetch additional user information
{:ok, userinfo} = OpenIDConnect.fetch_userinfo(google_config, tokens["access_token"])

See the README for more detailed examples and configuration options.

Summary

Types

OAuth 2.0 Client Identifier valid at the Authorization Server.

OAuth 2.0 Client Secret valid at the Authorization Server.

The configuration of a OpenID provider.

JSON Web Token

Redirection URI to which the response will be sent.

OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what parameters are returned from the endpoints used.

OAuth 2.0 Scope Values that the Client is declaring that it will restrict itself to using.

Functions

Builds an authorization URI for redirecting users to the identity provider.

Builds a URI for ending the user's session with the identity provider.

Fetches authentication tokens from the provider using the token endpoint.

Fetches information about the authenticated user from the userinfo endpoint.

Verifies the validity and authenticity of an OpenID Connect ID token.

Types

client_id()

@type client_id() :: String.t()

OAuth 2.0 Client Identifier valid at the Authorization Server.

client_secret()

@type client_secret() :: String.t()

OAuth 2.0 Client Secret valid at the Authorization Server.

config()

@type config() :: %{
  :discovery_document_uri => discovery_document_uri(),
  :client_id => client_id(),
  :client_secret => client_secret(),
  :response_type => response_type(),
  :scope => scope(),
  optional(:leeway) => non_neg_integer()
}

The configuration of a OpenID provider.

discovery_document_uri()

@type discovery_document_uri() :: String.t()

URL to a OpenID Discovery Document endpoint.

jwt()

@type jwt() :: String.t()

JSON Web Token

See: https://jwt.io/introduction/

redirect_uri()

@type redirect_uri() :: String.t()

Redirection URI to which the response will be sent.

This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison).

When using this flow, the Redirection URI SHOULD use the https scheme; however, it MAY use the http scheme, provided that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0, and provided the OP allows the use of http Redirection URIs in this case. The Redirection URI MAY use an alternate scheme, such as one that is intended to identify a callback into a native application.

response_type()

@type response_type() :: [String.t()] | String.t()

OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what parameters are returned from the endpoints used.

scope()

@type scope() :: [String.t()] | String.t()

OAuth 2.0 Scope Values that the Client is declaring that it will restrict itself to using.

Functions

authorization_uri(config, redirect_uri, params \\ %{})

@spec authorization_uri(
  config(),
  redirect_uri :: redirect_uri(),
  params :: %{optional(atom()) => term()}
) :: {:ok, uri :: String.t()} | {:error, term()}

Builds an authorization URI for redirecting users to the identity provider.

This function creates a URL that starts the OpenID Connect authentication flow by redirecting the user to the provider's authorization endpoint. The endpoint URL is automatically retrieved from the provider's discovery document.

Parameters

  • config - The provider configuration map
  • redirect_uri - URI to redirect to after authentication (must match one registered with the provider)
  • params - Optional map of additional query parameters to include in the authorization URI

Common Additional Parameters

  • state - Strongly recommended - Random token to prevent CSRF attacks
  • prompt - Controls the login experience, common values include:
    • "none" - No interactive prompt, fails if user authentication is required
    • "login" - Forces the user to enter their credentials even if already logged in
    • "consent" - Forces the consent screen to be displayed even if previously consented
    • "select_account" - Prompts the user to select an account
  • login_hint - Email address or sub identifier to pre-fill the login form
  • max_age - Maximum elapsed time in seconds since last authentication
  • ui_locales - Preferred languages for the UI, space-separated list of BCP47 tags
  • acr_values - Authentication Context Class Reference values

Provider-Specific Parameters

Some providers support additional custom parameters:

  • Google: hd (hosted domain) to restrict to specific Google Workspace domains
  • Azure AD: domain_hint to skip the home realm discovery page
  • Many others based on the provider

Returns

  • {:ok, uri} - On success, returns the authorization URI string
  • {:error, reason} - On failure, returns an error tuple with details

Example: Basic Usage

{:ok, uri} = OpenIDConnect.authorization_uri(
  google_config,
  "https://example.com/auth/callback",
  %{state: state_token}
)

Example: With Additional Parameters

{:ok, uri} = OpenIDConnect.authorization_uri(
  google_config,
  "https://example.com/auth/callback",
  %{
    state: state_token,
    prompt: "login",
    login_hint: "user@example.com",
    hd: "example.com"  # Google-specific parameter
  }
)

Security: State Parameter

The state parameter is critical for preventing cross-site request forgery attacks. Always include a state parameter with a secure random value and validate it when the user is redirected back to your application.

Creating a State Token

state_token = Base.encode64(:crypto.strong_rand_bytes(32), padding: false)

# Store the token in your session
conn = Plug.Conn.put_session(conn, :oidc_state_token, state_token)

# Include it in the authorization URI
{:ok, uri} = OpenIDConnect.authorization_uri(
  google_config,
  redirect_uri,
  %{state: state_token}
)

Validating the State Token

When the provider redirects back to your application, verify the state parameter:

stored_token = Plug.Conn.get_session(conn, :oidc_state_token)
received_token = params["state"]

if stored_token && Plug.Crypto.secure_compare(stored_token, received_token) do
  # State token is valid, proceed with token exchange
else
  # Invalid state token, reject the request
end

For more details on state tokens, see Google's Documentation.

end_session_uri(config, params \\ %{})

@spec end_session_uri(config(), params :: %{optional(atom()) => term()}) ::
  {:ok, uri :: String.t()} | {:error, term()}

Builds a URI for ending the user's session with the identity provider.

This function creates a URL for OpenID Connect RP-Initiated Logout, allowing your application to sign the user out of the identity provider's session. The endpoint URL is automatically retrieved from the provider's discovery document.

Parameters

  • config - The provider configuration map
  • params - Optional map of additional query parameters for the end session URI

Common Additional Parameters

  • id_token_hint - The ID token previously issued to the user (strongly recommended)
  • post_logout_redirect_uri - URI to redirect to after logout (must be registered with provider)
  • state - Opaque value for maintaining state between logout request and callback

Provider Requirements

Different providers have different requirements for logout:

  • Azure AD: Requires post_logout_redirect_uri and may accept id_token_hint
  • Auth0: Requires client_id and returnTo (instead of post_logout_redirect_uri)
  • Cognito: Requires client_id and logout_uri (their name for redirect URI)
  • Okta: Requires id_token_hint and accepts post_logout_redirect_uri
  • Keycloak: Accepts id_token_hint and post_logout_redirect_uri

Always consult your provider's documentation for specific requirements.

Returns

  • {:ok, uri} - On success, returns the end session URI string
  • {:error, :endpoint_not_set} - If the provider doesn't support RP-Initiated Logout
  • {:error, reason} - On other failures, returns an error tuple with details

Provider Support Note

Not all providers support RP-Initiated Logout. If the provider's discovery document doesn't include an end_session_endpoint, this function will return {:error, :endpoint_not_set}. In such cases, you'll need to implement session termination in your application only.

Example: Azure AD Logout

{:ok, logout_uri} = OpenIDConnect.end_session_uri(
  azure_config,
  %{
    post_logout_redirect_uri: "https://example.com/logged-out"
  }
)

# Redirect the user to the logout_uri
redirect(conn, external: logout_uri)

Example: With ID Token Hint

{:ok, logout_uri} = OpenIDConnect.end_session_uri(
  okta_config,
  %{
    id_token_hint: id_token,
    post_logout_redirect_uri: "https://example.com/logged-out"
  }
)

For more details, see the OpenID Connect RP-Initiated Logout Specification.

fetch_tokens(config, params)

@spec fetch_tokens(config(), params :: %{optional(atom()) => term()}) ::
  {:ok, response :: map()} | {:error, term()}

Fetches authentication tokens from the provider using the token endpoint.

This function exchanges the authorization code or refresh token for access and ID tokens from the identity provider's token endpoint. The endpoint URL is automatically retrieved from the provider's discovery document.

Parameters

  • config - The provider configuration map
  • params - A map of parameters to send to the token endpoint

Common Parameters

The params map should include different fields depending on the grant type:

Authorization Code Grant (most common)

%{
  code: "AUTHORIZATION_CODE_FROM_CALLBACK",
  redirect_uri: "https://example.com/callback", # Must match the original redirect_uri
  grant_type: "authorization_code" # Optional, defaults to "authorization_code"
}

Refresh Token Grant

%{
  refresh_token: "REFRESH_TOKEN",
  grant_type: "refresh_token"
}

Returns

  • {:ok, tokens} - On successful token exchange, returns a map containing at minimum:

    • "access_token" - The OAuth 2.0 access token
    • "token_type" - The token type (typically "Bearer")
    • "expires_in" - Token lifetime in seconds
    • "id_token" - The OpenID Connect ID token (JWT)
    • "refresh_token" - Optional refresh token for obtaining new access tokens
  • {:error, reason} - On failure, returns an error tuple with reason

Example

{:ok, tokens} = OpenIDConnect.fetch_tokens(
  google_config,
  %{
    code: params["code"],
    redirect_uri: "https://example.com/auth/callback" 
  }
)

# Access the tokens
access_token = tokens["access_token"]
id_token = tokens["id_token"]
refresh_token = tokens["refresh_token"]

For more details, see the OpenID Connect spec.

fetch_userinfo(config, access_token)

@spec fetch_userinfo(config(), jwt()) :: {:ok, response :: map()} | {:error, term()}

Fetches information about the authenticated user from the userinfo endpoint.

This function retrieves additional claims about the end-user from the provider's userinfo endpoint using the access token. This can be useful when you need user attributes that weren't included in the ID token claims.

Parameters

  • config - The provider configuration map
  • access_token - The OAuth 2.0 access token obtained from fetch_tokens/2

Returns

  • {:ok, userinfo} - On success, returns a map of user information
  • {:error, reason} - On failure, returns an error tuple with details

Userinfo Claims

The returned claims will depend on the scopes requested during authorization, but typically include:

  • "sub" - Subject identifier (unique user ID)
  • "name" - User's full name (if "profile" scope)
  • "given_name" - First name (if "profile" scope)
  • "family_name" - Last name (if "profile" scope)
  • "email" - Email address (if "email" scope)
  • "email_verified" - Boolean indicating if email is verified (if "email" scope)
  • "picture" - URL to profile picture (if "profile" scope)

Example

{:ok, userinfo} = OpenIDConnect.fetch_userinfo(google_config, access_token)

# Access user information
user_id = userinfo["sub"]
email = userinfo["email"]
name = userinfo["name"]

Security Note

The userinfo endpoint requires a valid access token. The token must have appropriate scopes (typically "openid" and others like "profile" or "email").

For more details, see the OpenID Connect UserInfo Endpoint.

verify(config, jwt)

@spec verify(config(), jwt :: String.t()) :: {:ok, claims :: map()} | {:error, term()}

Verifies the validity and authenticity of an OpenID Connect ID token.

This security-critical function verifies that:

  1. The token's signature is valid and was signed by the provider's key
  2. The token has not expired (checking the "exp" claim)
  3. The token is intended for your application (checking the "aud" claim)

Parameters

  • config - The provider configuration map
  • jwt - The ID token string (a JSON Web Token) from the tokens response

Returns

  • {:ok, claims} - On successful verification, returns the decoded claims from the token
  • {:error, reason} - On verification failure, returns an error tuple with details

Verification Process

  1. The signature is verified using the provider's JSON Web Keys (JWKs), which are fetched from the provider's JWKs endpoint listed in the discovery document.
  2. The "exp" (expiration) claim is checked to ensure the token has not expired. A configurable leeway (default: 30 seconds) is allowed to account for clock skew.
  3. The "aud" (audience) claim is verified to ensure the token is intended for your application, as identified by your client_id.

Example

{:ok, claims} = OpenIDConnect.verify(google_config, id_token)

# Common claims you might find in the response:
user_id = claims["sub"]         # Unique user identifier
email = claims["email"]         # User's email (if in scope)
name = claims["name"]           # User's name (if in scope)
picture = claims["picture"]     # URL to user's profile picture (if available)
issuer = claims["iss"]          # Identifies the token issuer

Security Warning

Always verify tokens before trusting their contents. Never use token data for authentication purposes without verification, as tokens could be forged or tampered with.