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,
77 final String signature) {
79 this.payload = payload;
80 this.encodedHeader = encodedHeader;
81 this.signature = signature;
88 this.payload = parsed.payload();
89 this.encodedHeader = parsed.encodedHeader();
90 this.signature = parsed.encodedSignature();
100 return payload.path(
"iss").asText(
"");
105 return payload.path(
"sub").asText(
"");
110 final List<String> values =
new ArrayList<>();
111 final JsonNode audience = payload.get(
"aud");
112 if (audience ==
null || audience.isNull()) {
115 if (audience.isArray()) {
116 audience.forEach(node -> values.add(node.asText()));
119 values.add(audience.asText());
125 return payload.path(
"exp").asLong(0L);
130 return payload.path(
"nbf").asLong(0L);
135 return payload.path(
"iat").asLong(0L);
140 return payload.path(
"jti").asText(
"");
144 public String
get(
final String property) {
145 return payload.path(property).asText(
"");
155 return encodedHeader;
160 final VerificationResult result = LegacySupport.VERIFIER.verify(token, Instant.now());
166 return payload.toString();
179 @Deprecated(since =
"3.1.0", forRemoval =
false)
180 public static class Builder {
181 private final Instant now = Instant.now();
182 private String issuer =
"rafex.dev";
183 private String subject;
184 private final List<String> audience =
new ArrayList<>();
185 private Instant issuedAt = now;
186 private Instant expiresAt;
187 private Instant notBefore;
188 private String jwtId = UUID.randomUUID().toString();
189 private final Map<String, Object> extraClaims =
new LinkedHashMap<>();
191 public Builder issuer(
final String iss) {
192 if (iss !=
null && !iss.isBlank()) {
198 public Builder subject(
final String sub) {
203 public Builder audience(
final String... aud) {
205 for (
final String value : aud) {
206 if (value !=
null && !value.isBlank()) {
214 public Builder expiration(
final long exp) {
216 expiresAt = Instant.ofEpochSecond(exp);
221 public Builder expirationPlusMinutes(
final int mins) {
223 expiresAt = now.plusSeconds(mins * 60L);
228 public Builder notBeforePlusSeconds(
final int secs) {
230 notBefore = now.plusSeconds(secs);
235 public Builder claim(
final String key,
final String val) {
236 if (key !=
null && !key.isBlank() && val !=
null && !val.isBlank()) {
237 extraClaims.put(key, val);
243 final TokenSpec.Builder spec =
TokenSpec.
builder().issuer(issuer).subject(subject).issuedAt(issuedAt)
244 .jwtId(jwtId).audience(audience.toArray(String[]::new));
246 if (expiresAt !=
null) {
247 spec.expiresAt(expiresAt);
249 if (notBefore !=
null) {
250 spec.notBefore(notBefore);
252 for (
final Map.Entry<String, Object> entry : extraClaims.entrySet()) {
253 spec.claim(entry.getKey(), entry.getValue());
256 final String token = LegacySupport.ISSUER.issue(spec.build());
258 return new JWebTokenImpl(token, parsed.payload(), parsed.encodedHeader(), parsed.encodedSignature());
262 private static final class LegacySupport {
263 private static final DefaultTokenIssuer ISSUER;
264 private static final DefaultTokenVerifier VERIFIER;
267 final JwtConfig config = loadConfig();
268 ISSUER =
new DefaultTokenIssuer(config);
269 VERIFIER =
new DefaultTokenVerifier(config);
272 private LegacySupport() {
275 private static JwtConfig loadConfig() {
276 final Properties properties =
new Properties();
277 try (InputStream in = Thread.currentThread().getContextClassLoader()
278 .getResourceAsStream(
"jwt.properties")) {
282 }
catch (
final Exception ignored) {
286 applySystemOverride(properties,
"jwt.algorithm");
287 applySystemOverride(properties,
"jwt.secret");
288 applySystemOverride(properties,
"jwt.privateKey");
289 applySystemOverride(properties,
"jwt.publicKey");
290 applySystemOverride(properties,
"jwt.privateKeyPath");
291 applySystemOverride(properties,
"jwt.publicKeyPath");
293 final KeyProvider provider = resolveKeyProvider(properties);
294 return JwtConfig.builder(provider).build();
297 private static void applySystemOverride(
final Properties properties,
final String key) {
298 final String sys = System.getProperty(key);
299 if (sys !=
null && !sys.isBlank()) {
300 properties.setProperty(key, sys);
304 private static KeyProvider resolveKeyProvider(
final Properties properties) {
305 final String configuredAlgorithm = firstNonBlank(properties.getProperty(
"jwt.algorithm"),
306 System.getenv(
"JWT_ALGORITHM"));
308 final String secret = firstNonBlank(properties.getProperty(
"jwt.secret"), System.getenv(
"JWT_SECRET"));
309 final String privateKeyText = readKeyText(properties,
"jwt.privateKey",
"JWT_PRIVATE_KEY",
310 "jwt.privateKeyPath",
"JWT_PRIVATE_KEY_PATH");
311 final String publicKeyText = readKeyText(properties,
"jwt.publicKey",
"JWT_PUBLIC_KEY",
"jwt.publicKeyPath",
312 "JWT_PUBLIC_KEY_PATH");
314 final JwtAlgorithm algorithm;
315 if (configuredAlgorithm !=
null) {
316 algorithm = JwtAlgorithm.valueOf(configuredAlgorithm.trim().toUpperCase());
317 }
else if (privateKeyText !=
null || publicKeyText !=
null) {
318 algorithm = JwtAlgorithm.RS256;
320 algorithm = JwtAlgorithm.HS256;
323 if (algorithm == JwtAlgorithm.HS256) {
324 if (secret ==
null || secret.isBlank()) {
325 throw new IllegalStateException(
"Missing jwt.secret/JWT_SECRET for HS256 legacy JWT config");
327 return KeyProvider.hmac(secret);
330 if (privateKeyText ==
null || publicKeyText ==
null) {
331 throw new IllegalStateException(
"Missing RSA key material for legacy RS256 JWT config");
335 final KeyFactory keyFactory = KeyFactory.getInstance(
"RSA");
336 final PrivateKey privateKey = keyFactory
337 .generatePrivate(
new PKCS8EncodedKeySpec(stripPem(privateKeyText)));
338 final PublicKey publicKey = keyFactory.generatePublic(
new X509EncodedKeySpec(stripPem(publicKeyText)));
339 return KeyProvider.rsa(privateKey, publicKey);
340 }
catch (
final Exception e) {
341 throw new IllegalStateException(
"Invalid RSA key material for legacy JWT config", e);
345 private static String readKeyText(
final Properties properties,
final String inlineProperty,
346 final String inlineEnv,
final String pathProperty,
final String pathEnv) {
347 final String inlineValue = firstNonBlank(properties.getProperty(inlineProperty), System.getenv(inlineEnv));
348 if (inlineValue !=
null) {
351 final String pathValue = firstNonBlank(properties.getProperty(pathProperty), System.getenv(pathEnv));
352 if (pathValue ==
null) {
356 return Files.readString(Path.of(pathValue), StandardCharsets.UTF_8);
357 }
catch (
final Exception e) {
358 throw new IllegalStateException(
"Unable to read JWT key path: " + pathValue, e);
362 private static String firstNonBlank(
final String first,
final String second) {
363 if (first !=
null && !first.isBlank()) {
366 if (second !=
null && !second.isBlank()) {
372 private static byte[] stripPem(
final String pem) {
373 final String body = pem.replaceAll(
"-----BEGIN (.*)-----",
"").replaceAll(
"-----END (.*)-----",
"")
374 .replaceAll(
"\\s",
"");
375 return Base64.getDecoder().decode(body);