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
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 2ea11f2..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,8 +188,9 @@ async fn oidc_login(State(state): State>) -> Result>,
+ headers: HeaderMap,
Query(params): Query,
-) -> Result, StatusCode> {
+) -> Result {
tracing::info!("OIDC callback called with params: code={:?}, state={:?}, error={:?}",
params.code, params.state, params.error);
@@ -324,10 +325,29 @@ 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
+ // 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))
}
// Helper function to create a new OIDC user
diff --git a/tests/integration_oidc_tests.rs b/tests/integration_oidc_tests.rs
index e26d323..f835d74 100644
--- a/tests/integration_oidc_tests.rs
+++ b/tests/integration_oidc_tests.rs
@@ -338,35 +338,49 @@ mod tests {
.unwrap();
let status = response.status();
- let body = axum::body::to_bytes(response.into_body(), usize::MAX)
- .await
- .unwrap();
-
- if status != StatusCode::OK {
+
+ // 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
+ .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 = 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]