Skip to content

Commit df63ee8

Browse files
committed
Fix login with PID
1 parent 52380b5 commit df63ee8

2 files changed

Lines changed: 138 additions & 2 deletions

File tree

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import kotlinx.datetime.format
3131
import kotlinx.datetime.format.char
3232
import kotlinx.datetime.toLocalDateTime
3333
import kotlinx.serialization.Serializable
34+
import kotlinx.serialization.Transient
3435
import kotlinx.serialization.json.Json
3536
import kotlinx.serialization.json.JsonElement
3637
import kotlinx.serialization.json.JsonObject
@@ -46,15 +47,30 @@ data class OpenId4VpUser(
4647
val credentials: Collection<ParsedCredential>?,
4748
val presentationError: String?,
4849
) : AuthenticatedPrincipal {
49-
val id = Json.encodeToString(this).sha256()
50+
@Transient
51+
val id = Json.encodeToString(OpenId4VpUserIdSource(idToken, idTokenError, credentials, presentationError)).sha256()
52+
53+
@Transient
5054
val firstname = credentials?.firstNotNullOfOrNull { it.getGivenName() } ?: "N/A"
55+
56+
@Transient
5157
val lastname = credentials?.firstNotNullOfOrNull { it.getFamilyName() } ?: "N/A"
58+
59+
@Transient
5260
val imageDataBase64 = credentials?.firstNotNullOfOrNull { it.getPortrait() }?.toImage()
5361

5462
override fun getName(): String = "$firstname $lastname ($id)"
5563

5664
}
5765

