2022-06-29 19:46:41 -04:00
using System.Net ;
2017-08-11 10:04:59 -04:00
using System.Net.Http.Headers ;
2022-01-21 09:36:25 -05:00
using System.Net.Http.Json ;
using System.Text.Json ;
2017-08-11 10:04:59 -04:00
using Bit.Core.Utilities ;
2017-08-14 13:06:44 -04:00
using Microsoft.Extensions.Logging ;
2017-08-11 10:04:59 -04:00
namespace Bit.Core.Services ;
2022-08-29 16:06:55 -04:00
2017-08-11 10:04:59 -04:00
public abstract class BaseIdentityClientService : IDisposable
{
2022-01-21 09:36:25 -05:00
private readonly IHttpClientFactory _httpFactory ;
private readonly string _identityScope ;
2018-08-15 18:43:26 -04:00
private readonly string _identityClientId ;
private readonly string _identityClientSecret ;
2022-01-21 09:36:25 -05:00
protected readonly ILogger < BaseIdentityClientService > _logger ;
2022-08-29 16:06:55 -04:00
2022-01-21 09:36:25 -05:00
private JsonDocument _decodedToken ;
private DateTime ? _nextAuthAttempt = null ;
2022-08-29 16:06:55 -04:00
2022-01-21 09:36:25 -05:00
public BaseIdentityClientService (
2018-08-15 18:43:26 -04:00
IHttpClientFactory httpFactory ,
string baseClientServerUri ,
string baseIdentityServerUri ,
string identityScope ,
string identityClientId ,
string identityClientSecret ,
ILogger < BaseIdentityClientService > logger )
2017-08-11 10:04:59 -04:00
{
2022-05-10 17:12:09 -04:00
_httpFactory = httpFactory ;
2018-08-15 18:43:26 -04:00
_identityScope = identityScope ;
_identityClientId = identityClientId ;
_identityClientSecret = identityClientSecret ;
2022-05-10 17:12:09 -04:00
_logger = logger ;
2022-08-29 16:06:55 -04:00
2022-05-10 17:12:09 -04:00
Client = _httpFactory . CreateClient ( "client" ) ;
Client . BaseAddress = new Uri ( baseClientServerUri ) ;
Client . DefaultRequestHeaders . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "application/json" ) ) ;
2022-08-29 16:06:55 -04:00
2022-05-10 17:12:09 -04:00
IdentityClient = _httpFactory . CreateClient ( "identity" ) ;
IdentityClient . BaseAddress = new Uri ( baseIdentityServerUri ) ;
2017-08-11 12:22:59 -04:00
IdentityClient . DefaultRequestHeaders . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "application/json" ) ) ;
2022-08-29 16:06:55 -04:00
}
2018-08-15 18:43:26 -04:00
2022-05-10 17:12:09 -04:00
protected HttpClient Client { get ; private set ; }
2022-01-21 09:36:25 -05:00
protected HttpClient IdentityClient { get ; private set ; }
protected string AccessToken { get ; private set ; }
2017-08-11 10:04:59 -04:00
2018-08-15 18:43:26 -04:00
protected Task SendAsync ( HttpMethod method , string path ) = >
2022-11-01 09:58:28 -04:00
SendAsync < object > ( method , path , null ) ;
2017-08-11 10:04:59 -04:00
2022-11-01 09:58:28 -04:00
protected Task SendAsync < TRequest > ( HttpMethod method , string path , TRequest requestModel ) = >
SendAsync < TRequest , object > ( method , path , requestModel , false ) ;
2017-08-11 10:04:59 -04:00
2022-11-01 09:58:28 -04:00
protected async Task < TResult > SendAsync < TRequest , TResult > ( HttpMethod method , string path ,
TRequest requestModel , bool hasJsonResult )
2022-08-29 16:06:55 -04:00
{
2022-10-19 10:22:40 -04:00
var fullRequestPath = string . Concat ( Client . BaseAddress , path ) ;
2017-08-11 12:22:59 -04:00
var tokenStateResponse = await HandleTokenStateAsync ( ) ;
if ( ! tokenStateResponse )
2022-08-29 16:06:55 -04:00
{
2022-11-01 09:58:28 -04:00
_logger . LogError ( "Unable to send {method} request to {requestUri} because an access token was unable to be obtained" ,
method . Method , fullRequestPath ) ;
2017-08-11 12:22:59 -04:00
return default ;
2017-08-11 10:04:59 -04:00
}
2022-05-10 17:12:09 -04:00
var message = new TokenHttpRequestMessage ( requestModel , AccessToken )
{
Method = method ,
2022-10-19 10:22:40 -04:00
RequestUri = new Uri ( fullRequestPath )
2022-08-29 16:06:55 -04:00
} ;
2022-05-10 17:12:09 -04:00
try
2022-08-29 16:06:55 -04:00
{
2022-05-10 17:12:09 -04:00
var response = await Client . SendAsync ( message ) ;
2022-11-01 09:58:28 -04:00
if ( response . IsSuccessStatusCode )
{
if ( hasJsonResult )
{
return await response . Content . ReadFromJsonAsync < TResult > ( ) ;
}
}
else
{
_logger . LogError ( "Request to {url} is unsuccessful with status of {code}-{reason}" ,
message . RequestUri . ToString ( ) , response . StatusCode , response . ReasonPhrase ) ;
}
return default ;
2022-08-29 16:06:55 -04:00
}
2022-05-10 17:12:09 -04:00
catch ( Exception e )
2022-08-29 16:06:55 -04:00
{
2022-05-10 17:12:09 -04:00
_logger . LogError ( 12334 , e , "Failed to send to {0}." , message . RequestUri . ToString ( ) ) ;
2017-08-11 10:04:59 -04:00
return default ;
2022-08-29 16:06:55 -04:00
}
}
2022-05-10 17:12:09 -04:00
protected async Task < bool > HandleTokenStateAsync ( )
2022-08-29 16:06:55 -04:00
{
2022-11-01 09:58:28 -04:00
if ( _nextAuthAttempt . HasValue & & DateTime . UtcNow < _nextAuthAttempt . Value )
2022-08-29 16:06:55 -04:00
{
2022-11-01 09:58:28 -04:00
_logger . LogInformation ( "Not requesting a token at {now} because the next request time is {nextAttempt}" , DateTime . UtcNow , _nextAuthAttempt . Value ) ;
2022-05-10 17:12:09 -04:00
return false ;
2022-08-29 16:06:55 -04:00
}
2022-05-10 17:12:09 -04:00
_nextAuthAttempt = null ;
2018-08-16 12:05:01 -04:00
if ( ! string . IsNullOrWhiteSpace ( AccessToken ) & & ! TokenNeedsRefresh ( ) )
{
return true ;
}
2017-08-11 10:04:59 -04:00
var requestMessage = new HttpRequestMessage
2022-08-29 15:53:48 -04:00
{
2022-05-10 17:12:09 -04:00
Method = HttpMethod . Post ,
RequestUri = new Uri ( string . Concat ( IdentityClient . BaseAddress , "connect/token" ) ) ,
2020-03-27 14:36:37 -04:00
Content = new FormUrlEncodedContent ( new Dictionary < string , string >
2022-08-29 16:06:55 -04:00
{
2017-08-11 10:04:59 -04:00
{ "grant_type" , "client_credentials" } ,
2018-08-15 18:43:26 -04:00
{ "scope" , _identityScope } ,
{ "client_id" , _identityClientId } ,
{ "client_secret" , _identityClientSecret }
2022-05-10 17:12:09 -04:00
} )
2022-08-29 15:53:48 -04:00
} ;
2022-08-29 16:06:55 -04:00
2017-08-14 13:06:44 -04:00
HttpResponseMessage response = null ;
2022-08-29 15:53:48 -04:00
try
{
2017-08-11 10:04:59 -04:00
response = await IdentityClient . SendAsync ( requestMessage ) ;
2022-08-29 15:53:48 -04:00
}
2017-08-11 10:04:59 -04:00
catch ( Exception e )
{
_logger . LogError ( 12339 , e , "Unable to authenticate with identity server." ) ;
}
2020-03-27 14:36:37 -04:00
if ( response = = null )
2022-08-29 14:53:16 -04:00
{
2022-11-14 11:41:17 -05:00
_logger . LogError ( "Empty token response from {identity} for client {clientId}" , IdentityClient . BaseAddress , _identityClientId ) ;
2020-03-27 14:36:37 -04:00
return false ;
2022-08-29 15:53:48 -04:00
}
2017-08-11 10:04:59 -04:00
2018-08-21 14:32:09 -04:00
if ( ! response . IsSuccessStatusCode )
2022-08-29 16:06:55 -04:00
{
2022-11-01 09:58:28 -04:00
_logger . LogError ( "Unsuccessful token response from {identity} for client {clientId} with status {code}-{reason}" , IdentityClient . BaseAddress , _identityClientId , response . StatusCode , response . ReasonPhrase ) ;
2017-08-14 13:06:44 -04:00
2020-03-27 14:36:37 -04:00
if ( response . StatusCode = = HttpStatusCode . BadRequest )
2017-08-15 14:48:56 -04:00
{
2022-05-16 09:57:00 -04:00
_nextAuthAttempt = DateTime . UtcNow . AddDays ( 1 ) ;
2017-08-15 14:48:56 -04:00
}
2017-08-14 13:06:44 -04:00
if ( _logger . IsEnabled ( LogLevel . Debug ) )
2022-08-29 15:53:48 -04:00
{
2022-05-16 09:57:00 -04:00
var responseBody = await response . Content . ReadAsStringAsync ( ) ;
2017-08-11 10:04:59 -04:00
_logger . LogDebug ( "Error response body:\n{ResponseBody}" , responseBody ) ;
2022-05-16 09:57:00 -04:00
}
2017-08-11 10:04:59 -04:00
2022-01-21 09:36:25 -05:00
return false ;
2022-08-29 15:53:48 -04:00
}
2022-11-01 09:58:28 -04:00
var content = await response . Content . ReadAsStreamAsync ( ) ;
using var jsonDocument = await JsonDocument . ParseAsync ( content ) ;
2022-08-29 15:53:48 -04:00
2017-08-11 10:04:59 -04:00
AccessToken = jsonDocument . RootElement . GetProperty ( "access_token" ) . GetString ( ) ;
return true ;
}
2019-05-30 23:06:02 -04:00
protected class TokenHttpRequestMessage : HttpRequestMessage
2022-08-29 16:06:55 -04:00
{
2022-01-21 09:36:25 -05:00
public TokenHttpRequestMessage ( string token )
2017-08-11 10:04:59 -04:00
{
Headers . Add ( "Authorization" , $"Bearer {token}" ) ;
}
public TokenHttpRequestMessage ( object requestObject , string token )
2022-01-21 09:36:25 -05:00
: this ( token )
2022-08-29 14:53:16 -04:00
{
2022-01-21 09:36:25 -05:00
if ( requestObject ! = null )
2022-08-29 15:53:48 -04:00
{
2017-08-11 10:04:59 -04:00
Content = JsonContent . Create ( requestObject ) ;
}
}
2022-08-29 16:06:55 -04:00
}
2017-08-11 10:04:59 -04:00
2022-01-21 09:36:25 -05:00
protected bool TokenNeedsRefresh ( int minutes = 5 )
2022-08-29 16:06:55 -04:00
{
2022-01-21 09:36:25 -05:00
var decoded = DecodeToken ( ) ;
if ( ! decoded . RootElement . TryGetProperty ( "exp" , out var expProp ) )
2022-08-29 14:53:16 -04:00
{
2017-08-11 10:04:59 -04:00
throw new InvalidOperationException ( "No exp in token." ) ;
2022-08-29 15:53:48 -04:00
}
2022-05-10 17:12:09 -04:00
var expiration = CoreHelpers . FromEpocSeconds ( expProp . GetInt64 ( ) ) ;
return DateTime . UtcNow . AddMinutes ( - 1 * minutes ) > expiration ;
2022-08-29 15:53:48 -04:00
}
2022-01-21 09:36:25 -05:00
protected JsonDocument DecodeToken ( )
2022-08-29 16:06:55 -04:00
{
2022-01-21 09:36:25 -05:00
if ( _decodedToken ! = null )
2022-08-29 16:06:55 -04:00
{
2022-01-21 09:36:25 -05:00
return _decodedToken ;
2022-08-29 15:53:48 -04:00
}
2022-01-21 09:36:25 -05:00
if ( AccessToken = = null )
2022-08-29 15:53:48 -04:00
{
2022-05-10 17:12:09 -04:00
throw new InvalidOperationException ( $"{nameof(AccessToken)} not found." ) ;
2022-08-29 15:53:48 -04:00
}
2022-01-21 09:36:25 -05:00
var parts = AccessToken . Split ( '.' ) ;
2020-03-27 14:36:37 -04:00
if ( parts . Length ! = 3 )
2022-08-29 16:06:55 -04:00
{
2022-01-21 09:36:25 -05:00
throw new InvalidOperationException ( $"{nameof(AccessToken)} must have 3 parts" ) ;
2017-08-11 10:04:59 -04:00
}
2022-01-21 09:36:25 -05:00
var decodedBytes = CoreHelpers . Base64UrlDecode ( parts [ 1 ] ) ;
if ( decodedBytes = = null | | decodedBytes . Length < 1 )
{
2022-05-10 17:12:09 -04:00
throw new InvalidOperationException ( $"{nameof(AccessToken)} must have 3 parts" ) ;
2022-01-21 09:36:25 -05:00
}
2022-08-29 16:06:55 -04:00
2022-01-21 09:36:25 -05:00
_decodedToken = JsonDocument . Parse ( decodedBytes ) ;
2017-08-11 10:04:59 -04:00
return _decodedToken ;
2022-08-29 16:06:55 -04:00
}
2022-01-21 09:36:25 -05:00
public void Dispose ( )
2022-08-29 16:06:55 -04:00
{
2022-05-10 17:12:09 -04:00
_decodedToken ? . Dispose ( ) ;
2017-08-11 10:04:59 -04:00
}
}