Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommended approach to share session information between applications on subdomains #2414

Closed
2 of 5 tasks
raphaelpc opened this issue Jul 21, 2021 · 24 comments
Closed
2 of 5 tasks
Labels
question Ask how to do something or how something works stale Did not receive any activity for 60 days

Comments

@raphaelpc
Copy link

raphaelpc commented Jul 21, 2021

Question 💬

Hello! First of all, thanks for all the great work on this amazing library!

I'm trying to implement a solution in which i have the same authentication on subdomains as well as on main domain.
What is the recommended approach to share session information between all applications?

This is a continuation of a discussion that started on some previous issues, like #405, #794, #1668.
All those issues are now closed without a full example or definitive answer, so i decided to make a summary of what was previously discussed, in hope that i can get some help on deciding what is the best approach to do that integration on all domains and subdomains of my application and help document that approach for others in the same need.

How to reproduce ☕️

The application i'm working on is, actually, an ecosystem of applications, and not all of them are made with Next.js (or even with React). Many are legacy applications written in JSP.

The main one isn't an Next.js app and will live in "www.my-domain.com".
It will have sub-domains, as in "app1.my-domain.com", "app2.my-domain.com", etc.
As explained in the FAQ:

If you are using a different framework for you website, you can create a website that handles sign in with Next.js and then access those sessions on a website that does not use Next.js as long as the websites are on the same domain.

If you use NextAuth.js on a website with a different subdomain then the rest of your website (e.g. auth.example.com vs www.example.com) you will need to set a custom cookie domain policy for the Session Token cookie.

So, since the main application isn't a React / Next.js app, I will have to implement an auth application so i can use NextAuth: "auth.my-domain.com". Notice that it's the ONLY application that includes NextAuth - from my understanding, it makes no sence to add NextAuth to all my other Next.js applications.

I created a diagram to help illustrate the ecosystem:

image

To set a custom cookie domain policy for the Session Token cookie, i did in my [...nextauth] API route what was originally posted by @Xodarap in #405 (comment):

const useSecureCookies = process.env.NEXTAUTH_URL.startsWith('https://')
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
const hostName = Url(process.env.NEXTAUTH_URL).hostname
const options = {
  cookies: {
    sessionToken: 
    {
      name: `${cookiePrefix}next-auth.session-token`,
      options: {
        httpOnly: true,
        sameSite: 'lax',
        path: '/',
        secure: useSecureCookies,
        domain: hostName == 'localhost' ? hostName : '.' + hostName // add a . in front so that subdomains are included
      }
    },
  },
}

Doing like that, i was successfully able to access the cookie "__Secure-next-auth.session-token", created by "auth.my-domain.com" and set in the domain ".my-domain.com", in all my other applications.

@iaincollins said in #405 (comment) that he was evaluating ways to make that configuration easier, but the issue #405 was closed because of inactivity.

As @iaincollins explained in #417 (comment):

Session Tokens are typically stored in cookies. To best protect against session hijacking (e.g. from third party JavaScript in advertising, tracking, browser extensions, XSS) these should be server only readable cookies that are not accessible directly from JavaScript. This is the model NextAuth.js uses.

If a user is signed in, any request from the client will have a secure, sever only readable cookie set that can be used to either look up a session in a database (if it is a database session token) or decoded and verified (if it is a JSON Web Token) to identify the user.

Using server readable only cookies is secure without a CSRF token.

So in regards to the protection needed to secure my backend all is done: the backend will receive the cookies together with the requests made by the frontend and decode and verify the identity of the user.

What i still don't know is what is the best approach to sharing the session information between all the frontend applications. Should i just read the information in the cookie in each of my applications? Because, doing like that, the cookie lifespam won't be increased, like happens when i access the "session" endpoint or the "useSession" / "getSession" client. For example:

// Set cookie, to also update expiry date on cookie
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: sessionExpires,
...cookies.sessionToken.options,
})

@iaincollins posted in #796 (comment) a quite great explanation of the challanges of defining the maxAge of the JWT. I quote:

By default, a Next.js Single Page App using NextAuth.js will load session once and on subsequent page transitions, the useSession hook will reuse the response it already has.

However, the app wide session state WILL be updated in the SPA if the window loses/regains focus or if the user logs out in another window in the same browser (it uses event hooks to do this) OR anytime you call getSession() in your app. This makes Single Page Apps efficient by default - they don't check your session on every page navigation, which keeps network traffic down and page responses snappy - but keeps the session state from getting stale by instead re-validating when a page gains focus again.

