Nextjs-auth0: Handling expanding session scope

Created on 4 Feb 2021  路  8Comments  路  Source: auth0/nextjs-auth0

Description

I've been looking through the documentation and other issues, but I haven't been able to find how this library handles the use-case of expanding scope in the session token.

Say for instance I follow the login process and in it, I request the scopes "openid profile". If later on, I need to add a new scope like 'cart' to the session access token how should I go about doing this?

`.tokenCache(req, res).getAccessToken({scopes: ['cart'],refresh: true,}); `

This throws an error when the session is missing the requested scope. But how should that error be handled?

Reproduction

As a test created a dummy endpoint api/request-scope that simply calls handle login again but passes different scopes

export default async (req, res) => {
  try {
    console.log('Handling log in: ');
    await keycloak().handleLogin(req, res, {
      authParams: {
        trading_title: BRAND.toUpperCase(),
        trading_title_name: BRAND_NICE_NAME,
        isCheckout: Boolean(req.query.isCheckout),
        scope:
          'openid profile cart',
      },
    });
  } catch (error) {
    console.log('Login error ', error);
    res.status(error.status || 500).end(error.message);
  }
};

I do the normal sing in flow asking for the scopes "openid profile" and afterwards I enter that endpoint manually in the browser.
It redirected straight to handle callback with the following error "state mismatch, expected eyJyZWRpcmVjdFRvIjoiLyIsIm5vbmNlIjoiOTgyZDUyNGE5OWYyZDRmOTlmZjFmNDViZTliMzU1MGYifQ, got: eyJub25jZSI6ImY0OGE4NzAyZjdlYjQ0NzFlOWYxYjQxMWE3NTNkN2EyIn0"

Environment

These are the initial values I pass when calling initAuth0

let keycloak;

const initKeycloak = () => {
  // Note: done this way to ensure that the runtime environment values are used.
  if (!keycloak) {
    keycloak = initAuth0({
      clientId: config.KEYCLOAK_CLIENT_ID,
      clientSecret: config.KEYCLOAK_CLIENT_SECRET,
      scope: config.KEYCLOAK_SCOPE,
      domain: config.KEYCLOAK_DOMAIN,
      redirectUri: config.KEYCLOAK_REDIRECT_URI,
      postLogoutRedirectUri: config.KEYCLOAK_DEFAULT_POST_LOGOUT_REDIRECT_URI,
      session: {
        cookieSecret: config.KEYCLOAK_SESSION_COOKIE_SECRET,
        cookieLifetime: config.KEYCLOAK_SESSION_COOKIE_LIFETIME_SECONDS,
        storeRefreshToken: true,
        storeAccessToken: true,
      },
    });
  }
  return keycloak;
};

question

All 8 comments

Hi @DTAPigeons - thanks for raising this

This throws an error when the session is missing the requested scope. But how should that error be handled?

If you want to increase the scope of your Access Token from openid profile to openid profile cart you'll need to login again, so prompting the user to login again would be the way to handle that error.

As a test created a dummy endpoint api/request-scope that simply calls handle login again but passes different scopes

Your code looks like it should work - my only guess is that perhaps you visited api/request-scope/ (note the trailing slash) and you dropped a a0:state cookie with eyJub25jZSI6ImY0OGE4NzAyZjdlYjQ0NzFlOWYxYjQxMWE3NTNkN2EyIn0 as the state (which when base64 decoded is {"nonce":"f48a8702f7eb4471e9f1b411a753d7a2"}) under the path /api/request-scope/. When you returned to api/callback it found a previous a0:state cookie of eyJyZWRpcmVjdFRvIjoiLyIsIm5vbmNlIjoiOTgyZDUyNGE5OWYyZDRmOTlmZjFmNDViZTliMzU1MGYifQ ({"redirectTo":"/","nonce":"982d524a99f2d4f99ff1f45be9b3550f"}) under the api/ path.

You could try making sure you don't use a trailing slash on your login url or alternatively using the beta which fixes this by defaulting the cookie path to /.

