Skip to content

feat(auth): auto-redirect login to the DOS ID provider (single-IdP SSO) - #78

Merged
JOY (JOY) merged 1 commit into
devfrom
feat/dos-id-auto-sso
Sep 18, 2026
Merged

JOY (JOY) merged 1 commit into
devfrom
feat/dos-id-auto-sso

Conversation

@JOY

Copy link
Copy Markdown

What

PoC of the single-IdP SSO pattern discussed with JOY: instead of maintaining a customized login page per forked app, unauthenticated visits to /auth/login (and the legacy /login alias) now hop to a tiny /auth/sso bridge page that immediately starts the next-auth dos-id sign-in. Users go straight to id.dos.me for sign-in and sign-up; CSRF/state/nonce stay owned by next-auth, nothing hand-rolled.

  • Middleware gate mirrors provider registration - the redirect only happens when OIDC_CLIENT_ID + secret are actually configured (same condition as DosIdProvider registration), so unconfigured environments keep the classic form and no redirect loop to an unregistered provider is possible.
  • Break-glass: /auth/login?direct=1 always shows the classic form (magic-link email + configured providers) for when the IdP is unreachable or an admin needs local sign-in.
  • No logout loop: the logout page keeps its explicit button; an IdP session never silently logs a user back in without a click.
  • callbackUrl forwarding preserved through the redirect chain.

Evidence

  • 29/29 proxy tests pass (6 new: redirect, callbackUrl forwarding, direct=1 hatch, unconfigured case, non-login paths, /login alias); CSP tests isolated from local env.
  • tsc clean on apps/web; biome warnings on changed lines: none (the one unused-variable warning is pre-existing code).

Follow-ups (separate PRs)

  • Replicate the pattern in Crove CRM / Desk / other forks.
  • Optionally retire the fork's login-view customizations once this proves out in production.

🤖 Generated by ZCode

When the dos-id provider is configured (same OIDC_CLIENT_ID/SECRET gate
as the provider registration in next-auth-options), the middleware now
sends /auth/login and /login to a small /auth/sso bridge page that
immediately starts the next-auth dos-id sign-in. CSRF, state and the
IdP redirect all stay owned by next-auth - nothing is hand-rolled.

The classic login form remains the break-glass path at
/auth/login?direct=1 (magic-link email + all configured providers) for
when the identity provider is unreachable or an admin needs a local
sign-in. The logout page keeps its explicit 'go back' button, so an
IdP session can never silently log a user back in without a click.

In unconfigured environments (no OIDC credentials) behavior is
unchanged - the middleware gate mirrors the provider registration
exactly, and 6 new proxy tests cover redirect, callbackUrl forwarding,
the direct=1 escape hatch, the unconfigured case and the /login alias.
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 5f933789-8134-4cf3-bfa9-fc7eb8ae8aed

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@JOY
JOY (JOY) merged commit 7e3f31c into dev Sep 18, 2026
10 checks passed
@JOY
JOY (JOY) deleted the feat/dos-id-auto-sso branch September 18, 2026 18:38

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces an automatic SSO redirect flow for DOS ID, routing users from the login page directly to a new SSO bridge page (/auth/sso) that triggers the NextAuth sign-in. Feedback on these changes highlights a potential infinite redirect loop if authentication fails and returns an error parameter, which can be resolved by checking for the error parameter in the middleware. Additionally, to prevent duplicate sign-in attempts and state mismatch errors in React 18 Strict Mode, it is recommended to use a useRef to ensure the sign-in effect runs only once, along with adding corresponding test coverage.

Comment thread apps/web/proxy.ts
Comment on lines +82 to +83
export const shouldRedirectToDosIdSso = (url: URL) =>
isDosIdProviderConfigured() && isLoginPath(url) && url.searchParams.get("direct") !== "1";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Infinite Redirect Loop on Authentication Failure

If the DOS ID authentication flow fails, NextAuth typically redirects the user back to the login page with an error query parameter (e.g., /auth/login?error=OAuthSignin).

Without checking for the presence of the error parameter, the middleware will immediately redirect the user back to /auth/sso, which triggers another signIn attempt, leading to an infinite redirect loop.

We should check for !url.searchParams.has("error") before redirecting to the SSO bridge.

export const shouldRedirectToDosIdSso = (url: URL) =>
  isDosIdProviderConfigured() &&
  isLoginPath(url) &&
  url.searchParams.get("direct") !== "1" &&
  !url.searchParams.has("error");

Comment on lines +28 to +36
useEffect(() => {
let cancelled = false;
signIn("dos-id", { callbackUrl, redirect: true }).catch(() => {
if (!cancelled) setFailed(true);
});
return () => {
cancelled = true;
};
}, [callbackUrl]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Prevent Duplicate signIn Calls in Strict Mode

In React 18 Strict Mode (and potentially during fast re-renders), useEffect runs twice on mount. Calling signIn concurrently twice can overwrite the NextAuth state/nonce cookies, leading to state mismatch errors (OAuthCallback errors) when the user returns from the IdP.

Using a useRef to ensure signIn is only called once solves this issue.

  const signInAttempted = useRef(false);

  useEffect(() => {
    if (signInAttempted.current) return;
    signInAttempted.current = true;

    let cancelled = false;
    signIn("dos-id", { callbackUrl, redirect: true }).catch(() => {
      if (!cancelled) setFailed(true);
    });
    return () => {
      cancelled = true;
    };
  }, [callbackUrl]);


import { signIn } from "next-auth/react";
import type { ParsedUrlQuery } from "node:querystring";
import { useEffect, useState } from "react";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Import useRef from React to track whether the sign-in flow has already been initiated, preventing duplicate execution in Strict Mode.

Suggested change
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";

Comment thread apps/web/proxy.test.ts
Comment on lines +478 to +483
it("keeps the classic form for ?direct=1 (break-glass)", async () => {
const req = createTestRequest({ url: `${WEBAPP_URL}/auth/login?direct=1` });

const res = await callProxy(req);
expect(getHeader(res, "x-middleware-next")).toBe("1");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add a test case to verify that the middleware does not redirect to the SSO bridge when an error parameter is present in the URL, preventing infinite redirect loops.

  it("keeps the classic form for ?direct=1 (break-glass)", async () => {
    const req = createTestRequest({ url: `${WEBAPP_URL}/auth/login?direct=1` });

    const res = await callProxy(req);
    expect(getHeader(res, "x-middleware-next")).toBe("1");
  });

  it("does not redirect when an error parameter is present", async () => {
    const req = createTestRequest({ url: `${WEBAPP_URL}/auth/login?error=OAuthSignin` });

    const res = await callProxy(req);
    expect(getHeader(res, "x-middleware-next")).toBe("1");
  });

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.

1 participant