From 154d1ce446562a03b77cf346e1832c9365a03208 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 11:25:22 -0700 Subject: [PATCH 01/16] move tests to the correct location --- .../GoogleApis.Auth.DotNet4.Tests.csproj | 4 ++-- .../DefaultCredentialProviderTests.cs | 2 +- .../{DefaultCredentialProvider => }/GoogleCredentialTests.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/{DefaultCredentialProvider => }/DefaultCredentialProviderTests.cs (99%) rename Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/{DefaultCredentialProvider => }/GoogleCredentialTests.cs (98%) diff --git a/Src/GoogleApis.Auth.DotNet4.Tests/GoogleApis.Auth.DotNet4.Tests.csproj b/Src/GoogleApis.Auth.DotNet4.Tests/GoogleApis.Auth.DotNet4.Tests.csproj index 86017eebb2..87175f1c7f 100644 --- a/Src/GoogleApis.Auth.DotNet4.Tests/GoogleApis.Auth.DotNet4.Tests.csproj +++ b/Src/GoogleApis.Auth.DotNet4.Tests/GoogleApis.Auth.DotNet4.Tests.csproj @@ -96,8 +96,8 @@ - - + + diff --git a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProvider/DefaultCredentialProviderTests.cs b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs similarity index 99% rename from Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProvider/DefaultCredentialProviderTests.cs rename to Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs index bef7c056cd..1b41741b39 100644 --- a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProvider/DefaultCredentialProviderTests.cs +++ b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs @@ -34,7 +34,7 @@ limitations under the License. using Google.Apis.Util; using Google.Apis.Util.Store; -namespace Google.Apis.Auth.OAuth2.DefaultCredentials +namespace Google.Apis.Auth.OAuth2 { /// A mock for the . class MockDefaultCredentialProvider : DefaultCredentialProvider diff --git a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProvider/GoogleCredentialTests.cs b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs similarity index 98% rename from Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProvider/GoogleCredentialTests.cs rename to Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs index c1476136c2..2b487a7bd1 100644 --- a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProvider/GoogleCredentialTests.cs +++ b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs @@ -21,7 +21,7 @@ limitations under the License. using NUnit.Framework; -namespace Google.Apis.Auth.OAuth2.DefaultCredentials +namespace Google.Apis.Auth.OAuth2 { /// Tests for . [TestFixture] From 525ab08783c274f2832419d6f5b5e33cc9dd4ec1 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 11:58:46 -0700 Subject: [PATCH 02/16] tiny refactorings in service account credential --- .../OAuth2/ServiceAccountCredential.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs index 889a1673a1..cb3988ac7c 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs @@ -106,6 +106,9 @@ public Initializer FromCertificate(X509Certificate2 certificate) #region Readonly fields + /// 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; @@ -132,7 +135,6 @@ public Initializer FromCertificate(X509Certificate2 certificate) public RSACryptoServiceProvider Key { get { return key; } } /// Constructs a new service account credential using the given initializer. - /// public ServiceAccountCredential(Initializer initializer) : base(initializer) { id = initializer.Id.ThrowIfNullOrEmpty("initializer.Id"); @@ -213,7 +215,7 @@ private static RSAParameters ConvertPKCS8ToRSAParameters(string pkcs8PrivateKey) /// private string GetSerializedPayload() { - var issued = (int)(Clock.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + var issued = (int)(Clock.UtcNow - UnixEpoch).TotalSeconds; var payload = new GoogleJsonWebSignature.Payload() { Issuer = Id, From 238f8ad8195a8bc4d3f46a6227a305d14c0178a5 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 12:38:38 -0700 Subject: [PATCH 03/16] service account credential refactoring --- .../OAuth2/ServiceAccountCredential.cs | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs index cb3988ac7c..ddcce6357f 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs @@ -153,25 +153,10 @@ public ServiceAccountCredential(Initializer initializer) : base(initializer) /// true if a new token was received successfully. public override async Task RequestAccessTokenAsync(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 hashAlg = new SHA256CryptoServiceProvider(); - byte[] assertionHash = hashAlg.ComputeHash(Encoding.ASCII.GetBytes(assertion.ToString())); - - var signature = UrlSafeBase64Encode(key.SignHash(assertionHash, "2.16.840.1.101.3.4.2.1" /* SHA256 OIG */)); - assertion.Append(".").Append(signature); - // Create the request. var request = new GoogleAssertionTokenRequest() { - Assertion = assertion.ToString() + Assertion = CreateAssertionFromPayload(CreatePayload()) }; Logger.Debug("Request a new access token. Assertion data is: " + request.Assertion); @@ -184,6 +169,30 @@ public override async Task RequestAccessTokenAsync(CancellationToken taskC #endregion + /// + /// Signs JWT token using the private key and returns the serialized assertion. + /// + /// the JWT payload to sign. + protected string CreateAssertionFromPayload(JsonWebSignature.Payload payload) + { + string serializedHeader = CreateSerializedHeader(); + string serializedPayload = NewtonsoftJsonSerializer.Instance.Serialize(payload); + + StringBuilder assertion = new StringBuilder(); + assertion.Append(UrlSafeBase64Encode(serializedHeader)) + .Append(".") + .Append(UrlSafeBase64Encode(serializedPayload)); + + // Sign the header and the payload. + var hashAlg = new SHA256CryptoServiceProvider(); + byte[] assertionHash = hashAlg.ComputeHash(Encoding.ASCII.GetBytes(assertion.ToString())); + + var signature = UrlSafeBase64Encode(key.SignHash(assertionHash, "2.16.840.1.101.3.4.2.1" /* SHA256 OIG */)); + assertion.Append(".").Append(signature); + return assertion.ToString(); + } + + // TODO: fix stale link /// /// Creates a serialized header as specified in /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader. @@ -209,14 +218,15 @@ private static RSAParameters ConvertPKCS8ToRSAParameters(string pkcs8PrivateKey) return DotNetUtilities.ToRSAParameters(crtParameters); } + // TODO: fix stale link /// - /// Creates a serialized claim set as specified in + /// Creates a claim set as specified in /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset. /// - private string GetSerializedPayload() + private GoogleJsonWebSignature.Payload CreatePayload() { var issued = (int)(Clock.UtcNow - UnixEpoch).TotalSeconds; - var payload = new GoogleJsonWebSignature.Payload() + return new GoogleJsonWebSignature.Payload() { Issuer = Id, Audience = TokenServerUrl, @@ -225,8 +235,6 @@ private string GetSerializedPayload() Subject = User, Scope = String.Join(" ", Scopes) }; - - return NewtonsoftJsonSerializer.Instance.Serialize(payload); } /// Encodes the provided UTF8 string into an URL safe base64 string. From de65a3f7a01203ff3a3fe5a8729ed59a8ad83de0 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 12:51:11 -0700 Subject: [PATCH 04/16] add scaffolding for JwtServiceAccountCredential --- .../GoogleApis.Auth.DotNet4.csproj | 1 + .../OAuth2/JwtServiceAccountCredential.cs | 77 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs diff --git a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj index c8b172c976..0976e995e8 100644 --- a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj +++ b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj @@ -104,6 +104,7 @@ + diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs new file mode 100644 index 0000000000..efd045cd26 --- /dev/null +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs @@ -0,0 +1,77 @@ +/* +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.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; + +using Google.Apis.Auth.OAuth2.Requests; +using Google.Apis.Json; +using Google.Apis.Logging; +using Google.Apis.Util; + +namespace Google.Apis.Auth.OAuth2 +{ + /// + /// Service account credential that also supports JSON Web Token access token scenario. + /// In this scenario, instead of sending a JWT to a token server and exchanging it for + /// an access token, a slightly different JWT containing the request URI is constructed + /// and used as an access token. + /// + /// + /// + public class JwtServiceAccountCredential : ServiceAccountCredential + { + /// An initializer class for the JWT service account credential. + public class Initializer : ServiceAccountCredential.Initializer + { + /// Constructs a new initializer using the given id. + public Initializer(string id) : base(id) { } + } + + /// Constructs a new JWT service account credential using the given initializer. + public JwtServiceAccountCredential(Initializer initializer) : base(initializer) + { + } + + /// + /// 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 issued = (int)(Clock.UtcNow - UnixEpoch).TotalSeconds; + var payload = new JsonWebSignature.Payload() + { + Issuer = Id, + Subject = Id, + Audience = authUri, + IssuedAtTimeSeconds = issued, + ExpirationTimeSeconds = issued + 3600, + }; + return CreateAssertionFromPayload(payload); + } + } +} From f03d42e3348e4abb88014a242d7b7ddfc46a91e7 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 13:31:35 -0700 Subject: [PATCH 05/16] refactoring of ServiceCredential and ServiceAccountCredentials --- .../OAuth2/GoogleCredential.cs | 2 +- .../OAuth2/ServiceAccountCredential.cs | 3 ++ .../OAuth2/ServiceCredential.cs | 34 +++++++++++++------ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs index 742d0f851e..fad91b76a8 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs @@ -229,7 +229,7 @@ public ServiceAccountGoogleCredential(ServiceAccountCredential credential) public override bool IsCreateScopedRequired { - get { return (credential.Scopes == null || credential.Scopes.Count() == 0); } + get { return !credential.HasScopes; } } public override GoogleCredential CreateScoped(IEnumerable scopes) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs index ddcce6357f..7df55d95de 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs @@ -134,6 +134,9 @@ public Initializer FromCertificate(X509Certificate2 certificate) /// public RSACryptoServiceProvider 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) { diff --git a/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs b/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs index c8d67d9273..dbf02f6598 100644 --- a/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs @@ -166,17 +166,8 @@ public void Initialize(ConfigurableHttpClient httpClient) 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 RequestAccessTokenAsync(cancellationToken).ConfigureAwait(false)) - { - 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); + var accessToken = await GetTokenMaybeRefreshAsync(request.RequestUri.ToString(), cancellationToken); + AccessMethod.Intercept(request, accessToken); } #endregion @@ -197,6 +188,27 @@ public async Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args) #endregion + + /// + /// Gets an access token that will be used for a request. + /// If the existing token has expired, try to refresh it first. + /// + /// The URI of the request. + /// the access token + protected virtual async Task GetTokenMaybeRefreshAsync(string authUri, CancellationToken cancellationToken) + { + if (Token == null || Token.IsExpired(Clock)) + { + Logger.Debug("Token has expired, trying to get a new one."); + if (!await RequestAccessTokenAsync(cancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException("The access token has expired but we can't refresh it"); + } + Logger.Info("New access token was received successfully"); + } + return Token.AccessToken; + } + /// Requests a new token. /// Cancellation token to cancel operation. /// true if a new token was received successfully. From 10a47c06e36401be827c5d8d34216716a3befc42 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 13:31:59 -0700 Subject: [PATCH 06/16] implement JWT service account credential --- .../OAuth2/JwtServiceAccountCredential.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs index efd045cd26..61607032fa 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs @@ -55,6 +55,26 @@ public JwtServiceAccountCredential(Initializer initializer) : base(initializer) { } + /// + /// Gets an access token that will be used for a request. + /// If this service account has some scopes associated with it, this credential + /// will behave exactly the same as . + /// Otherwise, it will construct a JWT access token for given . + /// + /// The URI of the request. + /// the access token + protected override async Task GetTokenMaybeRefreshAsync(string authUri, CancellationToken cancellationToken) + { + if (HasScopes) + { + return await base.GetTokenMaybeRefreshAsync(authUri, cancellationToken); + } + + // TODO(jtattermusch): support caching of JWT access tokens. + return CreateJwtAccessToken(authUri); + } + + // TODO(jtattermusch): create a custom type to hold the access token. /// /// 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. From c67964b858285ac9bfb97ea76f9c3825d9db098e Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 13:36:10 -0700 Subject: [PATCH 07/16] adding comments --- .../OAuth2/JwtServiceAccountCredential.cs | 3 ++- Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs index 61607032fa..1db1fb20ff 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs @@ -61,12 +61,13 @@ public JwtServiceAccountCredential(Initializer initializer) : base(initializer) /// will behave exactly the same as . /// Otherwise, it will construct a JWT access token for given . /// - /// The URI of the request. + /// The URI of the request to be authorized. /// the access token protected override async Task GetTokenMaybeRefreshAsync(string authUri, CancellationToken cancellationToken) { if (HasScopes) { + // fall back to default ServiceAccount behavior. return await base.GetTokenMaybeRefreshAsync(authUri, cancellationToken); } diff --git a/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs b/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs index dbf02f6598..dc36231517 100644 --- a/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs @@ -193,7 +193,7 @@ public async Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args) /// Gets an access token that will be used for a request. /// If the existing token has expired, try to refresh it first. /// - /// The URI of the request. + /// The URI of the request to be authorized. /// the access token protected virtual async Task GetTokenMaybeRefreshAsync(string authUri, CancellationToken cancellationToken) { From 57980bfa2428ce1e13fe7b2db76c83dc464f90e6 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 17:06:21 -0700 Subject: [PATCH 08/16] implemented JwtServiceAccountCredential --- .../GoogleApis.Auth.DotNet4.csproj | 1 - .../OAuth2/JwtServiceAccountCredential.cs | 74 ++++++++++++++----- Src/GoogleApis.Auth/GoogleApis.Auth.csproj | 1 + .../OAuth2/ITokenAccess.cs | 21 ++++-- .../OAuth2/ServiceCredential.cs | 16 ++-- Src/GoogleApis.Auth/OAuth2/UserCredential.cs | 35 ++++++--- 6 files changed, 103 insertions(+), 45 deletions(-) rename Src/{GoogleApis.Auth.DotNet4 => GoogleApis.Auth}/OAuth2/ITokenAccess.cs (54%) diff --git a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj index 0976e995e8..0e52627245 100644 --- a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj +++ b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj @@ -98,7 +98,6 @@ - diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs index 1db1fb20ff..1c92a1d99a 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs @@ -43,6 +43,9 @@ namespace Google.Apis.Auth.OAuth2 /// public class JwtServiceAccountCredential : ServiceAccountCredential { + /// Default lifetime of 1 hour + private const int DefaultExpiresInSeconds = 3600; + /// An initializer class for the JWT service account credential. public class Initializer : ServiceAccountCredential.Initializer { @@ -56,43 +59,78 @@ public JwtServiceAccountCredential(Initializer initializer) : base(initializer) } /// - /// Gets an access token that will be used for a request. - /// If this service account has some scopes associated with it, this credential - /// will behave exactly the same as . - /// Otherwise, it will construct a JWT access token for given . + /// Gets an access token to authorize a request. + /// If is set and this credential has no scopes associated + /// with it, a JWT access token for given is returned. + /// Otherwise, the access token will be obtained in exactly the same way as in + /// /// - /// The URI of the request to be authorized. - /// the access token - protected override async Task GetTokenMaybeRefreshAsync(string authUri, CancellationToken cancellationToken) + /// 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) + if (!HasScopes && authUri != null) { - // fall back to default ServiceAccount behavior. - return await base.GetTokenMaybeRefreshAsync(authUri, cancellationToken); + // 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).AccessToken; } - - // TODO(jtattermusch): support caching of JWT access tokens. - return CreateJwtAccessToken(authUri); + return await base.GetAccessTokenForRequestAsync(authUri, cancellationToken); } - // TODO(jtattermusch): create a custom type to hold the access token. /// /// 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) + private JwtAccessToken CreateJwtAccessToken(string authUri) { - var issued = (int)(Clock.UtcNow - UnixEpoch).TotalSeconds; + 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, + ExpirationTimeSeconds = issued + DefaultExpiresInSeconds, + }; + + return new JwtAccessToken + { + Issued = issuedDateTime, + ExpiresInSeconds = DefaultExpiresInSeconds, + AccessToken = CreateAssertionFromPayload(payload) }; - return CreateAssertionFromPayload(payload); + } + + /// + /// JWT access token + /// + internal class JwtAccessToken + { + /// The date and time that this token was issued. + public DateTime Issued { get; set; } + + /// Gets or sets the lifetime in seconds of the access token. + public long ExpiresInSeconds { get; set; } + + /// Gets or sets the access token. + public string AccessToken { get; set; } + + /// + /// Returns true if the token is expired or it's going to be expired in the next minute. + /// + public bool IsExpired(IClock clock) + { + if (AccessToken == null) + { + return true; + } + + return Issued.AddSeconds(ExpiresInSeconds - 60) <= clock.Now; + } } } } diff --git a/Src/GoogleApis.Auth/GoogleApis.Auth.csproj b/Src/GoogleApis.Auth/GoogleApis.Auth.csproj index ebf873381c..b2868beb75 100644 --- a/Src/GoogleApis.Auth/GoogleApis.Auth.csproj +++ b/Src/GoogleApis.Auth/GoogleApis.Auth.csproj @@ -72,6 +72,7 @@ + diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/ITokenAccess.cs b/Src/GoogleApis.Auth/OAuth2/ITokenAccess.cs similarity index 54% rename from Src/GoogleApis.Auth.DotNet4/OAuth2/ITokenAccess.cs rename to Src/GoogleApis.Auth/OAuth2/ITokenAccess.cs index 6c96eb19a4..f08b5f81be 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/ITokenAccess.cs +++ b/Src/GoogleApis.Auth/OAuth2/ITokenAccess.cs @@ -22,19 +22,24 @@ limitations under the License. namespace Google.Apis.Auth.OAuth2 { /// - /// Allows direct control over access token and its retrieval. + /// Allows direct retrieval of access tokens to authenticate requests. /// This is necessary for workflows where you don't want to use /// to access the API. /// (e.g. gRPC that implemenents the entire HTTP2 stack internally). /// public interface ITokenAccess { - /// Gets the token response which contains the access token. - TokenResponse Token { get; } - - /// Requests a new token. - /// Cancellation token to cancel operation. - /// true if a new token was received successfully. - Task RequestAccessTokenAsync(CancellationToken taskCancellationToken); + /// + /// Gets an access token to authorize a request. + /// Implementations should handle automatic refreshes of the token + /// if they are supported. + /// The might be required by some credential types + /// (e.g. ) while other credential types + /// migth just ignore it. + /// + /// The URI the returned token will grant access to. + /// The cancellation token. + /// The access token. + Task GetAccessTokenForRequestAsync(string authUri = null, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs b/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs index dc36231517..a2ee097304 100644 --- a/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs @@ -41,7 +41,7 @@ namespace Google.Apis.Auth.OAuth2 /// /// public abstract class ServiceCredential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler, - IConfigurableHttpClientInitializer + IConfigurableHttpClientInitializer, ITokenAccess { protected static readonly ILogger Logger = ApplicationContext.Logger.ForType(); @@ -166,7 +166,7 @@ public void Initialize(ConfigurableHttpClient httpClient) public async Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var accessToken = await GetTokenMaybeRefreshAsync(request.RequestUri.ToString(), cancellationToken); + var accessToken = await GetAccessTokenForRequestAsync(request.RequestUri.ToString(), cancellationToken); AccessMethod.Intercept(request, accessToken); } @@ -189,13 +189,13 @@ public async Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args) #endregion + #region ITokenAccess implementation + /// - /// Gets an access token that will be used for a request. - /// If the existing token has expired, try to refresh it first. + /// Gets an access token to authorize a request. If the existing token has expired, try to refresh it first. + /// /// - /// The URI of the request to be authorized. - /// the access token - protected virtual async Task GetTokenMaybeRefreshAsync(string authUri, CancellationToken cancellationToken) + public virtual async Task GetAccessTokenForRequestAsync(string authUri = null, CancellationToken cancellationToken = default(CancellationToken)) { if (Token == null || Token.IsExpired(Clock)) { @@ -209,6 +209,8 @@ protected virtual async Task GetTokenMaybeRefreshAsync(string authUri, C return Token.AccessToken; } + #endregion + /// Requests a new token. /// Cancellation token to cancel operation. /// true if a new token was received successfully. diff --git a/Src/GoogleApis.Auth/OAuth2/UserCredential.cs b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs index c586e1da1b..a0fa550e33 100644 --- a/Src/GoogleApis.Auth/OAuth2/UserCredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs @@ -32,7 +32,7 @@ namespace Google.Apis.Auth.OAuth2 /// the access token when it expires using a refresh token. /// public class UserCredential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler, - IConfigurableHttpClientInitializer + IConfigurableHttpClientInitializer, ITokenAccess { protected static readonly ILogger Logger = ApplicationContext.Logger.ForType(); @@ -91,17 +91,9 @@ public UserCredential(IAuthorizationCodeFlow flow, string userId, TokenResponse /// 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) + public async Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - 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"); - } - } - + var accessToken = await GetAccessTokenForRequestAsync(request.RequestUri.ToString(), cancellationToken); flow.AccessMethod.Intercept(request, Token.AccessToken); } @@ -133,6 +125,27 @@ public void Initialize(ConfigurableHttpClient httpClient) #endregion + #region ITokenAccess implementation + + /// + /// Gets an access token to authorize a request. If the existing token has expired, try to refresh it first. + /// + /// + public virtual async Task GetAccessTokenForRequestAsync(string authUri = null, CancellationToken cancellationToken = default(CancellationToken)) + { + if (Token.IsExpired(flow.Clock)) + { + Logger.Debug("Token has expired, trying to refresh it."); + if (!await RefreshTokenAsync(cancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException("The access token has expired but we can't refresh it"); + } + } + return token.AccessToken; + } + + #endregion + /// /// Refreshes the token by calling to /// . From 9732e167aff48834b9a35743a008c74bd39fed67 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 17:45:07 -0700 Subject: [PATCH 09/16] implemented JwtServiceAccountCredential --- .../OAuth2/DefaultCredentialProvider.cs | 6 +- .../OAuth2/GoogleCredential.cs | 156 ++++-------------- .../OAuth2/JwtServiceAccountCredential.cs | 12 +- 3 files changed, 37 insertions(+), 137 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs index 82aae3175d..5fbd0c8bf1 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs @@ -222,8 +222,8 @@ private static UserCredential CreateUserCredentialFromJson(JsonCredentialParamet return new UserCredential(flow, "ApplicationDefaultCredentials", token); } - /// Creates a service account credential from JSON data. - private static ServiceAccountCredential CreateServiceAccountCredentialFromJson(JsonCredentialParameters credentialParameters) + /// Creates a from JSON data. + private static JwtServiceAccountCredential CreateServiceAccountCredentialFromJson(JsonCredentialParameters credentialParameters) { if (credentialParameters.Type != JsonCredentialParameters.ServiceAccountCredentialType || string.IsNullOrEmpty(credentialParameters.ClientEmail) || @@ -232,7 +232,7 @@ private static ServiceAccountCredential CreateServiceAccountCredentialFromJson(J throw new InvalidOperationException("JSON data does not represent a valid service account credential."); } var initializer = new ServiceAccountCredential.Initializer(credentialParameters.ClientEmail); - return new ServiceAccountCredential(initializer.FromPrivateKey(credentialParameters.PrivateKey)); + return new JwtServiceAccountCredential(initializer.FromPrivateKey(credentialParameters.PrivateKey)); } /// diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs index fad91b76a8..36408bcaf5 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs @@ -36,10 +36,24 @@ namespace Google.Apis.Auth.OAuth2 /// See for the credential retrieval logic. /// /// - public abstract class GoogleCredential : IConfigurableHttpClientInitializer, ITokenAccess + public class GoogleCredential : IConfigurableHttpClientInitializer, ITokenAccess { private static DefaultCredentialProvider defaultCredentialProvider = new DefaultCredentialProvider(); + private readonly object credential; + private readonly ITokenAccess tokenAccess; + private readonly IConfigurableHttpClientInitializer clientInitializer; + + /// + /// Creates a new GoogleCredential. + /// + protected GoogleCredential(object credential, ITokenAccess tokenAccess, IConfigurableHttpClientInitializer clientInitializer) + { + this.credential = credential; + this.tokenAccess = tokenAccess; + this.clientInitializer = clientInitializer; + } + /// /// Returns the Application Default Credentials which are ambient credentials that identify and authorize /// the whole application. @@ -120,107 +134,52 @@ public virtual GoogleCredential CreateScoped(IEnumerable scopes) void IConfigurableHttpClientInitializer.Initialize(ConfigurableHttpClient httpClient) { - Initialize(httpClient); + clientInitializer.Initialize(httpClient); } #endregion #region ITokenAccess - TokenResponse ITokenAccess.Token - { - get { return Token; } - } - - Task ITokenAccess.RequestAccessTokenAsync(CancellationToken taskCancellationToken) + Task ITokenAccess.GetAccessTokenForRequestAsync(string authUri, CancellationToken cancellationToken) { - return RequestAccessTokenAsync(taskCancellationToken); + return tokenAccess.GetAccessTokenForRequestAsync(authUri, cancellationToken); } #endregion /// Provides access to the underlying credential object - internal abstract object UnderlyingCredential { get; } - - // We're explicitly implementing all the interfaces to only expose the members user actually - // needs to see. Because you cannot make explicit interface implementors abstract, they are redirecting - // to the following protected abstract members. - - /// Initializes a HTTP client. - protected abstract void Initialize(ConfigurableHttpClient httpClient); - - /// Gets the current access token. - protected abstract TokenResponse Token { get; } - - /// Requests refreshing the access token. - protected abstract Task RequestAccessTokenAsync(CancellationToken taskCancellationToken); + internal object UnderlyingCredential { get { return credential; } } #region Factory methods /// Creates a GoogleCredential wrapping a . internal static GoogleCredential FromCredential(ComputeCredential credential) { - return new ComputeGoogleCredential(credential); + return new GoogleCredential(credential, credential, credential); } /// Creates a GoogleCredential wrapping a . - internal static GoogleCredential FromCredential(ServiceAccountCredential credential) + internal static GoogleCredential FromCredential(JwtServiceAccountCredential credential) { - return new ServiceAccountGoogleCredential(credential); + return new JwtServiceAccountGoogleCredential(credential); } /// Creates a GoogleCredential wrapping a . internal static GoogleCredential FromCredential(UserCredential credential) { - return new UserGoogleCredential(credential); + return new GoogleCredential(credential, credential, credential); } #endregion - // TODO(jtattermush): Look into adjusting the API of ServiceAccountCredential, ComputeCredential - // and UserCredential so that they implement ITokenAccess. Then the boilerplate below will go away. - - /// Wraps ComputeCredential as GoogleCredential. - internal class ComputeGoogleCredential : GoogleCredential - { - private readonly ComputeCredential credential; - - public ComputeGoogleCredential(ComputeCredential credential) - { - this.credential = credential; - } - - #region GoogleCredential overrides - - internal override object UnderlyingCredential - { - get { return credential; } - } - - protected override void Initialize(ConfigurableHttpClient httpClient) - { - credential.Initialize(httpClient); - } - - protected override TokenResponse Token - { - get { return credential.Token; } - } - - protected override Task RequestAccessTokenAsync(CancellationToken taskCancellationToken) - { - return credential.RequestAccessTokenAsync(taskCancellationToken); - } - - #endregion - } - - /// Wraps ServiceAccountCredential as GoogleCredential. - internal class ServiceAccountGoogleCredential : GoogleCredential + /// Wraps JwtServiceAccountCredential as GoogleCredential. + internal class JwtServiceAccountGoogleCredential : GoogleCredential { - private readonly ServiceAccountCredential credential; + private readonly JwtServiceAccountCredential credential; - public ServiceAccountGoogleCredential(ServiceAccountCredential credential) + public JwtServiceAccountGoogleCredential(JwtServiceAccountCredential credential) + : base(credential, credential, credential) { this.credential = credential; } @@ -234,68 +193,13 @@ public override bool IsCreateScopedRequired public override GoogleCredential CreateScoped(IEnumerable scopes) { - var initializer = new ServiceAccountCredential.Initializer(credential.Id) + var initializer = new JwtServiceAccountCredential.Initializer(credential.Id) { User = credential.User, Key = credential.Key, Scopes = scopes }; - return GoogleCredential.FromCredential(new ServiceAccountCredential(initializer)); - } - - internal override object UnderlyingCredential - { - get { return credential; } - } - - protected override void Initialize(ConfigurableHttpClient httpClient) - { - credential.Initialize(httpClient); - } - - protected override TokenResponse Token - { - get { return credential.Token; } - } - - protected override Task RequestAccessTokenAsync(CancellationToken taskCancellationToken) - { - return credential.RequestAccessTokenAsync(taskCancellationToken); - } - - #endregion - } - - /// Wraps UserCredential as GoogleCredential. - internal class UserGoogleCredential : GoogleCredential - { - private readonly UserCredential credential; - - public UserGoogleCredential(UserCredential credential) - { - this.credential = credential; - } - - #region GoogleCredential overrides - - internal override object UnderlyingCredential - { - get { return credential; } - } - - protected override void Initialize(ConfigurableHttpClient httpClient) - { - credential.Initialize(httpClient); - } - - protected override TokenResponse Token - { - get { return credential.Token; } - } - - protected override Task RequestAccessTokenAsync(CancellationToken taskCancellationToken) - { - return credential.RefreshTokenAsync(taskCancellationToken); + return GoogleCredential.FromCredential(new JwtServiceAccountCredential(initializer)); } #endregion diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs index 1c92a1d99a..21c4d42a3c 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs @@ -38,6 +38,8 @@ namespace Google.Apis.Auth.OAuth2 /// In this scenario, instead of sending a JWT to a token server and exchanging it for /// an access token, a slightly different JWT containing the request URI is constructed /// and used as an access token. + /// See for explanation when JWT access token + /// is used and when regular OAuth2 token is used. /// /// /// @@ -46,15 +48,9 @@ public class JwtServiceAccountCredential : ServiceAccountCredential /// Default lifetime of 1 hour private const int DefaultExpiresInSeconds = 3600; - /// An initializer class for the JWT service account credential. - public class Initializer : ServiceAccountCredential.Initializer - { - /// Constructs a new initializer using the given id. - public Initializer(string id) : base(id) { } - } - /// Constructs a new JWT service account credential using the given initializer. - public JwtServiceAccountCredential(Initializer initializer) : base(initializer) + public JwtServiceAccountCredential(ServiceAccountCredential.Initializer initializer) + : base(initializer) { } From 7d967828e75b2f89e9ee40b94fdc6977999c60e8 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 17:48:25 -0700 Subject: [PATCH 10/16] updated tests --- .../OAuth2/DefaultCredentialProviderTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs index 1b41741b39..2f9c84d003 100644 --- a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs +++ b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs @@ -182,16 +182,16 @@ public async Task GetDefaultCredential_ServiceAccountCredential_FromEnvironmentV var credential = await credentialProvider.GetDefaultCredentialAsync(); - Assert.IsInstanceOf(typeof(ServiceAccountCredential), credential.UnderlyingCredential); + Assert.IsInstanceOf(typeof(JwtServiceAccountCredential), credential.UnderlyingCredential); Assert.IsTrue(credential.IsCreateScopedRequired); var scopes = new[] { "https://www.googleapis.com/auth/cloud-platform" }; var scopedCredential = credential.CreateScoped(scopes); Assert.AreNotSame(credential, scopedCredential); - Assert.IsInstanceOf(typeof(ServiceAccountCredential), scopedCredential.UnderlyingCredential); + Assert.IsInstanceOf(typeof(JwtServiceAccountCredential), scopedCredential.UnderlyingCredential); Assert.IsFalse(scopedCredential.IsCreateScopedRequired); - CollectionAssert.AreEqual(scopes, ((ServiceAccountCredential) scopedCredential.UnderlyingCredential).Scopes); + CollectionAssert.AreEqual(scopes, ((JwtServiceAccountCredential) scopedCredential.UnderlyingCredential).Scopes); } #endregion From fb2371d20222ec9a3536a9062c24e19d0f21f156 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 6 Aug 2015 19:49:36 -0700 Subject: [PATCH 11/16] added a test for JWT access token and removed unused usings --- .../OAuth2/GoogleCredentialTests.cs | 36 +++++++++++++++++++ .../OAuth2/DefaultCredentialProvider.cs | 10 ------ .../OAuth2/GoogleCredential.cs | 4 --- .../OAuth2/JwtServiceAccountCredential.cs | 11 ------ 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs index 2b487a7bd1..38f9c66668 100644 --- a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs +++ b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs @@ -19,6 +19,8 @@ limitations under the License. using System.Text; using System.Threading.Tasks; +using Google.Apis.Json; + using NUnit.Framework; namespace Google.Apis.Auth.OAuth2 @@ -27,6 +29,7 @@ namespace Google.Apis.Auth.OAuth2 [TestFixture] public class GoogleCredentialTests { + private const string DummyAuthUri = "https://www.googleapis.com/google.some_google_api"; private const string DummyUserCredentialFileContents = @"{ ""client_id"": ""CLIENT_ID"", ""client_secret"": ""CLIENT_SECRET"", @@ -67,7 +70,40 @@ public void FromStream_ServiceAccountCredential() { var stream = new MemoryStream(Encoding.UTF8.GetBytes(DummyServiceAccountCredentialFileContents)); var credential = GoogleCredential.FromStream(stream); + Assert.IsInstanceOf(typeof(JwtServiceAccountCredential), credential.UnderlyingCredential); Assert.IsTrue(credential.IsCreateScopedRequired); } + + /// + /// Creates service account credential from stream, obtains a JWT token + /// from the credential and checks the access token is well-formed. + /// + /// + [Test] + public async Task FromStream_ServiceAccountCredential_GetJwtAccessToken() + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(DummyServiceAccountCredentialFileContents)); + var credential = GoogleCredential.FromStream(stream); + + // Without adding scopes, the credential should be generating JWT scopes. + string accessToken = await (credential as ITokenAccess).GetAccessTokenForRequestAsync(DummyAuthUri); + var parts = accessToken.Split(new[] {'.'}, 3); + + var header = NewtonsoftJsonSerializer.Instance.Deserialize(UrlSafeDecode64(parts[0])); + Assert.AreEqual("JWT", header.Type); + Assert.AreEqual("RS256", header.Algorithm); + + var payload = NewtonsoftJsonSerializer.Instance.Deserialize(UrlSafeDecode64(parts[1])); + + Assert.AreEqual("CLIENT_EMAIL", payload.Issuer); + Assert.AreEqual("CLIENT_EMAIL", payload.Subject); + Assert.AreEqual(DummyAuthUri, payload.Audience); + Assert.AreEqual(3600, payload.ExpirationTimeSeconds - payload.IssuedAtTimeSeconds); + } + + private string UrlSafeDecode64(string urlSafeBase64) + { + return Encoding.UTF8.GetString(Convert.FromBase64String(urlSafeBase64)); + } } } diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs index 5fbd0c8bf1..a689d0b7ac 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs @@ -15,23 +15,13 @@ limitations under the License. */ using System; -using System.Collections; -using System.Collections.Generic; using System.IO; -using System.Net; -using System.Net.Http; -using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json; - using Google.Apis.Auth.OAuth2.Flows; 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 { diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs index 36408bcaf5..6685cb1ab7 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs @@ -14,15 +14,11 @@ You may obtain a copy of the License at limitations under the License. */ -using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Google.Apis.Auth.OAuth2.Responses; using Google.Apis.Http; namespace Google.Apis.Auth.OAuth2 diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs index 21c4d42a3c..73d0d738a5 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs @@ -15,20 +15,9 @@ limitations under the License. */ using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Security; - -using Google.Apis.Auth.OAuth2.Requests; -using Google.Apis.Json; -using Google.Apis.Logging; using Google.Apis.Util; namespace Google.Apis.Auth.OAuth2 From 0f7a6b18dd374dcabec4198a16276be71aef9dcf Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Fri, 7 Aug 2015 13:37:03 -0700 Subject: [PATCH 12/16] addressing comments --- .../OAuth2/GoogleCredentialTests.cs | 3 ++- .../GoogleApis.Auth.DotNet4.csproj | 1 - Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs | 4 +--- .../OAuth2/JwtServiceAccountCredential.cs | 7 ++----- .../OAuth2/ServiceAccountCredential.cs | 2 +- Src/GoogleApis.Auth/GoogleApis.Auth.csproj | 1 + .../OAuth2/JsonCredentialParameters.cs | 0 Src/GoogleApis.Auth/OAuth2/UserCredential.cs | 8 ++------ 8 files changed, 9 insertions(+), 17 deletions(-) rename Src/{GoogleApis.Auth.DotNet4 => GoogleApis.Auth}/OAuth2/JsonCredentialParameters.cs (100%) diff --git a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs index 38f9c66668..03b6762db6 100644 --- a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs +++ b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs @@ -87,7 +87,8 @@ public async Task FromStream_ServiceAccountCredential_GetJwtAccessToken() // Without adding scopes, the credential should be generating JWT scopes. string accessToken = await (credential as ITokenAccess).GetAccessTokenForRequestAsync(DummyAuthUri); - var parts = accessToken.Split(new[] {'.'}, 3); + var parts = accessToken.Split(new[] {'.'}); + Assert.AreEqual(3, parts.Length); var header = NewtonsoftJsonSerializer.Instance.Deserialize(UrlSafeDecode64(parts[0])); Assert.AreEqual("JWT", header.Type); diff --git a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj index 0e52627245..11193f8d31 100644 --- a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj +++ b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj @@ -97,7 +97,6 @@ - diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs index 6685cb1ab7..c1c9664af7 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs @@ -40,9 +40,7 @@ public class GoogleCredential : IConfigurableHttpClientInitializer, ITokenAccess private readonly ITokenAccess tokenAccess; private readonly IConfigurableHttpClientInitializer clientInitializer; - /// - /// Creates a new GoogleCredential. - /// + /// Creates a new GoogleCredential. protected GoogleCredential(object credential, ITokenAccess tokenAccess, IConfigurableHttpClientInitializer clientInitializer) { this.credential = credential; diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs index 73d0d738a5..cb688fe656 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs @@ -1,5 +1,5 @@ /* -Copyright 2013 Google Inc +Copyright 2015 Google Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ namespace Google.Apis.Auth.OAuth2 /// See for explanation when JWT access token /// is used and when regular OAuth2 token is used. /// - /// /// public class JwtServiceAccountCredential : ServiceAccountCredential { @@ -90,9 +89,7 @@ private JwtAccessToken CreateJwtAccessToken(string authUri) }; } - /// - /// JWT access token - /// + /// JWT access token. internal class JwtAccessToken { /// The date and time that this token was issued. diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs index 7df55d95de..0d67dd3c39 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs @@ -1,5 +1,5 @@ /* -Copyright 2013 Google Inc +Copyright 2015 Google Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Src/GoogleApis.Auth/GoogleApis.Auth.csproj b/Src/GoogleApis.Auth/GoogleApis.Auth.csproj index b2868beb75..eb377ea6ca 100644 --- a/Src/GoogleApis.Auth/GoogleApis.Auth.csproj +++ b/Src/GoogleApis.Auth/GoogleApis.Auth.csproj @@ -73,6 +73,7 @@ + diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JsonCredentialParameters.cs b/Src/GoogleApis.Auth/OAuth2/JsonCredentialParameters.cs similarity index 100% rename from Src/GoogleApis.Auth.DotNet4/OAuth2/JsonCredentialParameters.cs rename to Src/GoogleApis.Auth/OAuth2/JsonCredentialParameters.cs diff --git a/Src/GoogleApis.Auth/OAuth2/UserCredential.cs b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs index a0fa550e33..e0e93b659c 100644 --- a/Src/GoogleApis.Auth/OAuth2/UserCredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs @@ -91,9 +91,9 @@ public UserCredential(IAuthorizationCodeFlow flow, string userId, TokenResponse /// 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 cancellationToken) + public async Task InterceptAsync(HttpRequestMessage request, CancellationToken taskCancellationToken) { - var accessToken = await GetAccessTokenForRequestAsync(request.RequestUri.ToString(), cancellationToken); + var accessToken = await GetAccessTokenForRequestAsync(request.RequestUri.ToString(), taskCancellationToken).ConfigureAwait(false); flow.AccessMethod.Intercept(request, Token.AccessToken); } @@ -127,10 +127,6 @@ public void Initialize(ConfigurableHttpClient httpClient) #region ITokenAccess implementation - /// - /// Gets an access token to authorize a request. If the existing token has expired, try to refresh it first. - /// - /// public virtual async Task GetAccessTokenForRequestAsync(string authUri = null, CancellationToken cancellationToken = default(CancellationToken)) { if (Token.IsExpired(flow.Clock)) From 373f772c52fea6e42db5cf37ab95f385a54bb948 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Fri, 7 Aug 2015 15:34:43 -0700 Subject: [PATCH 13/16] addressing design comments --- .../OAuth2/DefaultCredentialProviderTests.cs | 6 +- .../OAuth2/GoogleCredentialTests.cs | 2 +- .../GoogleApis.Auth.DotNet4.csproj | 1 - .../OAuth2/DefaultCredentialProvider.cs | 10 +- .../OAuth2/GoogleCredential.cs | 44 +++---- .../OAuth2/JwtServiceAccountCredential.cs | 118 ------------------ .../OAuth2/ServiceAccountCredential.cs | 52 +++++++- Src/GoogleApis.Auth/GoogleApis.Auth.csproj | 1 + Src/GoogleApis.Auth/OAuth2/ICredential.cs | 25 ++++ Src/GoogleApis.Auth/OAuth2/ITokenAccess.cs | 2 +- .../OAuth2/JsonCredentialParameters.cs | 2 +- .../OAuth2/ServiceCredential.cs | 3 +- Src/GoogleApis.Auth/OAuth2/UserCredential.cs | 3 +- 13 files changed, 104 insertions(+), 165 deletions(-) delete mode 100644 Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs create mode 100644 Src/GoogleApis.Auth/OAuth2/ICredential.cs diff --git a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs index 2f9c84d003..1b41741b39 100644 --- a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs +++ b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/DefaultCredentialProviderTests.cs @@ -182,16 +182,16 @@ public async Task GetDefaultCredential_ServiceAccountCredential_FromEnvironmentV var credential = await credentialProvider.GetDefaultCredentialAsync(); - Assert.IsInstanceOf(typeof(JwtServiceAccountCredential), credential.UnderlyingCredential); + Assert.IsInstanceOf(typeof(ServiceAccountCredential), credential.UnderlyingCredential); Assert.IsTrue(credential.IsCreateScopedRequired); var scopes = new[] { "https://www.googleapis.com/auth/cloud-platform" }; var scopedCredential = credential.CreateScoped(scopes); Assert.AreNotSame(credential, scopedCredential); - Assert.IsInstanceOf(typeof(JwtServiceAccountCredential), scopedCredential.UnderlyingCredential); + Assert.IsInstanceOf(typeof(ServiceAccountCredential), scopedCredential.UnderlyingCredential); Assert.IsFalse(scopedCredential.IsCreateScopedRequired); - CollectionAssert.AreEqual(scopes, ((JwtServiceAccountCredential) scopedCredential.UnderlyingCredential).Scopes); + CollectionAssert.AreEqual(scopes, ((ServiceAccountCredential) scopedCredential.UnderlyingCredential).Scopes); } #endregion diff --git a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs index 03b6762db6..729c9a44dd 100644 --- a/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs +++ b/Src/GoogleApis.Auth.DotNet4.Tests/OAuth2/GoogleCredentialTests.cs @@ -70,7 +70,7 @@ public void FromStream_ServiceAccountCredential() { var stream = new MemoryStream(Encoding.UTF8.GetBytes(DummyServiceAccountCredentialFileContents)); var credential = GoogleCredential.FromStream(stream); - Assert.IsInstanceOf(typeof(JwtServiceAccountCredential), credential.UnderlyingCredential); + Assert.IsInstanceOf(typeof(ServiceAccountCredential), credential.UnderlyingCredential); Assert.IsTrue(credential.IsCreateScopedRequired); } diff --git a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj index 11193f8d31..f4a57d0231 100644 --- a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj +++ b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj @@ -102,7 +102,6 @@ - diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs index a689d0b7ac..ca12cdc009 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/DefaultCredentialProvider.cs @@ -132,7 +132,7 @@ private async Task CreateDefaultCredentialAsync() if (await ComputeCredential.IsRunningOnComputeEngine().ConfigureAwait(false)) { Logger.Debug("ComputeEngine check passed. Using ComputeEngine Credentials."); - return GoogleCredential.FromCredential(new ComputeCredential()); + return new GoogleCredential(new ComputeCredential()); } // If everything we tried has failed, throw an exception. @@ -175,7 +175,7 @@ private static GoogleCredential CreateDefaultCredentialFromJson(JsonCredentialPa switch (credentialParameters.Type) { case JsonCredentialParameters.AuthorizedUserCredentialType: - return GoogleCredential.FromCredential(CreateUserCredentialFromJson(credentialParameters)); + return new GoogleCredential(CreateUserCredentialFromJson(credentialParameters)); case JsonCredentialParameters.ServiceAccountCredentialType: return GoogleCredential.FromCredential(CreateServiceAccountCredentialFromJson(credentialParameters)); @@ -212,8 +212,8 @@ private static UserCredential CreateUserCredentialFromJson(JsonCredentialParamet return new UserCredential(flow, "ApplicationDefaultCredentials", token); } - /// Creates a from JSON data. - private static JwtServiceAccountCredential CreateServiceAccountCredentialFromJson(JsonCredentialParameters credentialParameters) + /// Creates a from JSON data. + private static ServiceAccountCredential CreateServiceAccountCredentialFromJson(JsonCredentialParameters credentialParameters) { if (credentialParameters.Type != JsonCredentialParameters.ServiceAccountCredentialType || string.IsNullOrEmpty(credentialParameters.ClientEmail) || @@ -222,7 +222,7 @@ private static JwtServiceAccountCredential CreateServiceAccountCredentialFromJso throw new InvalidOperationException("JSON data does not represent a valid service account credential."); } var initializer = new ServiceAccountCredential.Initializer(credentialParameters.ClientEmail); - return new JwtServiceAccountCredential(initializer.FromPrivateKey(credentialParameters.PrivateKey)); + return new ServiceAccountCredential(initializer.FromPrivateKey(credentialParameters.PrivateKey)); } /// diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs index c1c9664af7..f6813d6b74 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs @@ -36,16 +36,12 @@ public class GoogleCredential : IConfigurableHttpClientInitializer, ITokenAccess { private static DefaultCredentialProvider defaultCredentialProvider = new DefaultCredentialProvider(); - private readonly object credential; - private readonly ITokenAccess tokenAccess; - private readonly IConfigurableHttpClientInitializer clientInitializer; + private readonly ICredential credential; /// Creates a new GoogleCredential. - protected GoogleCredential(object credential, ITokenAccess tokenAccess, IConfigurableHttpClientInitializer clientInitializer) + internal GoogleCredential(ICredential credential) { this.credential = credential; - this.tokenAccess = tokenAccess; - this.clientInitializer = clientInitializer; } /// @@ -128,7 +124,7 @@ public virtual GoogleCredential CreateScoped(IEnumerable scopes) void IConfigurableHttpClientInitializer.Initialize(ConfigurableHttpClient httpClient) { - clientInitializer.Initialize(httpClient); + credential.Initialize(httpClient); } #endregion @@ -137,43 +133,31 @@ void IConfigurableHttpClientInitializer.Initialize(ConfigurableHttpClient httpCl Task ITokenAccess.GetAccessTokenForRequestAsync(string authUri, CancellationToken cancellationToken) { - return tokenAccess.GetAccessTokenForRequestAsync(authUri, cancellationToken); + return credential.GetAccessTokenForRequestAsync(authUri, cancellationToken); } #endregion /// Provides access to the underlying credential object - internal object UnderlyingCredential { get { return credential; } } + internal ICredential UnderlyingCredential { get { return credential; } } #region Factory methods - /// Creates a GoogleCredential wrapping a . - internal static GoogleCredential FromCredential(ComputeCredential credential) - { - return new GoogleCredential(credential, credential, credential); - } - /// Creates a GoogleCredential wrapping a . - internal static GoogleCredential FromCredential(JwtServiceAccountCredential credential) - { - return new JwtServiceAccountGoogleCredential(credential); - } - - /// Creates a GoogleCredential wrapping a . - internal static GoogleCredential FromCredential(UserCredential credential) + internal static GoogleCredential FromCredential(ServiceAccountCredential credential) { - return new GoogleCredential(credential, credential, credential); + return new ServiceAccountGoogleCredential(credential); } #endregion - /// Wraps JwtServiceAccountCredential as GoogleCredential. - internal class JwtServiceAccountGoogleCredential : GoogleCredential + /// Wraps ServiceAccountCredential as GoogleCredential. + internal class ServiceAccountGoogleCredential : GoogleCredential { - private readonly JwtServiceAccountCredential credential; + private readonly ServiceAccountCredential credential; - public JwtServiceAccountGoogleCredential(JwtServiceAccountCredential credential) - : base(credential, credential, credential) + public ServiceAccountGoogleCredential(ServiceAccountCredential credential) + : base(credential) { this.credential = credential; } @@ -187,13 +171,13 @@ public override bool IsCreateScopedRequired public override GoogleCredential CreateScoped(IEnumerable scopes) { - var initializer = new JwtServiceAccountCredential.Initializer(credential.Id) + var initializer = new ServiceAccountCredential.Initializer(credential.Id) { User = credential.User, Key = credential.Key, Scopes = scopes }; - return GoogleCredential.FromCredential(new JwtServiceAccountCredential(initializer)); + return GoogleCredential.FromCredential(new ServiceAccountCredential(initializer)); } #endregion diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs deleted file mode 100644 index cb688fe656..0000000000 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/JwtServiceAccountCredential.cs +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright 2015 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.Threading; -using System.Threading.Tasks; - -using Google.Apis.Util; - -namespace Google.Apis.Auth.OAuth2 -{ - /// - /// Service account credential that also supports JSON Web Token access token scenario. - /// In this scenario, instead of sending a JWT to a token server and exchanging it for - /// an access token, a slightly different JWT containing the request URI is constructed - /// and used as an access token. - /// See for explanation when JWT access token - /// is used and when regular OAuth2 token is used. - /// - /// - public class JwtServiceAccountCredential : ServiceAccountCredential - { - /// Default lifetime of 1 hour - private const int DefaultExpiresInSeconds = 3600; - - /// Constructs a new JWT service account credential using the given initializer. - public JwtServiceAccountCredential(ServiceAccountCredential.Initializer initializer) - : base(initializer) - { - } - - /// - /// Gets an access token to authorize a request. - /// If is set and this credential has no scopes associated - /// with it, a JWT access token for given is returned. - /// Otherwise, the access token will be obtained in exactly the same way as in - /// - /// - /// 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).AccessToken; - } - return await base.GetAccessTokenForRequestAsync(authUri, cancellationToken); - } - - /// - /// 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 JwtAccessToken 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 + DefaultExpiresInSeconds, - }; - - return new JwtAccessToken - { - Issued = issuedDateTime, - ExpiresInSeconds = DefaultExpiresInSeconds, - AccessToken = CreateAssertionFromPayload(payload) - }; - } - - /// JWT access token. - internal class JwtAccessToken - { - /// The date and time that this token was issued. - public DateTime Issued { get; set; } - - /// Gets or sets the lifetime in seconds of the access token. - public long ExpiresInSeconds { get; set; } - - /// Gets or sets the access token. - public string AccessToken { get; set; } - - /// - /// Returns true if the token is expired or it's going to be expired in the next minute. - /// - public bool IsExpired(IClock clock) - { - if (AccessToken == null) - { - return true; - } - - return Issued.AddSeconds(ExpiresInSeconds - 60) <= clock.Now; - } - } - } -} diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs index 0d67dd3c39..05c078ef5d 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs @@ -41,6 +41,14 @@ namespace Google.Apis.Auth.OAuth2 /// /// Take a look in https://developers.google.com/accounts/docs/OAuth2ServiceAccount for more details. /// + /// + /// Service account credential also supports JSON Web Token access token scenario. + /// In this scenario, instead of sending a JWT to a token server and exchanging it for + /// an access token, a slightly different JWT containing the request URI is constructed + /// and used as an access token. + /// See for explanation when JWT access token + /// is used and when regular OAuth2 token is used. + /// /// public class ServiceAccountCredential : ServiceCredential { @@ -170,13 +178,55 @@ public override async Task RequestAccessTokenAsync(CancellationToken taskC 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 its 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); + } + #endregion + + /// + /// 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. - protected string CreateAssertionFromPayload(JsonWebSignature.Payload payload) + private string CreateAssertionFromPayload(JsonWebSignature.Payload payload) { string serializedHeader = CreateSerializedHeader(); string serializedPayload = NewtonsoftJsonSerializer.Instance.Serialize(payload); diff --git a/Src/GoogleApis.Auth/GoogleApis.Auth.csproj b/Src/GoogleApis.Auth/GoogleApis.Auth.csproj index eb377ea6ca..af38d6f9eb 100644 --- a/Src/GoogleApis.Auth/GoogleApis.Auth.csproj +++ b/Src/GoogleApis.Auth/GoogleApis.Auth.csproj @@ -72,6 +72,7 @@ + diff --git a/Src/GoogleApis.Auth/OAuth2/ICredential.cs b/Src/GoogleApis.Auth/OAuth2/ICredential.cs new file mode 100644 index 0000000000..0c5905ca07 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/ICredential.cs @@ -0,0 +1,25 @@ +/* +Copyright 2015 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 Google.Apis.Http; + +namespace Google.Apis.Auth.OAuth2 +{ + /// Functionality common to all credential types. + public interface ICredential : IConfigurableHttpClientInitializer, ITokenAccess + { + } +} diff --git a/Src/GoogleApis.Auth/OAuth2/ITokenAccess.cs b/Src/GoogleApis.Auth/OAuth2/ITokenAccess.cs index f08b5f81be..e66b427974 100644 --- a/Src/GoogleApis.Auth/OAuth2/ITokenAccess.cs +++ b/Src/GoogleApis.Auth/OAuth2/ITokenAccess.cs @@ -34,7 +34,7 @@ public interface ITokenAccess /// Implementations should handle automatic refreshes of the token /// if they are supported. /// The might be required by some credential types - /// (e.g. ) while other credential types + /// (e.g. the JWT access token) while other credential types /// migth just ignore it. /// /// The URI the returned token will grant access to. diff --git a/Src/GoogleApis.Auth/OAuth2/JsonCredentialParameters.cs b/Src/GoogleApis.Auth/OAuth2/JsonCredentialParameters.cs index 0bc2cdb97c..6c0bf07da7 100644 --- a/Src/GoogleApis.Auth/OAuth2/JsonCredentialParameters.cs +++ b/Src/GoogleApis.Auth/OAuth2/JsonCredentialParameters.cs @@ -26,7 +26,7 @@ limitations under the License. namespace Google.Apis.Auth.OAuth2 { /// Holder for credential parameters read from JSON credential file. Fields are union of parameters for all supported credential types. - internal class JsonCredentialParameters + public class JsonCredentialParameters { /// UserCredential is created by the gcloud sdk tool when the user runs gcloud auth login. public const string AuthorizedUserCredentialType = "authorized_user"; diff --git a/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs b/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs index a2ee097304..425ab4069f 100644 --- a/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/ServiceCredential.cs @@ -40,8 +40,7 @@ namespace Google.Apis.Auth.OAuth2 /// https://cloud.google.com/compute/docs/authentication. /// /// - public abstract class ServiceCredential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler, - IConfigurableHttpClientInitializer, ITokenAccess + public abstract class ServiceCredential : ICredential, IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler { protected static readonly ILogger Logger = ApplicationContext.Logger.ForType(); diff --git a/Src/GoogleApis.Auth/OAuth2/UserCredential.cs b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs index e0e93b659c..9dbb7487fe 100644 --- a/Src/GoogleApis.Auth/OAuth2/UserCredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs @@ -31,8 +31,7 @@ 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, ITokenAccess + public class UserCredential : ICredential, IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler { protected static readonly ILogger Logger = ApplicationContext.Logger.ForType(); From 1e78aa3044011bc9210a28034ca3aed623a3f0cf Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Sat, 8 Aug 2015 15:52:59 -0700 Subject: [PATCH 14/16] addressing comments --- .../OAuth2/ServiceAccountCredential.cs | 12 +++++------- Src/GoogleApis.Auth/OAuth2/ICredential.cs | 8 +++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs index 05c078ef5d..a31183e56f 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs @@ -1,5 +1,5 @@ /* -Copyright 2015 Google Inc +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. @@ -42,10 +42,10 @@ namespace Google.Apis.Auth.OAuth2 /// Take a look in https://developers.google.com/accounts/docs/OAuth2ServiceAccount for more details. /// /// - /// Service account credential also supports JSON Web Token access token scenario. - /// In this scenario, instead of sending a JWT to a token server and exchanging it for - /// an access token, a slightly different JWT containing the request URI is constructed - /// and used as an access token. + /// 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. /// @@ -245,7 +245,6 @@ private string CreateAssertionFromPayload(JsonWebSignature.Payload payload) return assertion.ToString(); } - // TODO: fix stale link /// /// Creates a serialized header as specified in /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader. @@ -271,7 +270,6 @@ private static RSAParameters ConvertPKCS8ToRSAParameters(string pkcs8PrivateKey) return DotNetUtilities.ToRSAParameters(crtParameters); } - // TODO: fix stale link /// /// Creates a claim set as specified in /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset. diff --git a/Src/GoogleApis.Auth/OAuth2/ICredential.cs b/Src/GoogleApis.Auth/OAuth2/ICredential.cs index 0c5905ca07..a97a434f44 100644 --- a/Src/GoogleApis.Auth/OAuth2/ICredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/ICredential.cs @@ -18,7 +18,13 @@ limitations under the License. namespace Google.Apis.Auth.OAuth2 { - /// Functionality common to all credential types. + /// + /// The main interface to represent credential in the client library. + /// Service account, User account and Compute credential inherit from this interface + /// to provide access token functionality. In addition this interface inherits from + /// to be able to hook to http requests. + /// More details are available in the specific implementations. + /// public interface ICredential : IConfigurableHttpClientInitializer, ITokenAccess { } From 9994c5a7425f12c3714dd8de1905d8887029de2a Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Sat, 8 Aug 2015 15:57:58 -0700 Subject: [PATCH 15/16] addressing comments --- Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs index f6813d6b74..2713460f90 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs @@ -34,8 +34,10 @@ namespace Google.Apis.Auth.OAuth2 /// public class GoogleCredential : IConfigurableHttpClientInitializer, ITokenAccess { + /// Provider implements the logic for creating the application default credential. private static DefaultCredentialProvider defaultCredentialProvider = new DefaultCredentialProvider(); + /// The underlying credential being wrapped by this object. private readonly ICredential credential; /// Creates a new GoogleCredential. From b6873529a0ac5eb3d418065f40c8f7f04518883b Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Sat, 8 Aug 2015 16:20:46 -0700 Subject: [PATCH 16/16] addressing comments --- .../OAuth2/GoogleCredential.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs index 2713460f90..8723ea291a 100644 --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleCredential.cs @@ -38,7 +38,7 @@ public class GoogleCredential : IConfigurableHttpClientInitializer, ITokenAccess private static DefaultCredentialProvider defaultCredentialProvider = new DefaultCredentialProvider(); /// The underlying credential being wrapped by this object. - private readonly ICredential credential; + protected readonly ICredential credential; /// Creates a new GoogleCredential. internal GoogleCredential(ICredential credential) @@ -153,33 +153,35 @@ internal static GoogleCredential FromCredential(ServiceAccountCredential credent #endregion - /// Wraps ServiceAccountCredential as GoogleCredential. + /// + /// Wraps ServiceAccountCredential as GoogleCredential. + /// We need this subclass because wrapping ServiceAccountCredential (unlike other wrapped credential types) + /// requires special handling for IsCreateScopedRequired and CreateScoped members. + /// internal class ServiceAccountGoogleCredential : GoogleCredential { - private readonly ServiceAccountCredential credential; - public ServiceAccountGoogleCredential(ServiceAccountCredential credential) : base(credential) { - this.credential = credential; } #region GoogleCredential overrides public override bool IsCreateScopedRequired { - get { return !credential.HasScopes; } + get { return !(credential as ServiceAccountCredential).HasScopes; } } public override GoogleCredential CreateScoped(IEnumerable scopes) { - var initializer = new ServiceAccountCredential.Initializer(credential.Id) + var serviceAccountCredential = credential as ServiceAccountCredential; + var initializer = new ServiceAccountCredential.Initializer(serviceAccountCredential.Id) { - User = credential.User, - Key = credential.Key, + User = serviceAccountCredential.User, + Key = serviceAccountCredential.Key, Scopes = scopes }; - return GoogleCredential.FromCredential(new ServiceAccountCredential(initializer)); + return new ServiceAccountGoogleCredential(new ServiceAccountCredential(initializer)); } #endregion