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