You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, TokenRefreshInfo is used to hold on to access tokens and refresh tokens. This is a scoped class that is valid for the duration of the request.
This explicitly scoped instance can be removed because there is already a scoped ClaimsIdentity whose BootstrapContext can hold any object. It can therefore also hold a TokenRefreshInfo instance.
We can even restrict ourselves to a TokenInfo instance if we make sure that whatever token we put in there has a sufficiently long lifetime to be used throughout the request: then we don't need the refresh token information and a TokenInfo with access token information and expiration date (for checking) should suffice.
The advantage is that it simplifies the refresh logic: ITokenRefreshProvider and its implementation RefreshTokenProvider are no longer needed, because you make sure that when the BootstrapContext is initialized, it will contain a sufficiently "fresh" token.
If we want to do this, we need to resolve three problems:
Today, Arc4u currently uses BootstrapContext to store a simple string (i.e. the access token value)
Today, Arc4u assumes in some providers that TokenRefreshInfo is a scoped instance obtained from the service provider: this will need to be rewritten.
For cookie-based authentication, the BootstrapContext is never set.
Resolving problem 1 is trivial. There are only 13 occurrences of BootstrapContext in Arc4u, 3 of them are assignments.
For example, changing the assignments:
identity.BootstrapContext = token.Token; can be replaced with identity.BootstrapContext = token; (2 occurrences)
identity.BootstrapContext = accessToken.RawData; can be replaced with identity.BootstrapContext = new TokenInfo("access_token", accessToken.RawData, accessToken.ValidTo.ToUniversalTime()); (1 occurrence)
For the other 10 occurrences, it's a simple matter of casting to the correct type. Perhaps it can be factored out in an extension method on ClaimsIdentity:
Problem 2 is easy as well: we use the IApplicationContext to get at the principal. For example, here is what OidcTokenProvider would look like:
[Export(ProviderName,typeof(ITokenProvider))]publicclassOidcTokenProvider:ITokenProvider{publicconststringProviderName="Oidc";publicOidcTokenProvider(IApplicationContextapplicationContext){_applicationContext=applicationContext;}privatereadonlyIApplicationContext_applicationContext;publicTask<TokenInfo>GetTokenAsync(IKeyValueSettingssettings,objectplatformParameters){ArgumentNullException.ThrowIfNull(settings);if(_applicationContext.Principal.IdentityisClaimsIdentityclaimsIdentity&&claimsIdentity.BootstrapContextisTokenInfotokenInfo)returnTask.FromResult(tokenInfo.AccessToken);elsethrownewInvalidOperationException($"The bootstrap context is not correctly initialized");}publicValueTaskSignOutAsync(IKeyValueSettingssettings,CancellationTokencancellationToken){thrownewNotImplementedException();}}
Problem 3 is the most complicated one, but the cookie event ValidatePrincipal can be re-implemented to take care of the BootstrapContext initialization, making sure that the token is "fresh":
publicclassStandardCookieEvents:CookieAuthenticationEvents{publicStandardCookieEvents(IOptions<OidcAuthenticationOptions>oidcOptions,IOptionsMonitor<OpenIdConnectOptions>openIdConnectOptions,ILogger<StandardCookieEvents>logger){_httpClient=newHttpClient();// never disposed, but StandardCookieEvents is a singleton anyway_openIdConnectOptions=openIdConnectOptions;_logger=logger;_forceRefreshTimeout=oidcOptions.Value.ForceRefreshTimeoutTimeSpan;}privatereadonlyILogger<StandardCookieEvents>_logger;privatereadonlyIOptionsMonitor<OpenIdConnectOptions>_openIdConnectOptions;privatereadonlyHttpClient_httpClient;privatereadonlyTimeSpan_forceRefreshTimeout;publicoverrideasyncTaskValidatePrincipal(CookieValidatePrincipalContextcookieCtx){try{vartokens=cookieCtx.Properties.GetTokens();varexp=tokens.First(t =>t.Name=="expires_at");varexpires=DateTime.Parse(exp.Value,styles:DateTimeStyles.AssumeUniversal|DateTimeStyles.AdjustToUniversal);vartimeRemaining=expires.Subtract(DateTime.UtcNow);AuthenticationTokenaccessToken;varrefreshToken=tokens.FirstOrDefault(t =>t.Name=="refresh_token");//check to see if the token has expiredif(timeRemaining<_forceRefreshTimeout){// We can only renew if there's a refesh tokenif(refreshTokenisnull){awaitRejectAndLogout(cookieCtx);return;}varoptions=_openIdConnectOptions.Get(OpenIdConnectDefaults.AuthenticationScheme);varcancellationToken=cookieCtx.HttpContext.RequestAborted;varconfig=awaitoptions!.ConfigurationManager!.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);// Use the token client for standardized access. This is what also should happen in RefreshTokenProvider instead of the manual post request on the backchannel.tokenendpoint there.vartokenClient=newTokenClient(_httpClient,newTokenClientOptions{Address=config.TokenEndpoint,ClientId=options.ClientId,ClientSecret=options.ClientSecret});vartokenResponse=awaittokenClient.RequestRefreshTokenAsync(refreshToken.Value,cancellationToken:cancellationToken);// check for error while renewing - any error will trigger a new login.if(tokenResponse.IsError){if(tokenResponse.Exceptionis not null)_logger.Technical().LogException(tokenResponse.Exception);else_logger.Technical().LogError($"{tokenResponse.ErrorType}: {tokenResponse.Error}{tokenResponse.ErrorDescription}");awaitRejectAndLogout(cookieCtx);return;}// set new token values// Note that a new refresh token is never provided on ADFS 2019. We keep using the old one until it expires and the call to RequestRefreshTokenAsync will failif(!string.IsNullOrEmpty(tokenResponse.RefreshToken))refreshToken.Value=tokenResponse.RefreshToken;// the assumption we make here is that there is always an access_tokenaccessToken=tokens.First(t =>t.Name=="access_token");accessToken.Value=tokenResponse.AccessToken;// set new expiration dateexpires=DateTime.UtcNow+TimeSpan.FromSeconds(tokenResponse.ExpiresIn);exp.Value=expires.ToString("o",CultureInfo.InvariantCulture);// set tokens in auth properties cookieCtx.Properties.StoreTokens(tokens);// trigger context to renew cookie with new token valuescookieCtx.ShouldRenew=true;}elseaccessToken=tokens.First(t =>t.Name=="access_token");// Set the bootstrap token for the principal to the access token we've obtained. This is normally always possible.if(cookieCtx.Principal?.IdentityisClaimsIdentityclaimsIdentity)claimsIdentity.BootstrapContext=newTokenRefreshInfo{AccessToken=newTokenInfo(accessToken.Name,accessToken.Value,expires),// As not all the autorities are using a jwt token for the refresh token, the expiration date is not extracted from the token// A refresh token is not always present: we need to handle the null case even if TokenRefreshInfo.RefreshToken is not nullable at this time.RefreshToken=(refreshTokenisnull?null:newTokenInfo(refreshToken.Name,refreshToken.Value,cookieCtx.Properties.ExpiresUtc!.Value.UtcDateTime))!};}catch(Exceptione){_logger.Technical().LogException(e);awaitRejectAndLogout(cookieCtx);}}privatestaticTaskRejectAndLogout(CookieValidatePrincipalContextcookieValidationContext){cookieValidationContext.RejectPrincipal();returncookieValidationContext.HttpContext.SignOutAsync();}}
The text was updated successfully, but these errors were encountered:
Currently,
TokenRefreshInfo
is used to hold on to access tokens and refresh tokens. This is a scoped class that is valid for the duration of the request.This explicitly scoped instance can be removed because there is already a scoped
ClaimsIdentity
whoseBootstrapContext
can hold any object. It can therefore also hold aTokenRefreshInfo
instance.We can even restrict ourselves to a
TokenInfo
instance if we make sure that whatever token we put in there has a sufficiently long lifetime to be used throughout the request: then we don't need the refresh token information and a TokenInfo with access token information and expiration date (for checking) should suffice.The advantage is that it simplifies the refresh logic:
ITokenRefreshProvider
and its implementationRefreshTokenProvider
are no longer needed, because you make sure that when theBootstrapContext
is initialized, it will contain a sufficiently "fresh" token.If we want to do this, we need to resolve three problems:
BootstrapContext
to store a simple string (i.e. the access token value)TokenRefreshInfo
is a scoped instance obtained from the service provider: this will need to be rewritten.BootstrapContext
is never set.Resolving problem 1 is trivial. There are only 13 occurrences of
BootstrapContext
in Arc4u, 3 of them are assignments.For example, changing the assignments:
identity.BootstrapContext = token.Token;
can be replaced withidentity.BootstrapContext = token;
(2 occurrences)identity.BootstrapContext = accessToken.RawData;
can be replaced withidentity.BootstrapContext = new TokenInfo("access_token", accessToken.RawData, accessToken.ValidTo.ToUniversalTime());
(1 occurrence)For the other 10 occurrences, it's a simple matter of casting to the correct type. Perhaps it can be factored out in an extension method on
ClaimsIdentity
:Problem 2 is easy as well: we use the
IApplicationContext
to get at the principal. For example, here is whatOidcTokenProvider
would look like:Problem 3 is the most complicated one, but the cookie event
ValidatePrincipal
can be re-implemented to take care of theBootstrapContext
initialization, making sure that the token is "fresh":The text was updated successfully, but these errors were encountered: