Add authorization endpoint (#284)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user