How to Create an Rs256 Jwt Assertion with Bash/Shell Scripting

How do you create an RS256 JWT assertion with bash/shell scripting?

Consider the following, which supports both HS256 and RS256:

#!/usr/bin/env bash

# Inspired by implementation by Will Haley at:
# http://willhaley.com/blog/generate-jwt-with-bash/

set -o pipefail

# Shared content to use as template
header_template='{
"typ": "JWT",
"kid": "0001",
"iss": "https://stackoverflow.com/questions/46657001/how-do-you-create-an-rs256-jwt-assertion-with-bash-shell-scripting"
}'

build_header() {
jq -c \
--arg iat_str "$(date +%s)" \
--arg alg "${1:-HS256}" \
'
($iat_str | tonumber) as $iat
| .alg = $alg
| .iat = $iat
| .exp = ($iat + 1)
' <<<"$header_template" | tr -d '\n'
}

b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; }
json() { jq -c . | LC_CTYPE=C tr -d '\n'; }
hs_sign() { openssl dgst -binary -sha"${1}" -hmac "$2"; }
rs_sign() { openssl dgst -binary -sha"${1}" -sign <(printf '%s\n' "$2"); }

sign() {
local algo payload header sig secret=$3
algo=${1:-RS256}; algo=${algo^^}
header=$(build_header "$algo") || return
payload=${2:-$test_payload}
signed_content="$(json <<<"$header" | b64enc).$(json <<<"$payload" | b64enc)"
case $algo in
HS*) sig=$(printf %s "$signed_content" | hs_sign "${algo#HS}" "$secret" | b64enc) ;;
RS*) sig=$(printf %s "$signed_content" | rs_sign "${algo#RS}" "$secret" | b64enc) ;;
*) echo "Unknown algorithm" >&2; return 1 ;;
esac
printf '%s.%s\n' "${signed_content}" "${sig}"
}

(( $# )) && sign "$@"

...usage as:

rsa_secret='
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAtHEDjwkBpsjhit+wXZMMj2AaRHyWSKatjzLtVEGdyXrbQGgQ
PjbfqPtqKsBPjcifHh8VAgrEtETbLN8pbE/XLRaB9P76hib6DATBn2JC6XG/NkAu
0b2F8WB6ZuJh3fbubSOZaORRIyRvfidV5Wjb7NbEDhuSxFLaq0ad2+rQHyBgMfQS
43OqhEa463WQt5F9NuWRTqweh5UotT6Mg9YgkvmBdA4IbJMEDWGFNecUzAGuESYq
wzJaaQ4S58ce7HxFDywM0nFXlNx1pxZwZOZfG7bddUD8FuwbBMx5c3Z3U8LAA+J/
50A/kxuZoa6sRTb7gXfBxy2riechlOTL+5ut3wIDAQABAoIBAD8bm5wGEV7MuR1B
+MPxbx4iBW3YiRMlwGPp8tlaDZ5u6onPG4c21+iY7du/4NL8zLHTOxy4uW02+9To
w+sOzXoGejM+jk4nCaL0cueUjURqNO77aaSPfW4bSRP8ry/bci4Xmkr2N25sCtZ7
WW5fyzM9NdqdSCqDs9jdXM6ShHGt4aG1w4Q38pfl2O2KUqgGYA8j8S7oEpcuApIj
sNH8o2PIFaFuRoUBq6WxSZBY7YdvKM0xlE0NKiDMAUIeTIRqtm8GPo7ot8dV6VHU
EglN7gaEve75XW0DAkK2lDDpGPlVHJwLgKGiSuW0qMh6lY+dKjsZ8wyz85DqTnyo
+42ZI6kCgYEA36X4c4a/tlh0A6i+EaA1CqmN8jh3nNMYgZvovTnIezCvO+RuJJEG
KQQjr8/z+E8FYobImrrZsuSL+UFs1trl/nSndWh22B7fQQbJBdHwhv39YWReS0tW
7t3LJJG3oQnR/ChlqyTToHfS0WcdtYQ0cnFWSx73Hg/S+cu0vHtcUrUCgYEAzosH
dXq1VGRgf3TIoI9s4xJt/SnH+VHtP4dvLKzY7NN4K76DIYdQIn1xQ1Y3705v/XG+
xTNAaoOaH6hBnRxwxcv6GmCpICJ2C21puxA63RqCslab5fc23wvMv/wwoEWPtXhf
3OOKZxszLR1vFqZaYTWzVmTxg+r5b2aNBB0MtsMCgYBAv+6Ek/ihNE6yWIJe3AE/
SwOboxmOP9eSfq8NSdNvRxMUxffVgl9ENLyYRB6gP1CRy+/8TCiHEIAt8/Es60c3
OlLZPRtbSuTcELjWhIecraBUOBjMt809bt1HgyCk8RDoblGxEQJsLQTON4p0aQg+
Me4H5bkp7O7p/z4ea6C5GQKBgQCewy+QliocHKwwTMyK3rSMNvZky2DzvI3pb2l9
pb95C3Qr691QQHrQiCwv3m5QfLKI1o4VdzfkqBQokWUeJ2ZoJEqzS+m00ch7MDc9
m1Qj8OTVwM1FD6oV+TQBvxCBofa9PzIw0JbqenX0D9P8TRLb9jNMDXu4Mz5Y6zMq
HkpPkQKBgQCgrcW7U0Q+081N25VfghCPRd/o+dtqP3udXgj0nIX6y3qeCJiPeamZ
plMNqdZScaK37wMouAIPD0u5w1OCnlepuUxU3h5y55Lzx3PnDlU1H+yfBsTi1KL/
sDjgs31j//w80krxJNj5/i2AeYzATmybPwyM/c/PVBv/hecreUbTlQ==
-----END RSA PRIVATE KEY-----
'

test_payload='{
"Id": 1,
"Name": "Hello, world!"
}'
sign rs256 "$test_payload" "$rsa_secret"

Create RS256 JWT in bash

Successfully resolved this. It was caused by a couple of issues:

  1. openssl base64 inserts carriage returns every 70-odd characters by default. I believe you can override this, but just using the base64 command instead solved that.
  2. My signing step was wrong. I replaced rsautl with dgst as follows:

    openssl dgst -sha256 -sign private.key -out sig.txt unsigned.b64

RS256 signatures now pass validation in jwt.io

Complete working code (for posterity):

cat header.txt | tr -d '\n' | tr -d '\r' | base64 | tr +/ -_ | tr -d '=' > header.b64
cat payload.txt | tr -d '\n' | tr -d '\r' | base64 | tr +/ -_ |tr -d '=' > payload.b64
printf "%s" "$(<header.b64)" "." "$(<payload.b64)" > unsigned.b64
rm header.b64
rm payload.b64
openssl dgst -sha256 -sign -privateKey2.key -out sig.txt unsigned.b64
cat sig.txt | base64 | tr +/ -_ | tr -d '=' > sig.b64
printf "%s" "$(<unsigned.b64)" "." "$(<sig.b64)" > jwt.txt
rm unsigned.b64
rm sig.b64
rm sig.txt

How to create a Json Web Token (JWT) using OpenSSL shell commands?

I was able to recreate the JWT from https://jwt.io/

In your example, there was a hidden newline on the user secret. So in the below, I also add on that newline, purely to recreate the desired output.
Also the email address in your payload was not consistent, so for below I have used jordan@example.com.

I took a slightly different approach to the hmac step. I converted the user secret to hex bytes and used that as the key (using the hexkey option for the HMAC).

# Construct the header
jwt_header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)

# ans: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

