OAuth2-rs (Rust)

Authentication LibraryOAuth2RustPKCEType SafetyAsyncSecurity

Authentication Library

OAuth2-rs (Rust)

Overview

OAuth2-rs is an extensible, strongly-typed OAuth2 client library for the Rust programming language. It provides a complete implementation of the OAuth2 protocol compliant with RFC 6749, supporting both async and sync I/O. The library offers comprehensive OAuth2 flow functionality including obtaining access tokens, state validation, and refresh token acquisition, all implemented following security best practices.

Details

OAuth2-rs stands as the most mature OAuth2 library in the Rust ecosystem, actively developed and maintained as of 2024. Key characteristics include safety assurance through Rust's strong type system, support for modern asynchronous programming with async/await, and compliance with modern security standards like PKCE.

Since version 5.0, an endpoint typestate system has been introduced, allowing compile-time tracking of OAuth2 endpoint configuration states. This prevents runtime errors due to configuration issues and enables writing safer code. The MSRV (Minimum Supported Rust Version) is 1.65, with support for integration with popular HTTP clients like reqwest and curl.

Pros and Cons

Pros

  • Strong Type Safety: Prevents configuration errors using Rust's type system
  • RFC Compliant: Complete implementation of OAuth2.0 specification (RFC 6749)
  • Async/Sync Support: Supports both async/await and synchronous processing
  • PKCE Support: Complies with latest security standards
  • Customizable: Flexible configuration of HTTP clients and endpoints
  • Active Development: Actively updated and maintained as of 2024

Cons

  • Learning Curve: Requires understanding of Rust's type system and complex generics
  • Configuration Complexity: Initial setup can be complex due to the typestate system
  • Compile Time: Slight increase in compile time due to numerous generics
  • Documentation: Limited Japanese resources

Reference Pages

Code Examples

Basic OAuth2 Client Configuration

use oauth2::{
    AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, 
    PkceCodeChallenge, RedirectUrl, Scope, TokenUrl
};
use oauth2::basic::BasicClient;

// Initialize OAuth2 client
let client = BasicClient::new(
    ClientId::new("your-client-id".to_string()),
    Some(ClientSecret::new("your-client-secret".to_string())),
    AuthUrl::new("https://provider.com/oauth2/authorize".to_string())?,
    Some(TokenUrl::new("https://provider.com/oauth2/token".to_string())?)
)
.set_redirect_uri(RedirectUrl::new("http://localhost:8080/callback".to_string())?);

Authorization Code Grant Flow

use oauth2::{
    AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken,
    RedirectUrl, Scope, TokenUrl, TokenResponse, reqwest::async_http_client
};
use oauth2::basic::{BasicClient, BasicTokenType};
use url::Url;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Client configuration
    let client = BasicClient::new(
        ClientId::new("your-client-id".to_string()),
        Some(ClientSecret::new("your-client-secret".to_string())),
        AuthUrl::new("https://provider.com/oauth2/authorize".to_string())?,
        Some(TokenUrl::new("https://provider.com/oauth2/token".to_string())?)
    )
    .set_redirect_uri(RedirectUrl::new("http://localhost:8080/callback".to_string())?);

    // Generate authorization URL
    let (auth_url, csrf_token) = client
        .authorize_url(CsrfToken::new_random)
        .add_scope(Scope::new("read".to_string()))
        .add_scope(Scope::new("write".to_string()))
        .url();

    println!("Authorization URL: {}", auth_url);

    // Callback handling (in real applications, handled by web server)
    // let code = AuthorizationCode::new("received-auth-code".to_string());
    // let csrf_token_received = CsrfToken::new("received-csrf-token".to_string());

    // Token exchange
    let token_result = client
        .exchange_code(AuthorizationCode::new("auth-code".to_string()))
        .request_async(async_http_client)
        .await?;

    println!("Access Token: {}", token_result.access_token().secret());
    
    Ok(())
}

Authentication Flow with PKCE

use oauth2::{
    AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge,
    PkceCodeVerifier, RedirectUrl, Scope, TokenUrl, reqwest::async_http_client
};
use oauth2::basic::BasicClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = BasicClient::new(
        ClientId::new("your-client-id".to_string()),
        None, // Client secret not required
        AuthUrl::new("https://provider.com/oauth2/authorize".to_string())?,
        Some(TokenUrl::new("https://provider.com/oauth2/token".to_string())?)
    )
    .set_redirect_uri(RedirectUrl::new("http://localhost:8080/callback".to_string())?);

    // Generate PKCE challenge and verifier
    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

    // Generate authorization URL (with PKCE)
    let (auth_url, csrf_token) = client
        .authorize_url(CsrfToken::new_random)
        .add_scope(Scope::new("openid".to_string()))
        .set_pkce_challenge(pkce_challenge)
        .url();

    println!("Authorization URL (PKCE): {}", auth_url);

    // Token exchange (with PKCE verification)
    let token_result = client
        .exchange_code(AuthorizationCode::new("auth-code".to_string()))
        .set_pkce_verifier(pkce_verifier)
        .request_async(async_http_client)
        .await?;

    println!("Access Token: {}", token_result.access_token().secret());
    
    Ok(())
}

