I feel like I'm taking crazy pills here. Usually there's always a million library and samples floating around the web for any given task. I'm trying to implement authentication with a Google "Service Account" by use of JSON Web Tokens (JWT) as described here.
However there is only client libraries in PHP, Python, and Java. Even searching for JWT examples outside of Google's authentication, there is only crickets and drafts on the JWT concept. Is this really so new and possibly a Google proprietary system?
The java sample which is the closest I could manage to interpret looks pretty intensive and intimidating. There's got to be something out there in C# that I could at least start with. Any help with this would be great!
I found a base implementation of a Json Web Token and expanded on it with the Google flavor. I still haven't gotten it completely worked out but it's 97% there. This project lost it's steam, so hopefully this will help someone else get a good head-start:
Note:
Changes I made to the base implementation (Can't remember where I found it,) are:
Changed HS256 -> RS256
Swapped the JWT and alg order in the header. Not sure who got it wrong, Google or the spec, but google takes it the way It is below according to their docs.
public enum JwtHashAlgorithm
{
RS256,
HS384,
HS512
}
public class JsonWebToken
{
private static Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>> HashAlgorithms;
static JsonWebToken()
{
HashAlgorithms = new Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>>
{
{ JwtHashAlgorithm.RS256, (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } },
{ JwtHashAlgorithm.HS384, (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } },
{ JwtHashAlgorithm.HS512, (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } }
};
}
public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
{
return Encode(payload, Encoding.UTF8.GetBytes(key), algorithm);
}
public static string Encode(object payload, byte[] keyBytes, JwtHashAlgorithm algorithm)
{
var segments = new List<string>();
var header = new { alg = algorithm.ToString(), typ = "JWT" };
byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));
//byte[] payloadBytes = Encoding.UTF8.GetBytes(#"{"iss":"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5#developer.gserviceaccount.com","scope":"https://www.googleapis.com/auth/prediction","aud":"https://accounts.google.com/o/oauth2/token","exp":1328554385,"iat":1328550785}");
segments.Add(Base64UrlEncode(headerBytes));
segments.Add(Base64UrlEncode(payloadBytes));
var stringToSign = string.Join(".", segments.ToArray());
var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
byte[] signature = HashAlgorithms[algorithm](keyBytes, bytesToSign);
segments.Add(Base64UrlEncode(signature));
return string.Join(".", segments.ToArray());
}
public static string Decode(string token, string key)
{
return Decode(token, key, true);
}
public static string Decode(string token, string key, bool verify)
{
var parts = token.Split('.');
var header = parts[0];
var payload = parts[1];
byte[] crypto = Base64UrlDecode(parts[2]);
var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
var headerData = JObject.Parse(headerJson);
var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
var payloadData = JObject.Parse(payloadJson);
if (verify)
{
var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
var keyBytes = Encoding.UTF8.GetBytes(key);
var algorithm = (string)headerData["alg"];
var signature = HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign);
var decodedCrypto = Convert.ToBase64String(crypto);
var decodedSignature = Convert.ToBase64String(signature);
if (decodedCrypto != decodedSignature)
{
throw new ApplicationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
}
}
return payloadData.ToString();
}
private static JwtHashAlgorithm GetHashAlgorithm(string algorithm)
{
switch (algorithm)
{
case "RS256": return JwtHashAlgorithm.RS256;
case "HS384": return JwtHashAlgorithm.HS384;
case "HS512": return JwtHashAlgorithm.HS512;
default: throw new InvalidOperationException("Algorithm not supported.");
}
}
// from JWT spec
private static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
// from JWT spec
private static byte[] Base64UrlDecode(string input)
{
var output = input;
output = output.Replace('-', '+'); // 62nd char of encoding
output = output.Replace('_', '/'); // 63rd char of encoding
switch (output.Length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: output += "=="; break; // Two pad chars
case 3: output += "="; break; // One pad char
default: throw new System.Exception("Illegal base64url string!");
}
var converted = Convert.FromBase64String(output); // Standard base64 decoder
return converted;
}
}
And then my google specific JWT class:
public class GoogleJsonWebToken
{
public static string Encode(string email, string certificateFilePath)
{
var utc0 = new DateTime(1970,1,1,0,0,0,0, DateTimeKind.Utc);
var issueTime = DateTime.Now;
var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
var exp = (int)issueTime.AddMinutes(55).Subtract(utc0).TotalSeconds; // Expiration time is up to 1 hour, but lets play on safe side
var payload = new
{
iss = email,
scope = "https://www.googleapis.com/auth/gan.readonly",
aud = "https://accounts.google.com/o/oauth2/token",
exp = exp,
iat = iat
};
var certificate = new X509Certificate2(certificateFilePath, "notasecret");
var privateKey = certificate.Export(X509ContentType.Cert);
return JsonWebToken.Encode(payload, privateKey, JwtHashAlgorithm.RS256);
}
}
I've never used it but there is a JWT implementation on NuGet.
Package: https://nuget.org/packages/JWT
Source: https://github.com/johnsheehan/jwt
.NET 4.0 compatible: https://www.nuget.org/packages/jose-jwt/
You can also go here: https://jwt.io/ and click "libraries".
This is my implementation of (Google) JWT Validation in .NET.
It is based on other implementations on Stack Overflow and GitHub gists.
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace QuapiNet.Service
{
public class JwtTokenValidation
{
public async Task<Dictionary<string, X509Certificate2>> FetchGoogleCertificates()
{
using (var http = new HttpClient())
{
var response = await http.GetAsync("https://www.googleapis.com/oauth2/v1/certs");
var dictionary = await response.Content.ReadAsAsync<Dictionary<string, string>>();
return dictionary.ToDictionary(x => x.Key, x => new X509Certificate2(Encoding.UTF8.GetBytes(x.Value)));
}
}
private string CLIENT_ID = "xxx.apps.googleusercontent.com";
public async Task<ClaimsPrincipal> ValidateToken(string idToken)
{
var certificates = await this.FetchGoogleCertificates();
TokenValidationParameters tvp = new TokenValidationParameters()
{
ValidateActor = false, // check the profile ID
ValidateAudience = true, // check the client ID
ValidAudience = CLIENT_ID,
ValidateIssuer = true, // check token came from Google
ValidIssuers = new List<string> { "accounts.google.com", "https://accounts.google.com" },
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
IssuerSigningKeys = certificates.Values.Select(x => new X509SecurityKey(x)),
IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
{
return certificates
.Where(x => x.Key.ToUpper() == kid.ToUpper())
.Select(x => new X509SecurityKey(x.Value));
},
ValidateLifetime = true,
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromHours(13)
};
JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler();
SecurityToken validatedToken;
ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken);
return cp;
}
}
}
Note that, in order to use it, you need to add a reference to the NuGet package System.Net.Http.Formatting.Extension. Without this, the compiler will not recognize the ReadAsAsync<> method.
It would be better to use standard and famous libraries instead of writing the code from scratch.
JWT for encoding and decoding JWT tokens
Bouncy Castle supports encryption and decryption, especially RS256 get it here
Using these libraries you can generate a JWT token and sign it using RS256 as below.
public string GenerateJWTToken(string rsaPrivateKey)
{
var rsaParams = GetRsaParameters(rsaPrivateKey);
var encoder = GetRS256JWTEncoder(rsaParams);
// create the payload according to the Google's doc
var payload = new Dictionary<string, object>
{
{ "iss", ""},
{ "sub", "" },
// and other key-values according to the doc
};
// add headers. 'alg' and 'typ' key-values are added automatically.
var header = new Dictionary<string, object>
{
{ "kid", "{your_private_key_id}" },
};
var token = encoder.Encode(header,payload, new byte[0]);
return token;
}
private static IJwtEncoder GetRS256JWTEncoder(RSAParameters rsaParams)
{
var csp = new RSACryptoServiceProvider();
csp.ImportParameters(rsaParams);
var algorithm = new RS256Algorithm(csp, csp);
var serializer = new JsonNetSerializer();
var urlEncoder = new JwtBase64UrlEncoder();
var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
return encoder;
}
private static RSAParameters GetRsaParameters(string rsaPrivateKey)
{
var byteArray = Encoding.ASCII.GetBytes(rsaPrivateKey);
using (var ms = new MemoryStream(byteArray))
{
using (var sr = new StreamReader(ms))
{
// use Bouncy Castle to convert the private key to RSA parameters
var pemReader = new PemReader(sr);
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
return DotNetUtilities.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
}
}
}
ps: the RSA private key should have the following format:
-----BEGIN RSA PRIVATE KEY-----
{base64 formatted value}
-----END RSA PRIVATE KEY-----
Here is another REST-only working example for Google Service Accounts accessing G Suite Users and Groups, authenticating through JWT. This was only possible through reflection of Google libraries, since Google documentation of these APIs are beyond terrible. Anyone used to code in MS technologies will have a hard time figuring out how everything goes together in Google services.
$iss = "<name>#<serviceaccount>.iam.gserviceaccount.com"; # The email address of the service account.
$sub = "impersonate.user#mydomain.com"; # The user to impersonate (required).
$scope = "https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/admin.directory.group.readonly";
$certPath = "D:\temp\mycertificate.p12";
$grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
# Auxiliary functions
function UrlSafeEncode([String] $Data) {
return $Data.Replace("=", [String]::Empty).Replace("+", "-").Replace("/", "_");
}
function UrlSafeBase64Encode ([String] $Data) {
return (UrlSafeEncode -Data ([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Data))));
}
function KeyFromCertificate([System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
$privateKeyBlob = $Certificate.PrivateKey.ExportCspBlob($true);
$key = New-Object System.Security.Cryptography.RSACryptoServiceProvider;
$key.ImportCspBlob($privateKeyBlob);
return $key;
}
function CreateSignature ([Byte[]] $Data, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
$sha256 = [System.Security.Cryptography.SHA256]::Create();
$key = (KeyFromCertificate $Certificate);
$assertionHash = $sha256.ComputeHash($Data);
$sig = [Convert]::ToBase64String($key.SignHash($assertionHash, "2.16.840.1.101.3.4.2.1"));
$sha256.Dispose();
return $sig;
}
function CreateAssertionFromPayload ([String] $Payload, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) {
$header = #"
{"alg":"RS256","typ":"JWT"}
"#;
$assertion = New-Object System.Text.StringBuilder;
$assertion.Append((UrlSafeBase64Encode $header)).Append(".").Append((UrlSafeBase64Encode $Payload)) | Out-Null;
$signature = (CreateSignature -Data ([System.Text.Encoding]::ASCII.GetBytes($assertion.ToString())) -Certificate $Certificate);
$assertion.Append(".").Append((UrlSafeEncode $signature)) | Out-Null;
return $assertion.ToString();
}
$baseDateTime = New-Object DateTime(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc);
$timeInSeconds = [Math]::Truncate([DateTime]::UtcNow.Subtract($baseDateTime).TotalSeconds);
$jwtClaimSet = #"
{"scope":"$scope","email_verified":false,"iss":"$iss","sub":"$sub","aud":"https://oauth2.googleapis.com/token","exp":$($timeInSeconds + 3600),"iat":$timeInSeconds}
"#;
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, "notasecret", [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable);
$jwt = CreateAssertionFromPayload -Payload $jwtClaimSet -Certificate $cert;
# Retrieve the authorization token.
$authRes = Invoke-WebRequest -Uri "https://oauth2.googleapis.com/token" -Method Post -ContentType "application/x-www-form-urlencoded" -UseBasicParsing -Body #"
assertion=$jwt&grant_type=$([Uri]::EscapeDataString($grantType))
"#;
$authInfo = ConvertFrom-Json -InputObject $authRes.Content;
$resUsers = Invoke-WebRequest -Uri "https://www.googleapis.com/admin/directory/v1/users?domain=<required_domain_name_dont_trust_google_documentation_on_this>" -Method Get -Headers #{
"Authorization" = "$($authInfo.token_type) $($authInfo.access_token)"
}
$users = ConvertFrom-Json -InputObject $resUsers.Content;
$users.users | ft primaryEmail, isAdmin, suspended;
Here is the list of classes and functions:
open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging
open Microsoft.AspNetCore.Authorization
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.JwtBearer
open Microsoft.IdentityModel.Tokens
open System.IdentityModel.Tokens
open System.IdentityModel.Tokens.Jwt
open Microsoft.IdentityModel.JsonWebTokens
open System.Text
open Newtonsoft.Json
open System.Security.Claims
let theKey = "VerySecretKeyVerySecretKeyVerySecretKey"
let securityKey = SymmetricSecurityKey(Encoding.UTF8.GetBytes(theKey))
let credentials = SigningCredentials(securityKey, SecurityAlgorithms.RsaSsaPssSha256)
let expires = DateTime.UtcNow.AddMinutes(123.0) |> Nullable
let token = JwtSecurityToken(
"lahoda-pro-issuer",
"lahoda-pro-audience",
claims = null,
expires = expires,
signingCredentials = credentials
)
let tokenString = JwtSecurityTokenHandler().WriteToken(token)
Using System.Security.Cryptography.RSA I've modified John Sheehan's JWT library code that was expanded by #Levitikon to use RS256, RSASSA-PKCS1-v1_5 with the SHA-256 hash algorithm
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
/// <summary>
/// adapted from<br/>
/// https://github.com/jwt-dotnet/jwt<br/>
/// https://stackoverflow.com/questions/10055158/is-there-any-json-web-token-jwt-example-in-c<br/>
/// https://stackoverflow.com/a/10106800/6620171<br/>
/// <br/>
/// JSON Web Token (JWT) is a compact, URL-safe means of representing<br/>
/// claims to be transferred between two parties. The claims in a JWT<br/>
/// are encoded as a JSON object that is used as the payload of a JSON<br/>
/// Web Signature (JWS) structure or as the plaintext of a JSON Web<br/>
/// Encryption(JWE) structure, enabling the claims to be digitally<br/>
/// signed or integrity protected with a Message Authentication Code<br/>
/// (MAC) and/or encrypted.<br/>
/// <br/>
/// https://www.rfc-editor.org/rfc/rfc7519
/// </summary>
internal class JsonWebToken
{
/// <summary>
/// JWS uses cryptographic algorithms to digitally sign or create a MAC<br/>
/// of the contents of the JWS Protected Header and the JWS Payload.<br/>
/// <br/>
/// https://www.rfc-editor.org/rfc/rfc7518#section-3
/// </summary>
public enum JwsAlgorythm
{
/// <summary>
/// RSASSA-PKCS1-v1_5 using SHA-256<br/>
/// This section defines the use of the RSASSA-PKCS1-v1_5 digital<br/>
/// signature algorithm as defined in Section 8.2 of RFC 3447 [RFC3447]<br/>
/// (commonly known as PKCS #1), using SHA-2 [SHS] hash functions.<br/>
/// The RSASSA-PKCS1-v1_5 SHA-256 digital signature is generated as<br/>
/// follows: generate a digital signature of the JWS Signing Input using<br/>
/// RSASSA-PKCS1-v1_5-SIGN and the SHA-256 hash function with the desired<br/>
/// private key. This is the JWS Signature value.<br/>
/// <br/>
/// https://www.rfc-editor.org/rfc/rfc7518#section-3.3<br/>
/// https://www.rfc-editor.org/rfc/rfc3447#section-8.2<br/>
/// https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
/// </summary>
RS256,
/// <summary>
/// No digital signature or MAC performed<br/>
/// JWSs MAY also be created that do not provide integrity protection.<br/>
/// Such a JWS is called an Unsecured JWS. An Unsecured JWS uses the<br/>
/// "alg" value "none" and is formatted identically to other JWSs, but<br/>
/// MUST use the empty octet sequence as its JWS Signature value.<br/>
/// Recipients MUST verify that the JWS Signature value is the empty<br/>
/// octet sequence.<br/>
/// <br/>
/// https://www.rfc-editor.org/rfc/rfc7518#section-3.6<br/>
/// https://www.rfc-editor.org/rfc/rfc7519#section-6
/// </summary>
none
}
public static string Encode(object payload, JwsAlgorythm algo, RSA rsa)
{
if (payload == null) { throw new ArgumentNullException("payload"); }
if (algo != JwsAlgorythm.RS256 && algo != JwsAlgorythm.none) { throw new ArgumentException("Invalid JwsAlgorythm specified"); }
if (rsa == null && algo == JwsAlgorythm.RS256) { throw new ArgumentNullException("Encoding of secured JWT requires an RSA object"); }
List<string> segments = new List<string>();
var header = new { typ = "JWT", alg = algo.ToString() };
string strHeader = JsonConvert.SerializeObject(header, Formatting.None);
string strPayload = JsonConvert.SerializeObject(payload, Formatting.None);
byte[] headerBytes = Encoding.UTF8.GetBytes(strHeader);
byte[] payloadBytes = Encoding.UTF8.GetBytes(strPayload);
segments.Add(Base64UrlEncode(headerBytes));
segments.Add(Base64UrlEncode(payloadBytes));
if (algo == JwsAlgorythm.none)
{
segments.Add(string.Empty);
return string.Join(".", segments.ToArray());
}
string stringToSign = string.Join(".", segments.ToArray());
byte[] bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
byte[] signature = rsa.SignData(bytesToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
segments.Add(Base64UrlEncode(signature));
return string.Join(".", segments.ToArray());
}
public static Tuple<string, string> Decode(string token, JwsAlgorythm algo, bool verify, RSA rsa = null)
{
if (algo != JwsAlgorythm.RS256 && algo != JwsAlgorythm.none) { throw new ArgumentException("Invalid JwsAlgorythm specified"); }
if (verify && rsa == null && algo == JwsAlgorythm.RS256) { throw new ArgumentNullException("Verification of secured JWT requires an RSA object"); }
string[] parts = token.Split('.');
string header = parts[0];
string payload = parts[1];
byte[] crypto = Base64UrlDecode(parts[2]);
string headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
JObject headerData = JObject.Parse(headerJson);
string payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
JObject payloadData = JObject.Parse(payloadJson);
if (verify)
{
if (algo == JwsAlgorythm.none)
{
if (crypto.Length != 0)
{
throw new ApplicationException(string.Format("Invalid signature"));
}
}
else if (algo == JwsAlgorythm.RS256)
{
byte[] bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
bool valid = rsa.VerifyData(bytesToSign, crypto, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
if (!valid)
{
throw new ApplicationException(string.Format("Invalid signature"));
}
}
}
return new Tuple<string, string>(headerData.ToString(), payloadData.ToString());
}
// from JWT spec
private static string Base64UrlEncode(byte[] input)
{
string output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
// from JWT spec
private static byte[] Base64UrlDecode(string input)
{
string output = input;
output = output.Replace('-', '+'); // 62nd char of encoding
output = output.Replace('_', '/'); // 63rd char of encoding
switch (output.Length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: output += "=="; break; // Two pad chars
case 3: output += "="; break; // One pad char
default: throw new Exception("Invalid base64url string");
}
byte[] converted = Convert.FromBase64String(output); // Standard base64 decoder
return converted;
}
}
Usage:
X509Certificate2 cert = new X509Certificate2("C:\\test\\keypair.pfx", "notasecret");
long secsSinceEpoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var jwt = new
{
exp = secsSinceEpoch + 600,
iss = "my client id",
aud = "h++ps://webapi.com",
sub = "my subscriber id",
iat = secsSinceEpoch,
nbf = secsSinceEpoch,
jti = RandomString(21),
};
string jwtEncoded = JsonWebToken.Encode(jwt, JsonWebToken.JwsAlgorythm.RS256, cert.GetRSAPrivateKey());
Tuple<string, string> jwtDecoded = JsonWebToken.Decode(jwtEncoded, JsonWebToken.JwsAlgorythm.RS256, true, cert.GetRSAPublicKey());
Console.WriteLine(jwtDecoded);
Output:
({
"typ": "JWT",
"alg": "RS256"
}, {
"exp": 1668732075,
"iss": "my client id",
"aud": "h++ps://webapi.com",
"sub": "my subscriber id",
"iat": 1668731475,
"nbf": 1668731475,
"jti": "tCUpk2i5bNBVBcj7LzV5U"
})
Related
Ι try to create a valid jwt token
From settings i create an RSA keypairs and i get the private key without the "-----BEGIN RSA PRIVATE KEY----------END RSA PRIVATE KEY-----
"
var rsaPrivateKey = #"MIIEogIBAAKCAQEAoGujdXbYVy68a4CSWz963SpYxVs20/..............HQ/jW8pFom6gJreCDkca5axYo/gXp3W3rQHFTkooTNbOk2MyFMZUqRD3aCG1wuUW3w8TgGX4slrLDV0pP4=";
var jwt = Sign(rsaPrivateKey);
I follow the instructions here https://developers.docusign.com/docs/admin-api/admin101/application-auth/ and after a lot of hours i create this method
public string Sign(string privateKey)
{
List<string> segments = new List<string>();
var header = new { alg = "RS256", typ = "JWT" };
//For production environments, use account.docusign.com
var payload = new
{
iss = "4f489d61-dc8b------a828-3992e670dcbc",
iat = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds,
aud = "account-d.docusign.com",
scope = "signature impersonation"
};
byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));
segments.Add(Base64UrlEncode(headerBytes));
segments.Add(Base64UrlEncode(payloadBytes));
string stringToSign = string.Join(".", segments.ToArray());
byte[] bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
byte[] keyBytes = Convert.FromBase64String(privateKey);
var privKeyObj = Asn1Object.FromByteArray(keyBytes);
var privStruct = RsaPrivateKeyStructure.GetInstance((Asn1Sequence)privKeyObj);
ISigner sig = SignerUtilities.GetSigner("SHA256withRSA");
sig.Init(true, new RsaKeyParameters(true, privStruct.Modulus, privStruct.PrivateExponent));
sig.BlockUpdate(bytesToSign, 0, bytesToSign.Length);
byte[] signature = sig.GenerateSignature();
segments.Add(Base64UrlEncode(signature));
return string.Join(".", segments.ToArray());
}
private static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
When i check the JWT validation in this tool https://jwt.io/#debugger-io, i get invalid signature error.
How can i fix the token ?? I cant proceed with Step 2 Obtain the access token...
I'm sorry you having problems with JWT. I would recommend you use the DocuSign C# SDK instead of trying to write your own code.
Then you can find the example of how to use JWT here - https://github.com/docusign/code-examples-csharp.
The specific code relevant to JWT is here - https://github.com/docusign/code-examples-csharp/blob/38c2eb46948a3cbf55edcce758f88d775f80cae9/launcher-csharp/Common/RequestItemService.cs under the UpdateUserFromJWT() method.
Common problems with JWT:
Not obtaining consent.
Using public token instead of private.
Using malform token. Token must be exactly, including new-lines, as provided.
Not using correct UserId (GUID) in the request.
Not requesting "impersonation" scope in consent (#1 above).
I'm having trouble getting my .NET Core client to generate OAuth access tokens for a salesforce endpoint that requires OAuth of type 'JWT Bearer Flow'.
It seems there are limited .NET Framework examples that show a .NET client doing this, however none that show a .NET Core client doing it
e.g.
https://salesforce.stackexchange.com/questions/53662/oauth-jwt-token-bearer-flow-returns-invalid-client-credentials
So in my .NET Core 3.1 app i've generated a self signed certificate, added the private key to the above example's code when loading in the certificate, however a System.InvalidCastExceptionexception exception occurs on this line:
var rsa = certificate.GetRSAPrivateKey() as RSACryptoServiceProvider;
Exception:
System.InvalidCastException: 'Unable to cast object of type 'System.Security.Cryptography.RSACng' to type 'System.Security.Cryptography.RSACryptoServiceProvider'.'
It appears that this private key is used in the JWT Bearer Flow as part of the signature, and perhaps RSACryptoServiceProvider is not used in .NET core as it was in .NET Framework.
My question is this - is there actually a way in .NET Core to generate access tokens for the OAuth JWT Bearer Flow?
Full code that I'm using:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var token = GetAccessToken();
}
static dynamic GetAccessToken()
{
// get the certificate
var certificate = new X509Certificate2(#"C:\temp\cert.pfx");
// create a header
var header = new { alg = "RS256" };
// create a claimset
var expiryDate = GetExpiryDate();
var claimset = new
{
iss = "xxxxxx",
prn = "xxxxxx",
aud = "https://test.salesforce.com",
exp = expiryDate
};
// encoded header
var headerSerialized = JsonConvert.SerializeObject(header);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
var headerEncoded = ToBase64UrlString(headerBytes);
// encoded claimset
var claimsetSerialized = JsonConvert.SerializeObject(claimset);
var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
var claimsetEncoded = ToBase64UrlString(claimsetBytes);
// input
var input = headerEncoded + "." + claimsetEncoded;
var inputBytes = Encoding.UTF8.GetBytes(input);
// signature
var rsa = (RSACryptoServiceProvider) certificate.GetRSAPrivateKey();
var cspParam = new CspParameters
{
KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
var signatureEncoded = ToBase64UrlString(signatureBytes);
// jwt
var jwt = headerEncoded + "." + claimsetEncoded + "." + signatureEncoded;
var client = new WebClient();
client.Encoding = Encoding.UTF8;
var uri = "https://login.salesforce.com/services/oauth2/token";
var content = new NameValueCollection();
content["assertion"] = jwt;
content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));
var result = JsonConvert.DeserializeObject<dynamic>(response);
return result;
}
static int GetExpiryDate()
{
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var currentUtcTime = DateTime.UtcNow;
var exp = (int)currentUtcTime.AddMinutes(4).Subtract(utc0).TotalSeconds;
return exp;
}
static string ToBase64UrlString(byte[] input)
{
return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
Well - it turns out posting to stackoverflow gets the brain cogs turning.
The answer ended up being doing a deep dive to find a similar issue here and using the solution from x509certificate2 sign for jwt in .net core 2.1
I ended up replacing the following code:
var cspParam = new CspParameters
{
KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
var signatureEncoded = ToBase64UrlString(signatureBytes);
With this code which makes use of the System.IdentityModel.Tokens.Jwt nuget package:
var signingCredentials = new X509SigningCredentials(certificate, "RS256");
var signature = JwtTokenUtilities.CreateEncodedSignature(input, signingCredentials);
Full code after solution:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var token = GetAccessToken();
}
static dynamic GetAccessToken()
{
// get the certificate
var certificate = new X509Certificate2(#"C:\temp\cert.pfx");
// create a header
var header = new { alg = "RS256" };
// create a claimset
var expiryDate = GetExpiryDate();
var claimset = new
{
iss = "xxxxx",
prn = "xxxxx",
aud = "https://test.salesforce.com",
exp = expiryDate
};
// encoded header
var headerSerialized = JsonConvert.SerializeObject(header);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
var headerEncoded = ToBase64UrlString(headerBytes);
// encoded claimset
var claimsetSerialized = JsonConvert.SerializeObject(claimset);
var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
var claimsetEncoded = ToBase64UrlString(claimsetBytes);
// input
var input = headerEncoded + "." + claimsetEncoded;
var inputBytes = Encoding.UTF8.GetBytes(input);
var signingCredentials = new X509SigningCredentials(certificate, "RS256");
var signature = JwtTokenUtilities.CreateEncodedSignature(input, signingCredentials);
// jwt
var jwt = headerEncoded + "." + claimsetEncoded + "." + signature;
var client = new WebClient();
client.Encoding = Encoding.UTF8;
var uri = "https://test.salesforce.com/services/oauth2/token";
var content = new NameValueCollection();
content["assertion"] = jwt;
content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));
var result = JsonConvert.DeserializeObject<dynamic>(response);
return result;
}
static int GetExpiryDate()
{
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var currentUtcTime = DateTime.UtcNow;
var exp = (int)currentUtcTime.AddMinutes(4).Subtract(utc0).TotalSeconds;
return exp;
}
static string ToBase64UrlString(byte[] input)
{
return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
I am replying to this question just because such a similar answer would have helped me a lot when I landed on this page the first time.
First of all you don't have to generate the JWT from the C# client.
To generate a JWT token you can use this website: https://jwt.io/
There is a very well done video showing how to generate a JWT token:
https://www.youtube.com/watch?v=cViU2-xVscA&t=1680s
Once generated, use it from your C# client to call the get access_token endpoint
https://developer.salesforce.com/docs/atlas.en-us.api_iot.meta/api_iot/qs_auth_access_token.htm
(Watch the video on YT)
If all is correct you will get the access_token
To run the API calls, all you need is the access_token and not the JWT.
Once you have it add it to the HTTP calls like this
public static void AddBearerToken(this HttpRequestMessage request, string accessToken)
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
}
From time to time the access_token will expire. To check its validity you can call the token introspect api
https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oidc_token_introspection_endpoint.htm&type=5
You need to pass two additional parameters: client_id and client_secret
The client_id is the Consumer Key. You get it from the Connected App in Salesforce
The client_server is the Consumer Secret. You get it from Connected App in Salesforce
If the introspect token API returns a response with
{ active: false, ... }
it means that the access_token is expired and you need to issue a new one.
To issue a new access_token simply call the "/services/oauth2/token" again using the same JWT.
I'm trying to send push notifications to iOS devices, using token-based authentication.
As required, I generated an APNs Auth Key in Apple's Dev Portal, and downloaded it (it's a file with p8 extension).
To send push notifications from my C# server, I need to somehow use this p8 file to sign my JWT tokens. How do I do that?
I tried to load the file to X509Certificate2, but X509Certificate2 doesn't seem to accept p8 files, so then I tried to convert the file to pfx/p12, but couldn't find a way to do that that actually works.
I found a way to do that, using BouncyCastle:
private static CngKey GetPrivateKey()
{
using (var reader = File.OpenText("path/to/apns/auth/key/file.p8"))
{
var ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
return EccKey.New(x, y, d);
}
}
And now creating and signing the token (using jose-jwt):
private static string GetProviderToken()
{
var epochNow = (int) DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
var payload = new Dictionary<string, object>()
{
{"iss", "your team id"},
{"iat", epochNow}
};
var extraHeaders = new Dictionary<string, object>()
{
{"kid", "your key id"}
};
var privateKey = GetPrivateKey();
return JWT.Encode(payload, privateKey, JwsAlgorithm.ES256, extraHeaders);
}
I hope this will be a solution;
private static string GetToken(string fileName)
{
var fileContent = File.ReadAllText(fileName).Replace("-----BEGIN PRIVATE KEY-----", "").Replace
("-----END PRIVATE KEY-----", "").Replace("\r", "");
var signatureAlgorithm = GetEllipticCurveAlgorithm(fileContent);
ECDsaSecurityKey eCDsaSecurityKey = new ECDsaSecurityKey(signatureAlgorithm)
{
KeyId = "S********2"
};
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken token = handler.CreateJwtSecurityToken(
issuer: "********-****-****-****-************",
audience: "appstoreconnect-v1",
expires: DateTime.UtcNow.AddMinutes(5),
issuedAt: DateTime.UtcNow,
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(eCDsaSecurityKey, SecurityAlgorithms.EcdsaSha256));
return token.RawData;
}
private static ECDsa GetEllipticCurveAlgorithm(string privateKey)
{
var keyParams = (ECPrivateKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey));
var normalizedEcPoint = keyParams.Parameters.G.Multiply(keyParams.D).Normalize();
return ECDsa.Create(new ECParameters
{
Curve = ECCurve.CreateFromValue(keyParams.PublicKeyParamSet.Id),
D = keyParams.D.ToByteArrayUnsigned(),
Q =
{
X = normalizedEcPoint.XCoord.GetEncoded(),
Y = normalizedEcPoint.YCoord.GetEncoded()
}
});
}
A way to create the key without BouncyCastle:
var privateKeyString = (await File.ReadAllTextAsync("<PATH_TO_KEY_FILE>"))
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Replace("\n", "");
using var algorithm = ECDsa.Create();
algorithm.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKeyText), out var _);
var securityKey = new ECDsaSecurityKey(algorithm) { KeyId = "<KEY_ID>" };
I am trying to build a SCEP server to support Apple MDM Device Enrollment. This needs to be implemented into our current MDM Service, written in C#.
I have looked into the following for inspiration:
JSCEP, a java library for scep server implementation https://github.com/jscep/jscep
Bouncy Castle (complicated, and not a lot of documentation on the C# side)
Cisco SCEP documentation http://www.cisco.com/c/en/us/support/docs/security-vpn/public-key-infrastructure-pki/116167-technote-scep-00.html
Does anyone know of any solid examples, on creating a C# SCEP server? I haven't been able to find any good documentation for this.
UPDATE
This is what i have by now, it is still not working, but i think the issue lies with the signed pkcs7, but I am not sure what i am missing?
public class ScepModule: NancyModule
{
/// <summary>
/// The _log.
/// </summary>
private readonly ILog log = LogManager.GetLogger(typeof(ScepModule));
/// <summary>
/// Initializes a new instance of the <see cref="ScepModule"/> class.
/// </summary>
/// <param name="cp">
/// The certificate provider.
/// </param>
/// <param name="config">
/// The config.
/// </param>
public ScepModule(MdmConfigDTO config)
: base("/cimdm/scep")
{
this.log.Debug(m => m("Instanciating scep Module."));
this.Get["/"] = result =>
{
var message = Request.Query["message"];
var operation = Request.Query["operation"];
if (operation == "GetCACert")
{
return RespondWithCACert();
}
else if (operation == "GetCACaps")
{
return RespondWithCACaps();
}
return "";
};
this.Post["/"] = result =>
{
var message = Request.Query["message"];
var operation = Request.Query["operation"];
byte[] requestData = null;
using (var binaryReader = new BinaryReader(Request.Body))
{
requestData = binaryReader.ReadBytes((int)Request.Body.Length);
}
var headers = Request.Headers;
foreach (var header in headers)
{
this.log.Debug(m => m("Header: {0}, Value: {1}", header.Key, String.Join(",", header.Value)));
}
var caCert = getSignerCert();
var signingCert = createSigningCert(caCert, requestData, config);
return signingCert;
};
this.log.Debug(m => m("Finished Instanciating scep Module."));
}
private Response RespondWithCACert()
{
var caCert = getSignerCert();
var response = new Response();
response.ContentType = "application/x-x509-ca-cert";
response.Contents = stream =>
{
byte[] data = caCert.Export(X509ContentType.Cert);
stream.Write(data, 0, data.Length);
stream.Flush();
stream.Close();
};
return response;
}
private Response RespondWithCACaps()
{
var response = new Response();
response.ContentType = "text/plain; charset=ISO-8859-1";
//byte[] data = Encoding.UTF8.GetBytes("POSTPKIOperation\nSHA-512\nSHA-256\nSHA-1");
byte[] data = Encoding.UTF8.GetBytes("POSTPKIOperation\nSHA-1");
response.Contents = stream =>
{
stream.Write(data, 0, data.Length);
stream.Flush();
stream.Close();
};
return response;
}
private Response createSigningCert(X509Certificate2 caCert, byte[] data, MdmConfigDTO config)
{
var signedResponse = new SignedCms();
signedResponse.Decode(data);
var caChain = new X509Certificate2Collection(caCert);
signedResponse.CheckSignature(caChain, true);
var attributes = signedResponse
.SignerInfos
.Cast<System.Security.Cryptography.Pkcs.SignerInfo>()
.SelectMany(si => si.SignedAttributes.Cast<CryptographicAttributeObject>());
// Any errors then return null
if (attributes.Any(att => att.Oid.Value.Equals(Oids.Scep.FailInfo)))
{
return null;
}
byte[] msg = DecryptMsg(signedResponse.ContentInfo.Content, caChain);
byte[] certResult = GenerateSelfSignedClientCertificate(msg, caCert, config);
X509Certificate2Collection reqCerts = signedResponse.Certificates;
//Create Enveloped PKCS#7 data
var envelpeDataPkcs7 = createEnvelopedDataPkcs7(certResult);
//Create Signed PKCS#7 data
var signedDataPkcs7 = createSignedDataPkcs7(envelpeDataPkcs7, caCert, attributes);
var response = new Response();
response.ContentType = "application/x-pki-message";
response.WithHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.WithHeader("Pragma", "no-cache");
var execPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
File.WriteAllBytes(execPath + "\\signedDataPkcs7.p7b", signedDataPkcs7);
File.WriteAllBytes(execPath + "\\envelpeDataPkcs7.p7b", envelpeDataPkcs7);
response.Contents = stream =>
{
stream.Write(signedDataPkcs7, 0, signedDataPkcs7.Length);
stream.Flush();
stream.Close();
};
return response;
}
private byte[] GenerateSelfSignedClientCertificate(byte[] encodedPkcs10, X509Certificate2 caCert, MdmConfigDTO config)
{
var execPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
File.WriteAllText(execPath + "\\scepPkcs10.csr", "-----BEGIN CERTIFICATE REQUEST-----\n" + Convert.ToBase64String(encodedPkcs10) + "\n-----END CERTIFICATE REQUEST-----");
Pkcs10CertificationRequest csr = new Pkcs10CertificationRequest(encodedPkcs10);
CertificationRequestInfo csrInfo = csr.GetCertificationRequestInfo();
SubjectPublicKeyInfo pki = csrInfo.SubjectPublicKeyInfo;
Asn1Set attributes = csrInfo.Attributes;
//pub key for the signed cert
var publicKey = pki.GetPublicKey();
// Build a Version3 Certificate
DateTime startDate = DateTime.UtcNow.AddMonths(-1);
DateTime expiryDate = startDate.AddYears(10);
BigInteger serialNumber = new BigInteger(32, new Random());
this.log.Debug(m => m("Certificate Signing Request Subject is: {0}", csrInfo.Subject));
CmsSignedDataGenerator cmsGen = new CmsSignedDataGenerator();
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
string signerCN = caCert.GetNameInfo(X509NameType.SimpleName, true);
certGen.SetSubjectDN(new X509Name(String.Format("CN={0}", config.HostName)));
certGen.SetSerialNumber(serialNumber);
certGen.SetIssuerDN(new X509Name(String.Format("CN={0}", signerCN)));
certGen.SetNotBefore(startDate);
certGen.SetNotAfter(expiryDate);
certGen.SetSignatureAlgorithm("SHA1withRSA");
certGen.SetPublicKey(PublicKeyFactory.CreateKey(pki));
AsymmetricCipherKeyPair caPair = DotNetUtilities.GetKeyPair(caCert.PrivateKey);
for(int i=0; i!=attributes.Count; i++)
{
AttributeX509 attr = AttributeX509.GetInstance(attributes[i]);
//process extension request
if (attr.AttrType.Id.Equals(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest.Id))
{
X509Extensions extensions = X509Extensions.GetInstance(attr.AttrValues[0]);
var e = extensions.ExtensionOids.GetEnumerator();
while (e.MoveNext())
{
DerObjectIdentifier oid = (DerObjectIdentifier)e.Current;
Org.BouncyCastle.Asn1.X509.X509Extension ext = extensions.GetExtension(oid);
certGen.AddExtension(oid, ext.IsCritical, ext.GetParsedValue());
}
}
}
//certGen.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment));
certGen.AddExtension(X509Extensions.ExtendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeID.AnyExtendedKeyUsage));
certGen.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(0));
Org.BouncyCastle.X509.X509Certificate selfSignedCert = certGen.Generate(caPair.Private);
//Check if the certificate can be verified
selfSignedCert.Verify(caPair.Public);
if (log.IsDebugEnabled)
{
//var execPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
File.WriteAllBytes(execPath + "\\scepCertificate.pfx", selfSignedCert.GetEncoded());
}
return selfSignedCert.GetEncoded();
}
private byte[] createEnvelopedDataPkcs7(byte[] pkcs7RequestData)
{
var recipient = new CmsRecipient(new X509Certificate2(pkcs7RequestData));
var envelopedContent = new System.Security.Cryptography.Pkcs.ContentInfo(new Oid(Oids.Pkcs7.EncryptedData, "envelopedData"), pkcs7RequestData);
//var envelopedContent = new System.Security.Cryptography.Pkcs.ContentInfo(pkcs7RequestData);
var envelopedMessage = new EnvelopedCms(envelopedContent);
//var envelopedMessage = new EnvelopedCms(Con);
envelopedMessage.Encrypt(recipient);
var encryptedMessageData = envelopedMessage.Encode();
return encryptedMessageData;
}
private byte[] createSignedDataPkcs7(byte[] encryptedMessageData, X509Certificate2 localPrivateKey, IEnumerable<CryptographicAttributeObject> attributes)
{
var senderNonce = attributes
.Single(att => att.Oid.Value.Equals(Oids.Scep.SenderNonce))
.Values[0];
var transactionId = attributes
.Single(att => att.Oid.Value.Equals(Oids.Scep.TransactionId))
.Values[0];
// Create the outer envelope, signed with the local private key
var signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, localPrivateKey);
//{
// DigestAlgorithm = new Oid(Oids.Pkcs.SHA1, "digestAlorithm")
//};
//signer.SignedAttributes.Add(signingTime);
var messageType = new AsnEncodedData(Oids.Scep.MessageType, DerEncoding.EncodePrintableString("3"));
signer.SignedAttributes.Add(messageType);
signer.SignedAttributes.Add(transactionId);
var recipientNonce = new Pkcs9AttributeObject(Oids.Scep.RecipientNonce, DerEncoding.EncodeOctet(senderNonce.RawData));
signer.SignedAttributes.Add(recipientNonce);
var pkiStatus = new AsnEncodedData(Oids.Scep.PkiStatus, DerEncoding.EncodePrintableString("0"));
signer.SignedAttributes.Add(pkiStatus);
var nonceBytes = new byte[16];
RNGCryptoServiceProvider.Create().GetBytes(nonceBytes);
senderNonce = new Pkcs9AttributeObject(Oids.Scep.SenderNonce, DerEncoding.EncodeOctet(nonceBytes));
signer.SignedAttributes.Add(senderNonce);
//var failInfo = new Pkcs9AttributeObject(Oids.Scep.FailInfo, DerEncoding.EncodePrintableString("2"));
//signer.SignedAttributes.Add(failInfo);
// Seems that the oid is not needed for this envelope
var contentInfo = new System.Security.Cryptography.Pkcs.ContentInfo(encryptedMessageData); //new Oid("1.2.840.113549.1.7.1", "data"), encryptedMessageData);
var signedCms = new SignedCms(contentInfo, false);
signedCms.ComputeSignature(signer);
return signedCms.Encode();
}
private X509Certificate2 getSignerCert()
{
string thumbprint = "CACertThumbprint";
thumbprint = Regex.Replace(thumbprint, #"[^\da-zA-z]", string.Empty).ToUpper();
// Get a local private key for sigining the outer envelope
var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
certStore.Open(OpenFlags.ReadOnly);
var signerCert = certStore
.Certificates
.Find(X509FindType.FindByThumbprint, thumbprint, false)
.Cast<X509Certificate2>()
.Single();
certStore.Close();
return signerCert;
}
// Decrypt the encoded EnvelopedCms message for one of the
// recipients.
public Byte[] DecryptMsg(byte[] encodedEnvelopedCms, X509Certificate2Collection caChain)
{
// Prepare object in which to decode and decrypt.
EnvelopedCms envelopedCms = new EnvelopedCms();
// Decode the message.
envelopedCms.Decode(encodedEnvelopedCms);
// Display the number of recipients
DisplayEnvelopedCms(envelopedCms, false);
// Decrypt the message.
this.log.Debug("Decrypting Data for one recipient ... ");
envelopedCms.Decrypt(envelopedCms.RecipientInfos[0], caChain);
this.log.Debug("Done.");
// The decrypted message occupies the ContentInfo property
// after the Decrypt method is invoked.
return envelopedCms.ContentInfo.Content;
}
// Display the ContentInfo property of an EnvelopedCms object.
private void DisplayEnvelopedCmsContent(String desc,
EnvelopedCms envelopedCms)
{
this.log.Debug(string.Format(desc + " (length {0}): ",
envelopedCms.ContentInfo.Content.Length));
foreach (byte b in envelopedCms.ContentInfo.Content)
{
this.log.Debug(b.ToString() + " ");
}
}
// Display some properties of an EnvelopedCms object.
private void DisplayEnvelopedCms(EnvelopedCms e,
Boolean displayContent)
{
this.log.Debug("\nEnveloped PKCS #7 Message Information:");
this.log.Debug(string.Format(
"\tThe number of recipients for the Enveloped PKCS #7 " +
"is: {0}", e.RecipientInfos.Count));
for (int i = 0; i < e.RecipientInfos.Count; i++)
{
this.log.Debug(string.Format(
"\tRecipient #{0} has type {1}.",
i + 1,
e.RecipientInfos[i].RecipientIdentifier.Type));
}
if (displayContent)
{
DisplayEnvelopedCmsContent("Enveloped PKCS #7 Content", e);
}
}
}
I have a token, a file containing public key and I want to verify the signature.
I tried to verify signature based on this.
However, decodedCrypto and decodedSignature don't match.
Here is my code:
public static string Decode(string token, string key, bool verify)
{
var parts = token.Split('.');
var header = parts[0];
var payload = parts[1];
byte[] crypto = Base64UrlDecode(parts[2]);
var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
var headerData = JObject.Parse(headerJson);
var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
var payloadData = JObject.Parse(payloadJson);
if (verify)
{
var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
var keyBytes = Encoding.UTF8.GetBytes(key);
var algorithm = (string)headerData["alg"];
var signature = HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign);
var decodedCrypto = Convert.ToBase64String(crypto);
var decodedSignature = Convert.ToBase64String(signature);
if (decodedCrypto != decodedSignature)
{
throw new ApplicationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
}
}
return payloadData.ToString();
}
I'm sure that the signature of token is valid. I try to verify on https://jwt.io/ and it showed that Signature verified.
So the problem is the algorithm to encode, decode.
Is there anyone can solve this problem? The algorithm is RS256
How about using JwtSecurityTokenHandler?
it could look something like this:
public bool ValidateToken(string token, byte[] secret)
{
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningToken = new BinarySecretSecurityToken(secret)
};
SecurityToken validatedToken;
try
{
tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception)
{
return false;
}
return validatedToken != null;
}
Be aware I haven't tested it but we used a similar implementation in one of the projects
I finally got a solution from my colleague.
For those who have the same problem, try my code:
public static string Decode(string token, string key, bool verify = true)
{
string[] parts = token.Split('.');
string header = parts[0];
string payload = parts[1];
byte[] crypto = Base64UrlDecode(parts[2]);
string headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
JObject headerData = JObject.Parse(headerJson);
string payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
JObject payloadData = JObject.Parse(payloadJson);
if (verify)
{
var keyBytes = Convert.FromBase64String(key); // your key here
AsymmetricKeyParameter asymmetricKeyParameter = PublicKeyFactory.CreateKey(keyBytes);
RsaKeyParameters rsaKeyParameters = (RsaKeyParameters)asymmetricKeyParameter;
RSAParameters rsaParameters = new RSAParameters();
rsaParameters.Modulus = rsaKeyParameters.Modulus.ToByteArrayUnsigned();
rsaParameters.Exponent = rsaKeyParameters.Exponent.ToByteArrayUnsigned();
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParameters);
SHA256 sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(parts[0] + '.' + parts[1]));
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA256");
if (!rsaDeformatter.VerifySignature(hash, FromBase64Url(parts[2])))
throw new ApplicationException(string.Format("Invalid signature"));
}
return payloadData.ToString();
}
It works for me. The algorithm is RS256.
I know this is an old thread but I could have recommended you to use this library instead of writing on your own. It has got some good documentation to get started. Am using it without any issues.
byte[] crypto = Base64UrlDecode(parts[2]);
In this line you are base64 decoding signature part of JWT token, but as I know that part isn't base64 encoded. Please try this code. ( I have commented out unnecessary lines )
public static string Decode(string token, string key, bool verify)
{
var parts = token.Split('.');
var header = parts[0];
var payload = parts[1];
// byte[] crypto = Base64UrlDecode(parts[2]);
var jwtSignature = parts[2];
var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
var headerData = JObject.Parse(headerJson);
var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
var payloadData = JObject.Parse(payloadJson);
if (verify)
{
var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
var keyBytes = Encoding.UTF8.GetBytes(key);
var algorithm = (string)headerData["alg"];
var computedJwtSignature = Encoding.UTF8.GetString(HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign));
// var decodedCrypto = Convert.ToBase64String(crypto);
// var decodedSignature = Convert.ToBase64String(signature);
if (jwtSignature != computedJwtSignature)
{
throw new ApplicationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
}
}
return payloadData.ToString();
}