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 signature

Tip: 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