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

handleCallback and organizations using multiple (sub)domains #398

Closed
mrksbnch opened this issue May 17, 2021 · 13 comments
Closed

handleCallback and organizations using multiple (sub)domains #398

mrksbnch opened this issue May 17, 2021 · 13 comments
Labels
enhancement New feature or request

Comments

@mrksbnch
Copy link

mrksbnch commented May 17, 2021

Describe the problem you'd like to have solved

If you are using the organisations feature in Auth0 and use different domains for every organisation, e.g. https://ORG.exmaple.com, you may want to support the following two use-cases:

  1. If a user visits https://ORG.exmaple.com and is not yet logged in, redirect the user to the organisation specific Auth0 universal login page using the organization and redirect_uri attributes in the authorizationParams property for the handleLogin method. After successful authentication, the user is redirect back to the tenant specific URL, e.g. https://ORG.exmaple.com. To circumvent unauthorized_client errors in the callback, you can use the redirectUri property (doesn't seem to be documented yet) in the handleCallback method (as per Allow setting redirect_uri at runtime on the callback handler just like it's allowed on the login handler #298).
  2. If a user visits https://example.com and is not yet logged in, you can leave the organization attribute empty and use the "Display Organization Prompt" functionality that will prompt users to enter their organization name before logging in. After logging in, you want to redirect the user to https://ORG.exmaple.com and—more importantly—set the cookie for this domain.

While the first use-case can already be covered using the configuration options mentioned above, the second use-case doesn't seem to be supported yet for the following reasons:

  • When the user visits https://example.com, you don't yet know to which organisation the user belongs. Also, at this point you can't yet set a redirect_uri or organization attribute (unless you build your own organisation prompt and don't use the Auth0 organisation prompt).
  • While you can overwrite the redirect url using an Auth0 rule (context.redirect.url = URL), the handleCallback will fail because the state is not set for the domain https://ORG.example.com. The state is only set for https://example.com in that specific scenario.

Describe the ideal solution

Add a configuration option to allow setting the state and nonce cookies on the parent domain, e.g. .example.com in this example. The actual session cookie should still be set on a specific URL, e.g. https://org.exmaple.com. So, the workflow would then looks as follows:

  • Uses visits https://example.com
  • You initiate the authentication by using the handleLogin method (without passing any organisation or redirect_uri attributes) which will in return set the state and nonce cookies for .example.com
  • The user is prompted to enter their organisation in the Auth0 prompt (assuming that the "Display Organization Prompt" functionality is enabled)
  • The user logs in using their credentials
  • In an Auth0 rule you overwrite the redirect url to the organisation specific URL, e.g. "https://ORG.example.com/api/auth/callback"
  • The state check won't fail as the state and nonce cookies are accessible

Alternatives and current work-arounds

  • Use a custom organisation prompt
@adamjmcgrath adamjmcgrath added needs investigation This needs to be investigated further before proceeding enhancement New feature or request and removed needs investigation This needs to be investigated further before proceeding labels May 17, 2021
@adamjmcgrath
Copy link
Contributor

Hi @mrksbnch - thanks for raising this

I don't see a simple solution for the use case where you don't know the organization or callback url upfront.

Could you redirect back to https://example.com/api/auth/callback, then redirect on to the correct ORG.example.com domain after that?

If you do want to use the Auth0 pre login organization prompt, you should also look at https://auth0.com/docs/organizations/using-tokens#validating-tokens (The For web applications: section)

@mrksbnch
Copy link
Author

mrksbnch commented May 17, 2021

@adamjmcgrath

Could you redirect back to https://example.com/api/auth/callback, then redirect on to the correct ORG.example.com domain after that?

Not out of the box with the Next.js SDK as it (as far as I can see) automatically sets the session cookie (which includes the access token, refresh token, etc.) on the callback domain, so https://example.com. If I were to redirect to https://ORG.example.com after the callback, I wouldn't have access to the session token.

However, if there was an easy way to get the session cookie value in handleCallback, it would theoretically be possible to redirect and set an http only cookie as part of the redirect, wouldn't it?

I don't see a simple solution for the use case where you don't know the organization or callback url upfront.

Wouldn't it be possible (for such use cases) to set the state and nonce cookies on the parent domain (so they are accessible on all subdomains)? This is (as far as I can tell) the main issue here as I can redirect to the correct (org specific) callback within an Auth0 rule. If I do so with the current implement of the Next.js API, so...

