Next-auth: callbackUrl does not redirect upon sigin-in

Created on 19 Aug 2020  路  17Comments  路  Source: nextauthjs/next-auth

Your question
Why does the callbackUrl not redirect?

What are you trying to do
I am using google provider for OAuth signing. After signing it does not redirect to the query stringified callbackUrl (Which is correct on the sign-in page). It says on the same page (AND successfully logs in).

Package.json
"next": "latest",
"next-auth": "^3.1.0"

Feedback

  • [x] Found the documentation helpful
  • [] Found documentation but was incomplete
  • [] Could not find relevant documentation
  • [] Found the example project helpful
  • [x] Did not find the example project helpful
incomplete question

Most helpful comment

I had the same issue when trying to use this example with Github provider. I was always being redirected to the http://localhost:3000/auth/signin?callbackUrl=http://localhost:3000/ page.

I worked around it by explicitly passing the callbackUrl with the value from the query string:

const SignIn = ({ providers }: Props) => {
  const router = useRouter()

  return (
    <>
      {Object.values(providers).map((provider) => (
        <div key={provider.name}>
          <button
            onClick={() => signIn(provider.id, { callbackUrl: router.query.callbackUrl })}
          >
            Sign in with {provider.name}
          </button>
        </div>
      ))}
    </>
  )
}

I actually find the following code suspicious in https://github.com/nextauthjs/next-auth/blob/main/src/client/index.js#L224:

var callbackUrl = args && args.callbackUrl ? args.callbackUrl : window.location;

If we don't pass the callbackUrl arg, we end up using the current url which is http://localhost:3000/auth/signin?callbackUrl=http://localhost:3000/. I think we should extract the callbackUrl from the query string here. 馃

All 17 comments

Can you provide an example of how you are using it that is not working for you? Thanks!

Hi @iaincollins , sure thing apologies for the incompleteness.

Relevant parts of the package.json

{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "heroku-postbuild": "npm run build",
    "export": "next export",
    "start": "next start -p $PORT"
  },
  "dependencies": {
    "mongodb": "^3.6.0",
    "next": "latest",
    "next-auth": "^3.1.0",
    "node-jose": "^1.1.4",
    "node-sass": "^4.14.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
  },
  "engines": {
    "node": "13.x"
  },
}

[...nextauth].js

import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'

const options = {
  providers: [
    Providers.Google({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET
    }),
  ],
  database: process.env.DATABASE_URL,
  secret: process.env.JWT_SECRET,

  session: {
    jwt: true,
  },
  jwt: {
    signingKey: process.env.JWT_SIGNING_PRIVATE_KEY,
    secret: process.env.JWT_SECRET
  },

  pages: {
    signIn: '/auth/signin',
  },
  callbacks: { },
  events: { },

  debug: false,
}

export default (req, res) => NextAuth(req, res, options)

Then the page we use for signin:

import { providers, signIn } from 'next-auth/client'

