I'm trying to create a JWT to authorize with a service account as described in Google documentation using System.IdentityModel.Tokens.Jwt. I have the following code:
byte[] key = Convert.FromBase64String("...");
var certificate = new X509Certificate2(key, "notasecret");
DateTime now = DateTime.UtcNow;
TimeSpan span = now - UnixEpoch;
Claim[] claims =
{
new Claim("iss", "[email protected]"),
new Claim("scope", "https://www.googleapis.com/auth/plus.me"),
new Claim("aud", "https://accounts.google.com/o/oauth2/token"),
new Claim("iat", span.TotalSeconds.ToString()),
new Claim("exp", span.Add(TimeSpan.FromHours(1)).TotalSeconds.ToString())
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var descriptor = new SecurityTokenDescriptor
{
SigningCredentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(key),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
"http://www.w3.org/2001/04/xmlenc#sha256"),
Subject = new ClaimsIdentity(claims)
};
JwtSecurityToken jwtSecurityToken = (JwtSecurityToken)handler.CreateToken(descriptor);
string json = handler.WriteToken(jwtSecurityToken);
which outputs:
{ "typ" : "JWT" , "alg" : "HS256" }
While Google explicitly states it supports SHA-256:
Service accounts rely on the RSA SHA-256 algorithm and the JWT token format
According to wtSecurityTokenHandler.InboundAlgorithmMap:
RS256 => http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
HS256 => http://www.w3.org/2001/04/xmldsig-more#hmac-sha256
So when I change my code:
new SigningCredentials(
new InMemorySymmetricSecurityKey(key),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2001/04/xmlenc#sha256");
I'm getting an exception:
System.InvalidOperationException: IDX10632: SymmetricSecurityKey.GetKeyedHashAlgorithm( 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' ) threw an exception.
SymmetricSecurityKey: 'System.IdentityModel.Tokens.InMemorySymmetricSecurityKey'
SignatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', check to make sure the SignatureAlgorithm is supported.
Does it mean Microsoft doesn't support the algorithm Google supports exclusively?
It has been a while since this question was asked but I think that for future people coming on this page, it may be worth knowing that it's dead easy to get the same results in a few lines of code with the .NET Google Auth API (whose nuget is available here: Google.Apis.Auth
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
namespace GoogleTest
{
public class GoogleOAuth2
{
/// <summary>
/// Authorization scope for our requests
/// </summary>
private readonly string _defaultScope;
/// <summary>
/// Service account will be of the form [email protected]
/// </summary>
private readonly string _serviceAccount;
/// <summary>
/// Set this to the full path to your service account private key file.
/// </summary>
private readonly string _certificateFile;
public GoogleOAuth2(string defaultScope, string serviceAccount, string certificateFile)
{
_defaultScope = defaultScope;
_serviceAccount = serviceAccount;
_certificateFile = certificateFile;
}
/// <summary>
/// Access Token returned by Google Token Server
/// </summary>
public string AccessToken { get; set; }
public async Task<bool> RequestAccessTokenAsync()
{
var certificate = new X509Certificate2(_certificateFile, "notasecret", X509KeyStorageFlags.Exportable);
var serviceAccountCredential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(_serviceAccount)
{
Scopes = new[] { _defaultScope }
}.FromCertificate(certificate));
var status = await serviceAccountCredential.RequestAccessTokenAsync(CancellationToken.None);
if (status)
AccessToken = serviceAccountCredential.Token.AccessToken;
return status;
}
}
}
To get the Access Token, you just have to call the method RequestAccessTokenAsync and if the result is successful, you've got your token in the AccessToken property.
Note that this implementation assumes that in the developers console, you have exported your private key as a .P12 file.
Hope this answer will help.
private static async Task<string> GetAuthorizationToken(GoogleAuthOptions authOptions)
{
string jwt = CreateJwt(authOptions);
var dic = new Dictionary<string, string>
{
{ "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" },
{ "assertion", jwt }
};
var content = new FormUrlEncodedContent(dic);
var httpClient = new HttpClient { BaseAddress = new Uri("https://accounts.google.com") };
var response = await httpClient.PostAsync("/o/oauth2/token", content);
response.EnsureSuccessStatusCode();
dynamic dyn = await response.Content.ReadAsAsync<dynamic>();
return dyn.access_token;
}
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
private static string CreateJwt(GoogleAuthOptions authOptions)
{
var certificate = new X509Certificate2(Convert.FromBase64String(authOptions.CertificateKey), authOptions.CertificateSecret);
DateTime now = DateTime.UtcNow;
var claimset = new
{
iss = authOptions.Issuer,
scope = "https://www.googleapis.com/auth/plus.me",
aud = authOptions.Audience,
iat = ((int)now.Subtract(UnixEpoch).TotalSeconds).ToString(CultureInfo.InvariantCulture),
exp = ((int)now.AddMinutes(55).Subtract(UnixEpoch).TotalSeconds).ToString(CultureInfo.InvariantCulture)
};
// header
var header = new { typ = "JWT", alg = "RS256" };
// encoded header
var headerSerialized = JsonConvert.SerializeObject(header);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
var headerEncoded = TextEncodings.Base64Url.Encode(headerBytes);
// encoded claimset
var claimsetSerialized = JsonConvert.SerializeObject(claimset);
var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
var claimsetEncoded = TextEncodings.Base64Url.Encode(claimsetBytes);
// input
var input = String.Join(".", headerEncoded, claimsetEncoded);
var inputBytes = Encoding.UTF8.GetBytes(input);
// signiture
var rsa = (RSACryptoServiceProvider)certificate.PrivateKey;
var cspParam = new CspParameters
{
KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
var cryptoServiceProvider = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
var signatureBytes = cryptoServiceProvider.SignData(inputBytes, "SHA256");
var signatureEncoded = TextEncodings.Base64Url.Encode(signatureBytes);
// jwt
return String.Join(".", headerEncoded, claimsetEncoded, signatureEncoded);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With