Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify JWT with JWKS #663

Open
EPilz opened this issue May 28, 2021 · 16 comments
Open

Verify JWT with JWKS #663

EPilz opened this issue May 28, 2021 · 16 comments

Comments

@EPilz
Copy link

EPilz commented May 28, 2021

Is there a way to verify a JWT with JWKS?

@EPilz EPilz changed the title Verify JWT with JWKs Verify JWT with JWKS May 28, 2021
@bdemers
Copy link
Member

bdemers commented May 28, 2021

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

@EPilz
Copy link
Author

EPilz commented May 29, 2021

Thanks for the quick response! How is the access token then verified?

@bdemers
Copy link
Member

bdemers commented May 29, 2021

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)

@lhazlewood
Copy link
Contributor

lhazlewood commented Jun 2, 2021

Just a note: this will be easier when #113 is complete as JWK support is required for JWE.

@stale stale bot added the stale Stale issues pending deletion due to inactivity label Aug 4, 2021
@lhazlewood lhazlewood removed the stale Stale issues pending deletion due to inactivity label Aug 4, 2021
@jwtk jwtk deleted a comment from stale bot Aug 4, 2021
@stale stale bot added the stale Stale issues pending deletion due to inactivity label Apr 16, 2022
@bdemers bdemers removed the stale Stale issues pending deletion due to inactivity label Apr 18, 2022
@jwtk jwtk deleted a comment from stale bot Apr 20, 2022
@stale
Copy link

stale bot commented Jul 10, 2022

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.

@stale stale bot added the stale Stale issues pending deletion due to inactivity label Jul 10, 2022
@lhazlewood
Copy link
Contributor

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

@stale stale bot removed the stale Stale issues pending deletion due to inactivity label Aug 11, 2023
@jkellyinsf
Copy link

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.

@lhazlewood
Copy link
Contributor

lhazlewood commented Oct 5, 2023

@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 :)

@jkellyinsf
Copy link

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.

@lhazlewood
Copy link
Contributor

@jkellyinsf that's because what what you linked to is not a Jwk, it is a JwkSet. Try:

Jwks.setParser().build().parse(jwkSetJson);

@jkellyinsf
Copy link

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?

@lhazlewood
Copy link
Contributor

@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.

@jkellyinsf
Copy link

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!

@lhazlewood
Copy link
Contributor

lhazlewood commented Oct 6, 2023

@jkellyinsf don't forget that all JJWT Parsers 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.

@lhazlewood
Copy link
Contributor

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);

@rkennedy-mode
Copy link

rkennedy-mode commented Nov 15, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants