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:
- User attempts to access application A and is redirected to Auth0 (universal login).
- User chooses to use federated login through my custom connection.
- User signs in to the custom connection and is redirected back to Auth0.
- Auth0 sets login session cookies and redirects the user successfully back to application A.
- User attempts to access application B and is redirected to Auth0.
- Auth0 recognizes session cookies (from step 4) and redirects the user successfully back to application B.
- Some time passes (but less than the Idle Session Lifetime)…
- User attempts to access application A again and is redirected to Auth0.
- Auth0 recognizes session cookies (from step 4) and redirects the user successfully back to application A.
- Some more time passes (but less than the Idle Session Lifetime since step 9 and less than Maximum Session Lifetime since step 4)…
- User attempts to access application C and is redirected to Auth0.
- 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:
- 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?
- 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?