auth0 / auth0.js

Auth0 headless browser sdk
MIT License
998 stars 492 forks source link

Invalid token - `state` does not match #655

Closed jeiman closed 6 years ago

jeiman commented 6 years ago

So for some reason I can't seem to authenticate users on certain computers/laptops that has the same browser versions.

auth.js

export var webAuth = new auth0.WebAuth({
    domain: 'xxx.auth0.com',
    clientID: 'xxx',
    responseType: 'token id_token',
    audience: 'https://xxx.auth0.com/userinfo',
    redirectUri: 'http://localhost:3000'
  })

Login.vue

if (window.location.hash) {
        webAuth.parseHash({ hash: window.location.hash }, (err, authResult) => {
          if (err) {
            return console.log('parseHash error', err)
          }
          if (authResult) {
            webAuth.client.userInfo(authResult.accessToken, function(err, user) {
              if (err) {
                console.log('err accessToken', err)
              }
              localStorage.setItem('profile', JSON.stringify(user))
              localStorage.setItem('id_token', authResult.idToken)
              window.location = '/state'
            })
          }
        })
      }

Picture:

error

Other details:

Auth0 version: NPM - 8.12.2 ScriptJS - 8.12.2

Browser: Chrome Version 64.0.3282.140 (Official Build) (64-bit) O.S: Windows 10

The issue is not happening on my laptop (which is running on the the Browser version mentioned above), but it is happening on other laptops (same browser version).

Not too sure what else to try here. Been looking at the forums, nothing is working out.

Would love some insights on this. Thank you.

luisrudge commented 6 years ago

Can you reproduce this in any way? Your code looks fine, but we need to find a way to reproduce the issue.

VinSpee commented 6 years ago

I'm having the same issue, happens of everly login, but on refresh, i can get profiles without issue.

Here's my auth component:

import Auth0Lock from 'auth0-lock';
import { withRouter } from 'react-router-dom';
import { Component } from 'react';
import PropTypes from 'prop-types';

class AuthProvider extends Component {
  static propTypes = {
    history: PropTypes.object.isRequired, // eslint-disable-line
    children: PropTypes.func.isRequired,
  };

  constructor(props) {
    super(props);
    this.state = {
      profile: null,
    };

    this.history = props.history;
    this.lock = new Auth0Lock(AUTH_CONFIG.clientId, AUTH_CONFIG.domain, {
      autoclose: true,
      auth: {
        redirectUrl: AUTH_CONFIG.callbackURL,
        responseType: 'token id_token',
        audience: `https://${AUTH_CONFIG.domain}/userinfo`,
        params: {
          scope: 'openid profile',
        },
      },
    });
    this.handleAuthentication();
    // binds functions to keep this context
    this.getProfile = this.getProfile.bind(this);
  }

  componentWillMount() {
    const { userProfile, getProfile } = this;
    if (!userProfile) {
      getProfile((err, profile) => {
        this.setState(state => ({
          ...state,
          profile,
        }));
      });
    } else {
      this.setState(state => ({
        ...state,
        profile: userProfile,
      }));
    }
  }

  getProfile(cb) {
    const accessToken = this.getAccessToken();
    if (accessToken) {
      this.lock.getUserInfo(accessToken, (err, profile) => {
        if (profile) {
          this.userProfile = profile;
        }
        cb(err, profile);
      });
    }
  }

  getAccessToken = () => {
    const accessToken = localStorage.getItem('accessToken');
    if (!accessToken) {
      console.error('No access token found');
    }
    return accessToken;
  }

  setSession(authResult) {
    if (authResult && authResult.accessToken && authResult.idToken) {
      // Set the time that the access token will expire at
      const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
      localStorage.setItem('accessToken', authResult.accessToken);
      localStorage.setItem('idToken', authResult.idToken);
      localStorage.setItem('expiresAt', expiresAt);
      // navigate to the home route
      this.history.replace('/');
    }
  }

  handleAuthentication() {
    // Add a callback for Lock's `authenticated` event
    this.lock.on('authenticated', this.setSession.bind(this));
    // Add a callback for Lock's `authorization_error` event
    this.lock.on('authorization_error', (err) => {
      console.log(err); // eslint-disable-line no-console
      this.props.history.replace('/');
    });
  }

