How to validate signature of JWT from jwks without x5c
Using x5c
is just one way, but you can also retrieve the public key with the parameters e
(public exponent) and n
(modulus), which is also documented on the jose-jwt github page:
//If kid was found then load public key
if (jwkkey != null)
{
RSACryptoServiceProvider key = new RSACryptoServiceProvider();
key.ImportParameters(new RSAParameters
{
Modulus = Base64Url.Decode(jwkkey.n),
Exponent = Base64Url.Decode(jwkkey.e)
});
}
// get the public key in PEM format, e.g. to use it on jwt.io
var pubkey = Convert.ToBase64String(key.ExportSubjectPublicKeyInfo());
const string pemHeader = "-----BEGIN PUBLIC KEY-----";
const string pemFooter = "-----END PUBLIC KEY-----";
var publicKeyPem = pemHeader + Environment.NewLine + pubkey + Environment.NewLine + pemFooter;
var o = Jose.JWT.Decode(jsonToken.RawData, key);
You can also export the public key in PEM format again as shown in the code above, which will look like this:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIdJV4qWKyt3wkS66yBG5Ii9ew+eofuPU49TjlRIU5Iu5jX2mRMoHdcI7V78iKYSQHKYxz17cqzQyERxKnEiDgy/gwouStRgvPdm3H4rq//7p0t15SunsG2T1rEVf0sZEDnQ5qRkm7iqs6ZG1NqqIUtnOTd1Pd1MhbEqeENFtaPHvN37eZL82WmsQlJviFH4I9iZQVR/QT4GREQlRro8IjJTaloUyeDQTOQ+4ll1+4+g/ug2tZ+s9xleLzl5L9ZKSVJFhtMLn8WGaVldagarwa7kMLfuiVe8B5Lr7poQa4NCAR54ECPWoOHrABdPZKrkkxjVypTXUzL5cPzmzFC2xwIDAQAB
-----END PUBLIC KEY-----
and later use that key to manually verify your token on https://jwt.io
(key export corrected after a hint from @Topaco)
Verify a signature in JWT.IO
jwt.io
says to enter the key
Public Key or Certificate. Enter it in plain text only if you want to verify a token
so I have converted the JSON Web Key to a PEM format guessing it would need a base64 format, and it works!.
This is the public key built from modulus and exponent
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqnTksBdxOiOlsmRNd+mMS2M3o1IDpK4uAr0T4/YqO3zYHAGAWTwsq4ms+NWynqY5HaB4EThNxuq2GWC5JKpO1YirOrwS97B5x9LJyHXPsdJcSikEI9BxOkl6WLQ0UzPxHdYTLpR4/O+0ILAlXw8NU4+jB4AP8Sn9YGYJ5w0fLw5YmWioXeWvocz1wHrZdJPxS8XnqHXwMUozVzQj+x6daOv5FmrHU1r9/bbp0a1GLv4BbTtSh4kMyz1hXylho0EvPg5p9YIKStbNAW9eNWvv5R8HN7PPei21AsUqxekK0oW9jnEdHewckToX7x5zULWKwwZIksll0XnVczVgy7fCFwIDAQAB
-----END PUBLIC KEY-----
After some attemps I decided to write a simple test program to check if JWT signature is correct or it was a key format issue. You can test it (Java 8). It is fully functional
package test;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
public class JWKTest {
private static final String[] HEX_TABLE = new String[]{
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f",
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f",
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f",
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f",
"50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f",
"60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f",
"70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f",
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f",
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f",
"a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af",
"b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf",
"c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf",
"d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df",
"e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef",
"f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff",
};
public static String toHexFromBytes(byte[] bytes) {
StringBuffer rc = new StringBuffer(bytes.length * 2);
for (int i = 0; i < bytes.length; i++) {
rc.append(HEX_TABLE[0xFF & bytes[i]]);
}
return rc.toString();
}
// Build the public key from modulus and exponent
public static PublicKey getPublicKey (String modulusB64u, String exponentB64u) throws NoSuchAlgorithmException, InvalidKeySpecException{
//conversion to BigInteger. I have transformed to Hex because new BigDecimal(byte) does not work for me
byte exponentB[] = Base64.getUrlDecoder().decode(exponentB64u);
byte modulusB[] = Base64.getUrlDecoder().decode(modulusB64u);
BigInteger exponent = new BigInteger(toHexFromBytes(exponentB), 16);
BigInteger modulus = new BigInteger(toHexFromBytes(modulusB), 16);
//Build the public key
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory factory = KeyFactory.getInstance("RSA");
PublicKey pub = factory.generatePublic(spec);
return pub;
}
public final static void main (String argv[]) throws NoSuchAlgorithmException, InvalidKeySpecException, SignatureException, NoSuchProviderException, InvalidKeyException{
String exponentB64u = "AQAB";
String modulusB64u = "qnTksBdxOiOlsmRNd-mMS2M3o1IDpK4uAr0T4_YqO3zYHAGAWTwsq4ms-NWynqY5HaB4EThNxuq2GWC5JKpO1YirOrwS97B5x9LJyHXPsdJcSikEI9BxOkl6WLQ0UzPxHdYTLpR4_O-0ILAlXw8NU4-jB4AP8Sn9YGYJ5w0fLw5YmWioXeWvocz1wHrZdJPxS8XnqHXwMUozVzQj-x6daOv5FmrHU1r9_bbp0a1GLv4BbTtSh4kMyz1hXylho0EvPg5p9YIKStbNAW9eNWvv5R8HN7PPei21AsUqxekK0oW9jnEdHewckToX7x5zULWKwwZIksll0XnVczVgy7fCFw";
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJub25jZSI6IjYzNjA3MDM0OTc3NDIzODg2NS5OMlkxTldKbU1EZ3RZbU13TkMwME9XWTNMVGt5TlRJdE9ERXpOell4Wm1NME0yVmxNV1l5TkdOaFlXTXRaVEpqT1MwME4yRmpMVGd6WmpVdFpXWTVOVEEwWmpFMU1qWTEiLCJpYXQiOjE0NzE0MzgxODIsImF0X2hhc2giOiJLWUJpVkl1Uy1YZERzU3NHcWU5dTJBIiwic3ViIjoiMSIsImFtciI6InBhc3N3b3JkIiwiYXV0aF90aW1lIjoxNDcxNDM4MTgyLCJpZHAiOiJpZHNydiIsImlzcyI6Imh0dHBzOi8vZWx3ZWJhcHBsaWNhdGlvbjEuYXp1cmV3ZWJzaXRlcy5uZXQvaWRlbnRpdHkiLCJhdWQiOiJtdmMiLCJleHAiOjE0NzE0Mzg0ODIsIm5iZiI6MTQ3MTQzODE4Mn0.Ehck2-rA09cJzlfURhDMp-WcXm_t_dl-u0Mli3exdv1HxX8i77x5VfFPM6rP4lcpI3lpN8Yj-FefZYDTUY_UmxCYvXf6ILSrhzEfQVaXSPKX1RUQQIDJGPU6NuFLcR416JpUAkE8joYae3WPj5VsM4yNENGGjUANm4qgj6G_mYy_BiXcSqvRGRYwW5GHDsnnANrIw4oktIYS05yCbjdiNYgQZ043L6Pb2p-5eTPCFqG7WRHp208dhg8D3nhtYEov2Kxod93oKHXSp1zf-Ot0cadk6Ss4fClaTE9S1f29lbwxw7ZxI1L3R4oOL3FZPSSHGp4d3a3AdUKOjKvvTVPv6w";
//Build the public key from modulus and exponent
PublicKey publicKey = getPublicKey (modulusB64u,exponentB64u);
//print key as PEM (base64 and headers)
String publicKeyPEM =
"-----BEGIN PUBLIC KEY-----\n"
+ Base64.getEncoder().encodeToString(publicKey.getEncoded()) +"\n"
+ "-----END PUBLIC KEY-----";
System.out.println( publicKeyPEM);
//get signed data and signature from JWT
String signedData = jwt.substring(0, jwt.lastIndexOf("."));
String signatureB64u = jwt.substring(jwt.lastIndexOf(".")+1,jwt.length());
byte signature[] = Base64.getUrlDecoder().decode(signatureB64u);
//verify Signature
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
boolean v = sig.verify(signature);
System.out.println(v);
}
}
Verifying JWT Signature using public key endpoint
x5c
contains the certification chain. The first certificate of the chain must match with the key value represented by the other values in the JWK, in this case n
and e
, therefore the public key extracted from x5c[0]
and the one built with n
and e
must be exactly the same
JWK values are encoded in base64url, not in base64. Change
BigInteger modulus = new BigInteger(1, Base64.decodeBase64(jsonKey.getN()));
BigInteger exponent = new BigInteger(1, Base64.decodeBase64(jsonKey.getE()));
with
BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(jsonKey.getN()));
BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(jsonKey.getE()));
How to parse a set of JWK with x5c and verify JWT?
The reason for that error (required field e is missing
) is that the JWK Set from this url is invalid. Even if a JWK contains x5c
it still must contain the other required public key members for that specific kty
, which for the RSA keys listed in that URL means having n
and e
.
How to verify JWT signature with JWK in Go?
Below is an example of JWT decoding and verification. It uses both the jwt-go and jwk packages:
package main
import (
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/lestrrat-go/jwx/jwk"
)
const token = `eyJhbGciOiJSUzI1NiIsImtpZCI6Ind5TXdLNEE2Q0w5UXcxMXVvZlZleVExMTlYeVgteHlreW1ra1h5Z1o1T00ifQ.eyJzdWIiOiIwMHUxOGVlaHUzNDlhUzJ5WDFkOCIsIm5hbWUiOiJva3RhcHJveHkgb2t0YXByb3h5IiwidmVyIjoxLCJpc3MiOiJodHRwczovL2NvbXBhbnl4Lm9rdGEuY29tIiwiYXVkIjoidlpWNkNwOHJuNWx4ck45YVo2ODgiLCJpYXQiOjE0ODEzODg0NTMsImV4cCI6MTQ4MTM5MjA1MywianRpIjoiSUQuWm9QdVdIR3IxNkR6a3RUbEdXMFI4b1lRaUhnVWg0aUotTHo3Z3BGcGItUSIsImFtciI6WyJwd2QiXSwiaWRwIjoiMDBveTc0YzBnd0hOWE1SSkJGUkkiLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsInByZWZlcnJlZF91c2VybmFtZSI6Im9rdGFwcm94eUBva3RhLmNvbSIsImF1dGhfdGltZSI6MTQ4MTM4ODQ0MywiYXRfaGFzaCI6Im1YWlQtZjJJczhOQklIcV9CeE1ISFEifQ.OtVyCK0sE6Cuclg9VMD2AwLhqEyq2nv3a1bfxlzeS-bdu9KtYxcPSxJ6vxMcSSbMIIq9eEz9JFMU80zqgDPHBCjlOsC5SIPz7mm1Z3gCwq4zsFJ-2NIzYxA3p161ZRsPv_3bUyg9B_DPFyBoihgwWm6yrvrb4rmHXrDkjxpxCLPp3OeIpc_kb2t8r5HEQ5UBZPrsiScvuoVW13YwWpze59qBl_84n9xdmQ5pS7DklzkAVgqJT_NWBlb5uo6eW26HtJwHzss7xOIdQtcOtC1Gj3O82a55VJSQnsEEBeqG1ESb5Haq_hJgxYQnBssKydPCIxdZiye-0Ll9L8wWwpzwig`
const jwksURL = `https://companyx.okta.com/oauth2/v1/keys`
func getKey(token *jwt.Token) (interface{}, error) {
// TODO: cache response so we don't have to make a request every time
// we want to verify a JWT
set, err := jwk.FetchHTTP(jwksURL)
if err != nil {
return nil, err
}
keyID, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("expecting JWT header to have string kid")
}
if key := set.LookupKeyID(keyID); len(key) == 1 {
return key[0].Materialize()
}
return nil, fmt.Errorf("unable to find key %q", keyID)
}
func main() {
token, err := jwt.Parse(token, getKey)
if err != nil {
panic(err)
}
claims := token.Claims.(jwt.MapClaims)
for key, value := range claims {
fmt.Printf("%s\t%v\n", key, value)
}
}
Related Topics
JSON.Net Adding Backslash While Returning JSON Serialized String
How to Highlight Wrapped Text in a Control Using the Graphics
How to Specify Mapping Rule When Names of Properties Differ
How to Get the List of All Printers in Computer
How to Check If a Class Inherits Another Class Without Instantiating It
Button in a Column, Getting the Row from Which It Came on the Click Event Handler
How to Store and Retrieve Credentials on Windows Using C#
What's the Difference Between Utf8/Utf16 and Base64 in Terms of Encoding
Calculating Hmacsha256 Using C# to Match Payment Provider Example
What Is C# Analog of C++ Std::Pair
Visual Studio Installer > How to Launch App at End of Installer
Make a Specific Column Only Accept Numeric Value in Datagridview in Keypress Event