67public final class JWebTokenImpl implements
JWebToken {
69 private static final ObjectMapper MAPPER =
new ObjectMapper();
71 private final JsonNode payload;
72 private final String signature;
73 private final String encodedHeader;
74 private final String token;
76 private JWebTokenImpl(
final String token,
final JsonNode payload,
final String encodedHeader,
final String signature) {
78 this.payload = payload;
79 this.encodedHeader = encodedHeader;
80 this.signature = signature;
87 this.payload = parsed.payload();
88 this.encodedHeader = parsed.encodedHeader();
89 this.signature = parsed.encodedSignature();
99 return payload.path(
"iss").asText(
"");
104 return payload.path(
"sub").asText(
"");
109 final List<String> values =
new ArrayList<>();
110 final JsonNode audience = payload.get(
"aud");
111 if (audience ==
null || audience.isNull()) {
114 if (audience.isArray()) {
115 audience.forEach(node -> values.add(node.asText()));
118 values.add(audience.asText());
124 return payload.path(
"exp").asLong(0L);
129 return payload.path(
"nbf").asLong(0L);
134 return payload.path(
"iat").asLong(0L);
139 return payload.path(
"jti").asText(
"");
143 public String
get(
final String property) {
144 return payload.path(property).asText(
"");
154 return encodedHeader;
159 final VerificationResult result = LegacySupport.VERIFIER.verify(token, Instant.now());
165 return payload.toString();
178 @Deprecated(since =
"3.1.0", forRemoval =
false)
179 public static class Builder {
180 private final Instant now = Instant.now();
181 private String issuer =
"rafex.dev";
182 private String subject;
183 private final List<String> audience =
new ArrayList<>();
184 private Instant issuedAt = now;
185 private Instant expiresAt;
186 private Instant notBefore;
187 private String jwtId = UUID.randomUUID().toString();
188 private final Map<String, Object> extraClaims =
new LinkedHashMap<>();
190 public Builder issuer(
final String iss) {
191 if (iss !=
null && !iss.isBlank()) {
197 public Builder subject(
final String sub) {
202 public Builder audience(
final String... aud) {
204 for (
final String value : aud) {
205 if (value !=
null && !value.isBlank()) {
213 public Builder expiration(
final long exp) {
215 expiresAt = Instant.ofEpochSecond(exp);
220 public Builder expirationPlusMinutes(
final int mins) {
222 expiresAt = now.plusSeconds(mins * 60L);
227 public Builder notBeforePlusSeconds(
final int secs) {
229 notBefore = now.plusSeconds(secs);
234 public Builder claim(
final String key,
final String val) {
235 if (key !=
null && !key.isBlank() && val !=
null && !val.isBlank()) {
236 extraClaims.put(key, val);
247 .audience(audience.toArray(String[]::new));
249 if (expiresAt !=
null) {
250 spec.expiresAt(expiresAt);
252 if (notBefore !=
null) {
253 spec.notBefore(notBefore);
255 for (
final Map.Entry<String, Object> entry : extraClaims.entrySet()) {
256 spec.claim(entry.getKey(), entry.getValue());
259 final String token = LegacySupport.ISSUER.issue(spec.build());
261 return new JWebTokenImpl(token, parsed.payload(), parsed.encodedHeader(), parsed.encodedSignature());
265 private static final class LegacySupport {
266 private static final DefaultTokenIssuer ISSUER;
267 private static final DefaultTokenVerifier VERIFIER;
270 final JwtConfig config = loadConfig();
271 ISSUER =
new DefaultTokenIssuer(config);
272 VERIFIER =
new DefaultTokenVerifier(config);
275 private LegacySupport() {
278 private static JwtConfig loadConfig() {
279 final Properties properties =
new Properties();
280 try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(
"jwt.properties")) {
284 }
catch (
final Exception ignored) {
288 applySystemOverride(properties,
"jwt.algorithm");
289 applySystemOverride(properties,
"jwt.secret");
290 applySystemOverride(properties,
"jwt.privateKey");
291 applySystemOverride(properties,
"jwt.publicKey");
292 applySystemOverride(properties,
"jwt.privateKeyPath");
293 applySystemOverride(properties,
"jwt.publicKeyPath");
295 final KeyProvider provider = resolveKeyProvider(properties);
296 return JwtConfig.builder(provider).build();
299 private static void applySystemOverride(
final Properties properties,
final String key) {
300 final String sys = System.getProperty(key);
301 if (sys !=
null && !sys.isBlank()) {
302 properties.setProperty(key, sys);
306 private static KeyProvider resolveKeyProvider(
final Properties properties) {
307 final String configuredAlgorithm = firstNonBlank(properties.getProperty(
"jwt.algorithm"), System.getenv(
"JWT_ALGORITHM"));
309 final String secret = firstNonBlank(properties.getProperty(
"jwt.secret"), System.getenv(
"JWT_SECRET"));
310 final String privateKeyText = readKeyText(properties,
"jwt.privateKey",
"JWT_PRIVATE_KEY",
"jwt.privateKeyPath",
"JWT_PRIVATE_KEY_PATH");
311 final String publicKeyText = readKeyText(properties,
"jwt.publicKey",
"JWT_PUBLIC_KEY",
"jwt.publicKeyPath",
"JWT_PUBLIC_KEY_PATH");
313 final JwtAlgorithm algorithm;
314 if (configuredAlgorithm !=
null) {
315 algorithm = JwtAlgorithm.valueOf(configuredAlgorithm.trim().toUpperCase());
316 }
else if (privateKeyText !=
null || publicKeyText !=
null) {
317 algorithm = JwtAlgorithm.RS256;
319 algorithm = JwtAlgorithm.HS256;
322 if (algorithm == JwtAlgorithm.HS256) {
323 if (secret ==
null || secret.isBlank()) {
324 throw new IllegalStateException(
"Missing jwt.secret/JWT_SECRET for HS256 legacy JWT config");
326 return KeyProvider.hmac(secret);
329 if (privateKeyText ==
null || publicKeyText ==
null) {
330 throw new IllegalStateException(
"Missing RSA key material for legacy RS256 JWT config");
334 final KeyFactory keyFactory = KeyFactory.getInstance(
"RSA");
335 final PrivateKey privateKey = keyFactory.generatePrivate(
new PKCS8EncodedKeySpec(stripPem(privateKeyText)));
336 final PublicKey publicKey = keyFactory.generatePublic(
new X509EncodedKeySpec(stripPem(publicKeyText)));
337 return KeyProvider.rsa(privateKey, publicKey);
338 }
catch (
final Exception e) {
339 throw new IllegalStateException(
"Invalid RSA key material for legacy JWT config", e);
343 private static String readKeyText(
344 final Properties properties,
345 final String inlineProperty,
346 final String inlineEnv,
347 final String pathProperty,
348 final String pathEnv) {
349 final String inlineValue = firstNonBlank(properties.getProperty(inlineProperty), System.getenv(inlineEnv));
350 if (inlineValue !=
null) {
353 final String pathValue = firstNonBlank(properties.getProperty(pathProperty), System.getenv(pathEnv));
354 if (pathValue ==
null) {
358 return Files.readString(Path.of(pathValue), StandardCharsets.UTF_8);
359 }
catch (
final Exception e) {
360 throw new IllegalStateException(
"Unable to read JWT key path: " + pathValue, e);
364 private static String firstNonBlank(
final String first,
final String second) {
365 if (first !=
null && !first.isBlank()) {
368 if (second !=
null && !second.isBlank()) {
374 private static byte[] stripPem(
final String pem) {
375 final String body = pem
376 .replaceAll(
"-----BEGIN (.*)-----",
"")
377 .replaceAll(
"-----END (.*)-----",
"")
378 .replaceAll(
"\\s",
"");
379 return Base64.getDecoder().decode(body);