Skip to content

refactor(auth): clean up OIDC service architecture - #398

Merged
hedhoud merged 1 commit into
linagora:refactor/hexagonalfrom
hedhoud:refactor/cleaner-auth-architecture
May 19, 2026
Merged

refactor(auth): clean up OIDC service architecture#398
hedhoud merged 1 commit into
linagora:refactor/hexagonalfrom
hedhoud:refactor/cleaner-auth-architecture

Conversation

@hedhoud

@hedhoud hedhoud commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Context

This continues the hexagonal refactor by moving OIDC auth behavior out of the component layer and into the service layer. The goal is to make auth easier to test and evolve without keeping business logic tied to legacy component imports.

Problem

The OIDC client, session token handling, state cookie handling, and refresh flow still lived under components. That made the auth boundary blurry and forced newer code to depend on legacy locations.

Expected behavior

Existing imports continue to work through compatibility shims, while new code can depend on the service-layer auth package directly. The behavior should remain unchanged; this PR is about ownership and cleaner boundaries, not a product change.

Validation

Layer import guard passes. Focused Python syntax checks pass using an isolated pycache path. The focused pytest command could not start in this environment because psycopg2 requires pg_config to build.

Summary by CodeRabbit

Release Notes

  • Refactor
    • Reorganized authentication code structure into a dedicated service layer while maintaining existing functionality through compatibility shims.
    • Updated authentication-related tests to reference new internal module locations.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@hedhoud has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 36 minutes and 37 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7de0eaef-8031-4de8-b298-e309072d22d2

📥 Commits

Reviewing files that changed from the base of the PR and between 94fe029 and aa4aefb.

📒 Files selected for processing (14)
  • openrag/components/auth/deps.py
  • openrag/components/auth/oidc_client.py
  • openrag/components/auth/refresh.py
  • openrag/components/auth/session_tokens.py
  • openrag/components/auth/state_cookie.py
  • openrag/components/auth/test_middleware.py
  • openrag/routers/test_auth_router.py
  • openrag/services/auth/__init__.py
  • openrag/services/auth/deps.py
  • openrag/services/auth/oidc_client.py
  • openrag/services/auth/refresh.py
  • openrag/services/auth/session_tokens.py
  • openrag/services/auth/state_cookie.py
  • tests/api_tests/test_oidc_lifecycle.py
📝 Walkthrough

Walkthrough

This PR refactors OIDC and authentication logic from the component layer (openrag/components/auth/) into a new service layer (openrag/services/auth/), converting component modules to re-export shims. The service layer provides a complete OIDC relying party implementation with token utilities, session refresh with stampede prevention, and lazy singleton client lifecycle management.

Changes

OIDC Auth Service Layer Refactor

Layer / File(s) Summary
Core OIDC Relying Party Client
openrag/services/auth/oidc_client.py
OIDCClient class with discovery/JWKS caching, PKCE helpers, authorization URL building, code exchange with id_token verification, refresh-token flow, userinfo fetching, and back-channel logout token verification. Public types TokenBundle and LogoutTokenClaims carry verified claims.
Session Token Encryption and Hashing
openrag/services/auth/session_tokens.py
Opaque session token generation, SHA-256 hashing for DB lookup, and Fernet encrypt/decrypt for IdP tokens with standardized error handling via InvalidToken conversion.
OIDC Authorization State Cookie Serialization
openrag/services/auth/state_cookie.py
StateCookiePayload and StateCookieSerializer provide signed (not encrypted) OIDC state cookie handling for Authorization Code + PKCE flow with 600-second TTL default.
OIDC Client Lazy Singleton and Lifecycle
openrag/services/auth/deps.py
Thread-safe lazy singleton initialization of OIDCClient from environment variables (with default scopes). reset_oidc_client() test hook clears cache and performs best-effort async cleanup of old client.
Session Refresh with Stampede Prevention and Recovery
openrag/services/auth/refresh.py
refresh_session_if_needed() checks 60s expiry buffer, short-circuits refresh-token stampede when last_refresh_at within 5s, exchanges refresh token with IdP, recovers from failures via session re-read, and persists rotated tokens via vectordb.
Service Layer Package API Re-exports
openrag/services/auth/__init__.py
New package entrypoint re-exporting OIDCClient, TokenBundle, LogoutTokenClaims, session token helpers, state cookie types, and client lifecycle functions via __all__.
Component Layer Re-export Shims
openrag/components/auth/deps.py, oidc_client.py, refresh.py, session_tokens.py, state_cookie.py
Each component module converted to lightweight re-export shim that imports implementations from service layer and defines __all__ for backward compatibility.
Test Import Paths and Module Stubs
openrag/components/auth/test_middleware.py, openrag/routers/test_auth_router.py, tests/api_tests/test_oidc_lifecycle.py
Update test imports to use services.auth layer instead of components.auth. Add module stubs for utils.logger and openai to prevent real infrastructure imports during testing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • linagora/openrag#323: Introduces initial OIDC and auth components in openrag/components/auth/*, which this PR refactors into a service layer.
  • linagora/openrag#308: Adds core OIDC implementation primitives that this PR moves from components to services.

