Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/agents/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

## Timing Attack Protection

- `tokensMatch()` enforces a **100ms floor** on token comparison to mask cache hit vs miss timing
- Each auth request holds a servlet thread for at least 100ms during token validation
- `tokensMatch()` applies **randomized jitter (80–120ms)** on token comparison to mask cache hit vs miss timing
- Each auth request holds a servlet thread for the jitter duration during token validation
- **Thread pool sizing**: with a 200-thread pool, max theoretical auth throughput is ~2,000 req/s
- Size `server.tomcat.threads.max` accordingly for auth-heavy workloads

Expand Down
6 changes: 6 additions & 0 deletions docs/agents/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
- Tokens **BCrypt-hashed** before storage (never plaintext)
- Token comparison results **cached** (Caffeine, 1hr) to reduce BCrypt overhead
- Token prefix (8-char) enables **O(1) DB lookups** vs O(n) BCrypt scans
- **DUMMY_HASH** must be a valid 60-char BCrypt hash (not a short placeholder) so `BCryptPasswordEncoder.matches()` performs a full computation on the "user not found" path, preventing user enumeration via timing

### Sub-token Validation

- Fallback linear scan in `validateSubToken()` is bounded to `maxClients` entries to limit worst-case BCrypt comparisons
- Primary path (bearer with client ID) uses direct O(1) lookup via `getByUserIdAndClient()`

### Authentication Flow

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import jakarta.servlet.http.HttpServletResponse
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Base64
import java.util.concurrent.ThreadLocalRandom
import java.util.concurrent.TimeUnit
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.ObjectProvider
Expand Down Expand Up @@ -144,11 +145,11 @@ open class OgiriTokenService<T : OgiriToken>(

val result = tokenEqualityCache.get(key) { passwordEncoder.matches(token, tokenHash) }

// Ensure minimum 100ms to mask cache hit vs miss timing
// Randomized jitter to mask cache hit vs miss timing without a fixed floor
val jitter = ThreadLocalRandom.current().nextLong(80_000_000L, 120_000_000L)
val elapsed = System.nanoTime() - startTime
val minDelayNanos = 100_000_000L // 100ms
if (elapsed < minDelayNanos) {
Thread.sleep((minDelayNanos - elapsed) / 1_000_000)
if (elapsed < jitter) {
Thread.sleep((jitter - elapsed) / 1_000_000)
}

return result
Expand Down Expand Up @@ -723,8 +724,11 @@ open class OgiriTokenService<T : OgiriToken>(
companion object {
private val logger = LoggerFactory.getLogger(OgiriTokenService::class.java)

// Pre-computed BCrypt hash for timing normalization
private const val DUMMY_HASH = "\$2a\$10\$dummyhashvalueforconstanttimecheck"
// Pre-computed valid BCrypt hash for constant-time comparison when user is not found.
// Must be a properly formatted 60-char BCrypt hash so that BCryptPasswordEncoder.matches()
// performs a full BCrypt computation rather than short-circuiting on pattern validation.
private const val DUMMY_HASH =
"\$2a\$10\$II9GOMND5IObe5IA9/7EXuflfP5U77aqrWyXCEXEFpXolIDmBbLmS"
}

@Transactional
Expand Down Expand Up @@ -847,6 +851,7 @@ open class OgiriTokenService<T : OgiriToken>(
repository
.findByUserIdAndTokenSubtypeOrderByUpdatedAtDesc(user.getOgiriUserId(), subTokenName)
.filter { OgiriTokenType.of(it.tokenType) == OgiriTokenType.SUB }
.take(maxClients.toInt())
return all.any { tokenMatches(it, tokenField) && registration.validate(tokenField) }
}

Expand Down
Loading