Skip to content

OAuth2 Pushed Authorization Requests (PAR) — RFC 9126 for Secure Authorization Request Handling

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about OAuth2 Pushed Authorization Requests (PAR). We cover key concepts, practical examples, and best practices to help you master this topic.

OAuth2 Pushed Authorization Requests (PAR, RFC 9126) allow clients to POST authorization request parameters directly to the authorization server, returning a request URI that replaces the traditional query-string authorization request, preventing URL length limits and request tampering.

What You'll Learn

  • PAR flow and request endpoint
  • Benefits over query-string authorization requests
  • Request URI lifecycle and security
  • PAR with PKCE for defense in depth
  • Implementing PAR in authorization servers and clients

Why It Matters

Traditional authorization requests place all parameters in the URL query string, which has length limits (browser URL bars, reverse proxies) and exposes parameters to browser history and referrer headers. PAR solves both problems. DodaTech's authorization server adopted PAR in 2024, eliminating a class of authorization request tampering attacks.

Real-World Use

A mobile app needs to request 15 scopes with a complex PKCE challenge and redirect URI. The URL exceeds browser length limits. Using PAR, the app POSTs the parameters directly to the server and receives a short request_uri that fits in any browser without truncation or tampering.

sequenceDiagram
    participant Client
    participant Auth as Authorization Server
    participant Browser

    Client->>Auth: POST /as/par (client_id, redirect_uri, scope, code_challenge, state)
    Auth->>Auth: Validate and store params
    Auth-->>Client: request_uri = "urn:ietf:params:oauth:request_uri:abc123"
    Client->>Browser: Redirect to /authorize?client_id&request_uri=abc123
    Browser->>Auth: GET /authorize (request_uri)
    Auth->>Auth: Look up stored params
    Auth-->>Browser: Consent screen
    Browser-->>Client: Authorization code

Code Examples

Example 1: PAR Endpoint

from flask import Flask, request, jsonify
import secrets
import hashlib
from datetime import datetime, timedelta, timezone

app = Flask(__name__)

# In-memory store for PAR requests (use Redis in production)
par_store = {}

@app.route('/as/par', methods=['POST'])
def pushed_authorization_request():
    """RFC 9126 Pushed Authorization Request endpoint."""
    client_id = request.form.get('client_id')
    redirect_uri = request.form.get('redirect_uri')
    scope = request.form.get('scope')
    state = request.form.get('state')
    code_challenge = request.form.get('code_challenge')
    code_challenge_method = request.form.get('code_challenge_method', 'S256')

    # Validate client
    client = get_client(client_id)
    if not client:
        return jsonify({'error': 'invalid_client'}), 401

    # Validate redirect URI
    if redirect_uri not in client['redirect_uris']:
        return jsonify({'error': 'invalid_redirect_uri'}), 400

    # Validate scopes
    if scope:
        requested_scopes = scope.split()
        allowed_scopes = client.get('scopes', [])
        invalid = [s for s in requested_scopes if s not in allowed_scopes]
        if invalid:
            return jsonify({'error': 'invalid_scope'}), 400

    # Generate unique request URI
    request_uri = f"urn:ietf:params:oauth:request_uri:{secrets.token_urlsafe(16)}"

    # Store the request parameters with expiry
    par_store[request_uri] = {
        'client_id': client_id,
        'redirect_uri': redirect_uri,
        'scope': scope,
        'state': state,
        'code_challenge': code_challenge,
        'code_challenge_method': code_challenge_method,
        'expires_at': datetime.now(timezone.utc) + timedelta(seconds=60)
    }

    print(f"PAR stored: {request_uri} for client {client_id}")
    return jsonify({
        'request_uri': request_uri,
        'expires_in': 60
    }), 201

Example 2: Authorization Endpoint with PAR Support

@app.route('/authorize', methods=['GET'])
def authorize_with_par():
    """Authorization endpoint supporting both PAR and standard requests."""
    request_uri = request.args.get('request_uri')

    if request_uri:
        # PAR flow: resolve stored parameters
        stored = par_store.get(request_uri)

        if not stored:
            return render_error('invalid_request_uri',
                'Request URI not found or expired')

        if stored['expires_at'] < datetime.now(timezone.utc):
            del par_store[request_uri]
            return render_error('expired_request_uri',
                'Request URI has expired (max 60 seconds)')

        # Use stored parameters
        client_id = stored['client_id']
        redirect_uri = stored['redirect_uri']
        scope = stored['scope']
        state = stored['state']
        code_challenge = stored.get('code_challenge')
        code_challenge_method = stored.get('code_challenge_method')

        # Delete from store after use (single-use)
        del par_store[request_uri]

    else:
        # Standard query-string flow
        client_id = request.args.get('client_id')
        redirect_uri = request.args.get('redirect_uri')
        scope = request.args.get('scope')
        state = request.args.get('state')
        code_challenge = request.args.get('code_challenge')

    # Continue with standard authorization flow...
    user = authenticate_user(request)
    if not user:
        return redirect_to_login(request.url)

    return show_consent_screen(client_id, redirect_uri, scope, state,
                               code_challenge)

