Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist custom JWT properties when refreshing a token #1484

Open
stuellidge opened this issue Nov 9, 2021 · 10 comments
Open

Persist custom JWT properties when refreshing a token #1484

stuellidge opened this issue Nov 9, 2021 · 10 comments
Labels
enhancement New feature or request

Comments

@stuellidge
Copy link

stuellidge commented Nov 9, 2021

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

Related

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.

@BugShooter
Copy link

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

@stuellidge
Copy link
Author

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.

@mooreds
Copy link
Collaborator

mooreds commented Jan 10, 2022

@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 mooreds added the enhancement New feature or request label Jan 10, 2022
@CaLxCyMru
Copy link

@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.

@mooreds
Copy link
Collaborator

mooreds commented Jan 11, 2022

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

@robotdan
Copy link
Member

robotdan commented Jan 12, 2022

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).

@stuellidge
Copy link
Author

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

@robotdan robotdan added this to Backlog in FusionAuth Issues via automation Jan 15, 2022
@robotdan robotdan self-assigned this Jan 15, 2022
@robotdan robotdan added this to the 1.33.0 milestone Jan 15, 2022
@robotdan robotdan modified the milestones: 1.34.0, 1.35.0 Feb 17, 2022
@robotdan robotdan modified the milestones: 1.35.0, 1.36.0 Mar 8, 2022
@robotdan robotdan modified the milestones: 1.36.0, 1.37.0 Mar 21, 2022
@robotdan
Copy link
Member

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 robotdan modified the milestones: 1.37.0, 1.38.0 Jul 17, 2022
@robotdan robotdan modified the milestones: 1.38.0, 1.39.0, 1.40.0 Aug 12, 2022
@theogravity
Copy link

theogravity commented Sep 13, 2022

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.

@robotdan robotdan modified the milestones: 1.40.0, 1.42.0 Sep 16, 2022
@robotdan robotdan modified the milestones: 1.42.0, 1.43.0 Nov 17, 2022
@RicardoViteriR
Copy link

RicardoViteriR commented Jun 23, 2023

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants