401 Unauthorized on /userinfo for some tokens

For a small subset of our users, when they try to sign up, we experience this error on the /userinfo endpoint when used with an opaque access token passed directly from Google:

401 Unauthorized
(Header) WWW-Authenticate: The access token signature could not be validated.
...A common cause of this is requesting multiple audiences for an access token signed
...with HS256, as that signature scheme requires only a single recipient for its security.
...Please change your API to employ RS256 if you wish to have multiple audiences for your access tokens

What Is Expected To Happen

  1. We initialize a WebAuth object on page load (shown below).
  2. User asks to “Sign Up Through Google”, we call webAuth.authorize() (full request below).
  3. The user signs up through Google, receiving an accessToken, eg. M9SLko2_...eoeMME.
  4. We call webClient.userInfo() (full request below), which should return the user’s info.
  5. A user is created and the accessToken is stored in our database.
  6. We now attempt to log in the user, which happens in our backend. We pass the accessToken to one of our endpoints, then use it in a direct call to the /userinfo API endpoint (full request below).
  7. We check that the email included in the user info matches an account in our database (and other irrelevant checks), then sign a JWT to pass back to the frontend, indicating a successful login.

(1) WebAuth initialization:

const webAuth = new auth0.WebAuth({
  domain: config.services.auth0.domain,
  clientID: config.services.auth0.clientId,
  scope: 'openid profile email phone'
});

(2) WebAuth authorization:

webAuth.authorize(
    {
      redirectUri: config.services.auth0.signupRedirectUri,
      connection: 'google-oauth2',
      responseType: 'token',
      connection_scope: 'https://www.googleapis.com/auth/user.phonenumbers.read'
    },
    (err) => {
      logError(err);
      return false;
    }
  );

(4) WebAuth user info request (seemingly working):

webAuth.client.userInfo(accessToken, (err, user) => {
      if (err) {
        logError(err);
        reject(user);
      }
      else resolve(user);
    });
  1. /userinfo endpoint (not working):
const userInfoRes = await axios
    .get('https://atmos-prod.us.auth0.com/userinfo', {
      headers: {
        Authorization: `Bearer ${token}`, // accessToken
      },
    })

What Is Happening

Step (3) seems to work fine, as the user’s account is created. However, step (5) is failing with the original error. Since we store accessToken with user details in our database, I was able to verify that some access tokens work while others do not.

On inspecting details/permissions/etc. in the Auth0 dashboard, the users have the same raw JSON format, authorized applications (including audience: https://<our-application>.us.auth0.com/userinfo), permissions, etc.

In addition, the Auth0 dashboard’s history shows that the user successfully logged in (they tried several times) so we know that web authorization via Google works.

Notes

I’ve perused all other related topics I could find, though those seemed to focus either on (a) issues affecting on all users or (b) JWT-based access tokens (which we do not use).

Since I’m not able to reproduce, this is quite difficult to debug. I have an affected access token I’m able to test the endpoint with, though. I’ve confirmed that after (a) deleting my user in our database, (b) deleting my Auth0 user, and (c) clearing my OAuth permissions to our site via my Google Account, it works as expected on my personal account. I watched the FullStory video of the affected user and they followed the same path with different results.

1 Like