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