feat(ui): added daisy UI for beautification (#3)
Some checks failed
CI / Clippy (push) Successful in 2m33s
CI / Format (push) Successful in 7m0s
CI / Security Audit (push) Successful in 1m46s
CI / Tests (push) Successful in 3m1s
CI / Build & Push Image (push) Successful in 3m6s
CI / Changelog (push) Failing after 1m44s

Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com>
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-02-18 14:43:11 +00:00
parent 6d3e99220c
commit 0673f7867c
14 changed files with 1617 additions and 86 deletions

View File

@@ -39,8 +39,8 @@ pub fn App() -> Element {
crossorigin: "anonymous",
}
document::Link { rel: "stylesheet", href: GOOGLE_FONTS }
document::Link { rel: "stylesheet", href: MAIN_CSS }
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
Router::<Route> {}
document::Link { rel: "stylesheet", href: MAIN_CSS }
div { "data-theme": "certifai-dark", Router::<Route> {} }
}
}

View File

@@ -16,28 +16,37 @@ use crate::infrastructure::{state::User, Error, UserStateInner};
pub const LOGGED_IN_USER_SESS_KEY: &str = "logged-in-user";
/// In-memory store for pending OAuth states and their associated redirect
/// URLs. Keyed by the random state string. This avoids dependence on the
/// session cookie surviving the Keycloak redirect round-trip (the `dx serve`
/// proxy can drop `Set-Cookie` headers on 307 responses).
/// Data stored alongside each pending OAuth state. Holds the optional
/// post-login redirect URL and the PKCE code verifier needed for the
/// token exchange.
#[derive(Debug, Clone)]
struct PendingOAuthEntry {
redirect_url: Option<String>,
code_verifier: String,
}
/// In-memory store for pending OAuth states. Keyed by the random state
/// string. This avoids dependence on the session cookie surviving the
/// Keycloak redirect round-trip (the `dx serve` proxy can drop
/// `Set-Cookie` headers on 307 responses).
#[derive(Debug, Clone, Default)]
pub struct PendingOAuthStore(Arc<RwLock<HashMap<String, Option<String>>>>);
pub struct PendingOAuthStore(Arc<RwLock<HashMap<String, PendingOAuthEntry>>>);
impl PendingOAuthStore {
/// Insert a pending state with an optional post-login redirect URL.
fn insert(&self, state: String, redirect_url: Option<String>) {
/// Insert a pending state with an optional redirect URL and PKCE verifier.
fn insert(&self, state: String, entry: PendingOAuthEntry) {
// RwLock::write only panics if the lock is poisoned, which
// indicates a prior panic -- propagating is acceptable here.
#[allow(clippy::expect_used)]
self.0
.write()
.expect("pending oauth store lock poisoned")
.insert(state, redirect_url);
.insert(state, entry);
}
/// Remove and return the redirect URL if the state was pending.
/// Remove and return the entry if the state was pending.
/// Returns `None` if the state was never stored (CSRF failure).
fn take(&self, state: &str) -> Option<Option<String>> {
fn take(&self, state: &str) -> Option<PendingOAuthEntry> {
#[allow(clippy::expect_used)]
self.0
.write()
@@ -122,6 +131,28 @@ fn generate_state() -> String {
})
}
/// Generate a PKCE code verifier (43-128 char URL-safe random string).
///
/// Uses 32 random bytes encoded as base64url (no padding) to produce
/// a 43-character verifier per RFC 7636.
fn generate_code_verifier() -> String {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
let bytes: [u8; 32] = rand::rng().random();
URL_SAFE_NO_PAD.encode(bytes)
}
/// Derive the S256 code challenge from a code verifier per RFC 7636.
///
/// `code_challenge = BASE64URL(SHA256(code_verifier))`
fn derive_code_challenge(verifier: &str) -> String {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use sha2::{Digest, Sha256};
let digest = Sha256::digest(verifier.as_bytes());
URL_SAFE_NO_PAD.encode(digest)
}
/// Redirect the user to Keycloak's authorization page.
///
/// Generates a random CSRF state, stores it (along with the optional
@@ -142,9 +173,17 @@ pub async fn auth_login(
) -> Result<impl IntoResponse, Error> {
let config = OAuthConfig::from_env()?;
let state = generate_state();
let code_verifier = generate_code_verifier();
let code_challenge = derive_code_challenge(&code_verifier);
let redirect_url = params.get("redirect_url").cloned();
pending.insert(state.clone(), redirect_url);
pending.insert(
state.clone(),
PendingOAuthEntry {
redirect_url,
code_verifier,
},
);
let mut url = Url::parse(&config.auth_endpoint())
.map_err(|e| Error::StateError(format!("invalid auth endpoint URL: {e}")))?;
@@ -154,7 +193,9 @@ pub async fn auth_login(
.append_pair("redirect_uri", &config.redirect_uri)
.append_pair("response_type", "code")
.append_pair("scope", "openid profile email")
.append_pair("state", &state);
.append_pair("state", &state)
.append_pair("code_challenge", &code_challenge)
.append_pair("code_challenge_method", "S256");
Ok(Redirect::temporary(url.as_str()))
}
@@ -203,11 +244,11 @@ pub async fn auth_callback(
.get("state")
.ok_or_else(|| Error::StateError("missing state parameter".into()))?;
let redirect_url = pending
let entry = pending
.take(returned_state)
.ok_or_else(|| Error::StateError("unknown or expired oauth state".into()))?;
// --- Exchange code for tokens ---
// --- Exchange code for tokens (with PKCE code_verifier) ---
let code = params
.get("code")
.ok_or_else(|| Error::StateError("missing code parameter".into()))?;
@@ -220,6 +261,7 @@ pub async fn auth_callback(
("client_id", &config.client_id),
("redirect_uri", &config.redirect_uri),
("code", code),
("code_verifier", &entry.code_verifier),
])
.send()
.await
@@ -259,7 +301,8 @@ pub async fn auth_callback(
set_login_session(session, user_state).await?;
let target = redirect_url
let target = entry
.redirect_url
.filter(|u| !u.is_empty())
.unwrap_or_else(|| "/".into());