[OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow#47405
[OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow#47405tdiesler wants to merge 2 commits intokeycloak:mainfrom
Conversation
4ca0091 to
12ed169
Compare
309b796 to
38889bd
Compare
5446980 to
5615979
Compare
IngridPuppet
left a comment
There was a problem hiding this comment.
Hello @tdiesler - I only reviewed commit "[OID4VCI-HAIP] Pass oid4vci-1_0-issuer-happy-flow". I'm not sure if it is only commit relevant for this PR. As per my review, this PR enables ABCA for the HAIP test to pass. I left some comments, please could you check?
|
|
||
| if (kid == null || kid.equals(currentKid)) { | ||
| String kty = key.get("kty").asText(); |
There was a problem hiding this comment.
In case kid is null, only the first JWK in the list will be considered. I'd rather either all are considered as potential verifier keys or null kids are not supported at all. WDYT?
There was a problem hiding this comment.
Doing this for now ...
if (Strings.isEmpty(kid))
throw new IllegalStateException("Invalid attester kid: " + kid);
ABCAConfig attesterKeys = JsonSerialization.valueFromString(abcaConfigValue, ABCAConfig.class);
for (JWK key : attesterKeys.keys) {
if (kid.equals(key.getKeyId())) {
...
return new JWKParser(key).toPublicKey();
}
}
i.e. we require the JWS header to provide a valid kid - this is lookup semantics rather than iterate-retry-on-failure - works for conformance tests.
There was a problem hiding this comment.
This is fine for me. Thx.
|
|
||
| String n = key.get("n").asText(); | ||
| String e = key.get("e").asText(); | ||
|
|
||
| BigInteger modulus = new BigInteger(1, Base64Url.decode(n)); | ||
| BigInteger exponent = new BigInteger(1, Base64Url.decode(e)); | ||
|
|
||
| RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); | ||
| return KeyFactory.getInstance("RSA").generatePublic(spec); |
There was a problem hiding this comment.
Consider reusing this class for this related logic.
keycloak/core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java
Lines 33 to 48 in d7238a7
There was a problem hiding this comment.
We are doing this ...
// The signature of the Client Attestation JWT verifies with the public key of a known and trusted Attester
//
PublicKey publicKey = findAttesterPublicKey(context, jws.getHeader().getKeyId());
// Verification and Processing
//
TokenVerifier.create(headerValue, ClientAttestationJwt.class)
.publicKey(publicKey)
.withChecks(subCheck, issCheck, cnfCheck, TokenVerifier.IS_ACTIVE)
.verify().getToken();
Is that ok?
There was a problem hiding this comment.
Thank you for the context. No strong opinion, that's fine for me.
| HttpHeaders headers = httpRequest.getHttpHeaders(); | ||
| String headerValue = headers.getHeaderString(OAUTH_CLIENT_ATTESTATION_HEADER); | ||
| if (headerValue == null) | ||
| throw new IllegalStateException("Required header " + OAUTH_CLIENT_ATTESTATION_HEADER + " for is missing"); |
There was a problem hiding this comment.
This looks more like a BadRequestException. Same in the rest of the method.
There was a problem hiding this comment.
I changed to IllegalArgumentException where appropriate. It doesn't matter much for these private methods - every Exception is caught and converted
try {
ClientAttestationJwt attesterJwt = validateClientAttestationJwt(context);
validateClientAttestationPoPJwt(context, attesterJwt);
context.success();
} catch (Exception ex) {
ServicesLogger.LOGGER.errorValidatingAssertion(ex);
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), OAuthErrorException.INVALID_CLIENT_ATTESTATION, ex.getMessage());
context.failure(AuthenticationFlowError.INVALID_CLIENT_ATTESTATION, challengeResponse);
}
There was a problem hiding this comment.
Thank you for the clarification.
| String clientIdParam = formParams.getFirst(CLIENT_ID); | ||
| if (clientIdParam != null && !clientIdParam.equals(jwt.getSubject())) | ||
| throw new IllegalStateException("The client attestation subject does not match the client_id parameter"); |
There was a problem hiding this comment.
No error if clientIdParam == null. Does not seem intended.
There was a problem hiding this comment.
I believe this is ok. The AttestationBasedClientAuthenticator is called twice, first on the PAR endpoint with a client_id param, then on the token endpoint without it.
We do ...
// Get the client model from the JWT subject
ClientAttestationJwt attesterJwt = jws.readJsonContent(ClientAttestationJwt.class);
ClientModel clientModel = Optional.ofNullable(attesterJwt.getSubject())
.map(realmModel::getClientByClientId)
.orElseThrow(() -> new TokenVerificationException(attesterJwt, "The sub (subject) claim MUST identify a known client_id"));
String clientIdParam = formParams.getFirst(CLIENT_ID);
if (clientIdParam != null && !clientIdParam.equals(t.getSubject()))
throw new TokenVerificationException(t, "The sub claim (subject) MUST match the client_id parameter");
Actually, I'm not sure whether it is correct that this is done twice. The spec says
(6) The Client Instance sends both the Client Attestation JWT and the Client Attestation PoP JWT to the authorization server, e.g. within a token request. The authorization server validates the Client Attestation and thus authenticates the Client Instance
There was a problem hiding this comment.
Thank you for clarification that clientIdParam is not required at the token endpoint. It's worth confirming whether the client needs to be authenticated twice, but I see nothing wrong with it.
There was a problem hiding this comment.
Thank you for clarification that clientIdParam is not required at the token endpoint. It's worth confirming whether the client needs to be authenticated twice, but I see nothing wrong with it.
We need to look into this nevertheless. With the TokenRequest (i.e. when there might be no client_id) the server should still be able to remember that it previously received an AuthorizationRequest
Doing this is fishy ...
ClientModel clientModel = Optional.ofNullable(attesterJwt.getSubject())
It should use the provided client_id from the authorization request, but I couldn't find an obvious way to get to it.
|
I suggest we focus on the other two PRs that this one depends on. Then, I'll bring the ABCA implementation (which is now spread over multiple PRs) forward to this one and then we can address all comments related to that (non-trivial) change. Indeed, I expect and welcome quite a bit of feedback on the ABCA implementation. |
3bc0bba to
337dfd7
Compare
Now the actual work starts. I'll bring everything abca related forward to this PR - then we have a complete view on what is there already (and what isn't ;-) |
3b459be to
63f0df4
Compare
Signed-off-by: Thomas Diesler <[email protected]>
Signed-off-by: Thomas Diesler <[email protected]>
closes #47316
depends on