<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: cio fea</title>
    <description>The latest articles on DEV Community by cio fea (@ciofea).</description>
    <link>https://dev.to/ciofea</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4186470%2F6a6014cf-c752-49be-873a-0dfbea8187f3.jpg</url>
      <title>DEV Community: cio fea</title>
      <link>https://dev.to/ciofea</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ciofea"/>
    <language>en</language>
    <item>
      <title>How do I make NextAuth work with multiple domains?</title>
      <dc:creator>cio fea</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:41:19 +0000</pubDate>
      <link>https://dev.to/ciofea/how-do-i-make-nextauth-work-with-multiple-domains-3fcp</link>
      <guid>https://dev.to/ciofea/how-do-i-make-nextauth-work-with-multiple-domains-3fcp</guid>
      <description>&lt;p&gt;&lt;em&gt;I’m building &lt;a href="https://loginos.io" rel="noopener noreferrer"&gt;loginos&lt;/a&gt;, a sign-in service for products that run on more than one domain. This question comes up a lot, so here is the full answer: the general approach first, then how loginos handles it.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Leave AUTH_URL and NEXTAUTH_URL unset and turn on trustHost, so Auth.js builds its URLs from the host of each request instead of one fixed domain. Each domain then keeps its own session cookie, and your identity provider must accept the Auth.js callback URL on every domain. With loginos, someone signed in on one domain comes back on the others without a form.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You run one Next.js app on several domains, like &lt;code&gt;app.example.com&lt;/code&gt;, &lt;code&gt;example.net&lt;/code&gt; and domains your customers connect. NextAuth.js, now called Auth.js, signs people in on one of them and misbehaves on the rest: it sends them back to the wrong host, or stops with an &lt;code&gt;UntrustedHost&lt;/code&gt; error. One deployment can serve every domain. Auth.js has to build its URLs from each request, each domain keeps its own session, and your identity provider has to accept a callback on every domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Auth.js decide which URL to use?
&lt;/h2&gt;

&lt;p&gt;Auth.js needs your app’s own address to build the sign-in redirect, the &lt;code&gt;redirect_uri&lt;/code&gt; it sends to the provider, and the page people land on afterwards. Auth.js v5, which you install as &lt;code&gt;next-auth@beta&lt;/code&gt;, finds that address in one of three ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your setup&lt;/th&gt;
&lt;th&gt;Address Auth.js uses&lt;/th&gt;
&lt;th&gt;Works on many domains?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AUTH_URL&lt;/code&gt; or &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; is set&lt;/td&gt;
&lt;td&gt;That URL, for every request&lt;/td&gt;
&lt;td&gt;No: every callback goes to one domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neither is set, and the host is trusted&lt;/td&gt;
&lt;td&gt;The host of each request, from &lt;code&gt;X-Forwarded-Host&lt;/code&gt; or &lt;code&gt;Host&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neither is set, the host isn’t trusted, in production&lt;/td&gt;
&lt;td&gt;None: Auth.js stops with &lt;code&gt;UntrustedHost&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;AUTH_URL&lt;/code&gt;, and its older name &lt;code&gt;NEXTAUTH_URL&lt;/code&gt;, pin everything to one domain: next-auth rewrites the origin of each incoming request to that URL. A sign-in that starts on &lt;code&gt;example.net&lt;/code&gt; then tries to come back to &lt;code&gt;app.example.com&lt;/code&gt;. For several domains, remove both variables from every environment.&lt;/p&gt;

&lt;p&gt;The host counts as trusted when you set &lt;code&gt;trustHost: true&lt;/code&gt; in your config or &lt;code&gt;AUTH_TRUST_HOST=true&lt;/code&gt; in the environment. Auth.js also trusts it by itself on Vercel and Cloudflare Pages, and whenever &lt;code&gt;NODE_ENV&lt;/code&gt; isn’t &lt;code&gt;production&lt;/code&gt;. That last rule is why a multi-domain setup can work on your laptop and fail after you deploy.&lt;/p&gt;

&lt;p&gt;Trusting the host is safe when your hosting platform or reverse proxy sets these headers itself, instead of passing on whatever a client sends. The &lt;a href="https://authjs.dev/getting-started/deployment" rel="noopener noreferrer"&gt;Auth.js deployment guide&lt;/a&gt; covers the setting. Your provider’s callback allow list is a second check, because a forged host only produces a callback URL that the provider refuses.&lt;/p&gt;

&lt;h3&gt;
  
  
  On a self-hosted server, fix the route handler URL
&lt;/h3&gt;

&lt;p&gt;Server actions and &lt;code&gt;auth()&lt;/code&gt; read the host from the headers, but the Auth.js route handlers use the request’s URL. A self-hosted Next.js server started with &lt;code&gt;next start&lt;/code&gt; reports that URL as an internal address, like &lt;code&gt;http://localhost:3000&lt;/code&gt;, whatever domain the visitor used. The callback then sends the wrong &lt;code&gt;redirect_uri&lt;/code&gt; with the code, and the exchange fails. Rebuild the URL from the forwarded host before Auth.js sees it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// Give Auth.js the URL the visitor used, not the server's internal address.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPublicHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-forwarded-host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-forwarded-proto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onPublicHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onPublicHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the URL is already right, the wrapper changes nothing. On Vercel, for example, &lt;code&gt;X-Forwarded-Host&lt;/code&gt; is the same as the &lt;code&gt;Host&lt;/code&gt; the visitor used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does each domain get its own session?
&lt;/h2&gt;

