From 6a335d49268127d776b0df2de9c38be9b1581e47 Mon Sep 17 00:00:00 2001 From: Ignacio Baca Moreno-Torres Date: Tue, 14 Jul 2026 10:48:06 +0200 Subject: [PATCH] JSDK-105 security: remove session token exposure from sdk-java-samples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove sessionChangeHook that logged raw bearer token to stdout on every session rotation - Remove all session file code: saveSessionFile, loadSessionFile, deleteSessionFile, SESSION_FILE constant, session.local.properties - Remove sessionId arg — samples no longer accept or cache a session - newApi() now follows sdk-dotnet-samples pattern exactly: new GeotabApi(...) → authenticate() → return; transparent re-auth on expiry is handled internally by GeotabApi.doCall() - Add env var support: GEOTAB_SERVER, GEOTAB_DATABASE, GEOTAB_USER, GEOTAB_PASSWORD (checked after system props, before interactive prompt) - Update README: document three-priority credential resolution, remove session cache docs, add CI/CD usage examples Co-Authored-By: Claude --- README.md | 28 ++++- src/main/java/com/geotab/sdk/Util.java | 141 +++++-------------------- 2 files changed, 54 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 649fc96..e4f6cc7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,33 @@ mvn compile mvn exec:java -Dapp= ``` -Credentials are prompted interactively on first run and cached in `session.local.properties` (gitignored). The last chosen app is remembered in `app.local.properties`. +Credentials are resolved in priority order: + +1. **System properties** — `-Dserver=my.geotab.com -Ddatabase=G560 -Dusername=user@mail.com -Dpassword=secret` +2. **Environment variables** — `GEOTAB_SERVER`, `GEOTAB_DATABASE`, `GEOTAB_USER`, `GEOTAB_PASSWORD` +3. **Interactive prompts** — entered at the terminal when a required value is missing + +The password is never echoed or stored. No credential files are written to disk. + +### Example — environment variables (recommended for CI/CD) + +```shell +export GEOTAB_SERVER=my.geotab.com +export GEOTAB_DATABASE=G560 +export GEOTAB_USER=user@mail.com +export GEOTAB_PASSWORD=secret +mvn exec:java -Dapp=getCount +``` + +### Example — system properties + +```shell +mvn exec:java -Dapp=getCount \ + -Dserver=my.geotab.com \ + -Ddatabase=G560 \ + -Dusername=user@mail.com \ + -Dpassword=secret +``` ## Examples diff --git a/src/main/java/com/geotab/sdk/Util.java b/src/main/java/com/geotab/sdk/Util.java index d2174e7..17b439b 100644 --- a/src/main/java/com/geotab/sdk/Util.java +++ b/src/main/java/com/geotab/sdk/Util.java @@ -1,31 +1,22 @@ package com.geotab.sdk; import static com.geotab.http.invoker.ServerInvoker.DEFAULT_TIMEOUT; -import static com.geotab.plain.Entities.UserEntity; import static com.google.common.base.Strings.isNullOrEmpty; import com.geotab.api.GeotabApi; -import com.geotab.http.exception.InvalidUserException; import com.geotab.model.login.Credentials; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Properties; import java.util.stream.Stream; public interface Util { class Cmd { - private static final String SESSION_FILE = "session.local.properties"; - private final Class type; private final Map args = new LinkedHashMap<>(); public final String server; public Credentials credentials; - private boolean sessionLoaded; public Cmd(Class type, Arg... extraArgs) { this.type = type; @@ -33,29 +24,22 @@ public Cmd(Class type, Arg... extraArgs) { this.args.put("database", new Arg("database", true, "The database name (ex. G560)")); this.args.put("username", new Arg("username", true, "The Geotab user name (ex. user@mail.com)")); this.args.put("password", new Arg("password", false, "The Geotab password")); - this.args.put("sessionId", new Arg("sessionId", false, "The Geotab sessionId")); Stream.of(extraArgs).forEach(arg -> this.args.put(arg.name, arg)); - // System properties + // Priority 1: system properties (-Dserver=... -Ddatabase=... etc.) for (Arg arg : this.args.values()) { arg.value = System.getProperty(arg.name); } - // Session file fills gaps (system props take precedence) - Properties session = loadSessionFile(); - if (session != null) { - for (String key : new String[] { "server", "database", "username", "sessionId" }) { - Arg arg = this.args.get(key); - if (arg != null && isNullOrEmpty(arg.value)) { - arg.value = session.getProperty(key); - } - } - sessionLoaded = !isNullOrEmpty(this.args.get("sessionId").value); - } + // Priority 2: environment variables (GEOTAB_SERVER, GEOTAB_DATABASE, GEOTAB_USER, GEOTAB_PASSWORD) + fillFromEnv("server", "GEOTAB_SERVER"); + fillFromEnv("database", "GEOTAB_DATABASE"); + fillFromEnv("username", "GEOTAB_USER"); + fillFromEnv("password", "GEOTAB_PASSWORD"); - // Interactive prompts for missing args (required + optional, except password/sessionId) + // Priority 3: interactive prompts for missing args (all except password) for (Arg arg : this.args.values()) { - if (arg.name.equals("password") || arg.name.equals("sessionId")) continue; + if (arg.name.equals("password")) continue; if (!isNullOrEmpty(arg.value)) continue; if (System.console() != null) { String label = arg.name + (arg.required ? "" : " (optional)") + ": "; @@ -66,39 +50,38 @@ public Cmd(Class type, Arg... extraArgs) { if (arg.required && isNullOrEmpty(arg.value)) die("Missing parameter error: " + arg.name); } - // Password prompt if no session + // Password: prompt if not already set via system property or env var var passwordArg = this.args.get("password"); - if (!sessionLoaded && isNullOrEmpty(passwordArg.value)) { + if (isNullOrEmpty(passwordArg.value)) { if (System.console() != null) { char[] pw = System.console().readPassword("password: "); if (pw != null && pw.length > 0) passwordArg.value = new String(pw); } if (isNullOrEmpty(passwordArg.value)) { - die("Missing parameter error: password (or set sessionId instead)"); + die("Missing parameter error: password (or set GEOTAB_PASSWORD)"); } } - // Show configured parameters + // Echo configured parameters — password always masked for (Arg arg : this.args.values()) { String v = arg.value; if (v == null) continue; - boolean sensitive = arg.name.equals("password") || arg.name.equals("sessionId"); - System.out.println(arg.name + ": " + (sensitive ? "***" : v)); + System.out.println(arg.name + ": " + (arg.name.equals("password") ? "***" : v)); } server = this.args.get("server").value; - if (sessionLoaded) { - credentials = Credentials.builder() - .database(this.args.get("database").value) - .userName(this.args.get("username").value) - .sessionId(this.args.get("sessionId").value) - .build(); - } else { - credentials = Credentials.builder() - .database(this.args.get("database").value) - .userName(this.args.get("username").value) - .password(passwordArg.value) - .build(); + credentials = Credentials.builder() + .database(this.args.get("database").value) + .userName(this.args.get("username").value) + .password(passwordArg.value) + .build(); + } + + private void fillFromEnv(String argName, String envVar) { + Arg arg = this.args.get(argName); + if (arg != null && isNullOrEmpty(arg.value)) { + String v = System.getenv(envVar); + if (!isNullOrEmpty(v)) arg.value = v; } } @@ -106,9 +89,10 @@ private void die(String msg) { System.out.println(msg); System.out.println(); System.out.println("Usage: mvn exec:java -Dapp= [-DparamName=value…]"); + System.out.println(" or set environment variables: GEOTAB_SERVER, GEOTAB_DATABASE, GEOTAB_USER, GEOTAB_PASSWORD"); System.out.println("Parameters:"); for (Arg arg : args.values()) { - System.out.println(arg.name + " (" + (arg.required ? "required" : "optional") + "): " + arg.description); + System.out.println(" " + arg.name + " (" + (arg.required ? "required" : "optional") + "): " + arg.description); } System.exit(1); } @@ -120,80 +104,9 @@ public String get(String name) { public GeotabApi newApi() { GeotabApi api = new GeotabApi(this.credentials, this.server, DEFAULT_TIMEOUT); - setupSessionHook(api); - - if (sessionLoaded) { - try { - api.callGetCountOf(UserEntity, null); - return api; - } catch (InvalidUserException e) { - System.out.println("Session expired, re-authenticating..."); - deleteSessionFile(); - sessionLoaded = false; - String password = promptPassword(); - if (isNullOrEmpty(password)) throw new RuntimeException("password required for re-authentication"); - credentials = Credentials.builder() - .database(this.args.get("database").value) - .userName(this.args.get("username").value) - .password(password) - .build(); - api = new GeotabApi(this.credentials, this.server, DEFAULT_TIMEOUT); - setupSessionHook(api); - } - } - api.authenticate(); return api; } - - private void setupSessionHook(GeotabApi api) { - api.sessionChangeHook = session -> { - System.out.println("New Session ID: " + session); - saveSessionFile( - this.args.get("server").value, - this.args.get("database").value, - this.args.get("username").value, - session); - }; - } - - private String promptPassword() { - if (System.console() != null) { - char[] pw = System.console().readPassword("password: "); - if (pw != null && pw.length > 0) return new String(pw); - } - return null; - } - - private static Properties loadSessionFile() { - var file = new File(SESSION_FILE); - if (!file.exists()) return null; - var props = new Properties(); - try (var reader = new FileReader(file)) { - props.load(reader); - return props; - } catch (Exception e) { - return null; - } - } - - private static void saveSessionFile( - String server, String database, String username, String sessionId) { - var props = new Properties(); - if (!isNullOrEmpty(server)) props.setProperty("server", server); - props.setProperty("database", database); - props.setProperty("username", username); - props.setProperty("sessionId", sessionId); - try (var writer = new FileWriter(SESSION_FILE)) { - props.store(writer, "Geotab session — do not commit"); - } catch (Exception e) { - System.out.println("Warning: could not save session file: " + e.getMessage()); - } - } - - private static void deleteSessionFile() { - new File(SESSION_FILE).delete(); - } } class Arg {