graph-rs-sdk

repository·master·Indexed 41 days ago

https://github.com/sreeise/graph-rs-sdk

A Rust SDK client for interacting with the Microsoft Graph API and Microsoft Identity Platform. Version 3.0.1 provides async and blocking clients, support for OAuth flows (Authorization Code, Client Credentials, Hybrid, and OpenID Connect), paging, streaming, and interactive authentication.

Tokens
49.9K
Snippets
153
Records
179
Agent score
99%

What's inside graph-rs-sdk

  1. How Public and Confidential Client Applications work

    master

    The SDK uses application builders to manage authentication configuration. Once built, these applications handle access token requests and automatic token refresh for you. You then pass these applications to a Graph client to make API calls.

    There are two primary types of applications:

    • PublicClientApplication: Typically used for client-side applications where a client secret cannot be securely stored.
    • ConfidentialClientApplication: Used for server-side applications that can securely store a client secret.

    Passing either type to the Graph client enables automatic token management.

    let confidential_client: ConfidentialClientApplication<ClientSecretCredential> = ...
    
    let graph_client = Graph::from(confidential_client);
  2. How credential types work in graph-rs-sdk

    master

    The SDK categorizes authentication into two main application types for building OAuth or OpenID Connect flows:

    1. PublicClientApplication: Typically used for applications that cannot securely store a client secret (e.g., mobile or desktop apps).
    2. ConfidentialClientApplication: Used for applications capable of maintaining a client secret (e.g., web servers or background services).

    Most common flows, such as Authorization Code Grant and Client Credentials, utilize the ConfidentialClientApplication type.

  3. Enable Automatic Token Refresh

    master

    The SDK supports automatic token refresh using an in-memory cache. To enable this, you must include the offline_access scope in your request to ensure a refresh token is returned.

    • For Authorization Code Grant: Include offline_access in your scopes when building the sign-in URL and the client.
    • For Client Credentials Grant: Automatic refresh is handled natively as this flow does not rely on refresh tokens.

    Note: Currently, only an in-memory token cache is available. Persistent storage mechanisms (like Azure Key Vault or macOS Keychain) are planned for future releases.

    // 1. Create sign-in URL with offline_access scope
    pub fn authorization_sign_in_url(client_id: &str, tenant: &str, redirect_uri: url::Url) -> Url {
      let scope = vec!["offline_access"];
    
      AuthorizationCodeCredential::authorization_url_builder(client_id)
              .with_redirect_uri(redirect_uri)
              .with_scope(scope)
              .url()
              .unwrap()
    }
    
    // 2. Build client with the same scope
    async fn build_client(
      authorization_code: &str,
      client_id: &str,
      client_secret: &str,
      scope: Vec<String>, // must include "offline_access"
      redirect_uri: url::Url,
    ) -> anyhow::Result<GraphClient> {
      let mut confidential_client = ConfidentialClientApplication::builder(client_id)
              .with_auth_code(authorization_code)
              .with_client_secret(client_secret)
              .with_scope(scope)
              .with_redirect_uri(redirect_uri)
              .build();
    
      let graph_client = GraphClient::from(&confidential_client);
    
      Ok(graph_client)
    }
  4. Handle errors in paging responses

    master

    When performing paging operations, the response body is returned as a Result<T, ErrorMessage> when calling .body() or .into_body().

    Important Behavior with serde_json::Value: If you specify serde_json::Value as the target type for a paging call, the SDK will return errors as Ok(serde_json::Value) instead of Err(ErrorMessage). This happens because Microsoft Graph returns error details within the response body, and serde_json::Value can successfully deserialize those error structures.

    Handling Custom Types: If you use a custom type and the request fails (e.g., due to an expired access token), the deserialization of the error body into your custom type will fail, resulting in an Err(ErrorMessage).

  5. Distinguish between Id-based and Non-Id methods

    master

    The SDK uses two different naming schemes to distinguish between API methods that require a specific resource ID and those that operate on a collection (non-ID methods).

    • Non-Id methods (Collections): Use the plural form of the resource (e.g., users()). These are typically used for listing resources. For example, client.users().list_user().
    • Id-based methods (Single Resource): Use the singular form of the resource and require an ID (e.g., user(id)). These are used to retrieve or manipulate a specific resource. For example, client.user("USER_ID").get_user().
    // Non-Id example: Listing users
    let response = client.users().list_user().send().await.unwrap();
    
    // Id-based example: Getting a specific user
    let response = client.user("USER_ID").get_user().send().await.unwrap();
  6. Handle Paging with next links and delta links

    master

    When Microsoft Graph returns truncated data, it provides a URL (next link) to fetch the remaining data. The SDK provides several ways to handle these paging scenarios:

    1. Exhaustive JSON retrieval: Use .paging().json::<T>() to automatically follow all @odata.nextLink URLs and return all responses in a VecDeque<Response<Result<T>>>. Note that this method waits for all calls to complete, so latency increases with the number of pages.
    2. Streaming: Use .paging().stream::<T>() (async only) to process pages one by one as they arrive.
    3. Channels: Use .paging().channel::<T>() to receive responses via a channel receiver.

    Important Note on Response Types: To find the @odata.nextLink, the SDK must read the original response body. Consequently, the original reqwest::Response is consumed and replaced by a re-wrapped http::Response object that mimics the reqwest API.

    #[macro_use]
    extern crate serde;
    use graph_rs_sdk::*;
    
    #[derive(Debug, Serialize, Deserialize)]
    pub struct User {
        pub(crate) id: Option<String>,
        #[serde(rename = "userPrincipalName")]
        user_principal_name: Option<String>,
    }
    
    #[derive(Debug, Serialize, Deserialize)]
    pub struct Users {
      pub value: Vec<User>,
    }
    
    async fn paging() -> GraphResult<()> {
      let client = GraphClient::new("ACCESS_TOKEN");
    
      let deque = client
          .users()
          .list_user()
          .select(&["id", "userPrincipalName"])
          .paging()
          .json::<Users>()
          .await?;
      
      println!("{:#?}", deque);
      Ok(())
    }
  7. Use into_credential_builder for simplified authentication

    master

    By default, with_interactive_auth returns an AuthorizationEvent<CredentialBuilder>, which allows you to react to webview events (like the user closing the window).

    If you do not need to react to these specific events and simply want the credential builder, use the into_credential_builder() method. This maps the internal WebViewAuthorizationEvent into a standard Result<(AuthorizationResponse, CredentialBuilder)>.

    async fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: url::Url) -> anyhow::Result<()> {
        let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id)
            .with_tenant(tenant_id)
            .with_scope(vec!["user.read"])
            .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token.
            .with_redirect_uri(redirect_uri)
            .with_interactive_auth(Secret(client_secret.to_string()), Default::default())
            .into_credential_builder()?;
        Ok(())
    }
  8. Manage Mail and Messages

    master

    The SDK provides methods to interact with Microsoft Mail. You can retrieve mail folders, create messages, send mail, and list messages from specific folders like "Inbox".

    Key methods include:

    • me().mail_folder(id).get_mail_folders()
    • me().messages().create_messages(&payload)
    • me().send_mail(&payload)
    • me().user(id).mail_folder(name).messages().list_messages()
    // Get Inbox messages with a limit of 2
    let response = client
        .user(USER_ID)
        .mail_folder("Inbox")
        .messages()
        .list_messages()
        .top("2")
        .send()
        .await?;
    
    // Send a mail
    let response = client
        .me()
        .send_mail(&serde_json::json!({
            "message": {
              "subject": "Meet for lunch?",
              "body": {
                  "contentType": "Text",
                  "content": "The new cafeteria is open."
              },
              "toRecipients": [
                  { "emailAddress": { "address": "fannyd@contoso.onmicrosoft.com" } }
              ]
            },
            "saveToSentItems": "false"
          }))
        .send()
        .await?;
  9. Install graph-rs-sdk

    master

    Add graph-rs-sdk to your Cargo.toml. For async usage (the default), you should also include tokio with the full feature. If you plan to use custom types with serde, include serde with the derive feature and serde_json.

    graph-rs-sdk = "3.0.1"
    tokio = { version = "1.25.0", features = ["full"] }
    
    # For custom types
    serde = { version = "1", features = ["derive"] }
    serde_json = "1"
  10. Set up Interactive Authentication with a Certificate

    master

    To use interactive authentication with a certificate, you must enable both the interactive-auth and openssl features in your Cargo.toml.

    This returns an AuthorizationCodeCertificateCredentialBuilder. Like the client secret method, this builder is used to construct a ConfidentialClient for use with the GraphClient.

    async fn authenticate(x509: &X509Certificate, tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: url::Url) -> anyhow::Result<AuthorizationCodeCertificateCredentialBuilder> {
        let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id)
            .with_tenant(tenant_id)
            .with_scope(vec!["user.read"])
            .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token.
            .with_redirect_uri(redirect_uri)
            .with_certificate_interactive_auth(x509, Default::default())
            .into_credential_builder()?;
    
        println!("{authorization_response:#?}");
    
        Ok(credential_builder)
    }
  11. Handle Microsoft Graph API errors

    master

    When the Microsoft Graph API returns an error, the details are typically contained in the response body. Use the ResponseExt trait (for async) or BlockingResponseExt (for blocking) to extract the error message using .into_graph_error_message().

    use graph_rs_sdk::{http::ResponseExt, Graph};
    use std::error::Error;
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn Error>> {
        let client = GraphClient::new("token");
        let response = client.users().list_user().send().await?;
    
        if !response.status().is_success() {
            if let Ok(error) = response.into_graph_error_message().await {
                println!("{error:#?}");
            }
        }
    
        Ok(())
    }
  12. Set up Interactive Authentication with a Client Secret

    master

    Interactive Authentication uses a webview (via the wry crate) to perform sign-in and handle redirect URIs. This allows for an easy integration of the OAuth Authorization Code flow into your application.

    To use interactive authentication with a client secret, you must enable the interactive-auth feature in your Cargo.toml.

    The process returns a CredentialBuilder (specifically an AuthorizationCodeCredentialBuilder) which stores the authorization code. You can then use this builder to create a ConfidentialClient<AuthorizationCodeCredential>, which can be passed to a GraphClient. The client handles token acquisition and automatic refreshing if the offline_access scope is requested.

    Warning: Running interactive authentication in an asynchronous context may lead to crashes in some scenarios. Device code interactive auth does not currently work in async code.

    async fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: url::Url) -> anyhow::Result<AuthorizationCodeCredentialBuilder> {
        let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id)
            .with_tenant(tenant_id)
            .with_scope(vec!["user.read"])
            .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token.
            .with_redirect_uri(redirect_uri)
            .with_interactive_auth(Secret(client_secret.to_string()), Default::default())
            .into_credential_builder()?;
    
        println!("{authorization_response:#?}");
    
        Ok(credential_builder)
    }