OAuth2-rs (Rust)
認証ライブラリ
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())
)
}