diff --git a/src/Api.Rest/Common/Utilities/OAuthHelper.cs b/src/Api.Rest/Common/Utilities/OAuthHelper.cs index de8c3277..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, /// <cref>https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery</cref> /// </seealso> /// </summary> - private static async Task<OAuthConfiguration> GetOAuthConfigurationAsync( string instanceUrl ) + private static async Task<OAuthConfiguration> 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 /// <param name="refreshToken">Optional refresh token that is used to renew the authentication information.</param> /// <param name="requestCallbackAsync">Optional callback to request the user to interactively authenticate.</param> /// <param name="bypassLocalCache">Defines whether locally cached token information are neither used nor updated.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to cancel the operation.</param> /// <returns>A new <see cref="OAuthTokenCredential"/> instance, or <c>null</c>, if no token could be retrieved.</returns> public static async Task<OAuthTokenCredential> GetAuthenticationInformationForDatabaseUrlAsync( string databaseUrl, string refreshToken = null, Func<OAuthRequest, Task<OAuthResponse>> requestCallbackAsync = null, - bool bypassLocalCache = false ) + bool bypassLocalCache = false, + CancellationToken cancellationToken = default ) { var instanceUrl = GetInstanceUrl( databaseUrl ); @@ -272,16 +275,15 @@ public static async Task<OAuthTokenCredential> 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; - if( !bypassLocalCache ) - AccessTokenCache.Store( instanceUrl, result ); + AccessTokenCache.Store( instanceUrl, result ); return result; } @@ -317,8 +319,7 @@ public static OAuthTokenCredential GetAuthenticationInformationForDatabaseUrl( if( result == null ) return null; - if( !bypassLocalCache ) - AccessTokenCache.Store( instanceUrl, result ); + AccessTokenCache.Store( instanceUrl, result ); return result; } 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<OAuthTokenCredential> 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<OAuthTokenCredential> 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 } /// <inheritdoc /> - public async Task<OAuthTokenCredential> ExecuteAuthenticationFlowAsync( string refreshToken, OAuthConfiguration configuration, Func<OAuthRequest, Task<OAuthResponse>> requestCallbackAsync ) + public async Task<OAuthTokenCredential> ExecuteAuthenticationFlowAsync( string refreshToken, OAuthConfiguration configuration, Func<OAuthRequest, Task<OAuthResponse>> 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<OAuthTokenCredential> 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<OAuthTokenCredential> 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<OAuthTokenCredential> 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 } /// <inheritdoc /> - public async Task<OAuthTokenCredential> ExecuteAuthenticationFlowAsync( string refreshToken, OAuthConfiguration configuration, Func<OAuthRequest, Task<OAuthResponse>> requestCallbackAsync ) + public async Task<OAuthTokenCredential> ExecuteAuthenticationFlowAsync( string refreshToken, OAuthConfiguration configuration, Func<OAuthRequest, Task<OAuthResponse>> 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<OAuthTokenCredential> 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, /// <param name="refreshToken">Refresh token to acquire a new authentication token.</param> /// <param name="configuration">OAuth configuration containing the settings for authentication.</param> /// <param name="requestCallbackAsync">The asynchronous callback to execute for authentication, e.g opening a browser window.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to cancel the operation.</param> Task<OAuthTokenCredential> ExecuteAuthenticationFlowAsync( [CanBeNull] string refreshToken, OAuthConfiguration configuration, - Func<OAuthRequest, Task<OAuthResponse>> requestCallbackAsync ); + Func<OAuthRequest, Task<OAuthResponse>> 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 /// <param name="tokenInformation">Token information containing the discovery location and other settings.</param> protected static async Task<DiscoveryDocumentResponse> 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 ); } /// <summary> @@ -128,13 +129,13 @@ protected static string CreateOAuthStartUrl( string authorizeEndpoint, string re /// <param name="refreshToken">Refresh token to acquire a new authentication token.</param> /// <param name="configuration">OAuth configuration of the PiWeb Server.</param> /// <returns>A valid <see cref="OAuthTokenCredential"/> or <see langword="null"/> if no token could be retrieved.</returns> - protected static async Task<OAuthTokenCredential> TryGetOAuthTokenFromRefreshTokenAsync( TokenClient tokenClient, string userInfoEndpoint, string refreshToken, OAuthConfiguration configuration ) + protected static async Task<OAuthTokenCredential> 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;