# Construct the payload
payload=$(echo -n '{"email":"jordan@example.com"}' | base64 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//)

# ans: eyJlbWFpbCI6ImpvcmRhbkBleGFtcGxlLmNvbSJ9

# Store the raw user secret (with example of newline at end)
secret=$'bigsecretisveryhardtoguessbysneakypeopleright\n'

# Note, because the secret may have newline, need to reference using form $""
echo -n "$secret"

# Convert secret to hex (not base64)
hexsecret=$(echo -n "$secret" | xxd -p | paste -sd "")

# ans: 62696773656372657469737665727968617264746f67756573736279736e65616b7970656f706c6572696768740a

# For debug, also display secret in base64 (for input into https://jwt.io/)
echo -n "$secret" | base64

# ans: Ymlnc2VjcmV0aXN2ZXJ5aGFyZHRvZ3Vlc3NieXNuZWFreXBlb3BsZXJpZ2h0Cg==

# Calculate hmac signature -- note option to pass in the key as hex bytes
hmac_signature=$(echo -n "${jwt_header}.${payload}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:$hexsecret -binary | base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)

# Create the full token
jwt="${jwt_header}.${payload}.${hmac_signature}"

# ans: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvcmRhbkBleGFtcGxlLmNvbSJ9.C3MVjfmnul8dLNIgiv6Dt3jSefD07Y0QtDrOZ5oYSXo

How to generate JWT Assertion in python

Install pyjwt and cryptography first:

$ pip install pyjwt
$ pip install cryptography

Read your .pem file content and use that to generate assertion string like this:

import jwt

payload = {
"sub": "<EMAIL_ADDRESS>",
"aud": "https://api.einstein.ai/v2/oauth2/token",
"exp": "<EXPIRATION_SECONDS_IN_UNIX_TIME>" # UNIX timestamp (integer)
}

with open('einstein_platform.pem') as f:
assertion_string = jwt.encode(payload, f.read(), algorithm='RS256')

print(assertion_string)

Verify JWT with RS256 (asymmetric) in C#

  • Regarding your 1st question:

    According to your posted stack trace, you seem to be using .NET Core 3.1. This allows you to easily import your public X.509/SPKI key as follows:

    var pubKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQAB";

    RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
    rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(pubKey), out _); // import the public X.509/SPKI DER encoded key

    ImportSubjectPublicKeyInfo() is available since .NET Core 3.0.

    Edit start:
    In earlier versions of .NET Core (before 3.0) or in the .NET Framework ImportSubjectPublicKeyInfo() is not available, so at least .NET Standard 2.1 is required.

    For earlier versions, e.g. .NET Standard 2.0, one possibility is to use BouncyCastle, more precisely its Org.BouncyCastle.OpenSsl.PemReader class, which allows the import of public keys in X509/SPKI format (and, irrelevant for you, also in PKCS#1 format). In this answer you will find an example of how to use PemReader. PemReader processes, as the name suggests, a PEM encoding, i.e. the conversion to a DER encoding (i.e. the removal of header, footer and line breaks, as well as the Base64 decoding of the remainder) as required by ImportSubjectPublicKeyInfo() must not be done. Also note that PemReader expects at least one line break immediately after the header (-----BEGIN PUBLIC KEY-----\n) and a second one immediately before the footer (\n-----END PUBLIC KEY-----), the line breaks in the Base64 encoded body after every 64 characters are optional for PemReader.

    Another possibility is the package opensslkey providing the method opensslkey.DecodeX509PublicKey(), which can process an X509/SPKI key in DER encoding analogous to ImportSubjectPublicKeyInfo. Edit end

  • Regarding your 2nd question:

    There are several .NET standard versions, e.g. .NET Core 3.0 implements .NET Standard 2.1. The package System.IdentityModel.Tokens.Jwt 6.7.2-preview-10803222715 you are using requires .NET Standard 2.0.

    System.IdentityModel.Tokens.Jwt is a package that supports the creation and validation of JSON Web Tokens (JWT). In the case of the posted token, the validation could be implemented as follows:

    string jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iYv7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA";

    var tokenHandler = new JwtSecurityTokenHandler();
    bool verified = false;
    try
    {
    tokenHandler.ValidateToken(jwt, new TokenValidationParameters
    {
    ValidateAudience = false,
    ValidateLifetime = false,
    ValidateIssuer = false,
    IssuerSigningKey = new RsaSecurityKey(rsa)
    },
    out _);

    verified = true;
    }
    catch
    {
    verified = false;
    }

    Console.WriteLine("Verified: " + verified);

    The validation can be controlled via the validation parameters, i.e. via the 2nd parameter of ValidateToken(). Since the posted token does not contain the claims iss, aud and exp (this can be verified e.g. on https://jwt.io/), they are excluded from the validation in my example.

    In the tutorial Creating And Validating JWT Tokens In ASP.NET Core you will find a more detailed explanation, especially in the chapter Validating A Token.

    ValidateToken() essentially encapsulates the verification process of the JWT signature. A JWT is a data structure that consists of three parts: header, payload and signature, the individual parts being Base64url encoded and separated from each other by a dot.
    The signature is created using various algorithms, e.g. in your case RS256, which means that the data (Base64url encoded header and payload including separator) is signed using the algorithm RSA with PKCS#1 v1.5 padding and digest SHA256.
    The verification of a token corresponds to the verification of the signature, which can also be done solely with cryptographic APIs (i.e. without participation of System.IdentityModel.Tokens.Jwt), as it is done in the accepted answer of the linked question in the comment of @zaitsman.

Postman Pre-Request Script to create JWT for Google Service Account

I was able to solve this.

My private key had escaped white space characters in it (\n,\t)

I opened up google chrome dev tools and just saved it to a variable with template literals and console logged it out to get a properly formatted key.

Another option may just to do that in Postman as well.

After that, it worked.

Published Collection:
https://documenter.getpostman.com/view/8140651/SWECYFyf?version=latest

How to generate JWT Bearer Flow OAuth access tokens from a .net core client?

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('/', '_');
}


Related Topics



Leave a reply



Submit