/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.utils; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Date; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import net.curisit.integrity.commons.Utils; import net.curisit.securis.services.ApiResource; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.Base64; import java.nio.charset.StandardCharsets; /** * TokenHelper *
* Utility component to generate and validate short-lived authentication tokens * for SeCuris services. Tokens are: *
*Lifecycle & scope: {@code @ApplicationScoped}. Stateless and thread-safe.
* *Security notes: * The {@code seed} acts like a shared secret. Keep it private and rotate if compromised. * Tokens expire after {@link #VALID_TOKEN_PERIOD} hours except a special client token * defined by {@link ApiResource#API_CLIENT_USERNAME} issued at epoch-1 (see {@link #isTokenValid(String)}). *
* *Format details: *
* token = Base64( secret + " " + user + " " + Utils.toIsoFormat(date) ) * secret = hex(SHA-256(seed || user || isoDate)) ** * * @author JRA * Last reviewed by JRA on Oct 6, 2025. */ @ApplicationScoped public class TokenHelper { private static final Logger LOG = LogManager.getLogger(TokenHelper.class); /** * Validity window for standard tokens, in hours. *
* Any token with a creation date older than this window will be rejected * (unless it matches the special API client rule documented in * {@link #isTokenValid(String)}). *
*/ private static int VALID_TOKEN_PERIOD = 24; /** Standard HTTP header used by SeCuris clients to carry the token. */ public static final String TOKEN_HEADER_PĂ€RAM = "X-SECURIS-TOKEN"; /** * TokenHelper* CDI no-arg constructor. *
* Kept for dependency injection. No initialization logic is required. *
*/ @Inject public TokenHelper() { } /** * Static secret seed used to derive the token {@code secret} portion. ** Treat this as confidential. Changing it invalidates all outstanding tokens. *
*/ private static byte[] seed = "S3Cur15S33dForT0k3nG3n3r@tion".getBytes(); // --------------------------------------------------------------------- // Token generation // --------------------------------------------------------------------- /** * generateToken ** Convenience overload that generates a token for {@code user} using the current * system time as the issuance timestamp. *
* * @param user Username to embed in the token (must be non-null/non-empty). * @return Base64-encoded token string, or {@code null} if a cryptographic error occurs. */ public String generateToken(String user) { return generateToken(user, new Date()); } /** * generateToken ** Builds a token for a given user and issuance date. The token body is: *
*
* secret + " " + user + " " + Utils.toIsoFormat(date)
*
* * and then Base64-encoded in UTF-8. *
* * @param user Username to embed. * @param date Issuance date to include in the token (affects expiry and secret derivation). * @return Base64 token, or {@code null} upon failure. */ public String generateToken(String user, Date date) { try { String secret = generateSecret(user, date); StringBuilder sb = new StringBuilder(); sb.append(secret); sb.append(' '); sb.append(user); sb.append(' '); sb.append(Utils.toIsoFormat(date)); // Standard UTF-8 encoding before Base64 return Base64.getEncoder().encodeToString(sb.toString().getBytes(StandardCharsets.UTF_8)); } catch (NoSuchAlgorithmException e) { LOG.error("Error generating SHA-256 hash", e); } catch (UnsupportedEncodingException e) { LOG.error("Error generating SHA-256 hash", e); } return null; } /** * generateSecret ** Derives the deterministic secret (a 64-hex-character SHA-256 digest) used to * authenticate a token. Inputs are concatenated in the following order: *
** Validates the structure, signature and expiry of the given token. * Steps performed: *
** Extracts the username portion from a validly structured token. *
* * @param token Base64 token string (may be {@code null}). * @return Username if the token has at least three space-separated fields after decoding; * {@code null} on error or malformed input. */ public String extractUserFromToken(String token) { try { if (token == null) { return null; } String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8); String[] parts = StringUtils.split(tokenDecoded, ' '); if (parts == null || parts.length < 3) { return null; } String user = parts[1]; return user; } catch (Exception e) { LOG.error("Error decoding Base64 token", e); } return null; } /** * extractDateCreationFromToken ** Parses and returns the issuance {@link Date} embedded in the token, without * performing validation or expiry checks. *
* * @param token Base64 token string. * @return Issuance {@link Date}, or {@code null} if the token is malformed or cannot be decoded. */ public Date extractDateCreationFromToken(String token) { try { String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8); String[] parts = StringUtils.split(tokenDecoded, ' '); if (parts == null || parts.length < 3) { return null; } Date date = Utils.toDateFromIso(parts[2]); return date; } catch (Exception e) { LOG.error("Error decoding Base64 token", e); } return null; } // --------------------------------------------------------------------- // Demo / manual test // --------------------------------------------------------------------- /** * main ** Simple manual test demonstrating generation and validation of a special * "_client" token that bypasses expiry via a negative epoch date. *
* * @param args CLI args (unused). * @throws IOException If something goes wrong while encoding/decoding Base64 (unlikely). */ public static void main(String[] args) throws IOException { // client token: // OTk3ODRiMzY5NzQ5MWI5NmYyZGQyODRiYjY2ZTU2YzdmMTZjYzM3YTY3N2ExM2M3ODI2MjU5ZTMzOTIyYjUzNSBfY2xpZW50IDE5NzAtMDEtMDFUMDA6NTk6NTkuOTk5KzAxMDA= // OTk3ODRiMzY5NzQ5MWI5NmYyZGQyODRiYjY2ZTU2YzdmMTZjYzM3YTY3N2ExM2M3ODI2MjU5ZTMzOTIyYjUzNSBfY2xpZW50IDE5NzAtMDEtMDFUMDA6NTk6NTkuOTk5KzAxMDA= String t = new TokenHelper().generateToken("_client", new Date(-1)); System.out.println("client token: " + t); System.out.println("client token: " + new String(Base64.getDecoder().decode(t), StandardCharsets.UTF_8)); System.out.println("is valid client token: " + new TokenHelper().isTokenValid(t)); } }