Skip to content

Discovery URL — OIDC Provider Configuration and Endpoint Discovery

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Discovery URL. We cover key concepts, practical examples, and best practices to help you master this topic.

The OIDC discovery URL is a standardized endpoint at /.well-known/openid-configuration that returns a JSON document with the provider's metadata, including all endpoint URLs, supported scopes, and capabilities.

What You'll Learn

  • How the discovery URL works and why it is standardized
  • Key fields in the discovery document
  • Using discovery to dynamically configure OIDC clients

Why It Matters

Hardcoding provider endpoints makes your application fragile. If a provider changes its authorization or token endpoint URL (e.g., during an upgrade), your app breaks. Discovery lets your client dynamically fetch all endpoints at runtime.

Real-World Use

Doda Browser's OIDC client uses the discovery URL to configure itself. It fetches https://accounts.google.com/.well-known/openid-configuration on startup and uses the returned endpoints, supported scopes, and JWKS URI to configure authentication.

flowchart LR
    Client["OIDC Client"] -->|"GET /.well-known/openid-configuration"| Provider["OIDC Provider"]
    Provider -->|"Metadata JSON"| Client
    Client -->|"auth_endpoint"| Auth["Authorization Endpoint"]
    Client -->|"token_endpoint"| Token["Token Endpoint"]
    Client -->|"jwks_uri"| JWKS["JWKS URI"]
    Client -->|"userinfo_endpoint"| UI["UserInfo Endpoint"]
    style Client fill:#dbeafe,stroke:#2563eb

Fetching the Discovery Document

import requests

def discover_provider(issuer_url):
    discovery_url = f"{issuer_url.rstrip('/')}/.well-known/openid-configuration"
    resp = requests.get(discovery_url)
    resp.raise_for_status()
    return resp.json()

# Example: Google's discovery document
config = discover_provider("https://accounts.google.com")
print(f"Authorization endpoint: {config['authorization_endpoint']}")
print(f"Token endpoint: {config['token_endpoint']}")
print(f"UserInfo endpoint: {config['userinfo_endpoint']}")
print(f"JWKS URI: {config['jwks_uri']}")
print(f"Supported scopes: {config['scopes_supported']}")

Expected output:

Authorization endpoint: https://accounts.google.com/o/oauth2/v2/auth
Token endpoint: https://oauth2.googleapis.com/token
UserInfo endpoint: https://openidconnect.googleapis.com/v1/userinfo
JWKS URI: https://www.googleapis.com/oauth2/v3/certs
Supported scopes: ['openid', 'email', 'profile']

Key Discovery Document Fields

discovery_fields = {
    "issuer": "https://accounts.google.com",
    "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
    "token_endpoint": "https://oauth2.googleapis.com/token",
    "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
    "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
    "scopes_supported": ["openid", "email", "profile"],
    "response_types_supported": ["code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"],
    "response_modes_supported": ["query", "fragment", "form_post"],
    "grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:jwt-bearer"],
    "subject_types_supported": ["public"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
    "claims_supported": ["aud", "email", "email_verified", "exp", "family_name", "given_name", "iat", "iss", "locale", "name", "picture", "sub"],
}

Dynamic Client Configuration

class OIDCClient:
    def __init__(self, issuer_url, client_id, client_secret):
        self.config = discover_provider(issuer_url)
        self.client_id = client_id
        self.client_secret = client_secret

    def get_auth_url(self, redirect_uri, state, nonce):
        params = {
            "client_id": self.client_id,
            "response_type": "code",
            "scope": "openid profile email",
            "redirect_uri": redirect_uri,
            "state": state,
            "nonce": nonce,
        }
        auth_endpoint = self.config["authorization_endpoint"]
        import urllib.parse
        return f"{auth_endpoint}?{urllib.parse.urlencode(params)}"

    def exchange_code(self, code, redirect_uri):
        resp = requests.post(self.config["token_endpoint"], data={
            "code": code,
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "redirect_uri": redirect_uri,
            "grant_type": "authorization_code",
        })
        return resp.json()

Common Mistakes

1. Hardcoding Endpoints

If the provider updates its infrastructure and changes endpoints, your app breaks. Always use discovery.

2. Ignoring scopes_supported

Requesting a scope the provider does not support causes errors. Check scopes_supported before building the auth URL.

3. Not Caching the Discovery Document

Fetching discovery on every request is wasteful. Cache the document for the session or up to 24 hours.

4. Assuming All Providers Have the Same Fields

Optional fields like claims_supported, acr_values_supported, and request_parameter_supported may be missing. Handle missing fields gracefully.

5. Forgetting the Trailing Slash

Some providers return 404 if the discovery URL has or lacks a trailing slash. Try both /.well-known/openid-configuration and /.well-known/openid-configuration/.

Practice Questions

  1. What is the standard path for the OIDC discovery document?
  2. Why should you use discovery instead of hardcoding endpoints?
  3. What information does the jwks_uri field provide?
  4. How does scopes_supported help configure the auth request?
  5. Why should the discovery document be cached?

Answers:

  1. The path is /.well-known/openid-configuration relative to the issuer URL.
  2. Hardcoded endpoints break if the provider changes them. Discovery fetches current endpoints dynamically at runtime.
  3. jwks_uri provides the URL to fetch the provider's public keys, used to verify ID token signatures.
  4. scopes_supported tells you which scopes the provider supports. Requesting unsupported scopes causes errors.
  5. The discovery document rarely changes. Caching it for 24 hours reduces startup latency and provider load.

Challenge: Build a generic OIDC client that configures itself entirely from the discovery document. The client should accept only the issuer URL, client ID, and client secret, and derive everything else from discovery.

FAQ

Do all OIDC providers support the discovery endpoint?

: Most do, but it is optional. Google, Microsoft, Okta, Auth0, and Keycloak all support it. Apple does not.

What happens if the discovery request fails?

: Fall back to a cached version if available, or use hardcoded endpoints as a last resort. Log the failure for investigation.

Can the discovery document change at runtime?

: Rarely. Providers may add new endpoints or deprecate old ones. Clients should re-fetch periodically (e.g., every 24 hours).

What is the `issuer` field and how is it used?

: The issuer is the unique identifier for the provider. It must match the iss claim in ID tokens. Clients validate this to prevent token confusion.

Is the discovery document publicly accessible?

: Yes. It contains only configuration metadata, no secrets. Anyone can access it.

Mini Project

Create a Python class DynamicOIDCClient that takes an issuer URL, client ID, and client secret. On initialization, it fetches the discovery document. Implement methods for generating auth URLs, exchanging codes, and fetching JWKS. Cache the discovery document for 24 hours.

What's Next

Continue with Well-Known Configuration Deep Dive for a detailed field reference, or explore OIDC Scopes to understand how scopes control claim access.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro