Skip to content

fix: support Entra common issuer discovery for OIDC#1340

Merged
ggfevans merged 6 commits intomainfrom
fix/1335-entra-common-support
Feb 27, 2026
Merged

fix: support Entra common issuer discovery for OIDC#1340
ggfevans merged 6 commits intomainfrom
fix/1335-entra-common-support

Conversation

@ggfevans
Copy link
Collaborator

@ggfevans ggfevans commented Feb 26, 2026

User description

Summary

  • accept Microsoft Entra common issuer configuration when discovery resolves to a tenant-specific issuer
  • align /auth/login and /auth/callback compatibility routes with Better Auth generic OAuth2 flow
  • add OIDC integration coverage for Entra common issuer acceptance and wrong-audience rejection

Testing

  • bun test src/oidc.integration.test.ts src/security.test.ts src/auth-logger.test.ts

Closes #1335


CodeAnt-AI Description

Support Microsoft Entra 'common' OIDC discovery, restore legacy auth routes, and enforce fallback session idle timeout

What Changed

  • Accept Entra "common" issuer configuration even when the provider's discovery document returns a tenant-specific issuer, allowing logins started with the common issuer to complete.
  • Restore public compatibility routes (/auth/login, /auth/callback, /auth/check, /auth/logout) that initiate OIDC sign-in, proxy the provider callback, report auth-check status, and clear session cookies for web integrations.
  • Reject ID tokens whose audience does not match the configured client ID so callbacks do not create sessions from mismatched tokens.
  • Support a fallback session check that maps Better Auth provider session data into local auth claims, enforce idle-timeout based on persisted session metadata (no silent extension), and invalidate fallback sessions on logout to prevent replay.
  • Align cookie security and OIDC discovery behavior (configurable secure cookie flag, discovery timeout) and add integration tests covering Entra common→tenant discovery, wrong-audience rejection, fallback idle-timeout, and logout replay blocking.

Impact

✅ Fewer failed Entra logins
✅ Clearer callback rejection when client ID is wrong
✅ Fewer replayable fallback sessions after logout

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements OIDC (OpenID Connect) authentication support with Microsoft Entra ID multi-tenant issuer compatibility. It introduces OIDC discovery and verification utilities, updates authentication configuration and routes to support OAuth2 flows with fallback session claims, adds integration tests, and extends documentation with Entra ID configuration guidance.

Changes

Cohort / File(s) Summary
OIDC Configuration & Discovery
api/src/auth/config.ts
Adds extensive OIDC scaffolding: environment-driven configuration parsing, discovery URL resolution with issuer normalization and Microsoft Entra common-issuer compatibility, JWKS fetching, token verification, user info mapping, and OIDC plugin integration. Updates createAuth signature to accept an env: EnvMap parameter with defaults.
Authentication Routes & Application Logic
api/src/app.ts
Introduces OIDC-specific helpers and types (BetterAuthSessionLike, normalization utilities, claim validation). Replaces previous login/callback paths with OIDC-enabled /auth/login, /auth/callback, /auth/check, and /auth/logout routes plus API equivalents. Adds fallback session claims resolution, provider sign-out handling, and 501/502 error responses for unavailable OIDC endpoints.
Security Middleware Enhancement
api/src/security.ts
Adds optional resolveFallbackClaims?: (request: Request) => Promise<AuthSessionClaims | null> parameter to createAuthGateMiddleware to support fallback session claims when primary session validation fails.
Testing & Configuration Examples
api/.env.example, api/src/oidc.integration.test.ts
Adds commented OIDC issuer examples for single-tenant and multi-tenant Microsoft Entra ID. Introduces comprehensive integration test module with JWT mocking, OIDC fetch mocking, and tests for login flow, audience validation, idle timeout, and logout/replay prevention.
Documentation
docs/deployment/AUTHENTICATION.md
Adds Microsoft Entra ID (single-tenant and multi-tenant) OIDC issuer examples and troubleshooting guidance including issuer alignment requirements for Entra common scenarios.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant App as App<br/>/auth/login
    participant OidcProvider as OIDC Provider
    participant AppCallback as App<br/>/auth/callback

    Client->>App: GET /auth/login
    App->>OidcProvider: Fetch discovery document
    OidcProvider-->>App: Discovery metadata + JWKS URI
    App->>OidcProvider: Fetch JWKS
    OidcProvider-->>App: Public key set
    App->>OidcProvider: Redirect to authorization endpoint
    OidcProvider-->>Client: 302 to provider login
    Client->>OidcProvider: Complete authentication
    OidcProvider->>AppCallback: Redirect with auth code
    AppCallback->>OidcProvider: Exchange code for ID token
    OidcProvider-->>AppCallback: ID token (RS256 signed)
    AppCallback->>AppCallback: Verify token signature against JWKS
    AppCallback->>AppCallback: Validate issuer, audience, expiry
    AppCallback->>Client: 302 + Set-Cookie auth session
    Client->>Client: Store session cookie
Loading
sequenceDiagram
    participant Client
    participant AppGate as Auth Gate<br/>Middleware
    participant SessionProvider as Session<br/>Provider
    participant FallbackResolver as Fallback Claims<br/>Resolver

    Client->>AppGate: Request with session cookie
    AppGate->>SessionProvider: Validate primary session
    alt Primary session valid
        SessionProvider-->>AppGate: AuthSessionClaims
        AppGate->>AppGate: Set authSubject/authClaims
        AppGate->>Client: Continue to next handler
    else Primary session invalid or missing
        SessionProvider-->>AppGate: null
        AppGate->>FallbackResolver: resolveFallbackClaims(request)
        alt Fallback claims available
            FallbackResolver-->>AppGate: AuthSessionClaims (fallback)
            AppGate->>AppGate: Set authSubject/authClaims from fallback
            AppGate->>Client: Continue to next handler
        else No fallback claims
            FallbackResolver-->>AppGate: null
            AppGate->>AppGate: Log unauthorized
            AppGate->>Client: 401/Redirect
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

