Skip to content

Commit 7689692

Browse files
committed
JWKS support
1 parent f313cb7 commit 7689692

File tree

4 files changed

+745
-67
lines changed

4 files changed

+745
-67
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package convex.core.json;
2+
3+
import java.math.BigInteger;
4+
import java.security.KeyFactory;
5+
import java.security.interfaces.RSAPublicKey;
6+
import java.security.spec.RSAPublicKeySpec;
7+
import java.util.Base64;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
11+
import convex.core.data.ACell;
12+
import convex.core.data.AMap;
13+
import convex.core.data.ASequence;
14+
import convex.core.data.AString;
15+
import convex.core.data.Strings;
16+
import convex.core.lang.RT;
17+
import convex.core.util.JSON;
18+
19+
/**
20+
* Utility for parsing JWKS (JSON Web Key Set) JSON into RSA public keys.
21+
*
22+
* <p>Parses the standard JWKS format used by OAuth providers (Google, Microsoft, Auth0)
23+
* to extract RSA public keys for RS256 JWT verification.
24+
*
25+
* <p>This class does not perform HTTP fetching — callers must supply the raw JSON
26+
* from the provider's {@code jwks_uri} endpoint.
27+
*/
28+
public class JWKSKeys {
29+
30+
private static final Base64.Decoder decoder = Base64.getUrlDecoder();
31+
32+
/**
33+
* Parse a JWKS JSON string and return a map of kid to RSAPublicKey.
34+
* Only includes keys where kty=RSA and (use=sig or use is absent).
35+
*
36+
* @param jwksJson Raw JSON from a JWKS endpoint
37+
* @return Map of kid to RSAPublicKey, never null (empty if no valid keys)
38+
*/
39+
@SuppressWarnings("unchecked")
40+
public static Map<String, RSAPublicKey> parseKeys(String jwksJson) {
41+
Map<String, RSAPublicKey> result = new HashMap<>();
42+
try {
43+
AMap<AString,ACell> jwks = RT.ensureMap(JSON.parse(jwksJson));
44+
if (jwks == null) return result;
45+
46+
ASequence<ACell> keys = (ASequence<ACell>) RT.ensureSequence(jwks.get(Strings.create("keys")));
47+
if (keys == null) return result;
48+
49+
for (long i = 0; i < keys.count(); i++) {
50+
AMap<AString,ACell> key = RT.ensureMap(keys.get(i));
51+
if (key == null) continue;
52+
53+
String kty = str(key, "kty");
54+
if (!"RSA".equals(kty)) continue;
55+
56+
String use = str(key, "use");
57+
if (use != null && !"sig".equals(use)) continue;
58+
59+
String kid = str(key, "kid");
60+
String n = str(key, "n");
61+
String e = str(key, "e");
62+
if (kid == null || n == null || e == null) continue;
63+
64+
RSAPublicKey rsaKey = buildRSAPublicKey(n, e);
65+
if (rsaKey != null) {
66+
result.put(kid, rsaKey);
67+
}
68+
}
69+
} catch (Exception e) {
70+
// Return whatever we parsed so far
71+
}
72+
return result;
73+
}
74+
75+
/**
76+
* Build an RSAPublicKey from Base64URL-encoded modulus and exponent.
77+
*
78+
* @param modulusB64 Base64URL-encoded RSA modulus (n)
79+
* @param exponentB64 Base64URL-encoded RSA exponent (e)
80+
* @return RSAPublicKey, or null if construction fails
81+
*/
82+
public static RSAPublicKey buildRSAPublicKey(String modulusB64, String exponentB64) {
83+
try {
84+
BigInteger n = new BigInteger(1, decoder.decode(modulusB64));
85+
BigInteger e = new BigInteger(1, decoder.decode(exponentB64));
86+
RSAPublicKeySpec spec = new RSAPublicKeySpec(n, e);
87+
KeyFactory factory = KeyFactory.getInstance("RSA");
88+
return (RSAPublicKey) factory.generatePublic(spec);
89+
} catch (Exception e) {
90+
return null;
91+
}
92+
}
93+
94+
private static String str(AMap<AString,ACell> map, String key) {
95+
AString v = RT.ensureString(map.get(Strings.create(key)));
96+
return v != null ? v.toString() : null;
97+
}
98+
}

0 commit comments

Comments
 (0)