Discovery URL — OIDC Provider Configuration and Endpoint Discovery
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
- What is the standard path for the OIDC discovery document?
- Why should you use discovery instead of hardcoding endpoints?
- What information does the
jwks_urifield provide? - How does
scopes_supportedhelp configure the auth request? - Why should the discovery document be cached?
Answers:
- The path is
/.well-known/openid-configurationrelative to the issuer URL. - Hardcoded endpoints break if the provider changes them. Discovery fetches current endpoints dynamically at runtime.
jwks_uriprovides the URL to fetch the provider's public keys, used to verify ID token signatures.scopes_supportedtells you which scopes the provider supports. Requesting unsupported scopes causes errors.- 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
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