Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support response_code #91

Merged
merged 5 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import eu.europa.ec.eudi.verifier.endpoint.adapter.out.jose.VerifyJarmEncryptedJ
import eu.europa.ec.eudi.verifier.endpoint.adapter.out.persistence.PresentationInMemoryRepo
import eu.europa.ec.eudi.verifier.endpoint.domain.*
import eu.europa.ec.eudi.verifier.endpoint.port.input.*
import eu.europa.ec.eudi.verifier.endpoint.port.out.cfg.CreateQueryWalletResponseRedirectUri
import eu.europa.ec.eudi.verifier.endpoint.port.out.cfg.GenerateResponseCode
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import org.springframework.context.support.beans
Expand Down Expand Up @@ -71,6 +73,8 @@ internal fun beans(clock: Clock) = beans {
bean { loadIncompletePresentationsOlderThan }
}

bean { CreateQueryWalletResponseRedirectUri.Simple }

//
// Use cases
//
Expand All @@ -85,6 +89,7 @@ internal fun beans(clock: Clock) = beans {
ref(),
WalletApi.requestJwtByReference(env.publicUrl()),
WalletApi.presentationDefinitionByReference(env.publicUrl()),
ref(),
)
}

Expand All @@ -100,7 +105,8 @@ internal fun beans(clock: Clock) = beans {
)
}

