In post-login action, how to distinguish initial session login from reuse of existing session?

Context

I’m writing a post-login action that uses the IdP access token for a custom OAuth2 connection to fetch specific information from the IdP upon login and store that information in the user’s app_metadata.

Previously, my tenant utilized the Fetch User Profile Script to perform this action, until we discovered that including the app_metadata property in the callback result object will cause the entire app_metadata to be replaced rather than merged as in PATCH /api/v2/users/:id API call. This behavior is problematic because our applications and other tenant actions utilize app_metadata to store properties for other purposes, and we cannot risk wiping this information out.

Currently, my post-login action behaves according to the following (simplified) code:

exports.onExecutePostLogin = async (event, api) => {
  if (event.connection.strategy === 'oauth2'
      && event.connection.name === 'my-custom-connection-name') {
    // Obtain the IdP access token via the API (since it is not available in event.user.identities)
    const { ManagementClient } = require('auth0');
    const api = new ManagementClient(...);
    const { data: { identities } } = await api.users.get({ id: user_id });
    const { access_token } = identities.find(identity => identity.provider = 'oauth2' && identity.connection === event.connection.name);
    
    // Fetch extra information from the IdP
    const response = await fetch('https://example.com/userinfo', {
      headers: { authorization: `Bearer ${access_token}` }
    });
    if (response.ok) {
      const info = await response.json();

      // Save as metadata
      info.updated_at = new Date().toISOString();
      api.user.setAppMetadata('my_custom_connection_info', info);
    }
  }
}

Problem

This action executes as part of every login flow, including if the user already has a valid login session that is simply reused to issue a new set of tokens to an application in my tenant. In addition to this being redundant (and therefore unnecessarily slowing the login flow), this logic may re-execute long after the access token has expired, resulting in errors.

For illustration purposes, assume a user performs the following sequence of steps:

  1. User attempts to access application A and is redirected to Auth0 (universal login).
  2. User chooses to use federated login through my custom connection.
  3. User signs in to the custom connection and is redirected back to Auth0.
  4. Auth0 sets login session cookies and redirects the user successfully back to application A.
  5. User attempts to access application B and is redirected to Auth0.
  6. Auth0 recognizes session cookies (from step 4) and redirects the user successfully back to application B.
  7. Some time passes (but less than the Idle Session Lifetime)…
  8. User attempts to access application A again and is redirected to Auth0.
  9. Auth0 recognizes session cookies (from step 4) and redirects the user successfully back to application A.
  10. Some more time passes (but less than the Idle Session Lifetime since step 9 and less than Maximum Session Lifetime since step 4)…
  11. User attempts to access application C and is redirected to Auth0.
  12. Auth0 recognizes session cookies (from step 4) and redirects the user successfully back to application C.

In the above sequence, the connection’s Fetch User Profile Script only executes after step 3, as part of step 4.

Meanwhile, the post-login action executes as part of step 4, as well as a part of steps 6, 9, 12 and any other login flow where the user’s browser presents cookies pertaining to an existing valid session.

Of course, depending on the IdP’s policies, the access token stored in the connection’s identity may only be valid for step 4, as it may expire prior to later steps, and Auth0 does not attempt to refresh IdP tokens.

For these reasons, I’d like to add some condition to my action such that it will only perform the fetch of information from the IdP upon the initial federated login flow in a session (i.e. in step 4). (To clarify, the action should repeat the fetch of information from the IdP upon federated login in subsequent Auth0 sessions, such that the information is updated each time the user has successfully signed in via redirect to/return from the IdP.)

Potential Solutions

Check the event.connection.name property?

Originally I thought that I could use the presence of the event.connection.name property to confirm whether the user had just completed a federated login. This idea was based on the observation that the “Success Login” events (type: 's') logged for a full federated login have their connection property set to the connection name, as well as a corresponding connection_id property, while the “Success Login” events for later flows in the same session (as in steps 6, 9 and 12) lack a connection property and have connection_id: "", which is rendered in the user’s event history as Connection = “N/A”.

