diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/ActionCache.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/ActionCache.kt new file mode 100644 index 000000000..8f47dee46 --- /dev/null +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/data/ActionCache.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.auth.data + +data class ActionCache( + val actionType: ActionType, + val remainingAttempts: Int, +) + +enum class ActionType { + REGISTER, FORGET, LOGIN +} \ No newline at end of file diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Token.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Token.kt index 267c699da..f6002d91f 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Token.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/Token.kt @@ -10,7 +10,7 @@ data class PasswordFlowTokenRequest( val otp: String?, val rememberMe: Boolean = true, val captchaType: CaptchaType? = CaptchaType.INTERNAL, - val captchaCode: String, + val captchaCode: String?, ) data class RefreshTokenRequest( diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserRegister.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserRegister.kt index c287598bc..4fcbd50a5 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserRegister.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserRegister.kt @@ -5,7 +5,7 @@ data class RegisterUserRequest( val firstName: String? = null, val lastName: String? = null, val captchaType: CaptchaType? = CaptchaType.INTERNAL, - val captchaCode: String, + val captchaCode: String?, ) data class VerifyOTPRequest( @@ -59,5 +59,5 @@ data class ConfirmForgetRequest( data class ForgotPasswordRequest( val username: String, val captchaType: CaptchaType? = CaptchaType.INTERNAL, - val captchaCode: String, + val captchaCode: String?, ) \ No newline at end of file diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/CaptchaHandler.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/CaptchaHandler.kt new file mode 100644 index 000000000..0025157b3 --- /dev/null +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/CaptchaHandler.kt @@ -0,0 +1,39 @@ +package co.nilin.opex.auth.service + +import co.nilin.opex.auth.data.ActionCache +import co.nilin.opex.auth.data.ActionType +import co.nilin.opex.auth.model.CaptchaType +import co.nilin.opex.auth.proxy.CaptchaProxy +import co.nilin.opex.auth.utils.CacheManager +import co.nilin.opex.common.OpexError +import org.springframework.stereotype.Service +import java.util.concurrent.TimeUnit + +@Service +class CaptchaHandler( + private val cacheManager: CacheManager, + private val captchaProxy: CaptchaProxy +) { + suspend fun validateCaptchaWithActionCache( + username: String, + captchaCode: String?, + captchaType: CaptchaType?, + action: ActionType, + maxAttempts: Int = 3, + expireTimeMinutes: Long = 10 + ) { + val cache = cacheManager.get(username) + + if (cache == null || cache.actionType != action || cache.remainingAttempts <= 0) { + captchaProxy.validateCaptcha(captchaCode ?: throw OpexError.CaptchaRequired.exception(), captchaType ?: CaptchaType.INTERNAL) + cacheManager.put(username, ActionCache(action, maxAttempts), expireTimeMinutes, TimeUnit.MINUTES) + return + } + cacheManager.put( + username, + cache.copy(remainingAttempts = cache.remainingAttempts - 1), + expireTimeMinutes, + TimeUnit.MINUTES + ) + } +} diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/TokenService.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/TokenService.kt index 4a3fd0064..51d30ff30 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/TokenService.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/TokenService.kt @@ -1,7 +1,7 @@ package co.nilin.opex.auth.service +import co.nilin.opex.auth.data.ActionType import co.nilin.opex.auth.model.* -import co.nilin.opex.auth.proxy.CaptchaProxy import co.nilin.opex.auth.proxy.GoogleProxy import co.nilin.opex.auth.proxy.KeycloakProxy import co.nilin.opex.auth.proxy.OTPProxy @@ -13,11 +13,16 @@ class TokenService( private val otpProxy: OTPProxy, private val keycloakProxy: KeycloakProxy, private val googleProxy: GoogleProxy, - private val captchaProxy: CaptchaProxy, + private val captchaHandler: CaptchaHandler, ) { suspend fun getToken(request: PasswordFlowTokenRequest): TokenResponse { - captchaProxy.validateCaptcha(request.captchaCode, request.captchaType ?: CaptchaType.INTERNAL) + captchaHandler.validateCaptchaWithActionCache( + username = request.username, + captchaCode = request.captchaCode, + captchaType = request.captchaType, + action = ActionType.LOGIN + ) val username = Username.create(request.username) val user = keycloakProxy.findUserByUsername(username) ?: throw OpexError.UserNotFound.exception() diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt index 0378999d3..b93a7af87 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt @@ -1,10 +1,10 @@ package co.nilin.opex.auth.service +import co.nilin.opex.auth.data.ActionType import co.nilin.opex.auth.data.ActiveSession import co.nilin.opex.auth.data.UserCreatedEvent import co.nilin.opex.auth.kafka.AuthEventProducer import co.nilin.opex.auth.model.* -import co.nilin.opex.auth.proxy.CaptchaProxy import co.nilin.opex.auth.proxy.GoogleProxy import co.nilin.opex.auth.proxy.KeycloakProxy import co.nilin.opex.auth.proxy.OTPProxy @@ -26,15 +26,20 @@ class UserService( private val googleProxy: GoogleProxy, private val privateKey: PrivateKey, private val publicKey: PublicKey, - private val captchaProxy: CaptchaProxy, - private val authProducer: AuthEventProducer + private val authProducer: AuthEventProducer, + private val captchaHandler: CaptchaHandler ) { private val logger by LoggerDelegate() //TODO IMPORTANT: remove in production suspend fun registerUser(request: RegisterUserRequest): String { - captchaProxy.validateCaptcha(request.captchaCode, request.captchaType ?: CaptchaType.INTERNAL) + captchaHandler.validateCaptchaWithActionCache( + username = request.username, + captchaCode = request.captchaCode, + captchaType = request.captchaType, + action = ActionType.REGISTER + ) val username = Username.create(request.username) val userStatus = isUserDuplicate(username) @@ -106,7 +111,12 @@ class UserService( } suspend fun forgetPassword(request: ForgotPasswordRequest): String { - captchaProxy.validateCaptcha(request.captchaCode, request.captchaType ?: CaptchaType.INTERNAL) + captchaHandler.validateCaptchaWithActionCache( + username = request.username, + captchaCode = request.captchaCode, + captchaType = request.captchaType, + action = ActionType.FORGET + ) val uName = Username.create(request.username) val user = keycloakProxy.findUserByUsername(uName) ?: return null ?: "" //TODO IMPORTANT: remove in production diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/CacheManager.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/CacheManager.kt new file mode 100644 index 000000000..4b90dc798 --- /dev/null +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/utils/CacheManager.kt @@ -0,0 +1,43 @@ +package co.nilin.opex.auth.utils + +import org.springframework.stereotype.Component +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +@Component +class CacheManager { + + private val cacheMap = ConcurrentHashMap>() + + data class CacheEntry( + val value: T, + val timestamp: Long + ) + + fun put(key: K, value: V, expirationTime: Long, timeUnit: TimeUnit) { + val expirationMillis = timeUnit.toMillis(expirationTime) + cacheMap[key] = CacheEntry(value, System.currentTimeMillis() + expirationMillis) + } + + fun get(key: K): V? { + val entry = cacheMap[key] + return if (entry != null && !isExpired(entry)) { + entry.value + } else { + cacheMap.remove(key) + null + } + } + + private fun isExpired(entry: CacheEntry): Boolean { + return System.currentTimeMillis() > entry.timestamp + } + + fun remove(key: K) { + cacheMap.remove(key) + } + + fun clear() { + cacheMap.clear() + } +} diff --git a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt index 9dce084b1..435af0cce 100644 --- a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt +++ b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt @@ -52,6 +52,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus ExpiredOTP(5017, "OTP is expired", HttpStatus.BAD_REQUEST), InvalidToken(5018, "Invalid token", HttpStatus.BAD_REQUEST), InternalIdGenerateFailed(5019, "Internal id generate failed", HttpStatus.INTERNAL_SERVER_ERROR), + CaptchaRequired(5020, "Captcha required", HttpStatus.BAD_REQUEST),