use crate::api::{ApiResponse, ApiState};
use axum::{
Json,
extract::{Query, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Redirect},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct AuthorizeRequest {
pub response_type: String,
pub client_id: String,
pub redirect_uri: String,
pub scope: Option<String>,
pub state: Option<String>,
pub code_challenge: Option<String>,
pub code_challenge_method: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TokenRequest {
pub grant_type: String,
pub code: Option<String>,
pub client_id: String,
pub client_secret: Option<String>,
pub redirect_uri: Option<String>,
pub refresh_token: Option<String>,
pub code_verifier: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct TokenResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_token: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct OAuthError {
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ClientInfo {
pub client_id: String,
pub name: String,
pub description: String,
pub redirect_uris: Vec<String>,
pub scopes: Vec<String>,
}
pub async fn authorize(
State(_state): State<ApiState>,
Query(params): Query<AuthorizeRequest>,
) -> impl IntoResponse {
if params.response_type != "code" {
let error = OAuthError {
error: "unsupported_response_type".to_string(),
error_description: Some("Only 'code' response type is supported".to_string()),
error_uri: None,
state: params.state,
};
return (StatusCode::BAD_REQUEST, Json(error)).into_response();
}
if params.client_id.is_empty() {
let error = OAuthError {
error: "invalid_request".to_string(),
error_description: Some("client_id is required".to_string()),
error_uri: None,
state: params.state,
};
return (StatusCode::BAD_REQUEST, Json(error)).into_response();
}
if params.redirect_uri.is_empty() {
let error = OAuthError {
error: "invalid_request".to_string(),
error_description: Some("redirect_uri is required".to_string()),
error_uri: None,
state: params.state,
};
return (StatusCode::BAD_REQUEST, Json(error)).into_response();
}
let auth_code = format!("auth_code_{}", chrono::Utc::now().timestamp());
let mut redirect_url = params.redirect_uri;
redirect_url.push_str(&format!("?code={}", auth_code));
if let Some(state) = params.state {
redirect_url.push_str(&format!("&state={}", state));
}
tracing::info!("OAuth authorization for client: {}", params.client_id);
Redirect::to(&redirect_url).into_response()
}
pub async fn token(
State(state): State<ApiState>,
_headers: HeaderMap,
Json(req): Json<TokenRequest>,
) -> ApiResponse<TokenResponse> {
match req.grant_type.as_str() {
"authorization_code" => handle_authorization_code_grant(state, req).await,
"refresh_token" => handle_refresh_token_grant(state, req).await,
"client_credentials" => handle_client_credentials_grant(state, req).await,
_ => ApiResponse::error_typed(
"unsupported_grant_type",
format!("Unsupported grant type: {}", req.grant_type),
),
}
}
async fn handle_authorization_code_grant(
_state: ApiState,
req: TokenRequest,
) -> ApiResponse<TokenResponse> {
if req.code.is_none() {
return ApiResponse::error_typed("invalid_request", "authorization code is required");
}
if req.redirect_uri.is_none() {
return ApiResponse::error_typed("invalid_request", "redirect_uri is required");
}
let response = TokenResponse {
access_token: format!("access_token_{}", chrono::Utc::now().timestamp()),
token_type: "Bearer".to_string(),
expires_in: 3600,
refresh_token: Some(format!("refresh_token_{}", chrono::Utc::now().timestamp())),
scope: Some("read write".to_string()),
id_token: None,
};
tracing::info!("Authorization code exchanged for client: {}", req.client_id);
ApiResponse::<TokenResponse>::success(response)
}
async fn handle_refresh_token_grant(
_state: ApiState,
req: TokenRequest,
) -> ApiResponse<TokenResponse> {
if req.refresh_token.is_none() {
return ApiResponse::error_typed("invalid_request", "refresh_token is required");
}
let response = TokenResponse {
access_token: format!("new_access_token_{}", chrono::Utc::now().timestamp()),
token_type: "Bearer".to_string(),
expires_in: 3600,
refresh_token: req.refresh_token, scope: Some("read write".to_string()),
id_token: None,
};
tracing::info!("Refresh token used for client: {}", req.client_id);
ApiResponse::<TokenResponse>::success(response)
}
async fn handle_client_credentials_grant(
_state: ApiState,
req: TokenRequest,
) -> ApiResponse<TokenResponse> {
let response = TokenResponse {
access_token: format!("client_access_token_{}", chrono::Utc::now().timestamp()),
token_type: "Bearer".to_string(),
expires_in: 7200, refresh_token: None, scope: Some("api:read api:write".to_string()),
id_token: None,
};
tracing::info!("Client credentials grant for client: {}", req.client_id);
ApiResponse::<TokenResponse>::success(response)
}
#[derive(Debug, Deserialize)]
pub struct RevokeRequest {
pub token: String,
pub token_type_hint: Option<String>,
}
pub async fn revoke_token(
State(_state): State<ApiState>,
Json(req): Json<RevokeRequest>,
) -> ApiResponse<()> {
if req.token.is_empty() {
return ApiResponse::validation_error_typed("token is required");
}
tracing::info!("Token revoked: {}", &req.token[..10]);
ApiResponse::<()>::ok_with_message("Token revoked successfully")
}
#[derive(Debug, Deserialize)]
pub struct IntrospectRequest {
pub token: String,
pub token_type_hint: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct IntrospectResponse {
pub active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
}
pub async fn introspect_token(
State(_state): State<ApiState>,
Json(req): Json<IntrospectRequest>,
) -> ApiResponse<IntrospectResponse> {
if req.token.is_empty() {
return ApiResponse::validation_error_typed("token is required");
}
let response = IntrospectResponse {
active: true, scope: Some("read write".to_string()),
client_id: Some("example_client".to_string()),
username: Some("user@example.com".to_string()),
token_type: Some("Bearer".to_string()),
exp: Some(chrono::Utc::now().timestamp() as u64 + 3600),
iat: Some(chrono::Utc::now().timestamp() as u64),
sub: Some("user_123".to_string()),
};
tracing::info!("Token introspected: {}", &req.token[..10]);
ApiResponse::<IntrospectResponse>::success(response)
}
pub async fn get_client_info(
State(_state): State<ApiState>,
axum::extract::Path(client_id): axum::extract::Path<String>,
) -> ApiResponse<ClientInfo> {
let client = ClientInfo {
client_id: client_id.clone(),
name: format!("Client {}", client_id),
description: "OAuth 2.0 client application".to_string(),
redirect_uris: vec![
"https://example.com/callback".to_string(),
"https://app.example.com/auth/callback".to_string(),
],
scopes: vec![
"read".to_string(),
"write".to_string(),
"profile".to_string(),
],
};
ApiResponse::<ClientInfo>::success(client)
}