#[cfg(feature = "axum")]
use crate::oauth::OAuth2Provider;
#[cfg(feature = "axum")]
use std::sync::Arc;
pub trait OAuth2StateStore: Send + Sync {
fn save_state(&self, state: &str, client_id: &str);
fn get_state(&self, state: &str) -> Option<String>;
}
use parking_lot::Mutex;
use std::collections::HashMap;
#[derive(Default)]
pub struct MemoryOAuth2StateStore {
states: Mutex<HashMap<String, String>>,
}
impl MemoryOAuth2StateStore {
pub fn new() -> Self {
Self::default()
}
}
impl OAuth2StateStore for MemoryOAuth2StateStore {
fn save_state(&self, state: &str, client_id: &str) {
self.states
.lock()
.insert(state.to_string(), client_id.to_string());
}
fn get_state(&self, state: &str) -> Option<String> {
self.states.lock().get(state).cloned()
}
}
#[cfg(feature = "axum")]
mod axum_impl {
use super::*;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::get;
use serde::Deserialize;
pub struct OAuth2CallbackConfig {
pub provider: Arc<dyn OAuth2Provider>,
pub state_store: Arc<dyn OAuth2StateStore>,
#[cfg(feature = "redis-store")]
pub token_store: Option<Arc<dyn crate::oauth_store::OAuth2TokenStore>>,
pub success_redirect: String,
}
#[derive(Deserialize)]
pub struct CallbackQuery {
pub code: Option<String>,
pub state: Option<String>,
pub error: Option<String>,
}
pub async fn oauth2_callback_handler(
State(config): State<Arc<OAuth2CallbackConfig>>,
Query(query): Query<CallbackQuery>,
) -> Response {
if let Some(error) = &query.error {
return (StatusCode::BAD_GATEWAY, format!("OAuth2 错误: {error}")).into_response();
}
let code = match &query.code {
Some(c) if !c.is_empty() => c.clone(),
_ => {
return (StatusCode::BAD_REQUEST, "缺少授权码").into_response();
}
};
let state = match &query.state {
Some(s) if !s.is_empty() => s.clone(),
_ => {
return (StatusCode::BAD_REQUEST, "缺少 state 参数").into_response();
}
};
match config.state_store.get_state(&state) {
Some(_) => {}
None => {
return (
StatusCode::FORBIDDEN,
"OAUTH2_CSRF_STATE_MISMATCH: state 校验失败",
)
.into_response();
}
}
match config.provider.user_from_token(&code) {
Ok(_user) => {
Redirect::to(&config.success_redirect).into_response()
}
Err(err) => {
(
StatusCode::BAD_GATEWAY,
format!("OAuth2 token 交换失败: {err}"),
)
.into_response()
}
}
}
pub fn oauth2_callback_route(config: Arc<OAuth2CallbackConfig>) -> axum::Router {
axum::Router::new()
.route("/callback", get(oauth2_callback_handler))
.with_state(config)
}
}
#[cfg(feature = "axum")]
pub use axum_impl::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_state_store_save_get() {
let store = MemoryOAuth2StateStore::new();
store.save_state("state123", "client1");
let result = store.get_state("state123");
assert_eq!(result.as_deref(), Some("client1"));
}
#[test]
fn test_state_store_get_nonexistent() {
let store = MemoryOAuth2StateStore::new();
let result = store.get_state("nonexistent");
assert!(result.is_none());
}
#[test]
fn test_state_store_multiple() {
let store = MemoryOAuth2StateStore::new();
store.save_state("state1", "client1");
store.save_state("state2", "client2");
assert_eq!(store.get_state("state1").as_deref(), Some("client1"));
assert_eq!(store.get_state("state2").as_deref(), Some("client2"));
}
}