&lt;p&gt;Auth.js keeps the session in a cookie named &lt;code&gt;authjs.session-token&lt;/code&gt;, or &lt;code&gt;__Secure-authjs.session-token&lt;/code&gt; on HTTPS. Auth.js sets that cookie without a &lt;code&gt;Domain&lt;/code&gt; attribute, so the browser sends it back only to the exact host that set it. A session on &lt;code&gt;app.example.com&lt;/code&gt; is invisible to &lt;code&gt;example.net&lt;/code&gt;, because browsers never send one domain’s cookies to another.&lt;/p&gt;

&lt;p&gt;So “NextAuth on multiple domains” means that each domain signs the person in on its own. The default &lt;code&gt;redirect&lt;/code&gt; callback also only allows redirects to the origin of the current request, so someone who starts on &lt;code&gt;example.net&lt;/code&gt; ends on &lt;code&gt;example.net&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What makes this painless is the identity provider’s own session. When the second domain sends someone to the provider, the provider recognizes them and sends them straight back with a code, without a form. The answer on &lt;a href="https://loginos.io/answers/share-login-session-across-domains" rel="noopener noreferrer"&gt;sharing a login session across domains&lt;/a&gt; walks through that flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which callback URLs must the provider accept?
&lt;/h2&gt;

&lt;p&gt;Auth.js sends the provider &lt;code&gt;redirect_uri=https://&amp;lt;current host&amp;gt;/api/auth/callback/&amp;lt;provider id&amp;gt;&lt;/code&gt;. For a provider with the id &lt;code&gt;loginos&lt;/code&gt;, that is &lt;code&gt;https://example.net/api/auth/callback/loginos&lt;/code&gt; on &lt;code&gt;example.net&lt;/code&gt;. The provider must accept that exact URL on every domain, or it refuses the sign-in.&lt;/p&gt;

&lt;p&gt;You have three ways to get there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;List every callback URL&lt;/strong&gt; in the provider’s settings. That’s fine for two or three domains, and a chore once customers bring their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Auth.js redirect proxy.&lt;/strong&gt; With &lt;code&gt;AUTH_REDIRECT_PROXY_URL&lt;/code&gt;, one stable deployment receives every callback and forwards it to the URL that started the sign-in. Auth.js built it for preview deployments and calls it an option for advanced cases. Every deployment must share the same &lt;code&gt;AUTH_SECRET&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a provider built for many domains&lt;/strong&gt;, with an API for adding them and wildcards for subdomains. Wildcards have their own rules: see &lt;a href="https://loginos.io/answers/wildcard-redirect-uri-oauth" rel="noopener noreferrer"&gt;wildcard redirect URIs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How loginos does it
&lt;/h2&gt;

&lt;p&gt;loginos is a hosted OpenID Connect provider for one product on many domains. A platform has no limit on domains, and they all share one user pool, so you never maintain a list of callback URLs by hand.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a platform in the &lt;a href="https://loginos.io/developer" rel="noopener noreferrer"&gt;developer console&lt;/a&gt;, with a name and your first domain. Copy the app secret: it is shown once.&lt;/li&gt;
&lt;li&gt;In the console, set the platform’s callback path to &lt;code&gt;/api/auth/callback/loginos&lt;/code&gt;. The path is the same on every domain, and loginos only redirects to &lt;code&gt;https://&amp;lt;one of your domains&amp;gt;&amp;lt;callback path&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add your other domains in the console, &lt;a href="https://loginos.io/docs/domains#from-your-server" rel="noopener noreferrer"&gt;from your server&lt;/a&gt;, as a verified wildcard like &lt;code&gt;*.example.com&lt;/code&gt;, or as &lt;a href="https://loginos.io/docs/domains#automatic-domains" rel="noopener noreferrer"&gt;automatic domains&lt;/a&gt; that your server confirms the first time they’re used.&lt;/li&gt;
&lt;li&gt;Add loginos to Auth.js as an OpenID Connect provider, and copy &lt;code&gt;roles&lt;/code&gt; and &lt;code&gt;permissions&lt;/code&gt; from the ID token into the session:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NextAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DefaultSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;DefaultSession&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signOut&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NextAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;trustHost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// use each request's host; don't set AUTH_URL&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loginos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// callback: /api/auth/callback/loginos&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loginos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oidc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://loginos.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOGINOS_APP_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOGINOS_APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pkce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Only at sign-in: profile holds the claims of the loginos ID token.&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;providerAccountId&lt;/span&gt; &lt;span class="c1"&gt;// the loginos sub, the same on every domain&lt;/span&gt;
        &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;
        &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
      &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth.js reads the endpoints from &lt;code&gt;https://loginos.io/.well-known/openid-configuration&lt;/code&gt;, sends PKCE and &lt;code&gt;state&lt;/code&gt;, and checks the ID token. The &lt;code&gt;jwt&lt;/code&gt; callback copies the loginos &lt;code&gt;sub&lt;/code&gt; on purpose: without a database adapter, Auth.js gives every sign-in a new random user id. Key your own records on &lt;code&gt;session.user.id&lt;/code&gt;, never on the email address, because loginos doesn’t verify email addresses yet. Tokens carry the roles someone had when they signed in, and &lt;a href="https://loginos.io/docs/roles" rel="noopener noreferrer"&gt;roles and permissions&lt;/a&gt; shows how to read the current ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens on the second domain
&lt;/h3&gt;

&lt;p&gt;Someone who signed in on &lt;code&gt;app.example.com&lt;/code&gt; opens &lt;code&gt;example.net&lt;/code&gt; for the first time. Auth.js on &lt;code&gt;example.net&lt;/code&gt; has no session cookie for them yet.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They click your sign-in button, which calls &lt;code&gt;signIn("loginos")&lt;/code&gt; in a server action.&lt;/li&gt;
&lt;li&gt;Auth.js redirects to loginos with &lt;code&gt;redirect_uri=https://example.net/api/auth/callback/loginos&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;loginos finds its own session and sees that the person already uses your platform, so it sends them straight back with a code. There is no form.&lt;/li&gt;
&lt;li&gt;Auth.js exchanges the code on your server and sets the session cookie for &lt;code&gt;example.net&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; To check without ever showing a page, pass &lt;code&gt;prompt: "none"&lt;/code&gt; as the third argument: &lt;code&gt;signIn("loginos", { redirectTo: "/" }, { prompt: "none" })&lt;/code&gt;. Someone who isn’t signed in to loginos comes back with &lt;code&gt;error=login_required&lt;/code&gt;, which Auth.js turns into &lt;code&gt;?error=OAuthCallbackError&lt;/code&gt; on your sign-in page.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Auth.js is optional. The &lt;a href="https://loginos.io/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; has a Next.js version with two plain route handlers, one that redirects and one callback, on the default callback path. On a self-hosted server, build their callback URL from the forwarded host as well. Plain handlers also leave room for loginos features that need your own requests, like account linking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a separate deployment for each domain?
&lt;/h3&gt;

&lt;p&gt;No. One Next.js deployment can answer on every domain you point at it. Remove &lt;code&gt;AUTH_URL&lt;/code&gt; and &lt;code&gt;NEXTAUTH_URL&lt;/code&gt;, trust the host, and make sure the route handlers see the public URL. Then add each domain to your hosting and to your identity provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can NextAuth share one session between example.com and example.net?
&lt;/h3&gt;

&lt;p&gt;No. Auth.js stores the session in a cookie for the host that set it, and browsers never send one domain’s cookies to another. Each domain gets its own session. A central identity provider turns the second sign-in into a quick redirect instead of a form.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is trustHost: true safe in production?
&lt;/h3&gt;

&lt;p&gt;Yes, if your hosting platform or reverse proxy sets the &lt;code&gt;Host&lt;/code&gt; and &lt;code&gt;X-Forwarded-Host&lt;/code&gt; headers itself, instead of passing on what the client sent. Your provider’s callback allow list is a second check: a forged host only produces a callback URL that the provider refuses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this work with NextAuth v4?
&lt;/h3&gt;

&lt;p&gt;Yes. &lt;code&gt;next-auth@latest&lt;/code&gt; still installs v4. Leave &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; unset and set &lt;code&gt;AUTH_TRUST_HOST=true&lt;/code&gt;, and v4 builds its origin from &lt;code&gt;X-Forwarded-Host&lt;/code&gt; or &lt;code&gt;Host&lt;/code&gt; on every request. The callback path is the same, but v4 writes a custom OpenID Connect provider as &lt;code&gt;type: "oauth"&lt;/code&gt; with a &lt;code&gt;wellKnown&lt;/code&gt; discovery URL and &lt;code&gt;idToken: true&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is NextAuth still maintained?
&lt;/h3&gt;

&lt;p&gt;Yes, with a narrower focus. Since September 2025 the Better Auth team maintains Auth.js. Its announcement says existing users can keep using it and will get security patches, and it recommends Better Auth for new projects. Version 5 still ships under the &lt;code&gt;beta&lt;/code&gt; tag on npm: &lt;code&gt;5.0.0-beta.32&lt;/code&gt; came out in July 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try loginos
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://loginos.io" rel="noopener noreferrer"&gt;loginos&lt;/a&gt; is an OpenID Connect provider that gives every domain of your product one shared sign-in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any number of domains, one user pool.&lt;/strong&gt; Add them in the console or from your server, cover subdomains with a verified wildcard, or let your server approve new ones as people sign in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkeys built in.&lt;/strong&gt; Face ID, fingerprints and security keys, with passwords as a fallback, and no WebAuthn code in your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles and permissions in every token&lt;/strong&gt;, the same on every domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the users you have.&lt;/strong&gt; People link their existing account once, and every token carries your own user id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Continue as” on your own page&lt;/strong&gt; in Chrome and Edge, with a small loginos window everywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard OpenID Connect&lt;/strong&gt;, so Auth.js, Passport or any OIDC library works. There are no paid plans today, and you can start without a card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://loginos.io/register" rel="noopener noreferrer"&gt;Create a platform&lt;/a&gt; in a few minutes, follow the &lt;a href="https://loginos.io/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or read &lt;a href="https://loginos.io/answers" rel="noopener noreferrer"&gt;more answers like this one&lt;/a&gt;. This answer is kept up to date at &lt;a href="https://loginos.io/answers/nextauth-multiple-domains" rel="noopener noreferrer"&gt;loginos.io/answers&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>Can I use wildcard redirect URIs with OAuth?</title>
      <dc:creator>cio fea</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:41:03 +0000</pubDate>
      <link>https://dev.to/ciofea/can-i-use-wildcard-redirect-uris-with-oauth-2a64</link>
      <guid>https://dev.to/ciofea/can-i-use-wildcard-redirect-uris-with-oauth-2a64</guid>
      <description>&lt;p&gt;&lt;em&gt;I’m building &lt;a href="https://loginos.io" rel="noopener noreferrer"&gt;loginos&lt;/a&gt;, a sign-in service for products that run on more than one domain. This question comes up a lot, so here is the full answer: the general approach first, then how loginos handles it.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Usually not, and you shouldn’t rely on them. RFC 9700, the OAuth 2.0 Security Best Current Practice, requires authorization servers to match redirect URIs exactly, because loose patterns have let attackers steal authorization codes. Google rejects wildcards, while Auth0 and Okta allow narrow ones with warnings. Register exact URIs through an API instead, or use a provider that verifies wildcard ownership or asks your server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You run many subdomains, or your customers bring their own domains, and registering a callback URL for each one by hand doesn’t scale. A pattern like &lt;code&gt;https://*.example.com/callback&lt;/code&gt; looks like the obvious fix. The OAuth specs say no: a redirect URI should match a registered value exactly, character for character. Some providers still accept wildcards, with restrictions and warnings, and there are safer ways to get the same result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the OAuth specs say about wildcard redirect URIs?
&lt;/h2&gt;

&lt;p&gt;A redirect URI is the address where the authorization server sends the browser back after sign-in, with an authorization code. The code is what your app trades for tokens, so the redirect URI decides who gets it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RFC 6749&lt;/strong&gt;, the OAuth 2.0 framework from 2012, asked servers to require the complete redirect URI, but allowed registering only part of it when that wasn’t possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9700#section-2.1" rel="noopener noreferrer"&gt;RFC 9700&lt;/a&gt;&lt;/strong&gt;, the OAuth 2.0 Security Best Current Practice published in January 2025, updates RFC 6749 and closes that gap. Section 2.1 says authorization servers “MUST utilize exact string matching” when they compare redirect URIs. The only exception is the port of &lt;code&gt;localhost&lt;/code&gt; redirect URIs in native apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenID Connect Core 1.0&lt;/strong&gt; requires the &lt;code&gt;redirect_uri&lt;/code&gt; to exactly match a pre-registered value, compared as plain strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth 2.1&lt;/strong&gt;, still an Internet-Draft in October 2026, requires exact matching as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exact means exact: same scheme, host, port, path and query. &lt;code&gt;https://app.example.com/callback&lt;/code&gt; and &lt;code&gt;https://app.example.com/callback/&lt;/code&gt; are two different URIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are wildcard redirect URIs dangerous?
&lt;/h2&gt;

&lt;p&gt;Your client ID is public, so anyone can build an authorization request for your app with a redirect URI of their choice. The server’s matching is the only thing that decides where the code goes. RFC 9700 describes three ways patterns fail:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Matching bugs.&lt;/strong&gt; RFC 9700’s example registers &lt;code&gt;https://*.somesite.example/*&lt;/code&gt;. A naive server reads &lt;code&gt;*&lt;/code&gt; as “any characters” and accepts &lt;code&gt;https://attacker.example/.somesite.example&lt;/code&gt;, a different domain entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subdomain takeover.&lt;/strong&gt; Even correct matching trusts every subdomain. Subdomain takeover is when an attacker gains control of one of your subdomains, typically through a stale CNAME record that still points at a cloud service you stopped using. Once they claim that service, the wildcard hands them your users’ codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open redirects.&lt;/strong&gt; An open redirect is a URL on your site that forwards the browser to any address given in a parameter. A broad pattern can match such a URL, which then carries the sign-in response off your site. RFC 9700 forbids clients from exposing open redirectors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What leaks is an authorization code, or a token in older flows that return one directly. A code alone isn’t enough for a confidential client, since the exchange needs your client secret. But an attacker can try to inject the stolen code into your own callback. PKCE (Proof Key for Code Exchange), which binds each code to a secret held by whoever started the sign-in, blocks that. Exact matching plus PKCE is the baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which providers allow wildcard redirect URIs?
&lt;/h2&gt;

&lt;p&gt;Rules differ, and they change, so check your provider’s current docs. As of October 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Wildcards in redirect URIs&lt;/th&gt;
&lt;th&gt;Limits and warnings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Redirect URIs can’t contain &lt;code&gt;*&lt;/code&gt;, and &lt;code&gt;redirect_uri&lt;/code&gt; must exactly match an authorized URI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Entra ID&lt;/td&gt;
&lt;td&gt;Only for apps that sign in work or school accounts alone, added in the manifest editor&lt;/td&gt;
&lt;td&gt;Not for apps that also sign in personal Microsoft accounts. Microsoft says to avoid them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth0&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;*&lt;/code&gt; in the subdomain furthest from the root&lt;/td&gt;
&lt;td&gt;Matches one level only. Auth0 says not to use them in production and suggests &lt;code&gt;{organization_name}&lt;/code&gt; placeholders.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Okta&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;*&lt;/code&gt; in the lowest-level subdomain, once turned on per app&lt;/td&gt;
&lt;td&gt;HTTPS only, not in paths or logout URIs. Okta calls wildcard subdomains an insecure practice.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keycloak&lt;/td&gt;
&lt;td&gt;At the end of a URL pattern, like &lt;code&gt;https://app.example.com/*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A lone &lt;code&gt;*&lt;/code&gt; allows any URL; the docs say not to use it in production.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub OAuth apps&lt;/td&gt;
&lt;td&gt;Optional wildcard matching per callback URL&lt;/td&gt;
&lt;td&gt;Any subdomain or subdirectory of the callback URL matches. GitHub warns that attackers can then receive codes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On GitHub, an OAuth app that had a single callback URL before August 3, 2026 has wildcard matching turned on for it, and GitHub recommends turning it off if you don’t need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can you do instead of a wildcard?
&lt;/h2&gt;

&lt;p&gt;Four approaches give you many domains without a loose pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the parent domain first
&lt;/h3&gt;

&lt;p&gt;A wildcard is safer when the provider accepts it only after you prove control of the parent domain, usually with a DNS TXT record, and never lets it cover the parent domain itself. That rules out typos and patterns over domains you don’t control. The subdomains are still only as safe as your DNS, so remove records for services you no longer use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Register exact URIs through an API
&lt;/h3&gt;

&lt;p&gt;Auth0, Microsoft Entra ID and Amazon Cognito, among others, let your server change a client’s redirect URIs through a management API. Call it when a domain goes live and again when it goes away. Watch the caps. Auth0 allows 100 callback URLs per application and Amazon Cognito 100 per app client. Microsoft Entra ID allows 256 redirect URIs per app, or 100 if the app also signs in personal Microsoft accounts. &lt;a href="https://loginos.io/answers/add-redirect-uri-automatically" rel="noopener noreferrer"&gt;How to add a redirect URI automatically&lt;/a&gt; has the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  One fixed callback that redirects onward
&lt;/h3&gt;

&lt;p&gt;Register a single callback, like &lt;code&gt;https://login.example.com/callback&lt;/code&gt;, and carry a reference to the return address in &lt;code&gt;state&lt;/code&gt;. After the code exchange, the callback sends the person on to the right domain. Microsoft documents this pattern as its alternative to wildcards. It has two catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The onward redirect is an open redirect unless you check the target against your own list. Keep the target itself out of &lt;code&gt;state&lt;/code&gt;; Microsoft suggests a key that points to data you stored.&lt;/li&gt;
&lt;li&gt;The session starts on the login domain. Subdomains can share a cookie set for the parent domain, but a customer’s own domain can’t, so it needs a one-time hand-off token: a small identity provider you now maintain. &lt;a href="https://loginos.io/answers/saas-custom-domain-authentication" rel="noopener noreferrer"&gt;Authentication on customers’ custom domains&lt;/a&gt; compares the two designs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Let your server confirm new domains
&lt;/h3&gt;

&lt;p&gt;In this approach, the provider asks your server about each new domain the first time a sign-in comes from it, and your server answers from its own records. Nothing is trusted because of how a name looks.&lt;/p&gt;

&lt;p&gt;RFC 9126, the spec for pushed authorization requests (PAR), has a related idea. When a client sends its request to the server first, with authentication, the server may accept a redirect URI that was never registered. That is optional, so check whether your provider does it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How loginos does it
&lt;/h2&gt;

&lt;p&gt;In loginos, the callback path is the same on every domain, so you register domains, not URLs. loginos only sends people back to &lt;code&gt;https://&amp;lt;one of your domains&amp;gt;&amp;lt;callback path&amp;gt;&lt;/code&gt;, exactly, with no query string. Three things cover many domains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A verified wildcard.&lt;/strong&gt; &lt;code&gt;*.example.com&lt;/code&gt; covers every subdomain of example.com at any depth, but never example.com itself. You prove ownership once with a TXT record, and until loginos finds it, the wildcard matches nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact hosts from your server.&lt;/strong&gt; &lt;code&gt;POST /api/v1/domains&lt;/code&gt; takes up to 1,000 hosts at once and is safe to repeat, and &lt;code&gt;DELETE /api/v1/domains/{host}&lt;/code&gt; removes one. A platform has no limit on domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic domains.&lt;/strong&gt; loginos asks your server about an unknown host the first time someone signs in there, and adds it only if your server says yes. See &lt;a href="https://loginos.io/docs/domains#automatic-domains" rel="noopener noreferrer"&gt;automatic domains&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The record that proves you own example.com:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Type   TXT
Name   _loginos.example.com
Value  loginos-verify=&amp;lt;the value the console or the API gives you&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the wildcard in the console’s &lt;strong&gt;Domains&lt;/strong&gt; tab or from your server, then check the record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://loginos.io/api/v1/domains &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGINOS_APP_KEY&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$LOGINOS_APP_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"host": "*.example.com"}'&lt;/span&gt;   &lt;span class="c"&gt;# the answer includes the TXT record&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://loginos.io/api/v1/domains/*.example.com/verify"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGINOS_APP_KEY&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$LOGINOS_APP_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pushed requests get no shortcut: loginos applies the same domain check to &lt;a href="https://loginos.io/docs/oidc#post-oauth-par" rel="noopener noreferrer"&gt;&lt;code&gt;/oauth/par&lt;/code&gt;&lt;/a&gt; as to the redirect flow. A wildcard can’t cover your customers’ own domains, so register those by name or confirm them with automatic domains. &lt;a href="https://loginos.io/docs/domains#wildcards" rel="noopener noreferrer"&gt;Domains&lt;/a&gt; has the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does RFC 6749 allow wildcard redirect URIs?
&lt;/h3&gt;