size:XXL

Poem

🐰 A rabbit hops through OIDC gates,
With Entra tokens sealed by fates,
Fallback claims when sessions sleep,
Discovery caches secrets deep—
OAuth2 flows through fields so bright! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.90% 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
Title check ✅ Passed The title accurately reflects the main change: supporting Microsoft Entra common issuer discovery for OIDC authentication.
Linked Issues check ✅ Passed The PR directly addresses all key requirements from #1335: supports Entra common issuer via discovery issuer normalization, enforces strict audience checks, adds integration tests, and updates documentation.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #1335 objectives: Entra common issuer support, OIDC discovery/validation, legacy route restoration, fallback session handling, and related documentation updates.
Description check ✅ Passed The pull request description clearly explains the changes: support for Microsoft Entra common issuer discovery, restoration of public compatibility routes, audience validation for id_tokens, and new integration tests with documentation updates.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/1335-entra-common-support

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.

@codeant-ai codeant-ai bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Feb 26, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Set-Cookie handling
    Fallback logic for extracting Set-Cookie headers may miss multiple Set-Cookie values in some runtime environments. Relying on a single headers.get("set-cookie") or a non-standard getSetCookie method can lose cookies returned by the auth sign-in flow; ensure all Set-Cookie headers are collected reliably across Fetch/Node/Bun implementations.

  • Open redirect check
    The normalizeNextPath sanitiser is used to build callback URLs / redirect targets. Verify this logic against encoded or unusual inputs (e.g. encoded scheme or path containing colon/combinations) — ensure it fully prevents open-redirects in all edge cases relevant to your integrations.

  • Discovery issuer assumption
    The mock discovery document intentionally returns a tenant-specific issuer while the configured env uses the common issuer. This exercise tests the new acceptance logic, but reviewers should verify the production code handles this exact pattern reliably (e.g., string normalization, trailing slash handling, and canonicalization of issuer URLs).

  • Discovery fetch resiliency
    fetchOidcDiscoveryDocument uses a plain fetch without timeouts or an AbortController. Network stalls or slow responses could block startup/calls — add timeouts and clearer error handling for transient network failures.

  • Global fetch patching
    The test replaces globalThis.fetch with a mocked implementation and copies only the preconnect property. This global mutation can interfere with other tests running in parallel and may not fully emulate the original fetch (missing non-enumerable properties, prototype, or other helper methods). Consider isolating the mock or making the patched function preserve behavior/properties of the original.

Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/auth/config.ts`:
- Around line 224-270: The resolveDiscovery caching can race if multiple callers
trigger the branch before discoveryPromise is set; fix by assigning
discoveryPromise to a Promise synchronously (wrap the fetch in an
immediately-invoked async function) so the in-flight promise is created
atomically, and keep the existing catch handler to clear discoveryPromise and
jwks on error. Update the logic in createOidcUserInfoResolver’s resolveDiscovery
(the discoveryPromise assignment) to set discoveryPromise = (async () =>
fetchOidcDiscoveryDocument(options.discoveryUrl,
options.expectedIssuer))().catch((error: unknown) => { discoveryPromise =
undefined; jwks = undefined; throw error; }); so the promise is always created
immediately and subsequent callers see the same promise.
- Around line 362-377: The advanced cookie config currently sets
useSecureCookies: false which forces cookies to be non-Secure and overrides
defaultCookieAttributes; change this so cookies are Secure in production by
either removing useSecureCookies or setting useSecureCookies: true, and ensure
defaultCookieAttributes.secure is set appropriately (e.g., secure: env.NODE_ENV
=== "production" or secure: true when using custom names); update the
advanced.cookies.session_token (authSessionCookieName) handling to keep custom
name but not disable the Secure flag so Rackula middleware can read the cookie
while still enforcing HTTPS in production.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 068bbea and b51b905.

📒 Files selected for processing (6)
  • api/.env.example
  • api/src/app.ts
  • api/src/auth-logger.test.ts
  • api/src/auth/config.ts
  • api/src/oidc.integration.test.ts
  • docs/deployment/AUTHENTICATION.md

Comment on lines 362 to 376
advanced: {
// Keep custom cookie names exact (without auto __Secure- prefixing) so
// Rackula's CSRF/auth middleware can reliably read the configured cookie name.
useSecureCookies: false,
cookies: {
session_token: {
name: authSessionCookieName,
},
},
defaultCookieAttributes: {
httpOnly: true, // Prevent XSS access to cookie
sameSite: "lax", // CSRF protection
secure: process.env.NODE_ENV === "production", // HTTPS only in production
secure: env.NODE_ENV === "production", // HTTPS only in production
// domain: '.racku.la' // Uncomment if using subdomains
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: The secure flag for the Better Auth session cookie ignores the RACKULA_AUTH_SESSION_COOKIE_SECURE environment override and is always derived from NODE_ENV, which can diverge from security.ts's authSessionCookieSecure setting; this leads to inconsistent Secure attributes between cookies issued by Better Auth and those issued by your own auth middleware (e.g. on /auth/check and /auth/logout), and in configurations that explicitly disable Secure in production the refresh/logout routes will downgrade a previously secure cookie to a non-secure one. [logic error]

Severity Level: Major ⚠️
- ❌ Secure session cookie can be downgraded on auth-check/logout.
- ⚠️ Better Auth and middleware disagree on Secure cookie policy.
- ⚠️ RACKULA_AUTH_SESSION_COOKIE_SECURE override not honored by Better Auth.
Suggested change
advanced: {
// Keep custom cookie names exact (without auto __Secure- prefixing) so
// Rackula's CSRF/auth middleware can reliably read the configured cookie name.
useSecureCookies: false,
cookies: {
session_token: {
name: authSessionCookieName,
},
},
defaultCookieAttributes: {
httpOnly: true, // Prevent XSS access to cookie
sameSite: "lax", // CSRF protection
secure: process.env.NODE_ENV === "production", // HTTPS only in production
secure: env.NODE_ENV === "production", // HTTPS only in production
// domain: '.racku.la' // Uncomment if using subdomains
},
advanced: {
// Keep custom cookie names exact (without auto __Secure- prefixing) so
// Rackula's CSRF/auth middleware can reliably read the configured cookie name.
useSecureCookies: false,
cookies: {
session_token: {
name: authSessionCookieName,
},
},
defaultCookieAttributes: {
httpOnly: true, // Prevent XSS access to cookie
sameSite: "lax", // CSRF protection
secure:
(() => {
const override = readEnv(env, "RACKULA_AUTH_SESSION_COOKIE_SECURE");
if (!override) {
return env.NODE_ENV === "production";
}
const normalized = override.toLowerCase();
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
return env.NODE_ENV === "production";
})(), // Aligns with auth cookie secure flag configuration
// domain: '.racku.la' // Uncomment if using subdomains
},
},
Steps of Reproduction ✅
1. Configure the API with authentication enabled and conflicting cookie settings by
starting the app via `createApp(env)` in `api/src/app.ts:78` using an environment map
where `NODE_ENV="production"` and `RACKULA_AUTH_SESSION_COOKIE_SECURE="false"` (or
`"true"` with `NODE_ENV!="production"`). This mirrors documented override usage in
`docs/deployment/AUTHENTICATION.md:233-237` and `api/README.md:259-261`.

2. When `createApp` runs, it calls `resolveApiSecurityConfig(env)` in
`api/src/security.ts:933`, which computes `authSessionCookieSecure` using
`parseOptionalBoolean("RACKULA_AUTH_SESSION_COOKIE_SECURE",
env.RACKULA_AUTH_SESSION_COOKIE_SECURE, isProduction)` at `security.ts:1022-1025`. With
the override set to `"false"` and `NODE_ENV="production"`, `authSessionCookieSecure`
becomes `false`.

3. Still in `createApp`, Better Auth is instantiated as `const auth =
securityConfig.authSessionSecret ? createAuth(securityConfig.authSessionSecret, env) :
undefined;` at `api/src/app.ts:112-114`. Inside `createAuth` in
`api/src/auth/config.ts:293-381`, the `advanced.defaultCookieAttributes.secure` flag for
Better Auth is hard-coded to `env.NODE_ENV === "production"` at `config.ts:371-375`,
ignoring `RACKULA_AUTH_SESSION_COOKIE_SECURE`, so Better Auth will set `Secure` on its
session cookie whenever `NODE_ENV` is `"production"`.

4. Trigger a login via the Better Auth compatibility route `GET /auth/login`, wired in
`createApp` at `api/src/app.ts:281-287`. Better Auth (via `createAuth`) issues a
`Set-Cookie` header for the session cookie `rackula_auth_session` (name wired through
`advanced.cookies.session_token.name` at `auth/config.ts:366-370`) with `Secure` present
because `env.NODE_ENV === "production"` even though `authSessionCookieSecure` is `false`.

5. With the session established, call `GET /auth/check` (or `/api/auth/check`), which is
registered in `api/src/app.ts:283-289`. The handler `authCheckRouteHandler` at
`app.ts:229-259` reads the session cookie and then calls
`createRefreshedAuthSessionCookieHeader(claims, authSessionConfig)` at `app.ts:250-253`.
That function in `security.ts:841-885` ultimately uses `buildSessionCookieHeader` at
`security.ts:474-501`, which inspects `securityConfig.authSessionCookieSecure` at
`security.ts:496-498` to decide whether to append `Secure`. Because
`authSessionCookieSecure` was resolved to `false` in step 2, the refreshed `Set-Cookie`
header omits `Secure`, overwriting the previously `Secure` Better Auth cookie with a
non-secure cookie of the same name.

6. Observe in the HTTP response from `/auth/check` (or `/auth/logout`, which uses
`createExpiredAuthSessionCookieHeader` at `security.ts:809-830` via `app.ts:273-277`) that
the `Set-Cookie` header for `rackula_auth_session` lacks the `Secure` attribute even
though the original login cookie from Better Auth had `Secure`. This demonstrates that the
Better Auth configuration in `auth/config.ts` is not honoring the
`RACKULA_AUTH_SESSION_COOKIE_SECURE` override and can downgrade a previously secure
session cookie when the override disagrees with `NODE_ENV`, creating inconsistent cookie
security between Better Auth and the custom auth middleware.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** api/src/auth/config.ts
**Line:** 362:376
**Comment:**
	*Logic Error: The `secure` flag for the Better Auth session cookie ignores the `RACKULA_AUTH_SESSION_COOKIE_SECURE` environment override and is always derived from `NODE_ENV`, which can diverge from `security.ts`'s `authSessionCookieSecure` setting; this leads to inconsistent `Secure` attributes between cookies issued by Better Auth and those issued by your own auth middleware (e.g. on `/auth/check` and `/auth/logout`), and in configurations that explicitly disable `Secure` in production the refresh/logout routes will downgrade a previously secure cookie to a non-secure one.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

CodeAnt AI finished reviewing your PR.

@ggfevans
Copy link
Collaborator Author

Addressed all three review items in follow-up commit 75e73d2a:

  1. Discovery cache race (api/src/auth/config.ts): switched resolveDiscovery to atomic IIFE assignment so concurrent callers share one in-flight promise and failure resets cache cleanly.
  2. Hardcoded useSecureCookies: false: removed the hardcoded insecure behavior and now drive useSecureCookies from resolved cookie security policy.
  3. Secure flag mismatch with Rackula config: added explicit parsing of RACKULA_AUTH_SESSION_COOKIE_SECURE and used the resolved value for Better Auth useSecureCookies + defaultCookieAttributes.secure (fallback remains NODE_ENV === "production").

Validation:

  • bun test src/oidc.integration.test.ts src/security.test.ts src/auth-logger.test.ts (71 pass, 0 fail)

@ggfevans
Copy link
Collaborator Author

@CodeAnt-AI review
@CodeRabbit-ai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

@ggfevans, thank you for addressing those review items! I appreciate the detailed validation notes showing all tests passing. I'll trigger a comprehensive review of the PR now, including the fixes in commit 75e73d2a.


🧠 Learnings used
Learnt from: CR
Repo: RackulaLives/Rackula PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-01T03:29:11.563Z
Learning: CodeRabbit provides AI code review on every PR. Claude Code must wait for CodeRabbit approval before merging. Run local review before pushing with `coderabbit --prompt-only --type uncommitted` or `coderabbit --prompt-only --type committed` to catch issues early. Always use `--prompt-only` for token-efficient output. Never use `gh pr merge` until CodeRabbit has approved the PR.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

Question: review
@CodeRabbit-ai review

Answer:
Thanks — nice, focused change-set. Summary of what I looked at and the main findings, plus a few suggestions and small correctness/security notes.

What this PR does (high level)

  • Adds support for Microsoft Entra "common" issuer patterns: a configured issuer of https://login.microsoftonline.com/common/v2.0 is accepted even when discovery returns a tenant-specific issuer (e.g. //v2.0).
  • Restores public compatibility endpoints (/auth/login, /auth/callback, /auth/check, /auth/logout) that proxy to the Better Auth API handlers and provide web-friendly behavior (redirect to IdP, set cookies returned by provider, return 204 for valid check, etc.).
  • Adds logic to accept legacy Better Auth sessions as a fallback by calling auth.api.getSession and mapping to internal AuthSessionClaims (so existing Better Auth sessions are honored).
  • Adds ID token audience verification during callback (rejects id_tokens with wrong audience).
  • Adds integration tests for Entra common→tenant discovery, wrong-audience rejection, fallback idle timeout behavior, logout invalidation, etc.
  • Documentation and .env example updates for Entra.

Positive points

  • Tests: Good integration tests exercising the end-to-end flow and the Entra-specific behavior, including wrong-audience rejection and fallback idle-timeout semantics.
  • The Entra "common" matching is implemented defensively and only accepts the pattern for login.microsoftonline.com (so it's not a generic relaxation).
  • Returning/refilling cookies from provider responses (append Set-Cookie) seems correct and mirrors expected web flows.
  • The fallback session mapping and validation is designed to avoid extending idle timeout from request-time, which is the correct, conservative approach.

Concrete review items, suggestions, and potential issues

  1. Entra "common" matching
  • Code: isMicrosoftEntraCommonIssuerMatch + issuerMatchesExpected (auth/config.ts)
  • Looks good — it checks host/protocol/port equality and that expected is exactly /common/v2.0 and discovery is //v2.0.
  • Suggestion: add unit tests for isMicrosoftEntraCommonIssuerMatch to cover:
    • expected not exactly /common/v2.0 (e.g. trailing slash variants, uppercase/lowercase)
    • discovery path trailing slashes or additional path segments (should reject)
    • host mismatch or non-login.microsoftonline.com host
      This will prevent subtle regressions if URL normalization changes.
  1. Discovery URL normalization & parseAbsoluteUrl
  • resolveOidcDiscoveryUrl will treat RACKULA_OIDC_ISSUER if it already includes the discovery path.
  • parseAbsoluteUrl throws a raw Error with the cause — OK, but may bubble up to startup. Consider a clearer error message for operators (you do throw meaningful message already).
  • normalizeIssuerUrl uses URL and ensures pathname is either non-empty or "/" — good.
  1. ID token verification & error handling
  • createOidcUserInfoResolver uses jwtVerify with issuer: discovery.issuer and audience: clientId.
  • If jwtVerify fails, you log a console.warn and return null. That is acceptable for plugin getUserInfo since returning null will cause login failure. Consider adding more structured logging (e.g. safeLogAuthEvent) when jwtVerify fails in prod to aid debugging. But current console.warn is fine for most cases.
  1. Fallback session mapping and validation
  • mapFallbackSessionClaims and validateFallbackSessionClaims (app.ts)
  • The logic to compute idleExp from persisted issuedAt + idle timeout is correct and avoids extending by request-time.
  • validateFallbackSessionClaims re-signs and verifies token using createSignedAuthSessionToken and verifySignedAuthSessionToken to ensure the configured secret+generation etc. This is good — it prevents forging fallback sessions.
  • Security note: validateFallbackSessionClaims requires authSessionSecret to be present; if not present you return null — good.
  1. Cookie header handling
  • readSetCookieHeaders, appendSetCookieHeaders and cookie merging in tests: careful handling for Bun Headers vs standard Headers is needed. You attempt to read a getSetCookie helper if present — good defensive coding.
  • Minor: readSetCookieHeaders currently returns raw header string as single element if headers.get("set-cookie") exists but multiple Set-Cookie headers are concatenated by some runtimes. Your code accounts for provider library exposing getSetCookie; this is fine but be mindful that some fetch implementations may return a single Set-Cookie string with commas; tests/mocks cover this.
  1. Public compatibility routes (/auth/*)
  • You proxy /auth/callback to /api/auth/oauth2/callback/oidc by creating a new Request with original headers and same method. Good and simple.
  • CSRF: These public endpoints are allowed through the auth gate. You do not set CSRF tokens here; ensure your CSRF middleware logic (createCsrfProtectionMiddleware) and general routing prevents abuse. It appears the auth login/callback endpoints are designed to be public and only used to start/finish OAuth flows, so this is OK.
  1. Use of createAuth(secret, env) — passing env
  • You now pass env to createAuth so plugin resolvers can use env at startup. Good.
  1. Tests: mocking global fetch & Date.now
  • installMockOidcFetch patches globalThis.fetch and restores it — test cleanup uses restore in finally, good.
  • Several tests monkeypatch Date.now to simulate time progression. You restore Date.now in finally — good. Be sure these tests run isolated (they do).
  • Suggestion: in installMockOidcFetch you copy preconnect — okay but consider copying all properties to be safe; this is fine as-is.
  1. Potential issue: createSignedAuthSessionToken / verifySignedAuthSessionToken references
  • You import createSignedAuthSessionToken and verifySignedAuthSessionToken from "./security" at top of app.ts. Ensure those exports exist and their semantics match usage. I did not see them removed elsewhere in this diff, so assume present. If tests run, that will validate it.
  1. Error path & status codes
  • When auth provider not configured, you return 501 JSON. When OIDC login initiation fails you return 502. Those seem reasonable; maybe include provider error detail in logs (you already console.error the caught error in login initiation).
  1. README / docs
  • .env example and docs updated for Entra — helpful. The docs note the discovery issuer can be tenant-specific after login — good to call out.
  1. Minor code style/consistency
  • Very small formatting changes in tests (line breaks) — fine.
  • Some long functions in app.ts — consider extracting fallback session resolution logic and OIDC route handlers into their own small module/file to keep app.ts smaller and easier to review long-term.
  1. Edge-case: multiple scopes parsing
  • parseOidcScopes ensures "openid" present and deduplicates — good.
  1. Security: rejecting id_tokens if audience does not match
  • Good change — prevents a bad token from creating session. Tests validate this.

Actionable recommendations (prioritized)

  1. Add unit tests for isMicrosoftEntraCommonIssuerMatch and issuer normalization edge cases (trailing slashes, uppercase, port presence, unexpected paths).
  2. Consider instrumenting jwtVerify failures with safeLogAuthEvent (or more structured logging) in createOidcUserInfoResolver so operators can see token verification failures without inspecting console warnings.
  3. Add a short unit test for resolveOidcDiscoveryUrl when RACKULA_OIDC_ISSUER already contains /.well-known/openid-configuration (various trailing slash combinations).
  4. Consider splitting the new app.ts helpers (fallback resolver, cookie helpers, route handlers) into a small module for readability if this file will grow.

Overall impression

  • The changes are focused, well-reasoned and include integration tests that exercise the primary flows that motivated this change (Entra common issuer behavior and audience validation). I think this is a solid improvement that will reduce Entra login failures and improve diagnostics. With the additional unit tests suggested above and possibly a bit more structured logging on token verification failures, this should be ready to merge.

If you want, I can:

  • Run a quick search for the referenced functions (createSignedAuthSessionToken, etc.) to double-check signatures, or
  • Add the suggested unit tests (issuer matching) as a follow-up PR patch. Which would you prefer?

Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/app.ts`:
- Around line 154-162: The returned fallback session object currently hardcodes
role: "admin" (in the function that builds the fallback OIDC session mapping
where the object with keys sub, sid, iat, exp, idleExp, generation, role is
returned); add a brief explanatory comment immediately above this return that
states this is an MVP-only hardcoded admin role (temporary until role
configuration is added) and mention that future work will make the role
configurable via authSessionConfig or the OIDC claims; optionally annotate which
config key (authSessionConfig.authSessionGeneration) will be used when
implementing configurability so reviewers can find the intended extension point.

