Add client auth (#294)

This commit is contained in:
KirillPamPam
2023-09-13 14:31:46 +04:00
committed by GitHub
parent 231c3c5e67
commit d9b410e0ac
30 changed files with 532 additions and 47 deletions

View File

@@ -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) {

View File

@@ -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"]

View File

@@ -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")
} }

View File

@@ -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)

View File

@@ -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:
* ``` * ```

View File

@@ -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:
* ``` * ```

View File

@@ -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("")
}
} }
} }

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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")
} }

View File

@@ -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
} }

View File

@@ -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())
} }

View File

@@ -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)
}
}
}

View File

@@ -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]
}

View File

@@ -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
)
}

View File

@@ -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")
} }
} }

View File

@@ -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"
} }
} }
} }

View File

@@ -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,

View File

@@ -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

View File

@@ -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))

View File

@@ -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)
}
}

View File

@@ -0,0 +1,5 @@
auth:
enabled: true
publicKeyOwner: drpc
client:
private-key: "classpath:keys/priv-wrong.p8.key"

View File

@@ -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"

View File

@@ -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"

View File

@@ -0,0 +1,3 @@
auth:
enabled: true
publicKeyOwner: drpc

View File

@@ -1,3 +1,5 @@
auth: auth:
enabled: true enabled: true
publicKeyOwner: drpc publicKeyOwner: drpc
server:
nothing: true

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -7,3 +7,9 @@ upstreams:
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