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