Add authorization endpoint (#284)

This commit is contained in:
KirillPamPam
2023-08-23 15:57:08 +04:00
committed by GitHub
parent 31b301952a
commit 75e137161b
39 changed files with 949 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

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