&lt;p&gt;RFC 6749 left room for them. It asked servers to require the complete redirect URI. As a fallback, it allowed registering only part of it, such as everything except the query string, and some servers went further and accepted patterns. RFC 9700 updates RFC 6749 and requires exact string matching, so a server that follows current best practice doesn’t accept wildcards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I vary the path or query string of a redirect URI?
&lt;/h3&gt;

&lt;p&gt;Not with exact matching. Put per-request data in &lt;code&gt;state&lt;/code&gt;, which the server returns untouched, or keep it in your session and put only a random key in &lt;code&gt;state&lt;/code&gt;. RFC 6749 already suggested &lt;code&gt;state&lt;/code&gt; for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need wildcards for localhost?
&lt;/h3&gt;

&lt;p&gt;No. For native apps that listen on a loopback address, RFC 9700 makes servers accept any port, so the app can pick a free one. For web development, register your local address exactly, like &lt;code&gt;http://localhost:3001/callback&lt;/code&gt;, next to your production URIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a wildcard safe if I own every subdomain?
&lt;/h3&gt;

&lt;p&gt;Safer, not safe. Every subdomain becomes a valid place to receive codes, including forgotten ones with stale DNS records and any subdomain where customers or users can publish their own pages. If you use one, audit your DNS records and remove the ones you no longer need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can PAR replace redirect URI registration?
&lt;/h3&gt;

&lt;p&gt;Sometimes. RFC 9126 lets a server accept unregistered redirect URIs from clients that push their requests with authentication, but servers don’t have to. loginos doesn’t: a pushed request must use one of your domains like any other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try loginos
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://loginos.io" rel="noopener noreferrer"&gt;loginos&lt;/a&gt; is an OpenID Connect provider that gives every domain of your product one shared sign-in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any number of domains, one user pool.&lt;/strong&gt; Add them in the console or from your server, cover subdomains with a verified wildcard, or let your server approve new ones as people sign in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkeys built in.&lt;/strong&gt; Face ID, fingerprints and security keys, with passwords as a fallback, and no WebAuthn code in your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles and permissions in every token&lt;/strong&gt;, the same on every domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the users you have.&lt;/strong&gt; People link their existing account once, and every token carries your own user id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Continue as” on your own page&lt;/strong&gt; in Chrome and Edge, with a small loginos window everywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard OpenID Connect&lt;/strong&gt;, so Auth.js, Passport or any OIDC library works. There are no paid plans today, and you can start without a card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://loginos.io/register" rel="noopener noreferrer"&gt;Create a platform&lt;/a&gt; in a few minutes, follow the &lt;a href="https://loginos.io/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or read &lt;a href="https://loginos.io/answers" rel="noopener noreferrer"&gt;more answers like this one&lt;/a&gt;. This answer is kept up to date at &lt;a href="https://loginos.io/answers/wildcard-redirect-uri-oauth" rel="noopener noreferrer"&gt;loginos.io/answers&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>oauth</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>How do I share a login session across multiple domains?</title>
      <dc:creator>cio fea</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:40:47 +0000</pubDate>
      <link>https://dev.to/ciofea/how-do-i-share-a-login-session-across-multiple-domains-hif</link>
      <guid>https://dev.to/ciofea/how-do-i-share-a-login-session-across-multiple-domains-hif</guid>
      <description>&lt;p&gt;&lt;em&gt;I’m building &lt;a href="https://loginos.io" rel="noopener noreferrer"&gt;loginos&lt;/a&gt;, a sign-in service for products that run on more than one domain. This question comes up a lot, so here is the full answer: the general approach first, then how loginos handles it.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; You can’t share a session cookie between two different domains, because browsers only send a cookie to the domain that set it. Instead, each domain keeps its own session and gets it from one central login, an OpenID Connect provider. Someone who is already signed in there is sent straight back with a one-time code, without seeing a form.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You run more than one site, like &lt;code&gt;app.example.com&lt;/code&gt;, &lt;code&gt;example.net&lt;/code&gt; and &lt;code&gt;shop.example.org&lt;/code&gt;, and you want someone who signs in on one of them to be signed in on the others. The obvious idea is to share the session cookie. That works for subdomains, but not for different domains, and no setting changes that. What you share instead is the &lt;strong&gt;sign-in&lt;/strong&gt;: every domain asks the same place who the person is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can’t two domains share a session cookie?
&lt;/h2&gt;

&lt;p&gt;A cookie belongs to the site that set it. When &lt;code&gt;example.com&lt;/code&gt; sets &lt;code&gt;session=abc&lt;/code&gt;, the browser sends that cookie back to &lt;code&gt;example.com&lt;/code&gt; and, if the cookie has &lt;code&gt;Domain=example.com&lt;/code&gt;, to its subdomains. It never sends it to &lt;code&gt;example.net&lt;/code&gt;. This rule is what stops any website from reading your bank’s session cookie, so browsers will not relax it.&lt;/p&gt;

&lt;p&gt;Third-party cookies used to offer a workaround: an &lt;code&gt;example.net&lt;/code&gt; page could load something from &lt;code&gt;example.com&lt;/code&gt; and get that domain’s cookies with it. Safari and Firefox block third-party cookies by default, and storage inside iframes is now partitioned by the top-level site. Tricks built on hidden iframes or &lt;code&gt;postMessage&lt;/code&gt; between your domains break for many of your users, often without an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are your options?
&lt;/h2&gt;

&lt;p&gt;There are three ways to sign someone in on more than one site. Which one fits depends on whether your sites share a parent domain.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Works across different domains?&lt;/th&gt;
&lt;th&gt;What you build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One cookie for the parent domain&lt;/td&gt;
&lt;td&gt;No, only for subdomains like &lt;code&gt;app.example.com&lt;/code&gt; and &lt;code&gt;shop.example.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Nothing extra: set &lt;code&gt;Domain=example.com&lt;/code&gt; on the session cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A central login (identity provider)&lt;/td&gt;
&lt;td&gt;Yes, any number of domains&lt;/td&gt;
&lt;td&gt;A redirect route and a callback route on each site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand a token from one site to the other&lt;/td&gt;
&lt;td&gt;Yes, but you secure it yourself&lt;/td&gt;
&lt;td&gt;One-time tokens, expiry, replay protection, redirect checks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;One parent-domain cookie&lt;/strong&gt; is the simplest when it fits. Everything under &lt;code&gt;example.com&lt;/code&gt; can read a cookie set with &lt;code&gt;Domain=example.com&lt;/code&gt;. It stops at the edge of that domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handing tokens across&lt;/strong&gt; means site A creates a short-lived token and redirects to site B with it in the URL, and site B trades it for a session. It works, but you end up writing a small identity provider: tokens that work once, expire fast, are bound to the target site, and never land in logs. Most teams who start here move to a real provider by the third domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A central login&lt;/strong&gt; is the standard answer, and it is what “single sign-on” means on the web. One service, the identity provider, keeps its own session on its own domain. Every one of your sites sends people there to sign in and gets them back with a one-time code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a central login shares the sign-in
&lt;/h2&gt;

&lt;p&gt;The protocol most providers speak is OpenID Connect (OIDC), a layer on top of OAuth 2.0 that tells your app who signed in. Here is what happens when someone who signed in on &lt;code&gt;app.example.com&lt;/code&gt; opens &lt;code&gt;example.net&lt;/code&gt; for the first time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;example.net&lt;/code&gt; has no session for them, so it redirects the browser to the provider, with its own address as the return address.&lt;/li&gt;
&lt;li&gt;The provider sees its own session cookie, set when they signed in on &lt;code&gt;app.example.com&lt;/code&gt;. It doesn’t show a form.&lt;/li&gt;
&lt;li&gt;The provider sends the browser straight back to &lt;code&gt;example.net&lt;/code&gt; with a code that works once.&lt;/li&gt;
&lt;li&gt;The server of &lt;code&gt;example.net&lt;/code&gt; exchanges the code for the person’s identity, then sets its &lt;strong&gt;own&lt;/strong&gt; session cookie.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The person sees a short redirect and is signed in. Each domain still has its own session, and nothing crosses between domains except the browser itself, carrying a one-time code. That is why this works without third-party cookies.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; To check for a sign-in without ever showing a page, add &lt;code&gt;prompt=none&lt;/code&gt; to the redirect. The provider answers at once: with a code if the person is signed in, or with &lt;code&gt;error=login_required&lt;/code&gt; if not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to watch out for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check the return address.&lt;/strong&gt; The provider must only send codes to addresses you registered. A provider that accepts any return address hands sign-in codes to whoever asks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange codes on your server.&lt;/strong&gt; The code is worth nothing without your app’s secret, so the exchange belongs on the server, not in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;state&lt;/code&gt; and PKCE.&lt;/strong&gt; &lt;code&gt;state&lt;/code&gt; ties the callback to the browser that started the sign-in, and PKCE stops a stolen code from being used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-out is per domain.&lt;/strong&gt; Signing out of one site clears that site’s session. To end the central session too, send the browser to the provider’s logout address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t merge accounts by email&lt;/strong&gt; unless the provider has verified the address. Use a stable user id (&lt;code&gt;sub&lt;/code&gt; in OpenID Connect).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How loginos does it
&lt;/h2&gt;

