-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDPoP.kt
More file actions
176 lines (161 loc) · 6.55 KB
/
DPoP.kt
File metadata and controls
176 lines (161 loc) · 6.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/*
* Copyright (c) 2023-2026 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.openid4vci
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.JWK
import com.nimbusds.jwt.SignedJWT
import com.nimbusds.oauth2.sdk.dpop.DPoPUtils
import com.nimbusds.oauth2.sdk.id.JWTID
import eu.europa.ec.eudi.openid4vci.internal.JWTClaimsSetSerializer
import eu.europa.ec.eudi.openid4vci.internal.JsonSupport
import eu.europa.ec.eudi.openid4vci.internal.JwtSigner
import eu.europa.ec.eudi.openid4vci.internal.toJoseAlg
import eu.europa.ec.eudi.openid4vci.internal.use
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
import java.net.URL
import java.time.Clock
import java.util.*
import com.nimbusds.oauth2.sdk.dpop.DPoPProofFactory as NimbusDPoPProofFactory
import com.nimbusds.oauth2.sdk.token.DPoPAccessToken as NimbusDPoPAccessToken
import com.nimbusds.openid.connect.sdk.Nonce as NimbusNonce
const val DPoP = "DPoP"
enum class Htm {
GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE
}
/**
* Factory class to generate DPoP JWTs to be added as a request header `DPoP` based on spec https://datatracker.ietf.org/doc/rfc9449/
*/
class DPoPJwtFactory(
val signer: Signer<JWK>,
private val jtiByteLength: Int = NimbusDPoPProofFactory.MINIMAL_JTI_BYTE_LENGTH,
private val clock: Clock,
) {
init {
require(jtiByteLength > 0) { "jtiByteLength must be greater than zero" }
}
suspend fun createDPoPJwt(
htm: Htm,
htu: URL,
accessToken: AccessToken.DPoP? = null,
nonce: Nonce? = null,
): Result<SignedJWT> = runCatchingCancellable {
val jwtClaimsSet = DPoPUtils.createJWTClaimsSet(
jti(),
htm.name,
htu.toURI(),
now(),
accessToken?.let {
NimbusDPoPAccessToken(it.accessToken)
},
nonce?.let { NimbusNonce(it.value) },
)
val signedJwt = signer.use { signOperation ->
JwtSigner(
serializer = JWTClaimsSetSerializer,
signOperation = signOperation,
algorithm = signer.javaAlgorithm.toJoseAlg(),
customizeHeader = { key -> dpopJwtHeader(key) },
).sign(jwtClaimsSet)
}
SignedJWT.parse(signedJwt)
}
private fun JsonObjectBuilder.dpopJwtHeader(jwk: JWK) {
put("typ", NimbusDPoPProofFactory.TYPE.type)
put("jwk", JsonSupport.parseToJsonElement(jwk.toJSONString()))
}
private fun now(): Date = Date.from(clock.instant())
private fun jti(): JWTID = JWTID(jtiByteLength)
companion object {
/**
* Tries to create a [DPoPJwtFactory] given a [signer] and the [oauthServerMetadata]
* of the OAuth 2.0 authorization server.
*
* The factory will be created in case the server supports DPoP (this is indicated by a not empty array
* ` dpop_signing_alg_values_supported` and in addition if the [signer] uses a supported algorithm
*
* @return
* if the OAUTH2 server doesn't support DPoP result would be `Result.Success(null)`
* if the OAUTH2 server supports DPoP and Signer uses a supported algorithm result would be success
* Otherwise a failure will be returned
*
*/
fun createForServer(
signer: Signer<JWK>,
jtiByteLength: Int = NimbusDPoPProofFactory.MINIMAL_JTI_BYTE_LENGTH,
clock: Clock,
oauthServerMetadata: CIAuthorizationServerMetadata,
): Result<DPoPJwtFactory?> =
create(signer, jtiByteLength, clock, oauthServerMetadata.dPoPJWSAlgs.orEmpty())
/**
* Tries to create a [DPoPJwtFactory] given a [signer] and the
* [supportedDPopAlgorithms] of the OAuth 2.0 Authorization Server.
*
* The factory will be created in case the server supports DPoP (this is indicated by a not empty array
* ` dpop_signing_alg_values_supported` and in addition if the [signer] uses a supported algorithm
*
* @return
* if the OAUTH2 server doesn't support DPoP result would be `Result.Success(null)`
* if the OAUTH2 server supports DPoP and Signer uses a supported algorithm result would be success
* Otherwise a failure will be returned
*
*/
fun create(
signer: Signer<JWK>,
jtiByteLength: Int = NimbusDPoPProofFactory.MINIMAL_JTI_BYTE_LENGTH,
clock: Clock,
supportedDPopAlgorithms: List<JWSAlgorithm>,
): Result<DPoPJwtFactory?> = runCatching {
val signerAlg = signer.javaAlgorithm.toJoseAlg()
if (supportedDPopAlgorithms.isNotEmpty()) {
require(signerAlg in supportedDPopAlgorithms) {
"DPoP signer uses $signerAlg which is not dpop_signing_alg_values_supported= $supportedDPopAlgorithms"
}
DPoPJwtFactory(signer, jtiByteLength, clock)
} else null
}
}
}
/**
* Utility method to be used to set properly the DPoP header on the request under construction, targeted on the URL passed as [htu].
* Based on the passed [accessToken] DPoP header will be added if it is of type DPoP.
*/
fun HttpRequestBuilder.bearerOrDPoPAuth(
accessToken: AccessToken,
dpopJwt: String?,
) {
when (accessToken) {
is AccessToken.Bearer -> {
bearerAuth(accessToken)
}
is AccessToken.DPoP -> {
if (dpopJwt != null) {
header(DPoP, dpopJwt)
dpopAuth(accessToken)
} else {
bearerAuth(AccessToken.Bearer(accessToken.accessToken, accessToken.expiresIn))
}
}
}
}
private fun HttpRequestBuilder.dpopAuth(accessToken: AccessToken.DPoP) {
header(HttpHeaders.Authorization, "$DPoP ${accessToken.accessToken}")
}
private fun HttpRequestBuilder.bearerAuth(accessToken: AccessToken.Bearer) {
bearerAuth(accessToken.accessToken)
}