https://example.com -> https://example.auth0.com/... (organization prompt) -> https://example.auth0.com/... (organization login page) -> https://ORG.example.com/api/auth/callback

... the callback fails as ORG.example.com can't access the state and nonce cookies. This doesn't need to the default, but could potentially be an additional configuration option?

@adamjmcgrath
Copy link
Contributor

I can redirect to the correct (org specific) callback within an Auth0 rule

Are you sure you can do that? See https://community.auth0.com/t/context-request-query-redirect-url-does-not-seem-to-be-overridable-in-a-rule/11764/2

Could you redirect a user from a rule (eg https://auth0.com/docs/rules/redirect-users) to start the login process from the correct sub domain on your app after you know the origanisation?

https://example.com -> https://example.auth0.com/... (organization prompt) -> https://example.auth0.com/... (organization login page) -> https://ORG.example.com/api/auth/login (rule redirects back to start login from correct ORG) -> https://example.auth0.com/... (organization login page - you already have a session) -> https://ORG.example.com/api/auth/callback

@mrksbnch
Copy link
Author

mrksbnch commented May 20, 2021

Sorry for the delay in response, I had to verify and test some of these points. See answers below.

Are you sure you can do that? See https://community.auth0.com/t/context-request-query-redirect-url-does-not-seem-to-be-overridable-in-a-rule/11764/2

The method mentioned in this thread doesn't work for me either. But it "works" with context.redirect = { url: "URL_HERE" }. However, according to this documentation, this only "pauses" the authorization which then needs to be continued for a login session to be established. So, this doesn't seem to be a valid solution after all.

Could you redirect a user from a rule (eg https://auth0.com/docs/rules/redirect-users) to start the login process from the correct sub domain on your app after you know the origanisation?

Not as far as I can see. There doesn't seem to be a way (within a rule) to redirect the user after the login process (this is something that has to be done in the callback). Within a rule, you can only redirect the user to another page but then need to continue the authorization to establish a session (see link above for Rules or this link for Actions).

That being said, I think the only option would be to redirect in the callback. The most promising approach seems to be as follows: In the callback, I check if I'm already on the correct domain, e.g. "ORG.example.com". If I'm not, then I get the state, nonce and code_verifier cookies and set them on the parent domain, i.e. .example.com instead of exmaple.com. Afterwards I redirect to "ORG.example.com" and start the code challenge via handleCallback. That generally seems to work with something like this:

export default handleAuth({
  async callback(req, res) {
    try {
      // fake function to output the tenant subdomain name, so e.g. "test1" for "test.example.com", or null
      const orgName = getTenantSubdomainName();

      if (orgName) {
        // correct domain, begin code challenge
        await handleCallback(req, res, ...);
      } else {
        // incorrect domain, set cookies on parent domain and redirect
        const cookies = new Cookies(req, res);
        cookies.set("nonce", req.cookies.nonce, {
          domain: ".example.com",
          sameSite: "lax",
        });
        cookies.set("state", req.cookies.state, {
          domain: ".example.com",
          sameSite: "lax",
        });
        cookies.set("code_verifier", req.cookies.code_verifier, {
          domain: ".example.com",
          sameSite: "lax",
        });

        // Open question: How to find the correct organization that the user picked on the Auth0 login page
        res.redirect(...)
      }
    } catch (error) {
      res.status(error.status || 400).end(error.message);
    }
  },
});

However, there's one catch: What would be the best way to find out the organisation that the user picked on the Auth0 login page? Can I somehow add the organisation to the state in a rule or similar and then check the state in the callback?

Also, would it make sense for this SDK to allow setting the domain for the state and nonce cookies separately via e.g. AUTH0_STATE_COOKIE_DOMAIN (similar to AUTH0_COOKIE_DOMAIN)—just so that the workaround mentioned above (assuming that we can somehow get the chosen organisation in the callback) can be simplified a lot? I'm not sure if I'm the only one wanting to use separate subdomains for every organisation but I guess that this is a common use-case now that organisations are available.

@adamjmcgrath
Copy link
Contributor

What would be the best way to find out the organisation that the user picked on the Auth0 login page? Can I somehow add the organisation to the state in a rule or similar and then check the state in the callback?

Hi @mrksbnch - since you can't manipulate the redirect_uri (or the state) you could only get the org_id from the ID Token's claims. And you'll need to effectively login (call handleCallback) to do this.

Not as far as I can see. There doesn't seem to be a way (within a rule) to redirect the user after the login process (this is something that has to be done in the callback). Within a rule, you can only redirect the user to another page but then need to continue the authorization to establish a session (see link above for Rules or this link for Actions).

Could you redirect to the correct org subdomain's login (eg context.redirect = { url: "https://ORG.example.com/api/auth/login" }) in the rule, effectively abandoning the current login process now that you know the ORG and starting a new login process from the correct subdomain?

@mrksbnch
Copy link
Author

mrksbnch commented May 28, 2021

Could you redirect to the correct org subdomain's login (eg context.redirect = { url: "https://ORG.example.com/api/auth/login" }) in the rule, effectively abandoning the current login process now that you know the ORG and starting a new login process from the correct subdomain?

Thanks, that's similar to the other possible solution above. I've looked at this again because I originally thought that this solution won't work due to the auth session not being established if you redirect from an action/rule. However, it turns out that the main issue was related to the returnTo parameter (automatically set via withPageAuthRequired or manually defined in the login options (handleLogin)).

In my initial tests this solution only works if you provide an absolute URL in the returnTo parameter, so e.g. "https://ORG.example.com". If you provide a relative path (which withPageAuthRequired seems to do), then you will be redirected to https://example.com which will then just show the org prompt again (as you will never have a session on the main domain).

I'm currently trying to figure out what the best way is to "remember" the return path for the second login (so the one after the action/rule redirect). It seems like I can't access the returnTo parameter inside an action, so I assume the only way is to save the returnTo parameter in a cookie (first login) and then use this cookie value for the second login. Is there any better solution?

Also, is it problematic that the state, nonce and code_verifier cookies on "https://example.com" (first login) won't be deleted as I redirect from within an action? I don't think so because they don't hold any valuable information and will automatically expire anyway?

@adamjmcgrath
Copy link
Contributor

In my initial tests this solution only works if you provide an absolute URL in the returnTo parameter, so e.g. "https://ORG.example.com". If you provide a relative path (which withPageAuthRequired seems to do), then you will be redirected to https://example.com which will then just show the org prompt again (as you will never have a session on the main domain).

Yes, this is expected - you would need to provide a custom returnTo

I'm currently trying to figure out what the best way is to "remember" the return path for the second login (so the one after the action/rule redirect). It seems like I can't access the returnTo parameter inside an action, so I assume the only way is to save the returnTo parameter in a cookie (first login) and then use this cookie value for the second login. Is there any better solution?

You can pass custom parameters to /authorize(see https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#customize-handlers-behavior) and access them in context.request.query in your rule

Also, is it problematic that the state, nonce and code_verifier cookies on "https://example.com" (first login) won't be deleted as I redirect from within an action? I don't think so because they don't hold any valuable information and will automatically expire anyway?

You are correct, this is not an issue

@mrksbnch
Copy link
Author

mrksbnch commented Jun 4, 2021

Thanks for the help and suggestion, that seems to work fine. For everybody else with a similar use-case, here is some very barebones code to make that work:

// pages/api/auth/[...auth0].js
import { URL } from "url";
import {
  handleAuth,
  handleLogin,
  handleCallback,
  getSession,
} from "@auth0/nextjs-auth0";

function getOrgId(orgName) {
  // get org id from org name
  return ...;
}

function getReturnTo(path = "/", orgName) {
  const url = new URL(path, "http://localhost:3000");
  if (!url.host.match(/([^.]*)\.localhost(?::\d{2,4})?/i)) {
    url.host = `${orgName}.localhost:3000`;
  }
  return url.toString();
}

function getOrgName(hostname) {
  const matches = hostname.match(/([^.]*)\.localhost(?::\d{2,4})?/i);
  return Array.isArray(matches) ? matches[1] : null;
}

function getLoginOptions(req, res) {
  const authorizationParams = {
    response_type: "code",
    scope: "openid profile email offline_access",
    audience: "https://audience.example.com",
  };
  const orgName = getOrgName(req.headers.host);
  if (!orgName) {
    authorizationParams.returnTo = req.query.returnTo || "/";
    return { authorizationParams };
  }

  const orgId = getOrgId(orgName);
  if (!orgId) {
    res.status(400).end("Unable to resolve organization");
  }
  const returnTo = getReturnTo(req.query.returnTo, orgName);
  return {
    authorizationParams: {
      ...authorizationParams,
      redirect_uri: `http://${orgName}.localhost:3000/api/auth/callback`,
      organization: orgId,
      returnTo,
    },
    returnTo,
  };
}

export default handleAuth({
  async login(req, res) {
    try {
      // Check if user already has a valid session
      const orgName = getOrgName(req.headers.host);
      const hasSession = !!getSession(req, res);
      if (orgName && hasSession) {
        return res.redirect(getReturnTo(req.query.returnTo, orgName));
      }

      const { authorizationParams, returnTo } = getLoginOptions(req, res);
      await handleLogin(req, res, {
        authorizationParams,
        returnTo,
      });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  },

  async callback(req, res) {
    try {
      const orgName = getOrgName(req.headers.host);
      if (!orgName) {
        return res.status(400).end("Unable to resolve organization");
      }

      await handleCallback(req, res, {
        redirectUri: `http://${orgName}.localhost:3000/api/auth/callback`,
      });
    } catch (error) {
      res.status(error.status || 400).end(error.message);
    }
  },
});
// Auth0 Login Action
 exports.onExecutePostLogin = async (event, api) => {
  if (!event.organization) {
    api.access.deny("Invalid request");
  }

  if (event.request.query.redirect_uri === "http://localhost:3000/api/auth/callback") {
    api.redirect.sendUserTo(`http://${event.organization.name}.localhost:3000/api/auth/login`, {
      query: { 
        returnTo: event.request.query.returnTo || "/",
      },
    });
  }
};

exports.onContinuePostLogin = async (event, api) => {};

@mrksbnch mrksbnch closed this as completed Jun 4, 2021
@adamjmcgrath
Copy link
Contributor

Thanks @mrksbnch!

@ci-vamp
Copy link

ci-vamp commented Aug 19, 2023

@adamjmcgrath is what @mrksbnch posted above still the suggested approach to handling subdomains?

@mrksbnch
Copy link
Author

@ci-vamp While I can't say if this is the suggested approach, it's (from what I can tell) still the only reliable way to make this work. The alternative is to write your own auth library for Next.js to work around some of the cookie restrictions.

Recently, Auth0 made a change to allow you to pass the organization name instead of the organization ID to the authorization endpoint. This allows you to get rid of the "getOrgId" function in the code above.

@ci-vamp
Copy link

ci-vamp commented Aug 20, 2023

@mrksbnch Thanks man. Actually we are not using organizations yet. Currently we have the "classic" universal login and we separate our tenants into their own A0 Application + Connection. Each tenant has their own subdomain which informs us which Application config to use during authentication (by checking their subdomain).

No user ever logs in on our bare domain - only through their subdomain host.

We had this all working on a SPA but recently have begun work on moving to nextjs.

I have been trying to figure out how to consolidate under 1 A0 Application + individual Connections per tenant. However, I am not able to get the subdomains to work (at least locally). They are configured under the Callback/Logout URLs of the Application yet it always redirects me back to the bare localhost.

I think I've read every issue related to subdomain handling and I see a lot of mixed information. Some threads say you need individual A0 instances (initAuth0) per tenant subdomain, others say you do not. Each thread has some bits of information but not quite the full picture (e.g. are these a 1 Application + N Connections or N Applications + Connections, are they using Organizations or classic Universal Login).

Then I found your approach which looks sound but I believe may not be applicable to our use case since we never have base domain logins nor Organizations.

@mrksbnch
Copy link
Author

Without knowing all the details, this actually sounds somewhat similar to the use-case we were trying to solve. The only major difference is that you are using multiple Auth0 Applications (and Auth0 Connections) instead of Auth0 Organisations. The work around mentioned in this ticket is only applicable if you use Auth0 Organisations.

I think I've read every issue related to subdomain handling and I see a lot of mixed information. Some threads say you need individual A0 instances (initAuth0) per tenant subdomain, others say you do not. Each thread has some bits of information but not quite the full picture (e.g. are these a 1 Application + N Connections or N Applications + Connections, are they using Organizations or classic Universal Login).

Before we switched to Auth0 Organisations, we also used separate Auth0 Applications and Auth0 Connections for all of our tenants. If you are only using one Next.js application for all your tenants, then you need to use initAuth0 to pass a different client ID and client secret to the Auth0 Next.js SDK.

Auth0 Organisations make this a little easier because you only need one Auth0 Application and one Auth0 Connection for all your tenants.

The only issue we ran into when we migrated to Auth0 Organisations was that we wanted to have one page (at the "base" domain) where tenants could enter their tenant name to get redirected to their tenant specific login page (we need separate login pages because every tenant has the possibility to "Continue with ..." their own identity provider). We could have build this overview page ourselves (and probably will in the future) but at least until now we successfully use the work-around that I described above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants