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:
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user