Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
JWebTokenImpl.java
Go to the documentation of this file.
1package dev.rafex.ether.jwt.impl;
2
3/*-
4 * #%L
5 * ether-jwt
6 * %%
7 * Copyright (C) 2025 Raúl Eduardo González Argote
8 * %%
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 * #L%
27 */
28
29import com.fasterxml.jackson.databind.JsonNode;
30import com.fasterxml.jackson.databind.ObjectMapper;
31import com.fasterxml.jackson.databind.node.ObjectNode;
32import dev.rafex.ether.jwt.DefaultTokenIssuer;
33import dev.rafex.ether.jwt.DefaultTokenVerifier;
34import dev.rafex.ether.jwt.JWebToken;
35import dev.rafex.ether.jwt.JwtAlgorithm;
36import dev.rafex.ether.jwt.JwtConfig;
37import dev.rafex.ether.jwt.KeyProvider;
38import dev.rafex.ether.jwt.TokenSpec;
39import dev.rafex.ether.jwt.VerificationResult;
40import dev.rafex.ether.jwt.internal.ClaimsMapper;
41import dev.rafex.ether.jwt.internal.JwtCodec;
42
43import java.io.InputStream;
44import java.nio.charset.StandardCharsets;
45import java.nio.file.Files;
46import java.nio.file.Path;
47import java.security.KeyFactory;
48import java.security.PrivateKey;
49import java.security.PublicKey;
50import java.security.spec.PKCS8EncodedKeySpec;
51import java.security.spec.X509EncodedKeySpec;
52import java.time.Instant;
53import java.util.ArrayList;
54import java.util.Base64;
55import java.util.LinkedHashMap;
56import java.util.List;
57import java.util.Map;
58import java.util.Properties;
59import java.util.UUID;
60
61/**
62 * Legacy JWT implementation. Prefer {@link DefaultTokenIssuer} + {@link DefaultTokenVerifier}.
63 *
64 * @deprecated Use reusable APIs in package {@code dev.rafex.ether.jwt}.
65 */
66@Deprecated(since = "3.1.0", forRemoval = false)
67public final class JWebTokenImpl implements JWebToken {
68
69 private static final ObjectMapper MAPPER = new ObjectMapper();
70
71 private final JsonNode payload;
72 private final String signature;
73 private final String encodedHeader;
74 private final String token;
75
76 private JWebTokenImpl(final String token, final JsonNode payload, final String encodedHeader, final String signature) {
77 this.token = token;
78 this.payload = payload;
79 this.encodedHeader = encodedHeader;
80 this.signature = signature;
81 }
82
83 /** Parse an existing JWT token string. */
84 public JWebTokenImpl(final String token) {
85 final JwtCodec.ParsedJwt parsed = JwtCodec.parse(token);
86 this.token = token;
87 this.payload = parsed.payload();
88 this.encodedHeader = parsed.encodedHeader();
89 this.signature = parsed.encodedSignature();
90 }
91
92 @Override
93 public JsonNode getPayload() {
94 return payload;
95 }
96
97 @Override
98 public String getIssuer() {
99 return payload.path("iss").asText("");
100 }
101
102 @Override
103 public String getSubject() {
104 return payload.path("sub").asText("");
105 }
106
107 @Override
108 public List<String> getAudience() {
109 final List<String> values = new ArrayList<>();
110 final JsonNode audience = payload.get("aud");
111 if (audience == null || audience.isNull()) {
112 return values;
113 }
114 if (audience.isArray()) {
115 audience.forEach(node -> values.add(node.asText()));
116 return values;
117 }
118 values.add(audience.asText());
119 return values;
120 }
121
122 @Override
123 public Long getExpiration() {
124 return payload.path("exp").asLong(0L);
125 }
126
127 @Override
128 public Long getNotBefore() {
129 return payload.path("nbf").asLong(0L);
130 }
131
132 @Override
133 public Long getIssuedAt() {
134 return payload.path("iat").asLong(0L);
135 }
136
137 @Override
138 public String getJwtId() {
139 return payload.path("jti").asText("");
140 }
141
142 @Override
143 public String get(final String property) {
144 return payload.path(property).asText("");
145 }
146
147 @Override
148 public String getSignature() {
149 return signature;
150 }
151
152 @Override
153 public String getEncodedHeader() {
154 return encodedHeader;
155 }
156
157 @Override
158 public boolean isValid() {
159 final VerificationResult result = LegacySupport.VERIFIER.verify(token, Instant.now());
160 return result.ok();
161 }
162
163 @Override
164 public String aJson() {
165 return payload.toString();
166 }
167
168 @Override
169 public String toString() {
170 return token;
171 }
172
173 /**
174 * Legacy builder kept for migration compatibility.
175 *
176 * @deprecated Use {@link TokenSpec#builder()} + {@link DefaultTokenIssuer}.
177 */
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<>();
189
190 public Builder issuer(final String iss) {
191 if (iss != null && !iss.isBlank()) {
192 issuer = iss;
193 }
194 return this;
195 }
196
197 public Builder subject(final String sub) {
198 subject = sub;
199 return this;
200 }
201
202 public Builder audience(final String... aud) {
203 if (aud != null) {
204 for (final String value : aud) {
205 if (value != null && !value.isBlank()) {
206 audience.add(value);
207 }
208 }
209 }
210 return this;
211 }
212
213 public Builder expiration(final long exp) {
214 if (exp > 0) {
215 expiresAt = Instant.ofEpochSecond(exp);
216 }
217 return this;
218 }
219
220 public Builder expirationPlusMinutes(final int mins) {
221 if (mins > 0) {
222 expiresAt = now.plusSeconds(mins * 60L);
223 }
224 return this;
225 }
226
227 public Builder notBeforePlusSeconds(final int secs) {
228 if (secs > 0) {
229 notBefore = now.plusSeconds(secs);
230 }
231 return this;
232 }
233
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);
237 }
238 return this;
239 }
240
241 public JWebTokenImpl build() {
242 final TokenSpec.Builder spec = TokenSpec.builder()
243 .issuer(issuer)
244 .subject(subject)
245 .issuedAt(issuedAt)
246 .jwtId(jwtId)
247 .audience(audience.toArray(String[]::new));
248
249 if (expiresAt != null) {
250 spec.expiresAt(expiresAt);
251 }
252 if (notBefore != null) {
253 spec.notBefore(notBefore);
254 }
255 for (final Map.Entry<String, Object> entry : extraClaims.entrySet()) {
256 spec.claim(entry.getKey(), entry.getValue());
257 }
258
259 final String token = LegacySupport.ISSUER.issue(spec.build());
260 final JwtCodec.ParsedJwt parsed = JwtCodec.parse(token);
261 return new JWebTokenImpl(token, parsed.payload(), parsed.encodedHeader(), parsed.encodedSignature());
262 }
263 }
264
265 private static final class LegacySupport {
266 private static final DefaultTokenIssuer ISSUER;
267 private static final DefaultTokenVerifier VERIFIER;
268
269 static {
270 final JwtConfig config = loadConfig();
271 ISSUER = new DefaultTokenIssuer(config);
272 VERIFIER = new DefaultTokenVerifier(config);
273 }
274
275 private LegacySupport() {
276 }
277
278 private static JwtConfig loadConfig() {
279 final Properties properties = new Properties();
280 try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("jwt.properties")) {
281 if (in != null) {
282 properties.load(in);
283 }
284 } catch (final Exception ignored) {
285 // Legacy compatibility: ignore missing properties files.
286 }
287
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");
294
295 final KeyProvider provider = resolveKeyProvider(properties);
296 return JwtConfig.builder(provider).build();
297 }
298
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);
303 }
304 }
305
306 private static KeyProvider resolveKeyProvider(final Properties properties) {
307 final String configuredAlgorithm = firstNonBlank(properties.getProperty("jwt.algorithm"), System.getenv("JWT_ALGORITHM"));
308
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");
312
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;
318 } else {
319 algorithm = JwtAlgorithm.HS256;
320 }
321
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");
325 }
326 return KeyProvider.hmac(secret);
327 }
328
329 if (privateKeyText == null || publicKeyText == null) {
330 throw new IllegalStateException("Missing RSA key material for legacy RS256 JWT config");
331 }
332
333 try {
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);
340 }
341 }
342
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) {
351 return inlineValue;
352 }
353 final String pathValue = firstNonBlank(properties.getProperty(pathProperty), System.getenv(pathEnv));
354 if (pathValue == null) {
355 return null;
356 }
357 try {
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);
361 }
362 }
363
364 private static String firstNonBlank(final String first, final String second) {
365 if (first != null && !first.isBlank()) {
366 return first;
367 }
368 if (second != null && !second.isBlank()) {
369 return second;
370 }
371 return null;
372 }
373
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);
380 }
381 }
382}
Specification used by TokenIssuer to issue JWT tokens.
Result returned by token verification.
JWebTokenImpl(final String token)
Parse an existing JWT token string.
static ParsedJwt parse(final String token)
Definition JwtCodec.java:43
Legacy JWT contract.