Skip to content

v3 - CALLBACK_OAUTH_ERROR (Invalid state...) - Custom Provider Azure AD B2C #468

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

Closed
1 of 5 tasks
BenjaminWFox-Lumedic opened this issue Jul 23, 2020 · 20 comments
Closed
1 of 5 tasks

Comments

@BenjaminWFox-Lumedic
Copy link

BenjaminWFox-Lumedic commented Jul 23, 2020

Have moved my initial v2 implementation to v3. It was very easy! Everything works as far as I can tell except for the state parameter.

Describe the bug
Using custom provider Azure AD B2C next-auth gives an error (see below) in the callback after successful authentication with Azure AD B2C. This seems to happen whether or not I set useState: false.

This is a new after moving to v3. The same config had worked in v2.

B2C supports the state parameter. I don't think this is an issue with the authorization server, but I could be mistaken. From the B2C link, state is supported as:

A value included in the request that's also returned in the token response. It can be a string of any content that you want. A randomly generated unique value is typically used for preventing cross-site request forgery attacks. The state is also used to encode information about the user's state in the application before the authentication request occurred, such as the page they were on.

I've confirmed (see below) that the state provided by the authorization request is the same as the state returned from the authorization server.

To Reproduce
If this is not something obvious I might have missed please let me know, and I will set up a minimal reproduction.

Expected behavior
I would expect that if the state provided initially by the client & sent back by the authorization server are the same than I should not get an error.

Screenshots or error logs
The B2C Custom Provider looks like:

    {
      id: 'azureb2c',
      name: 'Azure B2C',
      type: 'oauth',
      version: '2.0',
      debug: true,
      scope: 'offline_access openid',
      // params: {
      //   grant_type: 'authorization_code',
      // },
      accessTokenUrl: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${userFlow}/oauth2/v2.0/token`,
      // requestTokenUrl: 'https://login.microsoftonline.com/${process.env.DIRECTORY_ID}/oauth2/v2.0/token',
      authorizationUrl: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${userFlow}/oauth2/v2.0/authorize?response_type=code+id_token&response_mode=form_post`,
      profileUrl: 'https://graph.microsoft.com/oidc/userinfo',
      profile: (profile) => {
        console.log('THE PROFILE', profile)

        return {
          id: profile.oid,
          fName: profile.given_name,
          lName: profile.surname,
          email: profile.emails.length ? profile.emails[0] : null,
        }
      },
      clientId: process.env.AUTH_CLIENT_ID,
      clientSecret: process.env.AUTH_CLIENT_SECRET,
      idToken: true,
      // useState: false,
    },

The B2C authorize url looks like:

https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{flow}/oauth2/v2.0/authorize
?response_type=code+id_token
&response_mode=form_post
&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fcallback%2Fazureb2c
&scope=offline_access%20openid
&state=45e76b516360e8aa4e79d44661344ba06f8ed0fc8a08beb3362bcbe7cde2fe90
&client_id=<client_id>

The Form Data response includes (along with the code & id_token):

state: 45e76b516360e8aa4e79d44661344ba06f8ed0fc8a08beb3362bcbe7cde2fe90

The next-auth error looks like:

[next-auth][error][callback_oauth_error] Error: Invalid state returned from oAuth provider
    at <irrelevant-project-path>/node_modules/next-auth/dist/server/lib/oauth/callback.js:46:27
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (<irrelevant-project-path>/node_modules/next-auth/dist/server/lib/oauth/callback.js:26:103)
    at _next (<irrelevant-project-path>/node_modules/next-auth/dist/server/lib/oauth/callback.js:28:194)
    at <irrelevant-project-path>/node_modules/next-auth/dist/server/lib/oauth/callback.js:28:364
    at new Promise (<anonymous>)
    at <irrelevant-project-path>/node_modules/next-auth/dist/server/lib/oauth/callback.js:28:97
    at <irrelevant-project-path>/node_modules/next-auth/dist/server/lib/oauth/callback.js:143:17
    at <irrelevant-project-path>/node_modules/next-auth/dist/server/routes/callback.js:58:31
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (<irrelevant-project-path>/node_modules/next-auth/dist/server/routes/callback.js:26:103)
    at _next (<irrelevant-project-path>/node_modules/next-auth/dist/server/routes/callback.js:28:194)
    at <irrelevant-project-path>/node_modules/next-auth/dist/server/routes/callback.js:28:364
    at new Promise (<anonymous>)
    at <irrelevant-project-path>/node_modules/next-auth/dist/server/routes/callback.js:28:97
    at <irrelevant-project-path>/node_modules/next-auth/dist/server/routes/callback.js:302:17
