Creating a valid sha1 signature for API - c#

I am trying to post to an API that requires a SHA1 signature
https://www.inkthreadable.co.uk/api-documentation#order-create
The Signature value will need to be generated at your side as
SHA1(request body + Secret Key).
This is the method I am using to create the signature
static string ComputeSignature(byte[] body, byte[] secret)
{
using (SHA1Managed sha1 = new SHA1Managed())
{
var key1 = sha1.ComputeHash(body);
var key2 = key1.Concat(secret).ToArray();
var key3 = sha1.ComputeHash(key2);
return Convert.ToBase64String(key3);
}
}
var body = Encoding.UTF8.GetBytes(json);
var secret = Encoding.UTF8.GetBytes(_inkThreadableSettings.Value.SecretKey);
var signature1 = ComputeSignature(body, secret);
I'm using rest client (vs code extension) to post the data. The json section below is what i am using to pass to the ComputeSignature method above (body param)
POST https://www.inkthreadable.co.uk/api/orders.php?AppId=APP-00202123&Signature=cbAJNd4lU+/+OXpN2bsVJczOuo8=
content-type: application/json
{
"brandName":"Inkthreadable",
"comment":"Test order.",
"shipping_address":{
"firstName":"Alex",
"lastName":"Cunliffe",
"company":"Inkthreadable",
"address1":"Unit 501a",
"address2":"Glenfield Park Two",
"city":"Blackburn",
"county":"Lancashire",
"postcode":"BB1 5QH",
"country":"United Kingdom",
"phone1":"+44 (0)1254 777070"
},
"shipping":{
"shippingMethod":"courier"
},
"items":[
{
"pn":"JH001",
"quantity":4,
"retailPrice":20,
"description":"Please print as large as posible",
"label":{
"type":"printed",
"name":"ink-label"
},
"designs":{
"front":"http://animalfair.com/wp-content/uploads/2014/06/little_cute_cat_1920x1080.jpg",
"back":"http://data3.whicdn.com/images/168204223/large.jpg"
}
}
]
}
The response I get is : "Invalid Signature"
I have tried a number of permutations of a compute signature function but not got it working.
Also tried this:
private string ComputeSignature7(byte[] body, byte[] secret)
{
using (SHA1Managed sha1 = new SHA1Managed())
{
var key = sha1.ComputeHash(body.Concat(secret).ToArray());
return Convert.ToBase64String(key);
}
}

Related

How do I send the data to API with encrypted data and signature with private key generated by RSA

I am C# developer and I have the below code works well in C# application. I am trying to use the same functionality in the VueJS and Node Js application. I have done with serialization and conversion to fromBase64String, but I am stuck here with generating signature to send it to API. please suggest.
byte[] privateKey = Convert.FromBase64String(File.ReadAllText(appConfig.PrivateKey));
using (CngKey signingKey = CngKey.Import(privateKey, CngKeyBlobFormat.Pkcs8PrivateBlob))
using (RSACng rsaCng = new RSACng(signingKey))
{
request.Data.Signature = Convert.ToBase64String(rsaCng.SignData(Encoding.UTF8.GetBytes(request.Data.Content), HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1));
}
var response = (Response)SendRequest(request, 1);
return response;
I have tried the below in Vue and node Js code
var utf8 = (encodeURIComponent(serializedProductInfo));
var dataInfo = [];
for (var i = 0; i < utf8.length; i++) {
dataInfo.push(utf8.charCodeAt(i));
}
let base64Contenet = window.btoa((encodeURIComponent(dataInfo)));
//let base64Contenet = Buffer.toString((encodeURIComponent(dataInfo)));
console.log(base64Contenet);
var privateKey = [];
privateKey = Buffer.from("My PrivateKey", "base64");
var crypto = require('crypto');
var sha1 = crypto.createHash('sha1');
sha1.key = privateKey;
sha1.update(dataInfo);
var hash = sha1.digest();
var signature = crypto.privateEncrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, hash);
console.log(signature);
I am getting the below error
vue.runtime.esm.js?2b0e:619 [Vue warn]: Error in v-on handler: "TypeError: Cannot read properties of null (reading '2')"

C# Client generated RSA-Signature NodeJS server side validation fails

