commit d9edb3f48a3e48244654baf0dfbe100fb65f531b
parent 8a510da2132106af7dbf1cfdd675fa549bf2c5bb
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date: Sun, 19 Oct 2025 14:34:05 +0200
oauth2_gateway: base implementation
Diffstat:
8 files changed, 423 insertions(+), 0 deletions(-)
diff --git a/oauth2_gateway/.gitignore b/oauth2_gateway/.gitignore
@@ -0,0 +1,8 @@
+# Rust
+/target/
+Cargo.lock
+.idea/
+.vscode/
+.DS_Store
+*.log
+*.tmp
+\ No newline at end of file
diff --git a/oauth2_gateway/Cargo.toml b/oauth2_gateway/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "oauth2-gateway"
+version = "0.0.1"
+edition = "2024"
+
+[dependencies]
+# Web framework
+axum = "0.8.6"
+tokio = { version = "1.48.0", features = ["full"] }
+tower = "0.5"
+tower-http = { version = "0.6.6", features = ["trace"] }
+
+# Serialization
+serde = { version = "1.0.228", features = ["derive"] }
+serde_json = "1.0.145"
+
+# HTTP client needed for performing requests
+reqwest = { version = "0.12", features = ["json"] }
+
+# Configuration
+rust-ini = "0.21.3"
+clap = { version = "4.5.49", features = ["derive"] }
+
+# Utilities
+uuid = { version = "1.18.1", features = ["v4", "serde"] }
+chrono = { version = "0.4.42", features = ["serde"] }
+
+# Logging
+tracing = "0.1.41"
+tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
+
+# Error handling
+anyhow = "1.0.100"
diff --git a/oauth2_gateway/config.example.ini b/oauth2_gateway/config.example.ini
@@ -0,0 +1,11 @@
+[server]
+host = 127.0.0.1
+port = 8080
+
+[exchange]
+base_url = https://exchange.example.com
+notification_endpoint = /oauth2gw/kyc/notify
+
+[verifier]
+base_url = https://verifier.example.com
+management_api_path = /management/api/verifications
diff --git a/oauth2_gateway/src/config.rs b/oauth2_gateway/src/config.rs
@@ -0,0 +1,91 @@
+use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+use ini::Ini;
+use std::path::Path;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Config {
+ pub server: ServerConfig,
+ pub exchange: ExchangeConfig,
+ pub verifier: VerifierConfig,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ServerConfig {
+ pub host: String,
+ pub port: u16,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ExchangeConfig {
+ pub base_url: String,
+ pub notification_endpoint: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct VerifierConfig {
+ pub base_url: String,
+ pub management_api_path: String,
+}
+
+impl Config {
+ pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
+ let ini = Ini::load_from_file(path.as_ref())
+ .context("Failed to load config file")?;
+
+ // Server section
+ let server_section = ini
+ .section(Some("server"))
+ .context("Missing [server] section")?;
+
+ let server = ServerConfig {
+ host: server_section
+ .get("host")
+ .unwrap_or("127.0.0.1")
+ .to_string(),
+ port: server_section
+ .get("port")
+ .unwrap_or("8080")
+ .parse()
+ .context("Invalid port")?,
+ };
+
+ // Exchange section
+ let exchange_section = ini
+ .section(Some("exchange"))
+ .context("Missing [exchange] section")?;
+
+ let exchange = ExchangeConfig {
+ base_url: exchange_section
+ .get("base_url")
+ .context("Missing exchange.base_url")?
+ .to_string(),
+ notification_endpoint: exchange_section
+ .get("notification_endpoint")
+ .unwrap_or("/oauth2gw/kyc/notify")
+ .to_string(),
+ };
+
+ // Verifier section
+ let verifier_section = ini
+ .section(Some("verifier"))
+ .context("Missing [verifier] section")?;
+
+ let verifier = VerifierConfig {
+ base_url: verifier_section
+ .get("base_url")
+ .context("Missing verifier.base_url")?
+ .to_string(),
+ management_api_path: verifier_section
+ .get("management_api_path")
+ .unwrap_or("/management/api/verifications")
+ .to_string(),
+ };
+
+ Ok(Config {
+ server,
+ exchange,
+ verifier,
+ })
+ }
+}
diff --git a/oauth2_gateway/src/handlers.rs b/oauth2_gateway/src/handlers.rs
@@ -0,0 +1,132 @@
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::IntoResponse,
+ Json,
+};
+use serde_json::json;
+
+use crate::{
+ models::*,
+ state::AppState,
+};
+
+// Health check endpoint
+pub async fn health_check() -> impl IntoResponse {
+ Json(json!({
+ "status": "healthy",
+ "service": "oauth2-gateway",
+ // "version": env!("CARGO_PKG_VERSION") // leak version info?
+ }))
+}
+
+// POST /setup/{clientId}
+pub async fn setup(
+ State(_state): State<AppState>,
+ Path(client_id): Path<String>,
+ Json(request): Json<SetupRequest>,
+) -> impl IntoResponse {
+ tracing::info!("Setup request for client: {}, scope: {}", client_id, request.scope);
+
+ // Generate a simple nonce for now
+ // TODO: Change to cyptographic nonce
+ let nonce = uuid::Uuid::new_v4().to_string();
+
+ tracing::info!("Generated nonce: {}", nonce);
+
+ (
+ StatusCode::OK,
+ Json(SetupResponse { nonce })
+ )
+}
+
+// GET /authorize/{nonce}
+pub async fn authorize(
+ State(_state): State<AppState>,
+ Path(nonce): Path<String>,
+) -> impl IntoResponse {
+ tracing::info!("Authorize request for nonce: {}", nonce);
+
+ // TODO: Call the SwiyuVerifier to generate the QR code/verification URL
+
+ // For now, return a mock response
+ let response = AuthorizeResponse {
+ verification_id: uuid::Uuid::new_v4(),
+ verification_url: format!("swiyu://verify?nonce={}", nonce),
+ };
+
+ (StatusCode::OK, Json(response))
+}
+
+// POST /token
+pub async fn token(
+ State(_state): State<AppState>,
+ Json(request): Json<TokenRequest>,
+) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
+ tracing::info!("Token request for code: {}", request.code);
+
+ if request.grant_type != "authorization_code" {
+ return Err((
+ StatusCode::BAD_REQUEST,
+ Json(ErrorResponse::new("invalid_grant_type"))
+ ));
+ }
+
+ // TODO: Validate nonce/code
+ // TODO: Change to cyptographic token
+
+ // Generate a simple access token
+ let access_token = uuid::Uuid::new_v4().to_string();
+
+ let response = TokenResponse {
+ access_token,
+ token_type: "Bearer".to_string(),
+ expires_in: 3600,
+ };
+
+ Ok((StatusCode::OK, Json(response)))
+}
+
+// GET /info
+pub async fn info(
+ State(_state): State<AppState>,
+) -> impl IntoResponse {
+ tracing::info!("Info request received");
+
+ // TODO: Validate access token
+ // TODO: Call the SwiyuVerifier to retrieve the VC data
+
+ let credential = VerifiableCredential {
+ data: json!({
+ "@context": ["https://www.w3.org/2018/credentials/v1"],
+ "type": ["VerifiableCredential"],
+ "credentialSubject": {
+ "givenName": "John",
+ "familyName": "Doe",
+ "dateOfBirth": "1990-01-01",
+ "age_over_18": true,
+ }
+ }),
+ };
+
+ (StatusCode::OK, Json(credential))
+}
+
+// POST /notification/{request_id}
+pub async fn notification_webhook(
+ State(_state): State<AppState>,
+ Path(request_id): Path<String>,
+ Json(request): Json<NotificationRequest>,
+) -> impl IntoResponse {
+ tracing::info!(
+ "Webhook notification received for request_id: {}, nonce: {}, complete: {}",
+ request_id,
+ request.nonce,
+ request.verification_complete
+ );
+
+ // TODO: When verification is complete, post the Exchange at
+ // TODO: {exchange_base_url}/oauth2gw/kyc/notify/{clientId}
+
+ StatusCode::OK
+}
diff --git a/oauth2_gateway/src/main.rs b/oauth2_gateway/src/main.rs
@@ -0,0 +1,65 @@
+use anyhow::Result;
+use axum::{
+ routing::{get, post},
+ Router,
+};
+use clap::Parser;
+use tower_http::trace::TraceLayer;
+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
+
+mod config;
+mod handlers;
+mod models;
+mod state;
+
+use config::Config;
+use state::AppState;
+
+#[derive(Parser, Debug)]
+#[command(version)]
+struct Args {
+ #[arg(short, value_name = "FILE")]
+ config: String,
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ // Init logging, tracing
+ tracing_subscriber::registry()
+ .with(
+ tracing_subscriber::EnvFilter::try_from_default_env()
+ .unwrap_or_else(|_| "oauth2_gateway=info,tower_http=info".into()),
+ )
+ .with(tracing_subscriber::fmt::layer())
+ .init();
+
+ let args = Args::parse();
+
+ tracing::info!("Starting OAuth2 Gateway v{}", env!("CARGO_PKG_VERSION"));
+ tracing::info!("Loading configuration from: {}", args.config);
+
+ let config = Config::from_file(&args.config)?;
+
+ // Share config between modules
+ let state = AppState::new(config.clone());
+
+ let app = Router::new()
+ .route("/health", get(handlers::health_check))
+ .route("/setup/{client_id}", post(handlers::setup))
+ .route("/authorize/{nonce}", get(handlers::authorize))
+ .route("/token", post(handlers::token))
+ .route("/info", get(handlers::info))
+ .route("/notify/{client_id}", post(handlers::notification_webhook))
+ .layer(TraceLayer::new_for_http())
+ .with_state(state);
+
+ let addr = format!("{}:{}", config.server.host, config.server.port);
+ let listener = tokio::net::TcpListener::bind(&addr).await?;
+
+ tracing::info!("Server listening on {}", addr);
+ tracing::info!("Health check available at: http://{}/health", addr);
+
+ axum::serve(listener, app).await?;
+
+ Ok(())
+}
diff --git a/oauth2_gateway/src/models.rs b/oauth2_gateway/src/models.rs
@@ -0,0 +1,67 @@
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+// Setup endpoint
+#[derive(Debug, Deserialize, Serialize)]
+pub struct SetupRequest {
+ pub scope: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct SetupResponse {
+ pub nonce: String,
+}
+
+// Authorize endpoint
+#[derive(Debug, Deserialize, Serialize)]
+pub struct AuthorizeResponse {
+ #[serde(rename = "verificationId")]
+ pub verification_id: Uuid, // Does the browser need this?
+ pub verification_url: String,
+}
+
+// Token endpoint
+// WARING: RFC 6749 also requires:
+// - redirect_uri
+// - client_id
+#[derive(Debug, Deserialize, Serialize)]
+pub struct TokenRequest {
+ pub grant_type: String,
+ pub code: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct TokenResponse {
+ pub access_token: String,
+ pub token_type: String,
+ pub expires_in: u64,
+}
+
+// Info endpoint
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct VerifiableCredential {
+ #[serde(flatten)]
+ pub data: serde_json::Value,
+}
+
+// Notification webhook
+#[derive(Debug, Deserialize, Serialize)]
+pub struct NotificationRequest {
+ pub nonce: String,
+ #[serde(default)]
+ pub verification_complete: bool,
+}
+
+// Error response
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ErrorResponse {
+ pub error: String,
+}
+
+impl ErrorResponse {
+ pub fn new(error: impl Into<String>) -> Self {
+ Self {
+ error: error.into(),
+ }
+ }
+}
diff --git a/oauth2_gateway/src/state.rs b/oauth2_gateway/src/state.rs
@@ -0,0 +1,15 @@
+use crate::config::Config;
+use std::sync::Arc;
+
+#[derive(Clone)]
+pub struct AppState {
+ pub config: Arc<Config>,
+}
+
+impl AppState {
+ pub fn new(config: Config) -> Self {
+ Self {
+ config: Arc::new(config),
+ }
+ }
+}