Skip to content

Commit

Permalink
Improve examples (#235)
Browse files Browse the repository at this point in the history
  • Loading branch information
babisRoutis authored May 30, 2024
1 parent efa4621 commit da68d47
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 171 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ sealed interface SubmittedRequest : java.io.Serializable {
* @param cNonce The c_nonce provided from issuer along the error
* @param errorDescription Description of the error that caused the failure
*/
class InvalidProof(
data class InvalidProof(
val cNonce: CNonce,
val errorDescription: String? = null,
) : Errored
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,13 @@ object CryptoGenerator {
val bindingKey = JwtBindingKey.Jwk(keyPair.toPublicJWK())
return PopSigner.jwtPopSigner(keyPair, JWSAlgorithm.ES256, bindingKey)
}

// TODO make this smarter
// taking into account the proofTypeMeta data
fun popSigner(proofTypeMeta: ProofTypeMeta): PopSigner? =
when (proofTypeMeta) {
ProofTypeMeta.Cwt -> null
is ProofTypeMeta.Jwt -> ecProofSigner()
ProofTypeMeta.LdpVp -> null
}
}
102 changes: 80 additions & 22 deletions src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/Commons.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,100 @@
package eu.europa.ec.eudi.openid4vci.examples

import com.nimbusds.jose.jwk.Curve
import eu.europa.ec.eudi.openid4vci.CredentialResponseEncryptionPolicy
import eu.europa.ec.eudi.openid4vci.CryptoGenerator
import eu.europa.ec.eudi.openid4vci.KeyGenerationConfig
import eu.europa.ec.eudi.openid4vci.OpenId4VCIConfig
import eu.europa.ec.eudi.openid4vci.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.apache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.cookies.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.apache.http.conn.ssl.NoopHostnameVerifier
import org.apache.http.conn.ssl.TrustSelfSignedStrategy
import org.apache.http.ssl.SSLContextBuilder
import org.jsoup.Jsoup
import org.jsoup.nodes.FormElement
import java.net.URI
import java.net.URL

const val PID_SdJwtVC_config_id = "eu.europa.ec.eudiw.pid_vc_sd_jwt"
const val PID_MsoMdoc_config_id = "eu.europa.ec.eudiw.pid_mso_mdoc"
const val MDL_config_id = "org.iso.18013.5.1.mDL"
internal object PidDevIssuer {
private const val BASE_URL = "https://dev.issuer-backend.eudiw.dev"
private const val WALLET_CLIENT_ID = "wallet-dev"
private val WalletRedirectURI = URI.create("urn:ietf:wg:oauth:2.0:oob")
val IssuerId = CredentialIssuerId(BASE_URL).getOrThrow()
val TestUser = ActingUser("tneal", "password")

internal class ActingUser(
val username: String,
val password: String,
)
internal class ActingUser(
val username: String,
val password: String,
)

val DefaultProofSignersMap = mapOf(
PID_SdJwtVC_config_id to CryptoGenerator.rsaProofSigner(),
PID_MsoMdoc_config_id to CryptoGenerator.ecProofSigner(),
MDL_config_id to CryptoGenerator.ecProofSigner(),
)
val PID_SdJwtVC_config_id = CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt")
val PID_MsoMdoc_config_id = CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_mso_mdoc")
val MDL_config_id = CredentialConfigurationIdentifier("org.iso.18013.5.1.mDL")

val AllCredentialConfigurationIds = listOf(
PID_SdJwtVC_config_id,
PID_MsoMdoc_config_id,
MDL_config_id,
)

val DefaultOpenId4VCIConfig = OpenId4VCIConfig(
clientId = "wallet-dev",
authFlowRedirectionURI = URI.create("urn:ietf:wg:oauth:2.0:oob"),
keyGenerationConfig = KeyGenerationConfig(Curve.P_256, 2048),
credentialResponseEncryptionPolicy = CredentialResponseEncryptionPolicy.SUPPORTED,
dPoPSigner = CryptoGenerator.ecProofSigner(),
val Cfg = OpenId4VCIConfig(
clientId = WALLET_CLIENT_ID,
authFlowRedirectionURI = WalletRedirectURI,
keyGenerationConfig = KeyGenerationConfig(Curve.P_256, 2048),
credentialResponseEncryptionPolicy = CredentialResponseEncryptionPolicy.SUPPORTED,
dPoPSigner = CryptoGenerator.ecProofSigner(),
)

suspend fun loginUserAndGetAuthCode(
preparedAuthorizationCodeRequest: AuthorizationRequestPrepared,
actingUser: ActingUser,
): Pair<String, String>? = coroutineScope {
suspend fun extractASLoginUrl(html: String): URL = withContext(Dispatchers.IO) {
val form = Jsoup.parse(html).body().getElementById("kc-form-login") as FormElement
val action = form.attr("action")
URL(action)
}

val response = createHttpClient().use { client ->
val loginUrl = async {
val url = preparedAuthorizationCodeRequest.authorizationCodeURL.value
val loginHtml = client.get(url).body<String>()
extractASLoginUrl(loginHtml)
}
client.submitForm(
url = loginUrl.await().toString(),
formParameters = Parameters.build {
append("username", actingUser.username)
append("password", actingUser.password)
},
)
}
val redirectLocation = response.headers["Location"].toString()
with(URLBuilder(redirectLocation)) {
parameters["code"] to parameters["state"]
}.toNullable()
}

private fun <A, B> Pair<A?, B?>.toNullable(): Pair<A, B>? {
return if (first != null && second != null) first!! to second!!
else null
}
}

val DefaultProofSignersMap = mapOf(
PidDevIssuer.PID_SdJwtVC_config_id to CryptoGenerator.rsaProofSigner(),
PidDevIssuer.PID_MsoMdoc_config_id to CryptoGenerator.ecProofSigner(),
PidDevIssuer.MDL_config_id to CryptoGenerator.ecProofSigner(),
)

internal fun createHttpClient(enableLogging: Boolean = true): HttpClient = HttpClient(Apache) {
Expand All @@ -70,6 +127,7 @@ internal fun createHttpClient(enableLogging: Boolean = true): HttpClient = HttpC
}
engine {
customizeClient {
followRedirects = true
setSSLContext(
SSLContextBuilder.create().loadTrustMaterial(TrustSelfSignedStrategy()).build(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,20 @@ package eu.europa.ec.eudi.openid4vci.examples

import eu.europa.ec.eudi.openid4vci.*
import eu.europa.ec.eudi.openid4vci.internal.ensure
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import kotlinx.coroutines.runBlocking
import org.jsoup.Jsoup
import org.jsoup.nodes.FormElement
import java.net.URL

private val actingUser = ActingUser("tneal", "password")

fun main(): Unit = runBlocking {
val credentialOfferUrl = "eudi-openid4ci://?credential_offer=%7B%22" +
"credential_issuer%22:%22https://dev.issuer-backend.eudiw.dev%22,%22" +
"credential_configuration_ids%22:[%22$PID_MsoMdoc_config_id%22," +
"%22$PID_SdJwtVC_config_id%22,%22$MDL_config_id%22]," +
"credential_configuration_ids%22:[%22${PidDevIssuer.PID_MsoMdoc_config_id.value}%22," +
"%22${PidDevIssuer.PID_SdJwtVC_config_id.value}%22,%22${PidDevIssuer.MDL_config_id.value}%22]," +
"%22grants%22:%7B%22authorization_code%22:%7B%22" +
"authorization_server%22:%22https://dev.auth.eudiw.dev/realms/pid-issuer-realm%22%7D%7D%7D"

println("[[Scenario: Issuance based on credential offer url: $credentialOfferUrl]] ")

val issuer = Issuer.make(
config = DefaultOpenId4VCIConfig,
config = PidDevIssuer.Cfg,
credentialOfferUri = credentialOfferUrl,
ktorHttpClientFactory = ::createHttpClient,
).getOrThrow()
Expand All @@ -50,7 +41,7 @@ fun main(): Unit = runBlocking {
}

authorizationLog("Using authorized code flow to authorize")
val authorizedRequest = authorizeRequestWithAuthCodeUseCase(issuer, actingUser)
val authorizedRequest = authorizeRequestWithAuthCodeUseCase(issuer, PidDevIssuer.TestUser)
authorizationLog("Authorization retrieved: $authorizedRequest")

val offerCredentialConfIds = credentialOffer.credentialConfigurationIdentifiers
Expand All @@ -75,18 +66,20 @@ fun main(): Unit = runBlocking {
}
}

private suspend fun authorizeRequestWithAuthCodeUseCase(issuer: Issuer, actingUser: ActingUser): AuthorizedRequest =
private suspend fun authorizeRequestWithAuthCodeUseCase(
issuer: Issuer,
actingUser: PidDevIssuer.ActingUser,
): AuthorizedRequest =
with(issuer) {
authorizationLog("Preparing authorization code request")

val prepareAuthorizationCodeRequest = issuer.prepareAuthorizationRequest().getOrThrow()

authorizationLog("Get authorization code URL is: ${prepareAuthorizationCodeRequest.authorizationCodeURL.value}")

val (authorizationCode, serverState) = loginUserAndGetAuthCode(
prepareAuthorizationCodeRequest.authorizationCodeURL.value,
actingUser,
) ?: error("Could not retrieve authorization code")
val (authorizationCode, serverState) =
PidDevIssuer.loginUserAndGetAuthCode(prepareAuthorizationCodeRequest, actingUser)
?: error("Could not retrieve authorization code")

authorizationLog("Authorization code retrieved: $authorizationCode")

Expand All @@ -100,33 +93,6 @@ private suspend fun authorizeRequestWithAuthCodeUseCase(issuer: Issuer, actingUs
authorizedRequest
}

private suspend fun loginUserAndGetAuthCode(getAuthorizationCodeUrl: URL, actingUser: ActingUser): Pair<String, String>? {
return createHttpClient().use { client ->
val loginUrl = client.get(getAuthorizationCodeUrl).body<String>().extractASLoginUrl()

val formParameters = mapOf(
"username" to actingUser.username,
"password" to actingUser.password,
)
val response = client.submitForm(
url = loginUrl.toString(),
formParameters = Parameters.build {
formParameters.entries.forEach { append(it.key, it.value) }
},
)
val redirectLocation = response.headers["Location"].toString()
with(URLBuilder(redirectLocation)) {
parameters["code"] to parameters["state"]
}.toNullable()
}
}

private fun String.extractASLoginUrl(): URL {
val form = Jsoup.parse(this).body().getElementById("kc-form-login") as FormElement
val action = form.attr("action")
return URL(action)
}

private suspend fun submitProvidingNoProofs(
issuer: Issuer,
authorized: AuthorizedRequest.NoProofRequired,
Expand Down Expand Up @@ -156,7 +122,7 @@ private suspend fun submitProvidingProofs(
credentialConfigurationId: CredentialConfigurationIdentifier,
): String {
with(issuer) {
val proofSigner = DefaultProofSignersMap[credentialConfigurationId.value]
val proofSigner = DefaultProofSignersMap[credentialConfigurationId]
?: error("No signer found for credential $credentialConfigurationId")

val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null)
Expand All @@ -176,7 +142,7 @@ private suspend fun handleSuccess(
submittedRequest: SubmittedRequest.Success,
issuer: Issuer,
authorized: AuthorizedRequest,
) = when (val issuedCredential = submittedRequest.credentials[0]) {
): String = when (val issuedCredential = submittedRequest.credentials[0]) {
is IssuedCredential.Issued -> issuedCredential.credential
is IssuedCredential.Deferred -> {
handleDeferred(issuer, authorized, issuedCredential)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private suspend fun submitProvidingProofs(
credentialConfigurationId: CredentialConfigurationIdentifier,
): String {
with(issuer) {
val proofSigner = DefaultProofSignersMap[credentialConfigurationId.value]
val proofSigner = DefaultProofSignersMap[credentialConfigurationId]
?: error("No signer found for credential $credentialConfigurationId")

val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null)
Expand Down
Loading

0 comments on commit da68d47

Please sign in to comment.