I am currently using Identity Server 4 which is .Net Core based to issue JWT tokens. I have a .Net Core web api that has middleware in order for the JWT to be validated in the Startup.cs:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5005";
options.Audience = "api1";
});
As we can see, its not asking for much except the location of the token server and who the api is which is `api1. Obviously, its doing some more complex things under the hood.
I found a Java based equivalent to the middleware above, which validates a JWT:
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
RSAPublicKey publicKey = //Get the key instance
RSAPrivateKey privateKey = //Get the key instance
try {
Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("auth0")
.build(); //Reusable verifier instance
DecodedJWT jwt = verifier.verify(token);
} catch (JWTVerificationException exception){
//Invalid signature/claims
}
This is from HERE as was recommended by the jwt.io site.
Basically I want to be able to implement something in Java that is doing the same thing as the .Net Core code above, but its clearly asking for things like a Public and Private key that I have no idea how to provide at this point, as the JWT is coming through the header of the request.
Under the hood, the Microsoft JWT middleware is going to IdentityServer's discovery endpoint and loading in configuration such as the issuer and JWKS (public keys). The discovery document is always hosted on /.well-known/openid-configuration.
To validate the token, you will at least need the public keys from the JWKS. In the past I've loaded it it using the jwks-rsa library: https://www.scottbrady91.com/Kotlin/JSON-Web-Token-Verification-in-Ktor-using-Kotlin-and-Java-JWT
When validating the access token, as a minimum, you'll also need to check the token's audience (is the token intended for you) and if it has expired.
Related
I have a token issued by Azure AD. I need to verify the token in my API, which is running on IBM platform.
I am writing the token verifier in Java,using
claims = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(accessToken);
The signingKeyResolver is returning a public key. No issues.
I use the following code to get the public key :
BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(key.getN()));
BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(key.getE()));
RSAPublicKeySpec publicSpec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory factory = KeyFactory.getInstance("RSA");
Provider provider = factory.getProvider();
PublicKey pubkey = factory.generatePublic(publicSpec);
return factory.generatePublic(publicSpec);
Can any one throw some light on Why the signature is invalid ? I observe one thing when I create the factory, the Provider name shows IBMJCEPLUS. Does it have any impact on the key generation ? If so, how do I create the correct factory for Microsoft issued keys ?
Looking for a general advice in creating the factory..
Several things.
the sample is incomplete, and seems mixed up. Is this the role of consumer doing a signature verification?
You are generating a public key which is not an expected behaviour for the verify method of a consumer, you typically 'use' the JWK retrieved from the JWKS URL provided by the JWT producer (AzureAD) not generate a new public key that was not used to generate the signature? To verify a signature, you need to reproduce the same signature by having all the inputs the server used when they generated it. A signature is simply a representation of the payload to ensure the payload was not modified, i.e. integrity check. This is the purpose of the signature and why you must have the same inputs to generate it on the client, wither you gain the inputs used out-of-band somehow (not how AzureAD works) or you use the JWKS to retrieve the appropriate public key for the verify method.
It appears you also expect there to be encrypted claims, based on the RSA reference in your example. If this is intended you would also require you to decipher the payload to access the cleartext. This requires you to have implemented the appropriate PKI. As a consumer (client) the private key must be generated and never shared (or retrieved). Encryption implemented correctly ensures that only the client intended to read the cleartext data 'can' decipher it with the private key, which it itself generated for this purpose, and only it (the client) has control of the private key. Have you the appropriate PKI to do the encrypted JWT properly? Or are you expecting to only utilise the HMAC-based JWT that has no encryption at all and relies on only signature verification (and therefore all claims are public, not protected).
To summaries, there are 2 distinct modes of JWT; 1) HMAC-based that uses signatures where the same inputs are needed on client to verify the signature as was used on server to generate the signature, and this is for integrity only, there is no encryption to protect data. 2) encryption-based JWT that uses a public key for signature verification and the private key used by clients to decipher the encrypted claims data.
1 requires PKI, 2 doesn't.
2 has no data protection and is useful for stateless claims that are intended to be public and therefore verification of the 'claims' before use is how you gain security assurance. Whereas 1 offers the security characteristic of confidentiality and if the PKI is done correctly (private keys are never shared) then the confidential claims data can have more private use cases.
I am implementing jose4j in my Java application to verify the signature of the access token issued by Azure.
The application works fine, however, I came across this documentation about Signing Key rollover. Does jose4j take care of it automatically when using the HttpsJwksVerificationKeyResolver?
I am currently using the following snippet to build the JwtConsumer
String azureKeyDiscoveryUrl =
"https://login.microsoftonline.com/{my-tenant-id}/discovery/keys";
HttpsJwks azureKeyDiscovery = new HttpsJwks(azureKeyDiscoveryUrl);
HttpsJwksVerificationKeyResolver azureJwksKeyResolver = new HttpsJwksVerificationKeyResolver(azureKeyDiscovery);
JwtConsumer azureJwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setAllowedClockSkewInSeconds(30)
.setRequireIssuedAt()
.setRequireNotBefore()
.setVerificationKeyResolver(azureJwksKeyResolver)
.setExpectedAudience("my-audience")
.setJwsAlgorithmConstraints(new AlgorithmConstraints(
AlgorithmConstraints.ConstraintType.WHITELIST, AlgorithmIdentifiers.RSA_USING_SHA256))
.build();
JwtClaims claims = azureJwtConsumer.processToClaims("tokenStringHere");
Yes, assuming Azure does the right/reasonable thing with the https://login.microsoftonline.com/{my-tenant-id}/discovery/keys endpoint, which I think they do, it'll work.
I created a small vertx auth-server which signs/generates JWT tokens using public/private key.
PrivateKey privateKey = CertUtil.getPrivateKey("config/private_key.der");
PublicKey publicKey = CertUtil.getPublicKey("config/public_key.der");
// Create a JWT Auth Provider
JWTAuth jwt = JWTAuth.create(vertx, new JWTAuthOptions()
.setPubSecKeys(List.of(new PubSecKeyOptions()
.setAlgorithm("RS256")
.setPublicKey(Base64.getEncoder().encodeToString(publicKey.getEncoded()))
.setSecretKey(Base64.getEncoder().encodeToString(privateKey.getEncoded())))));
// protect the API
router.route("/api/*").handler(JWTAuthHandler.create(jwt, "/api/new-token"));
// this route is excluded from the auth handler
router.get("/api/new-token").handler(ctx -> this.generateAndSendToken(ctx, jwt));
// this is the secret API
router.get("/api/protected").handler(ctx -> {
ctx.response().putHeader("Content-Type", "text/plain");
ctx.response().end("a secret you should keep for yourself...");
});
vertx.createHttpServer().requestHandler(router).listen(8080);
now when i access /api/new-token from client i get a JWT token back signed from my auth-server above. however I have some open questions:
How is auth-server making sure that client has server public key and it is genuine?
How can client send public key to auth-server?
How can i make /api/new-token secure so only legitimate client can connect to it?
Why don't you delegate this task to KeyCloak an Open Source Identity and Access Management. It adds authentication to your app and secures services with minimum fuss.
We have used it into our project and it works pretty well!
To plug it with Vert.x, you can follow these tutos :
https://vertx.io/blog/vertx-3-and-keycloak-tutorial/
https://medium.com/#alexpitacci/vert-x-and-keycloak-working-together-9d459a5ebd9e
http://paulbakker.io/java/jwt-keycloak-angular2/
https://piotrminkowski.wordpress.com/2017/09/15/building-secure-apis-with-vert-x-and-oauth2/
I am working with JWT api , i have generated a token using:
public void addAuthentication(HttpServletResponse response, String name) {
// We generate a token now.
String JWT = Jwts.builder()
.setSubject(name)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
response.addHeader(headerString, tokenPrefix + " " + JWT);
}
abd secret token prefix being string , however it does generate token, but when i copy it into
https://jwt.io/#debugger
It does undecode it and reveal all informations stored inside it , did i do something wrong or its as it should be? This does not seem secure at all.
Thanks for answers
This is secure at all. Don't worry about it. Just store your key in a secure way.
The remarkable point is decoded information can not be changed or token can not be generated without the key.
Avoid storing essential information in token like credit card number or password etc. I'm sure that you are not using the JWT for this purpose.
If you want to hide the payload, the JWT specification allows use encryption (see Json Web Encryption-JWE at RFC). If auth0 does not support it, you have a lot of libraries listed in jwt.io
Check this topics
Is JWT that secure?
Why are JWT's secure?
It's secure in the sense it tells you who the user is and what claims they have. You can verify that the user's identity and claims are valid by checking the JWTs signature.
Also, see this: https://stackoverflow.com/a/38459231/2115684
If you want to hide the payload, the JWT specification allows use encryption (see Json Web Encryption-JWE at RFC). If auth0 does not support it, you have a lot of libraries listed in jwt.io
I'm new to JWT, learning through standalone code to understand JWT API's. Below code sign and encrypt JWT token from sender's end and it get validated at receiver's end.
Library: JOSE 0.4.1
package com.one00bytes.jwt;
public class JWTSignEncryption {
public static void main(String[] args) throws Exception {
/***************************SENDER'S END ***********************************/
JwtClaims claims = new JwtClaims();
claims.setAudience("Admins");
claims.setIssuer("CA");
claims.setSubject("users");
claims.setClaim("email", "users#test.com");
claims.setClaim("Country", "Antartica");
System.out.println(claims.toJson());
//SIGNING
RsaJsonWebKey jsonSignKey = RsaJwkGenerator.generateJwk(2048);
JsonWebSignature jws = new JsonWebSignature();
jws.setKey(jsonSignKey.getPrivateKey());
jws.setPayload(claims.toJson());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA512);
String signedJwt = jws.getCompactSerialization();
System.out.println("Signed ::" + signedJwt);
//ENCRYPTING
RsaJsonWebKey keyEncrypt = RsaJwkGenerator.generateJwk(2048);
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey contentEncryptKey = keyGen.generateKey();
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setKey(keyEncrypt.getPublicKey());
jwe.setPayload(signedJwt);
jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256);
jwe.setContentEncryptionKey(contentEncryptKey.getEncoded());
jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_256_GCM);
SecureRandom iv = SecureRandom.getInstance("SHA1PRNG");
jwe.setIv(iv.generateSeed(32));
String encryptedJwt = jwe.getCompactSerialization();
System.out.println("Encrypted ::" + encryptedJwt);
/***************************RECEIVER'S END ***********************************/
JwtConsumer consumer = new JwtConsumerBuilder()
.setExpectedAudience("Admins")
.setExpectedIssuer("CA")
.setRequireSubject()
.setDecryptionKey(keyEncrypt.getPrivateKey())
.setVerificationKey(jsonSignKey.getPublicKey())
.build();
JwtClaims receivedClaims = consumer.processToClaims(encryptedJwt);
System.out.println("SUCESS :: JWT Validation :: " + receivedClaims);
}
}
Observing below exception when running this program:
Exception in thread "main" org.jose4j.jwt.consumer.InvalidJwtException: Unable to parse JWT Claim Set JSON: eyJhbGciOiJSUzUxMiJ9.eyJhdWQiOiJBZG1pbnMiLCJpc3MiOiJDQSIsInN1YiI6InVzZXJzIiwiaWF0IjoxNDM0NTM0MDgxLCJleHAiOjE0MzQ1MzQ2ODEsImp0aSI6IjJxUUpuMDVGY3RrLWF1VG1vVktuWXciLCJuYmYiOjE0MzQ1MzM5NjEsImVtYWlsIjoidXNlcnNAMTAwYnl0ZXMuY29tIiwiQ291bnRyeSI6IkFudGFydGljYSIsImhvYmJpZXMiOlsiQmxvZ2dpbmciLCJQbGF5aW5nIGNhcmRzIiwiR2FtZXMiXX0.soY_5Hbam569I-CnUW1F4GWdaqprh-XAOtAMOcb7zZSiRcIhXYUdJjEslrDbwphAP135SvmoXO4nVaVmo-d8oWREFYUeXEDzHbrqHNp7pp5pH6hGTJ5C4uE1UVzZ4bis3g_KEgZvEn31NnV4RcU_oRn2Q4inkrTlYKY-juEtCmpPQ0sSP4GiDbwVIfCj-kxZsKh_i9n28SSK890K3DIGiFWOUDwrnY4Yfr1UffsUS9ovyhtqrOcN4YsJR4XzGPaLehlR-qD7eOdAdmVb8RDtGKufNuCd7Q9OFfeKzBmGITHsvd6IPVYLLCfSCzO6PqQSIzkupl5D6HqoOqID8JZLxA
at org.jose4j.jwt.JwtClaims.<init>(JwtClaims.java:50)
at org.jose4j.jwt.JwtClaims.parse(JwtClaims.java:56)
at org.jose4j.jwt.consumer.JwtConsumer.process(JwtConsumer.java:267)
at org.jose4j.jwt.consumer.JwtConsumer.processToClaims(JwtConsumer.java:115)
at com.one00bytes.jwt.JWTSignEncryption.main(JWTSignEncryption.java:76)
Caused by: org.jose4j.lang.JoseException: Parsing error: org.jose4j.json.internal.json_simple.parser.ParseException: Unexpected character (e) at position 0.
at org.jose4j.json.JsonUtil.parseJson(JsonUtil.java:66)
at org.jose4j.jwt.JwtClaims.<init>(JwtClaims.java:45)
... 4 more
Caused by: org.jose4j.json.internal.json_simple.parser.ParseException: Unexpected character (e) at position 0.
at org.jose4j.json.internal.json_simple.parser.Yylex.yylex(Yylex.java:612)
at org.jose4j.json.internal.json_simple.parser.JSONParser.nextToken(JSONParser.java:269)
at org.jose4j.json.internal.json_simple.parser.JSONParser.parse(JSONParser.java:118)
at org.jose4j.json.internal.json_simple.parser.JSONParser.parse(JSONParser.java:81)
at org.jose4j.json.JsonUtil.parseJson(JsonUtil.java:62)
... 5 more
Signed JWT
eyJhbGciOiJSUzUxMiJ9.eyJhdWQiOiJBZG1pbnMiLCJpc3MiOiJDQSIsInN1YiI6InVzZXJzIiwiZW1haWwiOiJ1c2Vyc0B0ZXN0LmNvbSIsIkNvdW50cnkiOiJBbnRhcnRpY2EifQ.5Xu7v2MosIQmtAOlqfM2PE9eJeT0iZzL9x6RIvqx_PAHKer0ylo-0wT9eON_qX1H_QZekTWMf8ok4fxdZNv2KP_AkNqSKLXYJ65TjPnfcX8-dooDJM9txfRWOFqJWx4yj4CTMPNR6rNhizkC9jUaLisPIjogc_a_61qTSnvHXFnuaYmkovN2Y3WfuXjhUZCH98hodRL_ATg1_SpO0bPb7_N1Z76yrcv0RYQan0Y5kICWYdhHlk8Dw6I2fLMVsl3HiYiRq4XBJE8AY_g742Uq5kTS62PKohg3IjfRa-g2rjgKo1XW2sRLVc7vnns2L3TqESo5vgvorTjKnCTQKuHpIg
Encrypted JWT
eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.lZ2nqCeiPzsPmJShsrDD3uA55-06A649CMtwOyuY9nNzMtUGyzV-G8qc4w4ui1uWrtzypBs5Eyq4GfjnTtVHbcDVkS1HVc3tfxNAPY8dfjVrWNz59HyKt4bCjBdqqhBOdZezLtWB9aoWIwZoHLf4D8aUcVUtDsFELVcScmiQNtzHwvpDHZb4oxRfPl-OuOTkKA23C8lnnDMO1KUy8ZXHD4p0jQKAcaV877gYm8NbHDwOBEf-ItWJOGx2jV60apWd0hKqwfFR2QKD9wmGgXpbFZ08ro7X2fj8rTgKWhDgoBT_JVZdVFhVI4T4RLRDrCJqkyeciXhLm7W_xNhWBXAMrA.94SuB596ZLuUtw53wrofwN5jZXfT5f-ZarJhQc9Mj0M.0Ow5DXfilYX3ty49H4lNMNPljlWAFASc49zljhRSIIUSlmUHLZo0SAezn-n_FdxexAIYLk_FtRgnkMHDEyxJ1V1yHhqa1Jvdb36lTYyptqCJhMkOV1XGn58L4Z9QQmdrIZnn5iHxZ9-N1Jfjs0eoKiLBgR9O7ZEcs7QrWZVT6n_HrGrIloYQu_lFgmk5O7k47_15CVXaFqIohpHXETejoHEwjQj-iTToNRaHWNFAKvlpUBz4mUgk9RSIQCxK1GxxS8wxP44w5G4HdOIjFNwTsRDXeSZy0mU9zTNUCmDEUT9MFESfmVU1nPurdT-VoiPvVklbJZW8Sas0hWgqQkdQdP35nFY1sjCgfMB9iYUeEU-TCE219wkm1XXrLJwLEYZclL_4ckl4zExo2wb3Czwd8f5iO9fBQQWZ4mdwThK4VtZaPs1JEkxwGLI0SHA8Jr-e2PsDrkGEnxs74FsJ5MKluU2ZKvKcGXyQPaaTRa0ecJLD5-YYBuTtxOnU3gM_5aZm97pd_wiPk_h81r5aiwjSfRF3Ihxp37KNPfNOMJoA9xe2F51m1AvmjrOUgSM156LwmFyJFebVfarb9NPtJ_q1wU891sCu2Vmv520BR4QfIc-ayIwTVxLgZSN-BP7PhEJb_x8.XhZpINBxRdFFEgwPTcAgJg
Same code runs seperately for signing and encryption, but didn't run, if I include both.
Please help me to understand what I'm doing wrong.
Thanks In Advance
For a nested JWT (i.e. JWE[JWS[JSON Claims]] which is what you're dong), the "cty" (content type) header of the the JWE is supposed to have a value of "JWT" to indicate that the payload is itself a JWT. The definition of"cty" in the JWT spec, RFC 7519, talks about that a bit more. It helps the consumer/receiver know how to process things.
The exception you're seeing is the result of the library trying to parse the JWS compact serialization, which is the payload of the JWE, as JSON.
According to spec, you really should set the cty header to "JWT" on the JWE, which indicates that the JWE payload is itself a JWT. That can be done with jwe.setHeader(HeaderParameterNames.CONTENT_TYPE, "JWT"); or jwe.setContentTypeHeaderValue("JWT") as of v0.4.2.
You can also tell the JwtConsumer to be a bit more liberal in processing and make a best effort when the cty header isn’t present and the payload doesn’t parse as JSON but can be parsed into a JOSE object. This can be done with .setEnableLiberalContentTypeHandling() on the JwtConsumerBuilder.
Couple more observations:
You don't need to set the content encryption key or the IV on the JWE. The library uses a secure random to generate those for you with the appropriate lengths. So the following should be sufficent,
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setHeader(HeaderParameterNames.CONTENT_TYPE, "JWT");
jwe.setKey(keyEncrypt.getPublicKey());
jwe.setPayload(signedJwt);
jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256);
jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_256_GCM);
String encryptedJwt = jwe.getCompactSerialization();
System.out.println("Encrypted ::" + encryptedJwt);
Also, I'm guessing from the use of RSA_OAEP_256 and AES_256_GCM that you're using Bouncy Castle. I'd strongly recommend upgrading to jose4j 0.4.4 due to a security vulnerability that was identified when using the library with the Bouncy Castle security provider. See the Release Notes on v 0.4.4 for more info https://bitbucket.org/b_c/jose4j/wiki/Release%20Notes#!jose4j-044-july-24-2015
A JWT has as its payload, or Message, the UTF-8 representation of the Claims Set. From RFC 7519:
Let the Message be the octets of the UTF-8 representation of the
JWT Claims Set.
This is the case for both signed JWTs (which are JWS objects), and encrypted JWTs (using JWE):
if the JWT is a JWE, create a JWE using the Message as
the plaintext for the JWE; all steps specified in JWE for
creating a JWE MUST be followed.
Accordingly, for verification of an encrypted JWT, the payload is interpreted as a Claims Set:
Else, if the JWT is a JWE, follow the steps specified in
JWE for validating a JWE. Let the Message be the resulting
plaintext.
The mistake you have made in your program is using the serialization of the signed JWT as the payload of a JWE, but then attempting to process the resulting object as n encrypted JWE. Accordingly, the library attempts to interpret a serialized signed JWT (the JWS Flattened Serialization) as a serialized JWT Claims Set (a JSON object). This explains the exception you are getting:
Caused by: org.jose4j.lang.JoseException: Parsing error:
org.jose4j.json.internal.json_simple.parser.ParseException:
Unexpected character (e) at position 0.
It appears that you are attempting to produce a JWT that is both encrypted and authenticated. All JWE algorithms are authenticated encryption algorithms, so there is no need to do anything with JWS to achieve this - an encrypted JWT is sufficient.