Skip to content

Conversation

@parmesant
Copy link
Contributor

@parmesant parmesant commented Dec 5, 2025

This PR changes session management by storing the refresh token returned by the provider in memory. This is done by asking for the offline_access scope

It is used to renew the session once it expires.

Expiry is set based on the expires_in value returned by the provider

Fixes #1469

Description


This PR has:

  • been tested to ensure log ingestion and log query works.
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added documentation for new or modified features or behaviors.

Summary by CodeRabbit

  • New Features

    • Sessions can now automatically refresh when expired via OIDC token refresh, reducing forced re-authentication.
  • Enhancements

    • CLI default OIDC scope now includes offline_access.
    • Session expiry is now derived from token lifetimes with a configurable expiry duration for OAuth users.
    • OAuth login flow now preserves bearer/token info with sessions and user records.
  • Bug Fixes

    • Improved session validation and error handling for missing/expired sessions.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 5, 2025

Walkthrough

Implements session lifecycle and OIDC refresh: middleware now detects expired sessions and attempts token refresh; Bearer and expiry are propagated through OIDC/user/session flows; route APIs stop accepting an OpenIdClient parameter; RBAC expiry semantics and session helpers updated; CLI default OIDC scope includes offline_access.

Changes

Cohort / File(s) Summary
CLI
src/cli.rs
Default OIDC scope changed to include offline_access; help text updated.
HTTP middleware & OIDC flows
src/handlers/http/middleware.rs, src/handlers/http/oidc.rs
Middleware checks session expiry and attempts token refresh via DiscoveredClient; on success updates Bearer, user roles, and session expiry. Multiple OIDC functions changed to propagate Bearer and expires_in (signatures updated), and login redirect includes offline_access and consent.
Route configuration (remove OIDC param)
src/handlers/http/modal/mod.rs, src/handlers/http/modal/server.rs, src/handlers/http/modal/query_server.rs, src/handlers/http/modal/ingest_server.rs
Removed oidc_client: Option<OpenIdClient> from configure_routes() trait and implementations; get_oauth_webscope() made parameterless and call sites adjusted; oidc client injected via app_data instead of route parameter.
RBAC: sessions, expiry, roles, user
src/rbac/mod.rs, src/rbac/map.rs, src/rbac/user.rs
Added EXPIRY_DURATION const; new_session() now accepts expires_in: TimeDelta; added Sessions::is_session_expired() (parallel scan); roles_to_permission() made public; OAuth now stores optional Bearer boxed, new_oauth() and User::userid() updated.
Utilities
src/utils/mod.rs
get_user_from_request() maps session-key errors to RBACError::UserDoesNotExist instead of unwrapping.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Middleware
    participant Sessions
    participant RBAC
    participant OIDC as DiscoveredClient

    Client->>Middleware: HTTP request (cookie/session key)
    Middleware->>Sessions: extract session key & check presence
    Middleware->>Sessions: is_session_expired?(key)
    alt not expired
        Middleware->>RBAC: load user & permissions
        Middleware->>Client: forward request
    else expired
        Middleware->>RBAC: get user (reads optional Bearer)
        RBAC-->>Middleware: User + optional Bearer
        alt bearer present & OIDC available
            Middleware->>OIDC: refresh token using Bearer
            OIDC-->>Middleware: new Bearer + expires_in
            Middleware->>RBAC: update_user_if_changed(..., bearer)
            Middleware->>Sessions: new_session(user, key, expires_in)
            Middleware->>Client: forward request (refreshed)
        else refresh fails or missing data
            Middleware->>Client: 401 Unauthorized
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • src/handlers/http/oidc.rs (many signature and persistence changes).
    • src/handlers/http/middleware.rs (async refresh, state mutation, expiry logic).
    • src/rbac/user.rs and src/rbac/mod.rs (ownership changes: Box, Bearer handling, expiry semantics).
    • Route trait changes across src/handlers/http/modal/* for consistent API updates.

Possibly related PRs

Suggested labels

for next release

Suggested reviewers

  • nikhilsinhaparseable
  • nitisht

Poem

🐇 I nibble at tokens, soft and bright,
I hop to refresh them through the night,
Cookies warmed, expiries trimmed fine,
Roles restored and sessions align,
Hooray — auth refreshed, all systems light!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.03% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description addresses the main objective (offline_access scope and token refresh) but the template checklist items remain unchecked and incomplete. Please check the testing checklist items to confirm log ingestion/query testing was performed, and add documentation or comments explaining the session refresh mechanism and why offline_access is required.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix: Re-authentication with OIDC doesn't work after 24 hours' directly matches the core objective: enabling session refresh via stored refresh tokens and OIDC re-authentication when sessions expire.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/handlers/http/middleware.rs (1)

212-223: Casting logic is safe but could be clearer.

The expires_in handling correctly guards against overflow by checking if the value exceeds u32::MAX before casting. However, the cast *expires_in as u32 relies on the prior check to be safe. Consider using try_into() for clarity.

                                let expires_in =
                                    if let Some(expires_in) = refreshed_token.expires_in.as_ref() {
-                                        // need an i64 somehow
-                                        if *expires_in > u32::MAX.into() {
-                                            EXPIRY_DURATION
-                                        } else {
-                                            let v = i64::from(*expires_in as u32);
-                                            Duration::seconds(v)
-                                        }
+                                        match i64::try_from(*expires_in) {
+                                            Ok(v) => Duration::seconds(v),
+                                            Err(_) => EXPIRY_DURATION,
+                                        }
                                    } else {
                                        EXPIRY_DURATION
                                    };
src/handlers/http/oidc.rs (2)

330-335: Fragile URL construction: string concatenation for query parameters.

Appending query parameters via string concatenation assumes the URL already contains a ?. While auth_url typically includes query params, using proper URL manipulation is safer.

-    let mut url: String = auth_url.into();
-    url.push_str("&access_type=offline&prompt=consent");
+    let mut url: Url = auth_url.into();
+    url.query_pairs_mut()
+        .append_pair("access_type", "offline")
+        .append_pair("prompt", "consent");
     HttpResponse::TemporaryRedirect()
-        .insert_header((header::LOCATION, url))
+        .insert_header((header::LOCATION, url.to_string()))
         .finish()

248-258: Consider extracting duplicate expires_in calculation logic.

This expires_in handling logic is duplicated in src/handlers/http/middleware.rs (lines 212-223). Consider extracting to a shared utility function.

// In a shared module, e.g., src/rbac/mod.rs
pub fn expires_in_to_duration(expires_in: Option<&u64>) -> Duration {
    match expires_in {
        Some(&v) if v <= u32::MAX as u64 => Duration::seconds(v as i64),
        _ => EXPIRY_DURATION,
    }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b08f243 and e30df5f.

📒 Files selected for processing (11)
  • src/cli.rs (1 hunks)
  • src/handlers/http/middleware.rs (2 hunks)
  • src/handlers/http/modal/ingest_server.rs (2 hunks)
  • src/handlers/http/modal/mod.rs (4 hunks)
  • src/handlers/http/modal/query_server.rs (2 hunks)
  • src/handlers/http/modal/server.rs (3 hunks)
  • src/handlers/http/oidc.rs (17 hunks)
  • src/rbac/map.rs (2 hunks)
  • src/rbac/mod.rs (4 hunks)
  • src/rbac/user.rs (6 hunks)
  • src/utils/mod.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-16T09:50:38.636Z
Learnt from: nikhilsinhaparseable
Repo: parseablehq/parseable PR: 1346
File: src/parseable/streams.rs:319-331
Timestamp: 2025-06-16T09:50:38.636Z
Learning: In Parseable's Ingest or Query mode, the node_id is always available because it's generated during server initialization itself, before the get_node_id_string() function in streams.rs would be called. This makes the .expect() calls on QUERIER_META.get() and INGESTOR_META.get() safe in this context.

Applied to files:

  • src/handlers/http/modal/ingest_server.rs
  • src/handlers/http/modal/query_server.rs
📚 Learning: 2025-10-28T02:10:41.140Z
Learnt from: nikhilsinhaparseable
Repo: parseablehq/parseable PR: 1453
File: src/parseable/mod.rs:397-400
Timestamp: 2025-10-28T02:10:41.140Z
Learning: In Parseable enterprise deployments with multiple query nodes, hot tier configuration must be persisted in object storage so that newly started query nodes can fetch and synchronize the hot tier settings at startup (file: src/parseable/mod.rs, function: create_stream_and_schema_from_storage).

Applied to files:

  • src/handlers/http/modal/query_server.rs
📚 Learning: 2025-09-05T09:37:33.939Z
Learnt from: nikhilsinhaparseable
Repo: parseablehq/parseable PR: 1422
File: src/handlers/http/cluster/mod.rs:285-296
Timestamp: 2025-09-05T09:37:33.939Z
Learning: In the Parseable codebase, userid values do not require URL encoding when used in path segments because: (1) for basic/native auth, userid is the same as username, and (2) for OAuth users, userid is the "sub" claim from JWT tokens. Both of these are inherently URL-safe identifiers.

Applied to files:

  • src/rbac/user.rs
🧬 Code graph analysis (7)
src/utils/mod.rs (1)
src/utils/actix.rs (2)
  • extract_session_key_from_req (51-71)
  • req (31-31)
src/handlers/http/modal/ingest_server.rs (3)
src/handlers/http/modal/mod.rs (1)
  • configure_routes (74-76)
src/handlers/http/modal/query_server.rs (1)
  • configure_routes (52-88)
src/handlers/http/modal/server.rs (1)
  • configure_routes (72-114)
src/handlers/http/middleware.rs (6)
src/rbac/map.rs (6)
  • mut_sessions (99-105)
  • mut_users (67-73)
  • sessions (91-97)
  • users (59-65)
  • from (315-323)
  • from (367-375)
src/rbac/mod.rs (1)
  • roles_to_permission (233-245)
src/utils/mod.rs (1)
  • get_user_from_request (61-69)
src/handlers/http/oidc.rs (3)
  • req (77-77)
  • req (148-148)
  • req (178-178)
src/utils/actix.rs (2)
  • req (31-31)
  • extract_session_key (29-49)
src/rbac/user.rs (3)
  • userid (87-92)
  • userid (253-255)
  • from (215-225)
src/rbac/mod.rs (2)
src/rbac/map.rs (2)
  • mut_sessions (99-105)
  • roles (75-81)
src/rbac/user.rs (1)
  • roles (113-115)
src/handlers/http/modal/query_server.rs (1)
src/handlers/http/modal/server.rs (2)
  • configure_routes (72-114)
  • get_oauth_webscope (572-577)
src/handlers/http/modal/mod.rs (3)
src/handlers/http/modal/ingest_server.rs (1)
  • configure_routes (65-86)
src/handlers/http/modal/query_server.rs (1)
  • configure_routes (52-88)
src/handlers/http/modal/server.rs (1)
  • configure_routes (72-114)
src/handlers/http/oidc.rs (2)
src/rbac/user.rs (6)
  • username (249-251)
  • roles (113-115)
  • new (355-357)
  • userid (87-92)
  • userid (253-255)
  • new_oauth (70-85)
src/rbac/mod.rs (1)
  • put_user (54-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: coverage
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
🔇 Additional comments (21)
src/cli.rs (1)

458-461: LGTM! offline_access scope added for refresh token support.

The addition of offline_access to the default OIDC scope is correct and aligns with the PR's objective to implement OAuth token refresh functionality. The help text has been updated consistently.

src/handlers/http/modal/ingest_server.rs (1)

65-65: LGTM! Route configuration simplified.

The removal of the oidc_client parameter from configure_routes is consistent with the trait definition change and the broader refactoring. This aligns with the comment at line 134 indicating that ingestors shouldn't handle OpenId auth flow.

src/utils/mod.rs (1)

62-62: LGTM! Error handling improved.

Replacing unwrap() with map_err(|_| RBACError::UserDoesNotExist)? is a good improvement that avoids potential panics and provides more explicit error handling.

src/handlers/http/modal/query_server.rs (2)

52-52: LGTM! Signature updated consistently.

The removal of the oidc_client parameter from configure_routes is consistent with the trait definition change and aligns with similar updates in other modal server implementations.


69-69: LGTM! OAuth scope usage updated.

The call to Server::get_oauth_webscope() without parameters is consistent with the updated method signature in server.rs.

src/handlers/http/modal/mod.rs (3)

74-76: LGTM! Trait signature simplified.

Removing the oidc_client parameter from the configure_routes trait method is a good refactoring that simplifies the interface. The OIDC client is now injected via app_data in the start method instead.


103-103: LGTM! Removed unnecessary Arc wrapping.

Changing from Some(Arc::new(client)) to Some(client) is correct because OpenIdClient is already defined as Arc<openid::Client<Discovered, Claims>> (line 64), so the extra Arc::new was creating double-wrapping.


123-125: LGTM! Route configuration refactored correctly.

The OIDC client is now passed via app_data (line 123) and configure_routes is called without parameters (line 125), which is cleaner than threading the client through the call chain.

src/rbac/mod.rs (3)

40-40: LGTM! Session expiry duration defined.

The EXPIRY_DURATION constant of 1 hour is a reasonable default for OAuth session expiry, especially when paired with refresh token functionality.


152-159: LGTM! Session expiry made configurable.

Updating new_session to accept a configurable expires_in parameter (instead of a hardcoded 7-day duration) provides better flexibility for different authentication scenarios, particularly for OAuth sessions with refresh tokens.


233-245: LGTM! API exposure expanded.

Making roles_to_permission public is appropriate as it's needed by other modules (e.g., OIDC flows) to convert roles to permissions for session management.

src/handlers/http/modal/server.rs (2)

72-114: LGTM! Route configuration simplified.

The removal of the oidc_client parameter from configure_routes is consistent with the trait definition change and simplifies the implementation.


572-577: LGTM! OAuth scope construction simplified.

Removing the oidc_client parameter from get_oauth_webscope is a good refactoring. The OIDC client is now accessed via app_data within the route handlers instead of being passed through the scope construction.

src/rbac/map.rs (1)

171-189: Verify the semantics of the two false returns for consistency.

The expiry comparison on line 187 (expiry < &Utc::now()) is correct for identifying expired sessions. However, the method returns false when a session key exists in active_sessions but the corresponding user has no sessions in user_sessions. This suggests either:

  • This is a valid edge case where inconsistent state should be treated as "not expired"
  • There's a data consistency issue that should be prevented elsewhere

Clarify whether the second early return (line 183) is intended behavior or if it indicates a bug in session lifecycle management.

src/handlers/http/middleware.rs (2)

240-248: Verify intended behavior: non-OAuth expired sessions are silently renewed.

When a non-OAuth user's session expires, this branch creates a new session without re-authentication. This effectively extends sessions indefinitely. Confirm this is the intended behavior, as it bypasses the session expiry for native users.


172-176: LGTM: Session extraction pattern follows actix middleware conventions.

The extraction of session key and user id before service.call(req) is correct. The service future is created but not polled until after validation in the async block.

src/rbac/user.rs (2)

70-85: LGTM: Bearer token integration in OAuth user creation.

The new_oauth function correctly accepts an optional Bearer and properly boxes the OAuth value before assignment to UserType. The boxing is a good optimization to reduce enum size variance.


272-289: LGTM: GroupUser construction correctly handles boxed OAuth.

The pattern matching and field access for the boxed OAuth variant is correct. The display username fallback chain (name → email → userid) is reasonable.

src/handlers/http/oidc.rs (3)

418-438: LGTM: Bearer token stored only in memory, not persisted.

The implementation correctly creates the user without the bearer for metadata persistence (lines 424, 429), then adds the bearer only to the in-memory cache (lines 432-435). This is good security practice—refresh tokens should not be persisted to storage.


460-467: LGTM: Early return optimization when no changes detected.

The comparison correctly includes bearer in the change detection, avoiding unnecessary metadata updates and session invalidations when nothing has changed.


390-406: LGTM: request_token correctly returns Bearer for downstream use.

The function signature update to return (Claims, Userinfo, Bearer) is appropriate for propagating the bearer token through the OIDC flow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/handlers/http/oidc.rs (1)

248-258: Duplicate expires_in calculation logic.

This logic is duplicated from middleware.rs. As noted in that file's review, consider extracting to a shared helper.

🧹 Nitpick comments (4)
src/handlers/http/middleware.rs (1)

220-230: Consider extracting duplicate expires_in calculation logic.

This exact logic for handling expires_in overflow (checking against u32::MAX, falling back to EXPIRY_DURATION) is duplicated in src/handlers/http/oidc.rs (lines 248-258). Consider extracting to a helper function in the RBAC module to avoid divergence.

Example helper:

// In src/rbac/mod.rs or a utils module
pub fn compute_session_expiry(expires_in: Option<u64>) -> Duration {
    if let Some(expires_in) = expires_in {
        if expires_in > u32::MAX.into() {
            EXPIRY_DURATION
        } else {
            Duration::seconds(i64::from(expires_in as u32))
        }
    } else {
        EXPIRY_DURATION
    }
}
src/handlers/http/oidc.rs (3)

330-331: Consider using proper URL query parameter handling.

String concatenation assumes auth_url already contains query parameters. While this is typically true for OAuth authorization URLs, using url::Url parsing with proper query parameter APIs would be more robust and self-documenting.

-    let mut url: String = auth_url.into();
-    url.push_str("&access_type=offline&prompt=consent");
+    let mut url = Url::parse(&auth_url.to_string())
+        .expect("auth_url should be valid URL");
+    url.query_pairs_mut()
+        .append_pair("access_type", "offline")
+        .append_pair("prompt", "consent");
+    let url = url.to_string();

418-437: Add a comment explaining why Bearer is not persisted to metadata.

The pattern of passing None to new_oauth for metadata, then setting Bearer in memory (lines 431-434) is intentional to avoid persisting sensitive tokens. A brief comment would clarify this design decision for future maintainers.

     put_metadata(&metadata).await?;

-    // modify before storing
+    // Set bearer only in memory - tokens should not be persisted to storage
     if let user::UserType::OAuth(oauth) = &mut user.ty {
         oauth.bearer = Some(bearer);
     }

493-496: Same pattern as put_user - consider adding explanatory comment.

Same suggestion as for put_user: a comment explaining that Bearer is only set in memory (not persisted) would clarify the design.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e30df5f and e319bfe.

📒 Files selected for processing (3)
  • src/handlers/http/middleware.rs (2 hunks)
  • src/handlers/http/oidc.rs (17 hunks)
  • src/rbac/user.rs (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-05T09:37:33.939Z
Learnt from: nikhilsinhaparseable
Repo: parseablehq/parseable PR: 1422
File: src/handlers/http/cluster/mod.rs:285-296
Timestamp: 2025-09-05T09:37:33.939Z
Learning: In the Parseable codebase, userid values do not require URL encoding when used in path segments because: (1) for basic/native auth, userid is the same as username, and (2) for OAuth users, userid is the "sub" claim from JWT tokens. Both of these are inherently URL-safe identifiers.

Applied to files:

  • src/rbac/user.rs
🧬 Code graph analysis (2)
src/handlers/http/middleware.rs (10)
src/event/mod.rs (1)
  • map (137-137)
src/rbac/map.rs (5)
  • mut_users (67-73)
  • sessions (91-97)
  • users (59-65)
  • from (315-323)
  • from (367-375)
src/rbac/mod.rs (1)
  • roles_to_permission (233-245)
src/utils/mod.rs (1)
  • get_user_from_request (61-69)
src/handlers/http/oidc.rs (3)
  • req (77-77)
  • req (148-148)
  • req (178-178)
src/handlers/livetail.rs (1)
  • extract_session_key (244-267)
src/rbac/user.rs (3)
  • userid (87-92)
  • userid (259-261)
  • from (221-231)
src/parseable/streams.rs (1)
  • v (603-605)
src/storage/localfs.rs (1)
  • from (744-746)
src/utils/time.rs (1)
  • from (337-341)
src/handlers/http/oidc.rs (2)
src/utils/actix.rs (2)
  • extract_session_key_from_req (51-71)
  • req (31-31)
src/rbac/user.rs (7)
  • username (255-257)
  • from (221-231)
  • roles (113-115)
  • new (361-363)
  • userid (87-92)
  • userid (259-261)
  • new_oauth (70-85)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: coverage
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
🔇 Additional comments (12)
src/handlers/http/middleware.rs (4)

27-46: LGTM: Import additions support the new session refresh functionality.

The imports are well-organized and necessary for the session lifecycle management implementation.


172-182: LGTM: Session key extraction with proper error handling.

The early validation of the session key with a descriptive error message is appropriate for the authentication flow.


197-218: Approve: TOCTOU issue addressed with single lookup pattern.

The code now uses if let Some(user) = users().get(&userid) for a single atomic lookup, addressing the previous race condition concern. The token refresh error handling appropriately returns Unauthorized.


246-261: LGTM: Session tracking with appropriate fallback handling.

The implementation correctly handles both the token refresh path (with new expiry from provider) and the fallback path for users without bearer tokens. The session is properly re-tracked with updated permissions in both cases.

src/rbac/user.rs (4)

42-42: LGTM: Boxing OAuth variant is appropriate.

The change to OAuth(Box<OAuth>) is a good optimization since OAuth now contains an Option<Bearer> which increases its size. Boxing keeps the UserType enum size reasonable.


70-85: LGTM: Bearer integration in OAuth user creation.

The signature update correctly accepts an optional Bearer, allowing for both initial user creation (without bearer) and in-memory updates (with bearer).


188-197: Documentation comment addresses the safety invariant as discussed.

The documentation clearly explains the panic condition and why it's safe in practice - the trait is only called after verifying bearer.is_some(). This addresses the previous review feedback.


278-294: LGTM: GroupUser::from_user correctly handles boxed OAuth.

The pattern matching and field access adjustments are appropriate for the boxed OAuth variant.

src/handlers/http/oidc.rs (4)

77-83: LGTM: Safe OIDC client extraction in login and logout handlers.

The pattern safely handles the case where OIDC is not configured, avoiding potential panics.

Also applies to: 148-154


308-317: LGTM: Updated signature supports configurable session expiry.

The function now accepts expires_in allowing consistent session duration handling for basic auth exchanges.


390-406: LGTM: Token extraction now includes Bearer for downstream use.

The updated return type enables Bearer propagation through the authentication flow.


459-466: Verify: Bearer comparison may trigger updates on every login.

The comparison oauth_user.bearer.as_ref() == Some(&bearer) includes access tokens which change frequently. This means the function won't early-return on subsequent logins even if only the access token changed.

This may be intentional to ensure the in-memory Bearer is always current, but verify this is the desired behavior since it bypasses the optimization for unchanged users.

This PR changes session management by storing the refresh token returned by the provider in memory.

It is used to renew the session once it expires.

Expiry is set based on the expires_in value returned by the provider
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/handlers/http/middleware.rs (1)

184-261: Refactor: Extract session refresh logic to a helper function.

The session refresh implementation within the middleware is complex (78 lines) and handles multiple concerns: expiry checking, OIDC client retrieval, token refresh, expiry calculation, user updates, and session tracking. Consider extracting this into a dedicated helper function for better maintainability and testability.

Additionally, the expires_in calculation logic (lines 220-230) is duplicated in src/handlers/http/oidc.rs (lines 248-258). Consider extracting this into a shared utility function.

Example refactor:

// In a shared utility module (e.g., src/rbac/session_helpers.rs)
pub fn calculate_expires_in(bearer: &Bearer) -> Duration {
    if let Some(expires_in) = bearer.expires_in.as_ref() {
        if *expires_in > u32::MAX.into() {
            EXPIRY_DURATION
        } else {
            Duration::seconds(i64::from(*expires_in as u32))
        }
    } else {
        EXPIRY_DURATION
    }
}

async fn refresh_expired_session(
    key: &SessionKey,
    userid: &str,
    oidc_client: &DiscoveredClient,
) -> Result<(), Error> {
    // Move lines 197-259 here
    // Return Result for cleaner error handling
}

Then in the middleware:

if sessions().is_session_expired(&key) {
    if let Some(oidc_client) = get_oidc_client(&http_req) {
        if let Ok(userid) = userid {
            refresh_expired_session(&key, &userid, &oidc_client).await?;
        }
    }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e319bfe and 82e9a19.

📒 Files selected for processing (11)
  • src/cli.rs (1 hunks)
  • src/handlers/http/middleware.rs (2 hunks)
  • src/handlers/http/modal/ingest_server.rs (2 hunks)
  • src/handlers/http/modal/mod.rs (4 hunks)
  • src/handlers/http/modal/query_server.rs (2 hunks)
  • src/handlers/http/modal/server.rs (3 hunks)
  • src/handlers/http/oidc.rs (17 hunks)
  • src/rbac/map.rs (2 hunks)
  • src/rbac/mod.rs (4 hunks)
  • src/rbac/user.rs (6 hunks)
  • src/utils/mod.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/cli.rs
  • src/handlers/http/modal/ingest_server.rs
  • src/handlers/http/modal/mod.rs
  • src/rbac/map.rs
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-05T09:37:33.939Z
Learnt from: nikhilsinhaparseable
Repo: parseablehq/parseable PR: 1422
File: src/handlers/http/cluster/mod.rs:285-296
Timestamp: 2025-09-05T09:37:33.939Z
Learning: In the Parseable codebase, userid values do not require URL encoding when used in path segments because: (1) for basic/native auth, userid is the same as username, and (2) for OAuth users, userid is the "sub" claim from JWT tokens. Both of these are inherently URL-safe identifiers.

Applied to files:

  • src/rbac/user.rs
📚 Learning: 2025-10-28T02:10:41.140Z
Learnt from: nikhilsinhaparseable
Repo: parseablehq/parseable PR: 1453
File: src/parseable/mod.rs:397-400
Timestamp: 2025-10-28T02:10:41.140Z
Learning: In Parseable enterprise deployments with multiple query nodes, hot tier configuration must be persisted in object storage so that newly started query nodes can fetch and synchronize the hot tier settings at startup (file: src/parseable/mod.rs, function: create_stream_and_schema_from_storage).

Applied to files:

  • src/handlers/http/modal/query_server.rs
📚 Learning: 2025-06-16T09:50:38.636Z
Learnt from: nikhilsinhaparseable
Repo: parseablehq/parseable PR: 1346
File: src/parseable/streams.rs:319-331
Timestamp: 2025-06-16T09:50:38.636Z
Learning: In Parseable's Ingest or Query mode, the node_id is always available because it's generated during server initialization itself, before the get_node_id_string() function in streams.rs would be called. This makes the .expect() calls on QUERIER_META.get() and INGESTOR_META.get() safe in this context.

Applied to files:

  • src/handlers/http/modal/query_server.rs
🧬 Code graph analysis (5)
src/rbac/mod.rs (2)
src/rbac/map.rs (2)
  • mut_sessions (99-105)
  • roles (75-81)
src/rbac/user.rs (1)
  • roles (113-115)
src/utils/mod.rs (2)
src/utils/actix.rs (2)
  • extract_session_key_from_req (51-71)
  • req (31-31)
src/handlers/http/oidc.rs (3)
  • req (77-77)
  • req (148-148)
  • req (178-178)
src/handlers/http/middleware.rs (3)
src/rbac/map.rs (6)
  • mut_sessions (99-105)
  • mut_users (67-73)
  • sessions (91-97)
  • users (59-65)
  • from (315-323)
  • from (367-375)
src/utils/mod.rs (1)
  • get_user_from_request (61-69)
src/handlers/http/oidc.rs (3)
  • req (77-77)
  • req (148-148)
  • req (178-178)
src/handlers/http/modal/query_server.rs (1)
src/handlers/http/modal/server.rs (2)
  • configure_routes (72-114)
  • get_oauth_webscope (572-577)
src/handlers/http/oidc.rs (2)
src/utils/actix.rs (2)
  • extract_session_key_from_req (51-71)
  • req (31-31)
src/rbac/user.rs (7)
  • username (255-257)
  • from (221-231)
  • roles (113-115)
  • new (361-363)
  • userid (87-92)
  • userid (259-261)
  • new_oauth (70-85)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: coverage
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
🔇 Additional comments (15)
src/utils/mod.rs (1)

62-62: LGTM! Improved error handling.

The change from unwrap() to map_err(|_| RBACError::UserDoesNotExist)? properly propagates errors instead of panicking, making the code more robust.

src/handlers/http/modal/query_server.rs (1)

52-69: LGTM! Consistent with OIDC handling refactor.

The removal of the oidc_client parameter from configure_routes and the updated call to get_oauth_webscope() align with the PR's architectural change to centralize OIDC client handling via app data injection rather than route-level configuration.

src/handlers/http/modal/server.rs (2)

72-93: LGTM! Simplified route configuration.

The removal of the oidc_client parameter from configure_routes and the updated OAuth webscope wiring streamline the server configuration and are consistent with the PR's centralized OIDC handling approach.


572-577: LGTM! Simplified OAuth webscope.

The get_oauth_webscope method now takes no parameters and returns a scope with login, logout, and code callback routes. This aligns with the new pattern where OIDC client data is accessed via app data instead of being passed through route configuration.

src/rbac/mod.rs (3)

40-41: LGTM! Default session expiry constant.

The EXPIRY_DURATION constant provides a sensible default of 1 hour for session expiry and supports the OAuth token refresh implementation. This is used as a fallback when OAuth providers don't return an expires_in value.


152-159: LGTM! Parameterized session expiry.

The addition of the expires_in parameter to new_session enables dynamic session expiry based on OAuth token lifetimes, which is essential for proper token refresh handling.


233-233: LGTM! Public permissions helper.

Making roles_to_permission public allows the middleware and other modules to compute permissions directly, which is necessary for session refresh logic where permissions need to be recomputed after token refresh.

src/handlers/http/middleware.rs (2)

172-182: LGTM! Early extraction of session and user data.

Extracting the session key and user ID early in the middleware flow sets up proper authentication context for the subsequent session refresh logic.


252-259: Verify fallback session expiry behavior.

When a user exists but has no bearer token to refresh (lines 252-259), the code creates a new session with EXPIRY_DURATION (1 hour). This could reduce the session lifetime if the original session had a longer expiry. Confirm this is the intended behavior for non-OAuth users or sessions without refresh tokens.

src/rbac/user.rs (2)

70-85: LGTM! Bearer token support added to OAuth users.

The new_oauth function now accepts an optional bearer parameter, enabling storage of OAuth tokens for refresh. The Bearer is initially set to None in metadata (line 424) but populated to Some(bearer) in memory (line 433), which is the correct pattern for sensitive token data.


188-198: LGTM! Documented panic invariant.

The AsRef<Bearer> implementation includes documentation clarifying the panic condition and the safety invariant (bearer is always Some when OIDC is configured and this trait is called). This addresses the previous review concern.

src/handlers/http/oidc.rs (4)

77-83: LGTM! Safe OIDC client retrieval.

The code safely retrieves the OIDC client from app data with proper None handling, avoiding potential panics.


180-181: LGTM! Bearer token propagation.

The request_token function now returns the Bearer token along with claims and userinfo, enabling proper session expiry and token refresh implementation.


390-406: LGTM! Bearer token extraction from OIDC response.

The function now extracts and returns the Bearer token from the OIDC token response, enabling proper token refresh and session expiry management.


414-437: LGTM! Bearer persistence pattern.

The put_user function correctly persists user metadata without the Bearer token (line 424) but stores the Bearer in memory (line 433). This is the appropriate pattern for sensitive token data that shouldn't be persisted to storage.

Comment on lines +248 to +258
let expires_in = if let Some(expires_in) = bearer.expires_in.as_ref() {
// need an i64 somehow
if *expires_in > u32::MAX.into() {
EXPIRY_DURATION
} else {
let v = i64::from(*expires_in as u32);
Duration::seconds(v)
}
} else {
EXPIRY_DURATION
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Code duplication: expires_in calculation.

The expires_in calculation logic (lines 248-258) is duplicated in src/handlers/http/middleware.rs (lines 220-230). Consider extracting this into a shared utility function to maintain consistency and reduce duplication.

Example:

// In src/rbac/session_helpers.rs or similar
pub fn calculate_session_expiry(bearer: &Bearer) -> Duration {
    bearer.expires_in
        .as_ref()
        .filter(|&exp| *exp <= u32::MAX.into())
        .map(|exp| Duration::seconds(i64::from(*exp as u32)))
        .unwrap_or(EXPIRY_DURATION)
}

Then use calculate_session_expiry(&bearer) in both locations.

🤖 Prompt for AI Agents
In src/handlers/http/oidc.rs around lines 248 to 258 the expires_in calculation
is duplicated elsewhere (src/handlers/http/middleware.rs lines ~220-230);
extract the logic into a shared function (e.g.,
src/rbac/session_helpers.rs::calculate_session_expiry or similar) that accepts a
&Bearer and returns a chrono::Duration, implement the same safe-check (treat
values > u32::MAX or None as EXPIRY_DURATION, otherwise convert to i64 seconds),
make the function public where needed and replace both inline blocks with calls
to that helper to remove duplication and ensure consistent behavior.

Comment on lines +330 to +331
let mut url: String = auth_url.into();
url.push_str("&access_type=offline&prompt=consent");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if offline_access is documented or configured in scope handling
rg -n "offline_access" -A3 -B3

Repository: parseablehq/parseable

Length of output: 493


🏁 Script executed:

#!/bin/bash
# Also search for access_type and prompt parameters to understand their usage
rg -n "access_type|prompt=consent" -A2 -B2

Repository: parseablehq/parseable

Length of output: 408


🏁 Script executed:

#!/bin/bash
# Look for OIDC provider configuration and scope handling
rg -n "scope" src/handlers/http/oidc.rs -A2 -B2 | head -50

Repository: parseablehq/parseable

Length of output: 790


🏁 Script executed:

#!/bin/bash
# Check for any provider-specific configuration or comments
rg -n "OIDC|provider" src/handlers/http/oidc.rs -A2 -B2 | head -50

Repository: parseablehq/parseable

Length of output: 2037


🏁 Script executed:

#!/bin/bash
# Look at the context around lines 330-331 to understand the full flow
sed -n '320,340p' src/handlers/http/oidc.rs

Repository: parseablehq/parseable

Length of output: 877


🏁 Script executed:

#!/bin/bash
# Check if there's any provider detection or conditional logic
rg -n "provider|issuer|DiscoveredClient" src/handlers/http/oidc.rs -A2 -B2 | head -60

Repository: parseablehq/parseable

Length of output: 2228


🏁 Script executed:

#!/bin/bash
# Search for any documentation or issues about these parameters
rg -n "access_type|Google|prompt" src/ -A1 -B1

Repository: parseablehq/parseable

Length of output: 2826


🏁 Script executed:

#!/bin/bash
# Check the Options struct to understand what parameters are supported
fd -t f "\.rs$" | xargs grep -l "struct Options" | head -5

Repository: parseablehq/parseable

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Verify what openid library is being used and check its Options struct
fd -t f "\.rs$" src/ | xargs grep -l "Options\|auth_url" | grep -E "(oidc|openid)" | head -5

Repository: parseablehq/parseable

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Check the openid crate being used in Cargo.toml
cat Cargo.toml | grep -A2 -B2 "openid\|oidc"

Repository: parseablehq/parseable

Length of output: 218


Remove or conditionally apply Google-specific OAuth parameters to support multiple OIDC providers.

Lines 330-331 append &access_type=offline&prompt=consent to the authorization URL. This creates compatibility issues:

  1. access_type=offline is a Google OAuth 2.0 parameter, not standard OIDC—it will be ignored or rejected by other OIDC providers (Azure, Okta, etc.)
  2. The offline_access scope is already correctly configured in the default CLI options (openid profile email offline_access), which is the standard OIDC approach for obtaining refresh tokens
  3. prompt=consent forces users to consent on every login with no configuration option to make this optional

Consider removing the hardcoded parameters or adding provider detection to conditionally apply Google-specific parameters only when the OIDC issuer is Google.

🤖 Prompt for AI Agents
In src/handlers/http/oidc.rs around lines 330-331, the code unconditionally
appends the Google-only query params "&access_type=offline&prompt=consent" to
the auth URL; remove these hardcoded params or apply them only for Google
issuers to avoid breaking other OIDC providers. Fix by either (a) deleting the
push_str call so the standard "offline_access" scope handles refresh tokens, or
(b) add a conditional check using the configured issuer or parsed auth_url host
(e.g., issuer/host contains "accounts.google.com" or exact Google issuer) and
append those query params only when true; optionally make this behavior
configurable via a provider flag. Ensure tests/flows with Azure/Okta still work
and update comments accordingly.

@nitisht nitisht changed the title Possible fix for the oauth issue fix: Re-authentication with OIDC doesn't work after 24 hours Dec 11, 2025
@nitisht nitisht merged commit 8b02a0b into parseablehq:main Dec 11, 2025
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-authentication with oidc do not work after approx 24 hours

2 participants