auth-framework 0.4.2

A comprehensive, production-ready authentication and authorization framework for Rust applications
Documentation
//! OAuth 2.0 API Endpoints
//!
//! Handles OAuth 2.0 authorization, token exchange, and related operations

use crate::api::{ApiResponse, ApiState};
use axum::{
    Json,
    extract::{Query, State},
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Redirect},
};
use serde::{Deserialize, Serialize};

/// OAuth authorization request parameters
#[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>,
}

/// OAuth token request
#[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>,
}

/// OAuth token response
#[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>,
}

/// OAuth error response
#[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>,
}

/// Client information
#[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>,
}

/// GET /oauth/authorize
/// OAuth 2.0 authorization endpoint
pub async fn authorize(
    State(_state): State<ApiState>,
    Query(params): Query<AuthorizeRequest>,
) -> impl IntoResponse {
    // Validate required parameters
    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();
    }

    // In a real implementation:
    // 1. Validate client_id exists
    // 2. Validate redirect_uri is registered for client
    // 3. Check if user is authenticated
    // 4. Show consent screen if needed
    // 5. Generate authorization code
    // 6. Redirect with code

    // For now, simulate successful authorization
    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()
}

/// POST /oauth/token
/// OAuth 2.0 token endpoint
pub async fn token(
    State(state): State<ApiState>,
    _headers: HeaderMap,
    Json(req): Json<TokenRequest>,
) -> ApiResponse<TokenResponse> {
    // Validate grant type
    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> {
    // Validate required parameters
    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");
    }

    // In a real implementation:
    // 1. Validate authorization code
    // 2. Verify client credentials
    // 3. Validate redirect_uri matches
    // 4. Validate PKCE if used
    // 5. Generate access token and refresh token

    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");
    }

    // In a real implementation:
    // 1. Validate refresh token
    // 2. Verify client credentials
    // 3. Generate new access token
    // 4. Optionally rotate refresh token

    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, // Reuse existing 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> {
    // In a real implementation:
    // 1. Validate client credentials
    // 2. Check client is authorized for client_credentials grant
    // 3. Generate access token (no refresh token for client credentials)

    let response = TokenResponse {
        access_token: format!("client_access_token_{}", chrono::Utc::now().timestamp()),
        token_type: "Bearer".to_string(),
        expires_in: 7200,    // 2 hours for client credentials
        refresh_token: None, // No refresh token for client credentials
        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)
}

/// POST /oauth/revoke
/// Token revocation endpoint
#[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");
    }

    // In a real implementation:
    // 1. Validate client credentials
    // 2. Identify token type (access or refresh)
    // 3. Revoke the token
    // 4. If refresh token, revoke associated access tokens

    tracing::info!("Token revoked: {}", &req.token[..10]);
    ApiResponse::<()>::ok_with_message("Token revoked successfully")
}

/// POST /oauth/introspect
/// Token introspection endpoint (RFC 7662)
#[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");
    }

    // In a real implementation:
    // 1. Validate client credentials
    // 2. Look up token in storage
    // 3. Check if token is active and not expired
    // 4. Return token metadata

    let response = IntrospectResponse {
        active: true, // Placeholder
        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)
}

/// GET /oauth/clients/{client_id}
/// Get OAuth client information
pub async fn get_client_info(
    State(_state): State<ApiState>,
    axum::extract::Path(client_id): axum::extract::Path<String>,
) -> ApiResponse<ClientInfo> {
    // In a real implementation, fetch client from storage
    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)
}