Index: Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs
===================================================================
new file mode 100644
--- /dev/null
+++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs
@@ -0,0 +1,338 @@
+/*
+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.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Google.Apis.Auth.OAuth2.Requests;
+using Google.Apis.Auth.OAuth2.Responses;
+using Google.Apis.Http;
+using Google.Apis.Json;
+using Google.Apis.Logging;
+using Google.Apis.Util;
+
+namespace Google.Apis.Auth.OAuth2
+{
+ ///
+ /// Google OAuth 2.0 credential for accessing protected resources using an access token. The Google OAuth 2.0
+ /// Authorization Server supports server-to-server interactions such as those between a web application and Google
+ /// Cloud Storage. The requesting application has to prove its own identity to gain access to an API, and an
+ /// end-user doesn't have to be involved.
+ ///
+ /// Take a look in https://developers.google.com/accounts/docs/OAuth2ServiceAccount for more details.
+ ///
+ ///
+ public class ServiceAccountCredential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler,
+ IConfigurableHttpClientInitializer
+ {
+ private static readonly ILogger Logger = ApplicationContext.Logger.ForType();
+
+ /// An initializer class for the service account credential.
+ public class Initializer
+ {
+ /// Gets the service account ID (typically an e-mail address).
+ public string Id { get; private set; }
+
+ /// Gets the token server URL.
+ public string TokenServerUrl { get; private set; }
+
+ ///
+ /// Gets or sets the email address of the user the application is trying to impersonate in the service
+ /// account flow or null.
+ ///
+ public string User { get; set; }
+
+ /// Gets the scopes which indicate API access your application is requesting.
+ public IEnumerable Scopes { get; set; }
+
+ ///
+ /// Gets or sets the clock. The clock is used to determine if the token has expired, if so we will try to
+ /// refresh it. The default value is .
+ ///
+ public IClock Clock { get; set; }
+
+ ///
+ /// Gets or sets the key which is used to sign the request, as specified in
+ /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature.
+ ///
+ public RSACryptoServiceProvider Key { get; set; }
+
+ ///
+ /// Gets or sets the method for presenting the access token to the resource server.
+ /// The default value is .
+ ///
+ public IAccessMethod AccessMethod { get; set; }
+
+ ///
+ /// Gets or sets the factory for creating a instance.
+ ///
+ public IHttpClientFactory HttpClientFactory { get; set; }
+
+ ///
+ /// Get or sets the exponential back-off policy. Default value is UnsuccessfulResponse503, which
+ /// means that exponential back-off is used on 503 abnormal HTTP responses.
+ /// If the value is set to None, no exponential back-off policy is used, and it's up to the user to
+ /// configure the in an
+ /// to set a specific back-off
+ /// implementation (using ).
+ ///
+ public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; }
+
+ /// Constructs a new initializer using the given id.
+ public Initializer(string id)
+ : this(id, GoogleAuthConsts.TokenUrl) { }
+
+ /// Constructs a new initializer using the given id and the token server URL.
+ public Initializer(string id, string tokenServerUrl)
+ {
+ Id = id;
+ TokenServerUrl = tokenServerUrl;
+
+ AccessMethod = new BearerToken.AuthorizationHeaderAccessMethod();
+ Clock = SystemClock.Default;
+ DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503;
+ Scopes = new List();
+ }
+
+ /// Extracts a from the given certificate.
+ public Initializer FromCertificate(X509Certificate2 certificate)
+ {
+ // Workaround to correctly cast the private key as a RSACryptoServiceProvider type 24.
+ RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)certificate.PrivateKey;
+ byte[] privateKeyBlob = rsa.ExportCspBlob(true);
+ Key = new RSACryptoServiceProvider();
+ Key.ImportCspBlob(privateKeyBlob);
+ return this;
+ }
+ }
+
+ #region Readonly fields
+
+ private readonly string id;
+ private readonly string tokenServerUrl;
+ private readonly string user;
+ private readonly IEnumerable scopes;
+ private readonly IClock clock;
+ private readonly IAccessMethod accessMethod;
+ private readonly ConfigurableHttpClient httpClient;
+ private readonly RSACryptoServiceProvider key;
+
+ #endregion
+
+ /// Gets the service account ID (typically an e-mail address).
+ public string Id { get { return id; } }
+
+ /// Gets the token server URL.
+ public string TokenServerUrl { get { return tokenServerUrl; } }
+
+ ///
+ /// Gets the email address of the user the application is trying to impersonate in the service account flow
+ /// or null.
+ ///
+ public string User { get { return user; } }
+
+ /// Gets the service account scopes.
+ public IEnumerable Scopes { get { return scopes; } }
+
+ ///
+ /// Gets the clock. The clock is used to determine if the token has expired, if so we will try to refresh it.
+ ///
+ public IClock Clock { get { return clock; } }
+
+ /// Gets the method for presenting the access token to the resource server.
+ public IAccessMethod AccessMethod { get { return accessMethod; } }
+
+ /// Gets the HTTP client used to make authentication requests to the server.
+ public ConfigurableHttpClient HttpClient { get { return httpClient; } }
+
+ ///
+ /// Gets the key which is used to sign the request, as specified in
+ /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature.
+ ///
+ public RSACryptoServiceProvider Key { get { return key; } }
+
+ private TokenResponse token;
+ private object lockObject = new object();
+
+ /// Gets the token response which contains the access token.
+ public TokenResponse Token
+ {
+ get
+ {
+ lock (lockObject)
+ {
+ return token;
+ }
+ }
+ private set
+ {
+ lock (lockObject)
+ {
+ token = value;
+ }
+ }
+ }
+
+ /// Constructs a new service account credential using the given initializer.
+ ///
+ public ServiceAccountCredential(Initializer initializer)
+ {
+ id = initializer.Id.ThrowIfNullOrEmpty("initializer.Id");
+ user = initializer.User;
+ scopes = initializer.Scopes;
+ tokenServerUrl = initializer.TokenServerUrl;
+ accessMethod = initializer.AccessMethod.ThrowIfNull("initializer.AccessMethod");
+ clock = initializer.Clock.ThrowIfNull("initializer.Clock");
+
+ // Set the HTTP client.
+ var httpArgs = new CreateHttpClientArgs();
+
+ // Add exponential back-off initializer if necessary.
+ if (initializer.DefaultExponentialBackOffPolicy != ExponentialBackOffPolicy.None)
+ {
+ httpArgs.Initializers.Add(
+ new ExponentialBackOffInitializer(initializer.DefaultExponentialBackOffPolicy,
+ () => new BackOffHandler(new ExponentialBackOff())));
+ }
+ httpClient = (initializer.HttpClientFactory ?? new HttpClientFactory()).CreateHttpClient(httpArgs);
+ key = initializer.Key.ThrowIfNull("initializer.Key");
+ }
+
+ public void Initialize(ConfigurableHttpClient httpClient)
+ {
+ httpClient.MessageHandler.ExecuteInterceptors.Add(this);
+ httpClient.MessageHandler.UnsuccessfulResponseHandlers.Add(this);
+ }
+
+ public async Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (Token == null || Token.IsExpired(Clock))
+ {
+ Logger.Debug("Token has expired, trying to get a new one.");
+ if (!await RequestAccessToken(cancellationToken))
+ {
+ throw new InvalidOperationException("The access token has expired but we can't refresh it");
+ }
+ Logger.Info("New access token was received successfully");
+ }
+
+ AccessMethod.Intercept(request, Token.AccessToken);
+ }
+
+ public async Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args)
+ {
+ // TODO(peleyal): check WWW-Authenticate header
+ if (args.Response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ return !Object.Equals(Token.AccessToken, AccessMethod.GetAccessToken(args.Request))
+ || await RequestAccessToken(args.CancellationToken).ConfigureAwait(false);
+ }
+
+ return false;
+ }
+
+ ///
+ /// Requests a new token as specified in
+ /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#makingrequest.
+ ///
+ /// Cancellation token to cancel operation
+ /// true if a new token was received successfully
+ private async Task RequestAccessToken(CancellationToken taskCancellationToken)
+ {
+ string serializedHeader = CreateSerializedHeader();
+ string serializedPayload = GetSerializedPayload();
+
+ StringBuilder assertion = new StringBuilder();
+ assertion.Append(UrlSafeBase64Encode(serializedHeader))
+ .Append(".")
+ .Append(UrlSafeBase64Encode(serializedPayload));
+
+ // Sign the header and the payload.
+ var signature = UrlSafeBase64Encode(key.SignData(Encoding.ASCII.GetBytes(assertion.ToString()), "SHA256"));
+ assertion.Append(".").Append(signature);
+
+ // Create the request.
+ var request = new GoogleAssertionTokenRequest()
+ {
+ Assertion = assertion.ToString()
+ };
+
+ Logger.Debug("Request a new access token. Assertion data is: " + request.Assertion);
+
+ var newToken = await request.ExecuteAsync(httpClient, tokenServerUrl, taskCancellationToken, Clock);
+ Token = newToken;
+ return true;
+ }
+
+ ///
+ /// Creates a serialized header as specified in
+ /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader.
+ ///
+ private static string CreateSerializedHeader()
+ {
+ var header = new GoogleJsonWebSignature.Header()
+ {
+ Algorithm = "RS256",
+ Type = "JWT"
+ };
+
+ return NewtonsoftJsonSerializer.Instance.Serialize(header);
+ }
+
+ ///
+ /// Creates a serialized claim set as specified in
+ /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset
+ ///
+ private string GetSerializedPayload()
+ {
+ var issued = (int)(Clock.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
+ var payload = new GoogleJsonWebSignature.Payload()
+ {
+ Issuer = Id,
+ Audience = TokenServerUrl,
+ IssuedAtTimeSeconds = issued,
+ ExpirationTimeSeconds = issued + 3600,
+ Subject = User,
+ Scope = String.Join(" ", Scopes)
+ };
+
+ return NewtonsoftJsonSerializer.Instance.Serialize(payload);
+ }
+
+ /// Encodes the provided UTF8 string into an URL safe base64 string.
+ /// Value to encode
+ /// The URL safe base64 string
+ private string UrlSafeBase64Encode(string value)
+ {
+ return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(value));
+ }
+
+ /// Encodes the byte array into an URL safe base64 string.
+ /// Byte array to encode
+ /// The URL safe base64 string
+ private string UrlSafeBase64Encode(byte[] bytes)
+ {
+ return Convert.ToBase64String(bytes).Replace("=", String.Empty).Replace('+', '-').Replace('/', '_');
+ }
+ }
+}