Add SelfFreezoneClient wrapper for Self components

- Created SelfFreezoneClient in Self components
- Wraps SDK FreezoneScriptClient for Self-specific operations
- Implements send_verification_email method
- Uses Rhai script template for email verification
- Includes template variable substitution
- Added serde-wasm-bindgen dependency

Usage:
  let client = SelfFreezoneClient::builder()
      .supervisor_url("http://localhost:8080")
      .secret("my-secret")
      .build()?;

  client.send_verification_email(
      "user@example.com",
      "123456",
      "https://verify.com/abc"
  ).await?;
This commit is contained in:
Timur Gordon
2025-11-03 16:16:18 +01:00
parent be061409af
commit f970f3fb58
33 changed files with 8947 additions and 449 deletions

View File

@@ -23,3 +23,5 @@ tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0"
clap = { version = "4.0", features = ["derive"] }
jsonwebtoken = "9.0"
base64 = "0.22"

View File

@@ -1,6 +1,6 @@
use axum::{
extract::{Path, Request, State},
http::{header, StatusCode},
http::{header, HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response, Sse},
routing::{get, post},
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::Duration,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, StreamExt};
@@ -18,6 +18,8 @@ use tower_http::cors::CorsLayer;
use tracing::{info, warn};
use uuid::Uuid;
use clap::Parser;
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
use base64::{Engine as _, engine::general_purpose};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
@@ -39,6 +41,7 @@ struct EmailVerificationRequest {
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RegistrationRequest {
email: String,
name: String,
public_key: String,
}
@@ -49,6 +52,53 @@ struct RegistrationResponse {
user_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LoginRequest {
grant_type: String,
client_assertion_type: String,
client_assertion: String,
public_key: String,
challenge: String,
scope: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TokenResponse {
access_token: String,
token_type: String,
expires_in: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Claims {
sub: String, // Subject - should be the public key (user identifier)
iss: String, // Issuer
aud: String, // Audience
exp: usize, // Expiration time
iat: usize, // Issued at time
scope: String, // Scopes
#[serde(skip_serializing_if = "Option::is_none")]
challenge: Option<String>, // Challenge for authentication
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserInfo {
sub: String,
email: String,
name: String,
public_key: String,
created_at: String,
}
#[derive(Debug, Clone)]
struct User {
id: String,
email: String,
public_key: String,
name: String,
created_at: String,
}
#[derive(Debug, Clone)]
struct VerificationStatus {
email: String,
@@ -57,13 +107,16 @@ struct VerificationStatus {
}
type VerificationStore = Arc<Mutex<HashMap<String, VerificationStatus>>>;
type UserStore = Arc<Mutex<HashMap<String, User>>>;
type NotificationSender = broadcast::Sender<String>;
#[derive(Clone)]
struct AppState {
verifications: VerificationStore,
users: UserStore,
notification_tx: NotificationSender,
base_url: String,
jwt_secret: String,
}
#[tokio::main]
@@ -73,12 +126,16 @@ async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let verifications: VerificationStore = Arc::new(Mutex::new(HashMap::new()));
let users: UserStore = Arc::new(Mutex::new(HashMap::new()));
let (notification_tx, _) = broadcast::channel(100);
let jwt_secret = "your-secret-key".to_string(); // In production, use a proper secret
let state = AppState {
verifications,
users,
notification_tx,
base_url: args.base_url.clone(),
jwt_secret,
};
let app = Router::new()
@@ -86,6 +143,8 @@ async fn main() -> anyhow::Result<()> {
.route("/api/verification-status/:email", get(verification_status_sse))
.route("/api/verify/:token", get(verify_email))
.route("/api/register", post(register_user))
.route("/oauth/token", post(oauth_token))
.route("/oauth/userinfo", get(oauth_userinfo))
.route("/health", get(health_check))
.layer(axum::middleware::from_fn(log_requests))
.layer(
@@ -291,16 +350,20 @@ async fn register_user(
State(state): State<AppState>,
Json(request): Json<RegistrationRequest>,
) -> impl IntoResponse {
info!("👤 Registration request for email: {}, public_key: {}", request.email, request.public_key);
// Check if email is verified
let is_verified = {
let verifications = state.verifications.lock().unwrap();
verifications
.get(&request.email)
let verification_status = verifications.get(&request.email);
info!("📧 Email verification status for {}: {:?}", request.email, verification_status);
verification_status
.map(|status| status.verified)
.unwrap_or(false)
};
if !is_verified {
warn!("❌ Registration failed: Email {} not verified", request.email);
return Json(RegistrationResponse {
success: false,
message: "Email not verified".to_string(),
@@ -308,12 +371,32 @@ async fn register_user(
});
}
// Generate user ID
// Generate user ID and create user
let user_id = Uuid::new_v4().to_string();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string();
let user = User {
id: user_id.clone(),
email: request.email.clone(),
public_key: request.public_key.clone(),
name: request.name.clone(),
created_at: now,
};
// Store user in memory (in production, use a database)
{
let mut users = state.users.lock().unwrap();
users.insert(request.public_key.clone(), user.clone());
info!("💾 User stored with public key: {}", request.public_key);
info!("📊 Total users in store: {}", users.len());
}
// In a real implementation, store user data in database
info!(
"User registered successfully - Email: {}, Public Key: {}, User ID: {}",
"User registered successfully - Email: {}, Public Key: {}, User ID: {}",
request.email, request.public_key, user_id
);
@@ -323,3 +406,145 @@ async fn register_user(
user_id: Some(user_id),
})
}
async fn oauth_token(
State(state): State<AppState>,
Json(request): Json<LoginRequest>,
) -> impl IntoResponse {
info!("🔐 OAuth token request for public key: {}", request.public_key);
info!("📝 Request details: grant_type={}, scope={}", request.grant_type, request.scope);
// Find user by public key
let user = {
let users = state.users.lock().unwrap();
info!("🔍 Looking for user with public key: {}", request.public_key);
info!("📊 Total users in store: {}", users.len());
info!("🔑 Available public keys: {:?}", users.keys().collect::<Vec<_>>());
users.get(&request.public_key).cloned()
};
let user = match user {
Some(user) => user,
None => {
warn!("User not found for public key: {}", request.public_key);
let error_response = serde_json::json!({
"error": "invalid_client",
"error_description": "User not found"
});
info!("❌ Error response: {}", error_response);
return (StatusCode::UNAUTHORIZED, Json(error_response)).into_response();
}
};
// In a real implementation, verify the signature against the challenge
// For now, we'll accept any signature for development
info!("✅ Authentication successful for user: {}", user.email);
// Generate JWT token
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as usize;
let exp = now + 3600; // 1 hour expiration
let claims = Claims {
sub: user.public_key.clone(), // Subject should be the public key (user identifier)
iss: "self-sovereign-identity".to_string(),
aud: "identity-server".to_string(),
exp,
iat: now,
scope: request.scope.clone(),
challenge: None, // No challenge needed in response token
};
let token = match encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(state.jwt_secret.as_ref()),
) {
Ok(token) => token,
Err(e) => {
warn!("Failed to generate JWT token: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
"error": "server_error",
"error_description": "Failed to generate token"
}))).into_response();
}
};
Json(TokenResponse {
access_token: token,
token_type: "Bearer".to_string(),
expires_in: 3600,
}).into_response()
}
async fn oauth_userinfo(
State(state): State<AppState>,
headers: HeaderMap,
) -> impl IntoResponse {
// Extract Bearer token from Authorization header
let auth_header = match headers.get("Authorization") {
Some(header) => header.to_str().unwrap_or(""),
None => {
return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
"error": "invalid_token",
"error_description": "Missing Authorization header"
}))).into_response();
}
};
let token = match auth_header.strip_prefix("Bearer ") {
Some(token) => token,
None => {
return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
"error": "invalid_token",
"error_description": "Invalid Authorization header format"
}))).into_response();
}
};
// Decode and validate JWT token
let mut validation = Validation::default();
validation.set_audience(&["identity-server"]);
validation.set_issuer(&["self-sovereign-identity"]);
let claims = match decode::<Claims>(
token,
&DecodingKey::from_secret(state.jwt_secret.as_ref()),
&validation,
) {
Ok(token_data) => token_data.claims,
Err(e) => {
warn!("Invalid JWT token: {}", e);
return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
"error": "invalid_token",
"error_description": "Invalid or expired token"
}))).into_response();
}
};
// Find user by public key from JWT subject
let user = {
let users = state.users.lock().unwrap();
users.get(&claims.sub).cloned()
};
let user = match user {
Some(user) => user,
None => {
warn!("User not found for public key in token: {}", claims.sub);
return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
"error": "invalid_token",
"error_description": "User not found"
}))).into_response();
}
};
info!("📋 Returning user info for: {}", user.email);
Json(UserInfo {
sub: user.id,
email: user.email,
name: user.name,
public_key: user.public_key,
created_at: user.created_at,
}).into_response()
}