So i think it would be of great loss if we don't access the session via the "session" endpoint and, as a result, lose on that great functionality provided by NextAuth.

The problem: As @subhendukundu commented in #794 (comment), i'm also having problens trying to use the endpoints provided by NextAuth on my "auth.my-domain.com" (like "https://auth.my-domain.com/api/auth/session") application.

First, i was getting CORS related errors when i tried to consume the endpoints. About those, i get that may be related to the way Next.js protects it's API routes, but i think it is usefull to document the solution here, since it will be a mandatory step for the solution to work. I followed the guide provided in the Vercel documentation and in the Next.js documentation - in the "next.config.js" file, i created a "headers" function:

// next.config.js
// NEXT_PUBLIC_ENV is an environment variable defined on my .env, with values 'development', 'staging' or 'production' in the respective environments.
module.exports = {
  async headers() {
    // To help with local development...
    if (process.env.NEXT_PUBLIC_ENV === 'development') {
      return [
        {
          source: "/api/auth/:path*",
          headers: [
            { key: 'Access-Control-Allow-Credentials', value: 'true' },
            { key: 'Access-Control-Allow-Origin', value: '*' },
            { key: 'Access-Control-Allow-Methods', value: 'GET, OPTIONS, PATCH, DELETE, POST, PUT' },
            { key: 'Access-Control-Allow-Headers', value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' },
          ],
        },
      ];
    }

    // In the other environments...
    return [
      // https://vercel.com/support/articles/how-to-enable-cors#enabling-cors-in-a-next.js-app
      // https://nextjs.org/docs/api-reference/next.config.js/headers#header-cookie-and-query-matching
      {
        // matching all auth API routes
        source: "/api/auth/:path*",
         // if the origin has '.my-domain.com'...
        has: [
          { type: 'header', key: 'Origin', value: '(?<serviceName>^https:\/\/.*\.my-domain\.com$)' },
        ],
        // these headers will be applied
        headers: [
          { key: 'Access-Control-Allow-Credentials', value: 'true' },
          { key: 'Access-Control-Allow-Origin', value: '*' },
          { key: 'Access-Control-Allow-Methods', value: 'GET, OPTIONS, PATCH, DELETE, POST, PUT' },
          { key: 'Access-Control-Allow-Headers', value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' },
        ],
      },
    ];
  }
};

(edit: the code i previously posted to configure CORS was wrong (i was checking the host of the request, and the correct way to do that is checking the "Origin" header).
With that configuration, my applications on my subdomains can access the auth endpoints from my "https://auth.my-domain.com", and it still keeps protected from access made from other domains.

I'm using "fetch" to get the data from the "https://auth.my-domain.com/api/auth/session" endpoint. Something like that:

const a = await fetch('https://auth.my-domain.com/api/auth/session');
const b = await a.json();
// b will have the session information

If i do that fetch inside my "auth.my-domain.com" application, it works as expected, bringing the session information.
The problem is that, if i try to do that same fetch from my "app2.my-domain.com", for example, i don't receive the session information, only an empty object ({}). Why is that happening? Did i do something wrong?

Also, despite all the references i provided, i still don't know if that is the recommended approach to share the session information between my applications.

Thanks in advance!

Documentation feedback

  • Found the documentation helpful
  • Found documentation but was incomplete
  • Could not find relevant documentation
  • Found the example project helpful
  • Did not find the example project helpful

Contributing 🙌🏽

Yes, I am willing to help answer this question in a PR

@raphaelpc raphaelpc added the question Ask how to do something or how something works label Jul 21, 2021
@Fronix
Copy link

Fronix commented Jul 23, 2021

NextAuth isn't an Identity Provider it's a consumer of Identity Provider's. While it theoretically could be possible to do this by signing your own JWT tokens, it's going to be a hassle getting the tokens to be refreshed correctly since it has to be signed by the author (eg. auth.mydomain.com).

It would look something like this:
idp

Here [auth.domain.com] will be your only way to authenticate to your applications. auth.domain.com can in turn do requests to Google/Apple for the actual identification, but your session/JWT token will be handled by this application.

This in turn will allow you to use NextAuth or any other way of logging users in by adding your IdP as the Provider.

Didn't have time to go into more detail here, but this is the general idea. You could also use an SSO solution like Auth0, FusionAuth, KeyCloak etc. You can integrate your authentication methods like Google/Apple there and have auth.domain.com to point to that provider. The positive side of this is that you don't need to do anything more than configuring, the downside is that you have no real control over the data.

@raphaelpc
Copy link
Author

Hello @Fronix !
Indeed, NextAuth isn't an identity provider, like KeyCloak, for example.
But it really doesn't need to be in my case, since all applications are on the same domain and can share access to the same cookie, since the cookie has the domain ".my-domain.com".
The secret used to sign the cookie has to be known by the Auth application and by all backends that will consume the JWT (or, in my case, by an API Gateway that will verify if the JWT token is valid and, if it is, will set a new header on the request containing the user id, so the backends don't have to worry about that).
So, in my case, it really is only the Auth application that would have NextAuth. That is what the documentation suggests.

So the main challenge is, like you said: "it's going to be a hassle getting the tokens to be refreshed correctly since it has to be signed by the author (eg. auth.mydomain.com)".
But, from my understanding - and that is what i didn't find in the docs - if my frontend applications consume the session information using the "https://auth.my-domain.com/api/auth/session" endpoint, the exp time of the JWT token will be updated (and that is what i'm having trouble to make work, like i explained by the end of the post).

I hope i was able to explain all properly (english is not my main language!).

Thanks!

@Fronix
Copy link

Fronix commented Jul 23, 2021

@raphaelpc
I see, It was a lot of text, so I might have not read everything 100% ^^

I see what you are trying to accomplish here and yes it should be possible. You might be able to accomplish this by fetching api/auth/session using an iframe, that way you are interacting with auth.my-domain.com and that should trigger the JWT update in NextAuth on auth.my-domain.com which in turn will update your JWT cookie on any of the sites you are on.

@raphaelpc
Copy link
Author

Good news, i spent the last day trying and researching and trying and researching and was able to resolve the biggest of my problems! 🗡️

The default way that "fetch" works is to only send the cookies (credentials) with requests to same-origin URLs:

same-origin: Tells browsers to include credentials with requests to same-origin URLs, and use any credentials sent back in responses from same-origin URLs.
include: Tells browsers to include credentials in both same- and cross-origin requests, and always use any credentials sent back in responses.

Ref: https://developer.mozilla.org/pt-BR/docs/Web/API/Fetch_API/Using_Fetch

So, to force "fetch" to send the cookies, it has to be made like that:

let a = await fetch('http://auth.my-domain.com/api/auth/session', { credentials: 'include' })

But, doing like that, i got a series of all new errors related to CORS (WildcardOriginNotAllowed). After a lot of digging, i found out that:

When responding to a credentialed request, the server must specify an origin in the value of the Access-Control-Allow-Origin header, instead of specifying the "*" wildcard.

Because the request headers in the above example include a Cookie header, the request would fail if the value of the Access-Control-Allow-Origin header were "". But it does not fail: Because the value of the Access-Control-Allow-Origin header is "http://foo.example" (an actual origin) rather than the "" wildcard, the credential-cognizant content is returned to the invoking web content.
Ref: https://developer.mozilla.org/pt-BR/docs/Web/HTTP/CORS

So i changed the next.config.js of my AUTH application to be like that:

// next.config.js
// NEXT_PUBLIC_ENV is an environment variable defined on my .env, with values 'development', 'staging' or 'production' in the respective environments.
module.exports = {
  async headers() {
    // To help with local development...
    if (process.env.NEXT_PUBLIC_ENV === 'development') {
      return [
        {
          source: "/api/auth/:path*",
          has: [
            { type: 'header', key: 'Origin', value: '(?<origin>.*)' },
          ],
          headers: [
            { key: 'Access-Control-Allow-Credentials', value: 'true' },
            { key: 'Access-Control-Allow-Origin', value: ':origin' },
            { key: 'Access-Control-Allow-Methods', value: 'GET, OPTIONS, PATCH, DELETE, POST, PUT' },
            { key: 'Access-Control-Allow-Headers', value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' },
          ],
        },
      ];
    }

    // In the other environments...
    return [
      // https://vercel.com/support/articles/how-to-enable-cors#enabling-cors-in-a-next.js-app
      // https://nextjs.org/docs/api-reference/next.config.js/headers#header-cookie-and-query-matching
      {
        // matching all auth API routes
        source: "/api/auth/:path*",
         // if the origin has '.my-domain.com'...
        has: [
          { type: 'header', key: 'Origin', value: '(?<origin>^https:\/\/.*\.my-domain\.com$)' },
        ],
        // these headers will be applied
        headers: [
          { key: 'Access-Control-Allow-Credentials', value: 'true' },
          { key: 'Access-Control-Allow-Origin', value: ':origin' },
          { key: 'Access-Control-Allow-Methods', value: 'GET, OPTIONS, PATCH, DELETE, POST, PUT' },
          { key: 'Access-Control-Allow-Headers', value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' },
        ],
      },
    ];
  }
};

This way, i can now do a request to my AUTH application from the other applications on my domain to obtain the session information. I will still lose LOTS of functionalities that i would have if i had only one application with NextAuth in it, like refreshing the exp of the JWT when changing tabs, but i believe this is the best possible outcome in a situation like mine, where i can deal with legacy applications.

So, since i'm now able to get the session, the only question remaining is: i still don't know if that is the recommended approach to share the session information between my applications. :)

Thanks in advance!

@Fronix
Copy link

Fronix commented Jul 23, 2021

@raphaelpc Great that you solved it! About the refreshing part, you can go with my suggestion of using a hidden iframe to periodically load your auth.domain.com site that will refresh and keep your JWT up to date.

It would require you to add this to all your application, but a simple package that you can load via JavaScript should do it.

Something simple as this

<iframe height="0" width="0" src="https://auth.domain.com/url/to/something/that/refreshes/token"></iframe>

@raphaelpc
Copy link
Author

raphaelpc commented Jul 23, 2021

@Fronix that is a great suggestion! For now i will leave this open in hope that someone involved with the maintainance of NextAuth, like @balazsorban44, can give my summary a look to identify if the approach is correct. I also think this could be included somewhere in the docs (or i may create a Medium post with this solution, if correct? )

Thanks!

@balazsorban44
Copy link
Member

balazsorban44 commented Jul 23, 2021

Will have a look when I have the time. 😊 hopefully some time the next week or after that. this issue is complicated to wrap my head around, but I know it's a sought-after feature.

Thanks for the research and findings!

@ericvanular
Copy link

Is it possible to support single sign on using next-auth with custom domains (rather than just subdomains as shown here)?

@leo-petrucci
Copy link

leo-petrucci commented Sep 1, 2021

Is it possible to support single sign on using next-auth with custom domains (rather than just subdomains as shown here)?

Safari has discontinued sending HttpOnly cookies cross-domain, and I assume most browsers will follow suit soon enough. So getting this to work across domains seems unlikely, but let me know if I'm completely wrong 😅

@ericvanular
Copy link

Safari has discontinued sending HttpOnly cookies cross-domain, and I assume most browsers will follow suit soon enough. So getting this to work across domains seems unlikely, but let me know if I'm completely wrong 😅

Interesting, though Auth0 and other auth providers are still able to offer custom domain SSO so it must be possible somehow. Similar to how when you sign in on Gmail, you can navigate you Youtube and still be signed in

@ericvanular
Copy link

@raphaelpc Great that you solved it! About the refreshing part, you can go with my suggestion of using a hidden iframe to periodically load your auth.domain.com site that will refresh and keep your JWT up to date.

It would require you to add this to all your application, but a simple package that you can load via JavaScript should do it.

Something simple as this

<iframe height="0" width="0" src="https://auth.domain.com/url/to/something/that/refreshes/token"></iframe>

The iFrame & PostMessage API approach sounds like a great way to get cross domain SSO working by sharing the session cookie stored on the canonical URL (auth.domain.com) between domains. I'm trying to figure this out currently but am hitting several blockers. Did you ever manage to get this working with next-auth?

@raphaelpc
Copy link
Author

@ericvanular I also don't think NextAuth can be used to authenticate across sites on different domains.
I faced several roadblocks to implement the authentication across different subdomains but it's now working great.
The solution still wasnt deployed in production (it's a big change in the way the authentication currently works), but once it is in production and working well i will write an article or something like that explaining everything i had to do.

The biggest thing that i think you have to take a look is that the way NextAuth works is different from the way that Auth0 (or, for example, Keycloak) works. NextAuth is not a single signon service like those two. It just creates a cookie with the auth information, and has several methods implemented that makes that secure. To make it work across subdomains, all i had to do was play with the configurations of that cookie and of the fetchs (like using { credentials: 'include' } on my fetchs) ans CORS. But it's not secure to have cookies from a domain be sent to other domains, and i believe that most browsers currently block that. If you have to share authentication between applications on different domains, i think you will have to look for something like "https://www.keycloak.org/".

@ericvanular
Copy link

@raphaelpc thank you for the suggestion of Keycloak. I understand that SSO across different domains presents a number of additional challenges.

I did find this SO answer: https://stackoverflow.com/questions/64091604/authentication-across-across-multiple-domains-cors-with-nextauth in which @iaincollins mentions that it is a planned feature for next-auth. I'm curious if this is still on the roadmap and if there is anything we can do to help progress. Running and maintaining a Keycloak instance presents its own challenges and it would incredible if we could leverage next-auth for this functionality instead

@balazsorban44
Copy link
Member

If it is, that was planned by Iain, who hasn't shown interest in contributing recently.

I don't plan to add it in the NEAR future, but maybe some time later on. It is mainly because I lack the required knowledge at the moment.

If anyone with security background want to take a stab at it and either open a PR or su. up what would be required from next-auth, please do!

@eugenehp
Copy link

eugenehp commented Sep 26, 2021

I added required changes to the backend to enable cookies sharing across subdomains and tested it with few projects.
It's not audited fully, but should give you an idea on how to implement it.

https://github.com/eugenehp/next-auth/tree/eugenehp/subdomains

Comments about cookies above are on the right track too.

Here's my other comment about callbackUrls that turned out to be broken in the process too.

⚠️Please note this branch is based off 3.29.x branch and not current 4.x that is in beta.⚠️

@raphaelpc
Copy link
Author

My system using the NextAuth login implementation goes into production next week or the other, wish me luck kkk
After that, i hope to make a guide of the configs i had to do to get all working (i only had to use patch-package do resolve proxy issues i was having inside my corporate proxy, but everything else i was able to accomplish with the NextAuth configuration only).

@balazsorban44
Copy link
Member

balazsorban44 commented Sep 28, 2021

Nice work, @raphaelpc! Eagerly waiting for your results. I would like to read up and if we can, I would like to make the configuration for this easier.

@stale
Copy link

stale bot commented Nov 27, 2021

Hi there! It looks like this issue hasn't had any activity for a while. It will be closed if no further activity occurs. If you think your issue is still relevant, feel free to comment on it to keep it open. (Read more at #912) Thanks!

@stale stale bot added the stale Did not receive any activity for 60 days label Nov 27, 2021
@stale
Copy link

stale bot commented Dec 5, 2021

Hi there! It looks like this issue hasn't had any activity for a while. To keep things tidy, I am going to close this issue for now. If you think your issue is still relevant, just leave a comment and I will reopen it. (Read more at #912) Thanks!

@stale stale bot closed this as completed Dec 5, 2021
@viperfx
Copy link

viperfx commented Dec 31, 2021

Hey @raphaelpc - how did your deploy go with your implementation? Did you get closer to getting this use case working?

@zomars
Copy link

zomars commented Jan 4, 2022

A minimal reproduction example would be nice :)

@timosaikkonen
Copy link

timosaikkonen commented Jan 25, 2022

I'm looking to get this setup working as well. I've got a couple of Next sites under a single domain and would like them to share a single NextAuth server. I've got everything else working just fine (cookie shared across subdomain, JWTs packed and unpacked successfully on all the servers) except for the case where refocus initiates a fetch to /api/auth/session.

The cookie is not being sent when the request is not initiated by the domain where the cookie was issued. This is because fetchData is missing "credentials": "include" in its options.

A lightweight fix to this would be to accept default fetch options in SessionProviderProps.

How does this sound @balazsorban44 ? I'm happy to pop in a PR.

EDIT: Raised #3735

@becklen
Copy link

becklen commented Apr 22, 2022

@raphaelpc did you ever go live with this solution?

@jaydavid
Copy link

jaydavid commented Mar 15, 2023

I am trying to implement something similar, and after struggling with quite a few discussion threads and my own lack of knowledge around a lot of these pieces, I threw together a minimal example using create-next-app templates and some bare bones next-auth configuration plus the relevant bits of this discussion thread.

This repo contains a lot of copy and paste from this thread, and the non-development portions (i.e. anything running outside of localhost) I haven't tested directly. But it should hopefully serve as a basic idea of how this discussion went.

I have also not implemented anything regarding the iframe refresh strategy discussed above. Any comments are welcome, hopefully it is helpful to someone.

https://github.com/jaydavid/next-auth-subdomain

Edit - the only thing that I can think of that I added was a bit in the auth application that deals with redirecting. I was having some trouble with it defaulting to the base URL for the auth app and so I added a small callback to handle redirects using the same host name. There might be a better way to do this, or it may have even been discussed above and I am mistaken - but I wanted to call it out in case it's a piece that anyone needs to focus on.

Its current implementation as of writing this comment:

  callbacks: {
    redirect: async ({ url, baseUrl }: { url: string; baseUrl: string }) => {
      if (new URL(url).hostname === hostName) return Promise.resolve(url);
      return Promise.resolve(baseUrl);
    },
  },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Ask how to do something or how something works stale Did not receive any activity for 60 days
Projects
None yet
Development

No branches or pull requests