package net.rustnotmc.auth; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Session tokens, and the rules about when one is worth something. * * A token is 32 random bytes, compared in constant time, and carries the * account it belongs to and the moment it stops working. Logging out forgets * it immediately rather than waiting for it to expire, because a player who * clicked logout on a shared computer means it. * * Tokens live in memory only. A token that was written to disk would outlive * the process that issued it, and a stolen server data directory would then be * a stolen set of live sessions. */ public final class TokenService { /** How long a fresh token lasts. */ public static final long DEFAULT_LIFETIME_MILLIS = 12L * 60L * 60L * 1000L; /** One issued token. */ public static final class Token { public final String value; public final String account; public final long issuedAt; public final long expiresAt; Token(String value, String account, long issuedAt, long expiresAt) { this.value = value; this.account = account; this.issuedAt = issuedAt; this.expiresAt = expiresAt; } public boolean expired(long now) { return now >= this.expiresAt; } } private final Map live = new ConcurrentHashMap(); private final long lifetimeMillis; private final LoginThrottle.Clock clock; public TokenService() { this(DEFAULT_LIFETIME_MILLIS, new LoginThrottle.Clock() { public long now() { return System.currentTimeMillis(); } }); } public TokenService(long lifetimeMillis, LoginThrottle.Clock clock) { this.lifetimeMillis = lifetimeMillis; this.clock = clock; } /** Issues a token for an account, valid for the configured lifetime. */ public Token issue(String account) { long now = this.clock.now(); Token token = new Token(LoginThrottle.encode(Secure.bytes(32)), account, now, now + this.lifetimeMillis); this.live.put(token.value, token); return token; } /** The token behind a value, or null when it is unknown or expired. */ public Token check(String value) { if (value == null || value.isEmpty()) { return null; } Token token = this.live.get(value); if (token == null) { return null; } if (token.expired(this.clock.now())) { this.live.remove(value); return null; } return token; } public boolean valid(String value, String account) { Token token = check(value); return token != null && token.account.equalsIgnoreCase(account); } /** Forgets one token, so it stops being usable immediately. */ public void revoke(String value) { if (value != null) { this.live.remove(value); } } /** Forgets every token belonging to an account, for a password change. */ public int revokeAll(String account) { int gone = 0; for (Map.Entry entry : this.live.entrySet()) { if (entry.getValue().account.equalsIgnoreCase(account)) { this.live.remove(entry.getKey()); gone++; } } return gone; } /** Drops tokens that have run out, so the map does not grow forever. */ public int sweep() { long now = this.clock.now(); int gone = 0; for (Map.Entry entry : this.live.entrySet()) { if (entry.getValue().expired(now)) { this.live.remove(entry.getKey()); gone++; } } return gone; } public int live() { return this.live.size(); } public long lifetimeMillis() { return this.lifetimeMillis; } }