commit c90333cab497dcf2d801255023502464dd76b0ce
parent 8190ae9da8df13436f0e9d361ed07c72a75feb92
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date: Sun, 7 Dec 2025 16:10:34 +0100
oauth2_gateway: change /setup scope to /authorize, add setup bearer token validation
Diffstat:
7 files changed, 351 insertions(+), 160 deletions(-)
diff --git a/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql b/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql
@@ -14,8 +14,9 @@ CREATE TABLE IF NOT EXISTS clients (
client_id VARCHAR(255) UNIQUE NOT NULL,
secret_hash VARCHAR(255) NOT NULL,
webhook_url TEXT NOT NULL,
- verifier_url TEXT NOT NULL,
+ verifier_url TEXT NOT NULL,
verifier_management_api_path VARCHAR(255) DEFAULT '/management/api/verifications',
+ scope TEXT NOT NULL DEFAULT 'age_over_18',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
@@ -31,6 +32,8 @@ COMMENT ON COLUMN clients.verifier_url
IS 'Client URL where oauth2 gateway will callback';
COMMENT ON COLUMN clients.verifier_management_api_path
IS 'Swiyu verifier api endpoint to create verification requests';
+COMMENT ON COLUMN clients.scope
+ IS 'Space-delimited OAuth2 scope (requested verification attributes, e.g., "first_name last_name age_over_18")';
CREATE INDEX IF NOT EXISTS idx_clients_client_id ON clients(client_id);
diff --git a/oauth2_gateway/src/bin/client_management_cli.rs b/oauth2_gateway/src/bin/client_management_cli.rs
@@ -54,6 +54,10 @@ enum Commands {
/// Verifier management API path (default: /management/api/verifications)
#[arg(long)]
verifier_api_path: Option<String>,
+
+ /// OAuth2 scope (space-delimited attributes, default: "age_over_18")
+ #[arg(long)]
+ scope: Option<String>,
},
Update {
@@ -67,6 +71,9 @@ enum Commands {
#[arg(long)]
verifier_api_path: Option<String>,
+
+ #[arg(long)]
+ scope: Option<String>,
},
/// Delete a client (WARNING: cascades to all sessions)
@@ -101,6 +108,7 @@ async fn main() -> Result<()> {
webhook_url,
verifier_url,
verifier_api_path,
+ scope,
} => {
cmd_create_client(
&pool,
@@ -109,6 +117,7 @@ async fn main() -> Result<()> {
&webhook_url,
&verifier_url,
verifier_api_path.as_deref(),
+ scope.as_deref(),
)
.await?
}
@@ -117,6 +126,7 @@ async fn main() -> Result<()> {
webhook_url,
verifier_url,
verifier_api_path,
+ scope,
} => {
cmd_update_client(
&pool,
@@ -124,6 +134,7 @@ async fn main() -> Result<()> {
webhook_url.as_deref(),
verifier_url.as_deref(),
verifier_api_path.as_deref(),
+ scope.as_deref(),
)
.await?
}
@@ -170,6 +181,7 @@ async fn cmd_show_client(pool: &sqlx::PgPool, client_id: &str) -> Result<()> {
println!("Webhook URL: {}", client.webhook_url);
println!("Verifier URL: {}", client.verifier_url);
println!("Verifier API Path: {}", client.verifier_management_api_path);
+ println!("Scope: {}", client.scope);
println!("Created: {}", client.created_at);
println!("Updated: {}", client.updated_at);
@@ -183,6 +195,7 @@ async fn cmd_create_client(
webhook_url: &str,
verifier_url: &str,
verifier_api_path: Option<&str>,
+ scope: Option<&str>,
) -> Result<()> {
let client = db::clients::register_client(
pool,
@@ -191,6 +204,7 @@ async fn cmd_create_client(
webhook_url,
verifier_url,
verifier_api_path,
+ scope,
)
.await
.context("Failed to create client")?;
@@ -200,6 +214,7 @@ async fn cmd_create_client(
println!("UUID: {}", client.id);
println!("Client ID: {}", client.client_id);
println!("Webhook URL: {}", client.webhook_url);
+ println!("Scope: {}", client.scope);
Ok(())
}
@@ -210,9 +225,10 @@ async fn cmd_update_client(
webhook_url: Option<&str>,
verifier_url: Option<&str>,
verifier_api_path: Option<&str>,
+ scope: Option<&str>,
) -> Result<()> {
- if webhook_url.is_none() && verifier_url.is_none() && verifier_api_path.is_none() {
- anyhow::bail!("No fields to update. Specify at least one of: --webhook-url, --verifier-url, --verifier-api-path");
+ if webhook_url.is_none() && verifier_url.is_none() && verifier_api_path.is_none() && scope.is_none() {
+ anyhow::bail!("No fields to update. Specify at least one of: --webhook-url, --verifier-url, --verifier-api-path, --scope");
}
let client = db::clients::get_client_by_id(pool, client_id)
@@ -225,6 +241,7 @@ async fn cmd_update_client(
webhook_url,
verifier_url,
verifier_api_path,
+ scope,
)
.await
.context("Failed to update client")?;
@@ -236,6 +253,7 @@ async fn cmd_update_client(
println!("Webhook URL: {}", updated.webhook_url);
println!("Verifier URL: {}", updated.verifier_url);
println!("Verifier API Path: {}", updated.verifier_management_api_path);
+ println!("Scope: {}", updated.scope);
Ok(())
}
diff --git a/oauth2_gateway/src/db/clients.rs b/oauth2_gateway/src/db/clients.rs
@@ -14,6 +14,7 @@ pub struct Client {
pub webhook_url: String,
pub verifier_url: String,
pub verifier_management_api_path: String,
+ pub scope: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -26,19 +27,21 @@ pub async fn register_client(
webhook_url: &str,
verifier_url: &str,
verifier_management_api_path: Option<&str>,
+ scope: Option<&str>,
) -> Result<Client> {
let api_path = verifier_management_api_path
.unwrap_or("/management/api/verifications");
+ let scope_value = scope.unwrap_or("age_over_18");
let secret_hash = bcrypt::hash(client_secret, bcrypt::DEFAULT_COST)?;
let client = sqlx::query_as::<_, Client>(
r#"
INSERT INTO oauth2gw.clients
- (client_id, secret_hash, webhook_url, verifier_url, verifier_management_api_path)
- VALUES ($1, $2, $3, $4, $5)
+ (client_id, secret_hash, webhook_url, verifier_url, verifier_management_api_path, scope)
+ VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, client_id, secret_hash, webhook_url, verifier_url,
- verifier_management_api_path, created_at, updated_at
+ verifier_management_api_path, scope, created_at, updated_at
"#
)
.bind(client_id)
@@ -46,6 +49,7 @@ pub async fn register_client(
.bind(webhook_url)
.bind(verifier_url)
.bind(api_path)
+ .bind(scope_value)
.fetch_one(pool)
.await?;
@@ -60,7 +64,7 @@ pub async fn get_client_by_id(
let client = sqlx::query_as::<_, Client>(
r#"
SELECT id, client_id, secret_hash, webhook_url, verifier_url,
- verifier_management_api_path, created_at, updated_at
+ verifier_management_api_path, scope, created_at, updated_at
FROM oauth2gw.clients
WHERE client_id = $1
"#
@@ -80,7 +84,7 @@ pub async fn get_client_by_uuid(
let client = sqlx::query_as::<_, Client>(
r#"
SELECT id, client_id, secret_hash, webhook_url, verifier_url,
- verifier_management_api_path, created_at, updated_at
+ verifier_management_api_path, scope, created_at, updated_at
FROM oauth2gw.clients
WHERE id = $1
"#
@@ -92,6 +96,27 @@ pub async fn get_client_by_uuid(
Ok(client)
}
+/// Get client secret hash by client_id
+///
+/// Returns the secret hash if client exists, None otherwise
+pub async fn get_client_secret_hash(
+ pool: &PgPool,
+ client_id: &str,
+) -> Result<Option<String>> {
+ let result = sqlx::query_scalar::<_, String>(
+ r#"
+ SELECT secret_hash
+ FROM oauth2gw.clients
+ WHERE client_id = $1
+ "#
+ )
+ .bind(client_id)
+ .fetch_optional(pool)
+ .await?;
+
+ Ok(result)
+}
+
/// Authenticate a client by validating client_secret
///
/// Returns the client if authentication succeeds, None otherwise
@@ -103,7 +128,7 @@ pub async fn authenticate_client(
let client = sqlx::query_as::<_, Client>(
r#"
SELECT id, client_id, secret_hash, webhook_url, verifier_url,
- verifier_management_api_path, created_at, updated_at
+ verifier_management_api_path, scope, created_at, updated_at
FROM oauth2gw.clients
WHERE client_id = $1
"#
@@ -113,7 +138,7 @@ pub async fn authenticate_client(
.await?;
match client {
- Some(c) => {
+ Some(c) => {
if bcrypt::verify(client_secret, &c.secret_hash)? {
Ok(Some(c))
} else {
@@ -131,8 +156,8 @@ pub async fn update_client(
webhook_url: Option<&str>,
verifier_url: Option<&str>,
verifier_management_api_path: Option<&str>,
+ scope: Option<&str>,
) -> Result<Client> {
- // Get current client to use for fields that aren't being updated
let current = get_client_by_uuid(pool, id).await?
.ok_or_else(|| anyhow::anyhow!("Client not found"))?;
@@ -140,6 +165,7 @@ pub async fn update_client(
let new_verifier_url = verifier_url.unwrap_or(¤t.verifier_url);
let new_verifier_api_path = verifier_management_api_path
.unwrap_or(¤t.verifier_management_api_path);
+ let new_scope = scope.unwrap_or(¤t.scope);
let client = sqlx::query_as::<_, Client>(
r#"
@@ -148,15 +174,17 @@ pub async fn update_client(
webhook_url = $1,
verifier_url = $2,
verifier_management_api_path = $3,
+ scope = $4,
updated_at = CURRENT_TIMESTAMP
- WHERE id = $4
+ WHERE id = $5
RETURNING id, client_id, secret_hash, webhook_url, verifier_url,
- verifier_management_api_path, created_at, updated_at
+ verifier_management_api_path, scope, created_at, updated_at
"#
)
.bind(new_webhook_url)
.bind(new_verifier_url)
.bind(new_verifier_api_path)
+ .bind(new_scope)
.bind(id)
.fetch_one(pool)
.await?;
@@ -189,7 +217,7 @@ pub async fn list_clients(pool: &PgPool) -> Result<Vec<Client>> {
let clients = sqlx::query_as::<_, Client>(
r#"
SELECT id, client_id, secret_hash, webhook_url, verifier_url,
- verifier_management_api_path, created_at, updated_at
+ verifier_management_api_path, scope, created_at, updated_at
FROM oauth2gw.clients
ORDER BY created_at DESC
"#
diff --git a/oauth2_gateway/src/db/sessions.rs b/oauth2_gateway/src/db/sessions.rs
@@ -82,16 +82,15 @@ pub async fn create_session(
pool: &PgPool,
client_id: &str,
nonce: &str,
- scope: &str,
expires_in_minutes: i64,
) -> Result<Option<VerificationSession>> {
let session = sqlx::query_as::<_, VerificationSession>(
r#"
INSERT INTO oauth2gw.verification_sessions (client_id, nonce, scope,
expires_at, status)
- SELECT c.id, $1, $2, NOW() + $3 * INTERVAL '1 minute', 'pending'
+ SELECT c.id, $1, c.scope, NOW() + $2 * INTERVAL '1 minute', 'pending'
FROM oauth2gw.clients c
- WHERE c.client_id = $4
+ WHERE c.client_id = $3
RETURNING
id, client_id, nonce, scope, verification_url, request_id,
verifier_nonce, status, created_at, authorized_at,
@@ -99,7 +98,6 @@ pub async fn create_session(
"#
)
.bind(nonce)
- .bind(scope)
.bind(expires_in_minutes)
.bind(client_id)
.fetch_optional(pool)
diff --git a/oauth2_gateway/src/handlers.rs b/oauth2_gateway/src/handlers.rs
@@ -1,18 +1,13 @@
use axum::{
+ Json,
extract::{Path, Query, State},
- http::{header, StatusCode},
+ http::{StatusCode, header},
response::IntoResponse,
- Json,
};
-use serde_json::json;
use chrono::Utc;
+use serde_json::json;
-use crate::{
- db::sessions::SessionStatus,
- models::*,
- state::AppState,
- crypto,
-};
+use crate::{crypto, db::sessions::SessionStatus, models::*, state::AppState};
// Health check endpoint
pub async fn health_check() -> impl IntoResponse {
@@ -27,110 +22,189 @@ pub async fn health_check() -> impl IntoResponse {
pub async fn setup(
State(state): State<AppState>,
Path(client_id): Path<String>,
- Json(request): Json<SetupRequest>,
+ headers: axum::http::HeaderMap,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
+ tracing::info!("Setup request for client: {}", client_id);
- tracing::info!("Setup request for client: {}, scope: {}", client_id, request.scope);
+ let auth_header = headers
+ .get(header::AUTHORIZATION)
+ .and_then(|h| h.to_str().ok());
- let nonce = crypto::generate_nonce();
+ let bearer_token = match auth_header {
+ Some(h) if h.starts_with("Bearer ") => &h[7..],
+ _ => {
+ tracing::warn!(
+ "Missing or malformed Authorization header for client: {}",
+ client_id
+ );
+ return Err((
+ StatusCode::UNAUTHORIZED,
+ Json(ErrorResponse::new("unauthorized")),
+ ));
+ }
+ };
- tracing::debug!("Generated nonce: {}", nonce);
+ let secret_hash = crate::db::clients::get_client_secret_hash(&state.pool, &client_id)
+ .await
+ .map_err(|e| {
+ tracing::error!("DB error fetching client secret: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
+ })?;
- let session = crate::db::sessions::create_session(
- &state.pool,
- &client_id,
- &nonce,
- &request.scope,
- 15, // 15 minutes expiration
- ).await
- .map_err(|e| {
- tracing::error!("Failed to create session: {}", e);
- (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
+ let secret_hash = match secret_hash {
+ Some(hash) => hash,
+ None => {
+ tracing::warn!("Client not found: {}", client_id);
+ return Err((
+ StatusCode::UNAUTHORIZED,
+ Json(ErrorResponse::new("unauthorized")),
+ ));
+ }
+ };
+
+ let is_valid = bcrypt::verify(bearer_token, &secret_hash).map_err(|e| {
+ tracing::error!("Bcrypt verification error: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
})?;
+ if !is_valid {
+ tracing::warn!("Invalid bearer token for client: {}", client_id);
+ return Err((
+ StatusCode::UNAUTHORIZED,
+ Json(ErrorResponse::new("unauthorized")),
+ ));
+ }
+
+ let nonce = crypto::generate_nonce();
+
+ tracing::debug!("Generated nonce: {}", nonce);
+
+ let session = crate::db::sessions::create_session(&state.pool, &client_id, &nonce, 15)
+ .await
+ .map_err(|e| {
+ tracing::error!("Failed to create session: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
+ })?;
let session = match session {
Some(s) => s,
None => {
tracing::warn!("Client not found: {}", client_id);
- return Err((StatusCode::NOT_FOUND, Json(ErrorResponse::new("client_not_found"))))
+ return Err((
+ StatusCode::NOT_FOUND,
+ Json(ErrorResponse::new("client_not_found")),
+ ));
}
};
- tracing::info!("Created session {} for client {} with nonce {}", session.id, client_id, nonce);
+ tracing::info!(
+ "Created session {} for client {} with nonce {}",
+ session.id,
+ client_id,
+ nonce
+ );
Ok((StatusCode::OK, Json(SetupResponse { nonce })))
}
-// GET /authorize?response_type=code&client_id={client_id}&nonce={nonce}
+// GET /authorize/{nonce}?response_type=code&client_id={client_id}&redirect_uri={uri}&state={state}
pub async fn authorize(
State(state): State<AppState>,
+ Path(nonce): Path<String>,
Query(params): Query<AuthorizeQuery>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
+ tracing::info!(
+ "Authorize request for client: {}, nonce: {}",
+ params.client_id,
+ nonce
+ );
- tracing::info!("Authorize request for client: {}, nonce: {}",params.client_id, params.nonce);
-
-
- // Validate response_type
if params.response_type != "code" {
- return Err((StatusCode::BAD_REQUEST,
- Json(ErrorResponse::new("invalid_request"))));
+ return Err((
+ StatusCode::BAD_REQUEST,
+ Json(ErrorResponse::new("invalid_request")),
+ ));
}
-
- // Fetch session and client data (idempotent)
let session_data = crate::db::sessions::get_session_for_authorize(
&state.pool,
- ¶ms.nonce,
+ &nonce,
¶ms.client_id,
- ).await
+ )
+ .await
.map_err(|e| {
tracing::error!("DB error in authorize: {}", e);
- (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
})?;
-
let data = match session_data {
Some(d) => d,
None => {
- tracing::warn!("Session not found for nonce: {}", params.nonce);
- return Err((StatusCode::NOT_FOUND,
- Json(ErrorResponse::new("session_not_found"))));
+ tracing::warn!("Session not found for nonce: {}", nonce);
+ return Err((
+ StatusCode::NOT_FOUND,
+ Json(ErrorResponse::new("session_not_found")),
+ ));
}
};
-
// Backend validation
if data.expires_at < Utc::now() {
tracing::warn!("Session expired: {}", data.session_id);
- return Err((StatusCode::GONE, Json(ErrorResponse::new("session_expired"))));
+ return Err((
+ StatusCode::GONE,
+ Json(ErrorResponse::new("session_expired")),
+ ));
}
-
// Check status for idempotency
match data.status {
SessionStatus::Authorized => {
// Already authorized - return cached response
- tracing::info!("Session {} already authorized, returning cached response", data.session_id);
+ tracing::info!(
+ "Session {} already authorized, returning cached response",
+ data.session_id
+ );
- let verification_id = data.request_id
+ let verification_id = data
+ .request_id
.and_then(|id| uuid::Uuid::parse_str(&id).ok())
.unwrap_or(uuid::Uuid::nil());
- return Ok((StatusCode::OK, Json(AuthorizeResponse {
- verification_id,
- verification_url: data.verification_url.unwrap_or_default(),
- })));
+ return Ok((
+ StatusCode::OK,
+ Json(AuthorizeResponse {
+ verification_id,
+ verification_url: data.verification_url.unwrap_or_default(),
+ }),
+ ));
}
SessionStatus::Pending => {
// Proceed with authorization
}
_ => {
- tracing::warn!("Session {} in invalid status: {:?}",
- data.session_id, data.status);
- return Err((StatusCode::CONFLICT,
- Json(ErrorResponse::new("invalid_session_status"))));
+ tracing::warn!(
+ "Session {} in invalid status: {:?}",
+ data.session_id,
+ data.status
+ );
+ return Err((
+ StatusCode::CONFLICT,
+ Json(ErrorResponse::new("invalid_session_status")),
+ ));
}
}
@@ -151,38 +225,43 @@ pub async fn authorize(
dcql_query: None,
};
-
- tracing::debug!("Swiyu verifier request: {}", serde_json::to_string_pretty(&verifier_request).unwrap());
+ tracing::debug!(
+ "Swiyu verifier request: {}",
+ serde_json::to_string_pretty(&verifier_request).unwrap()
+ );
tracing::debug!("Calling Swiyu verifier at: {}", verifier_url);
-
- let verifier_response = state.http_client
+ let verifier_response = state
+ .http_client
.post(&verifier_url)
.json(&verifier_request)
.send()
.await
.map_err(|e| {
tracing::error!("Failed to call Swiyu verifier: {}", e);
- (StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_unavailable")))
+ (
+ StatusCode::BAD_GATEWAY,
+ Json(ErrorResponse::new("verifier_unavailable")),
+ )
})?;
-
if !verifier_response.status().is_success() {
let status = verifier_response.status();
let body = verifier_response.text().await.unwrap_or_default();
tracing::error!("Swiyu verifier returned error {}: {}", status, body);
- return Err((StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_error"))));
+ return Err((
+ StatusCode::BAD_GATEWAY,
+ Json(ErrorResponse::new("verifier_error")),
+ ));
}
-
- let swiyu_response: SwiyuManagementResponse = verifier_response
- .json()
- .await
- .map_err(|e| {
- tracing::error!("Failed to parse Swiyu response: {}", e);
- (StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_invalid_response")))
- })?;
-
+ let swiyu_response: SwiyuManagementResponse = verifier_response.json().await.map_err(|e| {
+ tracing::error!("Failed to parse Swiyu response: {}", e);
+ (
+ StatusCode::BAD_GATEWAY,
+ Json(ErrorResponse::new("verifier_invalid_response")),
+ )
+ })?;
// Update session with verifier data
let result = crate::db::sessions::update_session_authorized(
@@ -191,32 +270,44 @@ pub async fn authorize(
&swiyu_response.verification_url,
&swiyu_response.id.to_string(),
swiyu_response.request_nonce.as_deref(),
- ).await
+ )
+ .await
.map_err(|e| {
tracing::error!("Failed to update session: {}", e);
- (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
})?;
+ tracing::info!(
+ "Session {} authorized, verification_id: {}",
+ data.session_id,
+ swiyu_response.id
+ );
- tracing::info!("Session {} authorized, verification_id: {}",
- data.session_id, swiyu_response.id);
-
- Ok((StatusCode::OK, Json(AuthorizeResponse {
- verification_id: swiyu_response.id,
- verification_url: result.verification_url,
- })))
+ Ok((
+ StatusCode::OK,
+ Json(AuthorizeResponse {
+ verification_id: swiyu_response.id,
+ verification_url: result.verification_url,
+ }),
+ ))
}
/// Build a presentation definition from a space-delimited scope string
///
/// Example: "age_over_18" or "first_name last_name"
fn build_presentation_definition(scope: &str) -> PresentationDefinition {
- use uuid::Uuid;
use std::collections::HashMap;
+ use uuid::Uuid;
let attributes: Vec<&str> = scope.split_whitespace().collect();
- tracing::debug!("Building presentation definition for attributes: {:?}", attributes);
+ tracing::debug!(
+ "Building presentation definition for attributes: {:?}",
+ attributes
+ );
// First field: $.vct with filter for credential type
let vct_field = Field {
@@ -230,7 +321,6 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition {
}),
};
-
// Attribute fields from scope
let mut fields: Vec<Field> = vec![vct_field];
for attr in &attributes {
@@ -243,7 +333,6 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition {
});
}
-
let mut format = HashMap::new();
format.insert(
"vc+sd-jwt".to_string(),
@@ -275,14 +364,13 @@ 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);
// Validate grant_type
if request.grant_type != "authorization_code" {
return Err((
StatusCode::BAD_REQUEST,
- Json(ErrorResponse::new("unsupported_grant_type"))
+ Json(ErrorResponse::new("unsupported_grant_type")),
));
}
@@ -291,69 +379,100 @@ pub async fn token(
&state.pool,
&request.client_id,
&request.client_secret,
- ).await
+ )
+ .await
.map_err(|e| {
tracing::error!("DB error during client authentication: {}", e);
- (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
})?;
let client = match client {
Some(c) => c,
None => {
tracing::warn!("Client authentication failed for {}", request.client_id);
- return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse::new("invalid_client"))));
+ return Err((
+ StatusCode::UNAUTHORIZED,
+ Json(ErrorResponse::new("invalid_client")),
+ ));
}
};
-
// Fetch code (idempotent)
- let code_data = crate::db::authorization_codes::get_code_for_token_exchange(
- &state.pool,
- &request.code,
- ).await
- .map_err(|e| {
- tracing::error!("DB error in token exchange: {}", e);
- (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
- })?;
+ let code_data =
+ crate::db::authorization_codes::get_code_for_token_exchange(&state.pool, &request.code)
+ .await
+ .map_err(|e| {
+ tracing::error!("DB error in token exchange: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
+ })?;
let data = match code_data {
Some(d) => d,
None => {
tracing::warn!("Authorization code not found or expired: {}", request.code);
- return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse::new("invalid_grant"))));
+ return Err((
+ StatusCode::BAD_REQUEST,
+ Json(ErrorResponse::new("invalid_grant")),
+ ));
}
};
// Verify the authorization code belongs to the client
if data.client_id != client.id {
- tracing::warn!("Authorization code {} does not belong to the client {}",
- request.code, request.client_id);
+ tracing::warn!(
+ "Authorization code {} does not belong to the client {}",
+ request.code,
+ request.client_id
+ );
- return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse::new("invalid_grant"))));
+ return Err((
+ StatusCode::BAD_REQUEST,
+ Json(ErrorResponse::new("invalid_grant")),
+ ));
}
// Check for existing token
if let Some(existing_token) = data.existing_token {
- tracing::info!("Token already exists for session {}, returning cached response",
- data.session_id);
- return Ok((StatusCode::OK, Json(TokenResponse {
- access_token: existing_token,
- token_type: "Bearer".to_string(),
- expires_in: 3600,
- })));
+ tracing::info!(
+ "Token already exists for session {}, returning cached response",
+ data.session_id
+ );
+ return Ok((
+ StatusCode::OK,
+ Json(TokenResponse {
+ access_token: existing_token,
+ token_type: "Bearer".to_string(),
+ expires_in: 3600,
+ }),
+ ));
}
// Check if code was already used
if data.was_already_used {
tracing::warn!("Authorization code {} was already used", request.code);
- return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse::new("invalid_grant"))));
+ return Err((
+ StatusCode::BAD_REQUEST,
+ Json(ErrorResponse::new("invalid_grant")),
+ ));
}
// Validate session status
if data.session_status != SessionStatus::Verified {
- tracing::warn!("Session {} not in verified status: {:?}",
- data.session_id, data.session_status);
- return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse::new("invalid_grant"))));
+ tracing::warn!(
+ "Session {} not in verified status: {:?}",
+ data.session_id,
+ data.session_status
+ );
+ return Err((
+ StatusCode::BAD_REQUEST,
+ Json(ErrorResponse::new("invalid_grant")),
+ ));
}
// Generate new token and complete session
@@ -363,19 +482,26 @@ pub async fn token(
data.session_id,
&access_token,
3600, // 1 hour
- ).await
+ )
+ .await
.map_err(|e| {
tracing::error!("Failed to create token: {}", e);
- (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error")))
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
})?;
tracing::info!("Token created for session {}", data.session_id);
- Ok((StatusCode::OK, Json(TokenResponse {
- access_token: token.token,
- token_type: "Bearer".to_string(),
- expires_in: 3600,
- })))
+ Ok((
+ StatusCode::OK,
+ Json(TokenResponse {
+ access_token: token.token,
+ token_type: "Bearer".to_string(),
+ expires_in: 3600,
+ }),
+ ))
}
// GET /info
@@ -394,8 +520,10 @@ pub async fn info(
Some(h) if h.starts_with("Bearer ") => &h[7..],
_ => {
tracing::warn!("Missing or malformed Authorization header");
- return Err((StatusCode::UNAUTHORIZED,
- Json(ErrorResponse::new("invalid_token"))));
+ return Err((
+ StatusCode::UNAUTHORIZED,
+ Json(ErrorResponse::new("invalid_token")),
+ ));
}
};
@@ -404,30 +532,38 @@ pub async fn info(
.await
.map_err(|e| {
tracing::error!("DB error in info: {}", e);
- (StatusCode::INTERNAL_SERVER_ERROR,
- Json(ErrorResponse::new("internal_error")))
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse::new("internal_error")),
+ )
})?;
let data = match token_data {
Some(d) => d,
None => {
tracing::warn!("Token not found or expired");
- return Err((StatusCode::UNAUTHORIZED,
- Json(ErrorResponse::new("invalid_token"))));
+ return Err((
+ StatusCode::UNAUTHORIZED,
+ Json(ErrorResponse::new("invalid_token")),
+ ));
}
};
// Validate token
if data.revoked {
tracing::warn!("Token {} is revoked", data.token_id);
- return Err((StatusCode::UNAUTHORIZED,
- Json(ErrorResponse::new("invalid_token"))));
+ return Err((
+ StatusCode::UNAUTHORIZED,
+ Json(ErrorResponse::new("invalid_token")),
+ ));
}
if data.session_status != SessionStatus::Completed {
tracing::warn!("Session not completed: {:?}", data.session_status);
- return Err((StatusCode::UNAUTHORIZED,
- Json(ErrorResponse::new("invalid_token"))));
+ return Err((
+ StatusCode::UNAUTHORIZED,
+ Json(ErrorResponse::new("invalid_token")),
+ ));
}
// Return verifiable credential
@@ -456,10 +592,15 @@ pub async fn notification_webhook(
let session_data = match crate::db::sessions::get_session_for_notification(
&state.pool,
&webhook.verification_id.to_string(),
- ).await {
+ )
+ .await
+ {
Ok(Some(data)) => data,
Ok(None) => {
- tracing::warn!("Session not found for verification_id: {}", webhook.verification_id);
+ tracing::warn!(
+ "Session not found for verification_id: {}",
+ webhook.verification_id
+ );
return StatusCode::OK;
}
Err(e) => {
@@ -472,7 +613,8 @@ pub async fn notification_webhook(
if session_data.status != SessionStatus::Authorized {
tracing::warn!(
"Session {} not in authorized status: {:?}",
- session_data.session_id, session_data.status
+ session_data.session_id,
+ session_data.status
);
return StatusCode::OK;
}
@@ -514,7 +656,10 @@ pub async fn notification_webhook(
SwiyuVerificationStatus::Success => (SessionStatus::Verified, "verified"),
SwiyuVerificationStatus::Failed => (SessionStatus::Failed, "failed"),
SwiyuVerificationStatus::Pending => {
- tracing::info!("Verification {} still pending, ignoring webhook", webhook.verification_id);
+ tracing::info!(
+ "Verification {} still pending, ignoring webhook",
+ webhook.verification_id
+ );
return StatusCode::OK;
}
};
@@ -550,11 +695,14 @@ pub async fn notification_webhook(
&session_data.webhook_url,
&webhook_body,
swiyu_result.wallet_response.as_ref(),
- ).await {
+ )
+ .await
+ {
Ok(code) => {
tracing::info!(
"Session {} updated to {}, auth code created, webhook queued",
- session_data.session_id, status_str
+ session_data.session_id,
+ status_str
);
tracing::debug!("Generated authorization code: {}", code);
}
diff --git a/oauth2_gateway/src/main.rs b/oauth2_gateway/src/main.rs
@@ -42,7 +42,7 @@ async fn main() -> Result<()> {
let app = Router::new()
.route("/health", get(handlers::health_check))
.route("/setup/{client_id}", post(handlers::setup))
- .route("/authorize", get(handlers::authorize))
+ .route("/authorize/{nonce}", get(handlers::authorize))
.route("/token", post(handlers::token))
.route("/info", get(handlers::info))
.route("/notification", post(handlers::notification_webhook))
diff --git a/oauth2_gateway/src/models.rs b/oauth2_gateway/src/models.rs
@@ -3,11 +3,6 @@ use uuid::Uuid;
use std::collections::HashMap;
#[derive(Debug, Deserialize, Serialize)]
-pub struct SetupRequest {
- pub scope: String,
-}
-
-#[derive(Debug, Deserialize, Serialize)]
pub struct SetupResponse {
pub nonce: String,
}
@@ -16,7 +11,8 @@ pub struct SetupResponse {
pub struct AuthorizeQuery {
pub response_type: String,
pub client_id: String,
- pub nonce: String,
+ pub redirect_uri: String,
+ pub state: String,
}
#[derive(Debug, Deserialize, Serialize)]