bean { PostWalletResponseLive(ref(), ref(), ref(), clock, ref()) }
bean { GenerateResponseCode.Random }
bean { PostWalletResponseLive(ref(), ref(), ref(), clock, ref(), ref(), ref()) }
bean { GenerateEphemeralEncryptionKeyPairNimbus }
bean { GetWalletResponseLive(ref()) }
bean { GetJarmJwksLive(ref()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
package eu.europa.ec.eudi.verifier.endpoint.adapter.input.web

import arrow.core.raise.either
import eu.europa.ec.eudi.verifier.endpoint.domain.Nonce
import eu.europa.ec.eudi.verifier.endpoint.domain.PresentationId
import eu.europa.ec.eudi.verifier.endpoint.domain.RequestId
import eu.europa.ec.eudi.verifier.endpoint.domain.ResponseCode
import eu.europa.ec.eudi.verifier.endpoint.port.input.*
import kotlinx.serialization.SerializationException
import org.slf4j.Logger
Expand Down Expand Up @@ -65,12 +65,10 @@ class VerifierApi(
suspend fun found(walletResponse: WalletResponseTO) = ok().json().bodyValueAndAwait(walletResponse)

val presentationId = req.presentationId()
val nonce = req.queryParam("nonce").getOrNull()?.let { Nonce(it) }
val responseCode = req.queryParam("response_code").getOrNull()?.let { ResponseCode(it) }

logger.info("Handling GetWalletResponse for $presentationId and $nonce ...")
return if (nonce == null) {
ValidationError.MissingNonce.asBadRequest()
} else when (val result = getWalletResponse(presentationId, nonce)) {
logger.info("Handling GetWalletResponse for $presentationId and response_code: $responseCode ...")
return when (val result = getWalletResponse(presentationId, responseCode)) {
is QueryResponse.NotFound -> ServerResponse.notFound().buildAndAwait()
is QueryResponse.InvalidState -> badRequest().buildAndAwait()
is QueryResponse.Found -> found(result.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,14 @@ class WalletApi(
val walletResponse = req.awaitFormData().walletResponse()
val outcome = either { postWalletResponse(walletResponse) }
outcome.fold(
ifRight = { ok().buildAndAwait() },
ifRight = { response ->
logger.info("PostWalletResponse processed")
logger.info(response.fold({ "Verifier UI will poll for Wallet Response" }, { "Wallet must redirect to ${it.redirectUri}" }))
response.fold(
ifEmpty = { ok().buildAndAwait() },
ifSome = { ok().json().bodyValueAndAwait(it) },
)
},
ifLeft = { error ->
logger.error("$error while handling post of wallet response ")
badRequest().buildAndAwait()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ value class EphemeralEncryptionKeyPairJWK(val value: String) {
companion object
}

@JvmInline
value class ResponseCode(val value: String)

sealed interface GetWalletResponseMethod {
data object Poll : GetWalletResponseMethod
data class Redirect(val redirectUriTemplate: String) : GetWalletResponseMethod
}

/**
* The entity that represents the presentation process
*/
Expand All @@ -138,6 +146,7 @@ sealed interface Presentation {
val ephemeralEcPrivateKey: EphemeralEncryptionKeyPairJWK?,
val responseMode: ResponseModeOption,
val presentationDefinitionMode: EmbedOption<RequestId>,
val getWalletResponseMethod: GetWalletResponseMethod,
) : Presentation

/**
Expand All @@ -155,6 +164,7 @@ sealed interface Presentation {
val nonce: Nonce,
val ephemeralEcPrivateKey: EphemeralEncryptionKeyPairJWK?,
val responseMode: ResponseModeOption,
val getWalletResponseMethod: GetWalletResponseMethod,
) : Presentation {
init {
require(initiatedAt.isBefore(requestObjectRetrievedAt) || initiatedAt == requestObjectRetrievedAt)
Expand All @@ -172,6 +182,7 @@ sealed interface Presentation {
requested.nonce,
requested.ephemeralEcPrivateKey,
requested.responseMode,
requested.getWalletResponseMethod,
)
}
}
Expand All @@ -189,6 +200,7 @@ sealed interface Presentation {
var submittedAt: Instant,
val walletResponse: WalletResponse,
val nonce: Nonce,
val responseCode: ResponseCode?,
) : Presentation {

init {
Expand All @@ -200,6 +212,7 @@ sealed interface Presentation {
requestObjectRetrieved: RequestObjectRetrieved,
at: Instant,
walletResponse: WalletResponse,
responseCode: ResponseCode?,
): Result<Submitted> = runCatching {
with(requestObjectRetrieved) {
Submitted(
Expand All @@ -211,6 +224,7 @@ sealed interface Presentation {
at,
walletResponse,
nonce,
responseCode,
)
}
}
Expand Down Expand Up @@ -280,8 +294,9 @@ fun Presentation.RequestObjectRetrieved.timedOut(clock: Clock): Result<Presentat
fun Presentation.RequestObjectRetrieved.submit(
clock: Clock,
walletResponse: WalletResponse,
responseCode: ResponseCode?,
): Result<Presentation.Submitted> =
Presentation.Submitted.submitted(this, clock.instant(), walletResponse)
Presentation.Submitted.submitted(this, clock.instant(), walletResponse, responseCode)

fun Presentation.Submitted.timedOut(clock: Clock): Result<Presentation.TimedOut> =
Presentation.TimedOut.timeOut(this, clock.instant())
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
package eu.europa.ec.eudi.verifier.endpoint.port.input

import eu.europa.ec.eudi.prex.PresentationSubmission
import eu.europa.ec.eudi.verifier.endpoint.domain.Nonce
import eu.europa.ec.eudi.verifier.endpoint.domain.Presentation
import eu.europa.ec.eudi.verifier.endpoint.domain.PresentationId
import eu.europa.ec.eudi.verifier.endpoint.domain.WalletResponse
import eu.europa.ec.eudi.verifier.endpoint.domain.*
import eu.europa.ec.eudi.verifier.endpoint.port.input.QueryResponse.*
import eu.europa.ec.eudi.verifier.endpoint.port.out.persistence.LoadPresentationById
import kotlinx.serialization.SerialName
Expand Down Expand Up @@ -61,20 +58,22 @@ private fun WalletResponse.toTO(): WalletResponseTO {
* Given a [PresentationId] and a [Nonce] returns the [WalletResponse]
*/
interface GetWalletResponse {
suspend operator fun invoke(presentationId: PresentationId, nonce: Nonce): QueryResponse<WalletResponseTO>
suspend operator fun invoke(presentationId: PresentationId, responseCode: ResponseCode?): QueryResponse<WalletResponseTO>
}

class GetWalletResponseLive(
private val loadPresentationById: LoadPresentationById,
) : GetWalletResponse {
override suspend fun invoke(presentationId: PresentationId, nonce: Nonce): QueryResponse<WalletResponseTO> {
override suspend fun invoke(presentationId: PresentationId, responseCode: ResponseCode?): QueryResponse<WalletResponseTO> {
return when (val presentation = loadPresentationById(presentationId)) {
null -> NotFound
is Presentation.Submitted ->
if (nonce == presentation.nonce) {
Found(presentation.walletResponse.toTO())
} else {
InvalidState
when {
presentation.responseCode != null && responseCode == null -> InvalidState
presentation.responseCode == null && responseCode != null -> InvalidState
presentation.responseCode == null && responseCode == null -> Found(presentation.walletResponse.toTO())
presentation.responseCode == responseCode -> Found(presentation.walletResponse.toTO())
else -> InvalidState
}
else -> InvalidState
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import eu.europa.ec.eudi.prex.PresentationDefinition
import eu.europa.ec.eudi.verifier.endpoint.domain.*
import eu.europa.ec.eudi.verifier.endpoint.port.out.cfg.CreateQueryWalletResponseRedirectUri
import eu.europa.ec.eudi.verifier.endpoint.port.out.cfg.GeneratePresentationId
import eu.europa.ec.eudi.verifier.endpoint.port.out.cfg.GenerateRequestId
import eu.europa.ec.eudi.verifier.endpoint.port.out.jose.GenerateEphemeralEncryptionKeyPair
Expand Down Expand Up @@ -93,6 +94,7 @@ data class InitTransactionTO(
@SerialName("response_mode") val responseMode: ResponseModeTO? = null,
@SerialName("jar_mode") val jarMode: EmbedModeTO? = null,
@SerialName("presentation_definition_mode") val presentationDefinitionMode: EmbedModeTO? = null,
@SerialName("wallet_response_redirect_uri_template") val redirectUriTemplate: String? = null,
)

/**
Expand All @@ -101,6 +103,7 @@ data class InitTransactionTO(
enum class ValidationError {
MissingPresentationDefinition,
MissingNonce,
InvalidWalletResponseTemplate,
}

/**
Expand Down Expand Up @@ -142,6 +145,7 @@ class InitTransactionLive(
private val generateEphemeralEncryptionKeyPair: GenerateEphemeralEncryptionKeyPair,
private val requestJarByReference: EmbedOption.ByReference<RequestId>,
private val presentationDefinitionByReference: EmbedOption.ByReference<RequestId>,
private val createQueryWalletResponseRedirectUri: CreateQueryWalletResponseRedirectUri,

) : InitTransaction {

Expand All @@ -153,6 +157,7 @@ class InitTransactionLive(
// if response mode is direct post jwt then generate ephemeral key
val responseMode = responseMode(initTransactionTO)
val newEphemeralEcPublicKey = ephemeralEncryptionKeyPair(responseMode)
val getWalletResponseMethod = getWalletResponseMethod(initTransactionTO)

// Initialize presentation
val requestedPresentation = Presentation.Requested(
Expand All @@ -164,6 +169,7 @@ class InitTransactionLive(
ephemeralEcPrivateKey = newEphemeralEcPublicKey,
responseMode = responseMode,
presentationDefinitionMode = presentationDefinitionMode(initTransactionTO),
getWalletResponseMethod = getWalletResponseMethod,
)
// create request, which may update presentation
val (updatedPresentation, request) = createRequest(requestedPresentation, jarMode(initTransactionTO))
Expand Down Expand Up @@ -218,6 +224,16 @@ class InitTransactionLive(
}
}

context(Raise<ValidationError>)
private fun getWalletResponseMethod(initTransactionTO: InitTransactionTO): GetWalletResponseMethod =
initTransactionTO.redirectUriTemplate
?.let { template ->
with(createQueryWalletResponseRedirectUri) {
ensure(template.validTemplate()) { ValidationError.InvalidWalletResponseTemplate }
}
GetWalletResponseMethod.Redirect(template)
} ?: GetWalletResponseMethod.Poll

/**
* Gets the [ResponseModeOption] for the provided [InitTransactionTO].
* If none has been provided, falls back to [VerifierConfig.responseModeOption].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@
*/
package eu.europa.ec.eudi.verifier.endpoint.port.input

import arrow.core.None
import arrow.core.Option
import arrow.core.raise.Raise
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import arrow.core.some
import eu.europa.ec.eudi.prex.PresentationSubmission
import eu.europa.ec.eudi.verifier.endpoint.domain.*
import eu.europa.ec.eudi.verifier.endpoint.domain.Presentation.RequestObjectRetrieved
import eu.europa.ec.eudi.verifier.endpoint.port.out.cfg.CreateQueryWalletResponseRedirectUri
import eu.europa.ec.eudi.verifier.endpoint.port.out.cfg.GenerateResponseCode
import eu.europa.ec.eudi.verifier.endpoint.port.out.jose.VerifyJarmJwtSignature
import eu.europa.ec.eudi.verifier.endpoint.port.out.persistence.LoadPresentationByRequestId
import eu.europa.ec.eudi.verifier.endpoint.port.out.persistence.StorePresentation
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Clock

/**
Expand Down Expand Up @@ -97,6 +104,11 @@ internal fun AuthorisationResponseTO.toDomain(presentation: RequestObjectRetriev
}
}

@Serializable
data class WalletResponseAcceptedTO(
@SerialName("redirect_uri") val redirectUri: String,
)

/**
* This is use case 12 of the [Presentation] process.
*
Expand All @@ -105,7 +117,7 @@ internal fun AuthorisationResponseTO.toDomain(presentation: RequestObjectRetriev
fun interface PostWalletResponse {

context(Raise<WalletResponseValidationError>)
suspend operator fun invoke(walletResponse: AuthorisationResponse)
suspend operator fun invoke(walletResponse: AuthorisationResponse): Option<WalletResponseAcceptedTO>
}

class PostWalletResponseLive(
Expand All @@ -114,10 +126,12 @@ class PostWalletResponseLive(
private val verifyJarmJwtSignature: VerifyJarmJwtSignature,
private val clock: Clock,
private val verifierConfig: VerifierConfig,
private val generateResponseCode: GenerateResponseCode,
private val createQueryWalletResponseRedirectUri: CreateQueryWalletResponseRedirectUri,
) : PostWalletResponse {

context(Raise<WalletResponseValidationError>)
override suspend operator fun invoke(walletResponse: AuthorisationResponse) {
override suspend operator fun invoke(walletResponse: AuthorisationResponse): Option<WalletResponseAcceptedTO> {
val presentation = loadPresentation(walletResponse)

// Verify the AuthorisationResponse matches what is expected for the Presentation
Expand All @@ -131,7 +145,18 @@ class PostWalletResponseLive(
}

val responseObject = responseObject(walletResponse, presentation)
submit(presentation, responseObject).also { storePresentation(it) }
val submitted = submit(presentation, responseObject).also { storePresentation(it) }

return when (val getWalletResponseMethod = presentation.getWalletResponseMethod) {
is GetWalletResponseMethod.Redirect ->
with(createQueryWalletResponseRedirectUri) {
requireNotNull(submitted.responseCode) { "ResponseCode expected in Submitted state but not found" }
val redirectUri = getWalletResponseMethod.redirectUri(submitted.responseCode)
WalletResponseAcceptedTO(redirectUri.toExternalForm()).some()
}

GetWalletResponseMethod.Poll -> None
}
}

context(Raise<WalletResponseValidationError>)
Expand Down Expand Up @@ -171,13 +196,17 @@ class PostWalletResponseLive(
}

context(Raise<WalletResponseValidationError>)
private fun submit(
private suspend fun submit(
presentation: RequestObjectRetrieved,
responseObject: AuthorisationResponseTO,
): Presentation.Submitted {
// add the wallet response to the presentation
val walletResponse = responseObject.toDomain(presentation)
return presentation.submit(clock, walletResponse).getOrThrow()
val responseCode = when (presentation.getWalletResponseMethod) {
GetWalletResponseMethod.Poll -> null
is GetWalletResponseMethod.Redirect -> generateResponseCode()
}
return presentation.submit(clock, walletResponse, responseCode).getOrThrow()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 European Commission
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.europa.ec.eudi.verifier.endpoint.port.out.cfg

import eu.europa.ec.eudi.verifier.endpoint.domain.GetWalletResponseMethod
import eu.europa.ec.eudi.verifier.endpoint.domain.ResponseCode
import java.net.URL

interface CreateQueryWalletResponseRedirectUri {

fun GetWalletResponseMethod.Redirect.redirectUri(responseCode: ResponseCode): URL =
redirectUri(redirectUriTemplate, responseCode).getOrThrow()

fun redirectUri(template: String, responseCode: ResponseCode): Result<URL>

fun String.validTemplate(): Boolean = redirectUri(this, ResponseCode("test")).isSuccess

companion object {
const val RESPONSE_CODE_PLACE_HOLDER = "{RESPONSE_CODE}"
val Simple: CreateQueryWalletResponseRedirectUri = object : CreateQueryWalletResponseRedirectUri {
override fun redirectUri(template: String, responseCode: ResponseCode): Result<URL> = runCatching {
require(template.contains(RESPONSE_CODE_PLACE_HOLDER)) { "Expected response_code place holder not found in template" }
val url = template.replace(RESPONSE_CODE_PLACE_HOLDER, responseCode.value)
URL(url)
}
}
}
}
Loading