use yew::prelude::*; use web_sys::HtmlInputElement; use wasm_bindgen::JsCast; use serde_json; use wasm_bindgen_futures::spawn_local; use crate::vault::{Vault, VaultError}; use crate::crypto::KeyPair; use base64::{Engine as _, engine::general_purpose}; #[derive(Clone, PartialEq, Properties)] pub struct LoginConfig { pub server_url: String, } #[derive(Clone, PartialEq, Properties)] pub struct LoginProps { pub config: LoginConfig, pub on_login_success: Callback, // Callback with JWT token } #[derive(Clone, PartialEq)] pub enum LoginStatus { NotStarted, Processing, Success, Failed(String), } pub struct Login { password: String, private_key: String, status: LoginStatus, show_password: bool, show_private_key: bool, has_stored_key: bool, } #[derive(Clone)] pub enum LoginMsg { UpdatePassword(String), UpdatePrivateKey(String), TogglePasswordVisibility, TogglePrivateKeyVisibility, SubmitLogin, LoginSuccess(String), // JWT token LoginFailed(String), } impl Component for Login { type Message = LoginMsg; type Properties = LoginProps; fn create(_ctx: &Context) -> Self { let has_stored_key = Vault::has_stored_key(); Self { password: String::new(), private_key: String::new(), status: LoginStatus::NotStarted, show_password: false, show_private_key: false, has_stored_key, } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { LoginMsg::UpdatePassword(password) => { self.password = password; true } LoginMsg::UpdatePrivateKey(private_key) => { self.private_key = private_key; true } LoginMsg::TogglePasswordVisibility => { self.show_password = !self.show_password; true } LoginMsg::TogglePrivateKeyVisibility => { self.show_private_key = !self.show_private_key; true } LoginMsg::SubmitLogin => { if self.validate_form() { self.submit_login(ctx); } true } LoginMsg::LoginSuccess(jwt_token) => { self.status = LoginStatus::Success; // Store JWT token in localStorage for future use if let Some(window) = web_sys::window() { if let Ok(Some(storage)) = window.local_storage() { let _ = storage.set_item("jwt_token", &jwt_token); web_sys::console::log_1(&"JWT token stored in localStorage".into()); } } ctx.props().on_login_success.emit(jwt_token); true } LoginMsg::LoginFailed(error) => { self.status = LoginStatus::Failed(error); true } } } fn view(&self, ctx: &Context) -> Html { let link = ctx.link(); html! {

{"Sign In"}

{self.render_status_notification()}
{if !self.has_stored_key { html! {
{"Your private key will be encrypted and stored securely in your browser after login."}
} } else { html! {} }}

