Add client auth (#294)
This commit is contained in:
@@ -111,6 +111,7 @@ dependencies {
|
|||||||
testImplementation libs.groovy
|
testImplementation libs.groovy
|
||||||
testImplementation libs.bundles.testcontainers
|
testImplementation libs.bundles.testcontainers
|
||||||
testImplementation libs.bundles.junit
|
testImplementation libs.bundles.junit
|
||||||
|
testImplementation libs.mockito.inline
|
||||||
testImplementation libs.mockito.kotlin
|
testImplementation libs.mockito.kotlin
|
||||||
|
|
||||||
testImplementation(libs.spring.boot.starter.test) {
|
testImplementation(libs.spring.boot.starter.test) {
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ brave-instrumentation-grpc = "io.zipkin.brave:brave-instrumentation-grpc:5.15.0"
|
|||||||
|
|
||||||
auth0-jwt = "com.auth0:java-jwt:4.4.0"
|
auth0-jwt = "com.auth0:java-jwt:4.4.0"
|
||||||
|
|
||||||
|
mockito-inline = "org.mockito:mockito-inline:4.0.0"
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
apache-commons = ["commons-io", "apache-commons-lang3", "apache-commons-collections4"]
|
apache-commons = ["commons-io", "apache-commons-lang3", "apache-commons-collections4"]
|
||||||
etherjar = ["etherjar-domain", "etherjar-hex", "etherjar-rpc-api", "etherjar-rpc-http", "etherjar-rpc-ws", "etherjar-tx", "etherjar-contract", "etherjar-erc20"]
|
etherjar = ["etherjar-domain", "etherjar-hex", "etherjar-rpc-api", "etherjar-rpc-http", "etherjar-rpc-ws", "etherjar-tx", "etherjar-contract", "etherjar-erc20"]
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ open class GrpcServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverBuilder.intercept(grpcServerBraveInterceptor)
|
serverBuilder.intercept(grpcServerBraveInterceptor)
|
||||||
if (mainConfig.authorization.enabled) {
|
if (mainConfig.authorization.enabled && mainConfig.authorization.hasServerConfig()) {
|
||||||
serverBuilder.intercept(authInterceptor)
|
serverBuilder.intercept(authInterceptor)
|
||||||
log.info("Token authorization is turned on")
|
log.info("Token authorization is turned on")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class AuthService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val keys = rsaKeyReader.getKeyPair(
|
val keys = rsaKeyReader.getKeyPair(
|
||||||
authorizationConfig.providerPrivateKeyPath, authorizationConfig.externalPublicKeyPath
|
authorizationConfig.serverConfig.providerPrivateKeyPath,
|
||||||
|
authorizationConfig.serverConfig.externalPublicKeyPath
|
||||||
)
|
)
|
||||||
val decodedJwt = JWT.decode(token)
|
val decodedJwt = JWT.decode(token)
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ class AuthConfig {
|
|||||||
var key: String? = null
|
var key: String? = null
|
||||||
) : ClientAuth()
|
) : ClientAuth()
|
||||||
|
|
||||||
|
class ClientTokenAuth(
|
||||||
|
var publicKeyPath: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example config:
|
* Example config:
|
||||||
* ```
|
* ```
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ class AuthConfigReader : YamlConfigReader<AuthConfig>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun readTokenAuth(node: MappingNode?): AuthConfig.ClientTokenAuth? {
|
||||||
|
return getMapping(node, "token-auth")?.let {
|
||||||
|
val auth = AuthConfig.ClientTokenAuth()
|
||||||
|
auth.publicKeyPath = getValueAsString(it, "public-key")
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example config:
|
* Example config:
|
||||||
* ```
|
* ```
|
||||||
|
|||||||
@@ -3,12 +3,37 @@ package io.emeraldpay.dshackle.config
|
|||||||
data class AuthorizationConfig(
|
data class AuthorizationConfig(
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
val publicKeyOwner: String,
|
val publicKeyOwner: String,
|
||||||
val providerPrivateKeyPath: String,
|
val serverConfig: ServerConfig,
|
||||||
val externalPublicKeyPath: String
|
val clientConfig: ClientConfig
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun hasServerConfig() = serverConfig != ServerConfig.default()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun default() = AuthorizationConfig(false, "", "", "")
|
fun default() = AuthorizationConfig(
|
||||||
|
false, "",
|
||||||
|
ServerConfig.default(),
|
||||||
|
ClientConfig.default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ServerConfig(
|
||||||
|
val providerPrivateKeyPath: String,
|
||||||
|
val externalPublicKeyPath: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun default() = ServerConfig("", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ClientConfig(
|
||||||
|
val privateKeyPath: String,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun default() = ClientConfig("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,20 +27,38 @@ class AuthorizationConfigReader : YamlConfigReader<AuthorizationConfig>() {
|
|||||||
val publicKeyOwner = getValueAsString(auth, "publicKeyOwner")
|
val publicKeyOwner = getValueAsString(auth, "publicKeyOwner")
|
||||||
?: throw IllegalStateException("Public key owner in not specified")
|
?: throw IllegalStateException("Public key owner in not specified")
|
||||||
|
|
||||||
val keyPair = getMapping(auth, "keys") ?: throw IllegalStateException("Auth keys is not specified")
|
val authServer = getMapping(auth, "server")
|
||||||
val privateKey = getValueAsString(keyPair, "provider-private-key")
|
?.run {
|
||||||
?: throw IllegalStateException("Private key in not specified")
|
val keyPair = getMapping(this, "keys")
|
||||||
val publicKey = getValueAsString(keyPair, "external-public-key")
|
?: throw IllegalStateException("Auth keys is not specified")
|
||||||
?: throw IllegalStateException("External key in not specified")
|
val privateKey = getValueAsString(keyPair, "provider-private-key")
|
||||||
|
?: throw IllegalStateException("Private key in not specified")
|
||||||
|
val publicKey = getValueAsString(keyPair, "external-public-key")
|
||||||
|
?: throw IllegalStateException("External key in not specified")
|
||||||
|
|
||||||
if (fileNotExists(privateKey)) {
|
if (fileNotExists(privateKey)) {
|
||||||
throw IllegalStateException("There is no such file: $privateKey")
|
throw IllegalStateException("There is no such file: $privateKey")
|
||||||
}
|
}
|
||||||
if (fileNotExists(publicKey)) {
|
if (fileNotExists(publicKey)) {
|
||||||
throw IllegalStateException("There is no such file: $publicKey")
|
throw IllegalStateException("There is no such file: $publicKey")
|
||||||
|
}
|
||||||
|
AuthorizationConfig.ServerConfig(privateKey, publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
val authClient = getMapping(auth, "client")
|
||||||
|
?.run {
|
||||||
|
AuthorizationConfig.ClientConfig(getValueAsString(this, "private-key")!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authClient == null && authServer == null) {
|
||||||
|
throw IllegalStateException("Token auth server settings are not specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
return AuthorizationConfig(enabled, publicKeyOwner, privateKey, publicKey)
|
return AuthorizationConfig(
|
||||||
|
enabled, publicKeyOwner,
|
||||||
|
authServer ?: AuthorizationConfig.ServerConfig.default(),
|
||||||
|
authClient ?: AuthorizationConfig.ClientConfig.default()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileNotExists(path: String): Boolean {
|
private fun fileNotExists(path: String): Boolean {
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ open class UpstreamsConfig {
|
|||||||
var host: String? = null
|
var host: String? = null
|
||||||
var port: Int = 0
|
var port: Int = 0
|
||||||
var auth: AuthConfig.ClientTlsAuth? = null
|
var auth: AuthConfig.ClientTlsAuth? = null
|
||||||
|
var tokenAuth: AuthConfig.ClientTokenAuth? = null
|
||||||
var upstreamRating: Int = 0
|
var upstreamRating: Int = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class UpstreamsConfigReader(
|
|||||||
connection.port = it
|
connection.port = it
|
||||||
}
|
}
|
||||||
connection.auth = authConfigReader.readClientTls(connConfigNode)
|
connection.auth = authConfigReader.readClientTls(connConfigNode)
|
||||||
|
connection.tokenAuth = authConfigReader.readTokenAuth(connConfigNode)
|
||||||
} else {
|
} else {
|
||||||
log.error("Upstream at #0 has invalid configuration")
|
log.error("Upstream at #0 has invalid configuration")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import io.emeraldpay.dshackle.Chain
|
|||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
import io.emeraldpay.dshackle.Global
|
import io.emeraldpay.dshackle.Global
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
import io.emeraldpay.dshackle.config.ChainsConfig
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
import io.emeraldpay.dshackle.config.CompressionConfig
|
import io.emeraldpay.dshackle.config.CompressionConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
@@ -51,6 +52,7 @@ import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
|||||||
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.auth.GrpcAuthContext
|
||||||
import io.grpc.ClientInterceptor
|
import io.grpc.ClientInterceptor
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
@@ -84,7 +86,9 @@ open class ConfiguredUpstreams(
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private val clientSpansInterceptor: ClientInterceptor?,
|
private val clientSpansInterceptor: ClientInterceptor?,
|
||||||
@Qualifier("headScheduler")
|
@Qualifier("headScheduler")
|
||||||
private val headScheduler: Scheduler
|
private val headScheduler: Scheduler,
|
||||||
|
private val authorizationConfig: AuthorizationConfig,
|
||||||
|
private val grpcAuthContext: GrpcAuthContext
|
||||||
) : ApplicationRunner {
|
) : ApplicationRunner {
|
||||||
@Value("\${spring.application.max-metadata-size}")
|
@Value("\${spring.application.max-metadata-size}")
|
||||||
private var maxMetadataSize: Int = Defaults.maxMetadataSize
|
private var maxMetadataSize: Int = Defaults.maxMetadataSize
|
||||||
@@ -351,6 +355,8 @@ open class ConfiguredUpstreams(
|
|||||||
endpoint.host!!,
|
endpoint.host!!,
|
||||||
endpoint.port,
|
endpoint.port,
|
||||||
endpoint.auth,
|
endpoint.auth,
|
||||||
|
endpoint.tokenAuth,
|
||||||
|
authorizationConfig,
|
||||||
compression,
|
compression,
|
||||||
fileResolver,
|
fileResolver,
|
||||||
endpoint.upstreamRating,
|
endpoint.upstreamRating,
|
||||||
@@ -361,7 +367,8 @@ open class ConfiguredUpstreams(
|
|||||||
grpcTracing,
|
grpcTracing,
|
||||||
clientSpansInterceptor,
|
clientSpansInterceptor,
|
||||||
maxMetadataSize,
|
maxMetadataSize,
|
||||||
headScheduler
|
headScheduler,
|
||||||
|
grpcAuthContext
|
||||||
).apply {
|
).apply {
|
||||||
timeout = options.timeout
|
timeout = options.timeout
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,22 +22,30 @@ import io.emeraldpay.api.proto.BlockchainOuterClass.DescribeResponse
|
|||||||
import io.emeraldpay.api.proto.BlockchainOuterClass.StatusRequest
|
import io.emeraldpay.api.proto.BlockchainOuterClass.StatusRequest
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.api.proto.Common.ChainRef.UNRECOGNIZED
|
import io.emeraldpay.api.proto.Common.ChainRef.UNRECOGNIZED
|
||||||
|
import io.emeraldpay.api.proto.ReactorAuthGrpc
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.Chain
|
import io.emeraldpay.dshackle.Chain
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
import io.emeraldpay.dshackle.config.AuthConfig
|
import io.emeraldpay.dshackle.config.AuthConfig
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
import io.emeraldpay.dshackle.config.ChainsConfig
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
|
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
|
||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.Lifecycle
|
import io.emeraldpay.dshackle.upstream.Lifecycle
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.auth.AuthException
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.auth.ClientAuthenticationInterceptor
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.auth.GrpcAuthContext
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.auth.GrpcUpstreamsAuth
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.RpcMetrics
|
import io.emeraldpay.dshackle.upstream.rpcclient.RpcMetrics
|
||||||
import io.grpc.ClientInterceptor
|
import io.grpc.ClientInterceptor
|
||||||
import io.grpc.Codec
|
import io.grpc.Codec
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
import io.grpc.netty.NettyChannelBuilder
|
import io.grpc.netty.NettyChannelBuilder
|
||||||
import io.micrometer.core.instrument.Counter
|
import io.micrometer.core.instrument.Counter
|
||||||
import io.micrometer.core.instrument.Metrics
|
import io.micrometer.core.instrument.Metrics
|
||||||
@@ -52,6 +60,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -66,6 +75,8 @@ class GrpcUpstreams(
|
|||||||
private val host: String,
|
private val host: String,
|
||||||
private val port: Int,
|
private val port: Int,
|
||||||
private val auth: AuthConfig.ClientTlsAuth? = null,
|
private val auth: AuthConfig.ClientTlsAuth? = null,
|
||||||
|
private val tokenAuth: AuthConfig.ClientTokenAuth? = null,
|
||||||
|
private val authorizationConfig: AuthorizationConfig,
|
||||||
private val compression: Boolean,
|
private val compression: Boolean,
|
||||||
private val fileResolver: FileResolver,
|
private val fileResolver: FileResolver,
|
||||||
private val nodeRating: Int,
|
private val nodeRating: Int,
|
||||||
@@ -76,7 +87,8 @@ class GrpcUpstreams(
|
|||||||
private val grpcTracing: GrpcTracing,
|
private val grpcTracing: GrpcTracing,
|
||||||
private val clientSpansInterceptor: ClientInterceptor?,
|
private val clientSpansInterceptor: ClientInterceptor?,
|
||||||
private var maxMetadataSize: Int,
|
private var maxMetadataSize: Int,
|
||||||
private val headScheduler: Scheduler
|
private val headScheduler: Scheduler,
|
||||||
|
private val grpcAuthContext: GrpcAuthContext
|
||||||
) {
|
) {
|
||||||
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
|
||||||
|
|
||||||
@@ -92,7 +104,10 @@ class GrpcUpstreams(
|
|||||||
.maxInboundMessageSize(Defaults.maxMessageSize)
|
.maxInboundMessageSize(Defaults.maxMessageSize)
|
||||||
.maxInboundMetadataSize(maxMetadataSize)
|
.maxInboundMetadataSize(maxMetadataSize)
|
||||||
.enableRetry()
|
.enableRetry()
|
||||||
.intercept(grpcTracing.newClientInterceptor())
|
.intercept(
|
||||||
|
grpcTracing.newClientInterceptor(),
|
||||||
|
ClientAuthenticationInterceptor(id, grpcAuthContext),
|
||||||
|
)
|
||||||
.executor(grpcExecutor)
|
.executor(grpcExecutor)
|
||||||
.maxRetryAttempts(3)
|
.maxRetryAttempts(3)
|
||||||
clientSpansInterceptor?.let {
|
clientSpansInterceptor?.let {
|
||||||
@@ -108,17 +123,28 @@ class GrpcUpstreams(
|
|||||||
chanelBuilder.usePlaintext()
|
chanelBuilder.usePlaintext()
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = ReactorBlockchainGrpc.newReactorStub(chanelBuilder.build())
|
val channel = chanelBuilder.build()
|
||||||
|
var client = ReactorBlockchainGrpc.newReactorStub(channel)
|
||||||
if (compression) {
|
if (compression) {
|
||||||
client = client.withCompression(Codec.Gzip().messageEncoding)
|
client = client.withCompression(Codec.Gzip().messageEncoding)
|
||||||
}
|
}
|
||||||
this.client = client
|
this.client = client
|
||||||
|
|
||||||
|
val grpcUpstreamsAuth =
|
||||||
|
if (tokenAuth != null && authorizationConfig.enabled) {
|
||||||
|
GrpcUpstreamsAuth(
|
||||||
|
ReactorAuthGrpc.newReactorStub(channel),
|
||||||
|
authorizationConfig,
|
||||||
|
grpcAuthContext,
|
||||||
|
tokenAuth.publicKeyPath!!
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
|
||||||
val statusSubscriptions = mutableMapOf<Chain, Disposable>()
|
val statusSubscriptions = mutableMapOf<Chain, Disposable>()
|
||||||
|
|
||||||
return Flux.interval(Duration.ZERO, Duration.ofSeconds(20))
|
return Flux.interval(Duration.ZERO, Duration.ofSeconds(20))
|
||||||
.flatMap {
|
.flatMap {
|
||||||
client.describe(DescribeRequest.newBuilder().build())
|
authAndDescribe(grpcUpstreamsAuth)
|
||||||
}.onErrorContinue { t, _ ->
|
}.onErrorContinue { t, _ ->
|
||||||
if (ExceptionUtils.indexOfType(t, IOException::class.java) >= 0) {
|
if (ExceptionUtils.indexOfType(t, IOException::class.java) >= 0) {
|
||||||
log.warn("gRPC upstream $host:$port is unavailable. (${t.javaClass}: ${t.message})")
|
log.warn("gRPC upstream $host:$port is unavailable. (${t.javaClass}: ${t.message})")
|
||||||
@@ -133,7 +159,7 @@ class GrpcUpstreams(
|
|||||||
}.doOnNext {
|
}.doOnNext {
|
||||||
val sub = statusSubscriptions[it.chain]
|
val sub = statusSubscriptions[it.chain]
|
||||||
if (sub == null || sub.isDisposed) {
|
if (sub == null || sub.isDisposed) {
|
||||||
val subscription = client.subscribeStatus(
|
val subscription = this.client.subscribeStatus(
|
||||||
StatusRequest.newBuilder()
|
StatusRequest.newBuilder()
|
||||||
.addChains(Common.ChainRef.forNumber(it.chain.id)).build()
|
.addChains(Common.ChainRef.forNumber(it.chain.id)).build()
|
||||||
).subscribeOn(chainStatusScheduler)
|
).subscribeOn(chainStatusScheduler)
|
||||||
@@ -293,4 +319,43 @@ class GrpcUpstreams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun authAndDescribe(grpcUpstreamsAuth: GrpcUpstreamsAuth?): Mono<DescribeResponse> {
|
||||||
|
return Mono.justOrEmpty(grpcUpstreamsAuth)
|
||||||
|
.flatMap {
|
||||||
|
if (grpcAuthContext.containsToken(id)) {
|
||||||
|
Mono.empty()
|
||||||
|
} else {
|
||||||
|
auth(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
describe()
|
||||||
|
.onErrorResume {
|
||||||
|
if (it is StatusRuntimeException && it.status.code == Status.Code.UNAUTHENTICATED) {
|
||||||
|
grpcAuthContext.removeToken(id)
|
||||||
|
auth(grpcUpstreamsAuth).then(describe())
|
||||||
|
} else {
|
||||||
|
Mono.error(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun auth(grpcUpstreamsAuth: GrpcUpstreamsAuth?): Mono<Void> {
|
||||||
|
if (grpcUpstreamsAuth == null) {
|
||||||
|
return Mono.empty()
|
||||||
|
}
|
||||||
|
return grpcUpstreamsAuth.auth(id)
|
||||||
|
.flatMap { authRes ->
|
||||||
|
if (!authRes.passed) {
|
||||||
|
log.warn(authRes.cause)
|
||||||
|
Mono.error<AuthException>(AuthException(authRes.cause!!))
|
||||||
|
} else {
|
||||||
|
Mono.empty()
|
||||||
|
}
|
||||||
|
}.then()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun describe() = this.client.describe(DescribeRequest.newBuilder().build())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.grpc.auth
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.SESSION_ID
|
||||||
|
import io.grpc.CallOptions
|
||||||
|
import io.grpc.Channel
|
||||||
|
import io.grpc.ClientCall
|
||||||
|
import io.grpc.ClientInterceptor
|
||||||
|
import io.grpc.ForwardingClientCall
|
||||||
|
import io.grpc.Metadata
|
||||||
|
import io.grpc.MethodDescriptor
|
||||||
|
|
||||||
|
class ClientAuthenticationInterceptor(
|
||||||
|
private val upstreamId: String,
|
||||||
|
private val grpcAuthContext: GrpcAuthContext
|
||||||
|
) : ClientInterceptor {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val AUTHORIZATION_HEADER: Metadata.Key<String> =
|
||||||
|
Metadata.Key.of(SESSION_ID, Metadata.ASCII_STRING_MARSHALLER)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <ReqT, RespT> interceptCall(
|
||||||
|
method: MethodDescriptor<ReqT, RespT>,
|
||||||
|
callOptions: CallOptions,
|
||||||
|
next: Channel
|
||||||
|
): ClientCall<ReqT, RespT> =
|
||||||
|
object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
|
||||||
|
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
|
||||||
|
grpcAuthContext.getToken(upstreamId)?.let {
|
||||||
|
headers.put(AUTHORIZATION_HEADER, it)
|
||||||
|
}
|
||||||
|
super.start(responseListener, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.grpc.auth
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class GrpcAuthContext {
|
||||||
|
private val sessions = ConcurrentHashMap<String, String>()
|
||||||
|
|
||||||
|
fun putTokenInContext(upstreamId: String, sessionId: String) {
|
||||||
|
sessions[upstreamId] = sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeToken(upstreamId: String) {
|
||||||
|
sessions.remove(upstreamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsToken(upstreamId: String) = sessions.containsKey(upstreamId)
|
||||||
|
|
||||||
|
fun getToken(upstreamId: String) = sessions[upstreamId]
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.grpc.auth
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.JWTVerifier
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import io.emeraldpay.api.proto.AuthOuterClass
|
||||||
|
import io.emeraldpay.api.proto.ReactorAuthGrpc.ReactorAuthStub
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.AuthVersion
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.SESSION_ID
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.VERSION
|
||||||
|
import io.emeraldpay.dshackle.auth.service.RsaKeyReader
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
|
import java.security.interfaces.RSAPublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class AuthException(message: String) : RuntimeException(message)
|
||||||
|
|
||||||
|
class GrpcUpstreamsAuth(
|
||||||
|
private val authClient: ReactorAuthStub,
|
||||||
|
private val authorizationConfig: AuthorizationConfig,
|
||||||
|
private val grpcAuthContext: GrpcAuthContext,
|
||||||
|
publicKeyPath: String
|
||||||
|
) {
|
||||||
|
private val rsaKeyReader = RsaKeyReader()
|
||||||
|
private val keys = rsaKeyReader.getKeyPair(authorizationConfig.clientConfig.privateKeyPath, publicKeyPath)
|
||||||
|
|
||||||
|
fun auth(providerId: String): Mono<AuthResult> {
|
||||||
|
return authClient.authenticate(
|
||||||
|
AuthOuterClass.AuthRequest.newBuilder()
|
||||||
|
.setToken(generateToken())
|
||||||
|
.build()
|
||||||
|
).map {
|
||||||
|
verify(it.providerToken, providerId)
|
||||||
|
}.onErrorResume {
|
||||||
|
Mono.just(AuthResult(false, "Error during auth - ${it.message}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateToken(): String {
|
||||||
|
return JWT.create()
|
||||||
|
.withIssuedAt(Instant.now())
|
||||||
|
.withIssuer(authorizationConfig.publicKeyOwner)
|
||||||
|
.withClaim(VERSION, AuthVersion.V1.toString())
|
||||||
|
.sign(Algorithm.RSA256(keys.providerPrivateKey as RSAPrivateKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verify(token: String, providerId: String): AuthResult {
|
||||||
|
val verifier: JWTVerifier = JWT
|
||||||
|
.require(Algorithm.RSA256(keys.externalPublicKey as RSAPublicKey, null))
|
||||||
|
.withClaim(SESSION_ID) { claim, _ -> !claim.isMissing }
|
||||||
|
.build()
|
||||||
|
val decodedToken = verifier.verify(token)
|
||||||
|
grpcAuthContext.putTokenInContext(providerId, decodedToken.getClaim(SESSION_ID).asString())
|
||||||
|
|
||||||
|
return AuthResult(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AuthResult(
|
||||||
|
val passed: Boolean,
|
||||||
|
val cause: String? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,8 +28,8 @@ class AuthorizationConfigReaderSpec extends Specification {
|
|||||||
def act = AuthorizationConfig.default()
|
def act = AuthorizationConfig.default()
|
||||||
then:
|
then:
|
||||||
!act.enabled
|
!act.enabled
|
||||||
act.externalPublicKeyPath == ""
|
act.serverConfig.externalPublicKeyPath == ""
|
||||||
act.providerPrivateKeyPath == ""
|
act.serverConfig.providerPrivateKeyPath == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
def "exceptions if no settings"() {
|
def "exceptions if no settings"() {
|
||||||
@@ -48,5 +48,15 @@ class AuthorizationConfigReaderSpec extends Specification {
|
|||||||
"configs/auth-without-key-owner.yaml" | "Public key owner in not specified"
|
"configs/auth-without-key-owner.yaml" | "Public key owner in not specified"
|
||||||
"configs/auth-with-wrong-priv-key.yaml" | "There is no such file: classpath:keys/priv-wrong.p8.key"
|
"configs/auth-with-wrong-priv-key.yaml" | "There is no such file: classpath:keys/priv-wrong.p8.key"
|
||||||
"configs/auth-with-wrong-pub-key.yaml" | "There is no such file: classpath:keys/pub-wrong.key"
|
"configs/auth-with-wrong-pub-key.yaml" | "There is no such file: classpath:keys/pub-wrong.key"
|
||||||
|
"configs/auth-without-any-config.yaml" | "Token auth server settings are not specified"
|
||||||
|
}
|
||||||
|
|
||||||
|
def "client settings is correct"() {
|
||||||
|
setup:
|
||||||
|
def yamlIs = this.class.getClassLoader().getResourceAsStream("configs/auth-with-client-settings.yaml")
|
||||||
|
when:
|
||||||
|
def act = reader.read(yamlIs)
|
||||||
|
then:
|
||||||
|
act.clientConfig == new AuthorizationConfig.ClientConfig("classpath:keys/priv-wrong.p8.key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,22 +91,29 @@ class MainConfigReaderSpec extends Specification {
|
|||||||
chains == ["ethereum"]
|
chains == ["ethereum"]
|
||||||
options.minPeers == 3
|
options.minPeers == 3
|
||||||
}
|
}
|
||||||
upstreams.size() == 3
|
upstreams.size() == 4
|
||||||
with(upstreams[0]) {
|
with(upstreams[0]) {
|
||||||
id == "remote"
|
id == "remote"
|
||||||
}
|
}
|
||||||
with(upstreams[1]) {
|
with(upstreams[1]) {
|
||||||
id == "local"
|
id == "remoteTokenAuth"
|
||||||
|
connection instanceof UpstreamsConfig.GrpcConnection
|
||||||
|
with(connection as UpstreamsConfig.GrpcConnection) {
|
||||||
|
it.tokenAuth.publicKeyPath == "/path/to/key.pem"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
with(upstreams[2]) {
|
with(upstreams[2]) {
|
||||||
|
id == "local"
|
||||||
|
}
|
||||||
|
with(upstreams[3]) {
|
||||||
id == "infura"
|
id == "infura"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
act.authorization != null
|
act.authorization != null
|
||||||
with(act.authorization) {
|
with(act.authorization) {
|
||||||
enabled
|
enabled
|
||||||
providerPrivateKeyPath == "classpath:keys/priv.p8.key"
|
serverConfig.providerPrivateKeyPath == "classpath:keys/priv.p8.key"
|
||||||
externalPublicKeyPath == "classpath:keys/public.pem"
|
serverConfig.externalPublicKeyPath == "classpath:keys/public.pem"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import brave.Tracing
|
|||||||
import brave.grpc.GrpcTracing
|
import brave.grpc.GrpcTracing
|
||||||
import io.emeraldpay.dshackle.Chain
|
import io.emeraldpay.dshackle.Chain
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
import io.emeraldpay.dshackle.config.ChainsConfig
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
import io.emeraldpay.dshackle.config.CompressionConfig
|
import io.emeraldpay.dshackle.config.CompressionConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.quorum.NotNullQuorum
|
import io.emeraldpay.dshackle.quorum.NotNullQuorum
|
||||||
import io.emeraldpay.dshackle.upstream.CallTargetsHolder
|
import io.emeraldpay.dshackle.upstream.CallTargetsHolder
|
||||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.grpc.auth.GrpcAuthContext
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import reactor.core.scheduler.Schedulers
|
import reactor.core.scheduler.Schedulers
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
@@ -33,6 +35,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
AuthorizationConfig.default(),
|
||||||
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
def methods = new UpstreamsConfig.Methods(
|
def methods = new UpstreamsConfig.Methods(
|
||||||
[
|
[
|
||||||
@@ -65,6 +69,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
AuthorizationConfig.default(),
|
||||||
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
def methods = new UpstreamsConfig.Methods(
|
def methods = new UpstreamsConfig.Methods(
|
||||||
[
|
[
|
||||||
@@ -96,6 +102,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
AuthorizationConfig.default(),
|
||||||
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
expect:
|
expect:
|
||||||
configurer.getHash(node, src) == expected
|
configurer.getHash(node, src) == expected
|
||||||
@@ -122,6 +130,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
AuthorizationConfig.default(),
|
||||||
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
when:
|
when:
|
||||||
def h1 = configurer.getHash(null, "hohoho")
|
def h1 = configurer.getHash(null, "hohoho")
|
||||||
@@ -153,6 +163,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
AuthorizationConfig.default(),
|
||||||
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
def methodsGroup = new UpstreamsConfig.MethodGroups(
|
def methodsGroup = new UpstreamsConfig.MethodGroups(
|
||||||
["filter"] as Set,
|
["filter"] as Set,
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ import java.security.interfaces.RSAPublicKey
|
|||||||
import java.security.spec.X509EncodedKeySpec
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
|
||||||
class AuthProcessorV1Test {
|
class AuthProcessorV1Test {
|
||||||
private val processor = AuthProcessorV1(AuthorizationConfig(true, "drpc", "", ""))
|
private val processor = AuthProcessorV1(
|
||||||
|
AuthorizationConfig(
|
||||||
|
true, "drpc",
|
||||||
|
AuthorizationConfig.ServerConfig.default(),
|
||||||
|
AuthorizationConfig.ClientConfig.default()
|
||||||
|
)
|
||||||
|
)
|
||||||
private val rsaKeyReader = RsaKeyReader()
|
private val rsaKeyReader = RsaKeyReader()
|
||||||
private val privProviderPath = ResourceUtils.getFile("classpath:keys/priv.p8.key").path
|
private val privProviderPath = ResourceUtils.getFile("classpath:keys/priv.p8.key").path
|
||||||
private val publicDrpcPath = ResourceUtils.getFile("classpath:keys/public-drpc.pem").path
|
private val publicDrpcPath = ResourceUtils.getFile("classpath:keys/public-drpc.pem").path
|
||||||
|
|||||||
@@ -42,7 +42,14 @@ class AuthServiceTest {
|
|||||||
val tokenWrapper = AuthContext.TokenWrapper(
|
val tokenWrapper = AuthContext.TokenWrapper(
|
||||||
"token", Instant.now(), "sessionId"
|
"token", Instant.now(), "sessionId"
|
||||||
)
|
)
|
||||||
val authService = AuthService(AuthorizationConfig(true, "drpc", "privPath", "pubPath"), rsaKeyReader, factory)
|
val authService = AuthService(
|
||||||
|
AuthorizationConfig(
|
||||||
|
true, "drpc",
|
||||||
|
AuthorizationConfig.ServerConfig("privPath", "pubPath"),
|
||||||
|
AuthorizationConfig.ClientConfig.default()
|
||||||
|
),
|
||||||
|
rsaKeyReader, factory
|
||||||
|
)
|
||||||
val pair = KeyReader.Keys(mock(PrivateKey::class.java), mock(PublicKey::class.java))
|
val pair = KeyReader.Keys(mock(PrivateKey::class.java), mock(PublicKey::class.java))
|
||||||
|
|
||||||
`when`(rsaKeyReader.getKeyPair("privPath", "pubPath"))
|
`when`(rsaKeyReader.getKeyPair("privPath", "pubPath"))
|
||||||
@@ -64,7 +71,14 @@ class AuthServiceTest {
|
|||||||
"token", Instant.now(), "sessionIdNext"
|
"token", Instant.now(), "sessionIdNext"
|
||||||
)
|
)
|
||||||
val pair = KeyReader.Keys(mock(PrivateKey::class.java), mock(PublicKey::class.java))
|
val pair = KeyReader.Keys(mock(PrivateKey::class.java), mock(PublicKey::class.java))
|
||||||
val authService = AuthService(AuthorizationConfig(true, "drpc", "privPath", "pubPath"), rsaKeyReader, factory)
|
val authService = AuthService(
|
||||||
|
AuthorizationConfig(
|
||||||
|
true, "drpc",
|
||||||
|
AuthorizationConfig.ServerConfig("privPath", "pubPath"),
|
||||||
|
AuthorizationConfig.ClientConfig.default()
|
||||||
|
),
|
||||||
|
rsaKeyReader, factory
|
||||||
|
)
|
||||||
|
|
||||||
`when`(rsaKeyReader.getKeyPair("privPath", "pubPath")).thenReturn(pair)
|
`when`(rsaKeyReader.getKeyPair("privPath", "pubPath")).thenReturn(pair)
|
||||||
`when`(mockV1Processor.process(pair, token))
|
`when`(mockV1Processor.process(pair, token))
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.grpc.auth
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import io.emeraldpay.api.proto.AuthOuterClass
|
||||||
|
import io.emeraldpay.api.proto.AuthOuterClass.AuthRequest
|
||||||
|
import io.emeraldpay.api.proto.ReactorAuthGrpc.ReactorAuthStub
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.SESSION_ID
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
|
import org.bouncycastle.openssl.PEMParser
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.Mockito.any
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
|
import org.springframework.util.ResourceUtils
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.test.StepVerifier
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class GrpcUpstreamsAuthTest {
|
||||||
|
private val privateKeyPath = ResourceUtils.getFile("classpath:keys/priv-drpc.p8.key").path
|
||||||
|
private val providerPublicKeyPath = ResourceUtils.getFile("classpath:keys/public.pem").path
|
||||||
|
private val grpcAuthContext = GrpcAuthContext()
|
||||||
|
private val authConfig = AuthorizationConfig(
|
||||||
|
true, "drpc",
|
||||||
|
AuthorizationConfig.ServerConfig.default(),
|
||||||
|
AuthorizationConfig.ClientConfig(privateKeyPath)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val providerPrivateKeyPath = ResourceUtils.getFile("classpath:keys/priv.p8.key").path
|
||||||
|
|
||||||
|
private val upstreamId = "providerId"
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun clearSessions() {
|
||||||
|
grpcAuthContext.removeToken(upstreamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `success auth`() {
|
||||||
|
val sessionId = UUID.randomUUID().toString()
|
||||||
|
val authStub = Mockito.mock(ReactorAuthStub::class.java)
|
||||||
|
val grpcAuth = GrpcUpstreamsAuth(authStub, authConfig, grpcAuthContext, providerPublicKeyPath)
|
||||||
|
val token = JWT.create()
|
||||||
|
.withClaim(SESSION_ID, sessionId)
|
||||||
|
.sign(Algorithm.RSA256(generatePrivateKey(providerPrivateKeyPath) as RSAPrivateKey))
|
||||||
|
|
||||||
|
`when`(authStub.authenticate(any(AuthRequest::class.java)))
|
||||||
|
.thenReturn(
|
||||||
|
Mono.just(
|
||||||
|
AuthOuterClass.AuthResponse.newBuilder()
|
||||||
|
.setProviderToken(token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = grpcAuth.auth(upstreamId)
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNext(GrpcUpstreamsAuth.AuthResult(true))
|
||||||
|
.then {
|
||||||
|
assertEquals(sessionId, grpcAuthContext.getToken(upstreamId))
|
||||||
|
}
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `auth is failed`() {
|
||||||
|
val providerId = "providerId"
|
||||||
|
val sessionId = UUID.randomUUID().toString()
|
||||||
|
val authStub = Mockito.mock(ReactorAuthStub::class.java)
|
||||||
|
val grpcAuth = GrpcUpstreamsAuth(authStub, authConfig, grpcAuthContext, providerPublicKeyPath)
|
||||||
|
val token = JWT.create()
|
||||||
|
.withClaim(SESSION_ID, sessionId)
|
||||||
|
.sign(Algorithm.RSA256(generatePrivateKey(privateKeyPath) as RSAPrivateKey))
|
||||||
|
|
||||||
|
`when`(authStub.authenticate(any(AuthRequest::class.java)))
|
||||||
|
.thenReturn(
|
||||||
|
Mono.just(
|
||||||
|
AuthOuterClass.AuthResponse.newBuilder()
|
||||||
|
.setProviderToken(token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = grpcAuth.auth(providerId)
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNext(
|
||||||
|
GrpcUpstreamsAuth.AuthResult(
|
||||||
|
false,
|
||||||
|
"Error during auth - The Token's Signature resulted invalid when verified using the Algorithm: SHA256withRSA"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then {
|
||||||
|
assertEquals(null, grpcAuthContext.getToken(upstreamId))
|
||||||
|
}
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `replace sessionId for the same provider`() {
|
||||||
|
val providerId = "providerId"
|
||||||
|
val sessionId = UUID.randomUUID().toString()
|
||||||
|
val sessionId1 = UUID.randomUUID().toString()
|
||||||
|
val authStub = Mockito.mock(ReactorAuthStub::class.java)
|
||||||
|
val grpcAuth = GrpcUpstreamsAuth(authStub, authConfig, grpcAuthContext, providerPublicKeyPath)
|
||||||
|
val token = JWT.create()
|
||||||
|
.withClaim(SESSION_ID, sessionId)
|
||||||
|
.sign(Algorithm.RSA256(generatePrivateKey(providerPrivateKeyPath) as RSAPrivateKey))
|
||||||
|
val token1 = JWT.create()
|
||||||
|
.withClaim(SESSION_ID, sessionId1)
|
||||||
|
.sign(Algorithm.RSA256(generatePrivateKey(providerPrivateKeyPath) as RSAPrivateKey))
|
||||||
|
|
||||||
|
`when`(authStub.authenticate(any(AuthRequest::class.java)))
|
||||||
|
.thenReturn(
|
||||||
|
Mono.just(
|
||||||
|
AuthOuterClass.AuthResponse.newBuilder()
|
||||||
|
.setProviderToken(token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.thenReturn(
|
||||||
|
Mono.just(
|
||||||
|
AuthOuterClass.AuthResponse.newBuilder()
|
||||||
|
.setProviderToken(token1)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
grpcAuth.auth(providerId).block()
|
||||||
|
val result = grpcAuth.auth(providerId)
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNext(GrpcUpstreamsAuth.AuthResult(true))
|
||||||
|
.then {
|
||||||
|
assertEquals(sessionId1, grpcAuthContext.getToken(upstreamId))
|
||||||
|
}
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePrivateKey(path: String): PrivateKey {
|
||||||
|
val privateKeyReader = StringReader(Files.readString(Paths.get(path)))
|
||||||
|
val privatePem = PEMParser(privateKeyReader).readPemObject()
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(privatePem.content)
|
||||||
|
|
||||||
|
return KeyFactory.getInstance("RSA").generatePrivate(privateKeySpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
publicKeyOwner: drpc
|
||||||
|
client:
|
||||||
|
private-key: "classpath:keys/priv-wrong.p8.key"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
auth:
|
auth:
|
||||||
enabled: true
|
enabled: true
|
||||||
publicKeyOwner: drpc
|
publicKeyOwner: drpc
|
||||||
keys:
|
server:
|
||||||
provider-private-key: "classpath:keys/priv-wrong.p8.key"
|
keys:
|
||||||
external-public-key: "classpath:keys/pub-wrong.key"
|
provider-private-key: "classpath:keys/priv-wrong.p8.key"
|
||||||
|
external-public-key: "classpath:keys/pub-wrong.key"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
auth:
|
auth:
|
||||||
enabled: true
|
enabled: true
|
||||||
publicKeyOwner: drpc
|
publicKeyOwner: drpc
|
||||||
keys:
|
server:
|
||||||
provider-private-key: "classpath:keys/priv.p8.key"
|
keys:
|
||||||
external-public-key: "classpath:keys/pub-wrong.key"
|
provider-private-key: "classpath:keys/priv.p8.key"
|
||||||
|
external-public-key: "classpath:keys/pub-wrong.key"
|
||||||
3
src/test/resources/configs/auth-without-any-config.yaml
Normal file
3
src/test/resources/configs/auth-without-any-config.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
publicKeyOwner: drpc
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
auth:
|
auth:
|
||||||
enabled: true
|
enabled: true
|
||||||
publicKeyOwner: drpc
|
publicKeyOwner: drpc
|
||||||
|
server:
|
||||||
|
nothing: true
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
auth:
|
auth:
|
||||||
enabled: true
|
enabled: true
|
||||||
publicKeyOwner: drpc
|
publicKeyOwner: drpc
|
||||||
keys:
|
server:
|
||||||
external-public-key: /keys/pub.key
|
keys:
|
||||||
|
external-public-key: /keys/pub.key
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
auth:
|
auth:
|
||||||
enabled: true
|
enabled: true
|
||||||
publicKeyOwner: drpc
|
publicKeyOwner: drpc
|
||||||
keys:
|
server:
|
||||||
provider-private-key: /keys/priv.p8.key
|
keys:
|
||||||
|
provider-private-key: /keys/priv.p8.key
|
||||||
@@ -14,9 +14,10 @@ tls:
|
|||||||
auth:
|
auth:
|
||||||
enabled: true
|
enabled: true
|
||||||
publicKeyOwner: drpc
|
publicKeyOwner: drpc
|
||||||
keys:
|
server:
|
||||||
provider-private-key: "classpath:keys/priv.p8.key"
|
keys:
|
||||||
external-public-key: "classpath:keys/public.pem"
|
provider-private-key: "classpath:keys/priv.p8.key"
|
||||||
|
external-public-key: "classpath:keys/public.pem"
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@@ -6,4 +6,10 @@ upstreams:
|
|||||||
tls:
|
tls:
|
||||||
ca: /etc/ca.myservice.com.crt
|
ca: /etc/ca.myservice.com.crt
|
||||||
certificate: /etc/client1.myservice.com.crt
|
certificate: /etc/client1.myservice.com.crt
|
||||||
key: /etc/client1.myservice.com.key
|
key: /etc/client1.myservice.com.key
|
||||||
|
- id: remoteTokenAuth
|
||||||
|
connection:
|
||||||
|
grpc:
|
||||||
|
host: "11.12.10.115"
|
||||||
|
token-auth:
|
||||||
|
public-key: /path/to/key.pem
|
||||||
Reference in New Issue
Block a user