mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-18 18:37:54 -05:00
[OID4VCI] Revisit and fix OAuthClient.credentialOfferRequest()
Signed-off-by: Thomas Diesler <tdiesler@ibm.com>
This commit is contained in:
parent
05ff44b8a0
commit
64dee82f9f
10 changed files with 50 additions and 93 deletions
|
|
@ -90,8 +90,8 @@ public class Endpoints {
|
|||
return asString(getBase().path(RealmsResource.class).path("{realm}/protocol/oid4vc/nonce"));
|
||||
}
|
||||
|
||||
public String getOid4vcCredentialOffer(String nonce) {
|
||||
return asString(getBase().path(RealmsResource.class).path("{realm}/protocol/oid4vc/credential-offer/").path(nonce));
|
||||
public String getOid4vcCredentialOffer() {
|
||||
return asString(getBase().path(RealmsResource.class).path("{realm}/protocol/oid4vc/credential-offer"));
|
||||
}
|
||||
|
||||
public String getOid4vcCredentialOfferUri() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpGetRequest;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
|
||||
|
|
@ -9,28 +11,25 @@ import org.apache.http.client.methods.CloseableHttpResponse;
|
|||
|
||||
public class CredentialOfferRequest extends AbstractHttpGetRequest<CredentialOfferRequest, CredentialOfferResponse> {
|
||||
|
||||
private String nonce;
|
||||
private final CredentialOfferURI credOfferURI;
|
||||
|
||||
public CredentialOfferRequest(AbstractOAuthClient<?> client) {
|
||||
public CredentialOfferRequest(AbstractOAuthClient<?> client, CredentialOfferURI credOfferUri) {
|
||||
super(client);
|
||||
this.credOfferURI = credOfferUri;
|
||||
}
|
||||
|
||||
public CredentialOfferRequest(String nonce, AbstractOAuthClient<?> client) {
|
||||
public CredentialOfferRequest(AbstractOAuthClient<?> client, String nonce) {
|
||||
super(client);
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public CredentialOfferRequest nonce(String nonce) {
|
||||
this.nonce = nonce;
|
||||
return this;
|
||||
credOfferURI = new CredentialOfferURI();
|
||||
credOfferURI.setIssuer(client.getEndpoints().getOid4vcCredentialOffer());
|
||||
credOfferURI.setNonce(nonce);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpoint() {
|
||||
if (nonce == null) {
|
||||
throw new IllegalStateException("Nonce must be provided either via constructor, nonce() method, or endpoint must be overridden");
|
||||
}
|
||||
return client.getEndpoints().getOid4vcCredentialOffer(nonce);
|
||||
return Optional.ofNullable(credOfferURI)
|
||||
.map(CredentialOfferURI::getCredentialOfferUri)
|
||||
.orElseThrow(() -> new IllegalStateException("No credOfferURI"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpResponse;
|
||||
|
|
@ -21,6 +22,7 @@ public class CredentialOfferResponse extends AbstractHttpResponse {
|
|||
}
|
||||
|
||||
public CredentialsOffer getCredentialsOffer() {
|
||||
return credentialsOffer;
|
||||
return Optional.ofNullable(credentialsOffer).orElseThrow(() ->
|
||||
new IllegalStateException(String.format("[%s] %s", getError(), getErrorDescription())));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
|
|
@ -23,16 +24,12 @@ public class OID4VCClient {
|
|||
return new CredentialOfferUriRequest(client, credConfigId);
|
||||
}
|
||||
|
||||
public CredentialOfferRequest credentialOfferRequest() {
|
||||
return new CredentialOfferRequest(client);
|
||||
public CredentialOfferRequest credentialOfferRequest(CredentialOfferURI credOfferUri) {
|
||||
return new CredentialOfferRequest(client, credOfferUri);
|
||||
}
|
||||
|
||||
public CredentialOfferRequest credentialOfferRequest(String nonce) {
|
||||
return new CredentialOfferRequest(nonce, client);
|
||||
}
|
||||
|
||||
public CredentialOfferResponse doCredentialOfferRequest(String nonce) {
|
||||
return credentialOfferRequest(nonce).send();
|
||||
return new CredentialOfferRequest(client, nonce);
|
||||
}
|
||||
|
||||
public Oid4vcCredentialRequest credentialRequest() {
|
||||
|
|
@ -50,8 +47,4 @@ public class OID4VCClient {
|
|||
public Oid4vcNonceRequest nonceRequest() {
|
||||
return new Oid4vcNonceRequest(client);
|
||||
}
|
||||
|
||||
public String doNonceRequest() {
|
||||
return nonceRequest().send().getNonce();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
String appClient;
|
||||
CredentialIssuer issuerMetadata;
|
||||
OIDCConfigurationRepresentation authorizationMetadata;
|
||||
SupportedCredentialConfiguration supportedCredentialConfiguration;
|
||||
|
||||
SupportedCredentialConfiguration credentialConfiguration;
|
||||
|
||||
TestContext(boolean preAuth, String appClient, String appUser) {
|
||||
this.preAuthorized = preAuth;
|
||||
this.issUser = issUsername;
|
||||
|
|
@ -115,7 +115,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
this.appClient = appClient;
|
||||
this.issuerMetadata = getCredentialIssuerMetadata();
|
||||
this.authorizationMetadata = getAuthorizationMetadata(this.issuerMetadata.getAuthorizationServers().get(0));
|
||||
this.supportedCredentialConfiguration = this.issuerMetadata.getCredentialsSupported().get(credConfigId);
|
||||
this.credentialConfiguration = this.issuerMetadata.getCredentialsSupported().get(credConfigId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -252,13 +252,12 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
// Exclude scope: <credScope>
|
||||
// Require role: credential-offer-create
|
||||
verifyTokenJwt(ctx, issToken,
|
||||
List.of(), List.of(ctx.supportedCredentialConfiguration.getScope()),
|
||||
List.of(), List.of(ctx.credentialConfiguration.getScope()),
|
||||
List.of(CREDENTIAL_OFFER_CREATE.getName()), List.of());
|
||||
|
||||
// Retrieving the credential-offer-uri
|
||||
//
|
||||
CredentialOfferURI credOfferUri = getCredentialOfferUri(ctx, issToken);
|
||||
String offerUri = credOfferUri.getCredentialOfferUri();
|
||||
|
||||
// Issuer logout in order to remove unwanted session state
|
||||
//
|
||||
|
|
@ -268,7 +267,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
|
||||
// Using the uri to get the actual credential offer
|
||||
//
|
||||
CredentialsOffer credOffer = getCredentialsOffer(ctx, offerUri);
|
||||
CredentialsOffer credOffer = getCredentialsOffer(ctx, credOfferUri);
|
||||
|
||||
if (credOffer.getCredentialConfigurationIds().size() > 1)
|
||||
throw new IllegalStateException("Multiple credential configuration ids not supported in: " + JsonSerialization.valueAsString(credOffer));
|
||||
|
|
@ -397,7 +396,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
}
|
||||
|
||||
private CredentialOfferURI getCredentialOfferUri(TestContext ctx, String token) throws Exception {
|
||||
String credConfigId = ctx.supportedCredentialConfiguration.getId();
|
||||
String credConfigId = ctx.credentialConfiguration.getId();
|
||||
CredentialOfferUriResponse credentialOfferURIResponse = oauth.oid4vc()
|
||||
.credentialOfferUriRequest(credConfigId)
|
||||
.preAuthorized(ctx.preAuthorized)
|
||||
|
|
@ -411,19 +410,12 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
return credentialOfferURI;
|
||||
}
|
||||
|
||||
private CredentialsOffer getCredentialsOffer(TestContext ctx, String offerUri) throws Exception {
|
||||
private CredentialsOffer getCredentialsOffer(TestContext ctx, CredentialOfferURI credOfferUri) throws Exception {
|
||||
CredentialOfferResponse credentialOfferResponse = oauth.oid4vc()
|
||||
.credentialOfferRequest()
|
||||
.endpoint(offerUri)
|
||||
.credentialOfferRequest(credOfferUri)
|
||||
.send();
|
||||
int statusCode = credentialOfferResponse.getStatusCode();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
throw new IllegalStateException(credentialOfferResponse.getErrorDescription() != null
|
||||
? credentialOfferResponse.getErrorDescription()
|
||||
: "Request failed with status " + statusCode);
|
||||
}
|
||||
CredentialsOffer credOffer = credentialOfferResponse.getCredentialsOffer();
|
||||
assertEquals(List.of(ctx.supportedCredentialConfiguration.getId()), credOffer.getCredentialConfigurationIds());
|
||||
assertEquals(List.of(ctx.credentialConfiguration.getId()), credOffer.getCredentialConfigurationIds());
|
||||
return credOffer;
|
||||
}
|
||||
|
||||
|
|
@ -479,7 +471,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
// No authorization_details, use credential_configuration_id
|
||||
credentialRequest.setCredentialConfigurationId(credConfigIds.get(0));
|
||||
}
|
||||
|
||||
|
||||
return sendCredentialRequest(ctx, accessToken, credentialRequest);
|
||||
}
|
||||
|
||||
|
|
@ -512,7 +504,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
|
||||
private void verifyCredentialResponse(TestContext ctx, CredentialResponse credResponse) throws Exception {
|
||||
|
||||
String scope = ctx.supportedCredentialConfiguration.getScope();
|
||||
String scope = ctx.credentialConfiguration.getScope();
|
||||
CredentialResponse.Credential credentialObj = credResponse.getCredentials().get(0);
|
||||
assertNotNull("The first credential in the array should not be null", credentialObj);
|
||||
|
||||
|
|
|
|||
|
|
@ -138,8 +138,8 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
|||
// Clear events before credential offer request
|
||||
events.clear();
|
||||
|
||||
CredentialOfferResponse credentialOfferResponse = oauth.oid4vc().credentialOfferRequest()
|
||||
.endpoint(credentialOfferURI.getIssuer() + "/" + credentialOfferURI.getNonce())
|
||||
CredentialOfferResponse credentialOfferResponse = oauth.oid4vc()
|
||||
.credentialOfferRequest(credentialOfferURI)
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, credentialOfferResponse.getStatusCode());
|
||||
ctx.credentialsOffer = credentialOfferResponse.getCredentialsOffer();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import java.io.IOException;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
|
|
@ -83,7 +82,6 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
@Rule
|
||||
public TokenUtil tokenUtil = new TokenUtil();
|
||||
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
super.configureTestRealm(testRealm);
|
||||
|
|
@ -175,14 +173,14 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
public void testCredentialOfferSessionCorsValidOrigin() throws Exception {
|
||||
// First get a credential offer URI to obtain a nonce
|
||||
AccessTokenResponse tokenResponse = getAccessToken();
|
||||
String nonce = getNonceFromOfferUri(tokenResponse.getAccessToken());
|
||||
CredentialOfferURI credOfferUri = getCredentialOfferUri(tokenResponse.getAccessToken());
|
||||
|
||||
// Clear events before credential offer request
|
||||
events.clear();
|
||||
|
||||
// Test credential offer endpoint with valid origin
|
||||
CredentialOfferResponse response = oauth.oid4vc()
|
||||
.credentialOfferRequest(nonce)
|
||||
.credentialOfferRequest(credOfferUri)
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
|
|
@ -210,14 +208,11 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
public void testCredentialOfferSessionCorsInvalidOrigin() throws Exception {
|
||||
// First get a credential offer URI to obtain a nonce
|
||||
AccessTokenResponse tokenResponse = getAccessToken();
|
||||
String nonce = getNonceFromOfferUri(tokenResponse.getAccessToken());
|
||||
CredentialOfferURI credOfferUri = getCredentialOfferUri(tokenResponse.getAccessToken());
|
||||
|
||||
// Test credential offer endpoint with invalid origin
|
||||
String offerUrl = getCredentialOfferUrl(nonce);
|
||||
|
||||
CredentialOfferResponse response = oauth.oid4vc()
|
||||
.credentialOfferRequest()
|
||||
.endpoint(offerUrl)
|
||||
.credentialOfferRequest(credOfferUri)
|
||||
.header("Origin", INVALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
|
|
@ -230,10 +225,10 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
public void testCredentialOfferSessionCorsPreflightRequest() throws Exception {
|
||||
// First get a credential offer URI to obtain a nonce
|
||||
AccessTokenResponse tokenResponse = getAccessToken();
|
||||
String nonce = getNonceFromOfferUri(tokenResponse.getAccessToken());
|
||||
CredentialOfferURI credOfferUri = getCredentialOfferUri(tokenResponse.getAccessToken());
|
||||
|
||||
// Test preflight request for credential offer endpoint
|
||||
String offerUrl = getCredentialOfferUrl(nonce);
|
||||
String offerUrl = credOfferUri.getCredentialOfferUri();
|
||||
|
||||
try (CloseableHttpResponse response = makePreflightRequest(offerUrl, VALID_CORS_URL)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
|
|
@ -330,9 +325,8 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
|
||||
// Create an expired credential offer using testing client
|
||||
// Use AtomicReference to avoid serialization issues with lambda captures
|
||||
AtomicReference<String> nonceHolder = new AtomicReference<>();
|
||||
final String issuerPath = getRealmPath(TEST_REALM_NAME);
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
String issuerPath = getRealmPath(TEST_REALM_NAME);
|
||||
String nonce = testingClient.server(TEST_REALM_NAME).fetchString(session -> {
|
||||
CredentialsOffer credOffer = new CredentialsOffer()
|
||||
.setCredentialIssuer(issuerPath)
|
||||
.setGrants(new PreAuthorizedGrant().setPreAuthorizedCode(new PreAuthorizedCode().setPreAuthorizedCode("test-code")))
|
||||
|
|
@ -344,18 +338,15 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
CredentialOfferState offerState = new CredentialOfferState(credOffer, null, null, Time.currentTime() - 1);
|
||||
offerStorage.putOfferState(session, offerState);
|
||||
session.getTransactionManager().commit();
|
||||
nonceHolder.set(offerState.getNonce());
|
||||
return offerState.getNonce();
|
||||
});
|
||||
|
||||
String nonce = nonceHolder.get();
|
||||
assertNotNull("CredentialOffer nonce not null", nonce);
|
||||
|
||||
events.clear();
|
||||
|
||||
// Try to fetch the expired credential offer
|
||||
String offerUrl = getCredentialOfferUrl(nonce);
|
||||
CredentialOfferResponse response = oauth.oid4vc()
|
||||
.credentialOfferRequest()
|
||||
.endpoint(offerUrl)
|
||||
.credentialOfferRequest(nonce)
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
|
|
@ -401,7 +392,8 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
return res;
|
||||
}
|
||||
|
||||
private String getNonceFromOfferUri(String accessToken) throws Exception {
|
||||
private CredentialOfferURI getCredentialOfferUri(String accessToken) throws Exception {
|
||||
|
||||
CredentialOfferUriResponse response = oauth.oid4vc()
|
||||
.credentialOfferUriRequest(jwtTypeCredentialConfigurationIdName)
|
||||
.preAuthorized(true)
|
||||
|
|
@ -411,8 +403,7 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
.send();
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
CredentialOfferURI offerUri = response.getCredentialOfferURI();
|
||||
return offerUri.getNonce();
|
||||
return response.getCredentialOfferURI();
|
||||
}
|
||||
|
||||
private CloseableHttpResponse makeCorsRequest(String url, String origin, String accessToken) throws IOException {
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ import org.keycloak.testsuite.runonserver.RunOnServerException;
|
|||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.oauth.OpenIDProviderConfigurationResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialIssuerMetadataResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.Oid4vcCredentialResponse;
|
||||
import org.keycloak.userprofile.DeclarativeUserProfileProviderFactory;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
|
@ -617,25 +616,6 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
|||
return getBasePath("test") + "credential-offer/" + nonce;
|
||||
}
|
||||
|
||||
protected void requestCredential(String token,
|
||||
String credentialEndpoint,
|
||||
SupportedCredentialConfiguration offeredCredential,
|
||||
CredentialResponseHandler responseHandler,
|
||||
ClientScopeRepresentation expectedClientScope) throws IOException, VerificationException {
|
||||
Oid4vcCredentialResponse credentialRequestResponse = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.endpoint(credentialEndpoint)
|
||||
.bearerToken(token)
|
||||
.credentialConfigurationId(offeredCredential.getId())
|
||||
.send();
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, credentialRequestResponse.getStatusCode());
|
||||
CredentialResponse credentialResponse = credentialRequestResponse.getCredentialResponse();
|
||||
|
||||
// Use response handler to customize checks based on formats.
|
||||
responseHandler.handleCredentialResponse(credentialResponse, expectedClientScope);
|
||||
}
|
||||
|
||||
protected void requestCredentialWithIdentifier(String token,
|
||||
String credentialEndpoint,
|
||||
String credentialIdentifier,
|
||||
|
|
|
|||
|
|
@ -463,7 +463,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
|
||||
// 2. Using the uri to get the actual credential offer
|
||||
CredentialsOffer credentialsOffer = oauth.oid4vc()
|
||||
.credentialOfferRequest(credentialOfferURI.getNonce())
|
||||
.credentialOfferRequest(credentialOfferURI)
|
||||
.bearerToken(token)
|
||||
.send()
|
||||
.getCredentialsOffer();
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
|
||||
// 2. Using the uri to get the actual credential offer
|
||||
CredentialsOffer credentialsOffer = oauth.oid4vc()
|
||||
.credentialOfferRequest(credentialOfferURI.getNonce())
|
||||
.credentialOfferRequest(credentialOfferURI)
|
||||
.bearerToken(token)
|
||||
.send()
|
||||
.getCredentialsOffer();
|
||||
|
|
|
|||
Loading…
Reference in a new issue