/*
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.Linq;
using System.IO;
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.Json;
using Google.Apis.Util;
#if NETSTANDARD1_3
using RsaKey = System.Security.Cryptography.RSA;
#elif NET45
using RsaKey = System.Security.Cryptography.RSACryptoServiceProvider;
#elif DNX451
using RsaKey = System.Security.Cryptography.RSACryptoServiceProvider;
#else
#error Unsupported target
#endif
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.
///
///
/// Since version 1.9.3, service account credential also supports JSON Web Token access token scenario.
/// In this scenario, instead of sending a signed JWT claim to a token server and exchanging it for
/// an access token, a locally signed JWT claim bound to an appropriate URI is used as an access token
/// directly.
/// See for explanation when JWT access token
/// is used and when regular OAuth2 token is used.
///
///
public class ServiceAccountCredential : ServiceCredential
{
private const string Sha256Oid = "2.16.840.1.101.3.4.2.1";
/// An initializer class for the service account credential.
new public class Initializer : ServiceCredential.Initializer
{
/// Gets the service account ID (typically an e-mail address).
public string Id { 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 key which is used to sign the request, as specified in
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature.
///
public RsaKey Key { get; set; }
/// Constructs a new initializer using the given id.
public Initializer(string id)
: this(id, GoogleAuthConsts.OidcTokenUrl) { }
/// Constructs a new initializer using the given id and the token server URL.
public Initializer(string id, string tokenServerUrl) : base(tokenServerUrl)
{
Id = id;
Scopes = new List();
}
/// Extracts the from the given PKCS8 private key.
public Initializer FromPrivateKey(string privateKey)
{
RSAParameters rsaParameters = Pkcs8.DecodeRsaParameters(privateKey);
Key = (RsaKey)RSA.Create();
Key.ImportParameters(rsaParameters);
return this;
}
/// Extracts a from the given certificate.
public Initializer FromCertificate(X509Certificate2 certificate)
{
#if NETSTANDARD1_3
Key = certificate.GetRSAPrivateKey();
#elif NET45
// 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);
#elif DNX451
// 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);
#else
#error Unsupported target
#endif
return this;
}
}
/// Unix epoch as a DateTime
protected static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private readonly string id;
private readonly string user;
private readonly IEnumerable scopes;
private readonly RsaKey key;
/// Gets the service account ID (typically an e-mail address).
public string Id { get { return id; } }
///
/// 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 key which is used to sign the request, as specified in
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature.
///
public RsaKey Key { get { return key; } }
/// true if this credential has any scopes associated with it.
internal bool HasScopes { get { return scopes != null && scopes.Any(); } }
/// Constructs a new service account credential using the given initializer.
public ServiceAccountCredential(Initializer initializer) : base(initializer)
{
id = initializer.Id.ThrowIfNullOrEmpty("initializer.Id");
user = initializer.User;
scopes = initializer.Scopes;
key = initializer.Key.ThrowIfNull("initializer.Key");
}
///
/// Creates a new instance from JSON credential data.
///
/// The stream from which to read the JSON key data for a service account. Must not be null.
///
/// The does not contain valid JSON service account key data.
///
/// The credentials parsed from the service account key data.
public static ServiceAccountCredential FromServiceAccountData(Stream credentialData)
{
var credential = GoogleCredential.FromStream(credentialData);
var result = credential.UnderlyingCredential as ServiceAccountCredential;
if (result == null)
{
throw new InvalidOperationException("JSON data does not represent a valid service account credential.");
}
return result;
}
///
/// 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.
public override async Task RequestAccessTokenAsync(CancellationToken taskCancellationToken)
{
// Create the request.
var request = new GoogleAssertionTokenRequest()
{
Assertion = CreateAssertionFromPayload(CreatePayload())
};
Logger.Debug("Request a new access token. Assertion data is: " + request.Assertion);
var newToken = await request.ExecuteAsync(HttpClient, TokenServerUrl, taskCancellationToken, Clock)
.ConfigureAwait(false);
Token = newToken;
return true;
}
///
/// Gets an access token to authorize a request.
/// If is set and this credential has no scopes associated
/// with it, a locally signed JWT access token for given
/// is returned. Otherwise, an OAuth2 access token obtained from token server will be returned.
/// A cached token is used if possible and the token is only refreshed once it's close to its expiry.
///
/// The URI the returned token will grant access to.
/// The cancellation token.
/// The access token.
public override async Task GetAccessTokenForRequestAsync(string authUri = null,
CancellationToken cancellationToken = default(CancellationToken))
{
if (!HasScopes && authUri != null)
{
// TODO(jtattermusch): support caching of JWT access tokens per authUri, currently a new
// JWT access token is created each time, which can hurt performance.
return CreateJwtAccessToken(authUri);
}
return await base.GetAccessTokenForRequestAsync(authUri, cancellationToken).ConfigureAwait(false);
}
///
/// Creates a JWT access token than can be used in request headers instead of an OAuth2 token.
/// This is achieved by signing a special JWT using this service account's private key.
/// The URI for which the access token will be valid.
///
private string CreateJwtAccessToken(string authUri)
{
var issuedDateTime = Clock.UtcNow;
var issued = (int)(issuedDateTime - UnixEpoch).TotalSeconds;
var payload = new JsonWebSignature.Payload()
{
Issuer = Id,
Subject = Id,
Audience = authUri,
IssuedAtTimeSeconds = issued,
ExpirationTimeSeconds = issued + 3600,
};
return CreateAssertionFromPayload(payload);
}
///
/// Signs JWT token using the private key and returns the serialized assertion.
///
/// the JWT payload to sign.
private string CreateAssertionFromPayload(JsonWebSignature.Payload payload)
{
string serializedHeader = CreateSerializedHeader();
string serializedPayload = NewtonsoftJsonSerializer.Instance.Serialize(payload);
var assertion = new StringBuilder();
assertion.Append(UrlSafeBase64Encode(serializedHeader))
.Append('.')
.Append(UrlSafeBase64Encode(serializedPayload));
var signature = CreateSignature(Encoding.ASCII.GetBytes(assertion.ToString()));
assertion.Append('.') .Append(UrlSafeEncode(signature));
return assertion.ToString();
}
///
/// Creates a base64 encoded signature for the SHA-256 hash of the specified data.
///
/// The data to hash and sign. Must not be null.
/// The base-64 encoded signature.
public string CreateSignature(byte[] data)
{
data.ThrowIfNull(nameof(data));
using (var hashAlg = SHA256.Create())
{
byte[] assertionHash = hashAlg.ComputeHash(data);
#if NETSTANDARD1_3
var sigBytes = key.SignHash(assertionHash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
#elif NET45
var sigBytes = key.SignHash(assertionHash, Sha256Oid);
#elif DNX451
var sigBytes = key.SignHash(assertionHash, Sha256Oid);
#else
#error Unsupported target
#endif
return Convert.ToBase64String(sigBytes);
}
}
///
/// 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 claim set as specified in
/// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset.
///
private GoogleJsonWebSignature.Payload CreatePayload()
{
var issued = (int)(Clock.UtcNow - UnixEpoch).TotalSeconds;
return new GoogleJsonWebSignature.Payload()
{
Issuer = Id,
Audience = TokenServerUrl,
IssuedAtTimeSeconds = issued,
ExpirationTimeSeconds = issued + 3600,
Subject = User,
Scope = String.Join(" ", Scopes)
};
}
/// 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 UrlSafeEncode(Convert.ToBase64String(bytes));
}
/// Encodes the base64 string into an URL safe string.
/// The base64 string to make URL safe.
/// The URL safe base64 string.
private string UrlSafeEncode(string base64Value)
{
return base64Value.Replace("=", String.Empty).Replace('+', '-').Replace('/', '_');
}
}
}