Add authorization endpoint (#284)
This commit is contained in:
@@ -98,6 +98,8 @@ dependencies {
|
|||||||
implementation libs.caffeine
|
implementation libs.caffeine
|
||||||
implementation libs.javax.annotations
|
implementation libs.javax.annotations
|
||||||
|
|
||||||
|
implementation libs.auth0.jwt
|
||||||
|
|
||||||
testImplementation libs.cglib.nodep
|
testImplementation libs.cglib.nodep
|
||||||
testImplementation libs.spockframework.core
|
testImplementation libs.spockframework.core
|
||||||
testImplementation libs.grpc.testing
|
testImplementation libs.grpc.testing
|
||||||
|
|||||||
Submodule emerald-grpc updated: 55af14cf34...2c7c3286a5
@@ -124,6 +124,8 @@ spring-cloud-starter-sleuth = "org.springframework.cloud:spring-cloud-starter-sl
|
|||||||
spring-cloud-sleuth-zipkin = "org.springframework.cloud:spring-cloud-sleuth-zipkin:3.1.6"
|
spring-cloud-sleuth-zipkin = "org.springframework.cloud:spring-cloud-sleuth-zipkin:3.1.6"
|
||||||
brave-instrumentation-grpc = "io.zipkin.brave:brave-instrumentation-grpc:5.15.0"
|
brave-instrumentation-grpc = "io.zipkin.brave:brave-instrumentation-grpc:5.15.0"
|
||||||
|
|
||||||
|
auth0-jwt = "com.auth0:java-jwt:4.4.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"]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle
|
package io.emeraldpay.dshackle
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
import io.emeraldpay.dshackle.config.CacheConfig
|
import io.emeraldpay.dshackle.config.CacheConfig
|
||||||
import io.emeraldpay.dshackle.config.ChainsConfig
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
import io.emeraldpay.dshackle.config.CompressionConfig
|
import io.emeraldpay.dshackle.config.CompressionConfig
|
||||||
@@ -153,4 +154,9 @@ open class Config(
|
|||||||
open fun chainsConfig(mainConfig: MainConfig): ChainsConfig {
|
open fun chainsConfig(mainConfig: MainConfig): ChainsConfig {
|
||||||
return mainConfig.chains
|
return mainConfig.chains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun authorizationConfig(mainConfig: MainConfig): AuthorizationConfig {
|
||||||
|
return mainConfig.authorization
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle
|
package io.emeraldpay.dshackle
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.auth.AuthInterceptor
|
||||||
import io.emeraldpay.dshackle.config.MainConfig
|
import io.emeraldpay.dshackle.config.MainConfig
|
||||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerGrpc
|
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerGrpc
|
||||||
import io.grpc.Codec
|
import io.grpc.Codec
|
||||||
@@ -29,8 +30,6 @@ import io.micrometer.core.instrument.Metrics
|
|||||||
import io.micrometer.core.instrument.Tag
|
import io.micrometer.core.instrument.Tag
|
||||||
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics
|
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -46,9 +45,7 @@ open class GrpcServer(
|
|||||||
private val tlsSetup: TlsSetup,
|
private val tlsSetup: TlsSetup,
|
||||||
private val accessHandler: AccessHandlerGrpc,
|
private val accessHandler: AccessHandlerGrpc,
|
||||||
private val grpcServerBraveInterceptor: ServerInterceptor,
|
private val grpcServerBraveInterceptor: ServerInterceptor,
|
||||||
@Autowired(required = false)
|
private val authInterceptor: AuthInterceptor
|
||||||
@Qualifier("serverSpansInterceptor")
|
|
||||||
private val serverSpansInterceptor: ServerInterceptor?
|
|
||||||
) {
|
) {
|
||||||
@Value("\${spring.application.max-metadata-size}")
|
@Value("\${spring.application.max-metadata-size}")
|
||||||
private var maxMetadataSize: Int = Defaults.maxMetadataSize
|
private var maxMetadataSize: Int = Defaults.maxMetadataSize
|
||||||
@@ -87,9 +84,9 @@ open class GrpcServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverBuilder.intercept(grpcServerBraveInterceptor)
|
serverBuilder.intercept(grpcServerBraveInterceptor)
|
||||||
serverSpansInterceptor?.let {
|
if (mainConfig.authorization.enabled) {
|
||||||
serverBuilder.intercept(it)
|
serverBuilder.intercept(authInterceptor)
|
||||||
log.info("Collect spans from provider is enabled")
|
log.info("Token authorization is turned on")
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsSetup.setupServer("Native gRPC", mainConfig.tls, true)?.let {
|
tlsSetup.setupServer("Native gRPC", mainConfig.tls, true)?.let {
|
||||||
|
|||||||
25
src/main/kotlin/io/emeraldpay/dshackle/auth/AuthContext.kt
Normal file
25
src/main/kotlin/io/emeraldpay/dshackle/auth/AuthContext.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class AuthContext {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val sessions = ConcurrentHashMap<String, TokenWrapper>()
|
||||||
|
|
||||||
|
fun putTokenInContext(tokenWrapper: TokenWrapper) {
|
||||||
|
sessions[tokenWrapper.sessionId] = tokenWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeToken(sessionId: String) {
|
||||||
|
sessions.remove(sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TokenWrapper(
|
||||||
|
val token: String,
|
||||||
|
val issuedAt: Instant,
|
||||||
|
val sessionId: String
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.SESSION_ID
|
||||||
|
import io.grpc.Metadata
|
||||||
|
import io.grpc.Metadata.ASCII_STRING_MARSHALLER
|
||||||
|
import io.grpc.ServerCall
|
||||||
|
import io.grpc.ServerCallHandler
|
||||||
|
import io.grpc.ServerInterceptor
|
||||||
|
import io.grpc.Status
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
const val AUTH_METHOD_NAME = "emerald.Auth/Authenticate"
|
||||||
|
const val REFLECT_METHOD_NAME = "grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo"
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class AuthInterceptor : ServerInterceptor {
|
||||||
|
private val specialMethods = setOf(AUTH_METHOD_NAME, REFLECT_METHOD_NAME)
|
||||||
|
|
||||||
|
override fun <ReqT : Any, RespT : Any> interceptCall(
|
||||||
|
call: ServerCall<ReqT, RespT>,
|
||||||
|
headers: Metadata,
|
||||||
|
next: ServerCallHandler<ReqT, RespT>
|
||||||
|
): ServerCall.Listener<ReqT> {
|
||||||
|
val sessionId = headers.get(
|
||||||
|
Metadata.Key.of(SESSION_ID, ASCII_STRING_MARSHALLER)
|
||||||
|
)
|
||||||
|
val isOrdinaryMethod = !specialMethods.contains(call.methodDescriptor.fullMethodName)
|
||||||
|
|
||||||
|
if (isOrdinaryMethod && (sessionId == null || !AuthContext.sessions.containsKey(sessionId))) {
|
||||||
|
val cause = if (sessionId == null) "sessionId is not passed" else "Session $sessionId does not exist"
|
||||||
|
throw Status.UNAUTHENTICATED
|
||||||
|
.withDescription(cause)
|
||||||
|
.asException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.startCall(call, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/main/kotlin/io/emeraldpay/dshackle/auth/AuthRpc.kt
Normal file
47
src/main/kotlin/io/emeraldpay/dshackle/auth/AuthRpc.kt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.AuthOuterClass
|
||||||
|
import io.emeraldpay.api.proto.ReactorAuthGrpc
|
||||||
|
import io.emeraldpay.dshackle.auth.service.AuthService
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusException
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.scheduler.Scheduler
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AuthRpc(
|
||||||
|
private val authService: AuthService,
|
||||||
|
private val authScheduler: Scheduler
|
||||||
|
) : ReactorAuthGrpc.AuthImplBase() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(AuthRpc::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun authenticate(request: Mono<AuthOuterClass.AuthRequest>): Mono<AuthOuterClass.AuthResponse> {
|
||||||
|
log.info("Start auth process...")
|
||||||
|
return request
|
||||||
|
.subscribeOn(authScheduler)
|
||||||
|
.map {
|
||||||
|
val token = authService.authenticate(it.token)
|
||||||
|
AuthOuterClass.AuthResponse.newBuilder()
|
||||||
|
.setProviderToken(token)
|
||||||
|
.build()
|
||||||
|
}.onErrorResume {
|
||||||
|
if (it is StatusException) {
|
||||||
|
log.error(it.message)
|
||||||
|
Mono.error(it)
|
||||||
|
} else {
|
||||||
|
val message = "Internal error: ${it.message}"
|
||||||
|
log.error(message, it)
|
||||||
|
Mono.error(
|
||||||
|
Status.INTERNAL
|
||||||
|
.withDescription(message)
|
||||||
|
.asException()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.processor
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.JWTVerifier
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import io.emeraldpay.dshackle.auth.AuthContext
|
||||||
|
import io.emeraldpay.dshackle.auth.service.KeyReader
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
|
import io.grpc.Status
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
|
import java.security.interfaces.RSAPublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
const val SESSION_ID = "sessionId"
|
||||||
|
const val VERSION = "version"
|
||||||
|
|
||||||
|
enum class AuthVersion {
|
||||||
|
V1;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getVersion(version: String) = values().find { it.name == version }
|
||||||
|
?: throw Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("Unsupported auth version $version")
|
||||||
|
.asException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AuthProcessor(
|
||||||
|
private val authorizationConfig: AuthorizationConfig
|
||||||
|
) {
|
||||||
|
|
||||||
|
open fun process(keys: KeyReader.Keys, token: String): AuthContext.TokenWrapper {
|
||||||
|
try {
|
||||||
|
val verifier: JWTVerifier = JWT.require(verifyingAlgorithm(keys.externalPublicKey))
|
||||||
|
.withIssuer(authorizationConfig.publicKeyOwner)
|
||||||
|
.build()
|
||||||
|
verifier.verify(token)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("Invalid token: ${e.message}")
|
||||||
|
.asException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return processInternal(keys.providerPrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun processInternal(privateKey: PrivateKey): AuthContext.TokenWrapper
|
||||||
|
|
||||||
|
protected abstract fun verifyingAlgorithm(publicKey: PublicKey): Algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
open class AuthProcessorV1(
|
||||||
|
authorizationConfig: AuthorizationConfig
|
||||||
|
) : AuthProcessor(authorizationConfig) {
|
||||||
|
|
||||||
|
override fun processInternal(privateKey: PrivateKey): AuthContext.TokenWrapper {
|
||||||
|
val issAt = Instant.now()
|
||||||
|
val sessionId = UUID.randomUUID().toString()
|
||||||
|
val token = JWT.create()
|
||||||
|
.withIssuedAt(issAt)
|
||||||
|
.withClaim(SESSION_ID, sessionId)
|
||||||
|
.withClaim(VERSION, AuthVersion.V1.toString())
|
||||||
|
.sign(Algorithm.RSA256(privateKey as RSAPrivateKey))
|
||||||
|
|
||||||
|
return AuthContext.TokenWrapper(token, issAt, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verifyingAlgorithm(publicKey: PublicKey): Algorithm =
|
||||||
|
Algorithm.RSA256(publicKey as RSAPublicKey, null)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.processor
|
||||||
|
|
||||||
|
import com.auth0.jwt.interfaces.DecodedJWT
|
||||||
|
import io.grpc.Status
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class AuthProcessorResolver(
|
||||||
|
private val authProcessorV1: AuthProcessor
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(AuthProcessorResolver::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthProcessor(token: DecodedJWT): AuthProcessor {
|
||||||
|
val claimVersion = token.getClaim(VERSION)
|
||||||
|
if (claimVersion.isMissing) {
|
||||||
|
throw Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("Version is not specified in the token")
|
||||||
|
.asException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val version = AuthVersion.getVersion(claimVersion.asString())
|
||||||
|
log.info("Using $version of authentication")
|
||||||
|
|
||||||
|
if (version == AuthVersion.V1) {
|
||||||
|
return authProcessorV1
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("Unsupported auth version $version")
|
||||||
|
.asException()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.processor
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.auth.AuthContext
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
@Component
|
||||||
|
open class TokenProcessor {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(TokenProcessor::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 30000)
|
||||||
|
fun invalidateTokens() {
|
||||||
|
AuthContext.sessions
|
||||||
|
.filter { Instant.now().isAfter(it.value.issuedAt.plus(1, ChronoUnit.HOURS)) }
|
||||||
|
.forEach {
|
||||||
|
log.info("Invalidate token with sessionId ${it.key}")
|
||||||
|
AuthContext.removeToken(it.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.service
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import io.emeraldpay.dshackle.auth.AuthContext
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.AuthProcessorResolver
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
|
import io.grpc.Status
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AuthService(
|
||||||
|
private val authorizationConfig: AuthorizationConfig,
|
||||||
|
private val rsaKeyReader: KeyReader,
|
||||||
|
private val authProcessorResolver: AuthProcessorResolver
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun authenticate(token: String): String {
|
||||||
|
if (!authorizationConfig.enabled) {
|
||||||
|
throw Status.UNIMPLEMENTED
|
||||||
|
.withDescription("Authentication process is not enabled")
|
||||||
|
.asException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val keys = rsaKeyReader.getKeyPair(
|
||||||
|
authorizationConfig.providerPrivateKeyPath, authorizationConfig.externalPublicKeyPath
|
||||||
|
)
|
||||||
|
val decodedJwt = JWT.decode(token)
|
||||||
|
|
||||||
|
return authProcessorResolver
|
||||||
|
.getAuthProcessor(decodedJwt)
|
||||||
|
.process(keys, token)
|
||||||
|
.run {
|
||||||
|
AuthContext.putTokenInContext(this)
|
||||||
|
this.token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.service
|
||||||
|
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
|
interface KeyReader {
|
||||||
|
|
||||||
|
fun getKeyPair(providerPrivateKeyPath: String, externalPublicKeyPath: String): Keys
|
||||||
|
|
||||||
|
data class Keys(
|
||||||
|
val providerPrivateKey: PrivateKey,
|
||||||
|
val externalPublicKey: PublicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.service
|
||||||
|
|
||||||
|
import org.bouncycastle.openssl.PEMParser
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class RsaKeyReader : KeyReader {
|
||||||
|
private val factory = KeyFactory.getInstance("RSA")
|
||||||
|
|
||||||
|
override fun getKeyPair(providerPrivateKeyPath: String, externalPublicKeyPath: String): KeyReader.Keys {
|
||||||
|
val privateKeyReader = StringReader(Files.readString(Paths.get(providerPrivateKeyPath)))
|
||||||
|
val publicKeyReader = StringReader(Files.readString(Paths.get(externalPublicKeyPath)))
|
||||||
|
|
||||||
|
val privatePem = PEMParser(privateKeyReader).readPemObject()
|
||||||
|
val publicPem = PEMParser(publicKeyReader).readPemObject()
|
||||||
|
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(privatePem.content)
|
||||||
|
val publicKeySpec = X509EncodedKeySpec(publicPem.content)
|
||||||
|
|
||||||
|
val pubKey = factory.generatePublic(publicKeySpec)
|
||||||
|
val privateKey = factory.generatePrivate(privateKeySpec)
|
||||||
|
|
||||||
|
return KeyReader.Keys(
|
||||||
|
privateKey,
|
||||||
|
pubKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package io.emeraldpay.dshackle.config
|
||||||
|
|
||||||
|
data class AuthorizationConfig(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val publicKeyOwner: String,
|
||||||
|
val providerPrivateKeyPath: String,
|
||||||
|
val externalPublicKeyPath: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun default() = AuthorizationConfig(false, "", "", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package io.emeraldpay.dshackle.config
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.util.ResourceUtils
|
||||||
|
import org.yaml.snakeyaml.nodes.MappingNode
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
class AuthorizationConfigReader : YamlConfigReader<AuthorizationConfig>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(AuthorizationConfigReader::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(input: MappingNode?): AuthorizationConfig {
|
||||||
|
val auth = getMapping(input, "auth")
|
||||||
|
if (auth == null) {
|
||||||
|
log.warn("Authorization is not using")
|
||||||
|
return AuthorizationConfig.default()
|
||||||
|
}
|
||||||
|
|
||||||
|
val enabled = getValueAsBool(auth, "enabled")
|
||||||
|
if (enabled == null || !enabled) {
|
||||||
|
log.warn("Authorization is not enabled")
|
||||||
|
return AuthorizationConfig.default()
|
||||||
|
}
|
||||||
|
|
||||||
|
val publicKeyOwner = getValueAsString(auth, "publicKeyOwner")
|
||||||
|
?: throw IllegalStateException("Public key owner in not specified")
|
||||||
|
|
||||||
|
val keyPair = getMapping(auth, "keys") ?: throw IllegalStateException("Auth keys is 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)) {
|
||||||
|
throw IllegalStateException("There is no such file: $privateKey")
|
||||||
|
}
|
||||||
|
if (fileNotExists(publicKey)) {
|
||||||
|
throw IllegalStateException("There is no such file: $publicKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthorizationConfig(enabled, publicKeyOwner, privateKey, publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileNotExists(path: String): Boolean {
|
||||||
|
return try {
|
||||||
|
!ResourceUtils.getFile(path).exists()
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,4 +30,5 @@ class MainConfig {
|
|||||||
var signature: SignatureConfig? = null
|
var signature: SignatureConfig? = null
|
||||||
var compression: CompressionConfig = CompressionConfig.default()
|
var compression: CompressionConfig = CompressionConfig.default()
|
||||||
var chains: ChainsConfig = ChainsConfig.default()
|
var chains: ChainsConfig = ChainsConfig.default()
|
||||||
|
var authorization: AuthorizationConfig = AuthorizationConfig.default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class MainConfigReader(
|
|||||||
private val signatureConfigReader = SignatureConfigReader(fileResolver)
|
private val signatureConfigReader = SignatureConfigReader(fileResolver)
|
||||||
private val compressionConfigReader = CompressionConfigReader()
|
private val compressionConfigReader = CompressionConfigReader()
|
||||||
private val chainsConfigReader = ChainsConfigReader(upstreamsConfigReader)
|
private val chainsConfigReader = ChainsConfigReader(upstreamsConfigReader)
|
||||||
|
private val authorizationConfigReader = AuthorizationConfigReader()
|
||||||
|
|
||||||
override fun read(input: MappingNode?): MainConfig {
|
override fun read(input: MappingNode?): MainConfig {
|
||||||
val config = MainConfig()
|
val config = MainConfig()
|
||||||
@@ -80,6 +81,9 @@ class MainConfigReader(
|
|||||||
chainsConfigReader.read(input).let {
|
chainsConfigReader.read(input).let {
|
||||||
config.chains = it
|
config.chains = it
|
||||||
}
|
}
|
||||||
|
authorizationConfigReader.read(input).let {
|
||||||
|
config.authorization = it
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ open class SchedulersConfig {
|
|||||||
return makePool("grpc-client-channel", 10, monitoringConfig)
|
return makePool("grpc-client-channel", 10, monitoringConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun authScheduler(monitoringConfig: MonitoringConfig): Scheduler {
|
||||||
|
return makeScheduler("auth-scheduler", 4, monitoringConfig)
|
||||||
|
}
|
||||||
|
|
||||||
private fun makeScheduler(name: String, size: Int, monitoringConfig: MonitoringConfig): Scheduler {
|
private fun makeScheduler(name: String, size: Int, monitoringConfig: MonitoringConfig): Scheduler {
|
||||||
return Schedulers.fromExecutorService(makePool(name, size, monitoringConfig))
|
return Schedulers.fromExecutorService(makePool(name, size, monitoringConfig))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package io.emeraldpay.dshackle.config
|
||||||
|
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class AuthorizationConfigReaderSpec extends Specification {
|
||||||
|
def reader = new AuthorizationConfigReader()
|
||||||
|
|
||||||
|
def "if no auth option then default settings"() {
|
||||||
|
setup:
|
||||||
|
def yamlIs = this.class.getClassLoader().getResourceAsStream("configs/upstreams-basic.yaml")
|
||||||
|
when:
|
||||||
|
def act = reader.read(yamlIs)
|
||||||
|
then:
|
||||||
|
act == AuthorizationConfig.default()
|
||||||
|
}
|
||||||
|
|
||||||
|
def "if auth is disabled then defaults settings"() {
|
||||||
|
setup:
|
||||||
|
def yamlIs = this.class.getClassLoader().getResourceAsStream("configs/auth-disabled.yaml")
|
||||||
|
when:
|
||||||
|
def act = reader.read(yamlIs)
|
||||||
|
then:
|
||||||
|
act == AuthorizationConfig.default()
|
||||||
|
}
|
||||||
|
|
||||||
|
def "check default settings"() {
|
||||||
|
when:
|
||||||
|
def act = AuthorizationConfig.default()
|
||||||
|
then:
|
||||||
|
!act.enabled
|
||||||
|
act.externalPublicKeyPath == ""
|
||||||
|
act.providerPrivateKeyPath == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
def "exceptions if no settings"() {
|
||||||
|
setup:
|
||||||
|
def yamlIs = this.class.getClassLoader().getResourceAsStream(filePath)
|
||||||
|
when:
|
||||||
|
reader.read(yamlIs)
|
||||||
|
then:
|
||||||
|
def t = thrown(IllegalStateException)
|
||||||
|
t.message == message
|
||||||
|
where:
|
||||||
|
filePath | message
|
||||||
|
"configs/auth-without-public-key.yaml" | "External key in not specified"
|
||||||
|
"configs/auth-without-private-key.yaml" | "Private key in not specified"
|
||||||
|
"configs/auth-without-key-pair.yaml" | "Auth keys is 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-pub-key.yaml" | "There is no such file: classpath:keys/pub-wrong.key"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,5 +102,11 @@ class MainConfigReaderSpec extends Specification {
|
|||||||
id == "infura"
|
id == "infura"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
act.authorization != null
|
||||||
|
with(act.authorization) {
|
||||||
|
enabled
|
||||||
|
providerPrivateKeyPath == "classpath:keys/priv.p8.key"
|
||||||
|
externalPublicKeyPath == "classpath:keys/public.pem"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.processor
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
|
import io.grpc.StatusException
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class AuthProcessorResolverTest {
|
||||||
|
private val authProcessorV1 = AuthProcessorV1(AuthorizationConfig.default())
|
||||||
|
private val authProcessorResolver = AuthProcessorResolver(authProcessorV1)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get processor of V1 version`() {
|
||||||
|
val token = JWT.decode(
|
||||||
|
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg1OTMxLCJ2ZXJzaW9uI" +
|
||||||
|
"joiVjEifQ.BZILN0GQ7JzXGFz-GZIbFTT9E5L-miB4Nga0v4o_cQThk8gbDelBRzEfdsqxCq_ppPr3v_Own8M-vR9yQElx5nEdlI4xe5QAMdIvr3g" +
|
||||||
|
"12fMckydX9IsW4sVQ1kJJY8RrHb-WL-uI0WSWqoMSwf-Psb-UyiEHAjc3oK7fA72lBaGT4waPHOxRBPvezwg7N934vCZvZMAftFfVgmeEtbCeD7bF" +
|
||||||
|
"umEr0uEmkIKPTg4QwP-VMvqoLBYpMiJVzP_Ipg_wRHJ7fUN0BGEPjjMvhQ_6TWByiQUBz1kTMd0Ebf_kEuXFQeiwA-FXHJpWczzh66CbbmmWAWsi" +
|
||||||
|
"ehKw3KPZeBj0oQ"
|
||||||
|
)
|
||||||
|
val processor = authProcessorResolver.getAuthProcessor(token)
|
||||||
|
|
||||||
|
assertTrue(processor is AuthProcessorV1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `failed if no version is in a token`() {
|
||||||
|
val token = JWT.decode(
|
||||||
|
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg5NTUzfQ.sQ1Q3DFC7kOlHWdnWaxv2F" +
|
||||||
|
"vhGso6ajJQVWtrE1hYmL_AIH_Lz8LNpqFShlL2itEGfmyRz5-Vrbdf-yWjeyLyNDlf7pay2lNE5Pvm2-EtQd8GUYcCFFIz7Sxc2Iphe2" +
|
||||||
|
"YIx6kBwSlaR0RXBcmUlOtKrnON0bBNzSojBmtCT7-j4hTpoKhYr04Fr9EJWHfw7grVZjU8rEizAX_SR3ZNoufjK_pZaIyI9qUKVPYSepP" +
|
||||||
|
"lXtQzVjA80qSeYpkeFCOLwlQD_yTArDNWlwe7-CthtBOAtctoTMwyudfJezT2ilXrigzbauzU5BEi1cNxacHpjNuXhyY0TiacJGugWfRgaaGy6g"
|
||||||
|
)
|
||||||
|
|
||||||
|
val e = assertThrows(StatusException::class.java) { authProcessorResolver.getAuthProcessor(token) }
|
||||||
|
assertEquals("INVALID_ARGUMENT: Version is not specified in the token", e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `failed if the wrong version is specified`() {
|
||||||
|
val token = JWT.decode(
|
||||||
|
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg5OTI3LCJ2ZXJzaW9uIjoiVjIifQ.pT" +
|
||||||
|
"mG3Z-h9P1DlocTKZ3BpAKNdsvRGjXo71I0GUpUemAnauUaH-OgxUOHlNcZ2uB4f1poEWsbpExeuZ15YAiBf2WBCEV6J6xH1u0cPC1O-8" +
|
||||||
|
"hHNb46166ngzo-BtZ7Rn7FeVEayyICslc9iXM5GvoFJyJIdn9uLMzalyDJq2bUmIelym4edkQ3ybF-pqf8garuVVErnAsbOFXbYQlfO5ZJ" +
|
||||||
|
"4zq6PfJo7QSkreMzQ4tK_-JJIGG-EK1bQjsAD7JiXipY2cJY17VlHuavI0DfcJlOe-QggbTH63rxL6JvXCyFux7gI7zdqSBP1fNDJNzTLA" +
|
||||||
|
"cnR7jCN0kMIa8urQgdePZyRg"
|
||||||
|
)
|
||||||
|
|
||||||
|
val e = assertThrows(StatusException::class.java) { authProcessorResolver.getAuthProcessor(token) }
|
||||||
|
assertEquals("INVALID_ARGUMENT: Unsupported auth version V2", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.processor
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.JWTVerifier
|
||||||
|
import com.auth0.jwt.RegisteredClaims
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import io.emeraldpay.dshackle.auth.service.RsaKeyReader
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
|
import io.grpc.StatusException
|
||||||
|
import org.bouncycastle.openssl.PEMParser
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.util.ResourceUtils
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.interfaces.RSAPublicKey
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
|
||||||
|
class AuthProcessorV1Test {
|
||||||
|
private val processor = AuthProcessorV1(AuthorizationConfig(true, "drpc", "", ""))
|
||||||
|
private val rsaKeyReader = RsaKeyReader()
|
||||||
|
private val privProviderPath = ResourceUtils.getFile("classpath:keys/priv.p8.key").path
|
||||||
|
private val publicDrpcPath = ResourceUtils.getFile("classpath:keys/public-drpc.pem").path
|
||||||
|
private val token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg1OTMxLCJ2ZXJzaW9uI" +
|
||||||
|
"joiVjEifQ.BZILN0GQ7JzXGFz-GZIbFTT9E5L-miB4Nga0v4o_cQThk8gbDelBRzEfdsqxCq_ppPr3v_Own8M-vR9yQElx5nEdlI4xe5QAMdIvr3g" +
|
||||||
|
"12fMckydX9IsW4sVQ1kJJY8RrHb-WL-uI0WSWqoMSwf-Psb-UyiEHAjc3oK7fA72lBaGT4waPHOxRBPvezwg7N934vCZvZMAftFfVgmeEtbCeD7bF" +
|
||||||
|
"umEr0uEmkIKPTg4QwP-VMvqoLBYpMiJVzP_Ipg_wRHJ7fUN0BGEPjjMvhQ_6TWByiQUBz1kTMd0Ebf_kEuXFQeiwA-FXHJpWczzh66CbbmmWAWsi" +
|
||||||
|
"ehKw3KPZeBj0oQ"
|
||||||
|
private val keyPair = rsaKeyReader.getKeyPair(privProviderPath, publicDrpcPath)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verify tokens is successful`() {
|
||||||
|
val publicProviderPath = ResourceUtils.getFile("classpath:keys/public.pem").path
|
||||||
|
|
||||||
|
val providerToken = processor.process(keyPair, token).token
|
||||||
|
val verifier: JWTVerifier = JWT.require(Algorithm.RSA256(generatePublicKey(publicProviderPath) as RSAPublicKey, null))
|
||||||
|
.withClaim(VERSION, "V1")
|
||||||
|
.build()
|
||||||
|
val decodedToken = verifier.verify(providerToken)
|
||||||
|
assertTrue(!decodedToken.getClaim(SESSION_ID).isMissing)
|
||||||
|
assertTrue(!decodedToken.getClaim(RegisteredClaims.ISSUED_AT).isMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verify token is failed by wrong key`() {
|
||||||
|
val publicProviderPath = ResourceUtils.getFile("classpath:keys/wrong-public.pem").path
|
||||||
|
val keyPair = rsaKeyReader.getKeyPair(privProviderPath, publicProviderPath)
|
||||||
|
|
||||||
|
val e = assertThrows(StatusException::class.java) { processor.process(keyPair, token) }
|
||||||
|
assertEquals(
|
||||||
|
"INVALID_ARGUMENT: Invalid token: The Token's Signature resulted invalid when verified using the Algorithm: SHA256withRSA",
|
||||||
|
e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verify token is failed if no issuer`() {
|
||||||
|
val invalidToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjY2NjIiwiaWF0IjoxNjkyMTg3NDMwLCJ2ZXJzaW" +
|
||||||
|
"9uIjoiVjEifQ.J1WJ1GvjNJ9JQiMK0bwvtGX1o9P93F5-921myIx3TMa2X48qIG0GVEcoMgv01ca-_aisW-Amk27ygI09dPKE__Ijr6JhZ" +
|
||||||
|
"fDVNkw_ZArtQTcjhhJiCl3pqsouOlojc8EolpYUmyOefqemqycG0B84ibKAWTdXOtjibt1P5szWjIIV9yOYV7lTJkC0B5swcjjaMvTEPU7y" +
|
||||||
|
"ZJhg_wvCvT67yFM1K_Wnhys3-j-Xv1Y2wOkxNt4i5LKFDtMZml5eTIEscDpjp5ARjaSTW_Rs1Eixqltx_wz1ALiS0QXOJpX7pVMJjRcth4Nu" +
|
||||||
|
"R87ej434XoHZWqDmvOEM6M855WeHaO761A"
|
||||||
|
|
||||||
|
val e = assertThrows(StatusException::class.java) { processor.process(keyPair, invalidToken) }
|
||||||
|
assertEquals(
|
||||||
|
"INVALID_ARGUMENT: Invalid token: The Claim 'iss' value doesn't match the required issuer.",
|
||||||
|
e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePublicKey(path: String): PublicKey {
|
||||||
|
val publicKeyReader = StringReader(Files.readString(Paths.get(path)))
|
||||||
|
|
||||||
|
val publicPem = PEMParser(publicKeyReader).readPemObject()
|
||||||
|
|
||||||
|
val publicKeySpec = X509EncodedKeySpec(publicPem.content)
|
||||||
|
|
||||||
|
return KeyFactory.getInstance("RSA").generatePublic(publicKeySpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.processor
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.auth.AuthContext
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
class TokenProcessorTest {
|
||||||
|
private val tokenProcessor = TokenProcessor()
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun removeSessions() {
|
||||||
|
AuthContext.sessions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invalidate all tokens`() {
|
||||||
|
AuthContext.putTokenInContext(
|
||||||
|
AuthContext.TokenWrapper("token", Instant.now().minus(1, ChronoUnit.HOURS), "session1")
|
||||||
|
)
|
||||||
|
AuthContext.putTokenInContext(
|
||||||
|
AuthContext.TokenWrapper("token", Instant.now().minus(1, ChronoUnit.HOURS), "session2")
|
||||||
|
)
|
||||||
|
AuthContext.putTokenInContext(
|
||||||
|
AuthContext.TokenWrapper("token", Instant.now().minus(1, ChronoUnit.HOURS), "session3")
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenProcessor.invalidateTokens()
|
||||||
|
|
||||||
|
assertTrue(AuthContext.sessions.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `tokens are still in the context after invalidation`() {
|
||||||
|
val token1 = AuthContext.TokenWrapper("token", Instant.now().minus(30, ChronoUnit.MINUTES), "session1")
|
||||||
|
val token2 = AuthContext.TokenWrapper("token", Instant.now().minus(30, ChronoUnit.MINUTES), "session2")
|
||||||
|
val token3 = AuthContext.TokenWrapper("token", Instant.now().minus(30, ChronoUnit.MINUTES), "session3")
|
||||||
|
AuthContext.putTokenInContext(token1)
|
||||||
|
AuthContext.putTokenInContext(token2)
|
||||||
|
AuthContext.putTokenInContext(token3)
|
||||||
|
|
||||||
|
tokenProcessor.invalidateTokens()
|
||||||
|
|
||||||
|
assertEquals(3, AuthContext.sessions.size)
|
||||||
|
assertEquals(
|
||||||
|
mapOf(token1.sessionId to token1, token2.sessionId to token2, token3.sessionId to token3),
|
||||||
|
AuthContext.sessions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.service
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.auth.AuthContext
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.AuthProcessor
|
||||||
|
import io.emeraldpay.dshackle.auth.processor.AuthProcessorResolver
|
||||||
|
import io.emeraldpay.dshackle.config.AuthorizationConfig
|
||||||
|
import io.grpc.StatusException
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.times
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
|
class AuthServiceTest {
|
||||||
|
private val rsaKeyReader = mock(KeyReader::class.java)
|
||||||
|
private val mockV1Processor = mock(AuthProcessor::class.java)
|
||||||
|
private val factory = AuthProcessorResolver(mockV1Processor)
|
||||||
|
|
||||||
|
private val token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg1OTMxLCJ2ZXJzaW9uI" +
|
||||||
|
"joiVjEifQ.BZILN0GQ7JzXGFz-GZIbFTT9E5L-miB4Nga0v4o_cQThk8gbDelBRzEfdsqxCq_ppPr3v_Own8M-vR9yQElx5nEdlI4xe5QAMdIvr3g" +
|
||||||
|
"12fMckydX9IsW4sVQ1kJJY8RrHb-WL-uI0WSWqoMSwf-Psb-UyiEHAjc3oK7fA72lBaGT4waPHOxRBPvezwg7N934vCZvZMAftFfVgmeEtbCeD7bF" +
|
||||||
|
"umEr0uEmkIKPTg4QwP-VMvqoLBYpMiJVzP_Ipg_wRHJ7fUN0BGEPjjMvhQ_6TWByiQUBz1kTMd0Ebf_kEuXFQeiwA-FXHJpWczzh66CbbmmWAWsi" +
|
||||||
|
"ehKw3KPZeBj0oQ"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unimplemented error if auth is disabled`() {
|
||||||
|
val authService = AuthService(AuthorizationConfig.default(), rsaKeyReader, factory)
|
||||||
|
|
||||||
|
val e = assertThrows(StatusException::class.java) { authService.authenticate("") }
|
||||||
|
assertEquals("UNIMPLEMENTED: Authentication process is not enabled", e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `auth is successful`() {
|
||||||
|
val tokenWrapper = AuthContext.TokenWrapper(
|
||||||
|
"token", Instant.now(), "sessionId"
|
||||||
|
)
|
||||||
|
val authService = AuthService(AuthorizationConfig(true, "drpc", "privPath", "pubPath"), rsaKeyReader, factory)
|
||||||
|
val pair = KeyReader.Keys(mock(PrivateKey::class.java), mock(PublicKey::class.java))
|
||||||
|
|
||||||
|
`when`(rsaKeyReader.getKeyPair("privPath", "pubPath"))
|
||||||
|
.thenReturn(pair)
|
||||||
|
`when`(mockV1Processor.process(pair, token)).thenReturn(tokenWrapper)
|
||||||
|
|
||||||
|
authService.authenticate(token)
|
||||||
|
verify(rsaKeyReader).getKeyPair("privPath", "pubPath")
|
||||||
|
verify(mockV1Processor).process(pair, token)
|
||||||
|
assertTrue(AuthContext.sessions.containsKey(tokenWrapper.sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parallel try to auth is successful`() {
|
||||||
|
val tokenWrapper = AuthContext.TokenWrapper(
|
||||||
|
"token", Instant.now(), "sessionId"
|
||||||
|
)
|
||||||
|
val tokenWrapper1 = AuthContext.TokenWrapper(
|
||||||
|
"token", Instant.now(), "sessionIdNext"
|
||||||
|
)
|
||||||
|
val pair = KeyReader.Keys(mock(PrivateKey::class.java), mock(PublicKey::class.java))
|
||||||
|
val authService = AuthService(AuthorizationConfig(true, "drpc", "privPath", "pubPath"), rsaKeyReader, factory)
|
||||||
|
|
||||||
|
`when`(rsaKeyReader.getKeyPair("privPath", "pubPath")).thenReturn(pair)
|
||||||
|
`when`(mockV1Processor.process(pair, token))
|
||||||
|
.thenReturn(tokenWrapper)
|
||||||
|
.thenReturn(tokenWrapper1)
|
||||||
|
|
||||||
|
val task = Runnable { authService.authenticate(token) }
|
||||||
|
|
||||||
|
CompletableFuture.allOf(
|
||||||
|
CompletableFuture.runAsync(task), CompletableFuture.runAsync(task)
|
||||||
|
).join()
|
||||||
|
|
||||||
|
verify(rsaKeyReader, times(2)).getKeyPair("privPath", "pubPath")
|
||||||
|
verify(mockV1Processor, times(2)).process(pair, token)
|
||||||
|
assertTrue(AuthContext.sessions.containsKey(tokenWrapper.sessionId))
|
||||||
|
assertTrue(AuthContext.sessions.containsKey(tokenWrapper1.sessionId))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package io.emeraldpay.dshackle.auth.service
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.util.ResourceUtils
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
|
import java.security.interfaces.RSAPublicKey
|
||||||
|
|
||||||
|
class RsaKeyReaderTest {
|
||||||
|
private val rsaKeyReader = RsaKeyReader()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `read rsa keys`() {
|
||||||
|
val privPath = ResourceUtils.getFile("classpath:keys/priv.p8.key").path
|
||||||
|
val publicPath = ResourceUtils.getFile("classpath:keys/public-drpc.pem").path
|
||||||
|
|
||||||
|
val pair = rsaKeyReader.getKeyPair(privPath, publicPath)
|
||||||
|
val privateKey = pair.providerPrivateKey
|
||||||
|
val publicKey = pair.externalPublicKey
|
||||||
|
|
||||||
|
assertEquals("RSA", publicKey.algorithm)
|
||||||
|
assertEquals("RSA", privateKey.algorithm)
|
||||||
|
assertTrue(privateKey is RSAPrivateKey)
|
||||||
|
assertTrue(publicKey is RSAPublicKey)
|
||||||
|
assertTrue {
|
||||||
|
(publicKey as RSAPublicKey)
|
||||||
|
.run {
|
||||||
|
val pubExponent = BigInteger("65537")
|
||||||
|
val pubModulus = BigInteger(
|
||||||
|
"240721495118071395463408682378448427874712549978328672120875120395" +
|
||||||
|
"572530650526403138656868181996853461531503006596703750232991335166031612653183175739344513002793" +
|
||||||
|
"13348833980409910131805866311837600488070189526246817317791758685747539443624701130779843529258916" +
|
||||||
|
"650627570641542946773123818063304222185205897480168513227047010620610697362185901253798594085716467" +
|
||||||
|
"92700927695491574061920135297311257486773262784524717857554194798974994838378321973611865011031457" +
|
||||||
|
"8859834631052697211398759550437148562864384448640344034211059348047816397935347753413946137935402" +
|
||||||
|
"562264627177210569215727831448291731093608051334025332988696191"
|
||||||
|
)
|
||||||
|
publicExponent == pubExponent && modulus == pubModulus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue {
|
||||||
|
(privateKey as RSAPrivateKey)
|
||||||
|
.run {
|
||||||
|
val privModulus = BigInteger(
|
||||||
|
"2832492054027911929186769713106999105750517776225145627897719982164048887089826085864021820" +
|
||||||
|
"93695282142899084731750598834879176944012613634073603363270499693159806868257221336886781437442" +
|
||||||
|
"7025894179697109350195776864078690751332857203663624730020413376539574511375327958483292435380" +
|
||||||
|
"2072047577218574632871804997852794886795318485439021501388394533101191305015940756335545719496" +
|
||||||
|
"04557847518214006893286271137213491059994279470633688916032797849945887259137876037521925382454" +
|
||||||
|
"762238089597912107394417801381736627011587603418078578130206830219321107525729614610300336784925" +
|
||||||
|
"6741484379041499791605663418628739419306248595872949"
|
||||||
|
)
|
||||||
|
val privExponent = BigInteger(
|
||||||
|
"2704777724417882789147602456408780048462378556719441898242139736695157047955437327090684858" +
|
||||||
|
"9022442508913912020206014886681429805316382175637569992035330289450733224635356250217508515675" +
|
||||||
|
"1746547003301653260530619509144105809847739245799589488158044325999968951997941276374874356761" +
|
||||||
|
"7744591423894125208163912446945277960899800043458394321204754817284255735206518583285355872405" +
|
||||||
|
"3966258052672146605639969648388744960700258048094135244952249403403074548577386549705289154261" +
|
||||||
|
"2346391566526533514196717633725503640437930121313225102536030460301806443109607461162231878852" +
|
||||||
|
"64869837629637060803692577216204745443259318662678133769"
|
||||||
|
)
|
||||||
|
|
||||||
|
privateExponent == privExponent && modulus == privModulus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/test/resources/configs/auth-disabled.yaml
Normal file
2
src/test/resources/configs/auth-disabled.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
auth:
|
||||||
|
enabled: false
|
||||||
6
src/test/resources/configs/auth-with-wrong-priv-key.yaml
Normal file
6
src/test/resources/configs/auth-with-wrong-priv-key.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
publicKeyOwner: drpc
|
||||||
|
keys:
|
||||||
|
provider-private-key: "classpath:keys/priv-wrong.p8.key"
|
||||||
|
external-public-key: "classpath:keys/pub-wrong.key"
|
||||||
6
src/test/resources/configs/auth-with-wrong-pub-key.yaml
Normal file
6
src/test/resources/configs/auth-with-wrong-pub-key.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
publicKeyOwner: drpc
|
||||||
|
keys:
|
||||||
|
provider-private-key: "classpath:keys/priv.p8.key"
|
||||||
|
external-public-key: "classpath:keys/pub-wrong.key"
|
||||||
2
src/test/resources/configs/auth-without-key-owner.yaml
Normal file
2
src/test/resources/configs/auth-without-key-owner.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
3
src/test/resources/configs/auth-without-key-pair.yaml
Normal file
3
src/test/resources/configs/auth-without-key-pair.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
publicKeyOwner: drpc
|
||||||
5
src/test/resources/configs/auth-without-private-key.yaml
Normal file
5
src/test/resources/configs/auth-without-private-key.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
publicKeyOwner: drpc
|
||||||
|
keys:
|
||||||
|
external-public-key: /keys/pub.key
|
||||||
5
src/test/resources/configs/auth-without-public-key.yaml
Normal file
5
src/test/resources/configs/auth-without-public-key.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
publicKeyOwner: drpc
|
||||||
|
keys:
|
||||||
|
provider-private-key: /keys/priv.p8.key
|
||||||
@@ -11,6 +11,13 @@ tls:
|
|||||||
require: false
|
require: false
|
||||||
ca: "/path/ca.dshackle.test.crt"
|
ca: "/path/ca.dshackle.test.crt"
|
||||||
|
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
publicKeyOwner: drpc
|
||||||
|
keys:
|
||||||
|
provider-private-key: "classpath:keys/priv.p8.key"
|
||||||
|
external-public-key: "classpath:keys/public.pem"
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
redis:
|
redis:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
28
src/test/resources/keys/priv-drpc.p8.key
Normal file
28
src/test/resources/keys/priv-drpc.p8.key
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC+sCrzY7pN4L3C
|
||||||
|
uPyV8Q5dFilQYuP8kAWF2X6nAz2WFzHAPQJQXkwUeFz0T1Py0TtwrX6RcYu1KOuC
|
||||||
|
Bj78KvtptThFxFk9T9I3mpi00ZoIGVGPniW/4lMdN2DYqsXF/2r7K6dmtA9JQKAl
|
||||||
|
RjQiIkclwIn2/JYIFWi5viaeBl1XLG7m06TdksGt5JJ/c6yk0cFn0jlFeVLZr087
|
||||||
|
EWkj+3NN5+7dNHc+LCGunKHXX+cl+GHGc3JO6a953t6POhHXZh+/lX+WzD83uKUN
|
||||||
|
bYdCn0eyz2++6/lOhYR7x6Nc2d+ikEGzqGDnalD3bJPLV0hm5fXTUyd2zipPDtXc
|
||||||
|
R3UQsdZ/AgMBAAECggEBAJmDyEBDxGQYD40pUigAdhjY04/k3TiiVd/pNj6MrFMd
|
||||||
|
/y6Nr0Q2djdwox0IlEQC0ECpXCEJcXj6JYd7Mc/fZqI2g8QfC90BWvLq4g+IQuJM
|
||||||
|
R85AepGdSl5WDADCkxAQzfbBsTZMtZW6knMPrr7gK7kn5apStRJy8em/POWxqAmZ
|
||||||
|
1WLZP/CC2EBNk8zI+vJkZpWh4bbAXYalPBr+e+QXMdBJMlm8sO/cexqQSG3A2Igj
|
||||||
|
4DO16wxQn7ZahUF/caM4Y+v4LyO8fF75h+CLW8dZ5f344hI8QoEQWzbRjeDlgmie
|
||||||
|
BMkOAiB2Q6ejLO+3iPptVyTdFGrz5PkVfiHbVaD4A3kCgYEA9JwKDSBA03EyeIuf
|
||||||
|
PFkbcdiAOr8mSrtnfaFjfICOGYmiC/FQjVVa4l984J0OLRAIr+aEHe4zKY2wcWhZ
|
||||||
|
EaJGBaBAD7yEUR5nAMP+8zVnUNgVjIvvQuDhiVzPSl7qFj4QZOpztv2zBKmneBsW
|
||||||
|
HE2M5fxBcNxewSuPVPGzEh/p9/sCgYEAx5FWnAPu6SbCpXyK9JNty/Y9d+Ls+g/R
|
||||||
|
U9zZPAeuIFDclDEteP/GfE8fRkQ50IRRoxP0DUUDnNgHkqnCk+L/yB7lg4N9LlaK
|
||||||
|
9IF3o/LLuj40LwWZa8/E+72+16XtjuBCVNcglv9Ph/maGL0sYLXOYJUYMvslJm/4
|
||||||
|
ITtZicoiwE0CgYAwN/5HPh9pTvwrBSL7q3kchRgp/HpY6v5opoLNDS513ErEXeqK
|
||||||
|
IdRLoZUlVfBwc6OywRc0KzuMqnCounAsaLey7jcSow/WSc72OKyuVs2qAx6kWQVu
|
||||||
|
QpRTFqeKOpGcltl2ez0aSoKanbL0mypNo//taj+gNuC65ZJYpViubvoZNwKBgQC7
|
||||||
|
Eti3Y1B/ql/oNPklD1ZrTaaNTWGLf2xgSrQwe5qUnHhJSlgwBsQPHzRX7/iqKH9G
|
||||||
|
eZvbHIiobDGdIBlP/Ah9lcjcIVQlLecQEJUQULb9HPZZ5cvNrnQe792H1Pj9Z+eG
|
||||||
|
FCtuoGHzOt1it2J0Pbj5/Ik0sQxAHdHhhPsIGT2+OQKBgQDWcgBLTd1VNVR0iX74
|
||||||
|
aJx5VhYCMHQ+bA8qI46tyDid/c5GibXuWf8kHUfqlfcsRzv+HGsGRvyyvYn5LimE
|
||||||
|
HB9U3xy0z5q5i+G/6iwftw+ObaVgVYr5D5ovl4DT56nlG8IOYVNA1T1Heewkk5jc
|
||||||
|
ZrLvcE6YXcAKE1y5RWaDprVmjw==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
28
src/test/resources/keys/priv.p8.key
Normal file
28
src/test/resources/keys/priv.p8.key
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDgYGhG/i+93a7W
|
||||||
|
oLK7T4SQkPH1+q1UppWxlPvQ3FunlOx8iGkTMKYx+iJEUm3E36eX466gVJYJpGPp
|
||||||
|
n+oYQ0f2QDx4zcElcqTvJhu644BBp2b1qim4f+iaWdEWUmdgIEfoePHe9NkUyIxK
|
||||||
|
m/TOYwtLSa8Rb+Vs7XePDUi9F/t/aQrHe6AwII8vKxgdvHUsqL+8A9oHIAMFZ6gL
|
||||||
|
4tasbFJqCfGaMyphW2AOnVNnkDOvLAGi2C9q9cjWnHZRhaHO34pEFnP6Wpor7rau
|
||||||
|
xSqS3WYtM1/Uxff/l5JCMB9AyRofbZSNWpsZYUesubSghzRfxcZzzk76lXPeDKS+
|
||||||
|
mb8P1Jy1AgMBAAECggEBANZCeZFBbWmLZUfC0KoZdudt4gAYTqhFIzEeqZB6vHM8
|
||||||
|
vM72nBAdJrcWut4vMG8Ne8aJPtoPq+6tMlOHx01AAlneYWCiakxOO7260EQYtwL6
|
||||||
|
zlDNG2X5jq7GEemU6u5aJRCTXbSgbyNvlrgwUQc8gS51wF2QUq8/3CmYb9tSKXEx
|
||||||
|
nRSvIT4RzMHxPfHgkSppSIJt4AQcof25f7ImVFNdv3rSlKCKhPTjMf87gBzOqlxg
|
||||||
|
xyAfIuxqpaDeY9rjywVahhthDDfRk7zt7m52huSIxg1oQuRKHY8iExCYXZgqc5Il
|
||||||
|
oelZN9ZleA+y+2LjSdpiU6yp63IUXo52246knPo7PAkCgYEA/S1E0ZDrc51trjcj
|
||||||
|
3KbHbq+O0w81WCjUzSWwtykVh1cyhZbwasqFUq2ydeYvpyq1z0sxbkUCjB9TV3G7
|
||||||
|
LvQg+cUJmUgAvJEuP9BwCb2lwCfBnqVK33T1QnbG/AynPoey5SiyZ7/AnRcujGLo
|
||||||
|
OwsttnxPw7NM1Us27bn+bF9bSgMCgYEA4uDshYJbJa6xgqkvdlZvZZnmxxjq0HP8
|
||||||
|
htR3hdh2/cMsDRr/gjUknQPsTLZKVxTQw+qJaYVBZpskWd0y5Bx5e+mhitHGr7z9
|
||||||
|
5FLz2wrWuQBFw0Lh97zcPhAiupAlkrpW2VFzc3wQvVjiJv9oaxFX3zXnFXXFN668
|
||||||
|
Px2K423JnOcCgYBP3LTFqoIrITDU8L3WP6HPT3+tIjIAkji2UbpRvJr261GbhEZo
|
||||||
|
WWP+9Z3CxQ6pG7boId8A2rH4A3WlstGJ+SZhw37IpEbfNKizJowA9prPZ3sTES7y
|
||||||
|
GBG5moAgR9mFxwZudQz53yniI8riK9z3qwoLc2Ex6WBGNAiqqq4P/3BrgwKBgEX2
|
||||||
|
urmJM3ZxYsg6SCqkCQI57ZLkOjVCL1Oc3abm0/r5XvJDqxuKK9pHxWz6of4sqxxf
|
||||||
|
jTR/JWXw3crgjbsOlOADcg+PFUhIbbslYZHgy8qNLPZD/88X7IsvCqahoRSYZgPq
|
||||||
|
PEIWtkrNyB/ij17FIGXzB2n0wXakaeTExXnQ92VtAoGAcwA5881UZWvXEdz7hoOt
|
||||||
|
BPssHkyKEPKWVhyyuyhJ4M2c1//vCdQM53RiWuNo5aU3kztS5+o2bQYm9VkmRL0o
|
||||||
|
yjR+UP61ekyK8vd+c0hG5m++Rp6SqjLSj3b4Ln7jtarM9OdXD12l1qZetBrDXnFi
|
||||||
|
bd1Cq+3lNGpl9Ut/u5DbWgw=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
9
src/test/resources/keys/public-drpc.pem
Normal file
9
src/test/resources/keys/public-drpc.pem
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvrAq82O6TeC9wrj8lfEO
|
||||||
|
XRYpUGLj/JAFhdl+pwM9lhcxwD0CUF5MFHhc9E9T8tE7cK1+kXGLtSjrggY+/Cr7
|
||||||
|
abU4RcRZPU/SN5qYtNGaCBlRj54lv+JTHTdg2KrFxf9q+yunZrQPSUCgJUY0IiJH
|
||||||
|
JcCJ9vyWCBVoub4mngZdVyxu5tOk3ZLBreSSf3OspNHBZ9I5RXlS2a9POxFpI/tz
|
||||||
|
Tefu3TR3Piwhrpyh11/nJfhhxnNyTumved7ejzoR12Yfv5V/lsw/N7ilDW2HQp9H
|
||||||
|
ss9vvuv5ToWEe8ejXNnfopBBs6hg52pQ92yTy1dIZuX101Mnds4qTw7V3Ed1ELHW
|
||||||
|
fwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
9
src/test/resources/keys/public.pem
Normal file
9
src/test/resources/keys/public.pem
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4GBoRv4vvd2u1qCyu0+E
|
||||||
|
kJDx9fqtVKaVsZT70Nxbp5TsfIhpEzCmMfoiRFJtxN+nl+OuoFSWCaRj6Z/qGENH
|
||||||
|
9kA8eM3BJXKk7yYbuuOAQadm9aopuH/omlnRFlJnYCBH6Hjx3vTZFMiMSpv0zmML
|
||||||
|
S0mvEW/lbO13jw1IvRf7f2kKx3ugMCCPLysYHbx1LKi/vAPaByADBWeoC+LWrGxS
|
||||||
|
agnxmjMqYVtgDp1TZ5AzrywBotgvavXI1px2UYWhzt+KRBZz+lqaK+62rsUqkt1m
|
||||||
|
LTNf1MX3/5eSQjAfQMkaH22UjVqbGWFHrLm0oIc0X8XGc85O+pVz3gykvpm/D9Sc
|
||||||
|
tQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
9
src/test/resources/keys/wrong-public.pem
Normal file
9
src/test/resources/keys/wrong-public.pem
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy93QvZ7evi8V9crEQzPf
|
||||||
|
NxTdIx0tvvwUcp85TsoNfHJLAD41yjJaVPwXk2mD6KuoV7hkZjoGu5ow5pPKZUKp
|
||||||
|
MzOXsfVIAvGf0xEowWaANZflrX87ZwsWLK+4mZHSCA92THQoGncBu3kUGFPjukVn
|
||||||
|
vlKUcX+4lizXGDztMD35KrPXBNXziOMSxxqnL7jlWeNYDxqmCmquwy+kqF/mCwr1
|
||||||
|
n+k6GxF1H7plSGHNZalLRB6jJUQiC6EYDYl1hPv3EdPRPwjKXQ12J6LZ+XVdOLYC
|
||||||
|
s8Q1I1ajPsT6/rg5MjXn7paWc9GrBVG57lC7R0n9zSfo64ol3DuVQognOcU3MQT9
|
||||||
|
rQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
Reference in New Issue
Block a user