Suggested labels

refactor

Suggested reviewers

  • paultranvan

Poem

🐰 With whiskers twitching in the service layer, the OIDC client finds its home,
Token dancing, singletons cached and clean, stamps of stampede neatly sidestepped,
Component shims bow deeply to the new regime, reporting implementations elsewhere,
Tests hop along new import paths, stubs preventing unwanted infrastructure dreams! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: refactoring the OIDC authentication service architecture by moving implementation from components to services.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (3)
openrag/services/auth/deps.py (1)

71-83: 💤 Low value

Consider using asyncio.get_running_loop() for better Python 3.10+ compatibility.

asyncio.get_event_loop_policy().get_event_loop() can raise DeprecationWarning in Python 3.10+ when called outside an async context. A safer pattern would catch RuntimeError from get_running_loop() to distinguish the two cases.

Suggested alternative
     try:
         import asyncio
 
-        loop = asyncio.get_event_loop_policy().get_event_loop()
-        if loop.is_running():
-            # Schedule close on the running loop without awaiting — caller
-            # doesn't need to be async.
+        try:
+            loop = asyncio.get_running_loop()
             loop.create_task(old.aclose())
-        else:
+        except RuntimeError:
+            # No running loop — run synchronously.
+            asyncio.run(old.aclose())
-            loop.run_until_complete(old.aclose())
     except Exception:
         # Closing is best-effort; never let a reset blow up the caller.
         pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/auth/deps.py` around lines 71 - 83, Replace the current
event-loop detection with asyncio.get_running_loop() and explicitly handle
RuntimeError to detect "no running loop": call asyncio.get_running_loop() in a
try/except, and if it returns a loop use loop.create_task(old.aclose()),
otherwise (RuntimeError) obtain a non-running loop (e.g. via
get_event_loop_policy().get_event_loop() or by creating a new event loop) and
call loop.run_until_complete(old.aclose()) — keep the operation best-effort and
preserve the existing swallow-on-exception behavior; reference the existing
old.aclose() call and use asyncio.get_running_loop(), loop.create_task, and
loop.run_until_complete in the fix.
openrag/routers/test_auth_router.py (1)

287-295: ⚡ Quick win

Add bind() to the logger stub interface.

This stub can fail if router code uses contextual logging (get_logger().bind(...).info(...)). Add a no-op bind returning the logger object.

Proposed patch
     logger_stub = types.ModuleType("utils.logger")
-    logger_stub.get_logger = lambda: types.SimpleNamespace(
-        debug=lambda *args, **kwargs: None,
-        info=lambda *args, **kwargs: None,
-        warning=lambda *args, **kwargs: None,
-        error=lambda *args, **kwargs: None,
-        exception=lambda *args, **kwargs: None,
-    )
+    class _NoopLogger:
+        def bind(self, **kwargs):
+            return self
+        def debug(self, *args, **kwargs): ...
+        def info(self, *args, **kwargs): ...
+        def warning(self, *args, **kwargs): ...
+        def error(self, *args, **kwargs): ...
+        def exception(self, *args, **kwargs): ...
+
+    logger_stub.get_logger = lambda: _NoopLogger()

As per coding guidelines, "Use Loguru for structured logging with the get_logger() utility from openrag/utils/logger, and bind contextual information (e.g., file_id, partition) to log messages".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/routers/test_auth_router.py` around lines 287 - 295, The logger stub
returned by get_logger in the test (logger_stub.get_logger ->
types.SimpleNamespace) lacks a bind method, so any router code calling
get_logger().bind(...).info(...) will fail; add a no-op bind on the returned
logger object (a method that accepts *args, **kwargs and returns the same logger
instance) so contextual chaining works; update the SimpleNamespace created in
logger_stub.get_logger to include this bind method while keeping existing
debug/info/warning/error/exception no-ops.
tests/api_tests/test_oidc_lifecycle.py (1)

252-260: ⚡ Quick win

Mirror Loguru’s bind() in the test logger stub.

This stub should expose bind() to avoid AttributeError when code paths use contextual logging.

Proposed patch
     logger_stub = types.ModuleType("utils.logger")
-    logger_stub.get_logger = lambda: types.SimpleNamespace(
-        debug=lambda *args, **kwargs: None,
-        info=lambda *args, **kwargs: None,
-        warning=lambda *args, **kwargs: None,
-        error=lambda *args, **kwargs: None,
-        exception=lambda *args, **kwargs: None,
-    )
+    class _NoopLogger:
+        def bind(self, **kwargs):
+            return self
+        def debug(self, *args, **kwargs): ...
+        def info(self, *args, **kwargs): ...
+        def warning(self, *args, **kwargs): ...
+        def error(self, *args, **kwargs): ...
+        def exception(self, *args, **kwargs): ...
+
+    logger_stub.get_logger = lambda: _NoopLogger()