&lt;p&gt;loginos is an OpenID Connect provider built for exactly this: one product on many domains. You create a &lt;strong&gt;platform&lt;/strong&gt;, add your domains (as many as you have), and they all share one pool of people. Someone who creates an account on one domain is already a member on the others.&lt;/p&gt;

&lt;p&gt;On each domain you add two routes: one that redirects to loginos, and a callback that exchanges the code. The callback path is the same on every domain, &lt;code&gt;/api/auth/login/single/callback&lt;/code&gt; by default, and loginos only sends people back to &lt;code&gt;https://&amp;lt;one of your domains&amp;gt;&amp;lt;callback path&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOGINOS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://loginos.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The callback, on every one of your domains.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/auth/login/single/callback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lo_flow&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sign-in expired, try again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;LOGINOS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/oauth/token`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization_code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/api/auth/login/single/callback`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOGINOS_APP_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOGINOS_APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sign-in failed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// sub, preferred_username, roles, permissions…&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// this domain's own session&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://loginos.io/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; has the redirect route and complete versions for Next.js, Express and PHP. A few things come with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No form on the second domain.&lt;/strong&gt; Someone signed in to loginos who has used your platform before comes straight back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domains that come and go.&lt;/strong&gt; Add domains in the console or &lt;a href="https://loginos.io/docs/domains#from-your-server" rel="noopener noreferrer"&gt;from your server&lt;/a&gt;, cover subdomains with a verified &lt;a href="https://loginos.io/docs/domains#wildcards" rel="noopener noreferrer"&gt;wildcard&lt;/a&gt;, or let your server confirm &lt;a href="https://loginos.io/docs/domains#automatic-domains" rel="noopener noreferrer"&gt;new domains&lt;/a&gt; as people sign in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No redirect at all, if you like.&lt;/strong&gt; Chrome and Edge can show their own &lt;a href="https://loginos.io/docs/continue-as" rel="noopener noreferrer"&gt;“Continue as” prompt&lt;/a&gt; on your page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkeys&lt;/strong&gt;, and &lt;a href="https://loginos.io/docs/roles" rel="noopener noreferrer"&gt;roles and permissions&lt;/a&gt; in every token, on every domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I share cookies between example.com and example.net?
&lt;/h3&gt;

&lt;p&gt;No. Browsers send a cookie only to the domain that set it and, with the &lt;code&gt;Domain&lt;/code&gt; attribute, to that domain’s subdomains. &lt;code&gt;example.com&lt;/code&gt; and &lt;code&gt;example.net&lt;/code&gt; are different sites, so each needs its own session, issued after one central sign-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do subdomains count as different domains?
&lt;/h3&gt;

&lt;p&gt;Not for cookies. A cookie set with &lt;code&gt;Domain=example.com&lt;/code&gt; is sent to &lt;code&gt;app.example.com&lt;/code&gt; and &lt;code&gt;shop.example.com&lt;/code&gt; too. If all your sites are subdomains of one domain, a parent-domain cookie can be enough. As soon as you add a second domain, you need a central login.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will people have to sign in again on my second domain?
&lt;/h3&gt;

&lt;p&gt;Not with a central login. When they reach the second domain, it redirects them to the provider, which recognizes them from its own session and sends them back with a code. They see a quick redirect, not a form.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this still work when third-party cookies are blocked?
&lt;/h3&gt;

&lt;p&gt;Yes. The redirect flow only uses first-party cookies: the provider reads its own cookie on its own domain, and your site sets its own cookie on yours. It doesn’t depend on iframes or third-party storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when someone signs out?
&lt;/h3&gt;

&lt;p&gt;Each site clears its own session. The provider’s session stays until it ends or the person signs out there, so they may come back without a form. If you want sign-out to end everything, also send the browser to the provider’s logout address.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try loginos
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://loginos.io" rel="noopener noreferrer"&gt;loginos&lt;/a&gt; is an OpenID Connect provider that gives every domain of your product one shared sign-in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any number of domains, one user pool.&lt;/strong&gt; Add them in the console or from your server, cover subdomains with a verified wildcard, or let your server approve new ones as people sign in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkeys built in.&lt;/strong&gt; Face ID, fingerprints and security keys, with passwords as a fallback, and no WebAuthn code in your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles and permissions in every token&lt;/strong&gt;, the same on every domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the users you have.&lt;/strong&gt; People link their existing account once, and every token carries your own user id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Continue as” on your own page&lt;/strong&gt; in Chrome and Edge, with a small loginos window everywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard OpenID Connect&lt;/strong&gt;, so Auth.js, Passport or any OIDC library works. There are no paid plans today, and you can start without a card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://loginos.io/register" rel="noopener noreferrer"&gt;Create a platform&lt;/a&gt; in a few minutes, follow the &lt;a href="https://loginos.io/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or read &lt;a href="https://loginos.io/answers" rel="noopener noreferrer"&gt;more answers like this one&lt;/a&gt;. This answer is kept up to date at &lt;a href="https://loginos.io/answers/share-login-session-across-domains" rel="noopener noreferrer"&gt;loginos.io/answers&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>oauth</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