  isAuthenticated = () => {
    // Check whether the current time is past the
    // access token's expiry time
    const expiresAt = JSON.parse(localStorage.getItem('expiresAt'));
    return new Date().getTime() < expiresAt;
  }

  login = () => {
    // Call the show method to display the widget.
    this.lock.show();
  }

  logout = () => {
    // Clear access token and ID token from local storage
    localStorage.removeItem('accessToken');
    localStorage.removeItem('idToken');
    localStorage.removeItem('expiresAt');
    // navigate to the home route
    this.props.history.replace('/');
    this.setState(() => ({
      profile: null,
    }));
  }

  render() {
    const { children } = this.props;
    return children({
      profile: this.state.profile,
      authInstance: this.lock,
      login: this.login,
      logout: this.logout,
      authenticated: this.isAuthenticated(),
    });
  }
}

export default withRouter(AuthProvider);

and login:

import React from 'react';
import Button from 'components/button';
import Auth from 'components/auth';
import { withRouter } from 'react-router-dom';

const LoginPage = () => (
  <div>
    <Auth>
      {({
        login,
      }) => (
        <button
          onClick={login}
          className="
            Bd(n)
            Bg(n)
          "
        >
          <Button>
            Log In
          </Button>
        </button>
      )}
    </Auth>
  </div>
);

export default withRouter(LoginPage);
luisrudge commented 6 years ago

thanks @VinSpee can you build a simple repro so we can isolate the issue?

nicosabena commented 6 years ago

I've seen this happening sometimes if the authorize request is made from a different origin than the callback URL. E.g. you navigate to "http://myapp.com", and instead of redirecting first to "https://myapp.com", it goes directly to "https://yourtenant.auth0.com/authorize?...&redirect_uri=https://myapp.com" (notice the difference between "http://" and "https://"). Since Auth0.js stores the state in local storage before redirecting to Auth0 for later verification, you need to ensure that the origin where the authorize request originates is the same as the callback URL.

@luisrudge for easier troubleshooting, I might be a good idea to log in the console a little more data, like the state that came in the callback, and what was there in localStorage (no match?). Even a list of states present in LocalStorage might help.

Or maybe just handle one possible state storage slot instead (i.e. not including the state value as part of the key for local storage)?

luisrudge commented 6 years ago

@nicosabena that's a nice idea, but always logging that would not be good. Maybe we should have a debug mode (manually set localStorage or ?debug in the url or something).

VinSpee commented 6 years ago

Here's a minimal repro:

code: https://github.com/VinSpee/auth0-debug live: https://recondite-lake.surge.sh/login

luisrudge commented 6 years ago

@VinSpee are you having this issue with google apps only?

luisrudge commented 6 years ago

I tested your app a few times and I always get the profile back. Do I have to do something different to see the error? gh-state

VinSpee commented 6 years ago

@luisrudge to be more clear: I am able to get the profile, but always get the console error:

Object { error: "invalid_token", errorDescription: "state does not match." }

luisrudge commented 6 years ago

@VinSpee ah, right. Sorry I missed that! Does this happen only with google?

VinSpee commented 6 years ago

i've only tried in w/ google auth, I'll try with another provider

VinSpee commented 6 years ago

I just tried it w/ GitHub as the provider and got the same error:

Object { error: "invalid_token", errorDescription: "state does not match." }

luisrudge commented 6 years ago

@VinSpee your code is running twice:

image

Every time you use the <Auth> component, you're creating a new instance of Lock, and this is messing up the state handler.

So, it runs twice because you use the component both in the index route and in child routes: https://github.com/VinSpee/auth0-debug/blob/master/src/index.js#L20 https://github.com/VinSpee/auth0-debug/blob/master/src/Login.js#L6

We have a react sample here if you want to take a look.

You're also shipping two versions of react, because auth0-lock is using react@15, so you need to add this to your package.json file:

  "resolutions": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0"
  }
staab commented 6 years ago

