fusionauth-issues
fusionauth-issues copied to clipboard
Persist custom JWT properties when refreshing a token
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.
How about to store custom JWT properties in user.data object and use a lambda function to populate JWT? Will it solve your problem?
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 Just to make sure I understand the issue, is this what you are trying to do?
- Set up a lambda and put info into the token, like the loginInstant.
- Have a user login with the offline_access scope, so they get a refresh token
- The initial token includes the needed info.
- After a period of time, you use the refresh token against an endpoint (which one?) to get a new access token.
- The access token does not contain the custom claim (loginInstant), which is what you need.
Is that correct?
@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
.
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
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).
Thanks for the consideration @robotdan @mooreds and for the explanation @CaLxCyMru
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?
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) andamr
(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.
@stuellidge Just to make sure I understand the issue, is this what you are trying to do?
- Set up a lambda and put info into the token, like the loginInstant.
- Have a user login with the offline_access scope, so they get a refresh token
- The initial token includes the needed info.
- After a period of time, you use the refresh token against an endpoint (which one?) to get a new access token.
- 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?