From a23edca9380fc3f464c1358dced42a561173c267 Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Sun, 12 Oct 2025 01:11:47 +0000 Subject: [PATCH 1/5] fix(OIDC): redirect to frontend after OIDC credentials --- src/routes/auth.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 2ea11f2..e7e9351 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -189,7 +189,7 @@ async fn oidc_login(State(state): State>) -> Result>, Query(params): Query, -) -> Result, StatusCode> { +) -> Result { tracing::info!("OIDC callback called with params: code={:?}, state={:?}, error={:?}", params.code, params.state, params.error); @@ -324,10 +324,12 @@ async fn oidc_callback( StatusCode::INTERNAL_SERVER_ERROR })?; - Ok(Json(LoginResponse { - token, - user: user.into(), - })) + // Redirect to frontend with token in URL fragment + // The frontend should extract the token and store it + let redirect_url = format!("/#/auth/callback?token={}", urlencoding::encode(&token)); + tracing::info!("OIDC authentication successful for user: {}, redirecting to: {}", user.username, redirect_url); + + Ok(Redirect::to(&redirect_url)) } // Helper function to create a new OIDC user From 943a3eefaea6a5c873cec75bf9cf047bdfb03a63 Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Sun, 12 Oct 2025 02:15:47 +0000 Subject: [PATCH 2/5] fix(OIDC): redirect to frontend, jwt, and callback handling --- frontend/src/App.tsx | 2 +- frontend/src/components/Auth/OidcCallback.tsx | 35 +++++++------------ src/routes/auth.rs | 24 +++++++++++-- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 30b0371..3d2a550 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -58,7 +58,7 @@ function App(): React.ReactElement { : } /> - } /> + } /> { useEffect(() => { const handleCallback = async () => { try { - const code = searchParams.get('code'); + const token = searchParams.get('token'); const error = searchParams.get('error'); - const state = searchParams.get('state'); if (error) { setError(`Authentication failed: ${error}`); @@ -24,31 +23,23 @@ const OidcCallback: React.FC = () => { return; } - if (!code) { - setError('No authorization code received'); + if (!token) { + setError('No authentication token received from server'); setProcessing(false); return; } - // Call the backend OIDC callback endpoint - const response = await api.get(`/auth/oidc/callback?code=${code}&state=${state || ''}`); - - if (response.data && response.data.token) { - // Store the token and user data - localStorage.setItem('token', response.data.token); - api.defaults.headers.common['Authorization'] = `Bearer ${response.data.token}`; - - // Redirect to dashboard - the auth context will pick up the token on next page load - window.location.href = '/dashboard'; - } else { - setError('Invalid response from authentication server'); - setProcessing(false); - } + // Store the token and set up API authorization + localStorage.setItem('token', token); + api.defaults.headers.common['Authorization'] = `Bearer ${token}`; + + // Redirect to dashboard - the page will reload and AuthContext will pick up the token + window.location.href = '/dashboard'; } catch (err: any) { console.error('OIDC callback error:', err); - + const errorInfo = ErrorHelper.formatErrorForDisplay(err, true); - + // Handle specific OIDC callback errors if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_OIDC_AUTH_FAILED)) { setError('OIDC authentication failed. Please try logging in again or contact your administrator.'); @@ -58,7 +49,7 @@ const OidcCallback: React.FC = () => { setError('Authentication failed. Your OIDC credentials may be invalid or expired.'); } else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_ACCOUNT_DISABLED)) { setError('Your account has been disabled. Please contact an administrator for assistance.'); - } else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_SESSION_EXPIRED) || + } else if (ErrorHelper.isErrorCode(err, ErrorCodes.USER_SESSION_EXPIRED) || ErrorHelper.isErrorCode(err, ErrorCodes.USER_TOKEN_EXPIRED)) { setError('Authentication session expired. Please try logging in again.'); } else if (errorInfo.category === 'network') { @@ -68,7 +59,7 @@ const OidcCallback: React.FC = () => { } else { setError(errorInfo.message || 'Failed to complete authentication. Please try again.'); } - + setProcessing(false); } }; diff --git a/src/routes/auth.rs b/src/routes/auth.rs index e7e9351..791db32 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,6 +1,6 @@ use axum::{ extract::{Query, State}, - http::StatusCode, + http::{StatusCode, HeaderMap}, response::{IntoResponse, Json, Response, Redirect}, routing::{get, post}, Router, @@ -188,6 +188,7 @@ async fn oidc_login(State(state): State>) -> Result>, + headers: HeaderMap, Query(params): Query, ) -> Result { tracing::info!("OIDC callback called with params: code={:?}, state={:?}, error={:?}", @@ -326,8 +327,25 @@ async fn oidc_callback( // Redirect to frontend with token in URL fragment // The frontend should extract the token and store it - let redirect_url = format!("/#/auth/callback?token={}", urlencoding::encode(&token)); - tracing::info!("OIDC authentication successful for user: {}, redirecting to: {}", user.username, redirect_url); + // Use absolute URL to ensure hash fragment is handled correctly by the browser + let host = headers + .get("host") + .and_then(|h| h.to_str().ok()) + .unwrap_or("localhost:8000"); + + // Check if behind a proxy (X-Forwarded-Proto header) + let protocol = headers + .get("x-forwarded-proto") + .and_then(|h| h.to_str().ok()) + .unwrap_or("https"); + + let redirect_url = format!( + "{}://{}/auth/callback?token={}", + protocol, + host, + urlencoding::encode(&token) + ); + tracing::info!("OIDC authentication successful for user: {}, redirecting to callback", user.username); Ok(Redirect::to(&redirect_url)) } From a0101bbc7bf3b822d5f5ed783e225fcd75458370 Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Sun, 12 Oct 2025 02:40:06 +0000 Subject: [PATCH 3/5] chore(docs): bring docs up to speed with changes --- docs/oidc-setup.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/oidc-setup.md b/docs/oidc-setup.md index 12e862b..4d8b5d3 100644 --- a/docs/oidc-setup.md +++ b/docs/oidc-setup.md @@ -56,7 +56,7 @@ Configure OIDC by setting these environment variables: | `OIDC_CLIENT_SECRET` | ✅ | OAuth2 client secret from your provider | `very-secret-key` | | `OIDC_ISSUER_URL` | ✅ | OIDC provider's issuer URL | `https://accounts.google.com` | | `OIDC_REDIRECT_URI` | ✅ | Callback URL for your Readur instance | `https://readur.company.com/api/auth/oidc/callback` | -| `OIDC_AUTO_REGISTER` | ❌ | Allow new users to self-register (default: `true`) | `true` or `false` | +| `OIDC_AUTO_REGISTER` | ❌ | Allow new users to self-register (default: `false`) | `true` or `false` | | `ALLOW_LOCAL_AUTH` | ❌ | Allow username/password authentication (default: `true`) | `true` or `false` | ### Example Configurations @@ -476,19 +476,19 @@ Readur intelligently handles existing local users when they first log in via OID The `OIDC_AUTO_REGISTER` setting controls whether new users can self-register: -**When `OIDC_AUTO_REGISTER=true` (default)**: +**When `OIDC_AUTO_REGISTER=true`**: - New OIDC users are automatically created when they first log in - Perfect for open environments where any company employee should get access - Username is derived from OIDC claims (preferred_username or email) - Users get the default "user" role -**When `OIDC_AUTO_REGISTER=false`**: +**When `OIDC_AUTO_REGISTER=false` (default)**: - Only existing users (pre-created by admin or linked by email) can log in - OIDC login attempts by unregistered users are rejected with HTTP 403 - Ideal for production environments requiring controlled access - Admin must pre-create users before they can use OIDC -**Migration Strategy**: Set to `false` initially, have existing users log in to link accounts, then enable for new users. +**Migration Strategy**: The default (`false`) is ideal for production. Have existing users log in to link accounts by email, then optionally enable `true` for new user auto-registration. ### Disabling Local Authentication From 67fd253bea780370b86b04a5365a7a0e87e3eaaa Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Sun, 12 Oct 2025 03:20:03 +0000 Subject: [PATCH 4/5] fix(tests): expected 303 response --- tests/integration_oidc_tests.rs | 41 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/tests/integration_oidc_tests.rs b/tests/integration_oidc_tests.rs index e26d323..7dc357a 100644 --- a/tests/integration_oidc_tests.rs +++ b/tests/integration_oidc_tests.rs @@ -338,35 +338,46 @@ mod tests { .unwrap(); let status = response.status(); - let body = axum::body::to_bytes(response.into_body(), usize::MAX) - .await - .unwrap(); - - if status != StatusCode::OK { + + if status != StatusCode::SEE_OTHER { + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); let error_text = String::from_utf8_lossy(&body); eprintln!("Response status: {}", status); eprintln!("Response body: {}", error_text); - + // Also check if we made the expected API calls to the mock server eprintln!("Mock server received calls:"); let received_requests = mock_server.received_requests().await.unwrap(); for req in received_requests { eprintln!(" {} {} - {}", req.method, req.url.path(), String::from_utf8_lossy(&req.body)); } - + // Try to parse as JSON to see if there's a more detailed error message if let Ok(error_json) = serde_json::from_slice::(&body) { eprintln!("Error JSON: {:#}", error_json); } } - - assert_eq!(status, StatusCode::OK); - - let login_response: serde_json::Value = serde_json::from_slice(&body).unwrap(); - - assert!(login_response["token"].is_string()); - assert_eq!(login_response["user"]["username"], test_username); - assert_eq!(login_response["user"]["email"], test_email); + + // Expect a redirect (303 See Other) instead of JSON response + assert_eq!(status, StatusCode::SEE_OTHER); + + // Extract the token from the Location header + let location = response.headers().get("location").unwrap().to_str().unwrap(); + assert!(location.contains("/auth/callback?token=")); + + // Extract token from URL + let token_start = location.find("token=").unwrap() + 6; + let token = urlencoding::decode(&location[token_start..]).unwrap(); + + // Verify token is not empty + assert!(!token.is_empty()); + + // Verify user was created by checking database + let user = db.get_user_by_username(&test_username).await.unwrap().unwrap(); + assert_eq!(user.username, test_username); + assert_eq!(user.email, test_email); } #[tokio::test] From c39a4e2f615c8de88f1fcc7147f527dca6bb4bab Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Sun, 12 Oct 2025 03:29:10 +0000 Subject: [PATCH 5/5] fix(tests): race condition --- tests/integration_oidc_tests.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration_oidc_tests.rs b/tests/integration_oidc_tests.rs index 7dc357a..f835d74 100644 --- a/tests/integration_oidc_tests.rs +++ b/tests/integration_oidc_tests.rs @@ -339,6 +339,9 @@ mod tests { let status = response.status(); + // Extract headers before consuming response + let headers = response.headers().clone(); + if status != StatusCode::SEE_OTHER { let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await @@ -364,7 +367,7 @@ mod tests { assert_eq!(status, StatusCode::SEE_OTHER); // Extract the token from the Location header - let location = response.headers().get("location").unwrap().to_str().unwrap(); + let location = headers.get("location").unwrap().to_str().unwrap(); assert!(location.contains("/auth/callback?token=")); // Extract token from URL