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);
+ }
}
}