From f99cdb23af7444f9d43f0314c28eadd28d1183cc Mon Sep 17 00:00:00 2001 From: Daniel Flemming Date: Mon, 10 Feb 2025 15:31:03 +0100 Subject: [PATCH 1/2] [PIWEB-21318] fix: Always store valid access tokens in local cache * If we obtain a valid access token, we should always store that in the local cache, no matter what * When we don't store that we can never explicitly trigger the creation of a new token from calling code that is reused (stored in cache) * We either trigger the creation of a token that is not stored or we get a possibly outdated token from cache - neither of these 2 conditions is great --- src/Api.Rest/Common/Utilities/OAuthHelper.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Api.Rest/Common/Utilities/OAuthHelper.cs b/src/Api.Rest/Common/Utilities/OAuthHelper.cs index de8c3277..a6f025b4 100644 --- a/src/Api.Rest/Common/Utilities/OAuthHelper.cs +++ b/src/Api.Rest/Common/Utilities/OAuthHelper.cs @@ -280,8 +280,7 @@ public static async Task GetAuthenticationInformationForDa if( result == null ) return null; - if( !bypassLocalCache ) - AccessTokenCache.Store( instanceUrl, result ); + AccessTokenCache.Store( instanceUrl, result ); return result; } @@ -317,8 +316,7 @@ public static OAuthTokenCredential GetAuthenticationInformationForDatabaseUrl( if( result == null ) return null; - if( !bypassLocalCache ) - AccessTokenCache.Store( instanceUrl, result ); + AccessTokenCache.Store( instanceUrl, result ); return result; } From 4d3c888954dd7974a2d2f62c8c3fdb7664b9086a Mon Sep 17 00:00:00 2001 From: Daniel Flemming Date: Mon, 10 Feb 2025 15:31:38 +0100 Subject: [PATCH 2/2] [PIWEB-21318] feat: Add cancellation support to access token renewal --- src/Api.Rest/Common/Utilities/OAuthHelper.cs | 13 ++++++++----- .../AuthorizationCodeFlow.cs | 15 ++++++++++----- .../OAuth/AuthenticationFlows/HybridFlow.cs | 13 ++++++++----- .../IOidcAuthenticationFlow.cs | 5 ++++- .../OidcAuthenticationFlowBase.cs | 19 ++++++++++--------- 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/Api.Rest/Common/Utilities/OAuthHelper.cs b/src/Api.Rest/Common/Utilities/OAuthHelper.cs index a6f025b4..10a27027 100644 --- a/src/Api.Rest/Common/Utilities/OAuthHelper.cs +++ b/src/Api.Rest/Common/Utilities/OAuthHelper.cs @@ -18,6 +18,7 @@ namespace Zeiss.PiWeb.Api.Rest.Common.Utilities using System.IO; using System.Linq; using System.Security.Claims; + using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Zeiss.PiWeb.Api.Rest.HttpClient.OAuth; @@ -225,14 +226,14 @@ private static OAuthTokenCredential TryGetCurrentOAuthToken( string instanceUrl, /// https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery /// /// - private static async Task GetOAuthConfigurationAsync( string instanceUrl ) + private static async Task GetOAuthConfigurationAsync( string instanceUrl, CancellationToken cancellationToken = default ) { var oauthServiceRest = new OAuthServiceRestClient( new Uri( instanceUrl ) ) { UseDefaultWebProxy = true }; - var tokenInformation = await oauthServiceRest.GetOAuthConfiguration().ConfigureAwait( false ); + var tokenInformation = await oauthServiceRest.GetOAuthConfiguration( cancellationToken ).ConfigureAwait( false ); if( tokenInformation == null ) throw new InvalidOperationException( "Cannot detect OpenID token information from resource URL." ); @@ -256,12 +257,14 @@ private static IOidcAuthenticationFlow ChooseSuitableAuthenticationFlow( OAuthCo /// Optional refresh token that is used to renew the authentication information. /// Optional callback to request the user to interactively authenticate. /// Defines whether locally cached token information are neither used nor updated. + /// The that can be used to cancel the operation. /// A new instance, or null, if no token could be retrieved. public static async Task GetAuthenticationInformationForDatabaseUrlAsync( string databaseUrl, string refreshToken = null, Func> requestCallbackAsync = null, - bool bypassLocalCache = false ) + bool bypassLocalCache = false, + CancellationToken cancellationToken = default ) { var instanceUrl = GetInstanceUrl( databaseUrl ); @@ -272,10 +275,10 @@ public static async Task GetAuthenticationInformationForDa return cachedToken; } - var tokenInformation = await GetOAuthConfigurationAsync( instanceUrl ).ConfigureAwait( false ); + var tokenInformation = await GetOAuthConfigurationAsync( instanceUrl, cancellationToken ).ConfigureAwait( false ); var authenticationFlow = ChooseSuitableAuthenticationFlow( tokenInformation ); - var result = await authenticationFlow.ExecuteAuthenticationFlowAsync( refreshToken, tokenInformation, requestCallbackAsync ).ConfigureAwait( false ); + var result = await authenticationFlow.ExecuteAuthenticationFlowAsync( refreshToken, tokenInformation, requestCallbackAsync, cancellationToken ).ConfigureAwait( false ); if( result == null ) return null; diff --git a/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/AuthorizationCodeFlow.cs b/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/AuthorizationCodeFlow.cs index 7509cc89..153025f7 100644 --- a/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/AuthorizationCodeFlow.cs +++ b/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/AuthorizationCodeFlow.cs @@ -14,6 +14,7 @@ namespace Zeiss.PiWeb.Api.Rest.HttpClient.OAuth.AuthenticationFlows; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using IdentityModel.Client; using Zeiss.PiWeb.Api.Rest.Common.Utilities; @@ -32,7 +33,8 @@ private static async Task TryGetOAuthTokenFromAuthorizeRes CryptoNumbers cryptoNumbers, AuthorizeResponse response, OAuthConfiguration configuration, - DiscoveryDocumentResponse discoveryDocument ) + DiscoveryDocumentResponse discoveryDocument, + CancellationToken cancellationToken = default ) { if( response?.Code == null ) return null; @@ -44,11 +46,14 @@ private static async Task TryGetOAuthTokenFromAuthorizeRes var tokenResponse = await tokenClient.RequestAuthorizationCodeTokenAsync( code: response.Code, redirectUri: tokenInformation.RedirectUri, - codeVerifier: cryptoNumbers.Verifier ).ConfigureAwait( false ); + codeVerifier: cryptoNumbers.Verifier, + cancellationToken: cancellationToken ).ConfigureAwait( false ); if( tokenResponse.IsError ) + { throw new InvalidOperationException( $"Error during request of access token using authorization code: {tokenResponse.Error}. {tokenResponse.ErrorDescription}." ); + } // decode the IdentityToken claims var claims = OAuthHelper.DecodeSecurityToken( tokenResponse.IdentityToken ).Claims.ToArray(); @@ -108,13 +113,13 @@ public OAuthTokenCredential ExecuteAuthenticationFlow( string refreshToken, OAut } /// - public async Task ExecuteAuthenticationFlowAsync( string refreshToken, OAuthConfiguration configuration, Func> requestCallbackAsync ) + public async Task ExecuteAuthenticationFlowAsync( string refreshToken, OAuthConfiguration configuration, Func> requestCallbackAsync, CancellationToken cancellationToken ) { var discoveryInfo = await GetDiscoveryInfoAsync( configuration.UpstreamTokenInformation ).ConfigureAwait( false ); ThrowOnInvalidDiscoveryDocument( discoveryInfo ); var tokenClient = CreateTokenClient( discoveryInfo.TokenEndpoint, configuration.UpstreamTokenInformation.ClientID ); - var result = await TryGetOAuthTokenFromRefreshTokenAsync( tokenClient, discoveryInfo.UserInfoEndpoint, refreshToken, configuration ).ConfigureAwait( false ); + var result = await TryGetOAuthTokenFromRefreshTokenAsync( tokenClient, discoveryInfo.UserInfoEndpoint, refreshToken, configuration, cancellationToken ).ConfigureAwait( false ); if( result != null ) return result; @@ -130,7 +135,7 @@ public async Task ExecuteAuthenticationFlowAsync( string r ThrowOnInvalidAuthorizeResponse( response ); - result = await TryGetOAuthTokenFromAuthorizeResponseAsync( tokenClient, cryptoNumbers, response, configuration, discoveryInfo ).ConfigureAwait( false ); + result = await TryGetOAuthTokenFromAuthorizeResponseAsync( tokenClient, cryptoNumbers, response, configuration, discoveryInfo, cancellationToken ).ConfigureAwait( false ); return result; } diff --git a/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/HybridFlow.cs b/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/HybridFlow.cs index 46b80891..16e78089 100644 --- a/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/HybridFlow.cs +++ b/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/HybridFlow.cs @@ -15,6 +15,7 @@ namespace Zeiss.PiWeb.Api.Rest.HttpClient.OAuth.AuthenticationFlows; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using IdentityModel.Client; using Zeiss.PiWeb.Api.Rest.Common.Utilities; @@ -33,7 +34,8 @@ private static async Task TryGetOAuthTokenFromAuthorizeRes CryptoNumbers cryptoNumbers, AuthorizeResponse response, OAuthConfiguration configuration, - DiscoveryDocumentResponse discoveryDocument ) + DiscoveryDocumentResponse discoveryDocument, + CancellationToken cancellationToken = default ) { if( response == null ) return null; @@ -69,7 +71,8 @@ private static async Task TryGetOAuthTokenFromAuthorizeRes var tokenResponse = await tokenClient.RequestAuthorizationCodeTokenAsync( code: response.Code!, redirectUri: tokenInformation.RedirectUri, - codeVerifier: cryptoNumbers.Verifier ).ConfigureAwait( false ); + codeVerifier: cryptoNumbers.Verifier, + cancellationToken: cancellationToken ).ConfigureAwait( false ); if( tokenResponse.IsError ) throw new InvalidOperationException( $"Error during request of access token using authorization code: {tokenResponse.Error}." ); @@ -116,13 +119,13 @@ public OAuthTokenCredential ExecuteAuthenticationFlow( string refreshToken, OAut } /// - public async Task ExecuteAuthenticationFlowAsync( string refreshToken, OAuthConfiguration configuration, Func> requestCallbackAsync ) + public async Task ExecuteAuthenticationFlowAsync( string refreshToken, OAuthConfiguration configuration, Func> requestCallbackAsync, CancellationToken cancellationToken ) { var discoveryInfo = await GetDiscoveryInfoAsync( configuration.LocalTokenInformation ).ConfigureAwait( false ); ThrowOnInvalidDiscoveryDocument( discoveryInfo ); var tokenClient = CreateTokenClient( discoveryInfo.TokenEndpoint, configuration.LocalTokenInformation.ClientID ); - var result = await TryGetOAuthTokenFromRefreshTokenAsync( tokenClient, discoveryInfo.UserInfoEndpoint, refreshToken, configuration ).ConfigureAwait( false ); + var result = await TryGetOAuthTokenFromRefreshTokenAsync( tokenClient, discoveryInfo.UserInfoEndpoint, refreshToken, configuration, cancellationToken ).ConfigureAwait( false ); if( result != null ) return result; @@ -138,7 +141,7 @@ public async Task ExecuteAuthenticationFlowAsync( string r ThrowOnInvalidAuthorizeResponse( response ); - result = await TryGetOAuthTokenFromAuthorizeResponseAsync( tokenClient, cryptoNumbers, response, configuration, discoveryInfo ).ConfigureAwait( false ); + result = await TryGetOAuthTokenFromAuthorizeResponseAsync( tokenClient, cryptoNumbers, response, configuration, discoveryInfo, cancellationToken ).ConfigureAwait( false ); return result; } diff --git a/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/IOidcAuthenticationFlow.cs b/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/IOidcAuthenticationFlow.cs index f4350b17..65b7c533 100644 --- a/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/IOidcAuthenticationFlow.cs +++ b/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/IOidcAuthenticationFlow.cs @@ -13,6 +13,7 @@ namespace Zeiss.PiWeb.Api.Rest.HttpClient.OAuth.AuthenticationFlows; #region usings using System; +using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Zeiss.PiWeb.Api.Rest.Common.Utilities; @@ -42,9 +43,11 @@ OAuthTokenCredential ExecuteAuthenticationFlow( [CanBeNull] string refreshToken, /// Refresh token to acquire a new authentication token. /// OAuth configuration containing the settings for authentication. /// The asynchronous callback to execute for authentication, e.g opening a browser window. + /// The that can be used to cancel the operation. Task ExecuteAuthenticationFlowAsync( [CanBeNull] string refreshToken, OAuthConfiguration configuration, - Func> requestCallbackAsync ); + Func> requestCallbackAsync, + CancellationToken cancellationToken ); #endregion } \ No newline at end of file diff --git a/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/OidcAuthenticationFlowBase.cs b/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/OidcAuthenticationFlowBase.cs index 664d14da..5afc03a0 100644 --- a/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/OidcAuthenticationFlowBase.cs +++ b/src/Api.Rest/HttpClient/OAuth/AuthenticationFlows/OidcAuthenticationFlowBase.cs @@ -15,6 +15,7 @@ namespace Zeiss.PiWeb.Api.Rest.HttpClient.OAuth.AuthenticationFlows; using System; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using IdentityModel; using IdentityModel.Client; @@ -73,14 +74,14 @@ protected static string ChooseAccessToken( TokenResponse tokenResponse, OAuthCon /// Token information containing the discovery location and other settings. protected static async Task GetDiscoveryInfoAsync( OAuthTokenInformation tokenInformation ) { - var discoveryCache = new DiscoveryCache( tokenInformation.OpenIdAuthority, - new DiscoveryPolicy - { - AdditionalEndpointBaseAddresses = tokenInformation.AdditionalEndpointBaseAddresses - } ); + var discoveryPolicy = new DiscoveryPolicy + { + AdditionalEndpointBaseAddresses = tokenInformation.AdditionalEndpointBaseAddresses + }; + + var discoveryCache = new DiscoveryCache( tokenInformation.OpenIdAuthority, discoveryPolicy ); - var discoveryInfo = await discoveryCache.GetAsync().ConfigureAwait( false ); - return discoveryInfo; + return await discoveryCache.GetAsync().ConfigureAwait( false ); } /// @@ -128,13 +129,13 @@ protected static string CreateOAuthStartUrl( string authorizeEndpoint, string re /// Refresh token to acquire a new authentication token. /// OAuth configuration of the PiWeb Server. /// A valid or if no token could be retrieved. - protected static async Task TryGetOAuthTokenFromRefreshTokenAsync( TokenClient tokenClient, string userInfoEndpoint, string refreshToken, OAuthConfiguration configuration ) + protected static async Task TryGetOAuthTokenFromRefreshTokenAsync( TokenClient tokenClient, string userInfoEndpoint, string refreshToken, OAuthConfiguration configuration, CancellationToken cancellationToken = default ) { // when a refresh token is present try to use it to acquire a new access token if( string.IsNullOrEmpty( refreshToken ) ) return null; - var tokenResponse = await tokenClient.RequestRefreshTokenAsync( refreshToken ).ConfigureAwait( false ); + var tokenResponse = await tokenClient.RequestRefreshTokenAsync( refreshToken, cancellationToken: cancellationToken ).ConfigureAwait( false ); if( tokenResponse.IsError ) return null;