https://next-auth.js.org/errors#callback_oauth_error

Additional context
No additional context I can think of.

Documentation feedback
Documentation refers to searching through online documentation, code comments and issue history. The example project refers to next-auth-example.

  • 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
@BenjaminWFox-Lumedic BenjaminWFox-Lumedic added the bug Something isn't working label Jul 23, 2020
@iaincollins iaincollins removed the bug Something isn't working label Jul 24, 2020
@iaincollins
Copy link
Member

I believe the current option for this is state: true (and that useState is from an earlier v3 beta).

It's safe to use that option if a particular provider is doing something unexpected.

The only built in Provider I know has a problem with it is Apple.

The token used to verify state is generated is from the csrfToken - if that isn't being set for any reason then you might run into a problem, but in v3 you shouldn't be able to start an authorisation flow without csrfToken being set.

@BenjaminWFox-Lumedic
Copy link
Author

BenjaminWFox-Lumedic commented Jul 27, 2020

Gotcha, state: false is working for this.

Is the gist of this, then, that the csrfToken provides basically the same functionality as state?

I will close this. If I find out anything useful about what Azure B2C is doing I'll follow up. Thanks!

@RobbyUitbeijerse
Copy link

RobbyUitbeijerse commented Aug 18, 2020

Hey @BenjaminWFox-Lumedic , so far connecting with b2c has been a tedious process.

Any chance you could share your full setup for using next-auth with AD b2c?

@BenjaminWFox
Copy link
Contributor

@RobbyUitbeijerse I created a minimal example repo and wrote up the steps I took to set it up - take a look, let me know if anything in it is off: https://benjaminwfox.com/blog/tech/how-to-configure-azure-b2c-with-nextjs

@WaysToGo
Copy link

WaysToGo commented Sep 11, 2020

@BenjaminWFox-Lumedic With the above config code accessToken is undefined, were you able to make it work?.

@BenjaminWFox-Lumedic
Copy link
Author

@WaysToGo my example is using JWTs, so accessToken will not be generated (see warning here).

I believe you will have to use a database for your scenario, but I have not explored that route so can't comment specifically on implementation details :(

@BenjaminWFox
Copy link
Contributor

@WaysToGo in case this is relevant, I hadn't noticed before but there is the option to include 'Identity Provider Access Token' as a claim in the Azure B2C user flow.

I didn't have that box checked in my walkthrough, unsure if checking it (if you haven't already) would help in your case.

@WaysToGo
Copy link

@BenjaminWFox I was able to get the access token by using a different scope URL, which is mentioned here
https://docs.microsoft.com/en-us/azure/active-directory-b2c/add-web-api-application?tabs=app-reg-ga
current scope config looks something like this
scope :https://${tenantName}.onmicrosoft.com/api/demo.read openid offline_access,

@gyto23
Copy link

gyto23 commented Nov 3, 2020

@BenjaminWFox @WaysToGo I have read several times both of your setups and I have setup my own on azure and looks like I cannot even get the access token from azure. Which bring me to the question do I actually need this access token to work with my api? If I have refresh token it means that I can control user when I need and how I need, but if the token expires it should self logout from a system, correct? sorry for those odd questions I am new to Azure and NextAuth.
image

@BenjaminWFox
Copy link
Contributor

BenjaminWFox commented Nov 4, 2020

@gyto23 There is an extra step needed to get an access token from Azure B2C (assuming you're using B2C) - you'll need to follow the steps from this tutorial documentation: https://docs.microsoft.com/en-us/azure/active-directory-b2c/access-tokens

The main points are:

  • You need a 2nd app registration that represents your API
  • You need to use the scope you define in that app registration in your nextauth config.

I would not use the refresh token as any means of authentication/authorization. I'm not versed enough to tell you why it's a bad idea, but I'm confident that it is :)

I have an updated config I can share with you which may help. Following the steps in the link above to get the access token is worth the extra effort, as it allows you to much better control the B2C JWT, which is separate and distinct from the NextAuth JWT.

In the jwt callback with this setup the account has an accessToken (which is the B2C JWT) and refreshToken both of which I store on the NextAuth JWT. Then it's easy to use the NextAuth API in my NextJS API to get the accessToken from the JWT and pass it to my backend api.

Additionally, you can see the logic in there that checks for imminent expiration of the accessToken and then silently gets a new token if needed. If you instead wanted to not refresh the token you could just revoke the users NextAuth session at that point.

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

const tenantName = process.env.B2C_AUTH_TENANT_NAME
const loginFlow = process.env.B2C_LOGIN_FLOW_NAME
const maxAge = Number(process.env.CLIENT_SESSION_MAX_AGE)
const jwtSecret = process.env.NEXTAUTH_JWT_SECRET
const appSecret = process.env.NEXTAUTH_APP_SECRET
const clientId = process.env.B2C_AUTH_CLIENT_ID
const clientSecret = process.env.B2C_AUTH_CLIENT_SECRET
const apiAccessClaim = process.env.B2C_API_ACCESS_CLAIM
const signingKey = process.env.NEXTAUTH_SIGNING_KEY
const encryptionKey = process.env.NEXTAUTH_ENCRYPTION_KEY
const encryption = process.env.NEXTAUTH_ENCRYPT_JWT

const tokenUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${loginFlow}/oauth2/v2.0/token`

const options = {
  session: {
    jwt: true,
    maxAge,
  },
  jwt: {
    secret: jwtSecret,
    encryption,
    signingKey,
    encryptionKey,
  },
  secret: appSecret,
  pages: {
    signOut: '/auth/signout',
  },
  providers: [
    {
      id: 'azureb2c',
      name: 'Azure B2C',
      type: 'oauth',
      version: '2.0',
      debug: true,
      scope: `https://${tenantName}.onmicrosoft.com/api/${apiAccessClaim} offline_access openid`,
      params: {
        grant_type: 'authorization_code',
      },
      accessTokenUrl: tokenUrl,
      requestTokenUrl: tokenUrl,
      authorizationUrl: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${loginFlow}/oauth2/v2.0/authorize?response_type=code+id_token+token&response_mode=form_post`,
      profileUrl: 'https://graph.microsoft.com/oidc/userinfo',
      profile: (profile) => {
        console.debug('\n')
        console.debug('~~ PROFILE', profile)
        console.debug('\n')

        // The NextAuth `user` object available to the client
        return {
          id: profile.oid,
          name: `${profile.given_name} ${profile.family_name}`,
          email: profile.emails.length ? profile.emails[0] : null,
          image: undefined,
        }
      },
      clientId,
      clientSecret,
      idToken: true,
      state: false,
    },
  ],
  callbacks: {
    // eslint-disable-next-line no-unused-vars
    jwt: async (token, user, account, profile, isNewUser) => {
      const now = parseInt(Date.now() / 1000, 10)
      const tokenExpiryPaddingSeconds = 30
      const isSignIn = !!user

      // Add auth_time to token on signin in
      if (isSignIn) {
        // eslint-disable-next-line no-param-reassign
        token.b2c = {
          accessToken: account.accessToken,
          refreshToken: account.refreshToken,
          iat: profile.iat,
          exp: profile.exp,
        }
      }

      console.debug('\n Token expires / refreshes in: ', token.b2c.exp - now, ' / ', (token.b2c.exp - tokenExpiryPaddingSeconds) - now)

      /**
       * Add some extra seconds to refresh before it is completely expired to avoid
       * any edge case scenarios where the token expires in transit if it is almost
       * expired, but validated prior to an API call, and then sent in the
       * Authorization header and expires.
       */
      if (token.b2c.exp - tokenExpiryPaddingSeconds < now) {
        const refreshQuery = `?grant_type=refresh_token&refresh_token=${token.b2c.refreshToken}&scope=https://${tenantName}.onmicrosoft.com/api/${apiAccessClaim} offline_access openid&client_id=${process.env.B2C_AUTH_CLIENT_ID}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_secret=${process.env.B2C_AUTH_CLIENT_SECRET}`
        const response = await fetch(`${tokenUrl}${refreshQuery}`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
        })

        try {
          const result = await response.json()

          // eslint-disable-next-line no-param-reassign
          token.b2c = {
            accessToken: result.access_token,
            refreshToken: result.refresh_token,
            iat: result.not_before,
            exp: result.expires_on,
          }
        }
        catch (e) {
          console.error('There was an error trying to refresh the accessToken')
          console.error(e)
        }
      }

      return Promise.resolve(token)
    },
  },
}

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

@gyto23
Copy link

gyto23 commented Nov 6, 2020

@BenjaminWFox Thank you for your feedback, this is really helps. I hope you going to update the tutorial for the Azure setup for those people who searching similar implementation

@bjrb20
Copy link

bjrb20 commented Nov 9, 2020

Is it possible to use state with azure b2c? Without it you can only redirect back to the home page and you cant handle things like "Forgot my Password"

@gyto23
Copy link

gyto23 commented Nov 17, 2020

@BenjaminWFox weird question, why dont make azure b2c and azure ad to be part of the providers in the nextAuth this thing will popup multiple times, isnt it easy to make for user instruction for the setup and have it in it?

@vtrphan
Copy link

vtrphan commented Feb 24, 2021

can you please show me how to use B2C with authorization code flow with PKCE, without Implicit Grant ? Thanks

@jeremylynch
Copy link

Now that nextauth has an official AzureAdB2C provider, does anyone have an example of token refreshing which uses this method?

@BenjaminWFox
Copy link
Contributor

@jeremylynch be aware that the current provide marked AzureADB2C is not actually implementing AzureADB2C.

There's a PR to address this on the next branch, and you can see what the provider would look like. It's simplified but not fundamentally any different than what I showed in my comment above.

If I were to implement token refreshing with the Provider in that PR I'd do it in pretty much the same way as in my comment.

@parantha97
Copy link

parantha97 commented Apr 29, 2021

@BenjaminWFox I am trying to connect next-auth with Azure AD B2C.

this is my code

import NextAuth from 'next-auth';
import Providers from "next-auth/providers";
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
const tokenUrl = https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${userFlow}/oauth2/v2.0/token

const options:any = {
session: {
jwt: true,
},
secret : 'sdfsdfsdfsfg',
providers: [
{
id: 'azureb2c',
name: 'Azure B2C',
type: 'oauth',
version: '2.0',
debug: true,
scope: 'https://torsoai.onmicrosoft.com/api/54433534-12b2-46a4-a8f9-324d05a718f0/demo.read offline_access openid',
params: {
grant_type: 'authorization_code',
},
accessTokenUrl: tokenUrl,
requestTokenUrl: tokenUrl,
authorizationUrl:
https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${userFlow}/oauth2/v2.0/authorize? response_type=code+id_token+token&response_mode=form_post,
profileUrl: 'https://graph.microsoft.com/oidc/userinfo',
profile: (profile) => {
console.log('THE PROFILE', profile)
return {
id: profile.oid,
email: profile.emails.length ? profile.emails[0] : null,
}
},
clientId: clientId,
clientSecret: clientSecret,
idToken: true,
state:false
}
],
pages:{
signIn: '/auth/signin',
signOut: '/auth/signout',
},
};

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