In `@api/src/auth/config.ts`:
- Line 5: The EnvMap type is duplicated in config.ts; instead of redefining it,
import the existing EnvMap from api/src/security.ts to avoid duplication and
keep types consistent—update config.ts to remove the type alias and add an
import for EnvMap from the module that exports it (referencing the EnvMap symbol
and the security.ts module) and adjust any usages in functions or exports in
config.ts to use the imported type.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b51b905 and 75e73d2.

📒 Files selected for processing (4)
  • api/src/app.ts
  • api/src/auth/config.ts
  • api/src/oidc.integration.test.ts
  • api/src/security.ts

Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/app.ts`:
- Around line 148-152: When computing fallbackSubject (the expression using
session.user.email?.trim() || session.user.id?.trim() || "oidc-user"), add a
warning log when both email and id are missing and the code is about to use the
generic "oidc-user" value; locate the fallbackSubject assignment and insert a
single warning via the existing logger (e.g., processLogger or logger) that
includes enough non-sensitive context (e.g., session.id or request id) to aid
debugging, ensuring you do not log raw tokens or sensitive user data.

In `@api/src/auth/config.ts`:
- Around line 164-202: fetchOidcDiscoveryDocument currently calls
fetch(discoveryUrl, ...) without a timeout which can hang; wrap the fetch in an
AbortController (or use AbortSignal.timeout) with a sensible timeout value (e.g.
configurable constant) passed to fetch, pass controller.signal, and ensure you
call controller.abort() on timeout; update fetch error handling in
fetchOidcDiscoveryDocument to catch an AbortError and throw a clear
timeout-specific error (while keeping existing non-OK response and JSON parsing
checks), referencing discoveryUrl, expectedIssuer, normalizeIssuerUrl, and
parseAbsoluteUrl to locate the function to modify.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b51b905 and 75e73d2.

📒 Files selected for processing (4)
  • api/src/app.ts
  • api/src/auth/config.ts
  • api/src/oidc.integration.test.ts
  • api/src/security.ts

ggfevans added a commit that referenced this pull request Feb 27, 2026
- Import EnvMap from security.ts instead of redefining in config.ts
- Add 10s timeout to OIDC discovery fetch via AbortSignal.timeout
- Log warning when OIDC session falls back to generic "oidc-user" subject
- Document MVP-only hardcoded admin role in fallback session mapping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/app.ts`:
- Around line 155-164: The check "if (!fallbackSubject)" is dead because
fallbackSubject is always truthy due to the "|| 'oidc-user'" default; remove the
unreachable conditional or change the fallback logic so fallbackSubject can be
null/undefined when both session.user.email and session.user.id are missing.
Specifically, update the code around the fallbackSubject variable in app.ts (the
declaration and subsequent "if (!fallbackSubject)" block) by either deleting the
unreachable if block or by deriving fallbackSubject without the forced
"'oidc-user'" default and handling the null case explicitly where needed.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75e73d2 and aacf9f8.

