Skip to content

OIDC Authentication Request — Building Authorization URLs for User Login

DodaTech Updated 2026-06-28 4 min read

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

The OIDC authentication request is the initial URL that redirects the user to the provider's login page, containing parameters that specify the client, requested permissions, and security values like state and nonce.

What You'll Learn

  • All authentication request parameters and their purposes
  • How state and nonce protect against CSRF and replay attacks
  • Building and validating auth request URLs

Why It Matters

A malformed authentication request can fail silently, redirect to the wrong URL, or introduce security vulnerabilities. Understanding each parameter ensures a smooth, secure login flow for your users.

Real-World Use

When a user clicks "Sign In" on Doda Browser, the app builds an authentication request with a random state value (CSRF protection) and nonce (replay protection), encodes them in the redirect URL, and sends the user to the provider's login page.

sequenceDiagram
    Browser->>App: Click Sign In
    App->>App: Generate state + nonce
    App->>Provider: Redirect with auth request
    Provider->>User: Login + consent
    Provider->>App: Redirect callback with code
    App->>App: Verify state matches
    App->>Provider: Exchange code for tokens
    App->>App: Verify nonce in ID token

Building the Auth Request

import secrets
import urllib.parse

def build_auth_request(client_id, redirect_uri, authorization_endpoint,
                       scope="openid profile email", response_type="code"):
    state = secrets.token_urlsafe(32)
    nonce = secrets.token_urlsafe(16)

    params = {
        "client_id": client_id,
        "response_type": response_type,
        "redirect_uri": redirect_uri,
        "scope": scope,
        "state": state,
        "nonce": nonce,
    }

    auth_url = f"{authorization_endpoint}?{urllib.parse.urlencode(params)}"
    return auth_url, state, nonce

# Usage
auth_url, state, nonce = build_auth_request(
    client_id="your-client-id",
    redirect_uri="https://yourapp.com/callback",
    authorization_endpoint="https://provider.com/auth"
)
print(f"Redirect user to: {auth_url}")
print(f"Store state={state} and nonce={nonce} for verification")

Handling the Callback

from flask import Flask, request, session
import requests

app = Flask(__name__)

PROVIDER_CONFIG = {
    "token_endpoint": "https://provider.com/token",
    "client_id": "your-client-id",
    "client_secret": "your-client-secret",
}

@app.route("/callback")
def callback():
    # Verify state parameter (CSRF protection)
    returned_state = request.args.get("state")
    if returned_state != session.get("oauth_state"):
        return "Invalid state parameter", 400
    session.pop("oauth_state", None)

    # Exchange authorization code for tokens
    code = request.args.get("code")
    token_resp = requests.post(PROVIDER_CONFIG["token_endpoint"], data={
        "code": code,
        "client_id": PROVIDER_CONFIG["client_id"],
        "client_secret": PROVIDER_CONFIG["client_secret"],
        "redirect_uri": "https://yourapp.com/callback",
        "grant_type": "authorization_code",
    })
    tokens = token_resp.json()

    # Verify nonce in ID token
    id_token = tokens["id_token"]
    import jwt
    claims = jwt.decode(id_token, options={"verify_signature": False})
    if claims.get("nonce") != session.get("oauth_nonce"):
        return "Invalid nonce", 400
    session.pop("oauth_nonce", None)

    return f"Authenticated as {claims['name']}"

Complete Auth Request with All Parameters

def build_complete_auth_request(
    client_id, redirect_uri, authorization_endpoint,
    scope="openid profile email",
    response_type="code",
    response_mode="query",
    display="page",
    prompt=None,
    max_age=None,
    ui_locales=None,
    claims=None,
):
    state = secrets.token_urlsafe(32)
    nonce = secrets.token_urlsafe(16)

    params = {
        "client_id": client_id,
        "response_type": response_type,
        "redirect_uri": redirect_uri,
        "scope": scope,
        "state": state,
        "nonce": nonce,
    }

    if response_mode:
        params["response_mode"] = response_mode
    if display:
        params["display"] = display
    if prompt:
        params["prompt"] = prompt
    if max_age:
        params["max_age"] = str(max_age)
    if ui_locales:
        params["ui_locales"] = ui_locales
    if claims:
        import json
        params["claims"] = json.dumps(claims)

    auth_url = f"{authorization_endpoint}?{urllib.parse.urlencode(params)}"
    return auth_url, state, nonce

Common Mistakes

1. Not Using a Random State Value

Without state, your app is vulnerable to CSRF Attacks. An attacker can intercept the callback and inject their own authorization code.

2. Not Validating State in the Callback

Generating state but not checking it on return provides no protection. Always compare the returned state with the stored value.

3. Reusing Nonce Values

A reused nonce allows replay attacks. Generate a unique nonce for every authentication request.

4. Not Encoding Redirect URI Correctly

The redirect URI must exactly match what is registered with the provider. Mismatched URIs cause "redirect_uri_mismatch" errors.

5. Missing Scope Parameter

Without the openid scope, the request is treated as OAuth2 and no ID token is returned.

Practice Questions

  1. What is the purpose of the state parameter in an OIDC auth request?
  2. How does the nonce parameter differ from state?
  3. What happens if the redirect_uri does not match the registered URI?
  4. What does the prompt parameter control?
  5. Why should state and nonce be cryptographically random?

Answers:

  1. state prevents CSRF attacks by verifying that the response corresponds to the request initiated by the same user session.
  2. state protects against CSRF (session-level). nonce protects against replay attacks (token-level) and is embedded in the ID token.
  3. The provider returns a redirect_uri_mismatch error. The URI must match exactly, including protocol, domain, port, and path.
  4. prompt controls whether the provider shows login, consent, or select_account screens. Values: none, login, consent, select_account.
  5. Predictable state/nonce values allow attackers to forge auth requests or replay captured tokens.

Challenge: Build a complete Flask OIDC login flow with proper state and nonce handling, token exchange, and session creation. Include error handling for all failure modes.

FAQ

What is the `max_age` parameter?

: max_age specifies the maximum time in seconds since the user last authenticated. If exceeded, the provider re-authenticates the user.

Can I use the same redirect URI for multiple clients?

: No. Each client registration typically has its own redirect URI or a list of allowed URIs.

What is the `claims` parameter used for?

: The claims parameter allows requesting specific claims using JSON, giving more granular control than scopes.

What happens if the user denies consent?

: The provider redirects back with an error=access_denied parameter instead of a code.

How does the provider handle unrecognized parameters?

: OIDC providers should ignore unrecognized parameters per the spec. However, some providers reject unknown parameters.

Mini Project

Create a Flask app that implements a complete OIDC authentication flow. Include: auth URL generation with state and nonce, callback handler with state verification, token exchange, nonce verification, and user session creation. Test with a real OIDC provider.

What's Next

Continue with OIDC Response Types to understand how tokens are delivered, or explore OIDC Flows for different authentication grant types.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro