1818 */
1919package org .apache .ofbiz .webapp .control ;
2020
21+ import java .net .MalformedURLException ;
22+ import java .net .URL ;
23+ import java .security .interfaces .RSAPublicKey ;
2124import java .sql .Timestamp ;
2225import java .util .Calendar ;
2326import java .util .HashMap ;
2427import java .util .Map ;
28+ import java .util .concurrent .ConcurrentHashMap ;
29+ import java .util .concurrent .TimeUnit ;
2530
2631import jakarta .servlet .ServletContext ;
2732import jakarta .servlet .http .HttpServletRequest ;
4752import org .apache .ofbiz .service .ServiceUtil ;
4853import org .apache .ofbiz .webapp .WebAppUtil ;
4954
55+ import com .auth0 .jwk .Jwk ;
56+ import com .auth0 .jwk .JwkProvider ;
57+ import com .auth0 .jwk .JwkProviderBuilder ;
5058import com .auth0 .jwt .JWT ;
5159import com .auth0 .jwt .JWTCreator ;
5260import com .auth0 .jwt .JWTVerifier ;
6270public class JWTManager {
6371 private static final String MODULE = JWTManager .class .getName ();
6472
73+ // Static map of thread-safe JwkProvider instances for each delegator.
74+ private static volatile Map <String , JwkProvider > jwkProviders = new ConcurrentHashMap <>();
75+ /**
76+ * Returns a shared, thread-safe JwkProvider instance for the delegator.
77+ */
78+ private static JwkProvider getJwkProvider (Delegator delegator ) throws IllegalStateException , MalformedURLException {
79+ JwkProvider localRef = jwkProviders .get (delegator .getDelegatorName ());
80+ if (localRef == null ) {
81+ synchronized (JWTManager .class ) {
82+ localRef = jwkProviders .get (delegator .getDelegatorName ());
83+ if (localRef == null ) {
84+ String issuer = EntityUtilProperties .getPropertyValue ("security" , "security.token.issuer" , "" , delegator );
85+ String jwksUrl = issuer + "/protocol/openid-connect/certs" ;
86+ localRef = new JwkProviderBuilder (new URL (jwksUrl ))
87+ .cached (10 , 24 , TimeUnit .HOURS ) // cache up to 10 keys for 24h
88+ .rateLimited (10 , 1 , TimeUnit .MINUTES ) // prevent frequent fetches
89+ .build ();
90+ jwkProviders .put (delegator .getDelegatorName (), localRef );
91+ }
92+ }
93+ }
94+ return localRef ;
95+ }
6596 /**
6697 * OFBiz controller preprocessor event.
6798 * The method is designed to be used in a chain of controller preprocessor event: it always returns "success"
@@ -94,7 +125,7 @@ public static String checkJWTLogin(HttpServletRequest request, HttpServletRespon
94125 return "success" ;
95126 }
96127
97- Map <String , Object > claims = validateJwtToken (jwtToken , getJWTKey ( delegator ) );
128+ Map <String , Object > claims = validateJwtToken (delegator , jwtToken );
98129 if (claims .containsKey (ModelService .ERROR_MESSAGE )) {
99130 // The JWT is wrong somehow, stop the process, details are in log
100131 return "success" ;
@@ -130,17 +161,7 @@ public static String checkJWTLogin(HttpServletRequest request, HttpServletRespon
130161 * @param delegator the delegator
131162 * @return the JWT secret key
132163 */
133- public static String getJWTKey (Delegator delegator ) {
134- return getJWTKey (delegator , null );
135- }
136-
137- /**
138- * Get the JWT secret key from database or security.properties.
139- * @param delegator the delegator
140- * @return the JWT secret key
141- */
142-
143- public static String getJWTKey (Delegator delegator , String salt ) {
164+ private static String getJWTKey (Delegator delegator , String salt ) {
144165 String key = UtilProperties .getPropertyValue ("security" , "security.token.key" );
145166 if (key .length () < 64 ) { // The key must be 512 bits (ie 64 chars) as we use HMAC512 to create the token, cf. OFBIZ-12724
146167 throw new SecurityException ("The JWT secret key is too short. It must be at least 512 bites." );
@@ -151,7 +172,7 @@ public static String getJWTKey(Delegator delegator, String salt) {
151172 return key ;
152173 }
153174
154- /**
175+ /**
155176 * Get the authentication token based for user
156177 * This takes OOTB username/password and if user is authenticated it will generate the JWT token using a secret key.
157178 * @param request the http request in which the authentication token is searched and stored
@@ -226,26 +247,75 @@ public static String getHeaderAuthBearerToken(HttpServletRequest request) {
226247 return headerAuthValue .replaceFirst (bearerPrefix , "" ).trim ();
227248 }
228249
229- /** Validates the provided token using the secret key.
230- * If the token is valid it will get the conteined claims and return them.
231- * If token validation failed it will return an error.
232- * Public for API access from third party code.
233- * @param jwtToken the JWT token
234- * @param key the server side key to verify the signature
235- * @return Map of the claims contained in the token or an error
250+ /**
251+ * Validates a JSON Web Token (JWT) and extracts its claims.
252+ *
253+ * This method supports two validation modes:
254+ * External authentication server (JWK-based): if an issuer is configured
255+ * in the "security.token.issuer" property, the token is verified using a JWK provider and
256+ * the issuer's public key used to sign the token.
257+ * Local HMAC verification: If no issuer is configured, the token is verified
258+ * locally using an HMAC key derived from the secret key configured
259+ * in the "security.token.key" (and optionally a salt).
260+ *
261+ * If the token is successfully verified, the contained claims are returned as a map.
262+ * Otherwise, an error map is returned containing the failure message.
263+ *
264+ * @param delegator the delegator used to retrieve security properties and keys from a database
265+ * @param jwtToken the JWT string to validate
266+ * @param keySalt an optional salt used when building the local HMAC key (can be null or empty)
267+ * @return a map containing:
268+ * the token claims if validation succeeds
269+ * an error entry if validation fails
236270 */
237- public static Map <String , Object > validateToken (String jwtToken , String key ) {
238- Map <String , Object > result = new HashMap <>();
239- if (UtilValidate .isEmpty (jwtToken ) || UtilValidate .isEmpty (key )) {
271+ public static Map <String , Object > validateToken (Delegator delegator , String jwtToken , String keySalt ) {
272+ JWTVerifier verifier = null ;
273+ // Retrieve configured issuer (if present, assume external JWK-based validation)
274+ String issuer = EntityUtilProperties .getPropertyValue ("security" , "security.token.issuer" , "" , delegator );
275+ if (UtilValidate .isNotEmpty (issuer )) {
276+ String audience = EntityUtilProperties .getPropertyValue ("security" , "security.token.audience" , "" , delegator );
277+ try {
278+ // Decode the token to extract the Key ID (kid)
279+ DecodedJWT decodedJWT = JWT .decode (jwtToken );
280+ String kid = decodedJWT .getKeyId ();
281+
282+ // Fetch the corresponding JWK (JSON Web Key) for this Key ID
283+ JwkProvider provider = getJwkProvider (delegator );
284+ Jwk jwk = provider .get (kid );
285+
286+ // Build the RSA256 Algorithm using the JWK’s public key
287+ Algorithm algorithm = Algorithm .RSA256 ((RSAPublicKey ) jwk .getPublicKey (), null );
288+
289+ // Create a JWT verifier: include expected issuer and audience for safety
290+ verifier = JWT .require (algorithm )
291+ .withIssuer (issuer )
292+ .withAudience (audience )
293+ .build ();
294+ } catch (Exception e ) {
295+ String msg = "JWT token: unable to build a token verifier for tokens issued by " + issuer ;
296+ Debug .logError (msg , MODULE );
297+ return ServiceUtil .returnError (msg );
298+ }
299+ } else {
300+ // Fallback: validate using local secret key
301+ String key = getJWTKey (delegator , keySalt );
302+ if (UtilValidate .isEmpty (jwtToken ) || UtilValidate .isEmpty (key )) {
303+ String msg = "JWT token or key can not be empty." ;
304+ Debug .logError (msg , MODULE );
305+ return ServiceUtil .returnError (msg );
306+ }
307+ verifier = JWT .require (Algorithm .HMAC512 (key ))
308+ .withIssuer ("ApacheOFBiz" )
309+ .build ();
310+ }
311+ if (UtilValidate .isEmpty (verifier )) {
240312 String msg = "JWT token or key can not be empty." ;
241313 Debug .logError (msg , MODULE );
242314 return ServiceUtil .returnError (msg );
243315 }
244316 try {
245- JWTVerifier verifToken = JWT .require (Algorithm .HMAC512 (key ))
246- .withIssuer ("ApacheOFBiz" )
247- .build ();
248- DecodedJWT jwt = verifToken .verify (jwtToken );
317+ Map <String , Object > result = new HashMap <>();
318+ DecodedJWT jwt = verifier .verify (jwtToken );
249319 Map <String , Claim > claims = jwt .getClaims ();
250320 //OK, we can trust this JWT
251321 for (Map .Entry <String , Claim > entry : claims .entrySet ()) {
@@ -260,16 +330,23 @@ public static Map<String, Object> validateToken(String jwtToken, String key) {
260330 }
261331
262332 /**
263- * Validates the provided token using a salt to recreate the key from the secret
264- * If the token is valid it will get the contained claims and return them.
265- * If token validation failed it will return an error.
266- * @param delegator
267- * @param jwtToken
268- * @param keySalt
269- * @return Map of the claims contained in the token or an error
333+ * Validates a JSON Web Token (JWT) and extracts its claims using the default validation process.
334+ *
335+ * This method is a convenience overload that calls validateToken(Delegator, String, String)
336+ * without providing a key salt. The validation will use either an external authentication
337+ * server (if configured) or the locally stored secret key.
338+ *
339+ * If the token is successfully verified, the contained claims are returned as a map.
340+ * If validation fails, an error map is returned containing details about the failure.
341+ *
342+ * @param delegator the delegator used to retrieve security properties and keys from the database
343+ * @param jwtToken the JWT string to validate
344+ * @return a map containing the token claims if validation succeeds,
345+ * or an error entry if validation fails
346+ * @see #validateToken(Delegator, String, String)
270347 */
271- public static Map <String , Object > validateToken (Delegator delegator , String jwtToken , String keySalt ) {
272- return validateToken (jwtToken , JWTManager . getJWTKey ( delegator , keySalt ) );
348+ public static Map <String , Object > validateToken (Delegator delegator , String jwtToken ) {
349+ return validateToken (delegator , jwtToken , null );
273350 }
274351
275352 /**
@@ -396,8 +473,8 @@ private static GenericValue getUserlogin(Delegator delegator, Map<String, Object
396473 * @param key the secret key to decrypt the token
397474 * @return Map of name, value pairs composing the result
398475 */
399- private static Map <String , Object > validateJwtToken (String jwtToken , String key ) {
400- Map <String , Object > result = validateToken (jwtToken , key );
476+ private static Map <String , Object > validateJwtToken (Delegator delegator , String jwtToken ) {
477+ Map <String , Object > result = validateToken (delegator , jwtToken );
401478 if (result .containsKey (ModelService .ERROR_MESSAGE )) {
402479 // Something unexpected happened here
403480 Debug .logWarning ("There was a problem with the JWT token, no single sign on user login possible." , MODULE );
@@ -411,8 +488,8 @@ public static String createRefreshToken(Delegator delegator, String userLoginId)
411488 return createJwt (delegator , UtilMisc .toMap ("userLoginId" , userLoginId , "type" , "refresh" ), refreshTokenExpireTime );
412489 }
413490
414- public static Map <String , Object > validateRefreshToken (String refreshToken , String key ) {
415- Map <String , Object > claims = validateToken (refreshToken , key );
491+ public static Map <String , Object > validateRefreshToken (Delegator delegator , String refreshToken ) {
492+ Map <String , Object > claims = validateToken (delegator , refreshToken );
416493 if (!claims .containsKey ("type" ) || !"refresh" .equals (claims .get ("type" ))) {
417494 return ServiceUtil .returnError ("Invalid refresh token." );
418495 }
0 commit comments