Skip to content

Commit

Permalink
NetStandard improvements
Browse files Browse the repository at this point in the history
Support ServiceAccountCredentials in netstandard, although without initialization from X509 certificate. Fixes googleapis#797.

googleapis#805 adds tests for valid AccessToken from ServiceAccountCredential.

Fixed signing in netstandard builds so InternalsVisibleTo now works. Enabled all tests in netstandard that previously were disabled due to internal access.
  • Loading branch information
chrisdunelm committed Aug 11, 2016
1 parent c54b9f0 commit 6565630
Show file tree
Hide file tree
Showing 19 changed files with 365 additions and 75 deletions.
Expand Up @@ -49,6 +49,10 @@
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="BouncyCastle.Crypto, Version=1.7.4137.9688, Culture=neutral, PublicKeyToken=a4292a325f69b123, processorArchitecture=MSIL">
<HintPath>..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Moq, Version=4.2.1402.2112, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\packages\Moq.4.2.1402.2112\lib\net40\Moq.dll</HintPath>
<Private>True</Private>
Expand All @@ -66,6 +70,7 @@
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Core" />
<Reference Include="System.Net" />
<Reference Include="System.Net.Http" />
Expand All @@ -79,6 +84,7 @@
<ItemGroup>
<Compile Include="OAuth2\GoogleCredentialTests.cs" />
<Compile Include="OAuth2\DefaultCredentialProviderTests.cs" />
<Compile Include="OAuth2\ServiceAccountCredentialTests.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="..\CommonAssemblyInfo.cs">
Expand All @@ -98,6 +104,10 @@
<Project>{e83c6d98-c348-4944-ad7f-c4e428141079}</Project>
<Name>GoogleApis.Core_Net45</Name>
</ProjectReference>
<ProjectReference Include="..\GoogleApis.Tests\GoogleApis.Tests.csproj">
<Project>{9a8aa9ef-6904-43d8-8a26-0ab62069c2dc}</Project>
<Name>GoogleApis.Tests</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
Expand All @@ -106,6 +116,7 @@
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
Expand Down
@@ -0,0 +1,126 @@
using Google.Apis.Json;
using Google.Apis.Tests;
using NUnit.Framework;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities.IO.Pem;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace Google.Apis.Auth.OAuth2
{
[TestFixture]
public class ServiceAccountCredentialTests
{

[Test]
public async Task ValidLocallySignedAccessToken_FromPrivateKey()
{
const string dummyServiceAccountCredentialFileContents = @"{
""private_key_id"": ""PRIVATE_KEY_ID"",
""private_key"": ""-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJJM6HT4s6btOsfe
2x4zrzrwSUtmtR37XTTi0sPARTDF8uzmXy8UnE5RcVJzEH5T2Ssz/ylX4Sl/CI4L
no1l8j9GiHJb49LSRjWe4Yx936q0Xj9H0R1HTxvjUPqwAsTwy2fKBTog+q1frqc9
o8s2r6LYivUGDVbhuUzCaMJsf+x3AgMBAAECgYEAi0FTXsu/zRswAUGaViQiHjrL
uU65BSHXNVjV/2fLNEKnGWGqpli68z1IXY+S2nwbUak7rnGsq9/0F6jtsW+hZbLk
KXUOuuExpeC5Kd6ngWX/f2jqmhlUabiQijU9cVk7pMq8EHkRtvlosnMTUAEzempu
QUPwn1PZHhmJkBvZ4lECQQDCErrxl+e3BwUDcS0yVEEmCNSG6xdXs2878b8rzbe7
3Mmi6SuuOLi3PU92J+j+f/MOdtYrk13mEDdYmd5dhrt5AkEAwPvDEsDT/W4y4h5n
gv1awGBA5aLFE1JNWM/Gwn4D1cGpEDHKFREaBtxMDCASpHJuw8r7zUywpKhmBZcf
GS37bwJANdSAKfbafLfjuhqwUJ9yGpykZm/a36aTmerp/bpn1iHdg+RtCzwMcDb/
TWSwibbvsflgWmHbz657y4WSWhq+8QJAWrpCNN/ZCk2zuGDo80lfUBAwkoVat8G6
wWU1oZyS+vzIGef+hLb8kHsjeZPej9eIwZ39kcBbT54oELrCkRjwGwJAQ8V2A7lT
ZUp8AsbVqF6rbLiiUfJMo2btGclQu4DEVyS+ymFA65tXDLUuR9EDqJYdqHNZJ5B8
4Z5p2prkjWTLcA==
-----END PRIVATE KEY-----"",
""client_email"": ""CLIENT_EMAIL"",
""client_id"": ""CLIENT_ID"",
""type"": ""service_account""}";

var credentialParameters = NewtonsoftJsonSerializer.Instance.Deserialize<JsonCredentialParameters>(dummyServiceAccountCredentialFileContents);
var initializer = new ServiceAccountCredential.Initializer(credentialParameters.ClientEmail)
{
Clock = new MockClock { UtcNow = new DateTime(2016, 1, 1, 0, 0, 0, DateTimeKind.Utc) }
};
var cred = new ServiceAccountCredential(initializer.FromPrivateKey(credentialParameters.PrivateKey));

Assert.That(cred.Scopes?.Any(), Is.False); // HasScopes must be false for the type of access token we want to test.

string accessToken = await cred.GetAccessTokenForRequestAsync("http://authurl/");

string expectedToken =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJDTElFTlRfRU1BSUwiLCJz" +
"dWIiOiJDTElFTlRfRU1BSUwiLCJhdWQiOiJodHRwOi8vYXV0aHVybC8iLCJleHAiOjE0N" +
"TE2MTAwMDAsImlhdCI6MTQ1MTYwNjQwMH0.WLljSaAqxMVZnAxFA2SvpA3n2WRlQW71Nb" +
"CUkbN-ZI-EWoL-HhgiV_3ISrXMvbDHYhBR0vvtXE0PcRcsMEf51Y0jV4DXZ8hf-QJFq7O" +
"Hrepwe93dnDE6uNVnbj41_0phuy1WKwae29Qp2aPI2Y8E8Z2tXQlF87E_MdgjXVeTF8k";
Assert.That(accessToken, Is.EqualTo(expectedToken));
}

#if !NETSTANDARD // ServiceAccountCredential not currently supported on netstandard.
[Test]
public async Task ValidLocallySignedAccessToken_FromX509Certificate()
{
const string sPrivateKey = @"
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALQoMIfUT93VMvb5
4U5dsJLXD1D6z4o/VuVlNUaf5xH01qAv8Egpxe6f5frLB/p1eNyDjNIZ3QitD6aN
9Vfh4ifg6txBuBJtIMfSDvRJITNZ2SjKJREbflZuBk0HINKYQk2H+3TIzUbE/pN4
Cu6Mids5L/oVOnRWIe3bEC+PHR7ZAgMBAAECgYBI1qrwb+2ukeFeK59lcMnQRLVD
l3RLv9ohOy80E7h38RbJgzhR5NnK5ck1AduC7vXjqihIVf6g4F+ghmq4knI94KPQ
2fOyQGVV6jVeRVqMusx7tP9V1H26yABiK6TPklcCsWwADFmexfOxfIgbBGSmlbAd
N2/ad1Xog1xDebrbEQJBAO+2qSIDX1ahfxUyvvcMaix6Mrh/OYKb5aH1Y/7MfAav
lpbQavQHNvak2147aPNxy0ZvWdJ2HVA7hUMkgysYWSUCQQDAZalkZMSrve+Onh55
U+w5xjLklhh8PhYxx8plT8ae9VG75dGvWSz/W81B3ILg2lrNU6o08NKBJdZsJYmc
BeKlAkBtJh3zF9gEaTqlW1rqwKNjpyyLJ5r3JqczzLmAXnmmzbLi7vmULejP+5bL
XH/YQZtOcgtTMmb8jm2Kegijyc1lAkANGt+e5v4+dIGMxVhuCzlb9hQhXdftHo2E
dodivzxYN32Jvu25c+mMu0QP6GVBy53Dvp8pW/36rgkc9LGa3wvBAkEA7dGAl95T
UeNFVfuzkYNtQppcSgrx1oTcpTHoNgcvk8AgBf4yDdJJyo0IUONmgJCVffc1aTWn
nf/8cW9YC+0icQ==";
const string sCert = @"
MIICNDCCAZ2gAwIBAgIJAKl3qU1+NsuuMA0GCSqGSIb3DQEBCwUAMDMxCzAJBgNV
BAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMQ8wDQYDVQQKDAZHb29nbGUwHhcN
MTYwODEwMTQwOTQ2WhcNNDMxMjI3MTQwOTQ2WjAzMQswCQYDVQQGEwJHQjETMBEG
A1UECAwKU29tZS1TdGF0ZTEPMA0GA1UECgwGR29vZ2xlMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQC0KDCH1E/d1TL2+eFOXbCS1w9Q+s+KP1blZTVGn+cR9Nag
L/BIKcXun+X6ywf6dXjcg4zSGd0IrQ+mjfVX4eIn4OrcQbgSbSDH0g70SSEzWdko
yiURG35WbgZNByDSmEJNh/t0yM1GxP6TeArujInbOS/6FTp0ViHt2xAvjx0e2QID
AQABo1AwTjAdBgNVHQ4EFgQUMqzJi099PA8ML5CV1OSiHgiTGoUwHwYDVR0jBBgw
FoAUMqzJi099PA8ML5CV1OSiHgiTGoUwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
AQsFAAOBgQBQ9cMInb2rEcg8TTYq8MjDEegHWLUI9Dq/IvP/FHyKDczza4eX8m+G
3buutN74pX2/GHRgqvqEvvUUuAQUnZ36k6KjTNxfzNLiSXDPNeYmy6PWsUZy4Rye
/Van/ePiXdipTKMiUyl7V6dTjkE5p/e372wNVXUpcxOMmYdWmzSMvg==";

var privateParams = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(sPrivateKey));
var x509Cert = new X509Certificate2(Convert.FromBase64String(sCert));
var rsaParameters = DotNetUtilities.ToRSAParameters(privateParams);
var privateKey = new System.Security.Cryptography.RSACryptoServiceProvider();
privateKey.ImportParameters(rsaParameters);
x509Cert.PrivateKey = privateKey;

var initializer = new ServiceAccountCredential.Initializer("some-id")
{
Clock = new MockClock { UtcNow = new DateTime(2016, 1, 1, 0, 0, 0, DateTimeKind.Utc) }
};
var cred = new ServiceAccountCredential(initializer.FromCertificate(x509Cert));

Assert.That(cred.Scopes?.Any(), Is.False); // HasScopes must be false for the type of access token we want to test.

string accessToken = await cred.GetAccessTokenForRequestAsync("http://authurl/");
System.IO.File.WriteAllText(@"C:\Users\chrisbacon\zz.txt", accessToken);

string expectedToken =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzb21lLWlkIiwic3ViIjoi" +
"c29tZS1pZCIsImF1ZCI6Imh0dHA6Ly9hdXRodXJsLyIsImV4cCI6MTQ1MTYxMDAwMCwia" +
"WF0IjoxNDUxNjA2NDAwfQ.GfpDHgrFi4ZlGC5LuJEarLU4_eTrT5PVa-S40YtkdB2E1f3" +
"4naYG2ItcfBEFg7Gbdkr1cIAyipuhEd2yLfPmWGwhOwVcBRNyK_J5w8RodS44mxNJwau0" +
"jKy4x1K20ybLqcnNgzE0wag6fi5GHwdNIB0URdHDTiC88CRYdl1CIdk";
Assert.That(accessToken, Is.EqualTo(expectedToken));
}
#endif
}
}
4 changes: 3 additions & 1 deletion Src/Support/GoogleApis.Auth.DotNet4.Tests/Program.cs
Expand Up @@ -21,6 +21,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ***********************************************************************

#if !NETSTANDARD
using NUnitLite;

namespace NUnitLite.Tests
Expand All @@ -38,4 +39,5 @@ public static int Main(string[] args)
return new AutoRun().Execute(args);
}
}
}
}
#endif
1 change: 1 addition & 0 deletions Src/Support/GoogleApis.Auth.DotNet4.Tests/packages.config
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="BouncyCastle" version="1.7.0" targetFramework="net45" />
<package id="Moq" version="4.2.1402.2112" targetFramework="net45" />
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" />
<package id="NUnit" version="3.2.1" targetFramework="net45" />
Expand Down
Expand Up @@ -56,6 +56,7 @@
<Compile Include="..\CommonAssemblyInfo.cs">
<Link>Properties\CommonAssemblyInfo.cs</Link>
</Compile>
<Compile Include="OAuth2\RsaStandard.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
Expand All @@ -68,7 +69,6 @@
<Name>GoogleApis.Core_NetStandard</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<Import Project="..\GoogleApis.Auth.PlatformServices_Shared\GoogleApis.Auth.PlatformServices_Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Expand Down
77 changes: 77 additions & 0 deletions Src/Support/GoogleApis.Auth.NetStandard/OAuth2/RsaStandard.cs
@@ -0,0 +1,77 @@
using Org.BouncyCastle.Asn1.Nist;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using System;
using System.IO;
using System.Security.Cryptography;

namespace Google.Apis.Auth.OAuth2
{
/// <summary>
/// An implementation of RSA, just sufficient for the needs of ServiceAccountCredential.
/// This is not a general-purpose implementation of RSA.
/// </summary>
internal class RsaStandard : RSA
{
public RsaStandard(RsaPrivateCrtKeyParameters parameters)
{
_parameters = parameters;
}

private RsaPrivateCrtKeyParameters _parameters;

public override byte[] Decrypt(byte[] data, RSAEncryptionPadding padding)
{
throw new NotImplementedException();
}

public override byte[] Encrypt(byte[] data, RSAEncryptionPadding padding)
{
throw new NotImplementedException();
}

public override RSAParameters ExportParameters(bool includePrivateParameters)
{
throw new NotImplementedException();
}

public override void ImportParameters(RSAParameters parameters)
{
throw new NotImplementedException();
}

public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
{
if (hashAlgorithm != HashAlgorithmName.SHA256)
{
throw new ArgumentException(
$"Unsupported HashAlgorithmName '{hashAlgorithm}', only SHA256 supported.", nameof(hashAlgorithm));
}
if (padding != RSASignaturePadding.Pkcs1)
{
throw new ArgumentException(
$"Unsupported RSASignaturePadding '{padding}', only Pkcs1 supported.", nameof(padding));
}
var signer = new RsaDigestSigner(new NullDigest(), NistObjectIdentifiers.IdSha256);
signer.Init(true, _parameters);
signer.BlockUpdate(hash, 0, hash.Length);
return signer.GenerateSignature();
}

public override bool VerifyHash(byte[] hash, byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
{
throw new NotImplementedException();
}

protected override byte[] HashData(Stream data, HashAlgorithmName hashAlgorithm)
{
throw new NotImplementedException();
}

protected override byte[] HashData(byte[] data, int offset, int count, HashAlgorithmName hashAlgorithm)
{
throw new NotImplementedException();
}
}
}
12 changes: 12 additions & 0 deletions Src/Support/GoogleApis.Auth.NetStandard/Properties/AssemblyInfo.cs
Expand Up @@ -15,5 +15,17 @@
*/

using System.Reflection;
using System.Runtime.CompilerServices;

[assembly: AssemblyTitle("Google.Apis.Auth.PlatformServices")]

#if SIGNED
[assembly: InternalsVisibleTo("GoogleApis.Auth.Tests_dotnetcore,PublicKey=" +
"00240000048000009400000006020000002400005253413100040000010001003d69fa08add2ea" +
"7341cc102edb2f3a59bb49e7f7c8bf1bd96d494013c194f4d80ee30278f20e08a0b7cb863d6522" +
"d8c1c0071dd36748297deefeb99e899e6a80b9ddc490e88ea566d2f7d4f442211f7beb6b2387fb" +
"435bfaa3ecfe7afc0184cc46f80a5866e6bb8eb73f64a3964ed82f6a5036b91b1ac93e1f44508b" +
"65e51fce")]
#else
[assembly: InternalsVisibleTo("GoogleApis.Auth.Tests_dotnetcore")]
#endif
Expand Up @@ -182,14 +182,9 @@ private static GoogleCredential CreateDefaultCredentialFromJson(JsonCredentialPa
{
case JsonCredentialParameters.AuthorizedUserCredentialType:
return new GoogleCredential(CreateUserCredentialFromJson(credentialParameters));
#if !NETSTANDARD
case JsonCredentialParameters.ServiceAccountCredentialType:
return GoogleCredential.FromCredential(
CreateServiceAccountCredentialFromJson(credentialParameters));
#else
case JsonCredentialParameters.ServiceAccountCredentialType:
throw new InvalidOperationException("Service Account credentials are not supported in .NET Core.");
#endif
default:
throw new InvalidOperationException(
String.Format("Error creating credential from JSON. Unrecognized credential type {0}.",
Expand Down Expand Up @@ -224,7 +219,6 @@ private static UserCredential CreateUserCredentialFromJson(JsonCredentialParamet
return new UserCredential(flow, "ApplicationDefaultCredentials", token);
}

#if !NETSTANDARD
/// <summary>Creates a <see cref="ServiceAccountCredential"/> from JSON data.</summary>
private static ServiceAccountCredential CreateServiceAccountCredentialFromJson(
JsonCredentialParameters credentialParameters)
Expand All @@ -238,7 +232,6 @@ private static UserCredential CreateUserCredentialFromJson(JsonCredentialParamet
var initializer = new ServiceAccountCredential.Initializer(credentialParameters.ClientEmail);
return new ServiceAccountCredential(initializer.FromPrivateKey(credentialParameters.PrivateKey));
}
#endif

/// <summary>
/// Returns platform-specific well known credential file path. This file is created by
Expand Down
Expand Up @@ -148,38 +148,25 @@ public GoogleCredential CreateScoped(params string[] scopes)
return CreateScoped((IEnumerable<string>) scopes);
}

#region IConfigurableHttpClientInitializer

void IConfigurableHttpClientInitializer.Initialize(ConfigurableHttpClient httpClient)
{
credential.Initialize(httpClient);
}

#endregion

#region ITokenAccess

Task<string> ITokenAccess.GetAccessTokenForRequestAsync(string authUri, CancellationToken cancellationToken)
{
return credential.GetAccessTokenForRequestAsync(authUri, cancellationToken);
}

#endregion

/// <summary>Provides access to the underlying credential object</summary>
internal ICredential UnderlyingCredential { get { return credential; } }

#if !NETSTANDARD
#region Factory methods

/// <summary>Creates a <c>GoogleCredential</c> wrapping a <see cref="ServiceAccountCredential"/>.</summary>
internal static GoogleCredential FromCredential(ServiceAccountCredential credential)
{
return new ServiceAccountGoogleCredential(credential);
}

#endregion

/// <summary>
/// Wraps <c>ServiceAccountCredential</c> as <c>GoogleCredential</c>.
/// We need this subclass because wrapping <c>ServiceAccountCredential</c> (unlike other wrapped credential
Expand All @@ -190,8 +177,6 @@ internal class ServiceAccountGoogleCredential : GoogleCredential
public ServiceAccountGoogleCredential(ServiceAccountCredential credential)
: base(credential) { }

#region GoogleCredential overrides

public override bool IsCreateScopedRequired
{
get { return !(credential as ServiceAccountCredential).HasScopes; }
Expand All @@ -208,9 +193,6 @@ public override GoogleCredential CreateScoped(IEnumerable<string> scopes)
};
return new ServiceAccountGoogleCredential(new ServiceAccountCredential(initializer));
}

#endregion
}
#endif
}
}

0 comments on commit 6565630

Please sign in to comment.