I've been having the same problem, and I've found a workaround which I'll describe below just because I saw a lot of google results for this error, which seemed to be caused by all kinds of different things. However, I did find the proper solution, which is to use WebAuth.logout rather than buildAuthorizeUrl:

export function logout() {
  webAuth.logout({
    clientID: process.env.REACT_APP_AUTH0_CLIENT_ID,
    responseType: 'token',
    redirectUri: window.location.origin,
  })
}

My problem was that I generated a logout url using buildAuthorizeUrl with auth0-js v9.4.2. When I do this though, I redirect to the url manually, so the state doesn't get set by auth0:

export function logout() {
  const state = randomId()

  // Save state to localstorage for Auth0
  // https://auth0.com/docs/protocols/oauth2/oauth-state
  localStorage.setItem('auth0-authorize', state)

  const domain = process.env.REACT_APP_AUTH0_DOMAIN
  const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID
  const returnTo = webAuth.client.buildAuthorizeUrl({
    clientID: clientId,
    responseType: 'token',
    redirectUri: window.location.origin,
    state,
  })

  const params = stringify({returnTo, client_id: clientId})

  window.location.assign(`https://${domain}/v2/logout?${params}`)
}

When someone hits the site and isn't authenticated, I redirect them to auth0 with authorize. This sets the state in a different place in localstorage (seems to be a randomly generated string). To handle both my custom logout state and the built-in logout state, I'm providing an options parameter to parseHash with the state set only if my custom key is set (being sure to clear my custom one when I use it to avoid always rejecting one set by auth0). Here's my code:

function checkAuthResult(err, authResult) {
  if (err) {
    // If we tried to log them in automatically, redirect to auth0
    webAuth.authorize()
  } else if (!authResult) {
    // Try logging them automatically first
    // https://auth0.com/docs/libraries/auth0js/v8#using-checksession-to-acquire-new-tokens
    webAuth.checkSession({}, checkAuthResult)
  } else if (authResult.accessToken) {
    resolve(authResult)
  }
}

// If we log out, auth0 doesn't set the state properly, so we have to do it manually.
// We can't do it every time though, because when they're trying to log from a url
// not generated by buildAuthorizeUrl, we need to let the default behavior work.
const state = localStorage.getItem('auth0-authorize')
const params = state ? {state} : {}

// Make sure to clear our custom state key
localStorage.setItem('auth0-authorize', '')

webAuth.parseHash(params, checkAuthResult)
luisrudge commented 6 years ago

@staab not sure I understand: why are you setting a state when doing a log out?

staab commented 6 years ago

@luisrudge because I'm redirecting to the login page for auth0, which doubles as our landing page for folks who aren't signed in (I'm not using auth0-lock, and our software is login-only). As I understand it, there's no way to go to that page and have it successfully log someone in if state isn't set. Also, as far as I can tell, this is exactly what the logout() method was created for.

luisrudge commented 6 years ago

@staab

to log in with the universal login page, you do:

var options = {}; //anything you want, state is not required but you can use your own state here if you want
webAuth.authorize(options);

to log out from auth0, you do:

var options = {}; //again, no state required
webAuth.logout(options);

I mean, you're free to call the /v2/logout endpoint by yourself (although I don't see the point in doing it manually), but there's no need to send a state to the logout endpoint.

staab commented 6 years ago

@luisrudge that's what I'm doing now (see the top of my original comment). I was mostly including my manual logout solution for posterity, since I hadn't seen the mistake I had made anywhere else after some googling.

After some more digging, it turns out that it's not logout that's passing state, I'm redirecting back to my app after logging out, then calling authorize, which passes state. I had missed that step. So you're right, logout doesn't use state. To fix this, I'll see if I can pass an authorization url to redirectUri.

staab commented 6 years ago

Ok, so I tried passing a redirectUri built using buildAuthorizeUrl with a custom managed state to get the app > logout > hosted login page redirect flow I want, but logout always returns a 302 with Location: myappdomain rather than the redirect_uri that gets passed via query parameters. I think this is what threw me off originally; is there a way to get logout to set the Location header to mydomain.auth0.com/login?... with a valid state/audience/etc?

luisrudge commented 6 years ago

logout expects a returnTo param, not redirectUri.

