A library for ATProtocol identities.
at main 4.7 kB view raw
1//! OAuth authorization callback handler. 2//! 3//! Exchange authorization codes for tokens with state validation 4//! and comprehensive error handling for OAuth completion. 5 6use std::sync::Arc; 7 8use anyhow::Result; 9use atproto_identity::{ 10 key::{KeyResolver, identify_key}, 11 traits::DidDocumentStorage, 12}; 13use atproto_oauth::{ 14 resources::pds_resources, 15 storage::OAuthRequestStorage, 16 workflow::{OAuthClient, oauth_complete}, 17}; 18use axum::{ 19 Form, 20 extract::State, 21 response::{IntoResponse, Response}, 22}; 23use http::StatusCode; 24use serde::{Deserialize, Serialize}; 25 26use crate::{ 27 errors::OAuthCallbackError, 28 state::{HttpClient, OAuthClientConfig}, 29}; 30 31/// OAuth authorization callback form data. 32/// 33/// Contains the parameters sent by the authorization server during the OAuth callback. 34#[derive(Deserialize, Serialize)] 35pub struct OAuthCallbackForm { 36 /// OAuth state parameter for CSRF protection 37 pub state: String, 38 /// Authorization server issuer identifier 39 pub iss: String, 40 /// Authorization code from the authorization server 41 pub code: String, 42} 43 44impl IntoResponse for OAuthCallbackError { 45 fn into_response(self) -> Response { 46 tracing::error!(error = ?self, "OAuth callback error"); 47 ( 48 StatusCode::INTERNAL_SERVER_ERROR, 49 format!("OAuth callback failed: {}", self), 50 ) 51 .into_response() 52 } 53} 54 55/// Handles OAuth authorization callback requests. 56/// 57/// Processes the authorization callback by validating the OAuth state, exchanging 58/// the authorization code for tokens, and returning the complete OAuth response. 59pub async fn handle_oauth_callback( 60 oauth_client_config: OAuthClientConfig, 61 client: HttpClient, 62 oauth_request_storage: State<Arc<dyn OAuthRequestStorage>>, 63 did_document_storage: State<Arc<dyn DidDocumentStorage>>, 64 key_resolver: State<Arc<dyn KeyResolver>>, 65 Form(callback_form): Form<OAuthCallbackForm>, 66) -> Result<impl IntoResponse, OAuthCallbackError> { 67 let oauth_request = oauth_request_storage 68 .get_oauth_request_by_state(&callback_form.state) 69 .await?; 70 71 let oauth_request = oauth_request.ok_or(OAuthCallbackError::NoOAuthRequestFound)?; 72 73 if oauth_request.issuer != callback_form.iss { 74 return Err(OAuthCallbackError::InvalidIssuer { 75 expected: oauth_request.issuer.clone(), 76 actual: callback_form.iss.clone(), 77 }); 78 } 79 80 let private_signing_key_data = key_resolver 81 .resolve(&oauth_request.signing_public_key) 82 .await 83 .map_err(|_| OAuthCallbackError::NoSigningKeyFound)?; 84 85 let private_dpop_key_data = identify_key(&oauth_request.dpop_private_key)?; 86 87 let oauth_client = OAuthClient { 88 redirect_uri: oauth_client_config.redirect_uris.clone(), 89 client_id: oauth_client_config.client_id.clone(), 90 private_signing_key_data, 91 }; 92 93 // We need to get the DID from the token response after OAuth completion 94 // First, get authorization server from the issuer to complete OAuth 95 let (_, authorization_server) = 96 pds_resources(&client, &oauth_request.authorization_server).await?; 97 98 let token_response = oauth_complete( 99 &client, 100 &oauth_client, 101 &private_dpop_key_data, 102 &callback_form.code, 103 &oauth_request, 104 &authorization_server, 105 ) 106 .await?; 107 108 // Now get the DID from the token response subject claim 109 let did = token_response 110 .sub 111 .clone() 112 .ok_or(OAuthCallbackError::NoDIDDocumentFound)?; 113 114 let document = did_document_storage.get_document_by_did(&did).await?; 115 116 let document = document.ok_or(OAuthCallbackError::NoDIDDocumentFound)?; 117 118 // Format the response with OAuth tokens and DPoP key information 119 let response_body = format!( 120 "OAuth Callback Completed Successfully\n\ 121 =====================================\n\ 122 DID: {}\n\ 123 Issuer: {}\n\ 124 Access Token: {}\n\ 125 Refresh Token: {}\n\ 126 Token Type: {}\n\ 127 Expires In: {} seconds\n\ 128 Scope: {}\n\ 129 Subject: {}\n\ 130 Private DPoP Key: {}\n", 131 document.id, 132 oauth_request.issuer, 133 token_response.access_token, 134 token_response 135 .refresh_token 136 .clone() 137 .unwrap_or_else(|| "N/A".to_string()), 138 token_response.token_type, 139 token_response.expires_in, 140 token_response.scope, 141 token_response.sub.clone().unwrap_or("unknown".to_string()), 142 private_dpop_key_data 143 ); 144 145 Ok(( 146 StatusCode::OK, 147 [("Content-Type", "text/plain")], 148 response_body, 149 ) 150 .into_response()) 151}