A library for ATProtocol identities.

feature: AIP client credentials token helper

Changed files
+94 -4
crates
atproto-oauth-aip
+11 -4
crates/atproto-oauth-aip/src/errors.rs
··· 6 //! 7 //! ## Error Categories 8 //! 9 - //! - **`OAuthWorkflowError`** (workflow-1 to workflow-8): OAuth workflow operations including PAR, token exchange, and session management 10 //! 11 //! ## Error Format 12 //! ··· 44 #[error("error-atproto-oauth-aip-workflow-5 Token response json parsing failed: {0}")] 45 TokenResponseParseFailed(#[source] reqwest::Error), 46 47 /// Failed to send session exchange HTTP request. 48 - #[error("error-atproto-oauth-aip-workflow-6 Session request failed: {0}")] 49 SessionRequestFailed(#[source] reqwest::Error), 50 51 /// Failed to parse session response JSON. 52 - #[error("error-atproto-oauth-aip-workflow-7 Session json parsing failed: {0}")] 53 SessionResponseParseFailed(#[source] reqwest::Error), 54 55 /// Session response contained an error. 56 - #[error("error-atproto-oauth-aip-workflow-8 Session response invalid: {message}")] 57 SessionResponseInvalid { 58 /// Error message from the session response. 59 message: String,
··· 6 //! 7 //! ## Error Categories 8 //! 9 + //! - **`OAuthWorkflowError`** (workflow-1 to workflow-9): OAuth workflow operations including PAR, token exchange, and session management 10 //! 11 //! ## Error Format 12 //! ··· 44 #[error("error-atproto-oauth-aip-workflow-5 Token response json parsing failed: {0}")] 45 TokenResponseParseFailed(#[source] reqwest::Error), 46 47 + /// Token response contained an error. 48 + #[error("error-atproto-oauth-aip-workflow-6 Token response invalid: {message}")] 49 + TokenResponseInvalid { 50 + /// Error message from the token response. 51 + message: String, 52 + }, 53 + 54 /// Failed to send session exchange HTTP request. 55 + #[error("error-atproto-oauth-aip-workflow-7 Session request failed: {0}")] 56 SessionRequestFailed(#[source] reqwest::Error), 57 58 /// Failed to parse session response JSON. 59 + #[error("error-atproto-oauth-aip-workflow-8 Session json parsing failed: {0}")] 60 SessionResponseParseFailed(#[source] reqwest::Error), 61 62 /// Session response contained an error. 63 + #[error("error-atproto-oauth-aip-workflow-9 Session response invalid: {message}")] 64 SessionResponseInvalid { 65 /// Error message from the session response. 66 message: String,
+83
crates/atproto-oauth-aip/src/workflow.rs
··· 563 } 564 } 565 }
··· 563 } 564 } 565 } 566 + 567 + /// Obtains an access token using OAuth client credentials grant. 568 + /// 569 + /// This function implements the OAuth 2.0 client credentials flow for obtaining 570 + /// service-to-service access tokens. This is typically used when a service needs 571 + /// to authenticate itself rather than acting on behalf of a user. 572 + /// 573 + /// # Arguments 574 + /// 575 + /// * `http_client` - The HTTP client to use for making requests 576 + /// * `aip_hostname` - The hostname of the AT Protocol Identity Provider (AIP) 577 + /// * `aip_client_id` - The client ID for authenticating with the AIP 578 + /// * `aip_client_secret` - The client secret for authenticating with the AIP 579 + /// 580 + /// # Returns 581 + /// 582 + /// Returns a `TokenResponse` containing the access token and metadata, 583 + /// or an error if the token request fails. 584 + /// 585 + /// # Example 586 + /// 587 + /// ```no_run 588 + /// # async fn example() -> Result<(), Box<dyn std::error::Error>> { 589 + /// use atproto_oauth_aip::workflow::client_credentials_token; 590 + /// use atproto_oauth::workflow::TokenResponse; 591 + /// # let http_client = reqwest::Client::new(); 592 + /// # let aip_hostname = "auth.example.com"; 593 + /// # let client_id = "service-client-id"; 594 + /// # let client_secret = "service-client-secret"; 595 + /// 596 + /// let token = client_credentials_token( 597 + /// &http_client, 598 + /// aip_hostname, 599 + /// client_id, 600 + /// client_secret, 601 + /// ).await?; 602 + /// 603 + /// println!("Access token: {}", token.access_token); 604 + /// println!("Token type: {}", token.token_type); 605 + /// println!("Expires in: {} seconds", token.expires_in); 606 + /// # Ok(()) 607 + /// # } 608 + /// ``` 609 + pub async fn client_credentials_token( 610 + http_client: &reqwest::Client, 611 + aip_hostname: &str, 612 + aip_client_id: &str, 613 + aip_client_secret: &str, 614 + ) -> Result<atproto_oauth::workflow::TokenResponse> { 615 + // Construct the token endpoint URL 616 + let token_url = format!("https://{}/oauth/token", aip_hostname); 617 + 618 + // Prepare the form data for client credentials grant 619 + let params = [("grant_type", "client_credentials")]; 620 + 621 + // Send the request with Basic authentication 622 + let response = http_client 623 + .post(&token_url) 624 + .basic_auth(aip_client_id, Some(aip_client_secret)) 625 + .form(&params) 626 + .send() 627 + .await 628 + .map_err(OAuthWorkflowError::TokenRequestFailed)?; 629 + 630 + // Check if the request was successful 631 + if !response.status().is_success() { 632 + let status = response.status(); 633 + let error_text = response 634 + .text() 635 + .await 636 + .unwrap_or_else(|_| "Unknown error".to_string()); 637 + return Err(OAuthWorkflowError::TokenResponseInvalid { 638 + message: format!("Token request failed with status {}: {}", status, error_text), 639 + } 640 + .into()); 641 + } 642 + 643 + // Parse the response 644 + response 645 + .json::<atproto_oauth::workflow::TokenResponse>() 646 + .await 647 + .map_err(|e| OAuthWorkflowError::TokenResponseParseFailed(e).into()) 648 + }