const SignIn = ({ providers }) => {
  return (
    <div className="page-signin">
      <div className="page-signin__wrapper">
        <h1>Sign in</h1>
        <img className="page-signin__img" src={`/images/login.svg`} />
        <div className="page-signin__providers">
          {Object.values(providers).map(provider => (
            <div key={provider.name} className="page-signin__provider">
              <button onClick={() => signIn(provider.id)}>
                <img src={`/images/${provider.name.toLowerCase()}.svg`} />
                <span>Sign with {provider.name}</span>
              </button>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

SignIn.getInitialProps = async (context) => {
  return {
    providers: await providers(context)
  }
}

export default SignIn

Relevant environment variables:

NEXTAUTH_URL=http://localhost:3000 (or whatever the domain is ie. https://example.com

Online example of the issue:

http://easy-meal-prep.herokuapp.com/auth/signin

Thanks 馃檹

Hi @iaincollins , did you have a chance to look into it? We tried to replicate also: https://github.com/nextauthjs/next-auth-example which is the barebones app, and even if it works in the example it does not in our codebase...

I got same issue when I tried to use a custom page for signin.

Yep we had to do a patch job to go around it....being the following:

///[...nextauth].js
 callbacks: {
    redirect: async (url, baseUrl) => {
      return Promise.resolve(url)
    }
  }

Hi there,

Yep we had to do a patch job to go around it....being the following:

///[...nextauth].js
 callbacks: {
    redirect: async (url, baseUrl) => {
      return Promise.resolve(url)
    }
  }

The purpose of the URL checking by the redirect handler is to prevent people from creating URLs that trick people into signing in to your site, then redirecting to a malicious site (e.g. a clone of your site) which then tricks people into providing additional private information.

This protection is built in to most OAuth providers, but to make it easier to manage and configure for all providers - and to ensure this protection is added to all providers - NextAuth.js abstracts this behaviour.

The default redirect handler simply checks the URL starts with the same URL as you have configured for your site. If you want to allow different URLs (e.g. https://www.example.com, https://account.example.com) then you can use a custom handler for the redirect callback.

This is the default behaviour:

callbacks: {
  /**
   * @param  {string} url      URL provided as callback URL by the client
   * @param  {string} baseUrl  Default base URL of site (can be used as fallback)
   * @return {string}          URL the client will be redirect to
   */
  redirect: async (url, baseUrl) => {
    return url.startsWith(baseUrl)
      ? Promise.resolve(url)
      : Promise.resolve(baseUrl)
  }
}

If you disable all checking and simply return Promise.resolve(url) it makes it possible for bad actors to send people links to your site with callbackUrls that forward them to a URL they control, and can trick end users. For this reason it's not recommended.

If this approach resolves the problem you are seeing, you probably just need different logic in the callback, or to check that the callback URL and NEXTAUTH_URL is correct (e.g. both start with https:// and include matching hostnames, etc.)

@iaincollins Thank you for your details explanation.

My situation is straightforward. I use Facebook login only, but I want to customize my login page.

Here is my config [...nextauth].js

import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import Adapters from "next-auth/adapters";
import User from "@src/libs/auth";
const options = {
  // Configure one or more authentication providers
  providers: [
    Providers.Facebook({
      clientId: process.env.FACEBOOK_CLIENT_ID,
      clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
    }),
  ],

  pages: {
    signIn: '/auth/signin',
  },

  callbacks: {
    session: async (session, user) => {
      session.id = user.id;
      return Promise.resolve(session);
    },
  },

  jwt: {
    // A secret to use for key generation - you should set this explicitly
    // Defaults to NextAuth.js secret if not explicitly specified.
    secret: process.env.JWT_SECRET,
  },

  adapter: Adapters.TypeORM.Adapter(
    // The first argument should be a database connection string or TypeORM config object
    process.env.DATABASE_URL,
    // The second argument can be used to pass custom models and schemas
    {
      models: {
        User: User.User,
      },
    },
  ),

  // A database is optional, but required to persist accounts in a database
  database: process.env.DATABASE_URL,
};

export default (req, res) => NextAuth(req, res, options);

And this is my auth/signin.js

import { Button } from "@src/components/buttons";
import Layout from "@src/components/layout";
import React from "react";
import { providers, signIn } from "next-auth/client";

const SignIn = ({ providers }) => (
  <Layout>
        {Object.values(providers).map((provider) => (
          <div key={provider.name} >
            <Button onClick={() => signIn(provider.id)}> Login by {provider.name} </Button>
          </div>
        ))}
  </Layout>
);
export default SignIn;

SignIn.getInitialProps = async (context) => {
  return {
    providers: await providers(context),
  };
};

I am developing my app on my local machine and thus the NEXTAUTH_URL is http://localhost:3000.

The signin page has successfully showed the customization for this page, but when I click the login button it keeps redirect me back to signin page. (/auth/signin) If I delete the pages option, everything works fine.

I'm not sure is there any thing I missed for doing the customized sigin page?

I appreciate any hints or helps for my question.

yes i also facing the same issue with custom login page

This is how i solved this..

changed my getInitialProps -

SignIn.getInitialProps = async (context) => {
  const { req, res, query } = context;
  const session = await getSession({ req });

  const { callbackUrl } = query;

  if (session && res && session.accessToken) {
    res.writeHead(302, {
      Location: callbackUrl,
    });
    res.end();
    return;
  }

  return {
    session: undefined,
    providers: await providers(context),
  };
};

Same issue for me, it happens when I change the default page for the sign in page

pages: {
  signIn: "/signin",
},

In a pages/protected.tsx file I got the following

const onSignInClickHandler = () => {
  signIn();
};

<button onClick={onSignInClickHandler}>Sign in</button>

When clicking the Sign in button it redirects to http://localhost:3000/signin?callbackUrl=http://localhost:3000 notice callbackUrl should be http://localhost:3000/protected

After login with auth provider in this case Google. the user is redirected to http://localhost:3000. So I guess redirection is working fine the problem is the value we pass to callbackUrl.

Doesn't work even when using

signIn(undefined, {
  callbackUrl: window.location.href,
});

I'm using:

"next": "9.5.3",
"next-auth": "^3.1.0",
"react": "16.13.1",

It looks like I figured it out. I was doing

signIn(undefined, {
  callbackUrl: window.location.href,
});

in the pages/protected.tsx but just doing signIn(provider.id) in the pages/signin.tsx page.

I've fixed it in pages/signin.tsx by doing the following;

let redirectUrl = "http://location:3000";

useEffect(() => {
  const url = new URL(location.href);
  redirectUrl = url.searchParams.get("callbackUrl")!;
});
<button
  onClick={() => {
    signIn("google", {
      callbackUrl: redirectUrl,
    });
  }}
>
  Sign in with Google
</button>

Now it redirects to the original page the Sign In page was invoked.

I was following what the documentation says about creating custom pages for the Sign In Form. https://next-auth.js.org/configuration/pages#oauth-sign-in

I thought the callbackURL value from the URL was taken automatically by the signIn method. It wasn't clear in the documentation.

@asumaran

I thought the callbackURL value from the URL was taken automatically by the signIn method. It wasn't clear in the documentation.

Hmm yes it should be. The code that does this is here:
https://github.com/nextauthjs/next-auth/blob/main/src/client/index.js#L224

const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location

The only reason I can think this isn't working is if visitors are visiting the site using a different URL than the one configured as the canonical URL and that the default redirect handler is rejecting it (for security reasons).

e.g. a protocol, hostname or port mismatch.

@iaincollins Actually, I was looking at that method too. It could be I'm misusing the signIn method. What I'm doing is to protect some pages and redirect the user automatically to the "Sign In" page and then redirect them back to the "protected" page once the user has successfully signed in. Something like this:

user visits protected page (/protected)
  if user is logged in
    display protected page (stay in /protected)
  if user is NOT logged in
    redirect to Sign In page and pass the URL as `callbackUrl` (/signin?callbackUrl=http://localhost:3000/protected)

Initially, I'm doing the redirection manually with user interaction. So in page/protected.tsx I'm just doing signIn() when clicking on a button. I'm doing it this way for now. It's probably better to do that in the backend. So far it works as expected the user is redirected to the custom Sign In page I'm using (pages/signin.tsx) with the correct callbackUrl (/signin?callbackUrl=http://localhost:3000/protected)

In the custom Sign In page (/signin?callbackUrl=http://localhost:3000/protected) I was just doing signIn("google"). Notice the callbackURL is in the URL and is not used anywhere. And according to the method you shared I don't see how thesignIn method will get the callbackUrl from the URL.

I realized I had to pass the callbackUrl param manually after looking at https://github.com/nextauthjs/next-auth/blob/e0655527845107ef236633237b846f386758c90c/src/server/pages/signin.js#L55-L60

I think the signIn method should be updated and be able to get the callbackUrl from the URL If not passed kind of the way I did.

const url = new URL(location.href);
const callbackUrl = url.searchParams.get("callbackUrl");

Probably we should also verify the value of callbackUrl belongs to the same host for security purposes.

I had the same issue when trying to use this example with Github provider. I was always being redirected to the http://localhost:3000/auth/signin?callbackUrl=http://localhost:3000/ page.

I worked around it by explicitly passing the callbackUrl with the value from the query string:

const SignIn = ({ providers }: Props) => {
  const router = useRouter()

  return (
    <>
      {Object.values(providers).map((provider) => (
        <div key={provider.name}>
          <button
            onClick={() => signIn(provider.id, { callbackUrl: router.query.callbackUrl })}
          >
            Sign in with {provider.name}
          </button>
        </div>
      ))}
    </>
  )
}

I actually find the following code suspicious in https://github.com/nextauthjs/next-auth/blob/main/src/client/index.js#L224:

var callbackUrl = args && args.callbackUrl ? args.callbackUrl : window.location;

If we don't pass the callbackUrl arg, we end up using the current url which is http://localhost:3000/auth/signin?callbackUrl=http://localhost:3000/. I think we should extract the callbackUrl from the query string here. 馃

Thanks, @ValentinH.
Just ran into this issue while building a custom login page and your onClick update fixed the issue.

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!

This is still relevant :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

iaincollins picture iaincollins  路  3Comments

iaincollins picture iaincollins  路  3Comments

eatrocks picture eatrocks  路  3Comments

alex-cory picture alex-cory  路  3Comments

Xetera picture Xetera  路  3Comments