Index: Src/GoogleApis.Auth/OAuth2/Credential.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/Credential.cs +++ b/Src/GoogleApis.Auth/OAuth2/Credential.cs @@ -15,13 +15,125 @@ */ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Http; +using Google.Apis.Logging; namespace Google.Apis.Auth.OAuth2 { - public class Credential + /// + /// OAuth 2.0 credential for accessing protected resources using an access token, as well as optionally refreshing + /// the access token when it expires using a refresh token. + /// + public class Credential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler, + IConfigurableHttpClientInitializer { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + private TokenResponse token; + private object lockObject = new object(); + + public TokenResponse Token + { + get + { + lock (lockObject) + { + return token; + } + } + private set + { + lock (lockObject) + { + token = value; + } + } + } + + private readonly IAuthorizationCodeFlow flow; + private readonly string userId; + + /// Constructs a new credential instance. + /// Authorization code flow + /// User identifier + /// An initial token for the user + public Credential(IAuthorizationCodeFlow flow, string userId, TokenResponse token) + { + this.flow = flow; + this.userId = userId; + this.token = token; + } + + /// + /// Default implementation is to try to refresh the access token if there is no access token or if we are 1 + /// minute away from expiration. If token server is unavailable, it will try to use the access token even if + /// has expired. If successful, it will call . + /// + public async Task InterceptAsync(HttpRequestMessage request, CancellationToken taskCancellationToken) + { + if (Token.IsExpired(flow.Clock)) + { + if (!await RefreshTokenAsync(taskCancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException("The access token is expired but we can't refresh it"); + } + } + + flow.AccessMethod.Intercept(request, Token.AccessToken); + } + + /// + /// Refreshes the token by calling to . Then it + /// updates the with the new token instance. + /// + /// Cancellation token to cancel an operation + /// true if the token was refreshed + private async Task RefreshTokenAsync(CancellationToken taskCancellationToken) + { + if (Token.RefreshToken == null) + { + Logger.Warning("Refresh token is null, can't refresh the token!"); + return false; + } + + // It's possible that two concurrent calls will be made to refresh the token, in that case the last one + // will win. + var newToken = await flow.RefreshTokenAsync(userId, Token.RefreshToken, taskCancellationToken) + .ConfigureAwait(false); + + Logger.Info("Access token was refreshed"); + + if (newToken.RefreshToken == null) + { + newToken.RefreshToken = Token.RefreshToken; + } + + Token = newToken; + return true; + } + + public async Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args) + { + // TODO(peleyal): check WWW-Authenticate header + if (args.Response.StatusCode == HttpStatusCode.Unauthorized) + { + return !Object.Equals(Token.AccessToken, flow.AccessMethod.GetAccessToken(args.Request)) + || await RefreshTokenAsync(args.CancellationToken).ConfigureAwait(false); + } + + return false; + } + + public void Initialize(ConfigurableHttpClient httpClient) + { + httpClient.MessageHandler.ExecuteInterceptors.Add(this); + httpClient.MessageHandler.UnsuccessfulResponseHandlers.Add(this); + } } }