jjwt
jjwt copied to clipboard
Verify JWT with JWKS
Is there a way to verify a JWT with JWKS?
Not directly, but it’s pretty easy to add a custom key resolver to do it. https://github.com/okta/okta-jwt-verifier-java/blob/master/impl/src/main/java/com/okta/jwt/impl/jjwt/RemoteJwkSigningKeyResolver.java
(Mobile, sorry for the brief response) If this doesn’t help let me know
Thanks for the quick response! How is the access token then verified?
You can use JJWT to validate an JWT access token, but each IdP will have different guidelines as to which additional claims to validate. Which IdP are you using?
Note: any recommendations from an IdP would always be in addition the standard JWT validation (which JJWT does automatically)
Just a note: this will be easier when #113 is complete as JWK support is required for JWE.
This issue has been automatically marked as stale due to inactivity for 60 or more days. It will be closed in 7 days if no further activity occurs.
@EPilz when you created this issue, how specifically were you expecting to verify JWTs with JWKs? Do you mean like @bdemers suggested? If not, what is the use case (or usage paradigm) you wanted to support?
JWKs are fully supported in the master
branch via #178, and this functionality will be released in 0.12.0
. But I'm curious if there's a use case that is being requested beyond what is currently documented in the README? Please let us know, otherwise I'll close this issue (trying to close issues to prepare for the 0.12.0 release).
I'm using 0.12.1 and unclear what fully supported means. I'm expecting to be able to do something like this:
String webKeys = ... // fetch jwks.json from well-known URL
Jwk<?> jwk = Jwks.parser().build().parse(webKeys);
var payload = Jwts.parser().verifyWith(jwk).build().parse(accessToken).getPayload();
This doesn't work, and I can't figure out from the voluminous readme what I'm supposed to do with the Jwk object once I have it. I can see how I could parse it myself and then write a key locator that digs for the right kid
match, but I suspect I'm missing something.
@jkellyinsf Jwk
has a toKey()
method to represent it as a java.security.Key
instance, so you can do:
Jwts.parser().verifyWith(jwk.toKey())...
Or return jwk.toKey()
from a Locator<Key>
implementation.
There might be a chance in a future version for Jwk
to directly implement java.security.Key
so you can use it without calling toKey()
, but the Key
interface imposes implementation burdens around getFormat()
and getEncoded()
that we didn't want to tackle on the last release.
Does that help? I'm happy to clarify anything that we might be missing, and then add that to the README, because odds are high that if you have questions, others will as well :)
Thanks @lhazlewood, I'm struggling with that. I can get it to compile if I cast jwk.toKey() to either PublicKey or SecretKey. But regardless the Jwk parse fails with "JWK is missing required 'kty' (Key Type) parameter," I presume because the jwks.json follows this structure and contains more than one key.
@jkellyinsf that's because what what you linked to is not a Jwk
, it is a JwkSet
. Try:
Jwks.setParser().build().parse(jwkSetJson);
Ah, that makes sense. So that leaves me with a Set<Jwk<?>>
. Do I then implement a Locator that loops through the keys and picks the one whose kid
matches that in the jwt header?
@jkellyinsf I think that makes sense. FWIW, depending on the size of the JwkSet
, the first time you read it, you could iterate over the collection and put them in a map with the map key being the kid
. Then for your Locator
implementation, you could have something like:
@Override // extends from LocatorAdapter<Key>
protected Key locate(ProtectedHeader header) {
Jwk<?> jwk = keyMap.get(header.getKeyId());
return jwk.toKey();
}
which makes key location/lookups a constant-time operation.
Just to be careful however, if it were me, I would assert that the key being referenced in the header is allowed to be used for that particular JWS or JWE.
For example, if the header is a JwsHeader
indicating a JWS is being parsed, you could check the referenced jwk's operations (via jwk.getOperations()
) and if the operations exist (are not empty), but do not include Jwks.OP.SIGN
, then the referenced key is not allowed to be used. (or if the Jwk
is an AsymmetricJwk
, check it's asymmetricJwk.getPublicKeyUse()
and ensure it's allowed to be used for the particular JWS or JWE.
We're going to automate these additional kinds of checks in a future release, but we didn't have time to automate that for the 0.12.0
release.
Thanks, that's a good idea. For the benefit of future readers and GPT spiders, here's what I got to work:
// At initialization
String webKeys = Methanol.create().send(MutableRequest.GET(jwksUrl), HttpResponse.BodyHandlers.ofString()).body();
Map<String, ? extends Key> keyMap = Jwks.setParser().build()
.parse(webKeys).getKeys().stream()
.collect(toMap(Identifiable::getId, Jwk::toKey));
JwtParser jwtParser = Jwts.parser().keyLocator(header -> keyMap.get(header.getOrDefault("kid", "").toString())).build();
// ...
// Upon receiving a token
Claims claims = (Claims) jwtParser.parse(token).getPayload();
I appreciate the help, @lhazlewood!
@jkellyinsf don't forget that all JJWT Parser
s have a parse(InputStream)
method, so you could pass the HTTP content stream directly, and that would have better performance, eliminating the intermediate String/byte arrays on the heap. Something like:
Jwks.setParser().build().parse(httpBody.getInputStream()).getKeys().collect...
I dunno how Methanol works or if that's possible, but food for thought.
Also, if you are confident that the token
payload will always be Claims
, you can do the more type-safe alternative:
// Upon receiving a token
Claims claims = jwtParser.parseSignedClaims(token); // alias for parse(token).accept(Jws.CLAIMS);
UPDATE: Please disregard, I found what I needed in https://github.com/jwtk/jjwt#jwk-private-topub.
If you have a JWKS with both the private and public key pair and use the above, you end up with the following exception:
Caused by: io.jsonwebtoken.security.InvalidKeyException: PrivateKeys may not be used to verify digital signatures. PrivateKeys are used to sign, and PublicKeys are used to verify.
at io.jsonwebtoken.impl.DefaultJwtParser.verifySignature(DefaultJwtParser.java:298)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:577)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:362)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:94)
at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:36)
at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:29)
at io.jsonwebtoken.impl.DefaultJwtParser.parseSignedClaims(DefaultJwtParser.java:821)
The JWKS itself looks something like this (redacted) bit of JSON:
{
"keys": [
{
"p": "…",
"kty": "…",
"q": "…",
"d": "…",
"e": "…",
"use": "…",
"kid": "…",
"qi": "…",
"dp": "…",
"alg": "…",
"dq": "…",
"n": "…"
}
]
}
Is there a way to convert the PrivateKey down to a PublicKey for verification? Is this a silly/unsafe thing to do with JWKS/JWT (I'm new to using these things)? This application both generates and validates JWS if that makes a difference to the answer.