As per coding guidelines, "Use Loguru for structured logging with the get_logger() utility from openrag/utils/logger, and bind contextual information (e.g., file_id, partition) to log messages".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/api_tests/test_oidc_lifecycle.py` around lines 252 - 260, The test
logger stub lacks a bind() method, causing AttributeError when code expects
Loguru-style contextual logging; update the logger_stub.get_logger to return a
logger-like object that includes a bind(self, **kwargs) method which returns the
same (or a shallow copy of the) SimpleNamespace logger so callers can chain
bind(...). Ensure the returned object still exposes
debug/info/warning/error/exception and that bind simply returns an object with
those same methods (preserving the existing lambdas) so tests using
bind(file_id=..., partition=...) work without errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openrag/services/auth/oidc_client.py`:
- Around line 355-356: The logout-token nonce check uses decoded.get("nonce")
which treats empty/falsy values as missing; change the check to test key
presence (e.g., if "nonce" in decoded) so any presence of the "nonce" claim
(even empty string) triggers the same ValueError; update the conditional at the
decoded nonce check and keep the existing ValueError("logout_token must not
contain nonce") behavior.

In `@openrag/services/auth/refresh.py`:
- Line 109: Replace direct awaits of Ray actor method calls with the centralized
timeout wrapper: import call_ray_actor_with_timeout from
services.workers.ray_utils and change lines that call
vectordb.get_oidc_session_by_id.remote(...), and the two other
vectordb.<method>.remote(...) calls in this function to await
call_ray_actor_with_timeout(<actor_call>, timeout=<appropriate seconds>,
task_description="Re-read OIDC session for stampede recovery" or a short
descriptive string for the other two calls). Use the same timeout policy used
elsewhere in this module and ensure the task_description clearly identifies the
operation; keep the remote(...) call as the first argument to
call_ray_actor_with_timeout.

---

Nitpick comments:
In `@openrag/routers/test_auth_router.py`:
- Around line 287-295: The logger stub returned by get_logger in the test
(logger_stub.get_logger -> types.SimpleNamespace) lacks a bind method, so any
router code calling get_logger().bind(...).info(...) will fail; add a no-op bind
on the returned logger object (a method that accepts *args, **kwargs and returns
the same logger instance) so contextual chaining works; update the
SimpleNamespace created in logger_stub.get_logger to include this bind method
while keeping existing debug/info/warning/error/exception no-ops.

In `@openrag/services/auth/deps.py`:
- Around line 71-83: Replace the current event-loop detection with
asyncio.get_running_loop() and explicitly handle RuntimeError to detect "no
running loop": call asyncio.get_running_loop() in a try/except, and if it
returns a loop use loop.create_task(old.aclose()), otherwise (RuntimeError)
obtain a non-running loop (e.g. via get_event_loop_policy().get_event_loop() or
by creating a new event loop) and call loop.run_until_complete(old.aclose()) —
keep the operation best-effort and preserve the existing swallow-on-exception
behavior; reference the existing old.aclose() call and use
asyncio.get_running_loop(), loop.create_task, and loop.run_until_complete in the
fix.

In `@tests/api_tests/test_oidc_lifecycle.py`:
- Around line 252-260: The test logger stub lacks a bind() method, causing
AttributeError when code expects Loguru-style contextual logging; update the
logger_stub.get_logger to return a logger-like object that includes a bind(self,
**kwargs) method which returns the same (or a shallow copy of the)
SimpleNamespace logger so callers can chain bind(...). Ensure the returned
object still exposes debug/info/warning/error/exception and that bind simply
returns an object with those same methods (preserving the existing lambdas) so
tests using bind(file_id=..., partition=...) work without errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a89eb4b3-97d4-4e99-bac1-d373ab8b261b

📥 Commits

Reviewing files that changed from the base of the PR and between de42f86 and 94fe029.

📒 Files selected for processing (14)
  • openrag/components/auth/deps.py
  • openrag/components/auth/oidc_client.py
  • openrag/components/auth/refresh.py
  • openrag/components/auth/session_tokens.py
  • openrag/components/auth/state_cookie.py
  • openrag/components/auth/test_middleware.py
  • openrag/routers/test_auth_router.py
  • openrag/services/auth/__init__.py
  • openrag/services/auth/deps.py
  • openrag/services/auth/oidc_client.py
  • openrag/services/auth/refresh.py
  • openrag/services/auth/session_tokens.py
  • openrag/services/auth/state_cookie.py
  • tests/api_tests/test_oidc_lifecycle.py

Comment thread openrag/services/auth/oidc_client.py Outdated
Comment thread openrag/services/auth/refresh.py Outdated
@hedhoud
hedhoud force-pushed the refactor/cleaner-auth-architecture branch from 94fe029 to aa4aefb Compare May 19, 2026 13:59
@hedhoud
hedhoud merged commit 3d81910 into linagora:refactor/hexagonal May 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant