eu.europa.ec.eudi.openid4vp.internal.request.RequestObjectValidator.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of eudi-lib-jvm-siop-openid4vp-kt Show documentation
Show all versions of eudi-lib-jvm-siop-openid4vp-kt Show documentation
SIOP & OpenId4VP wallet role library
/*
* 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.openid4vp.internal.request
import eu.europa.ec.eudi.openid4vp.*
import eu.europa.ec.eudi.openid4vp.RequestValidationError.*
import eu.europa.ec.eudi.openid4vp.internal.ensure
import eu.europa.ec.eudi.openid4vp.internal.ensureNotNull
import eu.europa.ec.eudi.openid4vp.internal.request.ValidatedRequestObject.*
import eu.europa.ec.eudi.prex.PresentationDefinition
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import java.net.MalformedURLException
import java.net.URI
import java.net.URL
internal sealed interface PresentationDefinitionSource {
/**
* Presentation definition is given by value (that is embedded to the authorization request)
* by the verifier
*/
data class ByValue(val presentationDefinition: PresentationDefinition) : PresentationDefinitionSource
/**
* Presentation Definition can be retrieved from the resource at the specified
* URL, rather than being passed by value.
* The Wallet will send a GET request without additional parameters.
* The resource MUST be exposed without a further need to authenticate or authorize
*/
data class ByReference(val url: URL) : PresentationDefinitionSource
/**
* When a presentation definition is pre-agreed between wallet and verifier, using
* a specific [scope]. In this case, verifier doesn't communicate the presentation definition
* neither [by value][ByValue] nor by [ByReference]. Rather, the wallet
* has been configured (via a specific scope) with a well-known definition
*/
data class Implied(val scope: Scope) : PresentationDefinitionSource
}
/**
* Represents a request object that has been validated to
* represent one of the supported requests.
* Valid in this context, means that the authorization request had the necessary
* information to represent either
* - a [SiopAuthentication], or
* - a [OpenId4VPAuthorization], or
* - a [SiopOpenId4VPAuthentication]
*
*/
internal sealed interface ValidatedRequestObject {
val client: AuthenticatedClient
val clientMetaData: UnvalidatedClientMetaData?
val nonce: String
val responseMode: ResponseMode
val state: String?
/**
* A valid SIOP authentication
*/
data class SiopAuthentication(
val idTokenType: List,
override val clientMetaData: UnvalidatedClientMetaData?,
override val client: AuthenticatedClient,
override val nonce: String,
val scope: Scope,
override val responseMode: ResponseMode,
override val state: String?,
) : ValidatedRequestObject
/**
* A valid OpenID4VP authorization
*/
data class OpenId4VPAuthorization(
val presentationDefinitionSource: PresentationDefinitionSource,
override val clientMetaData: UnvalidatedClientMetaData?,
override val client: AuthenticatedClient,
override val nonce: String,
override val responseMode: ResponseMode,
override val state: String?,
) : ValidatedRequestObject
/**
* A valid combined SIOP & OpenID4VP request
*/
data class SiopOpenId4VPAuthentication(
val idTokenType: List,
val presentationDefinitionSource: PresentationDefinitionSource,
override val clientMetaData: UnvalidatedClientMetaData?,
override val client: AuthenticatedClient,
override val nonce: String,
val scope: Scope,
override val responseMode: ResponseMode,
override val state: String?,
) : ValidatedRequestObject
}
private val jsonSupport: Json = Json { ignoreUnknownKeys = true }
/**
* Validates that the given [request] represents a valid and supported [ValidatedRequestObject]
*
* @param request The request to validate
* @return if given [request] is valid returns an appropriate [ValidatedRequestObject]. Otherwise,
* returns a [failure][Result.Failure]. Validation rules violations are reported using [AuthorizationRequestError]
* wrapped inside a [specific exception][AuthorizationRequestException]
*/
internal fun validateRequestObject(request: AuthenticatedRequest): ValidatedRequestObject {
val (client, requestObject) = request
fun scope() = requiredScope(requestObject)
val state = requestObject.state
val nonce = requiredNonce(requestObject)
val responseType = requiredResponseType(requestObject)
val responseMode = requiredResponseMode(client, requestObject)
val presentationDefinitionSource =
optionalPresentationDefinitionSource(requestObject, responseType) { scope().getOrNull() }
val clientMetaData = optionalClientMetaData(responseMode, requestObject)
val idTokenType = optionalIdTokenType(requestObject)
fun idAndVpToken() = SiopOpenId4VPAuthentication(
idTokenType,
checkNotNull(presentationDefinitionSource) { "Presentation definition missing" },
clientMetaData,
client,
nonce,
scope().getOrThrow(),
responseMode,
state,
)
fun idToken() = SiopAuthentication(
idTokenType,
clientMetaData,
client,
nonce,
scope().getOrThrow(),
responseMode,
state,
)
fun vpToken() = OpenId4VPAuthorization(
checkNotNull(presentationDefinitionSource) { "Presentation definition missing" },
clientMetaData,
client,
nonce,
responseMode,
state,
)
return when (responseType) {
ResponseType.VpAndIdToken -> {
val requestedScopes = scope().map { it.items() }.getOrElse { emptyList() }
if ("openid" in requestedScopes) idAndVpToken()
else vpToken()
}
ResponseType.IdToken -> idToken()
ResponseType.VpToken -> vpToken()
}
}
private fun optionalPresentationDefinitionSource(
authorizationRequest: UnvalidatedRequestObject,
responseType: ResponseType,
scopeProvider: () -> Scope?,
): PresentationDefinitionSource? = when (responseType) {
ResponseType.VpToken, ResponseType.VpAndIdToken ->
parsePresentationDefinitionSource(authorizationRequest, scopeProvider())
ResponseType.IdToken -> null
}
private fun optionalIdTokenType(unvalidated: UnvalidatedRequestObject): List =
unvalidated.idTokenType
?.trim()
?.split(" ")
?.map { type ->
when (type) {
"subject_signed_id_token" -> IdTokenType.SubjectSigned
"attester_signed_id_token" -> IdTokenType.AttesterSigned
else -> error("Invalid id_token_type $type")
}
}
?: emptyList()
private fun requiredResponseMode(
client: AuthenticatedClient,
unvalidated: UnvalidatedRequestObject,
): ResponseMode {
fun requiredRedirectUriAndNotProvidedResponseUri(): URI {
ensure(unvalidated.responseUri == null) { ResponseUriMustNotBeProvided.asException() }
// Redirect URI can be omitted in case of RedirectURI
// and use clientId instead
val redirectUri = unvalidated.redirectUri?.asURI { InvalidRedirectUri.asException() }?.getOrThrow()
return when (client) {
is AuthenticatedClient.RedirectUri -> {
ensure(redirectUri == null || client.clientId == redirectUri) {
InvalidRedirectUri.asException()
}
client.clientId
}
else -> ensureNotNull(redirectUri) { MissingRedirectUri.asException() }
}
}
fun requiredResponseUriAndNotProvidedRedirectUri(): URL {
ensure(unvalidated.redirectUri == null) { RedirectUriMustNotBeProvided.asException() }
val uri = unvalidated.responseUri
ensureNotNull(uri) { MissingResponseUri.asException() }
return uri.asURL { InvalidResponseUri.asException() }.getOrThrow()
}
val responseMode = when (unvalidated.responseMode) {
"direct_post" -> requiredResponseUriAndNotProvidedRedirectUri().let { ResponseMode.DirectPost(it) }
"direct_post.jwt" -> requiredResponseUriAndNotProvidedRedirectUri().let { ResponseMode.DirectPostJwt(it) }
"query" -> requiredRedirectUriAndNotProvidedResponseUri().let { ResponseMode.Query(it) }
"query.jwt" -> requiredRedirectUriAndNotProvidedResponseUri().let { ResponseMode.QueryJwt(it) }
null, "fragment" -> requiredRedirectUriAndNotProvidedResponseUri().let { ResponseMode.Fragment(it) }
"fragment.jwt" -> requiredRedirectUriAndNotProvidedResponseUri().let { ResponseMode.FragmentJwt(it) }
else -> throw UnsupportedResponseMode(unvalidated.responseMode).asException()
}
val uri = responseMode.uri()
when (client) {
is AuthenticatedClient.Preregistered -> Unit
is AuthenticatedClient.X509SanDns -> ensure(client.clientId == uri.host) {
UnsupportedResponseMode("$responseMode host doesn't match ${client.clientId}").asException()
}
is AuthenticatedClient.X509SanUri -> ensure(client.clientId == uri) {
UnsupportedResponseMode("$responseMode doesn't match ${client.clientId}").asException()
}
is AuthenticatedClient.RedirectUri -> ensure(client.clientId == uri) {
UnsupportedResponseMode("$responseMode doesn't match ${client.clientId}").asException()
}
is AuthenticatedClient.DIDClient -> Unit
is AuthenticatedClient.Attested -> {
val allowedUris = when (responseMode) {
is ResponseMode.Query,
is ResponseMode.QueryJwt,
is ResponseMode.Fragment,
is ResponseMode.FragmentJwt,
-> client.claims.redirectUris
is ResponseMode.DirectPost,
is ResponseMode.DirectPostJwt,
-> client.claims.responseUris
}
if (!allowedUris.isNullOrEmpty()) {
ensure(uri.toString() in allowedUris) {
UnsupportedResponseMode("$responseMode use a URI that is not included in attested URIs $allowedUris").asException()
}
}
}
}
return responseMode
}
/**
* Makes sure that [unvalidated] contains a not-null scope
*
* @param unvalidated the request to validate
* @return the scope or [RequestValidationError.MissingScope]
*/
private fun requiredScope(unvalidated: UnvalidatedRequestObject): Result {
val scope = unvalidated.scope?.let { Scope.make(it) }
return if (scope != null) Result.success(scope)
else MissingScope.asFailure()
}
/**
* Makes sure that [unvalidated] contains a not-null nonce
*
* @param unvalidated the request to validate
* @return the nonce or [RequestValidationError.MissingNonce]
*/
private fun requiredNonce(unvalidated: UnvalidatedRequestObject): String =
ensureNotNull(unvalidated.nonce) { MissingNonce.asException() }
/**
* Makes sure that [unvalidated] contains a supported [ResponseType].
* Function check [UnvalidatedRequestObject.responseType]
*
* @param unvalidated the request to validate
* @return the supported [ResponseType], or [RequestValidationError.MissingResponseType] if the response type is not provided
* or [RequestValidationError.UnsupportedResponseType] if the response type is not supported
*/
private fun requiredResponseType(unvalidated: UnvalidatedRequestObject): ResponseType =
when (val rt = unvalidated.responseType?.trim()) {
"vp_token" -> ResponseType.VpToken
"vp_token id_token", "id_token vp_token" -> ResponseType.VpAndIdToken
"id_token" -> ResponseType.IdToken
null -> throw MissingResponseType.asException()
else -> throw UnsupportedResponseType(rt).asException()
}
/**
* Makes sure that [unvalidated] contains a supported [PresentationDefinitionSource].
*
* @param unvalidated the request to validate
*/
private fun parsePresentationDefinitionSource(
unvalidated: UnvalidatedRequestObject,
scope: Scope?,
): PresentationDefinitionSource {
val hasPd = !unvalidated.presentationDefinition.isNullOrEmpty()
val hasPdUri = !unvalidated.presentationDefinitionUri.isNullOrEmpty()
val hasScope = null != scope
fun requiredPd() = try {
checkNotNull(unvalidated.presentationDefinition)
val pd = jsonSupport.decodeFromJsonElement(unvalidated.presentationDefinition)
PresentationDefinitionSource.ByValue(pd)
} catch (t: SerializationException) {
throw InvalidPresentationDefinition(t).asException()
}
fun requiredPdUri() = try {
checkNotNull(unvalidated.presentationDefinitionUri)
val pdUri = unvalidated.presentationDefinitionUri.asURL().getOrThrow()
PresentationDefinitionSource.ByReference(pdUri)
} catch (t: MalformedURLException) {
throw InvalidPresentationDefinitionUri.asException()
}
fun requiredScope() = PresentationDefinitionSource.Implied(scope!!)
return when {
hasPd && !hasPdUri -> requiredPd()
!hasPd && hasPdUri -> requiredPdUri()
hasScope -> requiredScope()
else -> throw MissingPresentationDefinition.asException()
}
}
private fun optionalClientMetaData(
responseMode: ResponseMode,
unvalidated: UnvalidatedRequestObject,
): UnvalidatedClientMetaData? {
val hasCMD = !unvalidated.clientMetaData.isNullOrEmpty()
fun requiredClientMetaData(): UnvalidatedClientMetaData {
checkNotNull(unvalidated.clientMetaData)
return jsonSupport.decodeFromJsonElement(unvalidated.clientMetaData)
}
fun required() = when (responseMode) {
is ResponseMode.DirectPost -> false
is ResponseMode.DirectPostJwt -> true
is ResponseMode.Fragment -> false
is ResponseMode.FragmentJwt -> true
is ResponseMode.Query -> false
is ResponseMode.QueryJwt -> true
}
return when {
hasCMD -> requiredClientMetaData()
else -> {
ensure(!required()) {
InvalidClientMetaData("Missing client metadata").asException()
}
null
}
}
}
private fun ResponseMode.uri(): URI = when (this) {
is ResponseMode.DirectPost -> responseURI.toURI()
is ResponseMode.DirectPostJwt -> responseURI.toURI()
is ResponseMode.Fragment -> redirectUri
is ResponseMode.FragmentJwt -> redirectUri
is ResponseMode.Query -> redirectUri
is ResponseMode.QueryJwt -> redirectUri
}
private enum class ResponseType {
VpToken,
IdToken,
VpAndIdToken,
}