Unfortunately, it appears that no such distinction is present in the event.connection object passed to the post-login action. Instead, it appears that the connection strategy and name provided in later flows are the same as initial federated login flow.

Store and compare the session id

One solution that could feasibly work is to save the event.session.id alongside/with the custom info, and to compare that value before proceeding to fetch the IdP information:

exports.onExecutePostLogin = async (event, api) => {
  if (event.connection.strategy === 'oauth2'
      && event.connection.name === 'my-custom-connection-name'
      // FIX: Add this condition to avoid refetching in the same session
      && event.session.id !== event.user.app_metadata.my_custom_connection_info.session_id) {

    /* ... fetch from IdP as shown above ... */

    if (response.ok) {
      const info = await response.json();

      // FIX: Add this line so we can compare it above in later executions of this action
      info.session_id = event.session.id;
      api.user.setAppMetadata('my_custom_connection_info', info);
    }
  }
}

This solution would seem to work fine, but it poses the following concerns:

  1. Does saving a user’s session identifier in their app_metadata pose a security risk of some form? If this identifier becomes available to applications that can read the user’s metadata, can it be used to impersonate or otherwise obtain information about the user beyond what is available from the user record itself?
  2. What would happen with this solution if the user were simultaneously logged in on multiple devices/browsers? I suspect that each device/browser would have its own session identifier, and each time the user is redirected to Auth0 to login from an application on the other device/browser, then the session identifier would differ from the previous, and the action would attempt to fetch and save the IdP information, replacing the other device/browser’s session id.

If I’m right on #2, then this solution is insufficient to guarantee that the IdP fetch will only occur on the initial federated login for a particular session.

Check another (currently undocumented) event property

In my development tenant, I added some instrumentation to capture the full event object that is available for comparison between the initial federated login (step 4) and a subsequent login flow to the same application with an existing session (step 9). Comparing these objects, I observe only the following differences:

event property initial federated login subsequent login in same session
transaction.id some opaque string not present
transaction.linking_id null not present
transaction.login_hint null not present
transaction.state some opaque string another opaque string
request.query.protocol 'oauth2' not present
request.query.code_challenge some opaque string another opaque string
request.query.nonce some opaque string another opaque string
request.query.state some opaque string another opaque string

Based on these differences, it seems like the only possible way for the action to distinguish the initial federated login from subsequent logins in the same session is to check for either (a) the presence of event.transaction.id or (b) whether event.request.query.protocol === 'oauth2'.

Neither of these properties are documented members of the post-login event object, so I’m hesitant to rely on these as definitive proof that the user has just completed an initial federated login flow, for fear of perpetuating Hyrum’s law and becoming too sensitive to Auth0’s incidental current behavior.

Synopsis

Given that this logic cannot be executed as part of the “Fetch User Profile Script”, but it should be executed only once following a successful federated login with a specific OAuth2 connection, how can I craft the conditional in my action to safely and accurately determine when the logic should execute?

Hi @eterobby

Thank you for posting your question on the Community!

Unfortunately, there is no way to identify if the login occurred was through a normal login(email+password) or through an SSO session.

I can propose two solutions which are quite similar to the issue at hand:

  1. Store a boolian inside the app_metadata of the user is order to determine if the session is through an SSO or not
  • Once the user logs in normally, set a value under their app_metadata (ex: hasUserLoggedIn = true)
  • Add the condition to you action to fire only if(!hasUserLoggedIn)
  • Whenever they session expires and the user is logged out, make a Management API call to set the value to false in order for the action to fire.

2.This is a similar approach, where you would check if the session should have expired for the user using the event.session.expires_at

  • When the user logs in, save the value of event.session.expires_at inside the app_metadata of the user. // Whenever the session of the user is refreshed and this expiration changed, it should be updated.
  • Change your action in order to fire only if the current time has passed the expiration of the session. (if the session would be expired, that means the user is logged out and they need to authenticate with Auth0 again)

If you have any other questions or need extra help on the matter, feel free to leave a reply!

Kind Regards,
Nik
*

Hi NIk,

Thank you for your response and the proposed solutions. I really appreciate your timely feedback.

Storing some flag in app_metadata can work for distinguishing initial login from repeat logins. This is similar to my idea of storing and comparing the session id.

However, I see the following challenges:

Whenever they session expires and the user is logged out, make a Management API call to set the value to false in order for the action to fire.

  1. It’s not clear to me how to implement this point. Auth0 does not appear to support triggers that execute on logout actions – it seems like I would either need to add explicit logic in all of my applications to handle this case, or I would need to subscribe to the Auth0 event stream and implement some code that responds to slo events.

  2. It’s also not clear is how to execute that logic when the session expires – there’s no specific code executing (at Auth0 or in my application) when this occurs, and after the session has expired, the user no longer has valid credentials for making an API call to Auth0 or my application. It seems like you’ve addressed this challenge in your second solution by storing the event.session.expires_at timestamp, which would in fact detect sessions that have timed out. However, a user’s login session may also expire simply because their browser cookies are cleared, in which case the original session may still be valid, but they no longer have access to it, and furthermore we no longer have any knowledge of which Auth0 user they are/were, so there is no way to locate the app_metadata flag to reset.

  3. Storing a single flag in app_metadata presumes that the user has only a single login session at a time. However, users may be signed into our applications from multiple devices at once – using a laptop, tablet or phone – or they sometimes even utilize multiple browsers (or incognito windows) for various reasons. This solution could detect the initial SSO login on their first device, but then the initial SSO login flow on a secondary device would see the flag is already set, and be handled the same as a repeat flow on the original device. So this solution has similar problems as my original idea of storing and comparing the session id.

Compare event.authentication[].timestamp to app_metadata timestamp

Recognizing the limitations of all the above solutions, I decided to go with an alternative solution that (a) stores the timestamp of the last IdP fetch in app_metadata and (b) uses the event.authentication[].timestamp property (which doesn’t change on repeat session login) to avoid refetching within the same session:

exports.onExecutePostLogin = async (event, api) => {
  if (event.connection.strategy === 'oauth2'
      && event.connection.name === 'my-custom-connection-name') {

    // Ensure we only fetch once per SSO login
    const federatedTimestamp = event.authentication?.methods.find(({ name }) => name === 'federated')?.timestamp;
    if (!federatedTimestamp || federatedTimestamp < app_metadata.my_custom_connection_fetched_at) {
      return; // already fetched since SSO login
    }
    api.user.setAppMetadata('my_custom_connection_fetched_at', new Date().toISOString());

    /* ... fetch from IdP as shown above ... */
  }
}

This solution seems to have the right behavior: Since the federated authentication timestamp does not change on repeat logins for that session, we can compare its value vs the timestamp of the latest IdP fetch to determine whether this federated SSO login occurred more recently, and therefore an IdP fetch is warranted.

Outside of pathological scenarios (where the app_metadata update fails due to an error later in the login flow), the only potential scenario would be if a user initiated multiple initial SSO login flows near simultaneously. In this scenario, the IdP information would probably be fetched exactly once, rather than once per session, but we do not expect the IdP information to vary much over time, so this is of little concern. (It is also unlikely for multiple near-simultaneous flows to occur in general, but a conceivable scenario where it could occur is if the user restored multiple browser tabs upon a browser restart and each tab separately and concurrently redirected the user to authenticate via their SSO provider.)

Otherwise I am sufficiently satisfied with this new solution, and I appreciate the feedback you provided on potential ideas.

Hi @eterobby

I completely understand the suggestions I have provided above would present some challenges or difficulties in implementing them. Otherwise, thank you for providing the solution with the rest of the community regarding the matter!

If you ever have any other questions, feel free to post on the community. again! Thank you for taking the time to take into considerations my solutions!

Kind Regards,
Nik