logout({
  returnTo: 'https://myapp.com/login'
})
luisrudge commented 6 years ago

For reference: https://auth0.com/docs/libraries/auth0js/v9#logout https://auth0.com/docs/api/authentication#logout

staab commented 6 years ago

Ah ok thanks, that did the trick.

Now that my credibility has been destroyed by not being able to read the documentation properly, is redirecting to the login page straight from the logout endpoint a common use case or am I an outlier? If it's common, it would be really nice for logout/buildAuthorizeUrl to handle setting state the same way authorize does.

luisrudge commented 6 years ago

from logout directly to the universal hosted login page is the first time I saw it 😝 The best way to do what you want is:

your app > click logout > webauth.logout({returnTo: 'https://myapp.com/login'}) > https://myapp.com/login > webAuth.authorize() > universal login page.

So, you create a step in between the logout and redirecting to the universal login page and, in this new step, you call authorize(). Then auth0.js will handle all the state and everything should work!

staab commented 6 years ago

Fair enough! I had it set up the way you described before, but I figured saving a redirect to my tool would be the best use of our resources and user experience. Not a big deal in any event though. Thanks for the help!

bramski commented 6 years ago

I have come upon this error due to failure to JSON.stringify my state parameters before sending them to auth0 for authorize. Rather annoying error for a simple mistake. Auth0 library should really verify that the state is a "string" as objects don't work so well.

luisrudge commented 6 years ago

@bramski what was the error? we want to support objects so you can restore an app state if you need to

bramski commented 6 years ago

@luisrudge Here's the problem: screen shot 2018-04-11 at 5 20 50 pm

luisrudge commented 6 years ago

Ah, yeah. I misread your comment! Yes, the state param has to be a string. But you can stringify your object and use that if you want. This is documented here and here

maneshom commented 6 years ago

same error as @VinSpee .But it works fine with google chrome .In mozilla,Error showing only in the first time of login and after the cookie added it works.

DarryllRobinson commented 6 years ago

What was the final resolution for the error? I also get the 'state does not match' error the first time and it goes straight through the second.

benschaaf commented 6 years ago

@nicosabena mention of HTTP vs HTTPS made the difference for me.

Fix: Now forcing all traffic to HTTPS and all requests work.

Previously, a user would enter on HTTP and try to authenticate. The callback was set to send them back to the domain but on HTTPS, hence the mixed/non-matching state. As the callback was set to return to HTTPS, the second and all subsequently requests worked as expected. It was no longer mixed or not matching. It was HTTPS from there on out.

sgentile commented 6 years ago

same error as @VinSpee .But it works fine with google chrome .In mozilla,Error showing only in the first time of login and after the cookie added it works.

Same here - except for me it's with certain Internet Explorer users (not all). After several attempts it lets them in. There is definitely a bug here, but the documentation fails to mention what causes this error and any attempts to solve it.

sgentile commented 6 years ago

Also, I think there might be a timing issue here. It's as if it can't read the cookie (auth0 uses a cookie ?) the first time on the request/response.

What was the final resolution for the error? I also get the 'state does not match' error the first time and it goes straight through the second.

Lastly, I am https to https - I get it both with http to https and https to https.

DarryllRobinson commented 6 years ago

When I changed the AUTH_CONFIG to the one below, it worked for me. I think there was an issue with the way the callback URL was being created but I'm not sure.

export const AUTH_CONFIG = { domain: 'fcmcms.eu.auth0.com', clientId: 'f$%*$$%*$%$*%$*$$%$*y', callbackUrl: process.env.NODE_ENV === 'development' ? 'http://localhost:3000/callback' : 'https://domain.com/callback', }

luisrudge commented 6 years ago

@DarryllRobinson how it was before?

nicosabena commented 6 years ago

