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

Dpop nonce #1

Open
wants to merge 3 commits into
base: workingBranch
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ bin/
.vscode/

### Mac OS ###
.DS_Store
.DS_Store
local.properties
44 changes: 35 additions & 9 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,11 @@ plugins {
alias(libs.plugins.sonarqube)
alias(libs.plugins.dependency.check)
alias(libs.plugins.maven.publish)
id("maven-publish")
}

repositories {
mavenCentral()
mavenLocal()
maven {
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
mavenContent { snapshotsOnly() }
}
configurations.all {
resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS)
}

dependencies {
Expand All @@ -47,9 +43,33 @@ dependencies {
testImplementation(libs.logback.classic)
testImplementation(libs.cbor)
}
signing {
setRequired(project.hasProperty("signing.keyId"))
sign {
publishing.publications
}
}

afterEvaluate {
publishing {
publications {
create<MavenPublication>("mavenJava") {
groupId = "com.github.TICESoftware"
artifactId = "eudi-lib-jvm-openid4vci-kt"
version = "0.0.1"

from(components["java"])
}
}
}
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get()))
}
sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
}

kotlin {
Expand Down Expand Up @@ -103,9 +123,15 @@ tasks.withType<DokkaTask>().configureEach {
// contains descriptions for the module and the packages
includes.from("Module.md")

documentedVisibilities.set(setOf(DokkaConfiguration.Visibility.PUBLIC, DokkaConfiguration.Visibility.PROTECTED))
documentedVisibilities.set(
setOf(
DokkaConfiguration.Visibility.PUBLIC,
DokkaConfiguration.Visibility.PROTECTED
)
)

val remoteSourceUrl = System.getenv()["GIT_REF_NAME"]?.let { URL("${Meta.BASE_URL}/tree/$it/src") }
val remoteSourceUrl =
System.getenv()["GIT_REF_NAME"]?.let { URL("${Meta.BASE_URL}/tree/$it/src") }
remoteSourceUrl
?.let {
sourceLink {
Expand Down
5 changes: 5 additions & 0 deletions jitpack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
jdk:
- openjdk17
before_install:
- sdk install java 17.0.1-open
- sdk use java 17.0.1-open
19 changes: 17 additions & 2 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version("0.6.0")
id("org.gradle.toolchains.foojay-resolver-convention") version ("0.6.0")
}
rootProject.name = "eudi-lib-jvm-openid4vci-kt"

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenLocal()
google()
mavenCentral()
maven {
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
mavenContent { snapshotsOnly() }
}
maven { url = uri("https://jitpack.io") }
}
}

rootProject.name = "eudi-lib-jvm-openid4vci-kt"
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ sealed interface AuthorizedRequest : java.io.Serializable {
* @return The new state of the request.
*/
fun withCNonce(cNonce: CNonce): ProofRequired =
ProofRequired(accessToken, refreshToken, cNonce, credentialIdentifiers, timestamp)
ProofRequired(accessToken, refreshToken, cNonce, credentialIdentifiers, timestamp, "no dpopNonce")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"no dpopNonce" is used twice, could become a constant.


fun withRefreshedAccessToken(
refreshedAccessToken: AccessToken,
Expand Down Expand Up @@ -110,6 +110,7 @@ sealed interface AuthorizedRequest : java.io.Serializable {
val cNonce: CNonce,
override val credentialIdentifiers: Map<CredentialConfigurationIdentifier, List<CredentialIdentifier>>?,
override val timestamp: Instant,
val dpopNonce: String
) : AuthorizedRequest
}

Expand Down Expand Up @@ -143,6 +144,7 @@ interface AuthorizeIssuance {
suspend fun AuthorizationRequestPrepared.authorizeWithAuthorizationCode(
authorizationCode: AuthorizationCode,
serverState: String,
dpopNonce: String
): Result<AuthorizedRequest>

/**
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ interface RequestIssuance {
suspend fun AuthorizedRequest.ProofRequired.requestSingle(
requestPayload: IssuanceRequestPayload,
proofSigner: PopSigner,
dpopNonce: String
): Result<SubmissionOutcome>

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal data class TokenResponse(
val cNonce: CNonce?,
val authorizationDetails: Map<CredentialConfigurationIdentifier, List<CredentialIdentifier>> = emptyMap(),
val timestamp: Instant,
val dpopNonce: String
)

internal class AuthorizeIssuanceImpl(
Expand Down Expand Up @@ -81,11 +82,12 @@ internal class AuthorizeIssuanceImpl(
override suspend fun AuthorizationRequestPrepared.authorizeWithAuthorizationCode(
authorizationCode: AuthorizationCode,
serverState: String,
dpopNonce: String
): Result<AuthorizedRequest> =
runCatching {
ensure(serverState == state) { InvalidAuthorizationState() }
val tokenResponse =
tokenEndpointClient.requestAccessTokenAuthFlow(authorizationCode, pkceVerifier).getOrThrow()
tokenEndpointClient.requestAccessTokenAuthFlow(authorizationCode, pkceVerifier, dpopNonce).getOrThrow()
authorizedRequest(credentialOffer, tokenResponse)
}

Expand Down Expand Up @@ -128,7 +130,7 @@ internal fun TxCode.validate(txCode: String?) {

internal fun authorizedRequest(
offer: CredentialOffer,
tokenResponse: TokenResponse,
tokenResponse: TokenResponse
): AuthorizedRequest {
val offerRequiresProofs = offer.credentialConfigurationIdentifiers.any {
val credentialConfiguration = offer.credentialIssuerMetadata.credentialConfigurationsSupported[it]
Expand All @@ -137,7 +139,7 @@ internal fun authorizedRequest(
val (accessToken, refreshToken, cNonce, authorizationDetails, timestamp) = tokenResponse
return when {
cNonce != null && offerRequiresProofs ->
ProofRequired(accessToken, refreshToken, cNonce, authorizationDetails, timestamp)
ProofRequired(accessToken, refreshToken, cNonce, authorizationDetails, timestamp, tokenResponse.dpopNonce)

else ->
NoProofRequired(accessToken, refreshToken, authorizationDetails, timestamp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ internal fun HttpRequestBuilder.bearerOrDPoPAuth(
htu: URL,
htm: Htm,
accessToken: AccessToken,
dpopNonce: String? = null
) {
when (accessToken) {
is AccessToken.Bearer -> {
Expand All @@ -146,7 +147,7 @@ internal fun HttpRequestBuilder.bearerOrDPoPAuth(

is AccessToken.DPoP -> {
if (factory != null) {
dpop(factory, htu, htm, accessToken, nonce = null)
dpop(factory, htu, htm, accessToken, dpopNonce)
dpopAuth(accessToken)
} else {
bearerAuth(AccessToken.Bearer(accessToken.accessToken, accessToken.expiresIn))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ internal class RequestIssuanceImpl(
override suspend fun AuthorizedRequest.ProofRequired.requestSingle(
requestPayload: IssuanceRequestPayload,
proofSigner: PopSigner,
dpopNonce: String
): Result<SubmissionOutcome> = runCatching {
placeIssuanceRequest(accessToken) {
placeIssuanceRequest(accessToken, dpopNonce) {
singleRequest(requestPayload, proofFactory(proofSigner, cNonce), credentialIdentifiers)
}
}
Expand Down Expand Up @@ -175,13 +176,14 @@ internal class RequestIssuanceImpl(

private suspend fun placeIssuanceRequest(
token: AccessToken,
dpopNonce: String? = null,
issuanceRequestSupplier: suspend () -> CredentialIssuanceRequest,
): SubmissionOutcome {
fun handleIssuanceFailure(error: Throwable): SubmissionOutcome.Errored =
submitRequestFromError(error) ?: throw error
return when (val credentialRequest = issuanceRequestSupplier()) {
is CredentialIssuanceRequest.SingleRequest -> {
credentialEndpointClient.placeIssuanceRequest(token, credentialRequest).fold(
credentialEndpointClient.placeIssuanceRequest(token, credentialRequest, dpopNonce).fold(
onSuccess = { SubmissionOutcome.Success(it.credentials, it.cNonce) },
onFailure = { handleIssuanceFailure(it) },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ internal class CredentialEndpointClient(
suspend fun placeIssuanceRequest(
accessToken: AccessToken,
request: CredentialIssuanceRequest.SingleRequest,
dpopNonce: String? = null
): Result<CredentialIssuanceResponse> = runCatching {
ktorHttpClientFactory().use { client ->
val url = credentialEndpoint.value
val response = client.post(url) {
bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken)
bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken, dpopNonce)
contentType(ContentType.Application.Json)
setBody(CredentialRequestTO.from(request))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,29 @@
*/
package eu.europa.ec.eudi.openid4vci.internal.http

import eu.europa.ec.eudi.openid4vci.*
import eu.europa.ec.eudi.openid4vci.AccessToken
import eu.europa.ec.eudi.openid4vci.AuthorizationCode
import eu.europa.ec.eudi.openid4vci.CIAuthorizationServerMetadata
import eu.europa.ec.eudi.openid4vci.CNonce
import eu.europa.ec.eudi.openid4vci.ClientId
import eu.europa.ec.eudi.openid4vci.CredentialConfigurationIdentifier
import eu.europa.ec.eudi.openid4vci.CredentialIdentifier
import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.AccessTokenRequestFailed
import eu.europa.ec.eudi.openid4vci.Grants.PreAuthorizedCode
import eu.europa.ec.eudi.openid4vci.KtorHttpClientFactory
import eu.europa.ec.eudi.openid4vci.OpenId4VCIConfig
import eu.europa.ec.eudi.openid4vci.PKCEVerifier
import eu.europa.ec.eudi.openid4vci.RefreshToken
import eu.europa.ec.eudi.openid4vci.internal.DPoP
import eu.europa.ec.eudi.openid4vci.internal.DPoPJwtFactory
import eu.europa.ec.eudi.openid4vci.internal.GrantedAuthorizationDetailsSerializer
import eu.europa.ec.eudi.openid4vci.internal.Htm
import eu.europa.ec.eudi.openid4vci.internal.TokenResponse
import eu.europa.ec.eudi.openid4vci.internal.dpop
import io.ktor.client.call.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.client.call.body
import io.ktor.client.request.forms.submitForm
import io.ktor.http.Parameters
import io.ktor.http.isSuccess
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URI
Expand Down Expand Up @@ -59,6 +70,7 @@ internal sealed interface TokenResponseTO {
@SerialName(
"authorization_details",
) val authorizationDetails: Map<CredentialConfigurationIdentifier, List<CredentialIdentifier>>? = null,
var dpopNonce: String? = null
) : TokenResponseTO

/**
Expand Down Expand Up @@ -86,6 +98,7 @@ internal sealed interface TokenResponseTO {
cNonce = cNonce?.let { CNonce(it, cNonceExpiresIn) },
authorizationDetails = authorizationDetails ?: emptyMap(),
timestamp = clock.instant(),
dpopNonce = dpopNonce ?: "no dpopNonce"
)
}

Expand Down Expand Up @@ -128,14 +141,15 @@ internal class TokenEndpointClient(
suspend fun requestAccessTokenAuthFlow(
authorizationCode: AuthorizationCode,
pkceVerifier: PKCEVerifier,
dpopNonce: String
): Result<TokenResponse> = runCatching {
val params = TokenEndpointForm.authCodeFlow(
authorizationCode = authorizationCode,
redirectionURI = authFlowRedirectionURI,
clientId = clientId,
pkceVerifier = pkceVerifier,
)
requestAccessToken(params).tokensOrFail(clock)
requestAccessToken(params, dpopNonce).tokensOrFail(clock)
}

/**
Expand Down Expand Up @@ -167,27 +181,33 @@ internal class TokenEndpointClient(
* @return the token end point response, which will include a new [TokenResponse.accessToken] and possibly
* a new [TokenResponse.refreshToken]
*/
suspend fun refreshAccessToken(refreshToken: RefreshToken): Result<TokenResponse> = runCatching {
val params = TokenEndpointForm.refreshAccessToken(clientId, refreshToken)
requestAccessToken(params).tokensOrFail(clock = clock)
}
suspend fun refreshAccessToken(refreshToken: RefreshToken): Result<TokenResponse> =
runCatching {
val params = TokenEndpointForm.refreshAccessToken(clientId, refreshToken)
requestAccessToken(params).tokensOrFail(clock = clock)
}

private suspend fun requestAccessToken(
params: Map<String, String>,
dpopNonce: String? = null
): TokenResponseTO =
ktorHttpClientFactory().use { client ->
val formParameters = Parameters.build {
params.entries.forEach { (k, v) -> append(k, v) }
}
val response = client.submitForm(tokenEndpoint.toString(), formParameters) {
dPoPJwtFactory?.let { factory ->
dpop(factory, tokenEndpoint, Htm.POST, accessToken = null, nonce = null)
dpop(factory, tokenEndpoint, Htm.POST, accessToken = null, nonce = dpopNonce)
}
}
if (response.status.isSuccess()) response.body<TokenResponseTO.Success>()
if (response.status.isSuccess()) response.body<TokenResponseTO.Success>().also {
val dPoPNonce = response.headers["DPoP-Nonce"] ?: throw IllegalStateException("No DPoP-Nonce received")
it.dpopNonce = dPoPNonce
}
else response.body<TokenResponseTO.Failure>()
}
}

internal object TokenEndpointForm {
const val AUTHORIZATION_CODE_GRANT = "authorization_code"
const val PRE_AUTHORIZED_CODE_GRANT = "urn:ietf:params:oauth:grant-type:pre-authorized_code"
Expand Down