OAuth2-rs (Rust)

認証ライブラリOAuth2RustPKCE型安全非同期セキュリティ

認証ライブラリ

OAuth2-rs (Rust)

概要

OAuth2-rsは、Rust言語用の拡張可能で強力に型付けされたOAuth2クライアントライブラリです。RFC 6749に準拠したOAuth2プロトコルの完全実装を提供し、非同期・同期両方のI/Oをサポートします。アクセストークンの取得、state検証、リフレッシュトークンの取得など、OAuth2フローの包括的な機能を提供し、セキュリティベストプラクティスに従った実装となっています。

詳細

OAuth2-rsは、Rustエコシステムで最も成熟したOAuth2ライブラリとして、2024年現在も活発に開発・保守されています。ライブラリの特徴として、強力な型システムによる安全性の確保、async/awaitによる現代的な非同期プログラミングのサポート、PKCEなどのモダンなセキュリティ標準への対応があります。

バージョン5.0以降では、エンドポイントの型状態(typestate)システムが導入され、コンパイル時にOAuth2エンドポイントの設定状態を追跡できるようになりました。これにより、設定不備によるランタイムエラーを防ぎ、より安全なコードを書くことができます。MSRV(Minimum Supported Rust Version)は1.65で、reqwestやcurlなどの人気HTTPクライアントとの統合をサポートしています。

メリット・デメリット

メリット

  • 強力な型安全性: Rustの型システムを活用した設定エラーの防止
  • RFC準拠: OAuth2.0仕様(RFC 6749)の完全な実装
  • 非同期・同期対応: async/awaitと同期処理の両方をサポート
  • PKCEサポート: 最新のセキュリティ標準に対応
  • カスタマイズ可能: HTTPクライアントやエンドポイントの柔軟な設定
  • アクティブな開発: 2024年現在も活発に更新・保守されている

デメリット

  • 学習コーブ: Rustの型システムと複雑なジェネリクスの理解が必要
  • 設定の複雑さ: 型状態システムにより初期設定が複雑になる場合がある
  • コンパイル時間: 多数のジェネリクスにより若干のコンパイル時間増加
  • ドキュメント: 日本語リソースが限定的

参考ページ

書き方の例

基本的なOAuth2クライアント設定

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

// OAuth2クライアントの初期化
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フロー

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>> {
    // クライアント設定
    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())?);

    // 認証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!("認証URL: {}", auth_url);

    // コールバック処理(実際のアプリケーションではWebサーバーで処理)
    // let code = AuthorizationCode::new("received-auth-code".to_string());
    // let csrf_token_received = CsrfToken::new("received-csrf-token".to_string());

    // トークン交換
    let token_result = client
        .exchange_code(AuthorizationCode::new("auth-code".to_string()))
        .request_async(async_http_client)
        .await?;

    println!("アクセストークン: {}", token_result.access_token().secret());
    
    Ok(())
}

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, // クライアントシークレットは不要
        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())?);

    // PKCEチャレンジとベリファイアの生成
    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

    // 認証URLの生成(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!("認証URL (PKCE): {}", auth_url);

    // トークン交換(PKCE検証付き)
    let token_result = client
        .exchange_code(AuthorizationCode::new("auth-code".to_string()))
        .set_pkce_verifier(pkce_verifier)
        .request_async(async_http_client)
        .await?;

    println!("アクセストークン: {}", token_result.access_token().secret());
    
    Ok(())
}

Client Credentials Grantフロー

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())?)
    );

    // 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!("クライアントアクセストークン: {}", token_result.access_token().secret());
    
    Ok(())
}

リフレッシュトークンを使った認証

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!("新しいアクセストークン: {}", token_result.access_token().secret());
    
    // 新しいリフレッシュトークンが含まれる場合がある
    if let Some(new_refresh_token) = token_result.refresh_token() {
        println!("新しいリフレッシュトークン: {}", new_refresh_token.secret());
    }

    Ok(())
}

カスタムHTTPクライアントの使用

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

// カスタム非同期HTTPクライアント
fn custom_async_http_client(
    request: HttpRequest,
) -> Pin<Box<dyn Future<Output = Result<HttpResponse, reqwest::Error>> + Send>> {
    Box::pin(async move {
        // カスタムヘッダーやログ機能を追加
        let client = reqwest::Client::new();
        let response = client
            .request(request.method, &request.url.to_string())
            .headers(request.headers)
            .body(request.body)
            .send()
            .await?;

        // レスポンスの変換
        Ok(HttpResponse {
            status_code: response.status(),
            headers: response.headers().clone(),
            body: response.bytes().await?.to_vec(),
        })
    })
}

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

エラーハンドリング

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!("認証成功: {}", token.access_token().secret());
        }
        Err(RequestTokenError::ServerResponse(err)) => {
            println!("サーバーエラー: {:?}", err);
        }
        Err(RequestTokenError::Request(err)) => {
            println!("リクエストエラー: {:?}", err);
        }
        Err(RequestTokenError::Parse(err, response)) => {
            println!("パースエラー: {:?}, レスポンス: {:?}", err, response);
        }
        Err(RequestTokenError::Other(err)) => {
            println!("その他のエラー: {:?}", err);
        }
    }
}

型状態を使った安全な設定

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

// エンドポイント型状態を明示的に指定
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())
    )
}