{"Forgot your password? "} {"Reset it here"}

} } } impl Login { fn validate_form(&self) -> bool { let password_valid = !self.password.trim().is_empty(); if self.has_stored_key { password_valid } else { password_valid && !self.private_key.trim().is_empty() } } fn submit_login(&mut self, ctx: &Context) { self.status = LoginStatus::Processing; let password = self.password.clone(); let private_key = self.private_key.clone(); let has_stored_key = self.has_stored_key; let server_url = ctx.props().config.server_url.clone(); let link = ctx.link().clone(); spawn_local(async move { let (final_private_key, public_key) = if has_stored_key { // Decrypt stored keypair match Vault::retrieve_keypair(&password) { Ok((stored_private_key, stored_public_key)) => { web_sys::console::log_1(&"Successfully retrieved keypair from vault".into()); (stored_private_key, stored_public_key) } Err(e) => { link.send_message(LoginMsg::LoginFailed(format!("Failed to decrypt stored key: {}", e))); return; } } } else { // Derive public key from private key and store both encrypted let keypair = match KeyPair::from_private_key(&private_key) { Ok(keypair) => keypair, Err(e) => { link.send_message(LoginMsg::LoginFailed(format!("Invalid private key: {}", e))); return; } }; match Vault::store_keypair(&private_key, &keypair.public_key, &password) { Ok(_) => { web_sys::console::log_1(&"Keypair stored securely in vault".into()); } Err(e) => { web_sys::console::log_1(&format!("Warning: Failed to store keypair in vault: {}", e).into()); } } (private_key, keypair.public_key) }; // Authenticate with identity server using public key match Self::authenticate_with_server(&server_url, &public_key, &final_private_key).await { Ok(jwt_token) => { web_sys::console::log_1(&format!("Authentication successful, received JWT token").into()); link.send_message(LoginMsg::LoginSuccess(jwt_token)); } Err(e) => { link.send_message(LoginMsg::LoginFailed(format!("Authentication failed: {}", e))); } } }); } async fn authenticate_with_server(server_url: &str, public_key: &str, private_key: &str) -> Result { use web_sys::{Request, RequestInit, RequestMode, Response}; use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; // Create authentication challenge let challenge = format!("auth_challenge_{}", js_sys::Date::now()); // Sign the challenge with private key let signature = Self::create_auth_signature(&challenge, private_key, public_key)?; // Prepare authentication request let auth_request = serde_json::json!({ "grant_type": "client_credentials", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": signature, "public_key": public_key, "challenge": challenge, "scope": "openid profile email" }); let mut opts = RequestInit::new(); opts.method("POST"); opts.mode(RequestMode::Cors); let headers = js_sys::Object::new(); js_sys::Reflect::set(&headers, &"Content-Type".into(), &"application/json".into()).unwrap(); opts.headers(&headers); opts.body(Some(&JsValue::from_str(&auth_request.to_string()))); let url = format!("{}/oauth/token", server_url); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|_| "Failed to create request")?; let window = web_sys::window().ok_or("No window object")?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|_| "Network request failed")?; let resp: Response = resp_value.dyn_into() .map_err(|_| "Failed to cast response")?; if !resp.ok() { // Get error response body for debugging let error_json = JsFuture::from(resp.json().map_err(|_| "Failed to get error response JSON")?) .await .map_err(|_| "Failed to parse error JSON response")?; let error_text = js_sys::JSON::stringify(&error_json) .map_err(|_| "Failed to stringify error response")? .as_string() .ok_or("Failed to convert error response to string")?; return Err(format!("Authentication failed with status: {}\n\nrequest response: {}", resp.status(), error_text)); } let json = JsFuture::from(resp.json().map_err(|_| "Failed to get response JSON")?) .await .map_err(|_| "Failed to parse JSON response")?; let response_text = js_sys::JSON::stringify(&json) .map_err(|_| "Failed to stringify response")? .as_string() .ok_or("Failed to convert response to string")?; let response: serde_json::Value = serde_json::from_str(&response_text) .map_err(|_| "Failed to parse response JSON")?; response["access_token"] .as_str() .map(|s| s.to_string()) .ok_or("No access token in response".to_string()) } fn create_auth_signature(challenge: &str, private_key: &str, public_key: &str) -> Result { use sha2::{Sha256, Digest}; // Create JWT header let header = serde_json::json!({ "alg": "HS256", "typ": "JWT" }); // Create JWT payload with challenge - subject should be public key let payload = serde_json::json!({ "iss": "self-sovereign-identity", "sub": public_key, // Subject is the public key (user identifier) "aud": "identity-server", "exp": (js_sys::Date::now() / 1000.0) as u64 + 3600, // 1 hour expiry "iat": (js_sys::Date::now() / 1000.0) as u64, "scope": "openid profile email", "challenge": challenge }); let header_b64 = general_purpose::STANDARD.encode(header.to_string()); let payload_b64 = general_purpose::STANDARD.encode(payload.to_string()); let unsigned_token = format!("{}.{}", header_b64, payload_b64); // Sign with private key (simplified HMAC) let mut hasher = Sha256::new(); hasher.update(unsigned_token.as_bytes()); hasher.update(private_key.as_bytes()); let signature = hasher.finalize(); let signature_b64 = general_purpose::STANDARD.encode(signature); Ok(format!("{}.{}", unsigned_token, signature_b64)) } fn render_status_notification(&self) -> Html { match &self.status { LoginStatus::NotStarted => html! {}, LoginStatus::Processing => html! {
{"Signing you in..."}
}, LoginStatus::Success => html! {
{"Successfully signed in! Redirecting..."}
}, LoginStatus::Failed(error) => html! {
{error}
}, } } }