use crate::ayb_db::db_interfaces::AybDb;
use crate::error::AybError;
use crate::hosted_db::QueryMode;
use crate::http::structs::{OAuthErrorResponse, OAuthTokenRequest, OAuthTokenResponse};
use crate::server::config::AybConfig;
use crate::server::tokens::{generate_api_token, APITokenScope};
use crate::server::web_frontend::public_base_url;
use actix_web::{post, web, HttpResponse, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use chrono::Utc;
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
fn verify_pkce(code_verifier: &str, code_challenge: &str) -> bool {
let mut hasher = Sha256::new();
hasher.update(code_verifier.as_bytes());
let hash = hasher.finalize();
let computed_challenge = URL_SAFE_NO_PAD.encode(hash);
computed_challenge
.as_bytes()
.ct_eq(code_challenge.as_bytes())
.into()
}
#[post("/v1/oauth/token")]
pub async fn oauth_token(
body: web::Json<OAuthTokenRequest>,
ayb_db: web::Data<Box<dyn AybDb>>,
ayb_config: web::Data<AybConfig>,
) -> Result<HttpResponse> {
if body.grant_type != "authorization_code" {
return Ok(HttpResponse::BadRequest().json(OAuthErrorResponse {
error: "unsupported_grant_type".to_string(),
error_description: Some("Only grant_type=authorization_code is supported".to_string()),
}));
}
let auth_request = match ayb_db.get_oauth_authorization_request(&body.code).await {
Ok(req) => req,
Err(AybError::RecordNotFound { .. }) => {
return Ok(HttpResponse::BadRequest().json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
error_description: Some("Authorization code not found".to_string()),
}));
}
Err(err) => {
return Ok(
HttpResponse::InternalServerError().json(OAuthErrorResponse {
error: "server_error".to_string(),
error_description: Some(err.to_string()),
}),
);
}
};
if auth_request.used_at.is_some() {
return Ok(HttpResponse::BadRequest().json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
error_description: Some("Authorization code has already been used".to_string()),
}));
}
if auth_request.expires_at < Utc::now().naive_utc() {
return Ok(HttpResponse::BadRequest().json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
error_description: Some("Authorization code has expired".to_string()),
}));
}
if auth_request.redirect_uri != body.redirect_uri {
return Ok(HttpResponse::BadRequest().json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
error_description: Some("redirect_uri does not match".to_string()),
}));
}
if !verify_pkce(&body.code_verifier, &auth_request.code_challenge) {
return Ok(HttpResponse::BadRequest().json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
error_description: Some("PKCE verification failed".to_string()),
}));
}
if let Err(err) = ayb_db
.mark_oauth_authorization_request_used(&body.code)
.await
{
return Ok(
HttpResponse::InternalServerError().json(OAuthErrorResponse {
error: "server_error".to_string(),
error_description: Some(err.to_string()),
}),
);
}
let (api_token, token_string) = match generate_api_token(
auth_request.entity_id,
Some(APITokenScope {
database_id: auth_request.database_id,
query_permission_level: auth_request.query_permission_level,
app_name: auth_request.app_name.clone(),
}),
) {
Ok(result) => result,
Err(err) => {
return Ok(
HttpResponse::InternalServerError().json(OAuthErrorResponse {
error: "server_error".to_string(),
error_description: Some(err.to_string()),
}),
);
}
};
if let Err(err) = ayb_db.create_api_token(&api_token).await {
return Ok(
HttpResponse::InternalServerError().json(OAuthErrorResponse {
error: "server_error".to_string(),
error_description: Some(err.to_string()),
}),
);
}
let base_url = public_base_url(&ayb_config);
let database_path = format!(
"{}/{}",
auth_request.entity_slug, auth_request.database_slug
);
let database_url = format!("{base_url}/v1/{database_path}");
let permission_str =
QueryMode::try_from(auth_request.query_permission_level).map(|q| q.to_str().to_string())?;
Ok(HttpResponse::Ok().json(OAuthTokenResponse {
access_token: token_string,
token_type: "Bearer".to_string(),
database: database_path,
query_permission_level: permission_str,
database_url,
}))
}