📒 Files selected for processing (2)
  • api/src/app.ts
  • api/src/auth/config.ts

Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/app.ts`:
- Around line 141-142: The current guard uses falsy checks on the parsed epoch
values (if (!issuedAt || !expiresAt || expiresAt <= issuedAt)) which incorrectly
treats valid 0 timestamps as invalid; change the condition to explicit null
checks (e.g. issuedAt === null || expiresAt === null || expiresAt <= issuedAt)
so only null results from toEpochSeconds() are rejected while allowing 0, and
keep the same return null behavior when the check fails.
- Around line 82-90: The current code in the readSetCookieHeaders path uses a
defensive fallback that reads headers.get("set-cookie") after checking
withGetSetCookie.getSetCookie, which is unnecessary and can collapse multiple
cookies; update the implementation to assume Bun >= 1.0.0 and simply return
headers.getSetCookie() (or [] when headers is undefined). Remove the
withGetSetCookie cast, the conditional that checks getSetCookie, and the
rawSetCookie fallback; ensure the function (readSetCookieHeaders) first returns
[] if headers is falsy and otherwise returns headers.getSetCookie().

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aacf9f8 and 7824a83.

📒 Files selected for processing (1)
  • api/src/app.ts

Comment on lines +82 to +90
const withGetSetCookie = headers as Headers & {
getSetCookie?: () => string[];
};
if (typeof withGetSetCookie.getSetCookie === "function") {
return withGetSetCookie.getSetCookie();
}

const rawSetCookie = headers.get("set-cookie");
return rawSetCookie ? [rawSetCookie] : [];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Node.js Fetch API Headers getSetCookie() vs get() Set-Cookie multiple cookies

💡 Result:

  • headers.get('set-cookie') returns one value (a single string), because the Fetch Headers model historically can’t represent multiple Set-Cookie header fields (and Set-Cookie is also treated specially/forbidden in browser-exposed responses). (fetch.spec.whatwg.org)

    • Even if multiple cookies were sent, you must not reliably split that string on commas (cookie Expires= contains commas).
  • headers.getSetCookie() (Node/server-focused) returns an array of all Set-Cookie header values, preserving each cookie header separately (e.g. ["a=1; ...", "b=2; ..."]). (developer.mozilla.org)

  • If you’re using node-fetch (not Node’s built-in fetch), the common way to get all cookies is res.headers.raw()['set-cookie'] (array). (stackoverflow.com)

Citations:


🌐 Web query:

Bun Fetch Headers getSetCookie() multiple Set-Cookie handling

💡 Result:

In Bun (server-side), don’t use headers.get("set-cookie") to read cookies when a response can contain multiple Set-Cookie headers—get() historically can’t represent them correctly because Set-Cookie cannot be safely comma-joined.

Use Headers.getSetCookie(), which returns an array of all Set-Cookie header values:

const res = await fetch(url);
const setCookies: string[] = res.headers.getSetCookie();

This method exists specifically to handle “multiple Set-Cookie headers” properly. [1]

Sending multiple Set-Cookie headers in Bun

If you’re constructing a response and need multiple cookies, emit multiple header values (not a single comma-joined string). Bun’s CookieMap can produce the correct array form via toSetCookieHeaders():

import { CookieMap } from "bun";

const cookies = new CookieMap();
cookies.set("a", "1");
cookies.set("b", "2");

return new Response("ok", {
  headers: {
    "Set-Cookie": cookies.toSetCookieHeaders(), // string[]
  },
});

toSetCookieHeaders() explicitly returns a string[] intended to be used as multiple Set-Cookie header values. [2]

Note: In browsers, Set-Cookie is not exposed to frontend JS via Fetch/Headers; getSetCookie() is mainly for server runtimes. [1]

References: [1] MDN Headers.getSetCookie() docs, [2] Bun CookieMap.toSetCookieHeaders() docs.


🌐 Web query:

HTTP Set-Cookie headers multiple values get() method collapse

💡 Result:

Many HTTP client/server “header maps” treat repeated headers as a single header and collapse them (often by joining values with ,) when you call something like get("Set-Cookie"). That does not work for cookies:

  • Set-Cookie is special: to set multiple cookies, a response must send multiple Set-Cookie header fields, not a single comma-separated value. Servers are advised not to combine multiple Set-Cookie headers because commas are meaningful in cookie syntax (notably in Expires=), so combining can change semantics. [1]
  • In browsers (Fetch/XHR): you generally cannot read Set-Cookie at all—Fetch defines it as a forbidden response header name, so response.headers.get("set-cookie") won’t give you the cookies. The browser handles cookie storage internally. [2]
  • In server-side Fetch (and some runtimes): use the newer Headers.getSetCookie() API to retrieve all Set-Cookie header values as an array, avoiding the collapse problem. [3]

Practical fixes (depending on environment):

  • Don’t combine cookies into one header; emit multiple Set-Cookie lines. [1]
  • If you’re consuming responses in a server environment, prefer getSetCookie() (or a runtime-specific “raw headers” API) instead of get(). [3]

Sources:
[1] RFC 6265 (inline errata) guidance not to combine multiple Set-Cookie headers https://www.rfc-editor.org/rfc/inline-errata/rfc6265.html
[2] MDN: Set-Cookie is not accessible to frontend JS (forbidden response header) https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie
[3] MDN: Headers.getSetCookie() returns all Set-Cookie values (intended for server environments) https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie


🏁 Script executed:

cat -n api/src/app.ts | head -100 | tail -30

Repository: RackulaLives/Rackula

Length of output: 946


🏁 Script executed:

# Check for Node version requirements or runtime targets
fd -e json -e ts -e md | xargs grep -l "engines\|@types/node\|node-version\|Node.js" | head -5 | xargs cat | head -50

Repository: RackulaLives/Rackula

Length of output: 2269


🏁 Script executed:

# Check imports and context around the problematic code
cat -n api/src/app.ts | head -95 | tail -20

Repository: RackulaLives/Rackula

Length of output: 691


🏁 Script executed:

# Check package.json for Node version and dependencies
fd -name package.json | xargs cat | head -60

Repository: RackulaLives/Rackula

Length of output: 294


🏁 Script executed:

# Check if there are comments or documentation about this fallback
grep -B5 -A5 "readSetCookieHeaders\|getSetCookie" api/src/app.ts

Repository: RackulaLives/Rackula

Length of output: 746


🏁 Script executed:

# Check what runtime/framework is being used (looks like Hono based on Context)
rg "import.*Context|from.*hono" api/src/app.ts | head -10

Repository: RackulaLives/Rackula

Length of output: 223


🏁 Script executed:

cat api/package.json

Repository: RackulaLives/Rackula

Length of output: 582


🏁 Script executed:

# Check if there's any documentation or comments about Node version support
grep -r "node\|version\|engine" api/package.json | head -20

Repository: RackulaLives/Rackula

Length of output: 100


🏁 Script executed:

# Check when getSetCookie was added and what Node versions support it
web_search "Node.js getSetCookie() available version added"

Repository: RackulaLives/Rackula

Length of output: 114


🌐 Web query:

Bun Fetch Headers getSetCookie() available since version

💡 Result:

Bun doesn’t appear to document a specific Bun release where Headers.prototype.getSetCookie() was introduced. Bun states it implements the standard Headers Web API (used by fetch) but doesn’t provide per-method “since version” details. [1]

Headers.getSetCookie() itself is a Fetch-standard method (server/runtime-oriented) and is listed by MDN as a “Baseline 2023” API. [2]

Practical approach in Bun: feature-detect and/or pin a minimum Bun version in your project:

const supported = typeof new Headers().getSetCookie === "function";

Sources: [1] [2]


🌐 Web query:

Bun Headers API getSetCookie() support all versions

💡 Result:

Headers.prototype.getSetCookie() (Fetch standard) is supported in Bun starting with Bun v1.0.0 (2023-09-08), so it’s available across all Bun 1.x releases (1.0, 1.1, 1.2, 1.3, etc.). It is not guaranteed in pre-1.0 Bun builds. [1][2]

Refs:
[1] api.Headers.getSetCookie compatibility data (shows Bun v1.0.0+) (bcd-training.deno.dev)
[2] MDN: Headers.getSetCookie() description/spec context (developer.mozilla.org)

Citations:


Remove unnecessary fallback that assumes getSetCookie() unavailability.

The fallback to headers.get("set-cookie") is unreachable given the Bun >= 1.0.0 requirement in engines (where getSetCookie() is available). If executed in an older or non-standard runtime, the branch would collapse multiple cookies into a single value, breaking auth/session handling. Since the target runtime guarantees getSetCookie() support, simplify this to remove the defensive pattern:

function readSetCookieHeaders(headers: Headers | undefined): string[] {
  if (!headers) {
    return [];
  }
  return headers.getSetCookie();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/app.ts` around lines 82 - 90, The current code in the
