use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::AuthError;
#[derive(Debug, Clone)]
pub struct AuthorizationRequest {
pub client_id: String,
pub redirect_uri: String,
pub scope: String,
pub state: String,
pub response_type: String,
}
impl AuthorizationRequest {
pub fn new(
client_id: impl Into<String>,
redirect_uri: impl Into<String>,
scope: impl Into<String>,
state: impl Into<String>,
) -> Self {
Self {
client_id: client_id.into(),
redirect_uri: redirect_uri.into(),
scope: scope.into(),
state: state.into(),
response_type: "code".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct AuthorizationCode {
pub code: String,
pub client_id: String,
pub user_id: i64,
pub redirect_uri: String,
pub scope: String,
pub created_at: i64,
pub expires_at: i64,
pub used: bool,
}
impl AuthorizationCode {
const DEFAULT_LIFETIME_SECS: i64 = 600;
pub fn new(
code: impl Into<String>,
client_id: impl Into<String>,
user_id: i64,
redirect_uri: impl Into<String>,
scope: impl Into<String>,
) -> Self {
let now = current_secs();
Self {
code: code.into(),
client_id: client_id.into(),
user_id,
redirect_uri: redirect_uri.into(),
scope: scope.into(),
created_at: now,
expires_at: now + Self::DEFAULT_LIFETIME_SECS,
used: false,
}
}
pub fn is_expired(&self) -> bool {
current_secs() > self.expires_at
}
}
#[derive(Debug, Clone)]
pub struct TokenRequest {
pub grant_type: String,
pub code: String,
pub redirect_uri: String,
pub client_id: String,
}
impl TokenRequest {
pub fn new(
code: impl Into<String>,
redirect_uri: impl Into<String>,
client_id: impl Into<String>,
) -> Self {
Self {
grant_type: "authorization_code".to_string(),
code: code.into(),
redirect_uri: redirect_uri.into(),
client_id: client_id.into(),
}
}
}
pub struct OAuth2Server {
codes: Mutex<HashMap<String, AuthorizationCode>>,
clients: HashMap<String, String>,
}
impl OAuth2Server {
pub fn new(clients: HashMap<String, String>) -> Self {
Self {
codes: Mutex::new(HashMap::new()),
clients,
}
}
pub fn empty() -> Self {
Self {
codes: Mutex::new(HashMap::new()),
clients: HashMap::new(),
}
}
pub fn register_client(
&mut self,
client_id: impl Into<String>,
client_secret: impl Into<String>,
) {
self.clients.insert(client_id.into(), client_secret.into());
}
pub fn validate_client(&self, client_id: &str, client_secret: &str) -> bool {
self.clients
.get(client_id)
.map(|secret| secret == client_secret)
.unwrap_or(false)
}
pub fn has_client(&self, client_id: &str) -> bool {
self.clients.contains_key(client_id)
}
pub fn create_authorization_code(
&self,
req: &AuthorizationRequest,
user_id: i64,
) -> Result<AuthorizationCode, AuthError> {
if !self.has_client(&req.client_id) {
return Err(AuthError::Config(format!(
"Unregistered client: {}",
req.client_id
)));
}
if req.response_type != "code" {
return Err(AuthError::Config(format!(
"Unsupported response_type: {}",
req.response_type
)));
}
let code_value = generate_code();
let auth_code = AuthorizationCode::new(
code_value,
req.client_id.clone(),
user_id,
req.redirect_uri.clone(),
req.scope.clone(),
);
self.codes
.lock()
.unwrap()
.insert(auth_code.code.clone(), auth_code.clone());
Ok(auth_code)
}
pub fn exchange_code(&self, req: &TokenRequest) -> Result<AuthorizationCode, AuthError> {
let mut codes = self.codes.lock().unwrap();
let auth_code = codes
.get(&req.code)
.ok_or_else(|| AuthError::TokenInvalid("Invalid authorization code".to_string()))?;
if auth_code.is_expired() {
return Err(AuthError::TokenExpired(
"Authorization code expired".to_string(),
));
}
if auth_code.used {
return Err(AuthError::TokenInvalid(
"Authorization code already used".to_string(),
));
}
if auth_code.redirect_uri != req.redirect_uri {
return Err(AuthError::TokenInvalid("Redirect URI mismatch".to_string()));
}
if auth_code.client_id != req.client_id {
return Err(AuthError::TokenInvalid("Client ID mismatch".to_string()));
}
let result = auth_code.clone();
codes.get_mut(&req.code).unwrap().used = true;
Ok(result)
}
pub fn code_count(&self) -> usize {
self.codes.lock().unwrap().len()
}
pub fn cleanup(&self) -> usize {
let mut codes = self.codes.lock().unwrap();
let before = codes.len();
codes.retain(|_, c| !c.is_expired() && !c.used);
before - codes.len()
}
}
fn generate_code() -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
current_nanos().hash(&mut hasher);
let seed = hasher.finish();
format!("{:064x}", seed)
}
fn current_secs() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
fn current_nanos() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_server() -> OAuth2Server {
let mut clients = HashMap::new();
clients.insert("client1".to_string(), "secret1".to_string());
OAuth2Server::new(clients)
}
fn make_request() -> AuthorizationRequest {
AuthorizationRequest::new("client1", "https://app.com/cb", "read write", "xyz123")
}
#[test]
fn test_authorization_request_new() {
let req = AuthorizationRequest::new("cid", "https://cb", "read", "state");
assert_eq!(req.client_id, "cid");
assert_eq!(req.redirect_uri, "https://cb");
assert_eq!(req.scope, "read");
assert_eq!(req.state, "state");
assert_eq!(req.response_type, "code");
}
#[test]
fn test_oauth2_server_validate_client() {
let server = make_server();
assert!(server.validate_client("client1", "secret1"));
assert!(!server.validate_client("client1", "wrong"));
assert!(!server.validate_client("unknown", "secret1"));
}
#[test]
fn test_oauth2_server_has_client() {
let server = make_server();
assert!(server.has_client("client1"));
assert!(!server.has_client("unknown"));
}
#[test]
fn test_oauth2_server_register_client() {
let mut server = OAuth2Server::empty();
assert!(!server.has_client("new_client"));
server.register_client("new_client", "new_secret");
assert!(server.has_client("new_client"));
assert!(server.validate_client("new_client", "new_secret"));
}
#[test]
fn test_create_authorization_code_success() {
let server = make_server();
let req = make_request();
let code = server.create_authorization_code(&req, 42).unwrap();
assert_eq!(code.client_id, "client1");
assert_eq!(code.user_id, 42);
assert_eq!(code.redirect_uri, "https://app.com/cb");
assert_eq!(code.scope, "read write");
assert!(!code.used);
assert!(!code.is_expired());
assert_eq!(server.code_count(), 1);
}
#[test]
fn test_create_authorization_code_unregistered_client() {
let server = make_server();
let req = AuthorizationRequest::new("unknown", "https://cb", "read", "state");
let result = server.create_authorization_code(&req, 1);
assert!(matches!(result, Err(AuthError::Config(_))));
}
#[test]
fn test_create_authorization_code_wrong_response_type() {
let server = make_server();
let mut req = make_request();
req.response_type = "token".to_string();
let result = server.create_authorization_code(&req, 1);
assert!(matches!(result, Err(AuthError::Config(_))));
}
#[test]
fn test_exchange_code_success() {
let server = make_server();
let req = make_request();
let code = server.create_authorization_code(&req, 99).unwrap();
let token_req = TokenRequest::new(&code.code, "https://app.com/cb", "client1");
let result = server.exchange_code(&token_req).unwrap();
assert_eq!(result.user_id, 99);
assert_eq!(result.client_id, "client1");
}
#[test]
fn test_exchange_code_invalid_code() {
let server = make_server();
let token_req = TokenRequest::new("nonexistent", "https://app.com/cb", "client1");
let result = server.exchange_code(&token_req);
assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
}
#[test]
fn test_exchange_code_already_used() {
let server = make_server();
let req = make_request();
let code = server.create_authorization_code(&req, 1).unwrap();
let token_req = TokenRequest::new(&code.code, "https://app.com/cb", "client1");
server.exchange_code(&token_req).unwrap();
let result = server.exchange_code(&token_req);
assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
}
#[test]
fn test_exchange_code_redirect_uri_mismatch() {
let server = make_server();
let req = make_request();
let code = server.create_authorization_code(&req, 1).unwrap();
let token_req = TokenRequest::new(&code.code, "https://wrong.com/cb", "client1");
let result = server.exchange_code(&token_req);
assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
}
#[test]
fn test_exchange_code_client_id_mismatch() {
let server = make_server();
let req = make_request();
let code = server.create_authorization_code(&req, 1).unwrap();
let token_req = TokenRequest::new(&code.code, "https://app.com/cb", "wrong_client");
let result = server.exchange_code(&token_req);
assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
}
#[test]
fn test_authorization_code_is_expired() {
let mut code = AuthorizationCode::new("c", "cid", 1, "uri", "scope");
assert!(!code.is_expired());
code.expires_at = current_secs() - 100;
assert!(code.is_expired());
}
#[test]
fn test_oauth2_cleanup_removes_used() {
let server = make_server();
let req = make_request();
let code = server.create_authorization_code(&req, 1).unwrap();
let token_req = TokenRequest::new(&code.code, "https://app.com/cb", "client1");
server.exchange_code(&token_req).unwrap();
assert_eq!(server.code_count(), 1);
let removed = server.cleanup();
assert_eq!(removed, 1);
assert_eq!(server.code_count(), 0);
}
#[test]
fn test_oauth2_cleanup_keeps_valid() {
let server = make_server();
let req = make_request();
server.create_authorization_code(&req, 1).unwrap();
assert_eq!(server.code_count(), 1);
let removed = server.cleanup();
assert_eq!(removed, 0);
assert_eq!(server.code_count(), 1);
}
#[test]
fn test_generate_code_non_empty() {
let code = generate_code();
assert!(!code.is_empty());
assert_eq!(code.len(), 64);
}
#[test]
fn test_generate_code_different_each_call() {
let c1 = generate_code();
std::thread::sleep(std::time::Duration::from_millis(1));
let c2 = generate_code();
assert_ne!(c1, c2);
}
#[test]
fn test_oauth2_empty_server() {
let server = OAuth2Server::empty();
assert_eq!(server.code_count(), 0);
assert!(!server.has_client("any"));
}
#[test]
fn test_multiple_clients() {
let mut server = OAuth2Server::empty();
server.register_client("app1", "secret1");
server.register_client("app2", "secret2");
let req1 = AuthorizationRequest::new("app1", "https://a1/cb", "read", "s1");
let req2 = AuthorizationRequest::new("app2", "https://a2/cb", "write", "s2");
let c1 = server.create_authorization_code(&req1, 1).unwrap();
let c2 = server.create_authorization_code(&req2, 2).unwrap();
assert_eq!(server.code_count(), 2);
let wrong_req = TokenRequest::new(&c2.code, "https://a2/cb", "app1");
assert!(server.exchange_code(&wrong_req).is_err());
let right_req = TokenRequest::new(&c1.code, "https://a1/cb", "app1");
assert!(server.exchange_code(&right_req).is_ok());
}
}