Index: Src/GoogleApis.Auth/OAuth2/UserCredential.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs @@ -0,0 +1,140 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +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 +{ + /// + /// 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 UserCredential : 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 UserCredential(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)) + { + Logger.Debug("Token has expired, trying to refresh it."); + if (!await RefreshTokenAsync(taskCancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException("The access token has 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 successfully"); + + 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); + } + } +}