Class: AtprotoAuth::PAR::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/atproto_auth/par/client.rb

Overview

Client for making Pushed Authorization Requests (PAR) according to RFC 9126. Handles submitting authorization parameters to the PAR endpoint and building the subsequent authorization URL.

In AT Protocol OAuth, all authorization requests must first go through PAR. This means instead of sending authorization parameters directly to the authorization endpoint, clients:

  1. Submit parameters to the PAR endpoint via POST

  2. Receive a request_uri in response

  3. Use only the request_uri and client_id in the authorization redirect

All requests are made using HTTPS and include proper content-type headers. DPoP proofs can be included for enhanced security. The client validates all responses and provides clear error messages for any failures.

Examples:

Basic PAR flow

client = AtprotoAuth::PAR::Client.new(
  endpoint: "https://auth.example.com/par"
)

# Create and submit PAR request using builder pattern
request = AtprotoAuth::PAR::Request.build do |config|
  config.client_id = "https://app.example.com/client-metadata.json"
  config.redirect_uri = "https://app.example.com/callback"
  config.code_challenge = "abc123..."
  config.code_challenge_method = "S256"
  config.state = "xyz789..."
  config.scope = "atproto"
end

response = client.submit(request)

# Build authorization URL using response
auth_url = client.authorization_url(
  authorize_endpoint: "https://auth.example.com/authorize",
  request_uri: response.request_uri,
  client_id: request.client_id
)

With client authentication (confidential clients)

request = AtprotoAuth::PAR::Request.build do |config|
  # ... basic parameters ...
  config.client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
  config.client_assertion = jwt_token
end

With DPoP proof

request = AtprotoAuth::PAR::Request.build do |config|
  # ... basic parameters ...
  config.dpop_proof = dpop_proof_jwt
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(endpoint:, dpop_client:) ⇒ Client

Returns a new instance of Client.



59
60
61
62
63
64
# File 'lib/atproto_auth/par/client.rb', line 59

def initialize(endpoint:, dpop_client:)
  @endpoint = endpoint
  @dpop_client = dpop_client
  @nonce_manager = dpop_client.nonce_manager
  validate_endpoint!
end

Instance Attribute Details

#dpop_clientObject (readonly)

Returns the value of attribute dpop_client.



57
58
59
# File 'lib/atproto_auth/par/client.rb', line 57

def dpop_client
  @dpop_client
end

#endpointObject (readonly)

Returns the value of attribute endpoint.



57
58
59
# File 'lib/atproto_auth/par/client.rb', line 57

def endpoint
  @endpoint
end

#nonce_managerObject (readonly)

Returns the value of attribute nonce_manager.



57
58
59
# File 'lib/atproto_auth/par/client.rb', line 57

def nonce_manager
  @nonce_manager
end

Instance Method Details

#authorization_url(authorize_endpoint:, request_uri:, client_id:) ⇒ String

Builds authorization URL from PAR response

Parameters:

  • authorize_endpoint (String)

    Authorization endpoint URL

  • request_uri (String)

    PAR request_uri

  • client_id (String)

    OAuth client_id

Returns:

  • (String)

    Authorization URL



111
112
113
114
115
116
117
118
# File 'lib/atproto_auth/par/client.rb', line 111

def authorization_url(authorize_endpoint:, request_uri:, client_id:)
  uri = URI(authorize_endpoint)
  uri.query = encode_params(
    "request_uri" => request_uri,
    "client_id" => client_id
  )
  uri.to_s
end

#extract_nonce(response) ⇒ Object

Raises:



94
95
96
97
98
99
100
101
102
103
104
# File 'lib/atproto_auth/par/client.rb', line 94

def extract_nonce(response)
  # Try all possible header key formats
  headers = response[:headers]
  nonce = headers["DPoP-Nonce"] ||
          headers["dpop-nonce"] ||
          headers["Dpop-Nonce"]

  raise Error, "No DPoP nonce provided in response" unless nonce

  nonce
end

#submit(request) ⇒ Response

Submits a PAR request, handling DPoP nonce requirements

Parameters:

  • request (Request)

    The request to submit

Returns:

Raises:

  • (Error)

    if request fails



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/atproto_auth/par/client.rb', line 70

def submit(request)
  # Try the initial request
  response = make_request(request)

  return process_response(response) if response[:status] == 201

  # Handle DPoP nonce requirement
  if requires_nonce?(response)
    nonce = extract_nonce(response)
    store_nonce(nonce)

    # Get stored nonce to verify
    nonce_manager.get(server_origin)

    # Generate new proof with nonce and retry
    response = make_request(request)
    return process_response(response) if response[:status] == 201
  end

  handle_error_response(response)
rescue StandardError => e
  raise Error, "PAR request failed: #{e.message}"
end