So that is the intended way to handle it? Try the following flow:

    try {
      const { accessToken } = await keycloak()
        .tokenCache(req, res)
        .getAccessToken({
          scopes: scope.split(' '),
          refresh: true,
        });

      // console.log('Scope: ', scope.split(' '));
      // console.log('Access token: ', accessToken);

      if (accessToken) {
        // console.log('Has Token');
        return { access_token: accessToken };
      }
      return { status: 500 };
    } catch (e) {
      console.log('Has Error', e);
      logger('error', `fetchToken Error:`, e);
      if (e.code === 'insufficient_scope') {
        await keycloak().handleLogin(req, res, {
          authParams: {
            trading_title: BRAND.toUpperCase(),
            trading_title_name: BRAND_NICE_NAME,
            isCheckout: Boolean(req.query.isCheckout),
            scope:
              'openid profile accountdata shopping-cart site-config signin_nbrown_claim_mapper',
            redirectUri: '/',
          },
        });
        console.log('Need different scope');
      }

But nothing happened. It did register the error correctly and it did call handleLogin. But then it just silently failed.

Also about my test case I tried both /api/request-scope and explicitly passing a redirect to parameter /api/request-scope?redirectTo=/.
I also tried calling it from inside an existing page the way we do the normal login.

let path = `/api/request-scope?redirectTo=${encodeURIComponent(
      window.location.pathname
    )}`;
    router.push(path); 

I still get:
state mismatch, expected eyJyZWRpcmVjdFRvIjoiLyIsIm5vbmNlIjoiMzgzNGYyNmE4YzZjZGVjZjY0M2IxN2JiNDZiMjZlMDMifQ, got: eyJyZWRpcmVjdFRvIjoiLyIsIm5vbmNlIjoiOTljZGFmMTIwYTE4MDVjZDQwNmVkYThkNzE4OGQyZWYifQ

But nothing happened. It did register the error correctly and it did call handleLogin. But then it just silently failed.

From that sample - it looks like you're calling handleLogin in response to an ajax request, when it should be called in response to a UI request from your browser's address bar.

I still get:
state mismatch,

Can you share the full url of your /callback page and the full url of your /request-scope page - for some reason you're leaving a cookie at the /request-scope page which isn't being picked up by the /callback page

// router.push(path);

Also, can you try window.location.href = newUrl

Also - can you try the beta?

From that sample - it looks like you're calling handleLogin in response to an ajax request, when it should be called in response to a UI request from your browser's address bar.

Oh, so that's how it works. Thanks, I'll have to plan my auth logic around that.

Can you share the full url of your /callback page and the full url of your /request-scope page - for some reason you're leaving a cookie at the /request-scope page which isn't being picked up by the /callback page

It might be some conflicts with the initial config. I'll have to verify once I'm using the actual sing in URL I'll update here or start a separate issue if the problem continues.

@adamjmcgrath Could the cookie issue be because I'm calling handleLogIn twice without calling handleLogOut? I want to make it so the user doesn't have to re-enter their credentials if they still have an active session and just needs different scopes.

Hi @DTAPigeons -

Could the cookie issue be because I'm calling handleLogIn twice without calling handleLogOut

I just ran the https://github.com/auth0/nextjs-auth0/tree/master/examples/basic-example app and added a second login handler that requests extra scopes, eg:

// pages/api/login-too.js
import auth0 from '../../lib/auth0';

export default async function login(req, res) {
  try {
    await auth0.handleLogin(req, res, {
      authParams: {
        scope: 'openid profile read:products'
      }
    });
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}

I logged in using the /api/login route then, whilst still logged in, I visited the new /api/login-too route in my browser and logged in again successfully. I couldn't reproduce your issue.

Could you try and reproduce your issue on the basic example, then I can help you by debugging it

@adamjmcgrath Sorry I gave up on the api/request-scope and just changed the sing in to handle both cases. It works fine, for now, I guess I was passing wrong parameters somehow. Thanks for all of your help.

export default async (req, res) => {
  try {
    const { pageName } = req.query;
    delete req.query.pageName;

    let scope = pageScopes['site'];

    if (pageName) {
      scope += pageScopes[`${pageName}`] ? ` ${pageScopes[`${pageName}`]}` : '';
    }

    console.log('Sining in with new scope: ', scope);

    await keycloak().handleLogin(req, res, {
      authParams: {
        trading_title: BRAND.toUpperCase(),
        trading_title_name: BRAND_NICE_NAME,
        isCheckout: Boolean(req.query.isCheckout),
        scope,
      },
    });
  } catch (error) {
    console.log('Login error ', error);
    res.status(error.status || 500).end(error.message);
  }
};

no problem @DTAPigeons - glad you got it working

Was this page helpful?
0 / 5 - 0 ratings

Related issues

flapjackfritz picture flapjackfritz  路  6Comments

mustafaKamal-fe picture mustafaKamal-fe  路  5Comments

lunchboxav picture lunchboxav  路  5Comments

RyGuyM picture RyGuyM  路  5Comments

serendipity1004 picture serendipity1004  路  3Comments