but I am getting an error like this

[next-auth][error][oauth_get_access_token_error]
https://next-auth.js.org/errors#oauth_get_access_token_error TypeError: Cannot read property 'includes' of undefined
at exports.OAuth2. (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:171:29)
at Generator.next ()
at asyncGeneratorStep (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:24:103)
at _next (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:26:194)
at C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:26:364
at new Promise ()
at exports.OAuth2. (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:26:97)
at exports.OAuth2._getOAuth2AccessToken (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:219:32)
at exports.OAuth2.getOAuth2AccessToken [as getOAuthAccessToken] (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:109:32)
at C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:72:35
at Generator.next ()
at asyncGeneratorStep (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:24:103)
at _next (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:26:194)
at C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:26:364
at new Promise ()
at C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:26:97 azureb2c eyJraWQiOiJjcGltY29yZV8wOTI1MjAxNSIsInZlciI6IjEuMCIsInppcCI6IkRlZmxhdGUiLCJzZXIiOiIxLjAifQ..ca6T6qoF7fIfqPJ9.eSld8kcvBHARAhmVzIt196GSv7tB-ch4-SuLYOvtRZvb0-shcqL4hTE7YJXRK_OjKx_cqbmKxoV_cJ2We9zbMYKpZbyHWvie34ljJD-i5CkY55qybkNpbPD-7gCtaguvFf2Csw5jTblWbxKB76KHoDaLaha0ub6BThKT4qMR-WjoWTwoZ17Ddh0RyC1vJcZ1gfGRnrrGEWdSDmQSA5LrQQp2dp0kF0_a1sND9PLTgqVbvnSOXkzYAiOlErvfvyvvk0SIA4ccF3P8dK89rdAERspr5gU2kcs5nvcFkgHti8Qqx-W3b-UvduVTRy4YxNRIzInGjUoamnD-s2eeilJoyrpdB0GYc2h0nTyCz9pKtgK_hBAwRs3y1WuSNhwlTVcuqG4RUppZWnQE-J2dU5pM9SZVG3gBMxBdiIcpCcKxTGvDEbWH_99o0Cg54W796qJrKubJGxq9d3871KenNDbnNzN3txHsuU-OdgvOA0NlR3e_YNUSGWS1T_ppM_p7ScLBza4iEi_O5TxjlWniSKDE6mrEnjuFonlE9P1FFpTNqrCDqnbY6J2yPhl2kAaPi40wE-DT8LqvgwE1mr63yES6pLQQCDwgoqOpm9Zp5rdzY7mdjf5OzXMhvHE5_LCV5hD5LoviD_0ExDTTK_k.KPa2kwO_A8qluW8t8hXxbQ
[next-auth][error][oauth_callback_error]
https://next-auth.js.org/errors#oauth_callback_error TypeError: Cannot read property 'includes' of undefined
at exports.OAuth2. (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:171:29)
at Generator.next ()
at asyncGeneratorStep (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:24:103)
at _next (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:26:194)
at C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:26:364
at new Promise ()
at exports.OAuth2. (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:26:97)
at exports.OAuth2._getOAuth2AccessToken (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:219:32)
at exports.OAuth2.getOAuth2AccessToken [as getOAuthAccessToken] (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\client.js:109:32)
at C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:72:35
at Generator.next ()
at asyncGeneratorStep (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:24:103)
at _next (C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:26:194)
at C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:26:364
at new Promise ()
at C:\Torso\master_7\UI\Prod_UI\node_modules\next-auth\dist\server\lib\oauth\callback.js:26:97

@kiranupadhyak
Copy link

@parantha97 @BenjaminWFox I'm also getting the above error, any leads to solve this?

@parantha97
Copy link

@kiranupadhyak I reverted to next-auth version 3.13.3. it worked for me. but It's not a solution.

@balazsorban44
Copy link
Member

@parantha97 I would highly appreciate if you opened a new bug report with a reproduction, so we can have a look and fix any potential issues introduced after that version! 🙏

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

No branches or pull requests