| 1 | package com.rbctcsworld.ecommerce.auth; | |
| 2 | ||
| 3 | import org.slf4j.Logger; | |
| 4 | import org.slf4j.LoggerFactory; | |
| 5 | ||
| 6 | import java.time.Clock; | |
| 7 | import java.time.Duration; | |
| 8 | import java.time.Instant; | |
| 9 | import java.util.concurrent.atomic.AtomicLong; | |
| 10 | ||
| 11 | /** | |
| 12 | * Failed logins counted in Redis, shared by every backend instance and kept across restarts. | |
| 13 | * | |
| 14 | * - Keys hold a SHA-256 of the e-mail, never the e-mail itself: rbc:login:fail:{hash}, rbc:login:lock:{hash}. | |
| 15 | * - Redis expires the keys (window and lock time), so nothing has to be cleaned up. | |
| 16 | * - Redis unreachable: the login keeps working and the in-memory store takes over (fail-open with a | |
| 17 | * fallback). Choosing to keep the shop usable over a hard 503; the fallback still stops brute force on | |
| 18 | * each instance. One warning per minute in the log, not one per login. | |
| 19 | */ | |
| 20 | class RedisLoginAttemptStore implements LoginAttemptStore { | |
| 21 | ||
| 22 | private static final Logger log = LoggerFactory.getLogger(RedisLoginAttemptStore.class); | |
| 23 | static final String PREFIX = "rbc:login:"; | |
| 24 | ||
| 25 | private final RedisOps redis; | |
| 26 | private final LoginAttemptStore fallback; | |
| 27 | private final Clock clock; | |
| 28 | private final int maxFailures; | |
| 29 | private final long windowMillis; | |
| 30 | private final long lockMillis; | |
| 31 | private volatile Instant lastWarning = Instant.MIN; | |
| 32 | private final AtomicLong outages = new AtomicLong(); | |
| 33 | private final AtomicLong warnings = new AtomicLong(); | |
| 34 | ||
| 35 | RedisLoginAttemptStore(RedisOps redis, LoginAttemptStore fallback, Clock clock, | |
| 36 | int maxFailures, Duration window, Duration lock) { | |
| 37 | this.redis = redis; | |
| 38 | this.fallback = fallback; | |
| 39 | this.clock = clock; | |
| 40 | this.maxFailures = maxFailures; | |
| 41 | this.windowMillis = window.toMillis(); | |
| 42 | this.lockMillis = lock.toMillis(); | |
| 43 | } | |
| 44 | ||
| 45 | static String failKey(String email) { | |
| 46 |
1
1. failKey : replaced return value with "" for com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::failKey → KILLED |
return PREFIX + "fail:" + Hashes.sha256Hex(email); |
| 47 | } | |
| 48 | ||
| 49 | static String lockKey(String email) { | |
| 50 |
1
1. lockKey : replaced return value with "" for com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::lockKey → KILLED |
return PREFIX + "lock:" + Hashes.sha256Hex(email); |
| 51 | } | |
| 52 | ||
| 53 | @Override | |
| 54 | public long lockRemainingMillis(String email) { | |
| 55 | try { | |
| 56 |
1
1. lockRemainingMillis : replaced long return with 0 for com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::lockRemainingMillis → KILLED |
return Math.max(0, redis.ttlMillis(lockKey(email))); // -2 = no lock key |
| 57 | ||
| 58 | } catch (RuntimeException e) { | |
| 59 |
1
1. lockRemainingMillis : removed call to com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::unavailable → KILLED |
unavailable(e); |
| 60 |
1
1. lockRemainingMillis : replaced long return with 0 for com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::lockRemainingMillis → KILLED |
return fallback.lockRemainingMillis(email); |
| 61 | } | |
| 62 | } | |
| 63 | ||
| 64 | @Override | |
| 65 | public void recordFailure(String email) { | |
| 66 | try { | |
| 67 | redis.recordFailure(failKey(email), lockKey(email), windowMillis, maxFailures, lockMillis); | |
| 68 | } catch (RuntimeException e) { | |
| 69 |
1
1. recordFailure : removed call to com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::unavailable → KILLED |
unavailable(e); |
| 70 |
1
1. recordFailure : removed call to com/rbctcsworld/ecommerce/auth/LoginAttemptStore::recordFailure → KILLED |
fallback.recordFailure(email); |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | @Override | |
| 75 | public void reset(String email) { | |
| 76 |
1
1. reset : removed call to com/rbctcsworld/ecommerce/auth/LoginAttemptStore::reset → KILLED |
fallback.reset(email); |
| 77 | try { | |
| 78 |
1
1. reset : removed call to com/rbctcsworld/ecommerce/auth/RedisOps::delete → KILLED |
redis.delete(failKey(email)); |
| 79 | } catch (RuntimeException e) { | |
| 80 |
1
1. reset : removed call to com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::unavailable → KILLED |
unavailable(e); |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | @Override | |
| 85 | public int failures(String email) { | |
| 86 | try { | |
| 87 |
1
1. failures : replaced int return with 0 for com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::failures → KILLED |
return (int) redis.count(failKey(email)); |
| 88 | } catch (RuntimeException e) { | |
| 89 |
1
1. failures : removed call to com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::unavailable → KILLED |
unavailable(e); |
| 90 |
1
1. failures : replaced int return with 0 for com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::failures → KILLED |
return fallback.failures(email); |
| 91 | } | |
| 92 | } | |
| 93 | ||
| 94 | @Override | |
| 95 | public void close() { | |
| 96 |
1
1. close : removed call to com/rbctcsworld/ecommerce/auth/RedisOps::close → KILLED |
redis.close(); |
| 97 | } | |
| 98 | ||
| 99 | /** Calls answered by the in-memory fallback because Redis failed. */ | |
| 100 | long outages() { | |
| 101 |
1
1. outages : replaced long return with 0 for com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::outages → KILLED |
return outages.get(); |
| 102 | } | |
| 103 | ||
| 104 | /** Warnings written to the log (at most one per minute). */ | |
| 105 | long warnings() { | |
| 106 |
1
1. warnings : replaced long return with 0 for com/rbctcsworld/ecommerce/auth/RedisLoginAttemptStore::warnings → KILLED |
return warnings.get(); |
| 107 | } | |
| 108 | ||
| 109 | private void unavailable(RuntimeException e) { | |
| 110 | outages.incrementAndGet(); | |
| 111 | Instant now = clock.instant(); | |
| 112 |
1
1. unavailable : removed conditional - replaced equality check with false → KILLED |
if (now.isAfter(lastWarning.plusSeconds(60))) { |
| 113 | lastWarning = now; | |
| 114 | warnings.incrementAndGet(); | |
| 115 | log.warn("Redis is not reachable, login lockout uses the in-memory fallback: {}", e.toString()); | |
| 116 | } | |
| 117 | } | |
| 118 | } | |
Mutations | ||
| 46 |
1.1 |
|
| 50 |
1.1 |
|
| 56 |
1.1 |
|
| 59 |
1.1 |
|
| 60 |
1.1 |
|
| 69 |
1.1 |
|
| 70 |
1.1 |
|
| 76 |
1.1 |
|
| 78 |
1.1 |
|
| 80 |
1.1 |
|
| 87 |
1.1 |
|
| 89 |
1.1 |
|
| 90 |
1.1 |
|
| 96 |
1.1 |
|
| 101 |
1.1 |
|
| 106 |
1.1 |
|
| 112 |
1.1 |