OAuth2MultiClientIntegrator – Part 3: Obtaining Refresh Token Use case
In this part, we focus on obtaining a refresh token use case. As its name suggests, in this use case, the package tries to obtain a refresh token by adopting the following procedure. After studying the sequence diagram, you can see the source code and explanation of each participant entity in detail. Note that DefaultOAuth2AccessTokenProvider and DefaultOAuth2ClientGateway participants contain functionalities belonging to separate use cases. Therefore, we only explain details belonging to the mentioned use case. For the sake of completeness, we again describe the DefaultOAuth2ClientDataStore class.
1-DefaultOAuth2AccessTokenProvider
The purpose of the DefaultOAuth2AccessTokenProvider class is to provide the client code with a single method for obtaining OAuth2 access tokens.
internal sealed class DefaultOAuth2AccessTokenProvider : IOAuth2AccessTokenProvider { private readonly IOAuth2ClientGateway _oauth2ClientGateway; private readonly IOAuth2ClientDataStore _oAuth2ClientDataStore; private readonly List<OAuth2Client> _oAuth2Clients; private readonly IDateTimeProvider _dateTimeProvider; public DefaultOAuth2AccessTokenProvider( IOAuth2ClientGateway iOAuth2ClientGateway, IOAuth2ClientDataStore oAuth2ClientDataStore, List<OAuth2Client> oAuth2Clients, IDateTimeProvider dateTimeProvider) { _oauth2ClientGateway = iOAuth2ClientGateway; _oAuth2ClientDataStore = oAuth2ClientDataStore; _oAuth2Clients = oAuth2Clients; _dateTimeProvider = dateTimeProvider; } public async Task<string> GetAccessToken(string clientName) { var oAuth2Client = GetOAuth2ClientFromRegisteredClients(clientName); var clientId = oAuth2Client.ClientCredentialOptions.ClientId; var accessTokenResponse = await GetAccessTokenResponseFromDataStore(clientId); if (accessTokenResponse == null) { return await GetOrGenerateAccessToken(oAuth2Client); } return await GetAccessTokenOrGenerateRefreshAccessToken (oAuth2Client, accessTokenResponse); } private async Task<string> GetOrGenerateAccessToken(OAuth2Client oAuth2Client) { var clientId = oAuth2Client.ClientCredentialOptions.ClientId; var authorizationCodeResponse = await GetAuthorizationCodeResponseFromDataStore(clientId); if (authorizationCodeResponse != null) { var authorizationCodeStatus = authorizationCodeResponse .GetStatus(_dateTimeProvider.GetUTCDateTimeNow); if (authorizationCodeStatus == AuthorizationCodeStatus.Valid) { var accessTokenResponse = await GetAccessTokenResponseFromGateway (clientId, authorizationCodeResponse.AuthorizationCode); if (accessTokenResponse == null) { throw await ClearDataStoreAndRaiseException(clientId); } await SetAccessTokenResponseToDataStore (oAuth2Client.ClientCredentialOptions, accessTokenResponse); return accessTokenResponse.AccessToken; } } throw await ClearDataStoreAndRaiseException(clientId); } private async Task<string> GetAccessTokenOrGenerateRefreshAccessToken (OAuth2Client oAuth2Client, AccessTokenResponse accessTokenResponse) { var clientId = oAuth2Client.ClientCredentialOptions.ClientId; var accessTokenStatus = accessTokenResponse .GetStatus(_dateTimeProvider.GetUTCDateTimeNow, oAuth2Client.RefreshTokenOptions.StaticExpirationValue, oAuth2Client.RefreshTokenOptions.CanRenewAfterExpiration); if (accessTokenStatus == AccessTokenStatus.Valid) { return accessTokenResponse.AccessToken; } else if (accessTokenStatus == AccessTokenStatus.Invalid || accessTokenStatus == AccessTokenStatus.NearExpiration) { var newAccessTokenResponse = await GetRefreshAccessTokenResponseFromGatway (clientId, accessTokenResponse.RefreshToken); if (newAccessTokenResponse == null) { throw await ClearDataStoreAndRaiseException(clientId); } await SetAccessTokenResponseToDataStore (oAuth2Client.ClientCredentialOptions, newAccessTokenResponse); return newAccessTokenResponse.AccessToken; } throw await ClearDataStoreAndRaiseException(clientId); } private OAuth2Client GetOAuth2ClientFromRegisteredClients (string clientName) { var oAuth2Client = _oAuth2Clients.FirstOrDefault (t => t.ClientCredentialOptions.ClientName == clientName); if (oAuth2Client == null) { throw new OAuth2MultiClientIntegratorFailureException ("No suitable OAuthClient found."); } return oAuth2Client; } private async Task<InvalidOAuth2AccessTokenException> ClearDataStoreAndRaiseException(string clientId) { await _oAuth2ClientDataStore.ClearDataStore(clientId); return new InvalidOAuth2AccessTokenException(clientId); } private async Task<AccessTokenResponse> GetAccessTokenResponseFromDataStore(string clientId) { return await _oAuth2ClientDataStore .GetAccessTokenResponse(clientId); } private async Task<AuthorizationCodeResponse> GetAuthorizationCodeResponseFromDataStore(string clientId) { return await _oAuth2ClientDataStore. GetAuthorizationCodeResponse(clientId); } private async Task SetAccessTokenResponseToDataStore (ClientCredentialOptions clientCredentialOptions, AccessTokenResponse accessTokenResponse) { await _oAuth2ClientDataStore.SetAccessTokenResponse (clientCredentialOptions, accessTokenResponse); } private async Task<AccessTokenResponse> GetAccessTokenResponseFromGateway (string clientId, string authorizationCode) { return await _oauth2ClientGateway.GetAccessTokenResponse (clientId, authorizationCode); } private async Task<AccessTokenResponse> GetRefreshAccessTokenResponseFromGatway (string clientId, string refreshToken) { return await _oauth2ClientGateway .GetRefreshAccessTokenResponse (clientId, refreshToken); } }
Let's go through each method of the DefaultOAuth2AccessTokenProvider class in detail:
Constructor: The class has a constructor that takes four parameters: iOAuth2ClientGateway , oAuth2ClientDataStore , oAuth2Clients , and dateTimeProvider . These parameters are dependencies injected into the class for various functionalities.
GetAccessToken Method: This method is responsible for providing the OAuth2 access token for a given client name. It first obtains the OAuth2 client based on the provided client name. It then tries to get the access token response from the data store. If it doesn't exist, it calls GetOrGenerateAccessToken to generate a new access token. If the access token response exists, it calls GetAccessTokenOrGenerateRefreshAccessToken to check if the token is valid or needs to be refreshed.
GetAccessTokenOrGenerateRefreshAccessToken Method: This method is responsible for checking the validity of the access token. If the access token is valid, it is returned. If it is invalid or near expiration, a refresh token is used to obtain a new access token. It calls the GetRefreshAccessTokenResponseFromGatway method to obtain a new access token using the refresh token. If the new access token is obtained successfully, it updates the data store and returns the new access token. If there are issues during this process, it follows the above-mentioned procedure to redirect the user to the authorization URI by raising InvalidOAuth2AccessTokenException .
Other Private Helper Methods:
GetRefreshAccessTokenResponseFromGatway : Retrieves a new access token using the refresh token from the OAuth2 gateway.
2-DefaultOAuth2ClientGateway
internal sealed class DefaultOAuth2ClientGateway : IOAuth2ClientGateway { private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpContextAccessor _httpContextAccessor; private readonly List<OAuth2Client> _oAuth2Clients; public DefaultOAuth2ClientGateway(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor, List<OAuth2Client> oAuth2Clients) { _httpClientFactory = httpClientFactory; _httpContextAccessor = httpContextAccessor; _oAuth2Clients = oAuth2Clients; } public async Task<AccessTokenResponse> GetAccessTokenResponse (string clientId, string authorizationCode) { var oauth2Client = GetOAuth2Client(clientId); HttpClient httpClient = _httpClientFactory.CreateClient(); var authValue = new AuthenticationHeaderValue ("Basic", Convert.ToBase64String (Encoding.UTF8.GetBytes( $"{oauth2Client.ClientCredentialOptions.ClientId}:" + $"{oauth2Client.ClientCredentialOptions.ClientSecret}"))); httpClient.DefaultRequestHeaders.Authorization = authValue; var authorizationRedirectUri = _httpContextAccessor .HttpContext.Request.CreateAuthorizationRedirectUri(); var requestBody = new FormUrlEncodedContent( oauth2Client.AccessTokenOptions.GenerateSettings (authorizationCode, authorizationRedirectUri)); AccessTokenResponse accessTokenResponse = null; try { var response = await httpClient.PostAsync( oauth2Client.AccessTokenOptions.BaseAccessTokenUri, requestBody); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { accessTokenResponse = JsonConvert. DeserializeObject<AccessTokenResponse>(responseContent); SetRefreshTokenFieldIfNecessary(accessTokenResponse); SetExpiresInFieldIfNecessary(oauth2Client, accessTokenResponse); } } catch (Exception exception) { throw new OAuth2MultiClientIntegratorFailureException(exception.Message); } return accessTokenResponse; } public async Task<AccessTokenResponse> GetRefreshAccessTokenResponse (string clientId, string refreshToken) { var oauth2Client = GetOAuth2Client(clientId); HttpClient httpClient = _httpClientFactory.CreateClient(); var authValue = new AuthorizationHeaderValue ("Basic", Convert.ToBase64String (Encoding.UTF8.GetBytes( $"{oauth2Client.ClientCredentialOptions.ClientId}:" + $"{oauth2Client.ClientCredentialOptions.ClientSecret}"))); httpClient.DefaultRequestHeaders.Authorization = authValue; var requestBody = new FormUrlEncodedContent( oauth2Client.RefreshTokenOptions.GenerateSettings(refreshToken)); AccessTokenResponse accessTokenResponse = null; try { var response = await httpClient.PostAsync( oauth2Client.RefreshTokenOptions.BaseRefreshTokenUri, requestBody); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { accessTokenResponse = JsonConvert. DeserializeObject<AccessTokenResponse>(responseContent); SetRefreshTokenFieldIfNecessary(accessTokenResponse); SetExpiresInFieldIfNecessary(oauth2Client, accessTokenResponse); } } catch (Exception exception) { throw new OAuth2MultiClientIntegratorFailureException(exception.Message); } return accessTokenResponse; } private OAuth2Client GetOAuth2Client(string clientId) { var oAuth2Client = _oAuth2Clients.FirstOrDefault (t => t.ClientCredentialOptions.ClientId == clientId); if (oAuth2Client == null) { throw new OAuth2MultiClientIntegratorFailureException ("No suitable OAuthClient found."); } return oAuth2Client; } private static void SetExpiresInFieldIfNecessary (OAuth2Client oauth2Client, AccessTokenResponse accessTokenResponse) { if (!oauth2Client.AccessTokenOptions.CanRenewAfterExpiration && accessTokenResponse.ExpiresIn == 0) { accessTokenResponse.ExpiresIn = oauth2Client.AccessTokenOptions.StaticExpirationValue; } } private static void SetRefreshTokenFieldIfNecessary (AccessTokenResponse accessTokenResponse) { if (string.IsNullOrWhiteSpace(accessTokenResponse.RefreshToken)) { accessTokenResponse.RefreshToken = accessTokenResponse.AccessToken; } } }
Let's look at the DefaultOAuth2ClientGateway class in more detail.
Constructor: This constructor initializes the DefaultOAuth2ClientGateway class with essential dependencies, including an IHttpClientFactory for creating HTTP clients, an IHttpContextAccessor for accessing the current HTTP context, and a list of OAuth2Client instances containing OAuth2 client information. This setup ensures that the gateway has the necessary tools to interact with external OAuth2 services.
GetRefreshAccessTokenResponse Method: This method is responsible for refreshing an access token using a refresh token.
3-DefaultDbBasedClientDataStore
internal sealed class DefaultDbBasedClientDataStore : IOAuth2ClientDataStore { private readonly IOAuth2ServerAuthInfoRepository _oAuth2ServerAuthInfoRepository; public DefaultDbBasedClientDataStore( IOAuth2ServerAuthInfoRepository oauth2ServerAuthInfoRepository) { _oAuth2ServerAuthInfoRepository = oauth2ServerAuthInfoRepository; } public async Task<AuthorizationCodeResponse> GetAuthorizationCodeResponse(string clientId) { var oAuth2ServerAuthInfo = await _oAuth2ServerAuthInfoRepository.Get(clientId); if (oAuth2ServerAuthInfo != null) { return oAuth2ServerAuthInfo.AuthorizationCodeResponse; } return default; } public async Task SetAuthorizationCodeResponse( ClientCredentialOptions clientCredentialOptions, AuthorizationCodeResponse authorizationCodeResponse) { var oAuth2ServerAuthInfo = await _oAuth2ServerAuthInfoRepository .Get(clientCredentialOptions.ClientId); if (oAuth2ServerAuthInfo == null) { var newOAuth2ServerAuthInfo = new OAuth2ServerAuthInfo(clientCredentialOptions, authorizationCodeResponse); await _oAuth2ServerAuthInfoRepository .Add(newOAuth2ServerAuthInfo); } else { oAuth2ServerAuthInfo.UpdateAuthorizationCodeResponse (authorizationCodeResponse); await _oAuth2ServerAuthInfoRepository .Update(oAuth2ServerAuthInfo); } await _oAuth2ServerAuthInfoRepository.CommitAsync(); } public async Task<string> GetAuthorizationState(string clientId) { var oAuth2ServerAuthInfo = await _oAuth2ServerAuthInfoRepository.Get(clientId); if (oAuth2ServerAuthInfo != null) { return oAuth2ServerAuthInfo.AuthorizationState; } return default; } public async Task SetAuthorizationState( ClientCredentialOptions clientCredentialOptions , string authorizationState) { var oAuth2ServerAuthInfo = await _oAuth2ServerAuthInfoRepository .Get(clientCredentialOptions.ClientId); if (oAuth2ServerAuthInfo == null) { var newOAuth2ServerAuthInfo = new OAuth2ServerAuthInfo(clientCredentialOptions, authorizationState); await _oAuth2ServerAuthInfoRepository .Add(newOAuth2ServerAuthInfo); } else { oAuth2ServerAuthInfo .UpdateAuthorizationState(authorizationState); await _oAuth2ServerAuthInfoRepository .Update(oAuth2ServerAuthInfo); } await _oAuth2ServerAuthInfoRepository.CommitAsync(); } public async Task<AccessTokenResponse> GetAccessTokenResponse(string clientId) { var oAuth2ServerAuthInfo = await _oAuth2ServerAuthInfoRepository.Get(clientId); if (oAuth2ServerAuthInfo != null) { return oAuth2ServerAuthInfo.AccessTokenResponse; } return default; } public async Task SetAccessTokenResponse( ClientCredentialOptions clientCredentialOptions, AccessTokenResponse accessTokenResponse) { var oAuth2ServerAuthInfo = await _oAuth2ServerAuthInfoRepository .Get(clientCredentialOptions.ClientId); if (oAuth2ServerAuthInfo == null) { var newOAuth2ServerAuthInfo = new OAuth2ServerAuthInfo(clientCredentialOptions, accessTokenResponse); await _oAuth2ServerAuthInfoRepository .Add(newOAuth2ServerAuthInfo); } else { oAuth2ServerAuthInfo .UpdateAccessTokenResponse(accessTokenResponse); await _oAuth2ServerAuthInfoRepository .Update(oAuth2ServerAuthInfo); } await _oAuth2ServerAuthInfoRepository.CommitAsync(); } public async Task ClearDataStore(string clientId) { var oAuth2ServerAuthInfo = await _oAuth2ServerAuthInfoRepository.Get(clientId); if (oAuth2ServerAuthInfo != null) { await _oAuth2ServerAuthInfoRepository .Delete(oAuth2ServerAuthInfo); await _oAuth2ServerAuthInfoRepository.CommitAsync(); } } }
Let's break down the code and describe DefaultDbBasedClientDataStore's functionality:
Constructor: Initializes the class with an instance of IOAuth2ServerAuthInfoRepository , representing the repository for storing and retrieving OAuth2 server authorization information.
GetAuthorizationCodeResponse Method: Retrieves the authorization code response associated with a specific client ID from the repository. Returns the authorization code response if found; otherwise, returns the default value.
SetAuthorizationCodeResponse Method: Updates or adds OAuth2 server authorization information, including the authorization code response, based on the provided client credentials and authorization code response. Ensures data consistency by committing changes to the repository after the update.
GetAuthorizationState Method: Retrieves the authorization state associated with a specific client ID from the repository. Returns the authorization state if found; otherwise, returns the default value.
SetAuthorizationState Method: Updates or adds OAuth2 server authorization information, including the authorization state, based on the provided client credentials and authorization state. Commits changes to the repository after the update.
GetAccessTokenResponse Method: Retrieves the access token response associated with a specific client ID from the repository. Returns the access token response if found; otherwise, returns the default value.
SetAccessTokenResponse Method: Updates or adds OAuth2 server authorization information, including the access token response, based on the provided client credentials and access token response.
ClearDataStore Method: Removes the OAuth2 server authorization information associated with a specific client ID from the repository.
Leave a Reply
Your e-mail address will not be published. Required fields are marked *