atproto_oauth/workflow.rs
1//! OAuth workflow implementation for AT Protocol authorization flows.
2//!
3//! This module provides a complete OAuth 2.0 authorization code flow implementation specifically
4//! designed for AT Protocol, including Pushed Authorization Requests (PAR), DPoP security, PKCE
5//! protection, and client assertion handling.
6//!
7//! ## OAuth Flow Components
8//!
9//! - **`oauth_init()`**: Initiates OAuth flow with PAR request to authorization server
10//! - **`oauth_complete()`**: Completes OAuth flow by exchanging authorization code for tokens
11//! - **`OAuthClient`**: Client configuration with credentials and signing keys
12//! - **`OAuthRequest`**: Tracking structure for ongoing authorization requests
13//! - **`OAuthRequestState`**: Security parameters including state, nonce, and PKCE challenge
14//!
15//! ## Security Features
16//!
17//! - **PKCE**: Proof Key for Code Exchange protection against authorization code interception
18//! - **DPoP**: Demonstration of Proof-of-Possession for token binding
19//! - **Client Assertions**: JWT-based client authentication using private key signatures
20//! - **State Parameters**: CSRF protection using random state values
21//! - **Nonce Values**: Additional replay protection
22//!
23//! ## Example Usage
24//!
25//! ```rust,ignore
26//! use atproto_oauth::workflow::{oauth_init, oauth_complete, OAuthClient, OAuthRequestState};
27//! use atproto_oauth::pkce::generate;
28//! use atproto_identity::key::generate_key;
29//!
30//! // Generate security parameters
31//! let signing_key = generate_key(KeyType::P256Private)?;
32//! let dpop_key = generate_key(KeyType::P256Private)?;
33//! let (pkce_verifier, code_challenge) = generate();
34//!
35//! // Configure OAuth client
36//! let oauth_client = OAuthClient {
37//! redirect_uri: "https://app.example.com/callback".to_string(),
38//! client_id: "https://app.example.com/client-metadata.json".to_string(),
39//! private_signing_key_data: signing_key,
40//! };
41//!
42//! // Create request state
43//! let oauth_state = OAuthRequestState {
44//! state: "random-state-value".to_string(),
45//! nonce: "random-nonce-value".to_string(),
46//! code_challenge,
47//! scope: "atproto transition:generic".to_string(),
48//! };
49//!
50//! // Initiate OAuth flow
51//! let par_response = oauth_init(
52//! &http_client,
53//! &oauth_client,
54//! &dpop_key,
55//! "user.bsky.social",
56//! &authorization_server,
57//! &oauth_state,
58//! ).await?;
59//!
60//! // Build authorization URL
61//! let auth_url = format!(
62//! "{}?client_id={}&request_uri={}",
63//! authorization_server.authorization_endpoint,
64//! oauth_client.client_id,
65//! par_response.request_uri
66//! );
67//!
68//! // After user authorization and callback...
69//! let token_response = oauth_complete(
70//! &http_client,
71//! &oauth_client,
72//! &dpop_key,
73//! "authorization_code_from_callback",
74//! &oauth_request,
75//! &did_document,
76//! ).await?;
77//! ```
78
79use crate::{
80 dpop::{DpopRetry, auth_dpop},
81 errors::OAuthClientError,
82 jwt::{Claims, Header, JoseClaims, mint},
83 resources::{AuthorizationServer, pds_resources},
84};
85use atproto_identity::key::KeyData;
86use chrono::{DateTime, Utc};
87use rand::distributions::{Alphanumeric, DistString};
88use reqwest_chain::ChainMiddleware;
89use reqwest_middleware::ClientBuilder;
90
91use std::collections::HashMap;
92
93use serde::Deserialize;
94
95/// Response from a Pushed Authorization Request (PAR) endpoint.
96///
97/// Contains the request URI and expiration time returned by the authorization
98/// server after successfully processing a pushed authorization request.
99#[derive(Clone, Deserialize)]
100pub struct ParResponse {
101 /// The request URI to use in the authorization request.
102 pub request_uri: String,
103 /// The lifetime of the request URI in seconds.
104 pub expires_in: u64,
105
106 /// Additional fields returned by the authorization server.
107 #[serde(flatten)]
108 pub extra: HashMap<String, serde_json::Value>,
109}
110
111/// OAuth request state containing security parameters for the authorization flow.
112///
113/// This struct holds the security parameters needed to maintain state
114/// and prevent attacks during the OAuth authorization code flow.
115pub struct OAuthRequestState {
116 /// Random state parameter to prevent CSRF attacks.
117 pub state: String,
118 /// Random nonce value for additional security.
119 pub nonce: String,
120 /// PKCE code challenge derived from the code verifier.
121 pub code_challenge: String,
122 /// The scope of access requested for the authorization.
123 pub scope: String,
124}
125
126/// OAuth client configuration containing essential client credentials.
127///
128/// This struct holds the client configuration needed for OAuth authorization flows,
129/// including the redirect URI, client identifier, and signing key.
130pub struct OAuthClient {
131 /// The redirect URI where the authorization server will send the user after authorization.
132 pub redirect_uri: String,
133 /// The unique client identifier for this OAuth client.
134 pub client_id: String,
135 /// The private key data used for signing client assertions.
136 pub private_signing_key_data: KeyData,
137}
138
139/// OAuth request tracking information for ongoing authorization flows.
140///
141/// This struct contains all the necessary information to track and complete
142/// an OAuth authorization request, including security parameters and timing.
143#[derive(Clone, Debug, PartialEq)]
144pub struct OAuthRequest {
145 /// The OAuth state parameter used to prevent CSRF attacks.
146 pub oauth_state: String,
147 /// The authorization server issuer identifier.
148 pub issuer: String,
149 /// The DID (Decentralized Identifier) of the user.
150 pub did: String,
151 /// The nonce value for additional security.
152 pub nonce: String,
153 /// The PKCE code verifier for this authorization request.
154 pub pkce_verifier: String,
155 /// The public key used for signing (serialized).
156 pub signing_public_key: String,
157 /// The DPoP private key (serialized).
158 pub dpop_private_key: String,
159 /// When this OAuth request was created.
160 pub created_at: DateTime<Utc>,
161 /// When this OAuth request expires.
162 pub expires_at: DateTime<Utc>,
163}
164
165/// Response from the OAuth token endpoint containing access credentials.
166///
167/// This struct represents the successful response from an OAuth token exchange,
168/// containing the access token and related metadata.
169#[derive(Clone, Deserialize)]
170pub struct TokenResponse {
171 /// The access token that can be used to access protected resources.
172 pub access_token: String,
173 /// The type of token, typically "Bearer" or "DPoP".
174 pub token_type: String,
175 /// The refresh token that can be used to obtain new access tokens.
176 pub refresh_token: String,
177 /// The scope of access granted by the access token.
178 pub scope: String,
179 /// The lifetime of the access token in seconds.
180 pub expires_in: u32,
181 /// The subject identifier (usually the user's DID).
182 pub sub: String,
183
184 /// Additional fields returned by the authorization server.
185 #[serde(flatten)]
186 pub extra: HashMap<String, serde_json::Value>,
187}
188
189/// Initiates the OAuth authorization flow by making a Pushed Authorization Request (PAR).
190///
191/// This function creates a PAR request to the authorization server with the necessary
192/// OAuth parameters, DPoP proof, and client assertion. It handles the complete setup
193/// for the AT Protocol OAuth flow including PKCE and DPoP security mechanisms.
194///
195/// # Arguments
196/// * `http_client` - The HTTP client to use for making requests
197/// * `private_signing_key_data` - The private key for signing client assertions
198/// * `dpop_key_data` - The key data for creating DPoP proofs
199/// * `handle` - The user's handle for the login hint
200/// * `authorization_server` - The authorization server configuration
201/// * `oauth_request_state` - The OAuth state parameters for this request
202///
203/// # Returns
204/// A `ParResponse` containing the request URI and expiration time on success.
205///
206/// # Errors
207/// Returns `OAuthClientError` if the PAR request fails or response parsing fails.
208pub async fn oauth_init(
209 http_client: &reqwest::Client,
210 oauth_client: &OAuthClient,
211 dpop_key_data: &KeyData,
212 handle: &str,
213 authorization_server: &AuthorizationServer,
214 oauth_request_state: &OAuthRequestState,
215) -> Result<ParResponse, OAuthClientError> {
216 let par_url = authorization_server
217 .pushed_authorization_request_endpoint
218 .clone();
219
220 let scope = &oauth_request_state.scope;
221
222 let client_assertion_header: Header = (oauth_client.private_signing_key_data.clone())
223 .try_into()
224 .map_err(OAuthClientError::JWTHeaderCreationFailed)?;
225
226 let client_assertion_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30);
227 let client_assertion_claims = Claims::new(JoseClaims {
228 issuer: Some(oauth_client.client_id.clone()),
229 subject: Some(oauth_client.client_id.clone()),
230 audience: Some(authorization_server.issuer.clone()),
231 json_web_token_id: Some(client_assertion_jti),
232 issued_at: Some(chrono::Utc::now().timestamp() as u64),
233 ..Default::default()
234 });
235
236 let client_assertion_token = mint(
237 &oauth_client.private_signing_key_data,
238 &client_assertion_header,
239 &client_assertion_claims,
240 )
241 .map_err(OAuthClientError::MintTokenFailed)?;
242
243 let (dpop_token, dpop_header, dpop_claims) = auth_dpop(dpop_key_data, "POST", &par_url)
244 .map_err(OAuthClientError::DpopTokenCreationFailed)?;
245
246 tracing::info!(?dpop_token, ?dpop_header, ?dpop_claims, "debugging dpop");
247
248 let dpop_retry = DpopRetry::new(dpop_header, dpop_claims, dpop_key_data.clone(), true);
249
250 let dpop_retry_client = ClientBuilder::new(http_client.clone())
251 .with(ChainMiddleware::new(dpop_retry.clone()))
252 .build();
253
254 let params = [
255 ("response_type", "code"),
256 ("code_challenge", &oauth_request_state.code_challenge),
257 ("code_challenge_method", "S256"),
258 ("client_id", oauth_client.client_id.as_str()),
259 ("state", oauth_request_state.state.as_str()),
260 ("redirect_uri", oauth_client.redirect_uri.as_str()),
261 ("scope", scope),
262 ("login_hint", handle),
263 (
264 "client_assertion_type",
265 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
266 ),
267 ("client_assertion", client_assertion_token.as_str()),
268 ];
269
270 let response = dpop_retry_client
271 .post(par_url)
272 .header("DPoP", dpop_token.as_str())
273 .form(¶ms)
274 .send()
275 .await
276 .map_err(OAuthClientError::PARHttpRequestFailed)?
277 .json()
278 .await
279 .map_err(OAuthClientError::PARResponseJsonParsingFailed)?;
280
281 Ok(response)
282}
283
284/// Completes the OAuth authorization flow by exchanging the authorization code for tokens.
285///
286/// This function performs the final step of the OAuth authorization code flow by
287/// exchanging the authorization code received from the callback for access and refresh tokens.
288/// It handles DPoP proof generation and client assertion creation for secure token exchange.
289///
290/// # Arguments
291/// * `http_client` - The HTTP client to use for making requests
292/// * `oauth_client` - The OAuth client configuration
293/// * `dpop_key_data` - The key data for creating DPoP proofs
294/// * `callback_code` - The authorization code received from the callback
295/// * `oauth_request` - The original OAuth request state
296/// * `document` - The identity document containing PDS endpoints
297///
298/// # Returns
299/// A `TokenResponse` containing the access token, refresh token, and metadata on success.
300///
301/// # Errors
302/// Returns `OAuthClientError` if the token exchange fails or response parsing fails.
303pub async fn oauth_complete(
304 http_client: &reqwest::Client,
305 oauth_client: &OAuthClient,
306 dpop_key_data: &KeyData,
307 callback_code: &str,
308 oauth_request: &OAuthRequest,
309 document: &atproto_identity::model::Document,
310) -> Result<TokenResponse, OAuthClientError> {
311 let pds_endpoints = document.pds_endpoints();
312 let pds_endpoint = pds_endpoints
313 .first()
314 .ok_or(OAuthClientError::InvalidOAuthProtectedResource)?;
315 let (_, authorization_server) = pds_resources(http_client, pds_endpoint).await?;
316
317 let client_assertion_header: Header = (oauth_client.private_signing_key_data.clone())
318 .try_into()
319 .map_err(OAuthClientError::JWTHeaderCreationFailed)?;
320
321 let client_assertion_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30);
322 let client_assertion_claims = Claims::new(JoseClaims {
323 issuer: Some(oauth_client.client_id.clone()),
324 subject: Some(oauth_client.client_id.clone()),
325 audience: Some(authorization_server.issuer.clone()),
326 json_web_token_id: Some(client_assertion_jti),
327 issued_at: Some(chrono::Utc::now().timestamp() as u64),
328 ..Default::default()
329 });
330
331 let client_assertion_token = mint(
332 &oauth_client.private_signing_key_data,
333 &client_assertion_header,
334 &client_assertion_claims,
335 )
336 .map_err(OAuthClientError::MintTokenFailed)?;
337
338 let params = [
339 ("client_id", oauth_client.client_id.as_str()),
340 ("redirect_uri", oauth_client.redirect_uri.as_str()),
341 ("grant_type", "authorization_code"),
342 ("code", callback_code),
343 ("code_verifier", &oauth_request.pkce_verifier),
344 (
345 "client_assertion_type",
346 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
347 ),
348 ("client_assertion", client_assertion_token.as_str()),
349 ];
350
351 let token_endpoint = authorization_server.token_endpoint.clone();
352
353 let (dpop_token, dpop_header, dpop_claims) = auth_dpop(dpop_key_data, "POST", &token_endpoint)
354 .map_err(OAuthClientError::DpopTokenCreationFailed)?;
355
356 let dpop_retry = DpopRetry::new(dpop_header, dpop_claims, dpop_key_data.clone(), true);
357
358 let dpop_retry_client = ClientBuilder::new(http_client.clone())
359 .with(ChainMiddleware::new(dpop_retry.clone()))
360 .build();
361
362 dpop_retry_client
363 .post(token_endpoint)
364 .header("DPoP", dpop_token.as_str())
365 .form(¶ms)
366 .send()
367 .await
368 .map_err(OAuthClientError::TokenHttpRequestFailed)?
369 .json()
370 .await
371 .map_err(OAuthClientError::TokenResponseJsonParsingFailed)
372}
373
374/// Refreshes OAuth access tokens using a refresh token.
375///
376/// This function exchanges a refresh token for new access and refresh tokens.
377/// It handles DPoP proof generation and client assertion creation for secure
378/// token refresh operations according to AT Protocol OAuth requirements.
379///
380/// # Arguments
381/// * `http_client` - The HTTP client to use for making requests
382/// * `oauth_client` - The OAuth client configuration
383/// * `dpop_key_data` - The key data for creating DPoP proofs
384/// * `refresh_token` - The refresh token to exchange for new tokens
385/// * `document` - The identity document containing PDS endpoints
386///
387/// # Returns
388/// A `TokenResponse` containing the new access token, refresh token, and metadata on success.
389///
390/// # Errors
391/// Returns `OAuthClientError` if the token refresh fails or response parsing fails.
392pub async fn oauth_refresh(
393 http_client: &reqwest::Client,
394 oauth_client: &OAuthClient,
395 dpop_key_data: &KeyData,
396 refresh_token: &str,
397 document: &atproto_identity::model::Document,
398) -> Result<TokenResponse, OAuthClientError> {
399 let pds_endpoints = document.pds_endpoints();
400 let pds_endpoint = pds_endpoints
401 .first()
402 .ok_or(OAuthClientError::InvalidOAuthProtectedResource)?;
403 let (_, authorization_server) = pds_resources(http_client, pds_endpoint).await?;
404
405 let client_assertion_header: Header = (oauth_client.private_signing_key_data.clone())
406 .try_into()
407 .map_err(OAuthClientError::JWTHeaderCreationFailed)?;
408
409 let client_assertion_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30);
410 let client_assertion_claims = Claims::new(JoseClaims {
411 issuer: Some(oauth_client.client_id.clone()),
412 subject: Some(oauth_client.client_id.clone()),
413 audience: Some(authorization_server.issuer.clone()),
414 json_web_token_id: Some(client_assertion_jti),
415 issued_at: Some(chrono::Utc::now().timestamp() as u64),
416 ..Default::default()
417 });
418
419 let client_assertion_token = mint(
420 &oauth_client.private_signing_key_data,
421 &client_assertion_header,
422 &client_assertion_claims,
423 )
424 .map_err(OAuthClientError::MintTokenFailed)?;
425
426 let params = [
427 ("client_id", oauth_client.client_id.as_str()),
428 ("redirect_uri", oauth_client.redirect_uri.as_str()),
429 ("grant_type", "refresh_token"),
430 ("refresh_token", refresh_token),
431 (
432 "client_assertion_type",
433 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
434 ),
435 ("client_assertion", client_assertion_token.as_str()),
436 ];
437
438 let token_endpoint = authorization_server.token_endpoint.clone();
439
440 let (dpop_token, dpop_header, dpop_claims) = auth_dpop(dpop_key_data, "POST", &token_endpoint)
441 .map_err(OAuthClientError::DpopTokenCreationFailed)?;
442
443 let dpop_retry = DpopRetry::new(dpop_header, dpop_claims, dpop_key_data.clone(), true);
444
445 let dpop_retry_client = ClientBuilder::new(http_client.clone())
446 .with(ChainMiddleware::new(dpop_retry.clone()))
447 .build();
448
449 dpop_retry_client
450 .post(token_endpoint)
451 .header("DPoP", dpop_token.as_str())
452 .form(¶ms)
453 .send()
454 .await
455 .map_err(OAuthClientError::TokenHttpRequestFailed)?
456 .json()
457 .await
458 .map_err(OAuthClientError::TokenResponseJsonParsingFailed)
459}