Skip to content

Commit 0b74f52

Browse files
committed
Fix response serialization
1 parent a817d3d commit 0b74f52

2 files changed

Lines changed: 147 additions & 2 deletions

File tree

http/src/main/kotlin/at/asitplus/wallet/backend/controller/OpenId4VciController.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import at.asitplus.openid.ClientNonceResponse
44
import at.asitplus.openid.DisplayLogoProperties
55
import at.asitplus.openid.DisplayProperties
66
import at.asitplus.openid.OpenIdConstants
7+
import at.asitplus.signum.indispensable.josef.io.joseCompliantSerializer
78
import at.asitplus.wallet.backend.Paths
89
import at.asitplus.wallet.backend.config.BackendConfigurationProperties
910
import at.asitplus.wallet.backend.config.MetadataConfiguration
@@ -20,6 +21,7 @@ import io.github.aakira.napier.Napier
2021
import io.ktor.client.utils.CacheControl
2122
import io.ktor.http.*
2223
import jakarta.servlet.http.HttpServletRequest
24+
import kotlinx.serialization.encodeToString
2325
import org.springframework.http.HttpStatus
2426
import org.springframework.http.MediaType
2527
import org.springframework.http.MediaType.APPLICATION_JSON_VALUE
@@ -130,12 +132,11 @@ class OpenId4VciController(
130132
private suspend fun CredentialIssuer.CredentialResponse.Plain.toResponseEntity() =
131133
ResponseEntity.status(HttpStatus.OK)
132134
.contentType(MediaType.APPLICATION_JSON)
133-
.body(response)
135+
.body(joseCompliantSerializer.encodeToString(response))
134136

135137
private suspend fun CredentialIssuer.CredentialResponse.Encrypted.toResponseEntity() =
136138
ResponseEntity.status(HttpStatus.OK)
137139
.contentType(MediaType.parseMediaType(MediaTypes.Application.JWT))
138140
.body(response.serialize())
139141

140142
}
141-
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package at.asitplus.wallet.backend
2+
3+
import at.asitplus.catching
4+
import at.asitplus.openid.CredentialResponseParameters
5+
import at.asitplus.openid.TokenResponseParameters
6+
import at.asitplus.signum.indispensable.josef.JsonWebToken
7+
import at.asitplus.signum.indispensable.josef.io.joseCompliantSerializer
8+
import at.asitplus.wallet.backend.auth.SpringSecurityAuthenticationSupplier.toOidcUserInfoExtended
9+
import at.asitplus.wallet.eupidsdjwt.EuPidSdJwtScheme
10+
import at.asitplus.wallet.lib.agent.EphemeralKeyWithoutCert
11+
import at.asitplus.wallet.lib.data.ConstantIndex.CredentialRepresentation.SD_JWT
12+
import at.asitplus.wallet.lib.jws.JwsHeaderCertOrJwk
13+
import at.asitplus.wallet.lib.jws.SignJwt
14+
import at.asitplus.wallet.lib.jws.SignJwtFun
15+
import at.asitplus.wallet.lib.ktor.openid.DPoP
16+
import at.asitplus.wallet.lib.oauth2.OAuth2Client
17+
import at.asitplus.wallet.lib.oauth2.RequestInfo
18+
import at.asitplus.wallet.lib.oauth2.SimpleAuthorizationService
19+
import at.asitplus.wallet.lib.oidvci.BuildDPoPHeader
20+
import at.asitplus.wallet.lib.oidvci.CredentialIssuer
21+
import at.asitplus.wallet.lib.oidvci.WalletService
22+
import at.asitplus.wallet.lib.openid.AuthenticationResponseResult
23+
import com.benasher44.uuid.uuid4
24+
import io.kotest.matchers.nulls.shouldNotBeNull
25+
import io.kotest.matchers.string.shouldNotContain
26+
import io.kotest.matchers.types.shouldBeInstanceOf
27+
import io.ktor.http.HttpHeaders as KtorHttpHeaders
28+
import io.ktor.http.HttpMethod as KtorHttpMethod
29+
import kotlinx.coroutines.test.runTest
30+
import kotlinx.serialization.encodeToString
31+
import org.junit.jupiter.api.Test
32+
import org.springframework.beans.factory.annotation.Autowired
33+
import org.springframework.boot.test.context.SpringBootTest
34+
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc
35+
import org.springframework.http.HttpHeaders
36+
import org.springframework.http.MediaType
37+
import org.springframework.security.core.context.SecurityContextHolder
38+
import org.springframework.test.annotation.DirtiesContext
39+
import org.springframework.test.web.servlet.MockMvc
40+
import org.springframework.test.web.servlet.post
41+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch
42+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
43+
44+
@SpringBootTest
45+
@AutoConfigureMockMvc
46+
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
47+
class CredentialEndpointSerializationTest {
48+
49+
@Autowired
50+
private lateinit var mockMvc: MockMvc
51+
52+
@Autowired
53+
private lateinit var credentialIssuer: CredentialIssuer
54+
55+
@Autowired
56+
private lateinit var authorizationServer: SimpleAuthorizationService
57+
58+
@Test
59+
@WithOAuth2AuthenticationToken
60+
fun `credential endpoint serializes JsonElement credential as JSON value`() = runTest {
61+
val client = Client()
62+
val signDpop: SignJwtFun<JsonWebToken> = SignJwt(EphemeralKeyWithoutCert(), JwsHeaderCertOrJwk())
63+
val (accessToken, credentialRequest) = authorizeAndCreateCredentialRequest(client, signDpop)
64+
val credentialDpop = BuildDPoPHeader(
65+
signDpop = signDpop,
66+
url = "http://localhost${Paths.CredentialUrl}",
67+
httpMethod = KtorHttpMethod.Post.value,
68+
accessToken = accessToken.accessToken,
69+
nonce = authorizationServer.getDpopNonce(),
70+
)
71+
72+
val credentialResult = mockMvc.post(Paths.CredentialUrl) {
73+
contentType = MediaType.APPLICATION_JSON
74+
header(HttpHeaders.AUTHORIZATION, accessToken.toHttpHeaderValue())
75+
header(KtorHttpHeaders.DPoP, credentialDpop)
76+
content = joseCompliantSerializer.encodeToString(credentialRequest.request)
77+
}.andExpect { request { asyncStarted() } }.andReturn()
78+
79+
val responseBody = mockMvc.perform(asyncDispatch(credentialResult))
80+
.andExpect(status().isOk)
81+
.andReturn().response.contentAsString
82+
83+
responseBody shouldNotContain "coerceToInlineType"
84+
joseCompliantSerializer.decodeFromString<CredentialResponseParameters>(responseBody)
85+
.credentials.shouldNotBeNull()
86+
.first().credentialString.shouldNotBeNull()
87+
}
88+
89+
private suspend fun authorizeAndCreateCredentialRequest(
90+
client: Client,
91+
signDpop: SignJwtFun<JsonWebToken>,
92+
): Pair<TokenResponseParameters, WalletService.CredentialRequest.Plain> {
93+
val credentialFormat = client.oid4vciClient
94+
.selectSupportedCredentialFormat(
95+
WalletService.RequestOptions(EuPidSdJwtScheme, SD_JWT),
96+
credentialIssuer.metadata,
97+
).shouldNotBeNull()
98+
val scope = credentialFormat.scope
99+
val state = uuid4().toString()
100+
val authnRequest = client.oauth2Client.createAuthRequest(
101+
state = state,
102+
authorizationDetails = null,
103+
scope = scope,
104+
)
105+
val authorizationCode = authorizationServer.authorize(authnRequest) {
106+
catching {
107+
toOidcUserInfoExtended(SecurityContextHolder.getContext().authentication)
108+
?: error("No authenticated user")
109+
}
110+
}.getOrThrow()
111+
authorizationCode.shouldBeInstanceOf<AuthenticationResponseResult.Redirect>()
112+
val tokenRequest = client.oauth2Client.createTokenRequestParameters(
113+
authorization = OAuth2Client.AuthorizationForToken.Code(
114+
authorizationCode.params.shouldNotBeNull().code.shouldNotBeNull()
115+
),
116+
state = state,
117+
authorizationDetails = null,
118+
scope = scope,
119+
)
120+
val accessToken = authorizationServer.token(
121+
request = tokenRequest,
122+
httpRequest = RequestInfo(
123+
url = Paths.TokenUrl,
124+
method = KtorHttpMethod.Post,
125+
dpop = BuildDPoPHeader(
126+
signDpop = signDpop,
127+
url = Paths.TokenUrl,
128+
httpMethod = KtorHttpMethod.Post.value,
129+
nonce = authorizationServer.getDpopNonce(),
130+
),
131+
),
132+
).getOrThrow()
133+
val credentialNonce = credentialIssuer.nonceWithDpopNonce().getOrThrow()
134+
val credentialRequest = client.oid4vciClient.createCredential(
135+
tokenResponse = accessToken,
136+
metadata = credentialIssuer.metadata,
137+
credentialFormat = credentialFormat,
138+
clientNonce = credentialNonce.response.clientNonce,
139+
).getOrThrow().first()
140+
.shouldBeInstanceOf<WalletService.CredentialRequest.Plain>()
141+
142+
return accessToken to credentialRequest
143+
}
144+
}

0 commit comments

Comments
 (0)