readSetCookieHeaders path uses a defensive fallback that reads
headers.get("set-cookie") after checking withGetSetCookie.getSetCookie, which is
unnecessary and can collapse multiple cookies; update the implementation to
assume Bun >= 1.0.0 and simply return headers.getSetCookie() (or [] when headers
is undefined). Remove the withGetSetCookie cast, the conditional that checks
getSetCookie, and the rawSetCookie fallback; ensure the function
(readSetCookieHeaders) first returns [] if headers is falsy and otherwise
returns headers.getSetCookie().

Comment on lines +141 to +142
if (!issuedAt || !expiresAt || expiresAt <= issuedAt) {
return null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use explicit null checks for parsed epoch values.

toEpochSeconds() returns null for invalid values; using !issuedAt / !expiresAt also rejects valid 0 timestamps.

🛠️ Proposed fix
-  if (!issuedAt || !expiresAt || expiresAt <= issuedAt) {
+  if (issuedAt === null || expiresAt === null || expiresAt <= issuedAt) {
     return null;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!issuedAt || !expiresAt || expiresAt <= issuedAt) {
return null;
if (issuedAt === null || expiresAt === null || expiresAt <= issuedAt) {
return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/app.ts` around lines 141 - 142, The current guard uses falsy checks
on the parsed epoch values (if (!issuedAt || !expiresAt || expiresAt <=
issuedAt)) which incorrectly treats valid 0 timestamps as invalid; change the
condition to explicit null checks (e.g. issuedAt === null || expiresAt === null
|| expiresAt <= issuedAt) so only null results from toEpochSeconds() are
rejected while allowing 0, and keep the same return null behavior when the check
fails.

ggfevans and others added 6 commits February 26, 2026 22:16
Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com>
- Import EnvMap from security.ts instead of redefining in config.ts
- Add 10s timeout to OIDC discovery fetch via AbortSignal.timeout
- Log warning when OIDC session falls back to generic "oidc-user" subject
- Document MVP-only hardcoded admin role in fallback session mapping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The || "oidc-user" default guarantees fallbackSubject is always truthy,
making the if (!fallbackSubject) guard unreachable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ggfevans ggfevans force-pushed the fix/1335-entra-common-support branch from 7824a83 to 9a5c514 Compare February 27, 2026 06:21
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 27, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XL This PR changes 500-999 lines, ignoring generated files labels Feb 27, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 27, 2026

Sequence Diagram

Shows the main successful OIDC login path where a "common" Entra issuer is accepted even when discovery returns a tenant-specific issuer, the ID token audience is validated, a session cookie is created, and subsequent auth checks use the signed cookie or fallback Better Auth session mapping (with idle-timeout enforcement).

sequenceDiagram
    participant User
    participant App
    participant BetterAuth
    participant OIDC

    User->>App: GET /auth/login (next=/dashboard)
    App->>BetterAuth: signInWithOAuth2(providerId=oidc, callbackURL)
    BetterAuth->>OIDC: Fetch discovery & token exchange (discovery returns tenant-specific issuer)
    OIDC-->>BetterAuth: jwks + id_token (tenant issuer)
    BetterAuth-->>App: return id_token & set-cookie for provider flow
    App->>App: validate id_token (issuer accepted for Entra common→tenant match, audience == client_id)
    App-->>User: Set rackula_auth_session cookie and redirect to next

    User->>App: GET /auth/check (with cookies)
    alt Signed session cookie present & valid
        App-->>User: 204 (refresh cookie if needed)
    else Fallback session available from BetterAuth API
        App->>BetterAuth: getSession(headers) => mapFallbackSessionClaims (use persisted created/expires to enforce idle-timeout)
        alt Fallback valid
            App-->>User: 204 (no silent extension)
        else
            App-->>User: 401 Unauthorized
        end
    end
Loading

Generated by CodeAnt AI

Copy link

@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: 1

♻️ Duplicate comments (1)
api/src/app.ts (1)

77-91: 🧹 Nitpick | 🔵 Trivial

Simplify readSetCookieHeaders to use getSetCookie() directly.

Since the project targets Bun >= 1.0.0 where Headers.getSetCookie() is available, the fallback to headers.get("set-cookie") is unnecessary and could incorrectly collapse multiple cookies into a single string if executed.

♻️ Proposed simplification
 function readSetCookieHeaders(headers: Headers | undefined): string[] {
   if (!headers) {
     return [];
   }
-
-  const withGetSetCookie = headers as Headers & {
-    getSetCookie?: () => string[];
-  };
-  if (typeof withGetSetCookie.getSetCookie === "function") {
-    return withGetSetCookie.getSetCookie();
-  }
-
-  const rawSetCookie = headers.get("set-cookie");
-  return rawSetCookie ? [rawSetCookie] : [];
+  return headers.getSetCookie();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/app.ts` around lines 77 - 91, The readSetCookieHeaders function
currently falls back to headers.get("set-cookie") which can collapse multiple
cookies; since Bun >= 1.0.0 provides Headers.getSetCookie(), simplify the
function (readSetCookieHeaders) to check for headers presence and directly call
and return headers.getSetCookie(), returning an empty array if headers is
undefined; remove the custom type cast and the get("set-cookie") fallback so
multiple Set-Cookie values are preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/oidc.integration.test.ts`:
- Around line 32-45: The readSetCookies function can be simplified to call
Headers.getSetCookie() directly: replace the current implementation (including
the cast to withGetSetCookie, try/catch, and the headers.get("set-cookie")
fallback) with a single direct call to headers.getSetCookie() and return its
result (ensuring the return type is string[]); update the function
signature/typing only if necessary to reflect that getSetCookie exists on
Headers in Bun >=1.0.0.

---

Duplicate comments:
In `@api/src/app.ts`:
- Around line 77-91: The readSetCookieHeaders function currently falls back to
headers.get("set-cookie") which can collapse multiple cookies; since Bun >=
1.0.0 provides Headers.getSetCookie(), simplify the function
(readSetCookieHeaders) to check for headers presence and directly call and
return headers.getSetCookie(), returning an empty array if headers is undefined;
remove the custom type cast and the get("set-cookie") fallback so multiple
Set-Cookie values are preserved.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7824a83 and 9a5c514.

📒 Files selected for processing (6)
  • api/.env.example
  • api/src/app.ts
  • api/src/auth/config.ts
  • api/src/oidc.integration.test.ts
  • api/src/security.ts
  • docs/deployment/AUTHENTICATION.md

Comment on lines +32 to +45
function readSetCookies(headers: Headers): string[] {
const withGetSetCookie = headers as Headers & { getSetCookie: () => string[] };
try {
const setCookies = withGetSetCookie.getSetCookie();
if (Array.isArray(setCookies)) {
return setCookies;
}
} catch {
// Fall through to standard header handling.
}

const raw = headers.get("set-cookie");
return raw ? [raw] : [];
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Simplify readSetCookies to use getSetCookie() directly.

Since the project targets Bun >= 1.0.0 where Headers.getSetCookie() is available, the fallback to headers.get("set-cookie") is unnecessary. The fallback branch could also incorrectly handle multiple cookies by collapsing them into a single string.

♻️ Proposed simplification
 function readSetCookies(headers: Headers): string[] {
-  const withGetSetCookie = headers as Headers & { getSetCookie: () => string[] };
-  try {
-    const setCookies = withGetSetCookie.getSetCookie();
-    if (Array.isArray(setCookies)) {
-      return setCookies;
-    }
-  } catch {
-    // Fall through to standard header handling.
-  }
-
-  const raw = headers.get("set-cookie");
-  return raw ? [raw] : [];
+  return headers.getSetCookie();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/oidc.integration.test.ts` around lines 32 - 45, The readSetCookies
function can be simplified to call Headers.getSetCookie() directly: replace the
current implementation (including the cast to withGetSetCookie, try/catch, and
the headers.get("set-cookie") fallback) with a single direct call to
headers.getSetCookie() and return its result (ensuring the return type is
string[]); update the function signature/typing only if necessary to reflect
that getSetCookie exists on Headers in Bun >=1.0.0.

@ggfevans ggfevans merged commit 75b9afe into main Feb 27, 2026
10 checks passed
@ggfevans ggfevans deleted the fix/1335-entra-common-support branch February 27, 2026 06:27
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 27, 2026

CodeAnt AI Incremental review completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: resolve Entra common issuer validation mismatch in OIDC flow

1 participant