fusionauth-issues icon indicating copy to clipboard operation
fusionauth-issues copied to clipboard

Persist custom JWT properties when refreshing a token

Open stuellidge opened this issue 2 years ago • 9 comments

Persist custom JWT properties when refreshing a token

Problem

The Populate JWT lambda makes it possible to set custom JWT properties on a token. For some (for example, capturing the original login instant or preserving the original authentication type) it is not possible to define them on a subsequent token refresh because the original token is not available.

Solution

We would like the ability to preserve custom JWT properties between refreshed tokens

Alternatives/workarounds

An alternative (not available) would be to provide the previous token during a refresh JWT Populate so that we could copy properties across.

Additional context

N/A

Community guidelines

All issues filed in this repository must abide by the FusionAuth community guidelines.

How to vote

Please give us a thumbs up or thumbs down as a reaction to help us prioritize this feature. Feel free to comment if you have a particular need or comment on how this feature should work.

stuellidge avatar Nov 09 '21 09:11 stuellidge

How about to store custom JWT properties in user.data object and use a lambda function to populate JWT? Will it solve your problem?

BugShooter avatar Jan 10 '22 16:01 BugShooter

How about to store custom JWT properties in user.data object and use a lambda function to populate JWT? Will it solve your problem?

Hi - I'm afraid not. We thought about that as a possible option, but a user can have multiple concurrent sessions and so you can't guarantee which token's data is being stored in the user.data. Thanks for checking though.

stuellidge avatar Jan 10 '22 17:01 stuellidge

@stuellidge Just to make sure I understand the issue, is this what you are trying to do?

  1. Set up a lambda and put info into the token, like the loginInstant.
  2. Have a user login with the offline_access scope, so they get a refresh token
  3. The initial token includes the needed info.
  4. After a period of time, you use the refresh token against an endpoint (which one?) to get a new access token.
  5. The access token does not contain the custom claim (loginInstant), which is what you need.

Is that correct?

mooreds avatar Jan 10 '22 18:01 mooreds

@mooreds Hi, picking this for @stuellidge

Given we have a JWT Populate lambda using the following code:

function populate(jwt, user, registration) {
  // Debug logging
  console.debug('JWT Before Lambda Execution');
  console.debug(JSON.stringify(jwt));
 
  // This variable is just an example
  var passwordlessAuthenticationTypes = ['PASSWORDLESS', 'HYPR'];
  
  // Check to see if we have a `source` property on the JWT, if we login via a passwordless method, 
  if (!jwt.source && passwordlessAuthenticationTypes.indexOf(jwt.authenticationType) >= 0) {
    jwt.source = 'PASSWORDLESS';
  }
  
  console.debug('JWT After Lambda Execution');
  console.debug(JSON.stringify(jwt));
}

When we first login using a passwordless method (in our case HYPR) we correctly have the following JWT payload if we decode our token;

{
  ...,
  "exp": 1641913950,
  "iat": 1641913050, 
  "sub": "78c7cbef-dfdc-4403-b77a-e47cf7dae3b4",
  "authenticationType": "HYPR",
  "source": "PASSWORDLESS",
  ...
}

The source in this case is the property that we'd like to maintain through all refresh events.

For example, when POSTing to the /oauth2/token endpoint with the following data:

{
    "scopes": "offline_access",
    "client_id": "clientId",
    "client_secret": "clientSecret",
    "refresh_token": "<< USER REFRESH TOKEN HERE>>",
    "access_token": "<< USER ACCESS TOKEN THAT HAS/ABOUT TO EXPIRE >>",
    "grant_type": "refresh_token"
}

This then invokes our lambda shown above, but the jwt does not retain the existing source property (I assume as it's a newly generated JWT).

Our code also will not continue to work, as the authenticationType in this case is now REFRESH_TOKEN not one of the ones defined in our array. We should not add REFRESH_TOKEN to our array, as a user could bypass this by authenticating with a password, then performing a refresh and bypass the passwordless mandate.

If we were able to have access to the previous/existing/original jwt, we could do something like;

// We add a `originalJwt` to the `populate` method signature, which gives us access to the JSON of the JWT we refreshed (optional - not always going to be provided)
function populate(jwt, user, registration, originalJwt) {
  if (!jwt.source && originalJwt && originalJwt.source) {
      jwt.source = originalJwt.source;
  }
}

This also relates to #1484 and #1491 but I feel as if having the ability to preserve claims/properties through refreshes is beneficial to integration developers looking to preserve some unique state from the initial JWT issue. For example, as stated, the loginInstant.

CaLxCyMru avatar Jan 11 '22 15:01 CaLxCyMru

Thanks for the additional explanation @CaLxCyMru .

It sounds like there are two approaches that might solve the issues:

  • allowing access to the original JWT when a refresh is made
  • persisting certain fields (as defined through configuration) across refreshes

mooreds avatar Jan 11 '22 18:01 mooreds

persisting certain fields (as defined through configuration) across refreshes

This could work. One possible issue may be that if a custom claim is based upon user data, or user state, the claim may be "stale" so to speak if we just copy it along. Maybe this is buyer beware if you enable this feature.

allowing access to the original JWT when a refresh is made

If you were to present the existing JWT during the refresh request (we don't persist this), I suppose it is plausible that we could provide that as an argument to a populate lambda. This may be risky because in most cases I would assume the JWT will be expired. If it is expired, I don't think we would want to trust it or even present it to a JWT populate because we would be implying trust.

We'd also need to see how to add this capability to both the JWT Refresh API (easy) and the Token endpoint to support the same capability through the Refresh grant (more difficult).

robotdan avatar Jan 12 '22 04:01 robotdan

Thanks for the consideration @robotdan @mooreds and for the explanation @CaLxCyMru

stuellidge avatar Jan 14 '22 12:01 stuellidge

If we were to add this capability, not sure how we know what is "custom". A simple approach would be to take the object keyset prior to the lambda, and then any new keys after the lambda are considered "custom" (not added by FusionAuth) and we would store them away for use when we issue another JWT using a refresh token.

If we were to add claims such as auth_time (original auth time) and amr (authentication methods) and preserve these through token refreshes, would that cover this use case ? Or are there still cases where you want to add arbitrary claims in the lambda and those should be preserved across a refresh?

robotdan avatar Mar 23 '22 00:03 robotdan

If we were to add this capability, not sure how we know what is "custom". A simple approach would be to take the object keyset prior to the lambda, and then any new keys after the lambda are considered "custom" (not added by FusionAuth) and we would store them away for use when we issue another JWT using a refresh token.

If we were to add claims such as auth_time (original auth time) and amr (authentication methods) and preserve these through token refreshes, would that cover this use case ? Or are there still cases where you want to add arbitrary claims in the lambda and those should be preserved across a refresh?

In cognito, custom attributes are always prefixed withcustom:. Not saying it's the right approach.

theogravity avatar Sep 13 '22 23:09 theogravity

@stuellidge Just to make sure I understand the issue, is this what you are trying to do?

  1. Set up a lambda and put info into the token, like the loginInstant.
  2. Have a user login with the offline_access scope, so they get a refresh token
  3. The initial token includes the needed info.
  4. After a period of time, you use the refresh token against an endpoint (which one?) to get a new access token.
  5. The access token does not contain the custom claim (loginInstant), which is what you need.

Is that correct?

Hello @mooreds, this is exactly my issue. Why would the refresh token not have the claims? Any solution?

RicardoViteriR avatar Jun 23 '23 20:06 RicardoViteriR