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

redirectUri and postLogoutRedirectUri based on headers #108

Closed
krystian50 opened this issue May 15, 2020 · 17 comments
Closed

redirectUri and postLogoutRedirectUri based on headers #108

krystian50 opened this issue May 15, 2020 · 17 comments
Labels
enhancement New feature or request

Comments

@krystian50
Copy link

Description

Now and Vercel CLI allows to deploy app dynamically (preview mode). Unfortunately, there is no way to get the deployment URL on build time. Is it possible to add behavior for redirectUri and postLogoutRedirectUri to be set as the value of header eg. host or authority?

@krystian50 krystian50 changed the title Dynamic redirectUri redirectUri and postLogoutRedirectUri based on headers May 15, 2020
@nodabladam
Copy link

This is what I quickly hacked together so I could use Vercel

auth0.js

import { initAuth0 } from "@auth0/nextjs-auth0";

const defaultSettings = {
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  ...
};

export function preAuth0(req) {
 
  const deploymentUrl = req.headers
    ? req.headers["x-custom-host"] || req.headers?.host || null
    : null;

  const settings = {
    redirectUri:
      process.env.REDIRECT_URI || "https://" + deploymentUrl + "/api/callback",
    postLogoutRedirectUri:
      process.env.POST_LOGOUT_REDIRECT_URI || "https://" + deploymentUrl + "/"
  };

  return initAuth0({
    ...defaultSettings,
    ...settings
  });
}

someOtherFile.js - use preAuth0 and pass request to it

   import { preAuth0 } from "../../lib/auth0";
   ...
   const auth0 = preAuth0(req);

@krystian50
Copy link
Author

@nodabladam I'm aware of this solution. The thing is, each time you call this function, you actually reinitialize auth0. So in case of calling the same endpoint twice, you init auth0 twice, but there's no domain changed, so it's pointless. I know it's not so bad in case of serverless as it is in a traditional server. However, while lambda is still active, it's simply a pointless calculation.

A good option could be to memorize the result, so actual initialization would be performed once, at first shot. But it's still a bunch of hacking.

I believe this library should simply allow setting up custom options during runtime or simply to use relative paths (redirectUri - /api/callback)

@krystian50
Copy link
Author

Also, because of wrapping initAuth0 in function with req, you can't use requireAuthentication

@jacksonblankenship
Copy link

@krystian50 Did you ever figure out a solution to this issue?

@philihp
Copy link

philihp commented Jul 18, 2020

