Coding Interview Questions
Write code to create and verify an HS256 JWT using only the JDK.
Example
Signs header.payload with HMAC-SHA256, then verifies the signature (in constant time) and the expiry. A tampered payload fails.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
public class Main {
static final byte[] SECRET = "shopease-demo-secret-key-32-bytes!".getBytes(StandardCharsets.UTF_8);
static final Base64.Encoder B64 = Base64.getUrlEncoder().withoutPadding();
static byte[] hmac(String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET, "HmacSHA256"));
return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
}
static String sign(String payloadJson) throws Exception {
String header = B64.encodeToString("{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8));
String payload = B64.encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
return header + "." + payload + "." + B64.encodeToString(hmac(header + "." + payload));
}
static String verify(String token, long nowEpochSec) throws Exception {
String[] parts = token.split("\\.");
if (parts.length != 3) return "INVALID: malformed";
byte[] expected = hmac(parts[0] + "." + parts[1]);
byte[] actual = Base64.getUrlDecoder().decode(parts[2]);
if (!MessageDigest.isEqual(expected, actual)) return "INVALID: bad signature";
String json = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
long exp = Long.parseLong(json.replaceAll(".*\"exp\":(\\d+).*", "$1"));
if (nowEpochSec >= exp) return "INVALID: expired";
return "VALID " + json;
}
public static void main(String[] args) throws Exception {
String token = sign("{\"sub\":\"[email protected]\",\"role\":\"USER\",\"exp\":1760000900}");
System.out.println("Token: " + token);
System.out.println("Check at t=1760000000 -> " + verify(token, 1760000000L));
System.out.println("Check at t=1760001000 -> " + verify(token, 1760001000L));
// Attacker changes role to ADMIN but cannot re-sign without the secret
String[] p = token.split("\\.");
String forged = p[0] + "." + B64.encodeToString(
"{\"sub\":\"[email protected]\",\"role\":\"ADMIN\",\"exp\":1760000900}".getBytes(StandardCharsets.UTF_8)) + "." + p[2];
System.out.println("Forged token -> " + verify(forged, 1760000000L));
}
}Output
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhc2hhQHNob3BlYXNlLmluIiwicm9sZSI6IlVTRVIiLCJleHAiOjE3NjAwMDA5MDB9.2oOOu9eIJnB4-hzcOPAMBxeqwlEkWHXmbEDB2IvQ3gI
Check at t=1760000000 -> VALID {"sub":"[email protected]","role":"USER","exp":1760000900}
Check at t=1760001000 -> INVALID: expired
Forged token -> INVALID: bad signatureTip: Always compare signatures with MessageDigest.isEqual (constant time), not Arrays.equals or String.equals, to avoid timing attacks. Real projects use a library (Nimbus JOSE, jjwt) that also checks alg.
Implement salted password hashing and verification (like BCrypt) with PBKDF2.
Example
Stores algorithm$iterations$salt$hash, like a BCrypt string. The salt is fixed here only so the output is repeatable; real code uses SecureRandom.
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.MessageDigest;
import java.util.Base64;
public class Main {
static final int ITERATIONS = 100_000;
static byte[] pbkdf2(String password, byte[] salt, int iterations) throws Exception {
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, 256);
return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
}
static String encode(String password, byte[] salt) throws Exception {
Base64.Encoder b64 = Base64.getEncoder().withoutPadding();
return "pbkdf2$" + ITERATIONS + "$" + b64.encodeToString(salt) + "$"
+ b64.encodeToString(pbkdf2(password, salt, ITERATIONS));
}
static boolean matches(String raw, String stored) throws Exception {
String[] parts = stored.split("\\$");
int iterations = Integer.parseInt(parts[1]);
byte[] salt = Base64.getDecoder().decode(parts[2]);
byte[] expected = Base64.getDecoder().decode(parts[3]);
return MessageDigest.isEqual(expected, pbkdf2(raw, salt, iterations));
}
public static void main(String[] args) throws Exception {
byte[] saltA = "salt-for-asha-01".getBytes(); // demo only: use SecureRandom
byte[] saltB = "salt-for-ravi-02".getBytes();
String asha = encode("Mumbai@123", saltA);
String ravi = encode("Mumbai@123", saltB);
System.out.println("asha stored: " + asha);
System.out.println("ravi stored: " + ravi);
System.out.println("Same password, same hash? " + asha.equals(ravi));
System.out.println("asha logs in with Mumbai@123 -> " + matches("Mumbai@123", asha));
System.out.println("asha logs in with mumbai@123 -> " + matches("mumbai@123", asha));
}
}Output
asha stored: pbkdf2$100000$c2FsdC1mb3ItYXNoYS0wMQ$IkP9y0UjQ0rBlJ056H3OA7Znhka223Sl+7z3hBL4F14 ravi stored: pbkdf2$100000$c2FsdC1mb3ItcmF2aS0wMg$cxr7VT0FSQJjQ1pXIUfNKcmNAfcIstr2DPnNHbeiIV4 Same password, same hash? false asha logs in with Mumbai@123 -> true asha logs in with mumbai@123 -> false
Key points to say: unique salt per user (defeats rainbow tables), many iterations (slow brute force), constant-time comparison, and parameters stored with the hash so they can be raised later.
Implement role-based access checks with a role hierarchy and first-match URL rules.
Example
A mini version of authorizeHttpRequests + RoleHierarchy.
import java.util.*;
public class Main {
record Rule(String method, String pathPrefix, String requiredRole) {}
static final Map<String, List<String>> HIERARCHY = Map.of(
"ROLE_ADMIN", List.of("ROLE_MANAGER"),
"ROLE_MANAGER", List.of("ROLE_USER"));
static final List<Rule> RULES = List.of(
new Rule("GET", "/api/products", null), // permitAll
new Rule("*", "/api/products", "ROLE_ADMIN"),
new Rule("*", "/api/reports", "ROLE_MANAGER"),
new Rule("*", "/api/orders", "ROLE_USER"));
static Set<String> reachable(Set<String> granted) {
Set<String> all = new TreeSet<>();
Deque<String> todo = new ArrayDeque<>(granted);
while (!todo.isEmpty()) {
String r = todo.pop();
if (all.add(r)) todo.addAll(HIERARCHY.getOrDefault(r, List.of()));
}
return all;
}
static String check(Set<String> granted, String method, String path) {
for (Rule rule : RULES) { // first match wins
boolean methodOk = rule.method().equals("*") || rule.method().equals(method);
if (methodOk && path.startsWith(rule.pathPrefix())) {
if (rule.requiredRole() == null) return "200 (public)";
if (granted.isEmpty()) return "401 Unauthorized";
return reachable(granted).contains(rule.requiredRole()) ? "200 OK" : "403 Forbidden";
}
}
return "403 Forbidden (denyAll)";
}
public static void main(String[] args) {
Set<String> anonymous = Set.of(), user = Set.of("ROLE_USER"), admin = Set.of("ROLE_ADMIN");
System.out.println("admin expands to " + reachable(admin));
System.out.println("anonymous GET /api/products/7 -> " + check(anonymous, "GET", "/api/products/7"));
System.out.println("anonymous POST /api/products -> " + check(anonymous, "POST", "/api/products"));
System.out.println("user POST /api/products -> " + check(user, "POST", "/api/products"));
System.out.println("admin POST /api/products -> " + check(admin, "POST", "/api/products"));
System.out.println("user GET /api/reports -> " + check(user, "GET", "/api/reports"));
System.out.println("admin GET /api/orders/42 -> " + check(admin, "GET", "/api/orders/42"));
System.out.println("user GET /api/secret -> " + check(user, "GET", "/api/secret"));
}
}Output
admin expands to [ROLE_ADMIN, ROLE_MANAGER, ROLE_USER] anonymous GET /api/products/7 -> 200 (public) anonymous POST /api/products -> 401 Unauthorized user POST /api/products -> 403 Forbidden admin POST /api/products -> 200 OK user GET /api/reports -> 403 Forbidden admin GET /api/orders/42 -> 200 OK user GET /api/secret -> 403 Forbidden (denyAll)
Implement account lockout after 3 failed login attempts within 15 minutes.
Example
Timestamps are passed in (in seconds) so the logic is easy to test; a real app uses Clock and Redis/DB storage.
import java.util.*;
public class Main {
static final int MAX_FAILURES = 3;
static final long WINDOW_SEC = 15 * 60, LOCK_SEC = 30 * 60;
static final Map<String, Deque<Long>> failures = new HashMap<>();
static final Map<String, Long> lockedUntil = new HashMap<>();
static final Map<String, String> passwords = Map.of("[email protected]", "Mumbai@123");
static String login(String user, String password, long now) {
Long until = lockedUntil.get(user);
if (until != null && now < until) return "LOCKED (try after t=" + until + ")";
if (password.equals(passwords.get(user))) {
failures.remove(user);
lockedUntil.remove(user);
return "SUCCESS";
}
Deque<Long> times = failures.computeIfAbsent(user, k -> new ArrayDeque<>());
times.addLast(now);
while (!times.isEmpty() && now - times.peekFirst() > WINDOW_SEC) times.pollFirst();
if (times.size() >= MAX_FAILURES) {
lockedUntil.put(user, now + LOCK_SEC);
times.clear();
return "BAD CREDENTIALS -> account locked";
}
return "BAD CREDENTIALS (" + times.size() + "/" + MAX_FAILURES + ")";
}
public static void main(String[] args) {
String u = "[email protected]";
long[] times = {0, 60, 120, 180, 2000};
String[] tries = {"wrong1", "wrong2", "wrong3", "Mumbai@123", "Mumbai@123"};
for (int i = 0; i < times.length; i++) {
System.out.printf("t=%-5d %-11s -> %s%n", times[i], tries[i], login(u, tries[i], times[i]));
}
}
}Output
t=0 wrong1 -> BAD CREDENTIALS (1/3) t=60 wrong2 -> BAD CREDENTIALS (2/3) t=120 wrong3 -> BAD CREDENTIALS -> account locked t=180 Mumbai@123 -> LOCKED (try after t=1920) t=2000 Mumbai@123 -> SUCCESS
In Spring Security, record failures in an ApplicationListener<AuthenticationFailureBadCredentialsEvent> and return accountNonLocked = false from UserDetails; DaoAuthenticationProvider then throws LockedException.
Implement refresh token rotation with reuse detection.
Example
Each refresh returns a new token and retires the old one. Reusing a retired token revokes the whole family (likely theft).
import java.util.*;
public class Main {
record TokenInfo(String user, String family, boolean used) {}
static final Map<String, TokenInfo> store = new LinkedHashMap<>();
static final Set<String> revokedFamilies = new HashSet<>();
static int counter = 0;
static String issue(String user, String family) {
String token = "rt-" + (++counter);
store.put(token, new TokenInfo(user, family, false));
return token;
}
static String refresh(String token) {
TokenInfo info = store.get(token);
if (info == null) return "401 unknown token";
if (revokedFamilies.contains(info.family())) return "401 family revoked";
if (info.used()) {
revokedFamilies.add(info.family());
return "401 REUSE DETECTED, family " + info.family() + " revoked";
}
store.put(token, new TokenInfo(info.user(), info.family(), true));
return "200 new refresh token " + issue(info.user(), info.family());
}
public static void main(String[] args) {
String t1 = issue("[email protected]", "fam-A");
System.out.println("login -> " + t1);
System.out.println("refresh rt-1 -> " + refresh(t1));
System.out.println("refresh rt-2 -> " + refresh("rt-2"));
System.out.println("thief uses rt-1 again -> " + refresh(t1));
System.out.println("user uses rt-3 -> " + refresh("rt-3"));
}
}Output
login -> rt-1 refresh rt-1 -> 200 new refresh token rt-2 refresh rt-2 -> 200 new refresh token rt-3 thief uses rt-1 again -> 401 REUSE DETECTED, family fam-A revoked user uses rt-3 -> 401 family revoked