Dear Community,
fast forward. After an initial request I receive an random string from a server.
The client(ME) needs to generate an Digital-Signature via C# and send it back to the Node.js API (Express;TypeScript).
Then the server validats the signature and returns true, if the signature is successfully verified or false if not.
To sign the string I use C# default RSA object and SHA256 for Hashing.
The Server knows my public key and user identifier to make sure the digital-signature belongs to me.
Use-Case Tl;Dr:
Client(C#):
Receive a string from the server. Generate an digital-signature(RSA;SHA256) of this string and send it back via API-Request towards the Server.
Server(NodeJS;Express;TypeScript):
Receive the request with the digital-signature of the string inside and validats it.
Send an answer with true if the signature is successfully verified or false if not.
**The Problem:**
The result turns nerver out as true.
Generating an Signature of the string and Verify it localy, both systems (client and server) will return true.
Means if the Client generate a signature of the string and verifys it the process results localy in true.
If the server uses the same Key-Pair as the client and generate a signature of the same string the process will result in true.
BUT using the same Key-Pair both systems generate different signatures for the same string!
If I use the digital-signature of the string generated by the server and send it with the client, the server returns TRUE.
Code is a bit different. Names has changed.
Client (C#)
Request Methode
<!-- language: c# -->
private async Task<string> APIRequestSignature(RSA my_rsa, string randomString)
{
byte[] randomBytes = Encoding.UTF8.GetBytes(randomString);
byte[] signature = Crypto.GetDigitalSignatur256(randomBytes, my_rsa);
MeRequest meRequest = new(identifier, Convert.ToBase64String(signature));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = APIUri.ValidME,
Content = new StringContent(meRequest.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json),
};
var response = await client.SendAsync(request).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Console.WriteLine(responseBody);
}
internal class MeRequest: Request
{
private string sign;
public MeRequest(string identifier, string sign) : base(identifier, "")
{
this.sign = sign;
}
public string signature
{
get
{
return this.sign;
}
}
}
Crypto Class:
<!-- language: c# -->
public static byte[] GetDigitalSignatur256(byte[] str, RSA rsa)
{
byte[] hash = Hash256(str);
RSAPKCS1SignatureFormatter rsaFormatter = new(rsa);
rsaFormatter.SetHashAlgorithm("SHA256");
return rsaFormatter.CreateSignature(hash);
}
public static byte[] Hash256(byte[] arr) =>SHA256.Create().ComputeHash(arr);
Messi JavaScript Server Code: (Node.JS;Express;TypeScript)
<!-- language: lang-js -->
import \{Encryption\} from "../class/Encryption";
var randomString = "TESTTESTTESTTEST";
<!-- language: lang-js -->
//If User Exists need Ident and PKey /Get an random encrypted string
UserRoutes.get('/UserExist', function(req, res) {
//Create Hash from randomString
var hash = crypto.createHash("SHA256");
hash.update(randomString, "base64");
var randomStringHash = hash.digest("base64");
//Read Private Key from Client for Test Signature
var clientPrivateKey = fs.readFileSync(config.server.clientKeyPath + "identlarspk.txt", 'utf8');
//Create Sign with Hash from RandomString
var sign = crypto.createSign("SHA256");
sign.update(randomStringHash, "base64");
sign.end();
var signature = sign.sign({
key: clientPrivateKey,
padding: crypto.constants.RSA_PKCS1_PADDING
});
console.log("Signature Created By Server: " + signature);
res.json({
randomString: randomString
})
});
<!-- language: lang-js -->
//Validate User by Ident and Manipulated random encrypted String By Signature
UserRoutes.get('/UserSigning', async function(req, res) {
var encryption = new Encryption();
//Formating Public key
var userPKeyDB = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqdKBCUssRLefK9EzzRKxm+ftQ26PLmI5utmmGY6LgwEnKuIrJw/cWA5Fn+2ebJNAgdH9uFBAh8CEtHHjnfMB0SWCl6Qv8R62x8wJs8xNmoTIVLENe5lvHGi2FLVmzLg1LInfwqfsLBLmCM6re5WtQPX4BiXjfDaeYxYCLg3plfaQyYe9/PCzpswC4U4R3yMKzW3ptj4D+L0BGwp4EKFkh2qGJ1Bnm2PkDwott7CT8a8ef36vS7x1fb0XUuw4b5c87ilQZ+mG85aWDK45R7Tl2ZMebZsAe/kpLIRDsG3nnxdzpBelePFCGWSdCCwvFjygOcLyKXQtqJyp3tHvENGTjQIDAQAB";
var formatedEncryptionKey = await encryption.FormatePKey(userPKeyDB);
//Create Hash from RandomString
var hash = crypto.createHash("SHA256");
hash.update(randomString, "base64");
var randomStringHash = hash.digest("base64");
var bufferRandomStringHash = Buffer.from(randomStringHash, "base64");
var bufferClientSignature = Buffer.from(req.body.signature, "base64");
var verify = crypto.createVerify("SHA256");
verify.update(randomStringHash);
verify.end();
var isValidated = verify.verify(formatedEncryptionKey, bufferClientSignature);
res.json({
token: isValidated
})
});
<!-- language: lang-js -->
//Create start and end points for the Public Key in Encryption Class
public async FormatePKey(pKey: string): Promise<string> {
var bufferStartKey = Buffer.from("-----BEGIN PUBLIC KEY-----\n", "utf8");
var bufferStartKeyUTF8 = bufferStartKey.toString("utf8");
var bufferPubKey = Buffer.from(pKey, "base64");
var pubKeyBase64 = bufferPubKey.toString("base64");
var bufferEndKey = Buffer.from("\n-----END PUBLIC KEY-----", "utf8");
var bufferEndKeyUTF8 = bufferEndKey.toString("utf8");
var formatedPublicKey = bufferStartKeyUTF8 + pubKeyBase64 + bufferEndKeyUTF8;
var bufferPubicKey = Buffer.from(formatedPublicKey, "utf8");
var bufferPublicKeyUTF8 = bufferPubicKey.toString("utf8");
return bufferPublicKeyUTF8;
}

Create a valid JWT Token for DocuSign API

Ι 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).

Nodejs verify jwt token from .net failed

I create token from .net by this C# code (with System.IdentityModel.Tokens.Jwt):
var keybytes = Convert.FromBase64String("MYCUSTOMCODELONGMOD4NEEDBEZE");
var signingCredentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(keybytes),
SecurityAlgorithms.HmacSha256Signature,
SecurityAlgorithms.Sha256Digest);
var nbf = DateTime.UtcNow.AddDays(-100);
var exp = DateTime.UtcNow.AddDays(100);
var payload = new JwtPayload(null, "", new List<Claim>(), nbf, exp);
var user = new Dictionary<string, object>();
user.Add("userId", "1");
payload.Add("user", user);
payload.Add("success", true);
var jwtToken = new JwtSecurityToken(new JwtHeader(signingCredentials), payload);
var jwtTokenHandler = new JwtSecurityTokenHandler();
var resultToken = jwtTokenHandler.WriteToken(jwtToken);
I send the resultToken to nodejs and verify it (with jsonwebtoken library) with below code:
var jwt = require('jsonwebtoken');
var result = jwt.verify(
resultToken,
new Buffer('MYCUSTOMCODELONGMOD4NEEDBEZE').toString('base64'),
{ algorithms: ['HS256'] },
function(err, decoded) {
if (err) {
console.log('decode token failed with error: '+ JSON.stringify(err));
}
}
);
I got the error: invalid signature. The resultToken content:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTQ4MTMxMTUsIm5iZiI6MTQ3NzUzMzExNSwidXNlciI6eyJ1c2VySWQiOiIxIn0sInN1Y2Nlc3MiOnRydWV9.4bjYyIUFMouz-ctFyxXkJ_QcJJQofCEFffUuazWFjGw
I have debug it on jwt.io with above signature (MYCUSTOMCODELONGMOD4NEEDBEZE) and secret base64 encoded checked, it's ok.
I have tried a signature without base64 encoded by chaging keybytes in C# code:
var keybytes = Encoding.UTF8.GetBytes("MYCUSTOMCODELONGMOD4NEEDBEZE");
And it verified successfully in nodejs. So i think the issue comes from my nodejs code when verify a base64 encoded signature. Did i miss some options when verify token or somethings?
I have no idea what you did but this snippet is working for me with the token you provided above.
var jwt = require('jwt-simple')
var secret = new Buffer('MYCUSTOMCODELONGMOD4NEEDBEZE').toString('base64')
var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTQ4MTMxMTUsIm5iZiI6MTQ3NzUzMzExNSwidXNlciI6eyJ1c2VySWQiOiIxIn0sInN1Y2Nlc3MiOnRydWV9.4bjYyIUFMouz-ctFyxXkJ_QcJJQofCEFffUuazWFjGw'
var decoded = jwt.decode(token, secret)
console.log(decoded)
Output:
❯ node jwt.js
{ exp: 1494813115,
nbf: 1477533115,
user: { userId: '1' },
success: true }
Using jsonwebtoken library
// var jwt = require('jwt-simple')
var jwt = require('jsonwebtoken');
var secret = Buffer.from('MYCUSTOMCODELONGMOD4NEEDBEZE', 'base64')
var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTQ4MTMxMTUsIm5iZiI6MTQ3NzUzMzExNSwidXNlciI6eyJ1c2VySWQiOiIxIn0sInN1Y2Nlc3MiOnRydWV9.4bjYyIUFMouz-ctFyxXkJ_QcJJQofCEFffUuazWFjGw'
jwt.verify(token, secret, { algorithms: ['HS256'] }, function(err, decoded) {
if (err) {
console.log(err)
} else {
console.log(decoded)
}
})
Again still working fine.
The only difference i can see is the secret.

Is there any JSON Web Token (JWT) example in C#?

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"
})

Categories

Resources