A Minecraft Fabric mod that connects the game with ATProtocol ⛏️
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: remove deprecated server-side login

Login is handled client-side via the mod config screen. Removes:
- AtProtoCommands.loginDeprecated() (dead code, not registered)
- AtProtoSessionManager.createSession() (deprecated, unused)
- AtProtoClient.createSession() (server-side, unused)
- AtProtoClient.CreateSessionRequest data class
- RateLimiter field and cleanup() from AtProtoCommands
- Stale /atproto login references in help text

Unused imports (Duration, Instant, ServerPlayer, SecurityUtils)
cleaned up. RateLimiter.kt kept for potential future use.

+7 -226
+1 -1
src/main/kotlin/com/jollywhoppers/Atprotoconnect.kt
··· 235 235 // Cleanup rate limiter every hour 236 236 scheduler.scheduleAtFixedRate({ 237 237 try { 238 - commands.cleanup() 238 + // Rate limiter cleanup removed (server-side login deprecated) 239 239 logger.debug("Rate limiter cleanup completed") 240 240 } catch (e: Exception) { 241 241 logger.error("Failed to cleanup rate limiter", e)
-51
src/main/kotlin/com/jollywhoppers/atproto/server/AtProtoClient.kt
··· 88 88 ) 89 89 90 90 @Serializable 91 - data class CreateSessionRequest( 92 - val identifier: String, 93 - val password: String 94 - ) 95 - 96 - @Serializable 97 91 data class CreateSessionResponse( 98 92 val did: String, 99 93 val handle: String, ··· 260 254 val profile = json.decodeFromString<ProfileView>(response.body()) 261 255 logger.info("Retrieved profile successfully") 262 256 profile 263 - } 264 - 265 - /** 266 - * Creates an authenticated session using identifier and app password. 267 - * This is the primary authentication method for the mod. 268 - * Password is NOT logged for security. 269 - */ 270 - suspend fun createSession(identifier: String, password: String): Result<CreateSessionResponse> = runCatching { 271 - logger.info("Creating session for: ${SecurityUtils.sanitizeForLog(identifier)}") 272 - 273 - // Resolve to find the correct PDS 274 - val pdsUrl = try { 275 - val miniDoc = resolveMiniDoc(identifier).getOrThrow() 276 - miniDoc.pds 277 - } catch (e: Exception) { 278 - logger.warn("Could not resolve PDS via Slingshot, trying fallback") 279 - fallbackPdsUrl 280 - } 281 - 282 - val requestBody = CreateSessionRequest( 283 - identifier = identifier, 284 - password = password // Password is never logged 285 - ) 286 - 287 - val url = "$pdsUrl/xrpc/com.atproto.server.createSession" 288 - validateUrl(url) 289 - 290 - val request = HttpRequest.newBuilder() 291 - .uri(URI.create(url)) 292 - .POST(HttpRequest.BodyPublishers.ofString(json.encodeToString(CreateSessionRequest.serializer(), requestBody))) 293 - .header("Content-Type", "application/json") 294 - .timeout(Duration.ofSeconds(15)) 295 - .build() 296 - 297 - val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) 298 - 299 - if (response.statusCode() != 200) { 300 - // Don't log response body as it might contain sensitive info 301 - logger.error("Session creation failed: HTTP ${response.statusCode()}") 302 - throw Exception("Authentication failed") 303 - } 304 - 305 - val session = json.decodeFromString<CreateSessionResponse>(response.body()) 306 - logger.info("Session created successfully") 307 - session 308 257 } 309 258 310 259 /**
+6 -138
src/main/kotlin/com/jollywhoppers/atproto/server/AtProtoCommands.kt
··· 1 1 package com.jollywhoppers.atproto.server 2 2 3 - import com.jollywhoppers.security.RateLimiter 4 3 import com.jollywhoppers.security.SecurityAuditor 5 - import com.jollywhoppers.security.SecurityUtils 6 4 import com.mojang.brigadier.CommandDispatcher 7 5 import com.mojang.brigadier.arguments.StringArgumentType 8 6 import com.mojang.brigadier.context.CommandContext ··· 12 10 import net.minecraft.commands.CommandSourceStack 13 11 import net.minecraft.commands.Commands 14 12 import net.minecraft.network.chat.Component 15 - import net.minecraft.server.level.ServerPlayer 16 13 import org.slf4j.LoggerFactory 17 - import java.time.Duration 18 - import java.time.Instant 19 14 20 15 /** 21 16 * Handles AT Protocol-related commands for players. ··· 36 31 ) { 37 32 private val logger = LoggerFactory.getLogger("atproto-connect") 38 33 private val coroutineScope = CoroutineScope(Dispatchers.IO) 39 - 40 - // Rate limiter: 3 attempts per 15 minutes, 30 minute lockout 41 - private val rateLimiter = RateLimiter( 42 - maxAttempts = 3, 43 - windowSeconds = 900, // 15 minutes 44 - lockoutSeconds = 1800 // 30 minutes 45 - ) 46 34 47 35 /** 48 36 * Registers all AT Protocol commands. ··· 168 156 append(Component.literal("\n§7Display Name: §f$it")) 169 157 } 170 158 } 171 - .append(Component.literal("\n\n§eNote: Use §f/atproto login§e to authenticate and sync data")) 159 + .append(Component.literal("\n\n§eNote: Use the mod config screen to authenticate and sync data")) 172 160 ) 173 161 174 162 logger.info("Player ${player.name.string} (${player.uuid}) linked to ${profile.handle}") ··· 220 208 } 221 209 222 210 /** 223 - * Authenticates a player with their AT Protocol credentials. 224 - * @deprecated Login is now handled client-side 225 - */ 226 - @Deprecated("Login is now handled client-side") 227 - private fun loginDeprecated(context: CommandContext<CommandSourceStack>): Int { 228 - val player = context.source.playerOrException 229 - val identifier = StringArgumentType.getString(context, "identifier") 230 - val password = StringArgumentType.getString(context, "password") 231 - 232 - // Check rate limit BEFORE attempting authentication 233 - val rateLimit = rateLimiter.checkAttempt(player.uuid) 234 - if (!rateLimit.allowed) { 235 - val lockUntil = rateLimit.lockedUntil 236 - if (lockUntil != null) { 237 - val minutesRemaining = Duration.between( 238 - Instant.now(), 239 - lockUntil 240 - ).toMinutes() 241 - 242 - SecurityAuditor.logRateLimitLockout(player.uuid, player.name.string, minutesRemaining) 243 - 244 - context.source.sendFailure( 245 - Component.literal("§c✗ Too many failed authentication attempts") 246 - .append(Component.literal("\n§7Your account has been temporarily locked for security")) 247 - .append(Component.literal("\n§7Please try again in §f$minutesRemaining minutes")) 248 - .append(Component.literal("\n\n§7If you're having trouble, check your app password")) 249 - ) 250 - logger.warn("Player ${player.name.string} (${player.uuid}) blocked by rate limiter") 251 - return 0 252 - } 253 - } 254 - 255 - val attemptsRemaining = rateLimit.attemptsRemaining 256 - context.source.sendSuccess( 257 - { 258 - Component.literal("§eAuthenticating with AT Protocol...") 259 - .append(Component.literal(" §7(${attemptsRemaining} attempts remaining)")) 260 - }, 261 - false 262 - ) 263 - 264 - coroutineScope.launch { 265 - try { 266 - // Create session (password is not logged) 267 - val session = sessionManager.createSession(player.uuid, identifier, password).getOrThrow() 268 - 269 - // Link identity if not already linked 270 - if (!identityStore.isLinked(player.uuid)) { 271 - identityStore.linkIdentity(player.uuid, session.did, session.handle) 272 - } 273 - 274 - // Record successful authentication (clears rate limit) 275 - rateLimiter.recordSuccess(player.uuid) 276 - 277 - // Audit log 278 - SecurityAuditor.logAuthSuccess(player.uuid, session.handle, player.name.string) 279 - 280 - player.sendSystemMessage( 281 - Component.literal("§a✓ Successfully authenticated!") 282 - .append(Component.literal("\n§7Handle: §f${session.handle}")) 283 - .append(Component.literal("\n§7DID: §f${session.did}")) 284 - .append(Component.literal("\n§7PDS: §f${session.pdsUrl}")) 285 - .append(Component.literal("\n\n§aYou can now sync your Minecraft data to AT Protocol!")) 286 - ) 287 - 288 - logger.info("Player ${player.name.string} (${player.uuid}) authenticated as ${session.handle}") 289 - } catch (e: Exception) { 290 - // Record failed attempt 291 - rateLimiter.recordFailure(player.uuid) 292 - val status = rateLimiter.getStatus(player.uuid) 293 - 294 - // Audit log 295 - SecurityAuditor.logAuthFailure( 296 - player.uuid, 297 - SecurityUtils.sanitizeForLog(identifier), 298 - e.javaClass.simpleName, 299 - player.name.string 300 - ) 301 - 302 - if (status.attemptsRemaining > 0) { 303 - player.sendSystemMessage( 304 - Component.literal("§c✗ Authentication failed") 305 - .append(Component.literal("\n§7${sanitizeError(e)}")) 306 - .append(Component.literal("\n§7Attempts remaining: §f${status.attemptsRemaining}")) 307 - .append(Component.literal("\n\n§7Tip: Use an §fApp Password§7 from your AT Protocol account settings")) 308 - .append(Component.literal("\n§cNever use your main account password!")) 309 - ) 310 - } else { 311 - val lockUntil = status.lockedUntil 312 - val minutesRemaining = if (lockUntil != null) { 313 - Duration.between(Instant.now(), lockUntil).toMinutes() 314 - } else 30 315 - 316 - SecurityAuditor.logRateLimitHit(player.uuid, player.name.string) 317 - 318 - player.sendSystemMessage( 319 - Component.literal("§c✗ Authentication failed - Rate limit exceeded") 320 - .append(Component.literal("\n§7Too many failed attempts")) 321 - .append(Component.literal("\n§7Your account is locked for §f$minutesRemaining minutes")) 322 - .append(Component.literal("\n\n§7Please verify your app password and try again later")) 323 - ) 324 - } 325 - 326 - logger.error("Failed to authenticate player ${player.name.string}: ${e.javaClass.simpleName}") 327 - } 328 - } 329 - 330 - return 1 331 - } 332 - 333 - /** 334 211 * Logs out a player (removes their authentication session). 335 212 */ 336 213 private fun logout(context: CommandContext<CommandSourceStack>): Int { ··· 349 226 { 350 227 Component.literal("§a✓ Logged out successfully") 351 228 .append(Component.literal("\n§7Your identity link remains active")) 352 - .append(Component.literal("\n§7Use §f/atproto login§7 to authenticate again")) 229 + .append(Component.literal("\n§7Use the mod config screen to authenticate again")) 353 230 }, 354 231 false 355 232 ) ··· 395 272 .append(Component.literal("\n§7You can sync data to AT Protocol")) 396 273 } else { 397 274 Component.literal("\n§cAuthentication: §f✗ Not logged in") 398 - .append(Component.literal("\n§7Use §f/atproto login§7 to authenticate")) 275 + .append(Component.literal("\n§7Use the mod config screen to authenticate")) 399 276 } 400 277 ) 401 278 }, ··· 485 362 if (isLinked && isAuthenticated) { 486 363 Component.literal("\n\n§aReady to sync Minecraft data to AT Protocol!") 487 364 } else if (isLinked) { 488 - Component.literal("\n\n§eUse §f/atproto login§e to authenticate") 365 + Component.literal("\n\n§eUse the mod config screen to authenticate") 489 366 } else { 490 367 Component.literal("\n\n§eUse §f/atproto link <handle>§e to get started") 491 368 } ··· 642 519 .append(Component.literal("\n§e━━━ Client-Side Login (Type in Client) ━━━")) 643 520 .append(Component.literal("\n§eFor authentication, use the client-side commands:")) 644 521 .append(Component.literal("\n§f/atproto login <handle>")) 645 - .append(Component.literal("\n §7OAuth browser login (Recommended!)")) 646 - .append(Component.literal("\n§f/atproto login <handle> <app-password>")) 647 - .append(Component.literal("\n §7App password login (fallback)")) 522 + .append(Component.literal("\n §7Login is handled client-side (use the mod config screen)")) 648 523 .append(Component.literal("\n§7Authentication happens on your computer")) 649 524 .append(Component.literal("\n§7Your password never goes to the server!")) 650 525 }, ··· 692 567 } 693 568 694 569 /** 695 - * Periodic cleanup task for rate limiter. 696 - * Should be called from the main mod initializer. 697 - */ 698 - fun cleanup() { 699 - rateLimiter.cleanup() 700 - } 701 - 702 - /** 570 + * Builds a deterministic server ID from the server directory path. 703 571 * Builds a deterministic server ID from the server directory path. 704 572 * Matches the implementation in PlayerStatSyncService. 705 573 */
-36
src/main/kotlin/com/jollywhoppers/atproto/server/AtProtoSessionManager.kt
··· 78 78 } 79 79 80 80 /** 81 - * Creates or updates a session for a player. 82 - * @deprecated Use storeVerifiedSession for client-authenticated sessions 83 - */ 84 - @Deprecated("Use storeVerifiedSession for client-authenticated sessions") 85 - suspend fun createSession( 86 - uuid: UUID, 87 - identifier: String, 88 - password: String 89 - ): Result<PlayerSession> = runCatching { 90 - logger.info("Creating session for player $uuid with identifier ${SecurityUtils.sanitizeForLog(identifier)}") 91 - 92 - // Create the session via AT Protocol 93 - val sessionResponse = client.createSession(identifier, password).getOrThrow() 94 - 95 - // Resolve to get PDS URL 96 - val (did, handle, pdsUrl) = client.resolveIdentifier(sessionResponse.did).getOrThrow() 97 - 98 - val session = PlayerSession( 99 - uuid = uuid.toString(), 100 - did = did, 101 - handle = handle, 102 - pdsUrl = pdsUrl, 103 - accessJwt = sessionResponse.accessJwt, 104 - refreshJwt = sessionResponse.refreshJwt, 105 - createdAt = System.currentTimeMillis(), 106 - lastRefreshed = System.currentTimeMillis() 107 - ) 108 - 109 - sessions[uuid] = session 110 - save() 111 - 112 - logger.info("Session created successfully for $handle") 113 - session 114 - } 115 - 116 - /** 117 81 * Stores a verified session that was authenticated client-side. 118 82 * This is the preferred method for storing sessions. 119 83 * @param authType "oauth" or "app_password" (default: "app_password")