Example 3: Client-Side PAR Implementation

import requests

class PARClient:
    def __init__(self, client_id, client_secret, auth_server):
        self.client_id = client_id
        self.client_secret = client_secret
        self.auth_server = auth_server
        self.par_endpoint = f"{auth_server}/as/par"
        self.authorize_endpoint = f"{auth_server}/authorize"

    def start_authorization(self, redirect_uri, scope, state=None,
                            code_challenge=None):
        """Push authorization request via PAR, then build authorize URL."""
        params = {
            'client_id': self.client_id,
            'redirect_uri': redirect_uri,
            'scope': scope,
            'state': state or secrets.token_urlsafe(16)
        }

        if code_challenge:
            params['code_challenge'] = code_challenge
            params['code_challenge_method'] = 'S256'

        # Push to PAR endpoint
        response = requests.post(
            self.par_endpoint,
            data=params,
            auth=(self.client_id, self.client_secret)
        )

        if response.status_code != 201:
            raise Exception(f"PAR failed: {response.json()}")

        par_result = response.json()
        request_uri = par_result['request_uri']

        # Build minimal authorize URL
        authorize_url = (
            f"{self.authorize_endpoint}"
            f"?response_type=code"
            f"&client_id={self.client_id}"
            f"&request_uri={request_uri}"
        )

        return authorize_url, params['state']

    def exchange_code(self, code, redirect_uri, code_verifier=None):
        """Exchange authorization code for tokens."""
        params = {
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': redirect_uri,
            'client_id': self.client_id
        }

        if code_verifier:
            params['code_verifier'] = code_verifier

        response = requests.post(
            f"{self.auth_server}/token",
            data=params,
            auth=(self.client_id, self.client_secret)
        )
        return response.json()

# Usage
client = PARClient(CLIENT_ID, CLIENT_SECRET, 'https://auth.dodatech.com')

# Generate PKCE challenge
code_verifier = secrets.token_urlsafe(32)
code_challenge = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge).rstrip('=').decode()

# Start authorization via PAR
auth_url, state = client.start_authorization(
    redirect_uri='https://app.dodatech.com/callback',
    scope='read:threats write:remediation',
    code_challenge=code_challenge
)
print(f"Redirect user to: {auth_url}")

Common Mistakes

1. Creating Request URIs Without Client Authentication

The PAR endpoint must authenticate the client. Otherwise, anyone can push authorization requests.

2. Long Request URI Expiry

PAR request URIs must expire within seconds (max 60), not minutes or hours. Short expiry prevents replay.

3. Reusing Request URIs

Request URIs are single-use. After the authorization endpoint resolves them, delete them from the store.

4. Not Validating Parameters at the PAR Endpoint

Validate all parameters (redirect URI, scopes, client) at the PAR endpoint, not just at the authorize endpoint.

5. Mixing PAR and Non-PAR Parameters

When a request_uri is present, ignore all other query parameters. Don't merge them.

Practice Questions

  1. What problem does PAR solve?
  2. How long should a request URI be valid?
  3. Why must the client authenticate at the PAR endpoint?
  4. What happens when a request URI expires?
  5. Can PAR be combined with PKCE?

Answers:

  1. URL length limits on authorization requests and exposure of parameters in browser history/referrer headers.
  2. Maximum 60 seconds, typically 5-10 seconds. Short enough to prevent replay.
  3. Otherwise anyone could push a malicious authorization request associated with your client_id.
  4. The authorization endpoint returns an expired_request_uri error, and the client must push a new PAR request.
  5. Yes. PAR and PKCE are complementary — PAR protects request integrity; PKCE protects the authorization code exchange.

Challenge: Implement PAR for an existing OAuth2 authorization server. Add a PAR endpoint, modify the authorization endpoint to resolve request URIs, and build a client that uses PAR with PKCE.

FAQ

Does PAR replace PKCE?

: No. PAR protects the authorization request; PKCE protects the authorization code exchange. Use both together.

Is PAR mandatory for OAuth2?

: No, but it is strongly recommended. OAuth2 Security BCP recommends PAR for all authorization requests.

What format does the request_uri use?

: The opaque format urn:ietf:params:oauth:request_uri:{unique-id}. It must not be guessable.

Does PAR work with mobile apps?

: Yes. Mobile apps can use PAR with PKCE and ASWebAuthenticationSession for secure browser-based auth.

Can I use PAR without a client_secret?

: Yes, for public clients (mobile apps, SPAs). The client is identified by client_id, and PKCE provides the code exchange security.

What's Next

Combine PAR with {{< ilink "OAuth" "PKCE Extension" }} for a complete secure authorization flow, and review {{< ilink "OAuth" "OAuth2 Redirect URIs" }} for redirect URI security.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro