Skip to content

Commit

Permalink
Move query params into claims
Browse files Browse the repository at this point in the history
  • Loading branch information
Ifropc committed Mar 22, 2024
1 parent 3a53434 commit 4bcb148
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,42 @@ import kotlin.time.Duration.Companion.minutes
import kotlinx.datetime.Clock
import org.stellar.walletsdk.horizon.AccountKeyPair
import org.stellar.walletsdk.horizon.SigningKeyPair
import org.stellar.walletsdk.util.Util.toJava
import org.stellar.walletsdk.util.toJava

/** Header signer to sign JWT for GET /Auth request. */
interface AuthHeaderSigner {
fun createToken(url: String, clientDomain: String?, issuer: AccountKeyPair?): String
fun createToken(
claims: Map<String, String>,
clientDomain: String?,
issuer: AccountKeyPair?
): String
}

/** Header signer signing JWT for GET /Auth with a main custodial key */
open class DefaultAuthHeaderSigner(val expiration: Duration = 15.minutes) : AuthHeaderSigner {
override fun createToken(url: String, clientDomain: String?, issuer: AccountKeyPair?): String {
if (issuer == null) {
throw IllegalArgumentException("Default signer can't sign headers for client domain.")
}
if (issuer !is SigningKeyPair) {
throw IllegalArgumentException(
"SigningKeyPair must be provided to .auth() method in order to sign headers"
)
override fun createToken(
claims: Map<String, String>,
clientDomain: String?,
issuer: AccountKeyPair?
): String {
require(issuer != null) { "Default signer can't sign headers for client domain." }
require(issuer is SigningKeyPair) {
"SigningKeyPair must be provided to .auth() method in order to sign headers"
}

val timeExp = Instant.ofEpochSecond(Clock.System.now().plus(expiration).epochSeconds)
val builder = createBuilder(timeExp, url)
val builder = createBuilder(timeExp, claims)

builder.signWith(issuer.toJava().private, Jwts.SIG.EdDSA)

return builder.compact()
}

fun createBuilder(timeExp: Instant, url: String): JwtBuilder {
fun createBuilder(timeExp: Instant, claims: Map<String, String>): JwtBuilder {
return Jwts.builder()
.id(UUID.randomUUID().toString())
.issuedAt(Date.from(Instant.now()))
.expiration(Date.from(timeExp))
.claim("url", url)
.also { builder -> claims.forEach { builder.claim(it.key, it.value) } }
}
}
35 changes: 26 additions & 9 deletions wallet-sdk/src/main/kotlin/org/stellar/walletsdk/auth/Sep10.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,25 +95,36 @@ internal constructor(
val url = URLBuilder(webAuthEndpoint)

// Add required query params
url.parameters.append("account", account.address)
url.parameters.append("home_domain", homeDomain)
val parameters = mutableMapOf<String, String>()
parameters["account"] = account.address
parameters["home_domain"] = homeDomain

if (memoId != null) {
url.parameters.append("memo", memoId)
parameters["memo"] = memoId
}

if (!clientDomain.isNullOrBlank()) {
url.parameters.append("client_domain", clientDomain)
parameters["client_domain"] = clientDomain
}

parameters.forEach { url.parameters.append(it.key, it.value) }

log.debug {
"Challenge request: account = $account, memo = $memoId, client_domain = $clientDomain"
}

val urlString = url.build().toString()
val token = createAuthSignToken(account, urlString, clientDomain, authHeaderSigner)
val token =
createAuthSignToken(
account,
url.host,
url.encodedPath,
parameters,
clientDomain,
authHeaderSigner
)

val jsonResponse = httpClient.authGetStringToken<ChallengeResponse>(urlString, token)
val jsonResponse =
httpClient.authGetStringToken<ChallengeResponse>(url.build().toString(), token)

if (jsonResponse.transaction.isBlank()) {
throw MissingTransactionException
Expand Down Expand Up @@ -197,13 +208,19 @@ internal constructor(

internal fun createAuthSignToken(
account: AccountKeyPair,
urlString: String,
host: String,
path: String,
parameters: Map<String, String>,
clientDomain: String? = null,
authHeaderSigner: AuthHeaderSigner? = null
): String? {
if (authHeaderSigner != null) {
// For noncustodial issuer is unknown -> comes from SEP-1 toml file
val issuer = if (clientDomain == null) account else null
return authHeaderSigner.createToken(urlString, clientDomain, issuer)
val claims = parameters.toMutableMap()
claims["host"] = host
claims["path"] = path
return authHeaderSigner.createToken(claims, clientDomain, issuer)
}
return null
}
42 changes: 21 additions & 21 deletions wallet-sdk/src/main/kotlin/org/stellar/walletsdk/util/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,27 +138,6 @@ internal object Util {
throw AnchorRequestException("Failed to deserialize string: $this", e)
}
}

internal fun SigningKeyPair.toJava(): java.security.KeyPair {
Security.addProvider(BouncyCastleProvider())

val factory: KeyFactory = KeyFactory.getInstance("Ed25519")
val privKeyInfo =
PrivateKeyInfo(
AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519),
DEROctetString(StrKey.decodeEd25519SecretSeed(this.keyPair.secretSeed))
)
val pkcs8KeySpec = PKCS8EncodedKeySpec(privKeyInfo.getEncoded())

val pubKeyInfo =
SubjectPublicKeyInfo(AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), this.publicKey)
val x509KeySpec = X509EncodedKeySpec(pubKeyInfo.getEncoded())
var jcaPublicKey = factory.generatePublic(x509KeySpec)

val private: PrivateKey = factory.generatePrivate(pkcs8KeySpec)

return java.security.KeyPair(jcaPublicKey, private)
}
}

fun Duration.toTimeBounds(): TimeBounds {
Expand Down Expand Up @@ -189,3 +168,24 @@ fun String.toAssetId(): AssetId {
}
throw InvalidJsonException("Unknown scheme", str)
}

fun SigningKeyPair.toJava(): java.security.KeyPair {
Security.addProvider(BouncyCastleProvider())

val factory: KeyFactory = KeyFactory.getInstance("Ed25519")
val privKeyInfo =
PrivateKeyInfo(
AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519),
DEROctetString(StrKey.decodeEd25519SecretSeed(this.keyPair.secretSeed))
)
val pkcs8KeySpec = PKCS8EncodedKeySpec(privKeyInfo.getEncoded())

val pubKeyInfo =
SubjectPublicKeyInfo(AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), this.publicKey)
val x509KeySpec = X509EncodedKeySpec(pubKeyInfo.getEncoded())
val jcaPublicKey = factory.generatePublic(x509KeySpec)

val private: PrivateKey = factory.generatePrivate(pkcs8KeySpec)

return java.security.KeyPair(jcaPublicKey, private)
}
53 changes: 40 additions & 13 deletions wallet-sdk/src/test/kotlin/org/stellar/walletsdk/AuthTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.stellar.walletsdk

