From 146a0fb8b0e90f9196e569152f649baf60d6cc8f Mon Sep 17 00:00:00 2001
From: Joaquín Reñé <jrene@curisit.net>
Date: Tue, 07 Oct 2025 14:52:57 +0000
Subject: [PATCH] #4410 - Comments on classes

---
 securis/src/main/java/net/curisit/securis/utils/TokenHelper.java |  187 ++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 170 insertions(+), 17 deletions(-)

diff --git a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
index ff567f7..74455a8 100644
--- a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
+++ b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.utils;
 
 import java.io.IOException;
@@ -20,37 +23,108 @@
 import java.util.Base64;
 import java.nio.charset.StandardCharsets;
 
+/**
+ * TokenHelper
+ * <p>
+ * Utility component to generate and validate short-lived authentication tokens
+ * for SeCuris services. Tokens are:
+ * </p>
+ * <ul>
+ *   <li>Base64-encoded UTF-8 strings.</li>
+ *   <li>Composed as: {@code <secret> <username> <iso8601-timestamp>} (space-separated).</li>
+ *   <li>Where {@code secret} is a deterministic SHA-256 HMAC-like hash built from a static seed,
+ *       the username, and the issuance timestamp.</li>
+ * </ul>
+ *
+ * <p><b>Lifecycle & scope:</b> {@code @ApplicationScoped}. Stateless and thread-safe.</p>
+ *
+ * <p><b>Security notes:</b>
+ *   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)}).
+ * </p>
+ *
+ * <p><b>Format details:</b>
+ *   <pre>
+ *   token = Base64( secret + " " + user + " " + Utils.toIsoFormat(date) )
+ *   secret = hex(SHA-256(seed || user || isoDate))
+ *   </pre>
+ * </p>
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 6, 2025.
+ */
 @ApplicationScoped
 public class TokenHelper {
 
     private static final Logger LOG = LogManager.getLogger(TokenHelper.class);
 
     /**
-     * Period before token expires, set in hours.
+     * Validity window for standard tokens, in hours.
+     * <p>
+     * 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)}).
+     * </p>
      */
     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<p>
+     * CDI no-arg constructor.
+     * <p>
+     * Kept for dependency injection. No initialization logic is required.
+     * </p>
+     */
     @Inject
     public TokenHelper() {
     }
 
+    /**
+     * Static secret seed used to derive the token {@code secret} portion.
+     * <p>
+     * Treat this as confidential. Changing it invalidates all outstanding tokens.
+     * </p>
+     */
     private static byte[] seed = "S3Cur15S33dForT0k3nG3n3r@tion".getBytes();
 
+    // ---------------------------------------------------------------------
+    // Token generation
+    // ---------------------------------------------------------------------
+
     /**
-     * Generate a token encoded in Base64 for user passed as parameter and
-     * taking the current moment as token timestamp
-     * 
-     * @param user
-     * @return
+     * generateToken
+     * <p>
+     * Convenience overload that generates a token for {@code user} using the current
+     * system time as the issuance timestamp.
+     * </p>
+     *
+     * @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
+     * <p>
+     * Builds a token for a given user and issuance date. The token body is:
+     * </p>
+     * <pre>
+     * secret + " " + user + " " + Utils.toIsoFormat(date)
+     * </pre>
+     * <p>
+     * and then Base64-encoded in UTF-8.
+     * </p>
+     *
+     * @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);
@@ -61,7 +135,7 @@
             sb.append(' ');
             sb.append(Utils.toIsoFormat(date));
 
-            // Codificación estándar con UTF-8
+            // Standard UTF-8 encoding before Base64
             return Base64.getEncoder().encodeToString(sb.toString().getBytes(StandardCharsets.UTF_8));
 
         } catch (NoSuchAlgorithmException e) {
@@ -72,42 +146,86 @@
         return null;
     }
 
+    /**
+     * generateSecret
+     * <p>
+     * Derives the deterministic secret (a 64-hex-character SHA-256 digest) used to
+     * authenticate a token. Inputs are concatenated in the following order:
+     * </p>
+     * <ol>
+     *   <li>{@link #seed}</li>
+     *   <li>{@code user} (UTF-8 bytes)</li>
+     *   <li>{@code Utils.toIsoFormat(date)}</li>
+     * </ol>
+     *
+     * @param user Username to mix in the digest.
+     * @param date Token issuance date to mix in the digest.
+     * @return 64-char hex string.
+     * @throws UnsupportedEncodingException If UTF-8 is unavailable (unexpected).
+     * @throws NoSuchAlgorithmException     If SHA-256 is unavailable (unexpected).
+     */
     private String generateSecret(String user, Date date) throws UnsupportedEncodingException, NoSuchAlgorithmException {
         MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
         mDigest.update(seed, 0, seed.length);
+
         byte[] userbytes = user.getBytes("utf-8");
         mDigest.update(userbytes, 0, userbytes.length);
+
         byte[] isodate = Utils.toIsoFormat(date).getBytes();
         mDigest.update(isodate, 0, isodate.length);
+
         BigInteger i = new BigInteger(1, mDigest.digest());
         String secret = String.format("%1$064x", i);
         return secret;
     }
 
+    // ---------------------------------------------------------------------
+    // Token validation & parsing
+    // ---------------------------------------------------------------------
+
     /**
-     * Check if passed token is still valid, It use to check if token is expired
-     * the attribute VALID_TOKEN_PERIOD (in hours)
-     * 
-     * @param token
-     * @return
+     * isTokenValid
+     * <p>
+     * Validates the structure, signature and expiry of the given token.
+     * Steps performed:
+     * </p>
+     * <ol>
+     *   <li>Base64-decode the token into {@code "secret user isoDate"}.</li>
+     *   <li>Parse {@code user} and {@code isoDate}; recompute the expected secret via
+     *       {@link #generateSecret(String, Date)} and compare with the provided one.</li>
+     *   <li>Check expiry: if the token's timestamp is older than
+     *       {@link #VALID_TOKEN_PERIOD} hours, it's invalid.</li>
+     *   <li>Special-case: if {@code user} equals {@link ApiResource#API_CLIENT_USERNAME}
+     *       and the date returns a non-positive epoch time (e.g., created with {@code new Date(-1)}),
+     *       the expiry check is skipped (client integration token).</li>
+     * </ol>
+     *
+     * @param token Base64 token string.
+     * @return {@code true} if valid and not expired; {@code false} otherwise.
      */
     public boolean isTokenValid(String token) {
         try {
-        	String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
+            String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
             String[] parts = StringUtils.split(tokenDecoded, ' ');
             if (parts == null || parts.length < 3) {
                 return false;
             }
+
             String secret = parts[0];
             String user = parts[1];
             Date date = Utils.toDateFromIso(parts[2]);
+
+            // Expiry check (unless special client token rule applies)
             if (date.getTime() > 0 || !user.equals(ApiResource.API_CLIENT_USERNAME)) {
                 if (new Date().after(new Date(date.getTime() + VALID_TOKEN_PERIOD * 60 * 60 * 1000))) {
                     return false;
                 }
-            } // else: It's a securis-client API call
+            }
+
+            // Signature check
             String newSecret = generateSecret(user, date);
             return newSecret.equals(secret);
+
         } catch (IOException e) {
             LOG.error("Error decoding Base64 token", e);
         } catch (NoSuchAlgorithmException e) {
@@ -116,6 +234,16 @@
         return false;
     }
 
+    /**
+     * extractUserFromToken
+     * <p>
+     * Extracts the username portion from a validly structured token.
+     * </p>
+     *
+     * @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) {
@@ -134,6 +262,16 @@
         return null;
     }
 
+    /**
+     * extractDateCreationFromToken
+     * <p>
+     * Parses and returns the issuance {@link Date} embedded in the token, without
+     * performing validation or expiry checks.
+     * </p>
+     *
+     * @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);
@@ -149,6 +287,20 @@
         return null;
     }
 
+    // ---------------------------------------------------------------------
+    // Demo / manual test
+    // ---------------------------------------------------------------------
+
+    /**
+     * main
+     * <p>
+     * Simple manual test demonstrating generation and validation of a special
+     * "_client" token that bypasses expiry via a negative epoch date.
+     * </p>
+     *
+     * @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=
@@ -160,3 +312,4 @@
         System.out.println("is valid client token: " + new TokenHelper().isTokenValid(t));
     }
 }
+

--
Gitblit v1.3.2