Distinguish between first login and silent auth

CONTEXT:
My user sign up for the first time (loginsCount is 1).
My SPA make API calls with authorization Access Token (get it from @auth0/auth0-spa-js).
I have 3 rules. One of them saves the user when he sign up (loginsCount is 1) on an external database.

Code of this rule:

const count = context.stats && context.stats.loginsCount ? context.stats.loginsCount : 0;
if (count > 1) return callback(null, user, context);
try {
       const axios = require('axios');
       await axios.post(
...

PROBLEM:
If the user sign up for the first time, heā€™s using the SPA and making multiple API calls without log out; then the rules are called every time the API call (that uses Access Token from @auth0/auth0-spa-js) but the loginsCount still is 1.

How can I skip the rule to prevent saving the same user on external database if he has already been saved on external database?

IMPROVEMENT
You (Auth0 team) can add on context.stats the boolean variable signUp.


** Iā€™ve tried to use another variable, global.userSaved = true, to check if the rule has already been called when loginsCount === 1, but the global doesnā€™t save the new variable when the rule is called 5 secons later.
** Iā€™ve also read https://auth0.com/docs/best-practices/rules and https://www.webtask.io/docs/containers

@guillempuche,

One of the example rules uses this conditional for distinguishing signup from refresh:

 if (context.stats.loginsCount > 1 || context.protocol === 'oauth2-refresh-token') {
   return callback(null, user, context);
 }

Because this is a SPA and there is no refresh token, you may need to also check if context.request.query.prompt === 'none' (the prompt used for silent auth).

Let me know how it goes.

Thanks,
Dan

also be aware of the limitations of checking for silent auth in the context of MFA

1 Like

It works well Dan!

My code now is:

if (context.stats.loginsCount > 1 || context.protocol !== 'oidc-basic-profile' || context.request.query.prompt !== 'none')
    return cb(null, user, context);
...

But the SPA only uses (n all Webtask requests) the protocol oidc-basic-profile, not oauth2-refresh-token. Why this? Why I need to use the protocol on the condition?

*More about the type of protocols.

Some SPAs and mobile apps will use the oidc-implicit-profile for login. This is a different type of flow. The refresh token flow will be used for apps that can store a refresh token securely (native, regular web, not SPAs).

If you are using auth0-spa-js, this should work fine. If you use auth0.js, then you may need to add context.protocol !== 'oidc-implicit-profile'.

Also, I think you should be returning the cb if context.request.query.prompt === 'none'. Basically saying ā€˜if this is a silent auth, then start the callback. (instead of creating a user request to your DB)ā€™

You may be best off with this conditional:

if (context.stats.loginsCount > 1 || context.protocol === 'oauth2-refresh-token' || context.request.query.prompt === 'none'){
return cb(null, user, context);
}

Which says the following:

  • if the user has logged in more than once
    OR
  • if this is a token refresh
    OR
  • if this is a silent auth
    THEN
  • callback to your application
    OTHERWISE
  • assume this is a first login and create the user in your DB
1 Like

It doesnā€™t work with your condition neither my condition. It never returns the callback, always creates user in my DB.


IMPORTANT: My localhost SPA doesnā€™t skip the consent, then Iā€™m using getTokenWithPopup(...) to get the Access Token to make API calls through the SPA.


I only receive protocol ā€˜oidc-basic-profileā€™ and the prompt ā€˜noneā€™ when the user is consenting (the profile information) during the signup.

Here you have the logs of rule (with name saveUserOnExternalDatabase) printing the protocol and query 2 calls of the rule.

  • This is when I sign up with Google and before the consent page:
    image
  • This is after user click Accept on the consent page:
    image

Why the rule receives oidc-basic-profile (most used and web based login; according to context.protocols) and not oidc-implicit-profile (used on mobile devices and single-page apps) if Iā€™m using the SPA?

Do you know the answers?

Hi @guillempuche,

I moved this to a new topic as it is getting chatty and want to save the others from notifications. Also, sorry for the delay. I havenā€™t had the chance to debug this.

Can you post the code to your rule so we can see everything that is going on?

What library are you using? Auth0.js uses the implicit grant, and auth0-spa-js uses a different grant type, auth code + pkce. It will depend on which grant is being used (that doc is a little misleading).

@dan.woda

CONTEXT:

Iā€™m using @auth0/auth0-spa-js.
As I said before, the Webstask logs on the rule only


RULES:

Here are my rules:

CODE OF THE RULES:

  1. Rule ā€œSet default role to a user on first loginā€
async function setDefaultRoleToUser(user, context, callback) {
  // Only we add the role 'agent' if user is sign up.
  let count = context.stats && context.stats.loginsCount ? context.stats.loginsCount : 0;
  if (count > 1) return callback(null, user, context);
  
  try {
    const ManagementClient = require('auth0@2.19.0').ManagementClient;
    var management = new ManagementClient({
      domain: auth0.domain,
      // User API
      clientId: '...',
      clientSecret: '...',
      scope: 'read:roles update:users'
    });
    
    // https://github.com/auth0/node-auth0/blob/master/src/management/index.js#L2692
    let roles = await management.getRoles();
    
    // If user hasn't the that role yet, we want to prevent to call the Management API if the rule is called multiple times during the first login (loginsCount = 1).
    if (roles.some(el => el === 'agent') === false) {
      let roleId = roles.find(({name}) => name === 'agent').id;
	
      await management.assignRolestoUser({id: user.user_id}, {roles: [roleId]});

      // We have to add the role added to context for the next rules after this.
      if (context.authorization.roles) {
        // Only add the default role on the authorization if it doesn't exist.
      	if (context.authorization.roles.some(el => el === 'agent') === false){
        	context.authorization.roles.push('agent');
        }
        return callback(null, user, context);
      }
      else context.authorization.roles = ['agent'];
      console.log(`setDefaultRoleToUser - User ${user.user_id} has the new role 'agent'`);
    } 
  } catch(err) {
    console.error(err);
  }
}
  1. Rule ā€œAdd user roles on id and access tokenā€
/**
 * Documentation:
 * - https://auth0.com/docs/authorization/concepts/sample-use-cases-rules#add-user-roles-to-tokens
 * - https://auth0.com/docs/api-auth/tutorials/adoption/scope-custom-claims
 */
function addRolesOnTokens(user, context, callback) {
  // Namespace constraints: https://auth0.com/docs/api-auth/tutorials/adoption/scope-custom-claims#custom-claims
  const namespace = 'https://guillemau-dev.com';
  
  console.log("addRolesOnTokens - context.authorization", context.authorization);
  
  const assignedRoles = (context.authorization || {}).roles;

  let idTokenClaims = context.idToken || {};
  let accessTokenClaims = context.accessToken || {};

  idTokenClaims[`${namespace}/user/roles`] = assignedRoles;
  accessTokenClaims[`${namespace}/user/roles`] = assignedRoles;

  context.idToken = idTokenClaims;
  context.accessToken = accessTokenClaims;

  callback(null, user, context);
}
  1. Rule ā€œSave new user on external dbā€. Iā€™m using custom claims (set before by other rule) for the condition. Itā€™s not optimal condition, but it seems is working.
/**
 * Register the user on the external database only continue if he signs up for the first time. Else, exit the rule.
 * 
 * IMPORTANT: the rule will be skipped if the authorization hasn't any role assigned. 
 */ 
async function saveUserOnExternalDatabase(user, context, cb) {
  //if (context.stats.loginsCount > 1 || context.protocol === 'oauth2-refresh-token' || context.request.query.prompt === 'none')
  //if (context.stats.loginsCount > 1 || context.protocol !== 'oidc-basic-profile' || context.request.query.prompt === 'none')
  if (context.stats.loginsCount > 1 || context.authorization.roles.length >= 1)
  	return cb(null, user, context);

  console.log("saveUserOnExternalDatabase - Saving user on external db...");
  try {
    const axios = require('axios');
    await axios.post('https://guillemau-dev.ngrok.io/user/create', {
      name: {
        displayName: user.given_name || user.name
      },
      emails: {
        auth: user.email
      },
      roles: context.authorization.roles,
      auth: {
        auth0Id: user.user_id
      }
    }, {
      headers: {Authorization: `Bearer ${context.accessToken}`}
    });
    
    console.log(`saveUserOnExternalDatabase - User ${user.user_id} has been saved on an external db.`);
    
    return cb(null, user, context);
  } catch(err) {
    console.error(err);
    return cb(new UnauthorizedError('Not able to save the user correctly.'), user, context);
  }
}

PROBLEMS:

  • The conditions on the first two rules are if (count > 1) arenā€™t very optimal because when context.stats.loginsCount == 1, the rules are executing a lot of times because getting access token on the SPA (during the first login) is making them execute every time.
  • The condition on the rule of save user on an external db isnā€™t working every time (neither your code Dan).

What condition has to be to execute one time and only when the user is signing up?

Hi @guillempuche,

I have done some more research and seen the ā€˜add user to dbā€™ scenario handled with a flag in user.app_metadata. For example, you could add a flag to say user.app_metadata.inDatabase = true after a successful call to your create user endpoint.Checking for this is going to be easier and more reliable. This strategy could extend to the other issue with adding the role.

Does that seem like a viable solution?

Let me know.

Thanks,
Dan

Thanks Dan! :ok_hand:

Now, it works well and itā€™s intelligible.

I recommend you to update your guides about adding a default role to user.

@guillempuche,

Thanks for the recommendation, I am going to update it with this strategy. Thanks for your patience on this one.

Best,
Dan

This topic was automatically closed 15 days after the last reply. New replies are no longer allowed.