Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import co.nilin.opex.api.app.service.RateLimitCoordinatorService
import co.nilin.opex.api.core.spi.RateLimitConfigService
import co.nilin.opex.common.OpexError
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
Expand Down Expand Up @@ -45,7 +44,6 @@ class RateLimitConfig(
)
}


private fun applyRateLimitIfAuthenticated(
exchange: ServerWebExchange,
chain: WebFilterChain,
Expand All @@ -56,17 +54,13 @@ class RateLimitConfig(
.mapNotNull { it.authentication }
.filter { it.isAuthenticated }
.flatMap { auth ->
if (auth != null && !auth.name.isNullOrBlank())
applyRateLimit(
auth.name, exchange, chain, groupId
)
else
chain.filter(exchange)
applyRateLimit(auth.name, exchange, chain, groupId)
}

.switchIfEmpty(
chain.filter(exchange)
)
}


private fun applyRateLimit(
identity: String,
exchange: ServerWebExchange,
Expand All @@ -87,34 +81,9 @@ class RateLimitConfig(
)

return if (result.blocked) {
tooManyRequests(
exchange,
identity,
exchange.request.uri.path,
exchange.request.method.name(),
result.retryAfterSeconds
)
throw OpexError.RateLimit.exception()
} else {
chain.filter(exchange)
}
}

//TODO should throw opex error
private fun tooManyRequests(
exchange: ServerWebExchange,
identity: String,
url: String,
method: String,
retryAfterSeconds: Int
): Mono<Void> {
logger.info("Rate limit exceeded ($identity) -- $method:$url")
// exchange.response.statusCode = HttpStatus.TOO_MANY_REQUESTS
throw OpexError.RateLimit.exception()
// return exchange.response.writeWith(
// Mono.just(
// exchange.response.bufferFactory()
// .wrap("Rate limit exceeded ($identity) -- $method:$url -- Retry-After, $retryAfterSeconds".toByteArray())
// )
// )
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package co.nilin.opex.api.app.config
import co.nilin.opex.common.utils.LanguageUtils.getDefaultUserLanguage
import io.netty.channel.ChannelOption
import io.netty.handler.logging.LogLevel
import org.springframework.beans.factory.annotation.Value
import org.springframework.cloud.client.ServiceInstance
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
Expand All @@ -21,7 +22,11 @@ import reactor.netty.transport.logging.AdvancedByteBufFormat
import java.time.Duration

@Configuration
class WebClientConfig(private val logbook: Logbook) {
class WebClientConfig(
private val logbook: Logbook,
@Value("\${app.auth.url}")
private val url: String,
) {
private val provider = ConnectionProvider.builder("apiPool")
.maxConnections(150)
.pendingAcquireMaxCount(100)
Expand Down Expand Up @@ -57,24 +62,25 @@ class WebClientConfig(private val logbook: Logbook) {

@Bean("keycloakWebClient")
fun keycloakWebClient(logbook: Logbook): WebClient {
val provider = ConnectionProvider.builder("apiKeycloakPool")
.maxConnections(150)
val provider = ConnectionProvider.builder("keycloakPool")
.maxConnections(100)
.maxIdleTime(Duration.ofSeconds(30))
.maxLifeTime(Duration.ofMinutes(2))
.pendingAcquireTimeout(Duration.ofSeconds(5))
.pendingAcquireTimeout(Duration.ofSeconds(60))
.evictInBackground(Duration.ofMinutes(1))
.build()

val client = HttpClient.create(provider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(5))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.responseTimeout(Duration.ofSeconds(10))
.keepAlive(true)
.doOnConnected { it.addHandlerLast(LogbookClientHandler(logbook)) }

client.warmup().block()

return WebClient.builder()
.clientConnector(ReactorClientHttpConnector(client))
.baseUrl(url)
.build()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package co.nilin.opex.api.app.impl

import co.nilin.opex.api.app.service.RateLimitCoordinatorService
import co.nilin.opex.api.core.inout.ManualRateLimitGroupType
import co.nilin.opex.api.core.spi.ManualRateLimiterService
import co.nilin.opex.api.core.spi.RateLimitConfigService
import co.nilin.opex.common.OpexError
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange

@Component
class ManualRateLimiterImpl(
private val rateLimitConfig: RateLimitConfigService,
private val coordinator: RateLimitCoordinatorService
) : ManualRateLimiterService {

override fun check(
identity: String,
group: ManualRateLimitGroupType,
exchange: ServerWebExchange
) {
val group = rateLimitConfig.getGroup(group.name) ?: return
val result = coordinator.check(
identity = identity,
groupId = group.id!!,
maxRequests = group.requestCount,
windowSeconds = group.requestWindowSeconds,
apiPath = exchange.request.uri.path,
apiMethod = exchange.request.method.name()
)
if (result.blocked) {
throw OpexError.RateLimit.exception()
}
}
}
5 changes: 4 additions & 1 deletion api/api-app/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,11 @@ app:
storage:
url: http://storage
config:
url: http://opex-config
url: http://opex-config
auth-gateway:
url: http://opex-auth-gateway
auth:
url: http://keycloak:8080
cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
iss-url: ${TOKEN_ISSUER_URL:http://keycloak:8080/realms/opex}
token-url: http://keycloak:8080/realms/opex/protocol/openid-connect/token
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package co.nilin.opex.api.core.inout

enum class ManualRateLimitGroupType {
VERIFY_OTP,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package co.nilin.opex.api.core.inout.auth

data class Attribute(
val key: String,
val value: String
)

object Attributes {

const val EMAIL = "email"
const val MOBILE = "mobile"
const val OTP = "otpConfig"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package co.nilin.opex.api.core.inout.auth

enum class CaptchaType {
INTERNAL, ARCAPTCHA, HCAPTCHA
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package co.nilin.opex.api.core.inout.auth

open class Device {
var deviceUuid: String?=null
var appVersion: String?=null
var osVersion: String?=null
var os: Os?=null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package co.nilin.opex.api.core.inout.auth

import co.nilin.opex.api.core.inout.OTPType

data class OTPReceiver(
val receiver: String,
val type: OTPType,
)

data class OTPCode(
val code: String,
val otpType: OTPType,
)

data class OTPVerifyRequest(
val userId: String,
val otpCodes: List<OTPCode>
)

data class OTPVerifyResponse(
val result: Boolean,
val type: OTPResultType
)

data class TempOtpResponse(val otp: String?, val otpReceiver: OTPReceiver?)

enum class OTPAction {
REGISTER, FORGET, NONE
}

enum class OTPResultType {
VALID, EXPIRED, INCORRECT, INVALID
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package co.nilin.opex.api.core.inout.auth

import java.time.LocalDateTime

data class Sessions(
val deviceUuid: String?,
val os: Os?,
val osVersion: String?,
val appVersion: String?,
val firstLoginDate: LocalDateTime?,
val lastLoginDate: LocalDateTime?,
val sessionState: String?,
val sessionStatus: SessionStatus?,
val sessionCreateDate: LocalDateTime?,
val sessionExpireDate: LocalDateTime?,
var isCurrentSession: Boolean?=false
)

enum class SessionStatus {
ACTIVE,
EXPIRED,
TERMINATED
}

enum class Os {
ANDROID, IOS, MOBILE_WEB, DESKTOP_WEB
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package co.nilin.opex.api.core.inout.auth

data class SessionRequest(
var uuid: String? = null,
val limit: Int = 10,
val offset: Int = 0,
val ascendingByTime: Boolean = false,
val os: Os? = null,
val status: SessionStatus? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package co.nilin.opex.api.core.inout.auth

import co.nilin.opex.api.core.inout.OTPType
import co.nilin.opex.api.core.inout.auth.CaptchaType
import com.fasterxml.jackson.annotation.JsonProperty

data class PasswordFlowTokenRequest(
val username: String,
val password: String,
val clientId: String,
val clientSecret: String?,
val rememberMe: Boolean = true,
val captchaType: CaptchaType? = CaptchaType.INTERNAL,
val captchaCode: String,
):Device()

data class ConfirmPasswordFlowTokenRequest(
val username: String,
val token: String,
val clientId: String,
val clientSecret: String?,
val otp: String,
val rememberMe: Boolean = true,
): Device()

data class ResendOtpRequest(
val username: String,
val clientId: String
)


data class RefreshTokenRequest(
val clientId: String,
val clientSecret: String?,
val refreshToken: String
):Device()

data class ExternalIdpTokenRequest(
val idToken: String,
val accessToken: String,
val idp: String,
val otpVerifyRequest: OTPVerifyRequest?
):Device()

data class Token(
@JsonProperty("access_token")
val accessToken: String, // The access token

@JsonProperty("expires_in")
val expiresIn: Int, // Expiration time of the access token in seconds

@JsonProperty("refresh_expires_in")
var refreshExpiresIn: Int?, // Expiration time of the refresh token in seconds

@JsonProperty("refresh_token")
var refreshToken: String?, // The refresh token

@JsonProperty("token_type")
val tokenType: String?, // Type of token (usually "Bearer")

@JsonProperty("not-before-policy")
val notBeforePolicy: Int?, // Timestamp indicating when the token becomes valid

@JsonProperty("session_state")
val sessionState: String?, // Session state (optional)

@JsonProperty("scope")
val scope: String? // Scopes associated with the token

)

data class TokenResponse(
val token: Token?,
val otp: RequiredOTP?,
//TODO IMPORTANT: remove in production
val otpCode: String?,
)

data class RequiredOTP(
val type: OTPType,
val receiver: String?
)

data class ResendOtpResponse(
val otp: RequiredOTP?,
//TODO IMPORTANT: remove in production
val otpCode: String?,
)
Loading
Loading