66+
@Serializable
67+
private data class OpenId4VpUserIdSource(
68+
val idToken: IdToken?,
69+
val idTokenError: String?,
70+
val credentials: Collection<ParsedCredential>?,
71+
val presentationError: String?,
72+
)
73+
5874
@Serializable
5975
data class ParsedCredential(
6076
val jwtCredential: JsonElement? = null,
@@ -245,4 +261,3 @@ private fun TokenStatusValidationResult.errorMessage(): String? = when (this) {
245261
private fun String.sha256() = runCatching {
246262
MessageDigest.getInstance("SHA-256").digest(this.encodeToByteArray()).encodeToString(Base64UrlStrict)
247263
}.getOrElse { this.hashCode().toString() }
248-
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package at.asitplus.wallet.backend
2+
3+
import at.asitplus.openid.OidcUserInfoExtended
4+
import at.asitplus.wallet.backend.config.buildSdJwtClaims
5+
import at.asitplus.wallet.eupidsdjwt.EuPidSdJwtScheme
6+
import at.asitplus.wallet.lib.agent.EphemeralKeyWithoutCert
7+
import at.asitplus.wallet.lib.agent.HolderAgent
8+
import at.asitplus.wallet.lib.agent.IssuerAgent
9+
import at.asitplus.wallet.lib.agent.RandomSource
10+
import at.asitplus.wallet.lib.agent.toStoreCredentialInput
11+
import at.asitplus.wallet.lib.data.rfc3986.toUri
12+
import at.asitplus.wallet.lib.oidvci.formUrlEncode
13+
import at.asitplus.wallet.lib.openid.AuthenticationResponseResult
14+
import at.asitplus.wallet.lib.openid.OpenId4VpHolder
15+
import io.kotest.matchers.nulls.shouldNotBeNull
16+
import io.kotest.matchers.types.shouldBeInstanceOf
17+
import io.ktor.http.Url
18+
import kotlinx.coroutines.test.runTest
19+
import kotlinx.serialization.json.buildJsonObject
20+
import kotlinx.serialization.json.put
21+
import org.junit.jupiter.api.Test
22+
import org.springframework.beans.factory.annotation.Autowired
23+
import org.springframework.boot.test.context.SpringBootTest
24+
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc
25+
import org.springframework.http.MediaType
26+
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames
27+
import org.springframework.test.web.servlet.MockMvc
28+
import org.springframework.test.web.servlet.get
29+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch
30+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get as mvcGet
31+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post as mvcPost
32+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
33+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.request
34+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
35+
import java.net.URI
36+
import kotlin.time.Clock
37+
import kotlin.time.Duration.Companion.days
38+
39+
@SpringBootTest
40+
@AutoConfigureMockMvc
41+
class PidLoginFlowTest {
42+
43+
@Autowired
44+
private lateinit var mockMvc: MockMvc
45+
46+
@Test
47+
fun `login with PID from wallet completes session login`() = runTest {
48+
val holderOid4vp = holderWithPid()
49+
val loginResult = mockMvc.perform(asyncDispatch(
50+
mockMvc.get(Paths.LoginUrl)
51+
.andExpect { request { asyncStarted() } }
52+
.andReturn()
53+
))
54+
.andExpect(status().isOk)
55+
.andReturn()
56+
val authToken = loginResult.response.getHeader("X-Auth-Token").shouldNotBeNull()
57+
val loginPidUrl = loginResult.modelAndView?.model?.get("loginPidUrl")
58+
.shouldBeInstanceOf<String>()
59+
val requestUri = Url(loginPidUrl).parameters["request_uri"].shouldNotBeNull()
60+
61+
val authnRequest = mockMvc.perform(asyncDispatch(
62+
mockMvc.perform(mvcGet(URI(requestUri).rawPath).header("X-Auth-Token", authToken))
63+
.andExpect(request().asyncStarted())
64+
.andReturn()
65+
))
66+
.andExpect(status().isOk)
67+
.andReturn()
68+
.response.contentAsString
69+
70+
val authnResponse = holderOid4vp.createAuthnResponse(authnRequest)
71+
.getOrThrow()
72+
.shouldBeInstanceOf<AuthenticationResponseResult.Post>()
73+
74+
mockMvc.perform(asyncDispatch(
75+
mockMvc.perform(
76+
mvcPost(URI(authnResponse.url).rawPath)
77+
.header("X-Auth-Token", authToken)
78+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
79+
.content(authnResponse.params.formUrlEncode())
80+
)
81+
.andExpect(request().asyncStarted())
82+
.andReturn()
83+
))
84+
.andExpect(status().isOk)
85+
86+
mockMvc.perform(asyncDispatch(
87+
mockMvc.perform(mvcGet(Paths.LoginStatusUrl).header("X-Auth-Token", authToken))
88+
.andExpect(request().asyncStarted())
89+
.andReturn()
90+
))
91+
.andExpect(status().isOk)
92+
.andExpect(jsonPath("$.authenticated").value(true))
93+
}
94+
95+
private suspend fun holderWithPid(): OpenId4VpHolder {
96+
val holderKey = EphemeralKeyWithoutCert()
97+
val holder = HolderAgent(holderKey)
98+
val now = Clock.System.now()
99+
holder.storeCredential(
100+
IssuerAgent(
101+
identifier = "https://issuer.example.com/".toUri(),
102+
randomSource = RandomSource.Default,
103+
).issueCredential(
104+
EuPidSdJwtScheme.buildSdJwtClaims(
105+
userInfo = pidUserInfo(),
106+
iss = now,
107+
exp = now + 1.days,
108+
subjectPublicKey = holderKey.publicKey,
109+
)
110+
).getOrThrow().toStoreCredentialInput()
111+
).getOrThrow()
112+
return OpenId4VpHolder(holder = holder, randomSource = RandomSource.Default)
113+
}
114+
115+
private fun pidUserInfo() = OidcUserInfoExtended.fromJsonObject(buildJsonObject {
116+
put(IdTokenClaimNames.SUB, "IFOQP3T5XYLMSDOQAEGMF52MWGMWBPXN")
117+
put("birthdate", "1983-06-04")
118+
put("given_name", "XXXOzgur")
119+
put("family_name", "XXXTuzekci")
120+
}).getOrThrow()
121+
}

0 commit comments

Comments
 (0)