mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-13 10:12:19 -06:00
refactor(auth): implement session-based PKCE and eliminate provider duplication
- Replace in-memory PKCE storage with encrypted session cookies - Add PKCE verifier and CSRF state fields to JWT Claims struct - Move common PKCE validation logic to OAuthProvider trait - Extract provider-specific methods for token exchange and user fetching - Remove PkceManager and DashMap-based storage system - Update GitHub and Discord providers to use new session-based approach
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
use axum::{response::IntoResponse, response::Redirect};
|
||||
use axum_cookie::CookieManager;
|
||||
use jsonwebtoken::EncodingKey;
|
||||
use oauth2::{AuthorizationCode, CsrfToken, PkceCodeVerifier, Scope, TokenResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -6,11 +8,9 @@ use std::sync::Arc;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use crate::{
|
||||
auth::{
|
||||
pkce::PkceManager,
|
||||
provider::{AuthUser, OAuthProvider},
|
||||
},
|
||||
auth::provider::{AuthUser, OAuthProvider},
|
||||
errors::ErrorResponse,
|
||||
session,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -56,16 +56,11 @@ pub async fn fetch_github_user(
|
||||
pub struct GitHubProvider {
|
||||
pub client: super::OAuthClient,
|
||||
pub http: reqwest::Client,
|
||||
pkce: PkceManager,
|
||||
}
|
||||
|
||||
impl GitHubProvider {
|
||||
pub fn new(client: super::OAuthClient, http: reqwest::Client) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
client,
|
||||
http,
|
||||
pkce: PkceManager::default(),
|
||||
})
|
||||
Arc::new(Self { client, http })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +73,8 @@ impl OAuthProvider for GitHubProvider {
|
||||
"GitHub"
|
||||
}
|
||||
|
||||
async fn authorize(&self) -> axum::response::Response {
|
||||
let (pkce_challenge, verifier) = self.pkce.generate_challenge();
|
||||
async fn authorize(&self, cookie: &CookieManager, encoding_key: &EncodingKey) -> axum::response::Response {
|
||||
let (pkce_challenge, pkce_verifier) = oauth2::PkceCodeChallenge::new_random_sha256();
|
||||
let (authorize_url, csrf_state) = self
|
||||
.client
|
||||
.authorize_url(CsrfToken::new_random)
|
||||
@@ -87,44 +82,35 @@ impl OAuthProvider for GitHubProvider {
|
||||
.add_scope(Scope::new("user:email".to_string()))
|
||||
.add_scope(Scope::new("read:user".to_string()))
|
||||
.url();
|
||||
// store verifier keyed by the returned state
|
||||
self.pkce.store_verifier(csrf_state.secret(), verifier);
|
||||
|
||||
// Store PKCE verifier and CSRF state in session
|
||||
let session_token = session::create_pkce_session(pkce_verifier.secret(), csrf_state.secret(), encoding_key);
|
||||
session::set_session_cookie(cookie, &session_token);
|
||||
|
||||
trace!(state = %csrf_state.secret(), "Generated OAuth authorization URL");
|
||||
Redirect::to(authorize_url.as_str()).into_response()
|
||||
}
|
||||
|
||||
async fn handle_callback(&self, code: &str, state: &str) -> Result<AuthUser, ErrorResponse> {
|
||||
let Some(verifier) = self.pkce.take_verifier(state) else {
|
||||
warn!(%state, "Missing or expired PKCE verifier for state parameter");
|
||||
return Err(ErrorResponse::bad_request(
|
||||
"invalid_request",
|
||||
Some("missing or expired pkce verifier for state".into()),
|
||||
));
|
||||
};
|
||||
|
||||
async fn exchange_code_for_token(&self, code: &str, verifier: &str) -> Result<String, ErrorResponse> {
|
||||
let token = self
|
||||
.client
|
||||
.exchange_code(AuthorizationCode::new(code.to_string()))
|
||||
.set_pkce_verifier(PkceCodeVerifier::new(verifier))
|
||||
.set_pkce_verifier(PkceCodeVerifier::new(verifier.to_string()))
|
||||
.request_async(&self.http)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!(error = %e, %state, "Token exchange with GitHub failed");
|
||||
warn!(error = %e, "Token exchange with GitHub failed");
|
||||
ErrorResponse::bad_gateway("token_exchange_failed", Some(e.to_string()))
|
||||
})?;
|
||||
|
||||
let user = fetch_github_user(&self.http, token.access_token().secret())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!(error = %e, "Failed to fetch GitHub user profile");
|
||||
ErrorResponse::bad_gateway("github_api_error", Some(format!("failed to fetch user: {}", e)))
|
||||
})?;
|
||||
let _emails = fetch_github_emails(&self.http, token.access_token().secret())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!(error = %e, "Failed to fetch GitHub user emails");
|
||||
ErrorResponse::bad_gateway("github_api_error", Some(format!("failed to fetch emails: {}", e)))
|
||||
})?;
|
||||
Ok(token.access_token().secret().to_string())
|
||||
}
|
||||
|
||||
async fn fetch_user_from_token(&self, access_token: &str) -> Result<AuthUser, ErrorResponse> {
|
||||
let user = fetch_github_user(&self.http, access_token).await.map_err(|e| {
|
||||
warn!(error = %e, "Failed to fetch GitHub user profile");
|
||||
ErrorResponse::bad_gateway("github_api_error", Some(format!("failed to fetch user: {}", e)))
|
||||
})?;
|
||||
|
||||
Ok(AuthUser {
|
||||
id: user.id.to_string(),
|
||||
@@ -135,26 +121,3 @@ impl OAuthProvider for GitHubProvider {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GitHubProvider {}
|
||||
|
||||
/// Fetch user emails from GitHub API
|
||||
pub async fn fetch_github_emails(
|
||||
http_client: &reqwest::Client,
|
||||
access_token: &str,
|
||||
) -> Result<Vec<GitHubEmail>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let response = http_client
|
||||
.get("https://api.github.com/user/emails")
|
||||
.header("Authorization", format!("Bearer {}", access_token))
|
||||
.header("Accept", "application/vnd.github.v3+json")
|
||||
.header("User-Agent", crate::config::USER_AGENT)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("GitHub API error: {}", response.status()).into());
|
||||
}
|
||||
|
||||
let emails: Vec<GitHubEmail> = response.json().await?;
|
||||
Ok(emails)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user