I want to summarize the two common causes I see in customer support related to this state does not match error (apologies for repeating the content of my previous post, but I'm adding a second cause here):

luisrudge commented 6 years ago

Another super common case is when your users bookmark the /login endpoint instead of your app's url. This will cause the parseHash method to fail as well.

DarryllRobinson commented 6 years ago

@DarryllRobinson how it was before?

Sorry, I meant to say I had updated it to: callbackUrl: window.location.origin + '/callback'

therockstorm commented 5 years ago

I get this error in Private Browsing Firefox v63.0.3 with all extensions and Content Blocking disabled using the provided React sample app. To reproduce, login and then refresh the page, auth0-js v9.8.2. I do not get the error in Chrome or Safari.

enashnash commented 5 years ago

I want to summarize the two common causes I see in customer support related to this state does not match error (apologies for repeating the content of my previous post, but I'm adding a second cause here):

  • Callback URL set to a different domain. E.g. your app started at the http://myapp.com and the callback is set to https://myapp.com (note the different http vs https scheme). Auth0.js/Lock store the state in local storage, which is not shared across different domains. In the http/https case, the correct approach is to redirect to the https scheme first and then initiate the login.
  • A flow in the app is causing parseHash to be called twice. The first time it's run, the slot in local storage used for the specified state is cleared, so the second time you will always get this error. Make sure that your app only calls this once. Note that, by default, Lock calls it automatically upon initialization if it finds a result in the location of the hosting window.

For the latter, I noticed that my /callback page was being called a second time by browser-sync. The error went away when I added the following to my browser-sync configuration (https://browsersync.io/docs/options#option-snippetOptions):

  snippetOptions: {
    ignorePaths: "callback"
  }
hobbsh commented 5 years ago

I get this error in Private Browsing Firefox v63.0.3 with all extensions and Content Blocking disabled using the provided React sample app. To reproduce, login and then refresh the page, auth0-js v9.8.2. I do not get the error in Chrome or Safari.

We are hitting the same thing - refreshing the page shows invalid token - state does not match for Firefox only.

strongSoda commented 5 years ago

Hello,

I have made this todo app with auth0 todo app. Everything works fine on chrome & edge. On Mozilla, I am able to login/signup, and after that even add new todos, but I can't update existing todos, the only thing that shows up in the console is state does not match. This is also the case when https to https.

therockstorm commented 5 years ago

@luisrudge Can we get this re-opened? It's still an issue in Firefox.

luisrudge commented 5 years ago

Can you build and deploy a repro project? Also, make sure you are not doing any of the common mistakes that yield this error: https://github.com/auth0/auth0.js/issues/655#issuecomment-419966421

therockstorm commented 5 years ago

@luisrudge, the repro project is the official Auth0 React Sample Login project, https://github.com/auth0-samples/auth0-react-samples/tree/master/01-Login. Clone it, set the domain and clientId in auth0-variables.js, run npm start, login, and refresh the page in Firefox. You'll get "state does not match.".

jthannah commented 5 years ago

I am also seeing this problem in Firefox and am able to reproduce it using the official Auth0 Vue sample Login Project: https://github.com/auth0-samples/auth0-vue-samples/tree/master/01-Login

I notice when I refresh the page it goes back to the callback url. So maybe it has to do with how Firefox performs a page refresh?

luisrudge commented 5 years ago

Can you record a HAR file of this? I just came back from vacations and I'll take a look at this during this week.

jthannah commented 5 years ago

Attached is a har file: Archive 19-01-07 09-14-54.zip

strongSoda commented 5 years ago

@luisrudge here's the HAR file for the todo app I created https://drive.google.com/drive/folders/1LHAy05zQTvuc1MQJjx7OWYSEYESRwJe-?usp=sharing

sgentile commented 5 years ago

One of the main issues with this bug is Auth0 is missing something in their explanation. Everyone says 'make sure not sending twice', 'http to https' etc... but it's more than that.

I've tried all the above, sent HAR files, and still haven't had an explanation of the issue.

Works fine in all browsers but IE. In IE, I've had to do a retry work around to get past this issue.

On Mon, Jan 7, 2019 at 8:49 AM Luís Rudge notifications@github.com wrote:

Can you record a HAR file of this? I just came back from vacations and I'll take a look at this during this week.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/auth0/auth0.js/issues/655#issuecomment-451940264, or mute the thread https://github.com/notifications/unsubscribe-auth/AAIFNBCEKUhb7m0mILXo0OXB77CIAproks5vA1B0gaJpZM4R8Xka .