My Preview URLs work when I use the canonical URL (e.g. https://project-d686nh684.vercel.app/), rather than the branch preview URL (e.g. https://project-git-preview-branch-name.project.vercel.app/) which doesn't work. The Preview URL is given to the PR, but the canonical URL is the one given by VERCEL_URL.

I setup a redirect on my /api/login, which will redirect first to the canonical URL before tryna login.

import absoluteUrl from 'next-absolute-url'

export default async function login(request, response) {
  try {
    const baseUrl = process.env.VERCEL_URL
      ? `https://${process.env.VERCEL_URL}`
      : 'http://localhost:3000'
    const requestUrl = absoluteUrl(request).origin
    if (baseUrl !== requestUrl) {
      response.writeHead(301, { Location: `${baseUrl}${request.url}` })
      response.end()
    } else {
      await auth0.handleLogin(request, response)
    }
  } catch (error) {
    console.error(error)
    response.status(error.status || 500).end(error.message)
  }
}

This unfortunately works for preview domains, but fails for my production domain.

@cyrus-za
Copy link

I added BASE_URL as environment variable in production (on vercel dashboard) and left it blank in preview mode.

In my code I check if process.env.BASE_URL is defined else I set it to https://${process.env.VERCEL_URL}

That way it will set ti the cannonical url in preview, but in production it will use the production url.

I do not currently use the git integration (and manually deploy through CLI with git hooks) due to having a monorepo (waiting on vercel monorepo support to be finished). If I were to use git, I'm sure you could figure the url out with the branch name (process.env.VERCEL_GITHUB_COMMIT_REF)
See https://vercel.com/docs/v2/build-step#system-environment-variables

It's a pain to have to manually do it, but for now that works.

Auth0 requires you to set these domains in the allowed origins and callbacks etc too. Seeing as you dont know up front what random url will be generated, use a wildcard, with your project name in front and vercel.app at the end.

anandaroop added a commit to anandaroop/airtable-map-viewer that referenced this issue Sep 2, 2020
@Widcket Widcket added the enhancement New feature or request label Jan 18, 2021
@adamjmcgrath
Copy link
Contributor

Hi @krystian50 - thanks for raising this

Unfortunately, there is no way to get the deployment URL on build time.

With the new Beta you will not need the url details at build time.

We recommend you check it out here https://github.com/auth0/nextjs-auth0/tree/beta/src

There is still an issue with one named export from the beta requiring env vars at build time and we're leaving #154 open to track that work to fix it

@jakejscott
Copy link

This is what I did to get this working, hopefully it might help someone. I think it works... have done some basic testing for custom domains in vercel + preview urls and local dev.

const audience = process.env.AUTH0_AUDIENCE;
const scope = process.env.AUTH0_SCOPE;

function getUrls(req: NextApiRequest) {
  const host = req.headers['host'];
  const protocol = process.env.VERCEL_URL ? 'https' : 'http';
  const redirectUri = `${protocol}://${host}/api/auth/callback`;
  const returnTo = `${protocol}://${host}`;
  return {
    redirectUri,
    returnTo
  };
}

export default handleAuth({
  async callback(req: NextApiRequest, res: NextApiResponse) {
    try {
      const { redirectUri } = getUrls(req);
      await handleCallback(req, res, { redirectUri: redirectUri });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  },

  async login(req: NextApiRequest, res: NextApiResponse) {
    try {
      const { redirectUri, returnTo } = getUrls(req);

      await handleLogin(req, res, {
        authorizationParams: {
          audience: audience,
          scope: scope,
          redirect_uri: redirectUri
        },
        returnTo: returnTo
      });
    } catch (error) {
      res.status(error.status || 400).end(error.message);
    }
  },

  async logout(req: NextApiRequest, res: NextApiResponse) {
    const { returnTo } = getUrls(req);
    await handleLogout(req, res, {
      returnTo: returnTo
    });
  }
});

In Auth0 dev environment I had the following settings:

Allowed Callback URLs:

http://localhost:3000/api/auth/callback, https://mycooldomain.xyz/api/auth/callback, https://*.vercel.app/api/auth/callback

Allowed Logout URLs:

http://localhost:3000, https://mycooldomain.xyz, https://*.vercel.app

@fishactual
Copy link

This is what I did to get this working, hopefully it might help someone. I think it works... have done some basic testing for custom domains in vercel + preview urls and local dev.

const audience = process.env.AUTH0_AUDIENCE;
const scope = process.env.AUTH0_SCOPE;

function getUrls(req: NextApiRequest) {
  const host = req.headers['host'];
  const protocol = process.env.VERCEL_URL ? 'https' : 'http';
  const redirectUri = `${protocol}://${host}/api/auth/callback`;
  const returnTo = `${protocol}://${host}`;
  return {
    redirectUri,
    returnTo
  };
}

export default handleAuth({
  async callback(req: NextApiRequest, res: NextApiResponse) {
    try {
      const { redirectUri } = getUrls(req);
      await handleCallback(req, res, { redirectUri: redirectUri });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  },

  async login(req: NextApiRequest, res: NextApiResponse) {
    try {
      const { redirectUri, returnTo } = getUrls(req);

      await handleLogin(req, res, {
        authorizationParams: {
          audience: audience,
          scope: scope,
          redirect_uri: redirectUri
        },
        returnTo: returnTo
      });
    } catch (error) {
      res.status(error.status || 400).end(error.message);
    }
  },

  async logout(req: NextApiRequest, res: NextApiResponse) {
    const { returnTo } = getUrls(req);
    await handleLogout(req, res, {
      returnTo: returnTo
    });
  }
});

In Auth0 dev environment I had the following settings:

Allowed Callback URLs:

http://localhost:3000/api/auth/callback, https://mycooldomain.xyz/api/auth/callback, https://*.vercel.app/api/auth/callback

Allowed Logout URLs:

http://localhost:3000, https://mycooldomain.xyz, https://*.vercel.app

Awesome. This works nicely! Thanks @jakejscott 😄

@mattrossman
Copy link

@jakejscott What did you set AUTH0_BASE_URL to to make this work? And did you use the default instance or set up a custom one? When I try this solution with a blank AUTH0_BASE_URL I get a 500 error:

TypeError: "baseURL" is required
    at get (/var/task/node_modules/@auth0/nextjs-auth0/dist/auth0-session/get-config.js:164:15)
    at getConfig (/var/task/node_modules/@auth0/nextjs-auth0/dist/config.js:78:43)
    at _initAuth (/var/task/node_modules/@auth0/nextjs-auth0/dist/index.js:34:37)
    at getInstance (/var/task/node_modules/@auth0/nextjs-auth0/dist/index.js:23:38)
    at handleAuth (/var/task/node_modules/@auth0/nextjs-auth0/dist/index.js:150:18)
    at /var/task/.next/server/pages/api/auth/[...auth0].js:65:129

If I use the production AUTH0_BASE_URL then I get errors about an incorrect redirect_url being received in the callback handler.

@mattrossman
Copy link

I got it working by combining Jake's suggested solution with another from #420 (comment)

First I expose a function to create a memoized instance of the Auth0Server. This ensures that baseURL is properly set, even if the AUTH0_BASE_URL environment variable is not set.

// server/auth.ts

import { type Auth0Server, initAuth0 } from "@auth0/nextjs-auth0";
import { type IncomingMessage } from "http";

const instances = new Map<string, Auth0Server>();

export function getAuth0Urls(req: IncomingMessage) {
  const host = req.headers["host"];
  if (!host) throw new Error("Missing host in headers");

  const protocol = process.env.VERCEL_URL ? "https" : "http";
  const redirectUri = `${protocol}://${host}/api/auth/callback`;
  const returnTo = `${protocol}://${host}`;
  const baseURL = `${protocol}://${host}`;
  return {
    baseURL,
    redirectUri,
    returnTo,
  };
}

export function getAuth0Instance(req: IncomingMessage) {
  const { baseURL } = getAuth0Urls(req);

  let instance = instances.get(baseURL);

  if (!instance) {
    instance = initAuth0({ baseURL });
    instances.set(baseURL, instance);
  }

  return instance;
}

Then I use this instance to call .handleAuth() and its handlers, as Jake showed above. This sets the appropriate redirect_uri and returnTo URLs. If you use the regular named imports like handleCallback, it will have the wrong baseURL.

// pages/api/auth/[...auth0].js

import { type NextApiHandler } from "next";
import { getAuth0Instance, getAuth0Urls } from "~/server/auth";

const handler: NextApiHandler = (req, res) => {
  const instance = getAuth0Instance(req);

  const instanceHandler = instance.handleAuth({
    login: async (req, res) => {
      const { redirectUri, returnTo } = getAuth0Urls(req);

      await instance.handleLogin(req, res, {
        authorizationParams: {
          redirect_uri: redirectUri,
        },
        returnTo: returnTo,
      });
    },
    callback: async (req, res) => {
      const { redirectUri, returnTo } = getAuth0Urls(req);

      await instance.handleCallback(req, res, {
        authorizationParams: {
          redirect_uri: redirectUri,
        }
      });
    },
    logout: async (req, res) => {
      const { redirectUri, returnTo } = getAuth0Urls(req);

      await instance.handleLogout(req, res, {
        returnTo: returnTo,
      });
    },
  });

  instanceHandler(req, res);
};

export default handler;

With this setup, I don't need to set AUTH0_BASE_URL, and it works in both Production and Preview deploys, both from hashed URLs and branch URLs.

@CharlesOuverleaux
Copy link

@mattrossman thank you for your solution, this is amazing work. It is the only setup for me that worked with Vercel preview AND production deploys.

@janjachacz
Copy link

janjachacz commented Oct 9, 2023

Thx @ jakejscott,
finally made it work w/ Next app's router

import { AppRouteHandlerFnContext, handleAuth, handleCallback, handleLogin, handleLogout } from "@auth0/nextjs-auth0";
import { NextRequest, NextResponse } from "next/server";

export const GET = handleAuth({
    login: async (req: NextRequest, res: AppRouteHandlerFnContext) => {
        const { redirectUri, returnTo } = getAuth0Urls(req);
        return await handleLogin(req as NextRequest, res, {
            authorizationParams: {
                redirect_uri: redirectUri,
            },
            returnTo,
        });
    },
    callback: async (req: NextRequest, res: AppRouteHandlerFnContext) => {
        const { redirectUri } = getAuth0Urls(req);
        return await handleCallback(req, res, {
            redirectUri,
        });
    },
    logout: async (req: NextRequest, res: AppRouteHandlerFnContext) => {
        const { returnTo } = getAuth0Urls(req);
        return await handleLogout(req, res, {
            returnTo,
        });
    },
});

function getAuth0Urls(req: NextRequest) {
    const protocol = req.nextUrl.protocol;
    const host = req.nextUrl.host;
    const search = req.nextUrl.search;

    return {
        baseURL: `${protocol}//${host}`,
        redirectUri: `${protocol}//${host}/api/auth/callback${search}`,
        returnTo: `${protocol}//${host}${search}`,
    };
}

@ADTC
Copy link
Contributor

ADTC commented Jan 24, 2024

@janjachacz for your comment above, I suggest adding tsx after the three backticks above the code block so that the syntax highlighting will work.

In any case, I found that you don't need any of these complex changes. Just follow what's here on this example page, and it will work just fine. I didn't need to touch handleAuth at all. (You can skip modding the build command and output directory.)

@jdhurst
Copy link

jdhurst commented Mar 26, 2024

@mattrossman

1,000 blessings upon you ❤️

@nguaman
Copy link

nguaman commented Apr 11, 2024

Hello,
Thanks to @janjachacz, @jakejscott , and @mattrossman for their versions of the solutions.
Thanks to them, I was able to come up with a solution that works for my use case.

My main intention was not to have to use a custom SDK.

Stack: Next.js 14.1.4 (App router)+ Auth0 (Linkedin + Organizations)
Use case: Each client has its own organization within Auth0, which has a specific subdomain.

- src/app/api/auth/[auth0]/route.ts

import { AppRouteHandlerFnContext, handleAuth, handleCallback, handleLogin, handleLogout } from "@auth0/nextjs-auth0";
import { NextRequest } from "next/server";

interface DomainMap {
    [key: string]: string;
};


function getAuth0Urls(req: NextRequest) {

    const host = req.headers.get("host");

    if (!host) throw new Error("Missing host in headers");

    const protocol = process.env.NODE_ENV === "development" ? "http" : "https";

    const redirectUri = `${protocol}://${host}/api/auth/callback`;
    const returnTo = `${protocol}://${host}`;
    const baseURL = `${protocol}://${host}`;

    return {
        baseURL,
        redirectUri,
        returnTo,
    };
}

function getOrganizationIdFromSubdomain(req: Request) {

    // Define a map of subdomains to organization IDs
    // * Here's a connection to Prisma that I replaced with a dictionary.
    // company_a.domain.com, company_b.domain.com, ...
    
    const domains: DomainMap = {
        "company_a": "org_...",
        "compaby_b": "org_..."
    }

    // @ts-ignore
    const host = req.headers.get("host");

    // Extract the subdomain from the hostname
    const subdomain = host?.split('.')[0];

    // Get the organization ID from the domains map using the subdomain
    const organizationId = subdomain ? domains[subdomain] : undefined;

    if (!organizationId) {
        throw new Error(`Failed to get organization ID from subdomain. Subdomain ${subdomain} not found in domain map.`);
    }

    return organizationId;

}

export const GET = handleAuth({
    login: async (req: NextRequest, res: AppRouteHandlerFnContext) => {
        
        const { redirectUri, returnTo } = getAuth0Urls(req);

        const organizationId = getOrganizationIdFromSubdomain(req);

        return await handleLogin(req as NextRequest, res, {
            authorizationParams: {
                redirect_uri: redirectUri,
                organization: organizationId,
                prompt: 'login'
            },
            returnTo,
        });

    },
    callback: async (req: NextRequest, res: AppRouteHandlerFnContext) => {
        const { redirectUri } = getAuth0Urls(req);

        const organizationId = getOrganizationIdFromSubdomain(req);

        return await handleCallback(req, res, {
            redirectUri,
            organization: organizationId,
        });

    },
    logout: async (req: NextRequest, res: AppRouteHandlerFnContext) => {

        const { returnTo } = getAuth0Urls(req);

        return await handleLogout(req, res, {
            returnTo
        });
    },

});

- src/middleware.ts (https://vercel.com/guides/nextjs-multi-tenant-application)

import { NextRequest, NextResponse } from "next/server";
import { withMiddlewareAuthRequired, getSession } from "@auth0/nextjs-auth0/edge";

export const config = {
 matcher: [
   /*
    * Match all paths except for:
    * 1. /api routes
    * 2. /_next (Next.js internals)
    * 3. /_static (inside /public)
    * 4. all root files inside /public (e.g. /favicon.ico)
    */
   "/((?!api/|_next/|_static/|[\\w-]+\\.\\w+).*)",
 ],
};

export default withMiddlewareAuthRequired(async function middleware(req: NextRequest) {

 const url = req.nextUrl;
 const hostname = req.headers.get("host");
 const session = await getSession();

 if (!session) {
   return NextResponse.redirect("/api/auth/login");
 }

 const searchParams = req.nextUrl.searchParams.toString();
 const path = `${url.pathname}${searchParams.length > 0 ? `?${searchParams}` : ""}`;

 return NextResponse.rewrite(new URL(`/${hostname}${path}`, req.url));

});

And my organization configuration within my application in Auth0.

image

@Xamsix
Copy link

Xamsix commented Oct 11, 2024

Thx @ jakejscott, finally made it work w/ Next app's router

import { AppRouteHandlerFnContext, handleAuth, handleCallback, handleLogin, handleLogout } from "@auth0/nextjs-auth0";
import { NextRequest, NextResponse } from "next/server";

export const GET = handleAuth({
    login: async (req: NextRequest, res: AppRouteHandlerFnContext) => {
        const { redirectUri, returnTo } = getAuth0Urls(req);
        return await handleLogin(req as NextRequest, res, {
            authorizationParams: {
                redirect_uri: redirectUri,
            },
            returnTo,
        });
    },
    callback: async (req: NextRequest, res: AppRouteHandlerFnContext) => {
        const { redirectUri } = getAuth0Urls(req);
        return await handleCallback(req, res, {
            redirectUri,
        });
    },
    logout: async (req: NextRequest, res: AppRouteHandlerFnContext) => {
        const { returnTo } = getAuth0Urls(req);
        return await handleLogout(req, res, {
            returnTo,
        });
    },
});

function getAuth0Urls(req: NextRequest) {
    const protocol = req.nextUrl.protocol;
    const host = req.nextUrl.host;
    const search = req.nextUrl.search;

    return {
        baseURL: `${protocol}//${host}`,
        redirectUri: `${protocol}//${host}/api/auth/callback${search}`,
        returnTo: `${protocol}//${host}${search}`,
    };
}

Thank you so much, this just helped me a lot!

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