Client Credentials Grant Flow

use oauth2::{
    ClientCredentialsTokenRequest, ClientId, ClientSecret, Scope,
    TokenUrl, reqwest::async_http_client
};
use oauth2::basic::BasicClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = BasicClient::new(
        ClientId::new("your-client-id".to_string()),
        Some(ClientSecret::new("your-client-secret".to_string())),
        AuthUrl::new("https://provider.com/oauth2/authorize".to_string())?,
        Some(TokenUrl::new("https://provider.com/oauth2/token".to_string())?)
    );

    // Obtain token using Client Credentials Grant
    let token_result = client
        .exchange_client_credentials()
        .add_scope(Scope::new("api:read".to_string()))
        .request_async(async_http_client)
        .await?;

    println!("Client Access Token: {}", token_result.access_token().secret());
    
    Ok(())
}

Authentication with Refresh Token

use oauth2::{
    RefreshToken, Scope, TokenResponse, reqwest::async_http_client
};
use oauth2::basic::BasicClient;

async fn refresh_access_token(
    client: &BasicClient,
    refresh_token: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let token_result = client
        .exchange_refresh_token(&RefreshToken::new(refresh_token.to_string()))
        .add_scope(Scope::new("read".to_string()))
        .request_async(async_http_client)
        .await?;

    println!("New Access Token: {}", token_result.access_token().secret());
    
    // New refresh token may be included
    if let Some(new_refresh_token) = token_result.refresh_token() {
        println!("New Refresh Token: {}", new_refresh_token.secret());
    }

    Ok(())
}

Custom HTTP Client Usage

use oauth2::{HttpRequest, HttpResponse, async_http_client};
use std::future::Future;
use std::pin::Pin;

// Custom async HTTP client
fn custom_async_http_client(
    request: HttpRequest,
) -> Pin<Box<dyn Future<Output = Result<HttpResponse, reqwest::Error>> + Send>> {
    Box::pin(async move {
        // Add custom headers or logging functionality
        let client = reqwest::Client::new();
        let response = client
            .request(request.method, &request.url.to_string())
            .headers(request.headers)
            .body(request.body)
            .send()
            .await?;

        // Response conversion
        Ok(HttpResponse {
            status_code: response.status(),
            headers: response.headers().clone(),
            body: response.bytes().await?.to_vec(),
        })
    })
}

// Usage example
let token_result = client
    .exchange_code(AuthorizationCode::new("auth-code".to_string()))
    .request_async(custom_async_http_client)
    .await?;

Error Handling

use oauth2::{
    RequestTokenError, basic::BasicErrorResponse, StandardErrorResponse
};

async fn handle_oauth_errors() {
    match client.exchange_code(code).request_async(async_http_client).await {
        Ok(token) => {
            println!("Authentication successful: {}", token.access_token().secret());
        }
        Err(RequestTokenError::ServerResponse(err)) => {
            println!("Server error: {:?}", err);
        }
        Err(RequestTokenError::Request(err)) => {
            println!("Request error: {:?}", err);
        }
        Err(RequestTokenError::Parse(err, response)) => {
            println!("Parse error: {:?}, Response: {:?}", err, response);
        }
        Err(RequestTokenError::Other(err)) => {
            println!("Other error: {:?}", err);
        }
    }
}

Safe Configuration with Typestates

use oauth2::{
    AuthUrl, TokenUrl, ClientId, ClientSecret, EndpointSet, EndpointNotSet
};
use oauth2::basic::BasicClient;

// Explicitly specify endpoint typestates
type MyClient = BasicClient<
    EndpointSet,    // HasAuthUrl
    EndpointNotSet, // HasDeviceAuthUrl
    EndpointNotSet, // HasIntrospectionUrl
    EndpointNotSet, // HasRevocationUrl
    EndpointSet,    // HasTokenUrl
>;

fn create_configured_client() -> MyClient {
    BasicClient::new(
        ClientId::new("client-id".to_string()),
        Some(ClientSecret::new("client-secret".to_string())),
        AuthUrl::new("https://example.com/auth".to_string()).unwrap(),
        Some(TokenUrl::new("https://example.com/token".to_string()).unwrap())
    )
}