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
4 changes: 4 additions & 0 deletions api/api-app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>co.nilin.opex.utility.log</groupId>
<artifactId>logging-handler</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package co.nilin.opex.api.app

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.ComponentScan
import springfox.documentation.swagger2.annotations.EnableSwagger2

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

import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@EnableCaching
class CacheConfig {

@Bean
fun apiKeyCacheManager(): CacheManager {
return ConcurrentMapCacheManager("apiKey")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package co.nilin.opex.api.app.controller

import co.nilin.opex.api.app.data.APIKeyResponse
import co.nilin.opex.api.app.data.CreateAPIKeyRequest
import co.nilin.opex.api.app.service.APIKeyServiceImpl
import co.nilin.opex.api.ports.binance.util.jwtAuthentication
import co.nilin.opex.api.ports.binance.util.tokenValue
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.security.Principal

@RestController
@RequestMapping("/v1/api-key")
class APIKeyController(private val apiKeyService: APIKeyServiceImpl) {

@GetMapping
suspend fun getKeys(principal: Principal): List<APIKeyResponse> {
return apiKeyService.getKeysByUserId(principal.name)
.map { APIKeyResponse(it.label, it.expirationTime, it.allowedIPs, it.key, it.isEnabled) }
}

@PostMapping
suspend fun create(
@RequestBody request: CreateAPIKeyRequest,
@CurrentSecurityContext securityContext: SecurityContext
): Any {
val jwt = securityContext.jwtAuthentication()
val response = apiKeyService.createAPIKey(
jwt.name,
request.label,
request.expiration?.getLocalDateTime(),
request.allowedIPs,
jwt.tokenValue()
)
return object {
val apiKey = response.second.key
val secret = response.first
}
}

@PutMapping("/{key}/enable")
suspend fun enableKey(principal: Principal, @PathVariable key: String) {
apiKeyService.changeKeyState(principal.name, key, true)
}

@PutMapping("/{key}/disable")
suspend fun disableKey(principal: Principal, @PathVariable key: String) {
apiKeyService.changeKeyState(principal.name, key, false)
}

@DeleteMapping("/{key}")
suspend fun deleteKey(principal: Principal, @PathVariable key: String) {
apiKeyService.deleteKey(principal.name, key)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package co.nilin.opex.api.app.data

import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
import java.util.concurrent.TimeUnit

enum class APIKeyExpiration(private val unit: TimeUnit, private val duration: Long) {

ONE_MONTH(TimeUnit.DAYS, 30),
THREE_MONTHS(TimeUnit.DAYS, 90),
SIX_MONTHS(TimeUnit.DAYS, 180),
ONE_YEAR(TimeUnit.DAYS, 365);

private fun getDate() = Date(Date().time + unit.toMillis(duration))

fun getLocalDateTime(): LocalDateTime = with(Instant.ofEpochMilli(getDate().time)) {
LocalDateTime.ofInstant(this, ZoneId.systemDefault())
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package co.nilin.opex.api.app.data

import java.time.LocalDateTime
import java.util.*

data class APIKeyResponse(
val label: String,
val expirationTime: LocalDateTime?,
val allowedIPs: String?,
val key: String,
val enabled: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package co.nilin.opex.api.app.data

data class AccessTokenResponse(
val access_token: String,
val refresh_token: String,
val expires_in: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package co.nilin.opex.api.app.data

data class CreateAPIKeyRequest(
val label: String,
val expiration: APIKeyExpiration?,
val allowedIPs: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package co.nilin.opex.api.app.interceptor

import co.nilin.opex.api.app.service.APIKeyServiceImpl
import co.nilin.opex.api.core.spi.APIKeyFilter
import kotlinx.coroutines.runBlocking
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono

@Component
class APIKeyFilterImpl(private val apiKeyService: APIKeyServiceImpl) : APIKeyFilter, WebFilter {

override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val request = exchange.request
val key = request.headers["X-API-KEY"]
if (!key.isNullOrEmpty()) {
val secret = request.headers["X-API-SECRET"]
if (secret.isNullOrEmpty())
return chain.filter(exchange)

val apiKey = runBlocking { apiKeyService.getAPIKey(key[0], secret[0]) }
if (apiKey != null && apiKey.isEnabled && apiKey.accessToken != null && !apiKey.isExpired) {
val req = exchange.request.mutate()
.header("Authorization", "Bearer ${apiKey.accessToken}")
.build()
return chain.filter(exchange.mutate().request(req).build())
}
}
return chain.filter(exchange)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package co.nilin.opex.api.app.proxy

import co.nilin.opex.api.app.data.AccessTokenResponse
import kotlinx.coroutines.reactor.awaitSingle
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToMono

@Component
class AuthProxy(
private val client: WebClient,
@Value("\${app.auth.token-url}")
private val tokenUrl: String
) {

private val logger = LoggerFactory.getLogger(AuthProxy::class.java)

suspend fun exchangeToken(clientSecret: String, token: String): AccessTokenResponse {
val body = BodyInserters.fromFormData("client_id", "opex-api-key")
.with("client_secret", clientSecret)
.with("subject_token", token)
.with("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
.with("scope", "offline_access")

logger.info("Request token exchange for user")
return client.post()
.uri(tokenUrl)
.accept(MediaType.APPLICATION_JSON)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
.bodyToMono<AccessTokenResponse>()
.awaitSingle()
}

suspend fun refreshToken(clientSecret: String, refreshToken: String): AccessTokenResponse {
val body = BodyInserters.fromFormData("client_id", "opex-api-key")
.with("client_secret", clientSecret)
.with("refresh_token", refreshToken)
.with("grant_type", "refresh_token")

logger.info("Refreshing token")
return client.post()
.uri(tokenUrl)
.accept(MediaType.APPLICATION_JSON)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
.bodyToMono<AccessTokenResponse>()
.awaitSingle()
}
}
Loading