A library for ATProtocol identities.
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}