import io.jsonwebtoken.Jwts
import io.ktor.http.*
import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
Expand All @@ -13,7 +14,7 @@ import org.stellar.walletsdk.auth.DefaultAuthHeaderSigner
import org.stellar.walletsdk.auth.createAuthSignToken
import org.stellar.walletsdk.horizon.AccountKeyPair
import org.stellar.walletsdk.horizon.SigningKeyPair
import org.stellar.walletsdk.util.Util.toJava
import org.stellar.walletsdk.util.toJava

internal class AuthTest : SuspendTest() {
private val cfg = TestWallet.cfg
Expand Down Expand Up @@ -72,26 +73,39 @@ internal class AuthTest : SuspendTest() {
fun headerSignerTest() {
val kp = SigningKeyPair.fromSecret("SBPPLU2KO3PDBLSDFIWARQSW5SAOIHTJDUQIWN3BQS7KPNMVUDSU37QO")
val signer = DefaultAuthHeaderSigner()
val token = signer.createToken("test", null, kp)
val token = signer.createToken(mapOf("testkey" to "test"), null, kp)

val claims = Jwts.parser().verifyWith(kp.toJava().public).build().parseSignedClaims(token)

assertEquals("test", claims.payload["url"])
assertEquals("test", claims.payload["testkey"])
}

@Test
fun `create token test custodial`() {
val signer = DefaultAuthHeaderSigner(100000.days)
val accountKp =
SigningKeyPair.fromSecret("SBPPLU2KO3PDBLSDFIWARQSW5SAOIHTJDUQIWN3BQS7KPNMVUDSU37QO")
val url =
"https://auth.example.com/?account=GCXXH6AYJUVTDGIHT42OZNMF3LHCV4DOKCX6HHDKWECUZYXDZSWZN6HS&memo=1234567"
val token = createAuthSignToken(accountKp, url, authHeaderSigner = signer)
val memo = "1234567"
val url = "https://auth.example.com/?account=${accountKp.address}&memo=$memo"
val builder = URLBuilder(url)
val params = mapOf("account" to accountKp.address, "memo" to memo)

val token =
createAuthSignToken(
accountKp,
builder.host,
builder.encodedPath,
params,
authHeaderSigner = signer
)

val claims =
Jwts.parser().verifyWith(accountKp.toJava().public).build().parseSignedClaims(token)

assertEquals(url, claims.payload["url"])
assertEquals(builder.host, claims.payload["host"])
assertEquals(builder.encodedPath, claims.payload["path"])
assertEquals(accountKp.address, claims.payload["account"])
assertEquals(memo, claims.payload["memo"])

println(token)
}
Expand All @@ -106,24 +120,37 @@ internal class AuthTest : SuspendTest() {
val signer =
object : DefaultAuthHeaderSigner(100000.days) {
override fun createToken(
url: String,
claims: Map<String, String>,
clientDomain: String?,
issuer: AccountKeyPair?
): String {
val timeExp = Instant.ofEpochSecond(Clock.System.now().plus(expiration).epochSeconds)
val builder = createBuilder(timeExp, url)
val builder = createBuilder(timeExp, claims)

builder.signWith(domainKp.toJava().private, Jwts.SIG.EdDSA)

return builder.compact()
}
}
val url =
"https://auth.example.com/?account=GCIBUCGPOHWMMMFPFTDWBSVHQRT4DIBJ7AD6BZJYDITBK2LCVBYW7HUQ&client_domain=example-wallet.stellar.org"
val token = createAuthSignToken(accountKp, url, authHeaderSigner = signer)
val clientDomain = "example-wallet.stellar.org"
val url = "https://auth.example.com/?account=${accountKp.address}&client_domain=${clientDomain}"
val builder = URLBuilder(url)
val params = mapOf("account" to accountKp.address, "client_domain" to clientDomain)

val token =
createAuthSignToken(
accountKp,
builder.host,
builder.encodedPath,
params,
authHeaderSigner = signer
)
val claims = Jwts.parser().verifyWith(domainKp.toJava().public).build().parseSignedClaims(token)

assertEquals(url, claims.payload["url"])
assertEquals(builder.host, claims.payload["host"])
assertEquals(builder.encodedPath, claims.payload["path"])
assertEquals(accountKp.address, claims.payload["account"])
assertEquals(clientDomain, claims.payload["client_domain"])

println(token)
}
Expand Down

0 comments on commit 4bcb148

Please sign in to comment.