mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-03 20:39:33 -05:00
[OID4VCI] Add OID4VCI request/response support to OAuthClient utility (#45784)
closes: #44671 Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
This commit is contained in:
parent
5e3c0b6b28
commit
f2f185b367
35 changed files with 1943 additions and 1627 deletions
|
|
@ -314,7 +314,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
|
|||
logger.warnf(e, "Error when processing authorization_details");
|
||||
event.detail(Details.REASON, e.getMessage());
|
||||
event.error(Errors.INVALID_AUTHORIZATION_DETAILS);
|
||||
throw new CorsErrorResponseException(cors, Errors.INVALID_AUTHORIZATION_DETAILS, e.getMessage(), Response.Status.BAD_REQUEST);
|
||||
throw new CorsErrorResponseException(cors, Errors.INVALID_AUTHORIZATION_DETAILS, "Error when processing authorization_details: " + e.getMessage(), Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ public class AuthorizationDetailsProcessorManager {
|
|||
|
||||
List<AuthorizationDetailsJSONRepresentation> authzDetails = parseAuthorizationDetails(authorizationDetailsParam);
|
||||
|
||||
if (authzDetails.isEmpty()) {
|
||||
throw new InvalidAuthorizationDetailsException("Authorization_Details parameter cannot be empty");
|
||||
}
|
||||
|
||||
Map<String, AuthorizationDetailsProcessor<?>> processors = getProcessors(session);
|
||||
|
||||
for (AuthorizationDetailsJSONRepresentation authzDetail : authzDetails) {
|
||||
|
|
|
|||
|
|
@ -1,30 +1,69 @@
|
|||
package org.keycloak.testsuite.util.oauth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
||||
public abstract class AbstractHttpGetRequest<R> {
|
||||
public abstract class AbstractHttpGetRequest<T, R> {
|
||||
|
||||
protected final AbstractOAuthClient<?> client;
|
||||
|
||||
private HttpGet get;
|
||||
protected String endpointOverride;
|
||||
protected String bearerToken;
|
||||
protected Map<String, String> headers = new HashMap<>();
|
||||
|
||||
public AbstractHttpGetRequest(AbstractOAuthClient<?> client) {
|
||||
this.client = client;
|
||||
this.headers.put("Accept", MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
protected abstract String getEndpoint();
|
||||
|
||||
protected abstract void initRequest();
|
||||
|
||||
/**
|
||||
* Override the endpoint URL for this request.
|
||||
* When specified, this takes precedence over {@link #getEndpoint()}.
|
||||
*
|
||||
* @param endpoint the endpoint URL to use
|
||||
* @return this request instance for method chaining
|
||||
*/
|
||||
public T endpoint(String endpoint) {
|
||||
this.endpointOverride = endpoint;
|
||||
return request();
|
||||
}
|
||||
|
||||
public T bearerToken(String bearerToken) {
|
||||
this.bearerToken = bearerToken;
|
||||
return request();
|
||||
}
|
||||
|
||||
public T header(String name, String value) {
|
||||
if (value != null) {
|
||||
this.headers.put(name, value);
|
||||
}
|
||||
return request();
|
||||
}
|
||||
|
||||
public R send() {
|
||||
get = new HttpGet(getEndpoint());
|
||||
get.addHeader("Accept", MediaType.APPLICATION_JSON);
|
||||
get = new HttpGet(endpointOverride != null ? endpointOverride : getEndpoint());
|
||||
|
||||
initRequest();
|
||||
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
get.setHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
if (bearerToken != null) {
|
||||
get.addHeader("Authorization", "Bearer " + bearerToken);
|
||||
}
|
||||
|
||||
try {
|
||||
return toResponse(client.httpClient().get().execute(get));
|
||||
} catch (IOException e) {
|
||||
|
|
@ -32,12 +71,11 @@ public abstract class AbstractHttpGetRequest<R> {
|
|||
}
|
||||
}
|
||||
|
||||
protected void header(String name, String value) {
|
||||
if (value != null) {
|
||||
get.addHeader(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract R toResponse(CloseableHttpResponse response) throws IOException;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private T request() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,16 +34,30 @@ public abstract class AbstractHttpPostRequest<T, R> {
|
|||
protected Map<String, String> headers = new HashMap<>();
|
||||
protected List<NameValuePair> parameters = new LinkedList<>();
|
||||
|
||||
protected String endpoint;
|
||||
|
||||
public AbstractHttpPostRequest(AbstractOAuthClient<?> client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
protected abstract String getEndpoint();
|
||||
|
||||
/**
|
||||
* Override the endpoint URL for this request.
|
||||
* When specified, this takes precedence over {@link #getEndpoint()}.
|
||||
*
|
||||
* @param endpoint the endpoint URL to use
|
||||
* @return this request instance for method chaining
|
||||
*/
|
||||
public T endpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
return request();
|
||||
}
|
||||
|
||||
protected abstract void initRequest();
|
||||
|
||||
public R send() {
|
||||
post = new HttpPost(getEndpoint());
|
||||
post = new HttpPost(endpoint != null ? endpoint : getEndpoint());
|
||||
post.addHeader("Accept", getAccept());
|
||||
post.addHeader("Origin", client.config().getOrigin());
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import org.keycloak.representations.JsonWebToken;
|
|||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.testsuite.util.oauth.ciba.CibaClient;
|
||||
import org.keycloak.testsuite.util.oauth.device.DeviceClient;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.OID4VCClient;
|
||||
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
|
@ -234,6 +235,10 @@ public abstract class AbstractOAuthClient<T> {
|
|||
return new DeviceClient(this);
|
||||
}
|
||||
|
||||
public OID4VCClient oid4vc() {
|
||||
return new OID4VCClient(this);
|
||||
}
|
||||
|
||||
public ParRequest pushedAuthorizationRequest() {
|
||||
return new ParRequest(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@ package org.keycloak.testsuite.util.oauth;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse;
|
||||
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class AccessTokenResponse extends AbstractHttpResponse {
|
||||
|
|
@ -24,6 +27,7 @@ public class AccessTokenResponse extends AbstractHttpResponse {
|
|||
private String scope;
|
||||
private String sessionState;
|
||||
private List<AuthorizationDetailsJSONRepresentation> authorizationDetails;
|
||||
private Map<String, Object> responseJson;
|
||||
|
||||
private Map<String, Object> otherClaims;
|
||||
|
||||
|
|
@ -32,8 +36,8 @@ public class AccessTokenResponse extends AbstractHttpResponse {
|
|||
}
|
||||
|
||||
protected void parseContent() throws IOException {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> responseJson = asJson(Map.class);
|
||||
this.responseJson = responseJson;
|
||||
|
||||
otherClaims = new HashMap<>();
|
||||
|
||||
|
|
@ -123,12 +127,37 @@ public class AccessTokenResponse extends AbstractHttpResponse {
|
|||
}
|
||||
|
||||
public <ADR extends AuthorizationDetailsJSONRepresentation> List<ADR> getAuthorizationDetails(Class<ADR> clazz) {
|
||||
if (getAuthorizationDetails() == null) {
|
||||
if (authorizationDetails == null) {
|
||||
return null;
|
||||
} else {
|
||||
return getAuthorizationDetails().stream()
|
||||
.map(authzResponse -> authzResponse.asSubtype(clazz))
|
||||
.toList();
|
||||
}
|
||||
return authorizationDetails.stream()
|
||||
.map(authzResponse -> authzResponse.asSubtype(clazz))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization details as OID4VC-specific response objects.
|
||||
* This is useful when you need to access OID4VC-specific fields like credential_identifiers.
|
||||
*
|
||||
* @return a list of authorization details, or an empty list if none are present.
|
||||
* @throws RuntimeException if there's an error parsing the JSON response
|
||||
*/
|
||||
public List<OID4VCAuthorizationDetailResponse> getOid4vcAuthorizationDetails() {
|
||||
if (responseJson == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Object authDetailsObj = responseJson.get(OAuth2Constants.AUTHORIZATION_DETAILS);
|
||||
if (authDetailsObj == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
return JsonSerialization.readValue(
|
||||
JsonSerialization.writeValueAsString(authDetailsObj),
|
||||
new TypeReference<List<OID4VCAuthorizationDetailResponse>>() {
|
||||
}
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to parse authorization_details from token response", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,31 @@ public class Endpoints {
|
|||
return asString(CibaGrantType.authenticationUrl(getBase()));
|
||||
}
|
||||
|
||||
public String getOid4vcIssuerMetadata() {
|
||||
return asString(getBase().path(RealmsResource.class).path("{realm}/.well-known/openid-credential-issuer"));
|
||||
}
|
||||
|
||||
public String getOid4vcCredential() {
|
||||
return asString(getBase().path(RealmsResource.class).path("{realm}/protocol/oid4vc/credential"));
|
||||
}
|
||||
|
||||
public String getOid4vcNonce() {
|
||||
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 getOid4vcCredentialOfferUri(String configId, Boolean preAuthorized, String username, String appClientId) {
|
||||
UriBuilder builder = getBase().path(RealmsResource.class).path("{realm}/protocol/oid4vc/credential-offer-uri");
|
||||
if (configId != null && !configId.isBlank()) builder.queryParam("credential_configuration_id", configId);
|
||||
if (preAuthorized != null) builder.queryParam("pre_authorized", preAuthorized);
|
||||
if (username != null && !username.isBlank()) builder.queryParam("username", username);
|
||||
if (appClientId != null && !appClientId.isBlank()) builder.queryParam("client_id", appClientId);
|
||||
return asString(builder);
|
||||
}
|
||||
|
||||
UriBuilder getBase() {
|
||||
return UriBuilder.fromUri(baseUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import jakarta.ws.rs.core.UriBuilder;
|
|||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class FetchExternalIdpTokenRequest extends AbstractHttpGetRequest<AccessTokenResponse> {
|
||||
public class FetchExternalIdpTokenRequest extends AbstractHttpGetRequest<FetchExternalIdpTokenRequest, AccessTokenResponse> {
|
||||
|
||||
private final String providerAlias;
|
||||
private final String accessToken;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@ import java.io.IOException;
|
|||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class OpenIDProviderConfigurationRequest extends AbstractHttpGetRequest<OpenIDProviderConfigurationResponse> {
|
||||
public class OpenIDProviderConfigurationRequest extends AbstractHttpGetRequest<OpenIDProviderConfigurationRequest, OpenIDProviderConfigurationResponse> {
|
||||
|
||||
public OpenIDProviderConfigurationRequest(AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
public OpenIDProviderConfigurationRequest url(String url) {
|
||||
return endpoint(url + (url.endsWith("/") ? "" : "/") + ".well-known/openid-configuration");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpoint() {
|
||||
return client.getEndpoints().getOpenIDConfiguration();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
package org.keycloak.testsuite.util.oauth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class ParRequest extends AbstractHttpPostRequest<ParRequest, ParResponse> {
|
||||
|
||||
private boolean scopeExplicitlySet = false;
|
||||
|
||||
public ParRequest(AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
}
|
||||
|
|
@ -58,6 +62,11 @@ public class ParRequest extends AbstractHttpPostRequest<ParRequest, ParResponse>
|
|||
return this;
|
||||
}
|
||||
|
||||
public ParRequest authorizationDetails(List<?> authDetails) {
|
||||
parameter(OAuth2Constants.AUTHORIZATION_DETAILS, JsonSerialization.valueAsString(authDetails));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParRequest request(String request) {
|
||||
parameter(OIDCLoginProtocol.REQUEST_PARAM, request);
|
||||
return this;
|
||||
|
|
@ -68,12 +77,20 @@ public class ParRequest extends AbstractHttpPostRequest<ParRequest, ParResponse>
|
|||
return this;
|
||||
}
|
||||
|
||||
public ParRequest scopeParam(String scope) {
|
||||
scopeExplicitlySet = true;
|
||||
parameter(OAuth2Constants.SCOPE, scope);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initRequest() {
|
||||
parameter(OAuth2Constants.RESPONSE_TYPE, client.config().getResponseType());
|
||||
parameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM, client.config().getResponseMode());
|
||||
parameter(OAuth2Constants.REDIRECT_URI, client.config().getRedirectUri());
|
||||
parameter(OAuth2Constants.SCOPE, client.config().getScope());
|
||||
if (!scopeExplicitlySet) {
|
||||
parameter(OAuth2Constants.SCOPE, client.config().getScope());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import org.keycloak.util.TokenUtil;
|
|||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class UserInfoRequest extends AbstractHttpGetRequest<UserInfoResponse> {
|
||||
public class UserInfoRequest extends AbstractHttpGetRequest<UserInfoRequest, UserInfoResponse> {
|
||||
|
||||
private final String token;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
|
||||
public abstract class AbstractOid4vcRequest<T, R> {
|
||||
|
||||
protected final AbstractOAuthClient<?> client;
|
||||
protected String bearerToken;
|
||||
protected String endpointOverride;
|
||||
protected Map<String, String> headers = new HashMap<>();
|
||||
|
||||
public AbstractOid4vcRequest(AbstractOAuthClient<?> client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public T bearerToken(String bearerToken) {
|
||||
this.bearerToken = bearerToken;
|
||||
return request();
|
||||
}
|
||||
|
||||
public T endpoint(String endpoint) {
|
||||
this.endpointOverride = endpoint;
|
||||
return request();
|
||||
}
|
||||
|
||||
public T header(String name, String value) {
|
||||
headers.put(name, value);
|
||||
return request();
|
||||
}
|
||||
|
||||
protected abstract String getEndpoint();
|
||||
|
||||
protected abstract Object getBody();
|
||||
|
||||
public R send() {
|
||||
HttpPost post = new HttpPost(endpointOverride != null ? endpointOverride : getEndpoint());
|
||||
post.addHeader("Accept", MediaType.APPLICATION_JSON);
|
||||
post.addHeader("Content-Type", MediaType.APPLICATION_JSON);
|
||||
|
||||
if (bearerToken != null) {
|
||||
post.addHeader("Authorization", "Bearer " + bearerToken);
|
||||
}
|
||||
|
||||
headers.forEach(post::addHeader);
|
||||
|
||||
try {
|
||||
Object body = getBody();
|
||||
if (body != null) {
|
||||
String jsonBody;
|
||||
if (body instanceof String) {
|
||||
jsonBody = (String) body;
|
||||
} else {
|
||||
jsonBody = JsonSerialization.writeValueAsString(body);
|
||||
}
|
||||
// Set entity even if empty string to support empty payload tests
|
||||
post.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));
|
||||
}
|
||||
// If body is null, don't set entity (no body)
|
||||
return toResponse(client.httpClient().get().execute(post));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract R toResponse(CloseableHttpResponse response) throws IOException;
|
||||
|
||||
private T request() {
|
||||
return (T) this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpGetRequest;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class CredentialIssuerMetadataRequest extends AbstractHttpGetRequest<CredentialIssuerMetadataRequest, CredentialIssuerMetadataResponse> {
|
||||
|
||||
public CredentialIssuerMetadataRequest(AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpoint() {
|
||||
return client.getEndpoints().getOid4vcIssuerMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initRequest() {
|
||||
// No specific parameters for metadata request
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CredentialIssuerMetadataResponse toResponse(CloseableHttpResponse response) throws IOException {
|
||||
return new CredentialIssuerMetadataResponse(response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class CredentialIssuerMetadataResponse extends AbstractHttpResponse {
|
||||
|
||||
private CredentialIssuer metadata;
|
||||
private String content;
|
||||
|
||||
public CredentialIssuerMetadataResponse(CloseableHttpResponse response) throws IOException {
|
||||
super(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseContent() throws IOException {
|
||||
content = asString();
|
||||
String contentType = getHeader("Content-Type");
|
||||
if (contentType != null && contentType.startsWith("application/json")) {
|
||||
// Check if this is OID4VC metadata (has "credential_issuer") vs JWT VC metadata (has "issuer" only)
|
||||
// JWT VC metadata uses JWTVCIssuerMetadata model with "issuer" field
|
||||
// OID4VC metadata uses CredentialIssuer model with "credential_issuer" field
|
||||
// Only parse if it's OID4VC format - JWT VC endpoints return different format
|
||||
JsonNode node = JsonSerialization.readValue(content, JsonNode.class);
|
||||
if (node.has("credential_issuer")) {
|
||||
metadata = JsonSerialization.mapper.treeToValue(node, CredentialIssuer.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CredentialIssuer getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpGetRequest;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class CredentialOfferRequest extends AbstractHttpGetRequest<CredentialOfferRequest, CredentialOfferResponse> {
|
||||
|
||||
private String nonce;
|
||||
|
||||
public CredentialOfferRequest(AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
public CredentialOfferRequest(String nonce, AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public CredentialOfferRequest nonce(String nonce) {
|
||||
this.nonce = nonce;
|
||||
return this;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initRequest() {
|
||||
// No additional step needed for basic GET
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CredentialOfferResponse toResponse(CloseableHttpResponse response) throws IOException {
|
||||
return new CredentialOfferResponse(response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpResponse;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class CredentialOfferResponse extends AbstractHttpResponse {
|
||||
|
||||
private CredentialsOffer credentialsOffer;
|
||||
|
||||
public CredentialOfferResponse(CloseableHttpResponse response) throws IOException {
|
||||
super(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseContent() throws IOException {
|
||||
credentialsOffer = asJson(CredentialsOffer.class);
|
||||
}
|
||||
|
||||
public CredentialsOffer getCredentialsOffer() {
|
||||
return credentialsOffer;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpGetRequest;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class CredentialOfferUriRequest extends AbstractHttpGetRequest<CredentialOfferUriRequest, CredentialOfferUriResponse> {
|
||||
|
||||
private String credentialConfigurationId;
|
||||
private Boolean preAuthorized;
|
||||
private String username;
|
||||
private String clientIdParam;
|
||||
|
||||
public CredentialOfferUriRequest(AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
public CredentialOfferUriRequest credentialConfigurationId(String credentialConfigurationId) {
|
||||
this.credentialConfigurationId = credentialConfigurationId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialOfferUriRequest preAuthorized(Boolean preAuthorized) {
|
||||
this.preAuthorized = preAuthorized;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialOfferUriRequest username(String username) {
|
||||
this.username = username;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialOfferUriRequest clientId(String clientId) {
|
||||
this.clientIdParam = clientId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpoint() {
|
||||
return client.getEndpoints().getOid4vcCredentialOfferUri(credentialConfigurationId, preAuthorized, username, clientIdParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initRequest() {
|
||||
// All parameters are in the URL for this specific Keycloak test endpoint
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CredentialOfferUriResponse toResponse(CloseableHttpResponse response) throws IOException {
|
||||
return new CredentialOfferUriResponse(response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpResponse;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class CredentialOfferUriResponse extends AbstractHttpResponse {
|
||||
|
||||
private CredentialOfferURI credentialOfferURI;
|
||||
|
||||
public CredentialOfferUriResponse(CloseableHttpResponse response) throws IOException {
|
||||
super(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseContent() throws IOException {
|
||||
credentialOfferURI = asJson(CredentialOfferURI.class);
|
||||
}
|
||||
|
||||
public CredentialOfferURI getCredentialOfferURI() {
|
||||
return credentialOfferURI;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
public class OID4VCClient {
|
||||
|
||||
private final AbstractOAuthClient<?> client;
|
||||
|
||||
public OID4VCClient(AbstractOAuthClient<?> client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public CredentialIssuerMetadataRequest issuerMetadataRequest() {
|
||||
return new CredentialIssuerMetadataRequest(client);
|
||||
}
|
||||
|
||||
public CredentialIssuerMetadataResponse doIssuerMetadataRequest() {
|
||||
return issuerMetadataRequest().send();
|
||||
}
|
||||
|
||||
public CredentialOfferUriRequest credentialOfferUriRequest() {
|
||||
return new CredentialOfferUriRequest(client);
|
||||
}
|
||||
|
||||
public Oid4vcCredentialRequest credentialRequest() {
|
||||
return new Oid4vcCredentialRequest(client);
|
||||
}
|
||||
|
||||
public Oid4vcCredentialResponse doCredentialRequest(String accessToken, String credentialConfigurationId, Proofs proofs) {
|
||||
return credentialRequest()
|
||||
.bearerToken(accessToken)
|
||||
.credentialConfigurationId(credentialConfigurationId)
|
||||
.proofs(proofs)
|
||||
.send();
|
||||
}
|
||||
|
||||
public PreAuthorizedCodeGrantRequest preAuthorizedCodeGrantRequest(String preAuthorizedCode) {
|
||||
return new PreAuthorizedCodeGrantRequest(preAuthorizedCode, client);
|
||||
}
|
||||
|
||||
public AccessTokenResponse doPreAuthorizedCodeGrant(String preAuthorizedCode) {
|
||||
return preAuthorizedCodeGrantRequest(preAuthorizedCode).send();
|
||||
}
|
||||
|
||||
public CredentialOfferRequest credentialOfferRequest() {
|
||||
return new CredentialOfferRequest(client);
|
||||
}
|
||||
|
||||
public CredentialOfferRequest credentialOfferRequest(String nonce) {
|
||||
return new CredentialOfferRequest(nonce, client);
|
||||
}
|
||||
|
||||
public CredentialOfferResponse doCredentialOfferRequest(String nonce) {
|
||||
return credentialOfferRequest(nonce).send();
|
||||
}
|
||||
|
||||
public Oid4vcNonceRequest nonceRequest() {
|
||||
return new Oid4vcNonceRequest(client);
|
||||
}
|
||||
|
||||
public String doNonceRequest() {
|
||||
return nonceRequest().send().getNonce();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class Oid4vcCredentialRequest extends AbstractOid4vcRequest<Oid4vcCredentialRequest, Oid4vcCredentialResponse> {
|
||||
|
||||
private final CredentialRequest body = new CredentialRequest();
|
||||
private boolean emptyBody = false;
|
||||
|
||||
public Oid4vcCredentialRequest(AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
public Oid4vcCredentialRequest credentialConfigurationId(String credentialConfigurationId) {
|
||||
body.setCredentialConfigurationId(credentialConfigurationId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Oid4vcCredentialRequest credentialIdentifier(String credentialIdentifier) {
|
||||
body.setCredentialIdentifier(credentialIdentifier);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Oid4vcCredentialRequest proofs(Proofs proofs) {
|
||||
body.setProofs(proofs);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the request to send an empty payload body.
|
||||
* This is useful for testing edge cases where an empty body should be sent.
|
||||
*/
|
||||
public Oid4vcCredentialRequest emptyBody() {
|
||||
this.emptyBody = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpoint() {
|
||||
return client.getEndpoints().getOid4vcCredential();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request body. If {@link #emptyBody()} was called, returns an empty string ("")
|
||||
* to trigger an empty payload in {@link AbstractOid4vcRequest#send()}.
|
||||
* If not, returns the {@link CredentialRequest} object to be serialized as JSON.
|
||||
*
|
||||
* @return the request body object or empty string
|
||||
*/
|
||||
@Override
|
||||
protected Object getBody() {
|
||||
if (emptyBody) {
|
||||
return "";
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Oid4vcCredentialResponse toResponse(CloseableHttpResponse response) throws IOException {
|
||||
return new Oid4vcCredentialResponse(response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpResponse;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class Oid4vcCredentialResponse extends AbstractHttpResponse {
|
||||
|
||||
private CredentialResponse credentialResponse;
|
||||
|
||||
public Oid4vcCredentialResponse(CloseableHttpResponse response) throws IOException {
|
||||
super(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseContent() throws IOException {
|
||||
credentialResponse = asJson(CredentialResponse.class);
|
||||
}
|
||||
|
||||
public CredentialResponse getCredentialResponse() {
|
||||
return credentialResponse;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpPostRequest;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class Oid4vcNonceRequest extends AbstractHttpPostRequest<Oid4vcNonceRequest, Oid4vcNonceResponse> {
|
||||
|
||||
public Oid4vcNonceRequest(AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpoint() {
|
||||
return client.getEndpoints().getOid4vcNonce();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initRequest() {
|
||||
// No parameters needed for nonce request
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Oid4vcNonceResponse toResponse(CloseableHttpResponse response) throws IOException {
|
||||
return new Oid4vcNonceResponse(response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.NonceResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpResponse;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class Oid4vcNonceResponse extends AbstractHttpResponse {
|
||||
|
||||
private NonceResponse nonceResponse;
|
||||
|
||||
public Oid4vcNonceResponse(CloseableHttpResponse response) throws IOException {
|
||||
super(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseContent() throws IOException {
|
||||
nonceResponse = asJson(NonceResponse.class);
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonceResponse != null ? nonceResponse.getNonce() : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package org.keycloak.testsuite.util.oauth.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpPostRequest;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public class PreAuthorizedCodeGrantRequest extends AbstractHttpPostRequest<PreAuthorizedCodeGrantRequest, AccessTokenResponse> {
|
||||
|
||||
private final String preAuthorizedCode;
|
||||
|
||||
public PreAuthorizedCodeGrantRequest(String preAuthorizedCode, AbstractOAuthClient<?> client) {
|
||||
super(client);
|
||||
this.preAuthorizedCode = preAuthorizedCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpoint() {
|
||||
return client.getEndpoints().getToken();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initRequest() {
|
||||
parameter(OAuth2Constants.GRANT_TYPE, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE);
|
||||
parameter(PreAuthorizedCodeGrantTypeFactory.CODE_REQUEST_PARAM, preAuthorizedCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom parameter to the token request.
|
||||
* This is useful for adding authorization_details or other custom parameters.
|
||||
*/
|
||||
public PreAuthorizedCodeGrantRequest addParameter(String name, String value) {
|
||||
parameter(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AccessTokenResponse toResponse(CloseableHttpResponse response) throws IOException {
|
||||
return new AccessTokenResponse(response);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,16 +18,11 @@ package org.keycloak.testsuite.oid4vc.issuance;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
|
|
@ -41,7 +36,6 @@ import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
|
|||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
|
@ -49,21 +43,14 @@ import org.keycloak.representations.idm.UserRepresentation;
|
|||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.oid4vc.issuance.signing.OID4VCIssuerEndpointTest;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialOfferResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialOfferUriResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.Oid4vcCredentialRequest;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.Oid4vcCredentialResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.directory.api.util.Strings;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
|
||||
|
|
@ -273,15 +260,18 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
// 4. does not reflect anything from the credential offer
|
||||
//
|
||||
AccessTokenResponse accessToken = getPreAuthorizedAccessTokenResponse(ctx, credOffer);
|
||||
List<OID4VCAuthorizationDetailResponse> authDetails = accessToken.getAuthorizationDetails(OID4VCAuthorizationDetailResponse.class);
|
||||
if (authDetails == null)
|
||||
List<OID4VCAuthorizationDetailResponse> authDetailsResponse = accessToken.getOid4vcAuthorizationDetails();
|
||||
if (authDetailsResponse == null || authDetailsResponse.isEmpty()) {
|
||||
throw new IllegalStateException("No authorization_details in token response");
|
||||
if (authDetails.size() > 1)
|
||||
}
|
||||
if (authDetailsResponse.size() > 1) {
|
||||
throw new IllegalStateException("Multiple authorization_details in token response");
|
||||
}
|
||||
OID4VCAuthorizationDetailResponse authDetailResponse = authDetailsResponse.get(0);
|
||||
|
||||
// Get the credential and verify
|
||||
//
|
||||
CredentialResponse credResponse = getCredentialByAuthDetail(ctx, accessToken.getAccessToken(), authDetails.get(0));
|
||||
CredentialResponse credResponse = getCredentialByAuthDetail(ctx, accessToken.getAccessToken(), authDetailResponse);
|
||||
verifyCredentialResponse(ctx, credResponse);
|
||||
|
||||
} else {
|
||||
|
|
@ -342,50 +332,59 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
private CredentialOfferURI getCredentialOfferUri(OfferTestContext ctx, String token) throws Exception {
|
||||
String credConfigId = ctx.supportedCredentialConfiguration.getId();
|
||||
String credOfferUriUrl = getCredentialOfferUriUrl(credConfigId, ctx.preAuthorized, ctx.appUser, ctx.appClient);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(credOfferUriUrl);
|
||||
getCredentialOfferURI.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferURIResponse = httpClient.execute(getCredentialOfferURI);
|
||||
int statusCode = credentialOfferURIResponse.getStatusLine().getStatusCode();
|
||||
CredentialOfferUriResponse credentialOfferURIResponse = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.endpoint(credOfferUriUrl)
|
||||
.bearerToken(token)
|
||||
.send();
|
||||
int statusCode = credentialOfferURIResponse.getStatusCode();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
HttpEntity entity = credentialOfferURIResponse.getEntity();
|
||||
throw new IllegalStateException(EntityUtils.toString(entity));
|
||||
String error = credentialOfferURIResponse.getError();
|
||||
String errorDescription = credentialOfferURIResponse.getErrorDescription();
|
||||
String errorMessage = error != null ? error : "";
|
||||
if (errorDescription != null) {
|
||||
errorMessage += (errorMessage.isEmpty() ? "" : " ") + errorDescription;
|
||||
}
|
||||
if (errorMessage.isEmpty()) {
|
||||
errorMessage = "Request failed with status " + statusCode;
|
||||
}
|
||||
throw new IllegalStateException(errorMessage);
|
||||
}
|
||||
String s = IOUtils.toString(credentialOfferURIResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialOfferURI credentialOfferURI = JsonSerialization.valueFromString(s, CredentialOfferURI.class);
|
||||
CredentialOfferURI credentialOfferURI = credentialOfferURIResponse.getCredentialOfferURI();
|
||||
assertTrue(credentialOfferURI.getIssuer().startsWith(ctx.issuerMetadata.getCredentialIssuer()));
|
||||
assertTrue(Strings.isNotEmpty(credentialOfferURI.getNonce()));
|
||||
return credentialOfferURI;
|
||||
}
|
||||
|
||||
private CredentialsOffer getCredentialsOffer(OfferTestContext ctx, String offerUri) throws Exception {
|
||||
HttpGet getCredentialOffer = new HttpGet(offerUri);
|
||||
CloseableHttpResponse credentialOfferResponse = httpClient.execute(getCredentialOffer);
|
||||
int statusCode = credentialOfferResponse.getStatusLine().getStatusCode();
|
||||
CredentialOfferResponse credentialOfferResponse = oauth.oid4vc()
|
||||
.credentialOfferRequest()
|
||||
.endpoint(offerUri)
|
||||
.send();
|
||||
int statusCode = credentialOfferResponse.getStatusCode();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
HttpEntity entity = credentialOfferResponse.getEntity();
|
||||
throw new IllegalStateException(EntityUtils.toString(entity));
|
||||
throw new IllegalStateException(credentialOfferResponse.getErrorDescription() != null
|
||||
? credentialOfferResponse.getErrorDescription()
|
||||
: "Request failed with status " + statusCode);
|
||||
}
|
||||
String s = IOUtils.toString(credentialOfferResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialsOffer credOffer = JsonSerialization.valueFromString(s, CredentialsOffer.class);
|
||||
CredentialsOffer credOffer = credentialOfferResponse.getCredentialsOffer();
|
||||
assertEquals(List.of(ctx.supportedCredentialConfiguration.getId()), credOffer.getCredentialConfigurationIds());
|
||||
return credOffer;
|
||||
}
|
||||
|
||||
private AccessTokenResponse getPreAuthorizedAccessTokenResponse(OID4VCICredentialOfferMatrixTest.OfferTestContext ctx, CredentialsOffer credOffer) throws Exception {
|
||||
PreAuthorizedCode preAuthorizedCode = credOffer.getGrants().getPreAuthorizedCode();
|
||||
HttpPost postPreAuthorizedCode = new HttpPost(ctx.authorizationMetadata.getTokenEndpoint());
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
parameters.add(new BasicNameValuePair(PreAuthorizedCodeGrantTypeFactory.CODE_REQUEST_PARAM, preAuthorizedCode.getPreAuthorizedCode()));
|
||||
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8);
|
||||
postPreAuthorizedCode.setEntity(formEntity);
|
||||
CloseableHttpResponse accessTokenResponse = httpClient.execute(postPreAuthorizedCode);
|
||||
int statusCode = accessTokenResponse.getStatusLine().getStatusCode();
|
||||
AccessTokenResponse accessTokenResponse = oauth.oid4vc()
|
||||
.preAuthorizedCodeGrantRequest(preAuthorizedCode.getPreAuthorizedCode())
|
||||
.endpoint(ctx.authorizationMetadata.getTokenEndpoint())
|
||||
.send();
|
||||
int statusCode = accessTokenResponse.getStatusCode();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
HttpEntity entity = accessTokenResponse.getEntity();
|
||||
throw new IllegalStateException(EntityUtils.toString(entity));
|
||||
throw new IllegalStateException(accessTokenResponse.getErrorDescription() != null
|
||||
? accessTokenResponse.getErrorDescription()
|
||||
: "Request failed with status " + statusCode);
|
||||
}
|
||||
return new AccessTokenResponse(accessTokenResponse);
|
||||
return accessTokenResponse;
|
||||
}
|
||||
|
||||
private CredentialResponse getCredentialByAuthDetail(OfferTestContext ctx, String accessToken, OID4VCAuthorizationDetailResponse authDetail) throws Exception {
|
||||
|
|
@ -413,21 +412,27 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
|||
}
|
||||
|
||||
private CredentialResponse sendCredentialRequest(OfferTestContext ctx, String accessToken, CredentialRequest credentialRequest) throws Exception {
|
||||
HttpPost postCredential = new HttpPost(ctx.issuerMetadata.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
|
||||
StringEntity stringEntity = new StringEntity(JsonSerialization.valueAsString(credentialRequest), ContentType.APPLICATION_JSON);
|
||||
postCredential.setEntity(stringEntity);
|
||||
Oid4vcCredentialRequest request = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.endpoint(ctx.issuerMetadata.getCredentialEndpoint())
|
||||
.bearerToken(accessToken);
|
||||
|
||||
CloseableHttpResponse credentialRequestResponse = httpClient.execute(postCredential);
|
||||
int statusCode = credentialRequestResponse.getStatusLine().getStatusCode();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
HttpEntity entity = credentialRequestResponse.getEntity();
|
||||
throw new IllegalStateException(EntityUtils.toString(entity));
|
||||
if (credentialRequest.getCredentialConfigurationId() != null) {
|
||||
request.credentialConfigurationId(credentialRequest.getCredentialConfigurationId());
|
||||
}
|
||||
if (credentialRequest.getCredentialIdentifier() != null) {
|
||||
request.credentialIdentifier(credentialRequest.getCredentialIdentifier());
|
||||
}
|
||||
|
||||
String s = IOUtils.toString(credentialRequestResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialResponse credentialResponse = JsonSerialization.valueFromString(s, CredentialResponse.class);
|
||||
Oid4vcCredentialResponse credentialRequestResponse = request.send();
|
||||
int statusCode = credentialRequestResponse.getStatusCode();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
throw new IllegalStateException(credentialRequestResponse.getErrorDescription() != null
|
||||
? credentialRequestResponse.getErrorDescription()
|
||||
: "Request failed with status " + statusCode);
|
||||
}
|
||||
|
||||
CredentialResponse credentialResponse = credentialRequestResponse.getCredentialResponse();
|
||||
assertNotNull("The credentials array should be present in the response", credentialResponse.getCredentials());
|
||||
assertFalse("The credentials array should not be empty", credentialResponse.getCredentials().isEmpty());
|
||||
return credentialResponse;
|
||||
|
|
|
|||
|
|
@ -17,25 +17,17 @@
|
|||
|
||||
package org.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientScopeResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.models.oid4vci.Oid4vcProtocolMapperModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse;
|
||||
|
|
@ -43,39 +35,29 @@ import org.keycloak.protocol.oid4vc.model.ClaimsDescription;
|
|||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.ErrorType;
|
||||
import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.OpenIDProviderConfigurationResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialIssuerMetadataResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.Oid4vcCredentialRequest;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.Oid4vcCredentialResponse;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
|
||||
import static org.keycloak.models.oid4vci.CredentialScopeModel.SIGNING_ALG;
|
||||
import static org.keycloak.models.oid4vci.CredentialScopeModel.SIGNING_KEY_ID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Base class for authorization code flow tests with authorization details and claims validation.
|
||||
|
|
@ -123,20 +105,19 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
Oid4vcTestContext ctx = new Oid4vcTestContext();
|
||||
|
||||
// Get credential issuer metadata
|
||||
HttpGet getCredentialIssuer = new HttpGet(getRealmMetadataPath(TEST_REALM_NAME));
|
||||
try (CloseableHttpResponse response = httpClient.execute(getCredentialIssuer)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
ctx.credentialIssuer = JsonSerialization.readValue(s, CredentialIssuer.class);
|
||||
}
|
||||
CredentialIssuerMetadataResponse metadataResponse = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(getRealmMetadataPath(TEST_REALM_NAME))
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, metadataResponse.getStatusCode());
|
||||
ctx.credentialIssuer = metadataResponse.getMetadata();
|
||||
|
||||
// Get OpenID configuration
|
||||
HttpGet getOpenidConfiguration = new HttpGet(ctx.credentialIssuer.getAuthorizationServers().get(0) + "/.well-known/openid-configuration");
|
||||
try (CloseableHttpResponse response = httpClient.execute(getOpenidConfiguration)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
ctx.openidConfig = JsonSerialization.readValue(s, OIDCConfigurationRepresentation.class);
|
||||
}
|
||||
OpenIDProviderConfigurationResponse openIDProviderConfigurationResponse = oauth.wellknownRequest()
|
||||
.url(ctx.credentialIssuer.getAuthorizationServers().get(0))
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, openIDProviderConfigurationResponse.getStatusCode());
|
||||
ctx.openidConfig = openIDProviderConfigurationResponse.getOidcConfiguration();
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
|
@ -163,25 +144,12 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
String secondCode = oauth.parseLoginResponse().getCode();
|
||||
assertNotNull("Second authorization code should not be null", secondCode);
|
||||
|
||||
// Exchange second code for tokens WITHOUT authorization_details
|
||||
HttpPost postSecondToken = new HttpPost(ctx.openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> secondTokenParameters = new LinkedList<>();
|
||||
secondTokenParameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
|
||||
secondTokenParameters.add(new BasicNameValuePair(OAuth2Constants.CODE, secondCode));
|
||||
secondTokenParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||
secondTokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
|
||||
secondTokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
|
||||
// NOTE: NO authorization_details parameter in this request
|
||||
|
||||
UrlEncodedFormEntity secondTokenFormEntity = new UrlEncodedFormEntity(secondTokenParameters, StandardCharsets.UTF_8);
|
||||
postSecondToken.setEntity(secondTokenFormEntity);
|
||||
|
||||
AccessTokenResponse secondTokenResponse;
|
||||
try (CloseableHttpResponse tokenHttpResponse = httpClient.execute(postSecondToken)) {
|
||||
assertEquals("Second token exchange should succeed", HttpStatus.SC_OK, tokenHttpResponse.getStatusLine().getStatusCode());
|
||||
String tokenResponseBody = IOUtils.toString(tokenHttpResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
secondTokenResponse = JsonSerialization.readValue(tokenResponseBody, AccessTokenResponse.class);
|
||||
}
|
||||
// Exchange second code for tokens WITHOUT authorization_details using OAuthClient
|
||||
AccessTokenResponse secondTokenResponse = oauth.accessTokenRequest(secondCode)
|
||||
.endpoint(ctx.openidConfig.getTokenEndpoint())
|
||||
.client(client.getClientId(), "password")
|
||||
.send();
|
||||
assertEquals("Second token exchange should succeed", HttpStatus.SC_OK, secondTokenResponse.getStatusCode());
|
||||
|
||||
// ===== STEP 3: Verify second token does NOT have authorization_details =====
|
||||
assertNull("Second token (regular SSO) should NOT have authorization_details", secondTokenResponse.getAuthorizationDetails());
|
||||
|
|
@ -191,23 +159,26 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialIdentifier(credentialIdentifier);
|
||||
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + secondTokenResponse.getToken());
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
postCredential.setEntity(new StringEntity(JsonSerialization.writeValueAsString(credentialRequest), StandardCharsets.UTF_8));
|
||||
// Credential request with second token should fail using OID4VCI utilities
|
||||
Oid4vcCredentialRequest credentialRequestBuilder = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.endpoint(ctx.credentialIssuer.getCredentialEndpoint())
|
||||
.bearerToken(secondTokenResponse.getAccessToken())
|
||||
.credentialIdentifier(credentialIdentifier);
|
||||
|
||||
Oid4vcCredentialResponse credentialResponse = credentialRequestBuilder.send();
|
||||
|
||||
// Credential request with second token should fail
|
||||
// The second token doesn't have the OID4VCI scope, so it should fail at scope check
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertEquals("Credential request with token without OID4VCI scope should fail",
|
||||
HttpStatus.SC_BAD_REQUEST, credentialResponse.getStatusLine().getStatusCode());
|
||||
// The second token doesn't have the OID4VCI scope, so it should fail
|
||||
assertEquals("Credential request with token without OID4VCI scope should fail",
|
||||
HttpStatus.SC_BAD_REQUEST, credentialResponse.getStatusCode());
|
||||
|
||||
String errorBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue("Error should indicate scope check failure. Actual error: " + errorBody,
|
||||
errorBody.contains("Scope check failure"));
|
||||
}
|
||||
String error = credentialResponse.getError();
|
||||
String errorDescription = credentialResponse.getErrorDescription();
|
||||
|
||||
assertEquals("Credential request should fail with unknown credential configuration when OID4VCI scope is missing",
|
||||
"UNKNOWN_CREDENTIAL_CONFIGURATION", error);
|
||||
assertEquals("Scope check failure", errorDescription);
|
||||
}
|
||||
|
||||
// Test for the whole authorization_code flow with the credentialRequest using credential_configuration_id
|
||||
|
|
@ -250,20 +221,30 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
|
||||
// Refresh token now
|
||||
org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponseRef = oauth.refreshRequest(tokenResponse.getRefreshToken()).send();
|
||||
// TODO: Converting from one to the other... This is dummy and should be replaced once we start using "OAuthClient" in this test instead of hand-written HTTP requests...
|
||||
AccessTokenResponse tokenResponse2 = new AccessTokenResponse();
|
||||
tokenResponse2.setAuthorizationDetails(tokenResponseRef.getAuthorizationDetails());
|
||||
tokenResponse2.setToken(tokenResponseRef.getAccessToken());
|
||||
|
||||
String credentialIdentifier = assertTokenResponse(tokenResponse2);
|
||||
// Extract values from refreshed token for credential request
|
||||
String accessToken = tokenResponseRef.getAccessToken();
|
||||
List<OID4VCAuthorizationDetailResponse> authDetails = tokenResponseRef.getOid4vcAuthorizationDetails();
|
||||
|
||||
String credentialIdentifier = null;
|
||||
if (authDetails != null && !authDetails.isEmpty()) {
|
||||
List<String> credentialIdentifiers = authDetails.get(0).getCredentialIdentifiers();
|
||||
if (credentialIdentifiers != null && !credentialIdentifiers.isEmpty()) {
|
||||
credentialIdentifier = credentialIdentifiers.get(0);
|
||||
}
|
||||
}
|
||||
String credentialConfigurationId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credRequestSupplier, tokenResponse2, credentialConfigurationId, credentialIdentifier);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
// Request the actual credential using the refreshed token
|
||||
Oid4vcCredentialRequest credentialRequest = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.endpoint(ctx.credentialIssuer.getCredentialEndpoint())
|
||||
.bearerToken(accessToken);
|
||||
if (credentialIdentifier != null) {
|
||||
credentialRequest.credentialIdentifier(credentialIdentifier);
|
||||
}
|
||||
Oid4vcCredentialResponse credentialResponse = credentialRequest.send();
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
|
||||
// Test for the authorization_code flow with "mandatory" claim specified in the "authorization_details" parameter
|
||||
|
|
@ -294,29 +275,27 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
events.clear();
|
||||
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
Oid4vcCredentialRequest credentialRequest = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
Oid4vcCredentialResponse credentialResponse = credentialRequest.send();
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse_mandatoryClaimsMissing(credentialResponse);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired with details about missing mandatory claim
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("The requested claims are not available in the user profile"))
|
||||
.assertEvent();
|
||||
}
|
||||
assertErrorCredentialResponse(credentialResponse);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired with details about missing mandatory claim
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("The requested claims are not available in the user profile"))
|
||||
.assertEvent();
|
||||
|
||||
// 3 - Update user to add "lastName"
|
||||
userRep.setLastName("Doe");
|
||||
user.update(userRep);
|
||||
|
||||
// 4 - Test the credential-request again. Should be OK now
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
credentialResponse = credentialRequest.send();
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -350,17 +329,15 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
String credentialIdentifier = assertTokenResponse(tokenResponseWithMandatoryLastName);
|
||||
String credentialConfigurationId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credRequestSupplier, tokenResponseWithMandatoryLastName, credentialConfigurationId, credentialIdentifier);
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse_mandatoryClaimsMissing(credentialResponse);
|
||||
}
|
||||
Oid4vcCredentialRequest credentialRequest = getCredentialRequest(ctx, credRequestSupplier, tokenResponseWithMandatoryLastName, credentialConfigurationId, credentialIdentifier);
|
||||
Oid4vcCredentialResponse credentialResponse = credentialRequest.send();
|
||||
assertErrorCredentialResponse_mandatoryClaimsMissing(credentialResponse);
|
||||
|
||||
// Request without mandatory lastName should work. Authorization_Details from accessToken will be used by Keycloak for processing this request
|
||||
credentialIdentifier = assertTokenResponse(tokenResponse);
|
||||
postCredential = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
credentialRequest = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
credentialResponse = credentialRequest.send();
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
} finally {
|
||||
// Revert user changes and add lastName back
|
||||
userRep.setLastName("Doe");
|
||||
|
|
@ -411,20 +388,19 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
events.clear();
|
||||
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
Oid4vcCredentialRequest credentialRequest = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
Oid4vcCredentialResponse credentialResponse = credentialRequest.send();
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse_mandatoryClaimsMissing(credentialResponse);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired with details about missing mandatory claim
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("The requested claims are not available in the user profile"))
|
||||
.assertEvent();
|
||||
}
|
||||
assertErrorCredentialResponse(credentialResponse);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired with details about missing mandatory claim
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("The requested claims are not available in the user profile"))
|
||||
.assertEvent();
|
||||
|
||||
// 3 - Update user to add "lastName", but keep "firstName" missing. Credential request should still fail
|
||||
userRep.setLastName("Doe");
|
||||
|
|
@ -434,18 +410,17 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
// Clear events before credential request
|
||||
events.clear();
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse_mandatoryClaimsMissing(credentialResponse);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("The requested claims are not available in the user profile"))
|
||||
.assertEvent();
|
||||
}
|
||||
credentialResponse = credentialRequest.send();
|
||||
assertErrorCredentialResponse(credentialResponse);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("The requested claims are not available in the user profile"))
|
||||
.assertEvent();
|
||||
|
||||
// 4 - Update user to add "firstName", but missing "lastName"
|
||||
userRep.setLastName(null);
|
||||
|
|
@ -455,27 +430,25 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
// Clear events before credential request
|
||||
events.clear();
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse_mandatoryClaimsMissing(credentialResponse);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("The requested claims are not available in the user profile"))
|
||||
.assertEvent();
|
||||
}
|
||||
credentialResponse = credentialRequest.send();
|
||||
assertErrorCredentialResponse(credentialResponse);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("The requested claims are not available in the user profile"))
|
||||
.assertEvent();
|
||||
|
||||
// 5 - Update user to both "firstName" and "lastName". Credential request should be successful
|
||||
userRep.setLastName("Doe");
|
||||
userRep.setFirstName("John");
|
||||
user.update(userRep);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
credentialResponse = credentialRequest.send();
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
} finally {
|
||||
// 6 - Revert protocol mapper config
|
||||
protocolMapper.getConfig().put(Oid4vcProtocolMapperModel.MANDATORY, "false");
|
||||
|
|
@ -483,88 +456,8 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompleteFlowWithSigningAlgorithmAndKeyIdConfigured() throws Exception {
|
||||
BiFunction<String, String, CredentialRequest> credRequestSupplier = (credentialConfigurationId, credentialIdentifier) -> {
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialIdentifier(credentialIdentifier);
|
||||
return credentialRequest;
|
||||
};
|
||||
|
||||
ClientScopeResource clientScope = ApiUtil.findClientScopeByName(testRealm(), getCredentialClientScope().getName());
|
||||
ClientScopeRepresentation clientScopeRep = clientScope.toRepresentation();
|
||||
Map<String, String> origAttributes = new HashMap<>(clientScopeRep.getAttributes());
|
||||
|
||||
try {
|
||||
// 1 - Configure signature algorithm, but not keyId. Make sure that credential signed with the target algorithm
|
||||
clientScopeRep.getAttributes().put(SIGNING_ALG, Algorithm.ES512);
|
||||
clientScopeRep.getAttributes().put(SIGNING_KEY_ID, null);
|
||||
clientScope.update(clientScopeRep);
|
||||
|
||||
Object credentialObj = testCompleteFlowWithClaimsValidationAuthorizationCode(credRequestSupplier);
|
||||
JWSHeader jwsHeader = verifyCredentialSignature(credentialObj, Algorithm.ES512);
|
||||
String es512keyId = jwsHeader.getKeyId();
|
||||
logoutUser("john");
|
||||
|
||||
// 2 - Configure signature algorithm, and keyId with blank value "" (just to simulate what admin console was doing when clientScope was saved).
|
||||
// Make sure that credential signed with the target algorithm and keyId is not considered
|
||||
clientScopeRep.getAttributes().put(SIGNING_ALG, Algorithm.EdDSA);
|
||||
clientScopeRep.getAttributes().put(SIGNING_KEY_ID, "");
|
||||
clientScope.update(clientScopeRep);
|
||||
|
||||
credentialObj = testCompleteFlowWithClaimsValidationAuthorizationCode(credRequestSupplier);
|
||||
verifyCredentialSignature(credentialObj, Algorithm.EdDSA);
|
||||
logoutUser("john");
|
||||
|
||||
// 3 - Configure signature algorithm, and keyId with some value. Make sure that
|
||||
// credential signed with the target algorithm and keyId as expected
|
||||
clientScopeRep.getAttributes().put(SIGNING_ALG, Algorithm.ES512);
|
||||
clientScopeRep.getAttributes().put(SIGNING_KEY_ID, es512keyId);
|
||||
clientScope.update(clientScopeRep);
|
||||
|
||||
credentialObj = testCompleteFlowWithClaimsValidationAuthorizationCode(credRequestSupplier);
|
||||
JWSHeader newJWSHeader = verifyCredentialSignature(credentialObj, Algorithm.ES512);
|
||||
assertEquals(es512keyId, newJWSHeader.getKeyId());
|
||||
logoutUser("john");
|
||||
|
||||
// 4 - Configure different signature algorithm not matching with key specified by keyId. Error is expected
|
||||
clientScopeRep.getAttributes().put(SIGNING_ALG, Algorithm.EdDSA);
|
||||
clientScopeRep.getAttributes().put(SIGNING_KEY_ID, es512keyId);
|
||||
clientScope.update(clientScopeRep);
|
||||
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext();
|
||||
AccessTokenResponse tokenResponse = authzCodeFlow(ctx);
|
||||
String credentialIdentifier = assertTokenResponse(tokenResponse);
|
||||
String credentialConfigurationId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
|
||||
// Clear events before credential request
|
||||
events.clear();
|
||||
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
String expectedError = "Signing of credential failed: No key for id '" + es512keyId + "' and algorithm 'EdDSA' available.";
|
||||
assertErrorCredentialResponse(credentialResponse, ErrorType.INVALID_CREDENTIAL_REQUEST.name(), expectedError);
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired with details about missing mandatory claim
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(ErrorType.INVALID_CREDENTIAL_REQUEST.getValue())
|
||||
.detail(Details.REASON, expectedError)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
} finally {
|
||||
// Revert clientScope config
|
||||
clientScopeRep.setAttributes(origAttributes);
|
||||
clientScope.update(clientScopeRep);
|
||||
}
|
||||
}
|
||||
|
||||
// Return VC credential object
|
||||
private Object testCompleteFlowWithClaimsValidationAuthorizationCode(BiFunction<String, String, CredentialRequest> credentialRequestSupplier) throws Exception {
|
||||
private void testCompleteFlowWithClaimsValidationAuthorizationCode(BiFunction<String, String, CredentialRequest> credentialRequestSupplier) throws Exception {
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext();
|
||||
|
||||
// Perform authorization code flow to get authorization code
|
||||
|
|
@ -572,25 +465,11 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
String credentialIdentifier = assertTokenResponse(tokenResponse);
|
||||
String credentialConfigurationId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
|
||||
events.clear();
|
||||
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credentialRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
Oid4vcCredentialRequest credentialRequest = getCredentialRequest(ctx, credentialRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
Oid4vcCredentialResponse credentialResponse = credentialRequest.send();
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
Object credential = assertSuccessfulCredentialResponse(credentialResponse);
|
||||
|
||||
// Verify event
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST)
|
||||
.client(client.getClientId())
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.USERNAME, "john")
|
||||
.detail(Details.CREDENTIAL_TYPE, credentialConfigurationId)
|
||||
.assertEvent();
|
||||
|
||||
return credential;
|
||||
}
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
|
||||
// Successful authorization_code flow
|
||||
|
|
@ -635,31 +514,19 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
|
||||
List<OID4VCAuthorizationDetail> authDetails = List.of(authDetail);
|
||||
String authDetailsJson = JsonSerialization.writeValueAsString(authDetails);
|
||||
|
||||
// Exchange authorization code for tokens with authorization_details
|
||||
HttpPost postToken = new HttpPost(ctx.openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> tokenParameters = new LinkedList<>();
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.AUTHORIZATION_DETAILS, authDetailsJson));
|
||||
UrlEncodedFormEntity tokenFormEntity = new UrlEncodedFormEntity(tokenParameters, StandardCharsets.UTF_8);
|
||||
postToken.setEntity(tokenFormEntity);
|
||||
|
||||
try (CloseableHttpResponse tokenHttpResponse = httpClient.execute(postToken)) {
|
||||
assertEquals(HttpStatus.SC_OK, tokenHttpResponse.getStatusLine().getStatusCode());
|
||||
String tokenResponseBody = IOUtils.toString(tokenHttpResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
return JsonSerialization.readValue(tokenResponseBody, AccessTokenResponse.class);
|
||||
}
|
||||
return oauth.accessTokenRequest(code)
|
||||
.endpoint(ctx.openidConfig.getTokenEndpoint())
|
||||
.client(client.getClientId(), "password")
|
||||
.authorizationDetails(authDetails)
|
||||
.send();
|
||||
}
|
||||
|
||||
// Test successful token response. Returns "Credential identifier" of the VC credential
|
||||
private String assertTokenResponse(AccessTokenResponse tokenResponse) throws Exception {
|
||||
// Extract authorization_details from token response
|
||||
List<OID4VCAuthorizationDetailResponse> authDetailsResponse = parseAuthorizationDetails(tokenResponse);
|
||||
List<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
|
||||
assertNotNull("authorization_details should be present in the response", authDetailsResponse);
|
||||
assertEquals(1, authDetailsResponse.size());
|
||||
|
||||
|
|
@ -677,28 +544,31 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
return credentialIdentifiers.get(0);
|
||||
}
|
||||
|
||||
private HttpPost getCredentialRequest(Oid4vcTestContext ctx, BiFunction<String, String, CredentialRequest> credentialRequestSupplier, AccessTokenResponse tokenResponse,
|
||||
String credentialConfigurationId, String credentialIdentifier) throws Exception {
|
||||
private Oid4vcCredentialRequest getCredentialRequest(Oid4vcTestContext ctx, BiFunction<String, String, CredentialRequest> credentialRequestSupplier, AccessTokenResponse tokenResponse,
|
||||
String credentialConfigurationId, String credentialIdentifier) throws Exception {
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + tokenResponse.getToken());
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
|
||||
CredentialRequest credentialRequest = credentialRequestSupplier.apply(credentialConfigurationId, credentialIdentifier);
|
||||
|
||||
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
|
||||
Oid4vcCredentialRequest request = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.endpoint(ctx.credentialIssuer.getCredentialEndpoint())
|
||||
.bearerToken(tokenResponse.getAccessToken());
|
||||
|
||||
return postCredential;
|
||||
if (credentialRequest.getCredentialConfigurationId() != null) {
|
||||
request.credentialConfigurationId(credentialRequest.getCredentialConfigurationId());
|
||||
}
|
||||
if (credentialRequest.getCredentialIdentifier() != null) {
|
||||
request.credentialIdentifier(credentialRequest.getCredentialIdentifier());
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
// Test successful credential response and returns credential object
|
||||
private Object assertSuccessfulCredentialResponse(CloseableHttpResponse credentialResponse) throws Exception {
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
private void assertSuccessfulCredentialResponse(Oid4vcCredentialResponse credentialResponse) throws Exception {
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusCode());
|
||||
|
||||
// Parse the credential response
|
||||
CredentialResponse parsedResponse = JsonSerialization.readValue(responseBody, CredentialResponse.class);
|
||||
CredentialResponse parsedResponse = credentialResponse.getCredentialResponse();
|
||||
assertNotNull("Credential response should not be null", parsedResponse);
|
||||
assertNotNull("Credentials should be present", parsedResponse.getCredentials());
|
||||
assertEquals("Should have exactly one credential", 1, parsedResponse.getCredentials().size());
|
||||
|
|
@ -713,23 +583,18 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
|
||||
// Verify the credential structure based on formatfix-authorization_details-processing
|
||||
verifyCredentialStructure(credentialObj);
|
||||
|
||||
return credentialObj;
|
||||
}
|
||||
|
||||
private void assertErrorCredentialResponse_mandatoryClaimsMissing(CloseableHttpResponse credentialResponse) throws Exception {
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
OAuth2ErrorRepresentation error = JsonSerialization.readValue(responseBody, OAuth2ErrorRepresentation.class);
|
||||
assertEquals("Credential issuance failed: No elements selected after processing claims path pointer. The requested claims are not available in the user profile.", error.getError());
|
||||
private void assertErrorCredentialResponse(Oid4vcCredentialResponse credentialResponse) throws Exception {
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, credentialResponse.getStatusCode());
|
||||
String error = credentialResponse.getError();
|
||||
assertEquals("Credential issuance failed: No elements selected after processing claims path pointer. The requested claims are not available in the user profile.", error);
|
||||
}
|
||||
|
||||
private void assertErrorCredentialResponse(CloseableHttpResponse credentialResponse, String expectedError, String expectedErrorDescription) throws Exception {
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
OAuth2ErrorRepresentation error = JsonSerialization.readValue(responseBody, OAuth2ErrorRepresentation.class);
|
||||
assertEquals(expectedError, error.getError());
|
||||
assertEquals(expectedErrorDescription, error.getErrorDescription());
|
||||
private void assertErrorCredentialResponse_mandatoryClaimsMissing(Oid4vcCredentialResponse credentialResponse) throws Exception {
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, credentialResponse.getStatusCode());
|
||||
String error = credentialResponse.getError();
|
||||
assertEquals("Credential issuance failed: No elements selected after processing claims path pointer. The requested claims are not available in the user profile.", error);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -740,23 +605,4 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
|||
// Default implementation - subclasses should override
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify credential signature on VC credential is of expected algorithm and optionally expected keyId.
|
||||
*
|
||||
* @param vcCredential Verifiable credential
|
||||
* @param expectedSignatureAlgorithm expected signature algorithm of the VC credential
|
||||
* @return JWS header used for the VC credential. Can be used for further checks in the tests
|
||||
*/
|
||||
protected abstract JWSHeader verifyCredentialSignature(Object vcCredential, String expectedSignatureAlgorithm) throws Exception;
|
||||
|
||||
/**
|
||||
* Parse authorization details from the token response.
|
||||
*/
|
||||
protected List<OID4VCAuthorizationDetailResponse> parseAuthorizationDetails(AccessTokenResponse tokenResponse) {
|
||||
return tokenResponse.getAuthorizationDetails()
|
||||
.stream()
|
||||
.map(authzDetailsResponse -> authzDetailsResponse.asSubtype(OID4VCAuthorizationDetailResponse.class))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,40 +17,26 @@
|
|||
|
||||
package org.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.ClaimsDescription;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.OpenIDProviderConfigurationResponse;
|
||||
import org.keycloak.testsuite.util.oauth.ParResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialIssuerMetadataResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.Oid4vcCredentialResponse;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
|
||||
|
|
@ -99,20 +85,19 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
Oid4vcTestContext ctx = new Oid4vcTestContext();
|
||||
|
||||
// Get credential issuer metadata
|
||||
HttpGet getCredentialIssuer = new HttpGet(getRealmMetadataPath(TEST_REALM_NAME));
|
||||
try (CloseableHttpResponse response = httpClient.execute(getCredentialIssuer)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
ctx.credentialIssuer = JsonSerialization.readValue(s, CredentialIssuer.class);
|
||||
}
|
||||
CredentialIssuerMetadataResponse metadataResponse = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(getRealmMetadataPath(TEST_REALM_NAME))
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, metadataResponse.getStatusCode());
|
||||
ctx.credentialIssuer = metadataResponse.getMetadata();
|
||||
|
||||
// Get OpenID configuration
|
||||
HttpGet getOpenidConfiguration = new HttpGet(ctx.credentialIssuer.getAuthorizationServers().get(0) + "/.well-known/openid-configuration");
|
||||
try (CloseableHttpResponse response = httpClient.execute(getOpenidConfiguration)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
ctx.openidConfig = JsonSerialization.readValue(s, OIDCConfigurationRepresentation.class);
|
||||
}
|
||||
OpenIDProviderConfigurationResponse openIDProviderConfigurationResponse = oauth.wellknownRequest()
|
||||
.url(ctx.credentialIssuer.getAuthorizationServers().get(0))
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, openIDProviderConfigurationResponse.getStatusCode());
|
||||
ctx.openidConfig = openIDProviderConfigurationResponse.getOidcConfiguration();
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
|
@ -137,31 +122,19 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
|
||||
List<OID4VCAuthorizationDetail> authDetails = List.of(authDetail);
|
||||
String authDetailsJson = JsonSerialization.writeValueAsString(authDetails);
|
||||
|
||||
// Create PAR request
|
||||
HttpPost parRequest = new HttpPost(ctx.openidConfig.getPushedAuthorizationRequestEndpoint());
|
||||
List<NameValuePair> parParameters = new LinkedList<>();
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, getCredentialClientScope().getName()));
|
||||
parParameters.add(new BasicNameValuePair("authorization_details", authDetailsJson));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.STATE, "test-state"));
|
||||
parParameters.add(new BasicNameValuePair(OIDCLoginProtocol.NONCE_PARAM, "test-nonce"));
|
||||
|
||||
UrlEncodedFormEntity parFormEntity = new UrlEncodedFormEntity(parParameters, StandardCharsets.UTF_8);
|
||||
parRequest.setEntity(parFormEntity);
|
||||
|
||||
String requestUri;
|
||||
try (CloseableHttpResponse parResponse = httpClient.execute(parRequest)) {
|
||||
assertEquals(HttpStatus.SC_CREATED, parResponse.getStatusLine().getStatusCode());
|
||||
String parResponseBody = IOUtils.toString(parResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
Map<String, Object> parResult = JsonSerialization.readValue(parResponseBody, Map.class);
|
||||
requestUri = (String) parResult.get("request_uri");
|
||||
assertNotNull("Request URI should not be null", requestUri);
|
||||
}
|
||||
ParResponse parResponse = oauth.pushedAuthorizationRequest()
|
||||
.endpoint(ctx.openidConfig.getPushedAuthorizationRequestEndpoint())
|
||||
.client(oauth.getClientId(), "password")
|
||||
.scopeParam(getCredentialClientScope().getName())
|
||||
.authorizationDetails(authDetails)
|
||||
.state("test-state")
|
||||
.nonce("test-nonce")
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_CREATED, parResponse.getStatusCode());
|
||||
String requestUri = parResponse.getRequestUri();
|
||||
assertNotNull("Request URI should not be null", requestUri);
|
||||
|
||||
// Step 2: Perform authorization with PAR
|
||||
oauth.client(client.getClientId());
|
||||
|
|
@ -173,27 +146,14 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
|
||||
// Step 3: Exchange authorization code for tokens (WITHOUT authorization_details in token request)
|
||||
// This tests that authorization_details from PAR request is processed and returned
|
||||
HttpPost postToken = new HttpPost(ctx.openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> tokenParameters = new LinkedList<>();
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
|
||||
// Note: NO authorization_details parameter in token request - it should come from PAR
|
||||
|
||||
UrlEncodedFormEntity tokenFormEntity = new UrlEncodedFormEntity(tokenParameters, StandardCharsets.UTF_8);
|
||||
postToken.setEntity(tokenFormEntity);
|
||||
|
||||
AccessTokenResponse tokenResponse;
|
||||
try (CloseableHttpResponse tokenHttpResponse = httpClient.execute(postToken)) {
|
||||
assertEquals(HttpStatus.SC_OK, tokenHttpResponse.getStatusLine().getStatusCode());
|
||||
String tokenResponseBody = IOUtils.toString(tokenHttpResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
tokenResponse = JsonSerialization.readValue(tokenResponseBody, AccessTokenResponse.class);
|
||||
}
|
||||
AccessTokenResponse tokenResponse = oauth.accessTokenRequest(code)
|
||||
.endpoint(ctx.openidConfig.getTokenEndpoint())
|
||||
.client(oauth.getClientId(), "password")
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode());
|
||||
|
||||
// Step 4: Verify authorization_details is present in token response
|
||||
List<OID4VCAuthorizationDetailResponse> authDetailsResponse = parseAuthorizationDetails(JsonSerialization.writeValueAsString(tokenResponse));
|
||||
List<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
|
||||
assertNotNull("authorization_details should be present in the response", authDetailsResponse);
|
||||
assertEquals("Should have exactly one authorization detail", 1, authDetailsResponse.size());
|
||||
|
||||
|
|
@ -224,36 +184,30 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
}
|
||||
|
||||
// Step 5: Request the actual credential using the identifier
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + tokenResponse.getToken());
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
Oid4vcCredentialResponse credentialResponse = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.endpoint(ctx.credentialIssuer.getCredentialEndpoint())
|
||||
.bearerToken(tokenResponse.getAccessToken())
|
||||
.credentialConfigurationId(credentialConfigurationId)
|
||||
.send();
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialConfigurationId(credentialConfigurationId);
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusCode());
|
||||
|
||||
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
|
||||
// Parse the credential response
|
||||
CredentialResponse parsedResponse = credentialResponse.getCredentialResponse();
|
||||
assertNotNull("Credential response should not be null", parsedResponse);
|
||||
assertNotNull("Credentials should be present", parsedResponse.getCredentials());
|
||||
assertEquals("Should have exactly one credential", 1, parsedResponse.getCredentials().size());
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
// Verify that the issued credential contains the requested claims
|
||||
CredentialResponse.Credential credentialWrapper = parsedResponse.getCredentials().get(0);
|
||||
assertNotNull("Credential wrapper should not be null", credentialWrapper);
|
||||
|
||||
// Parse the credential response
|
||||
CredentialResponse parsedResponse = JsonSerialization.readValue(responseBody, CredentialResponse.class);
|
||||
assertNotNull("Credential response should not be null", parsedResponse);
|
||||
assertNotNull("Credentials should be present", parsedResponse.getCredentials());
|
||||
assertEquals("Should have exactly one credential", 1, parsedResponse.getCredentials().size());
|
||||
Object credentialObj = credentialWrapper.getCredential();
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
|
||||
// Verify that the issued credential contains the requested claims
|
||||
CredentialResponse.Credential credentialWrapper = parsedResponse.getCredentials().get(0);
|
||||
assertNotNull("Credential wrapper should not be null", credentialWrapper);
|
||||
|
||||
Object credentialObj = credentialWrapper.getCredential();
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
|
||||
// Verify the credential structure
|
||||
verifyCredentialStructure(credentialObj);
|
||||
}
|
||||
// Verify the credential structure
|
||||
verifyCredentialStructure(credentialObj);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -268,31 +222,19 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
|
||||
List<OID4VCAuthorizationDetail> authDetails = List.of(authDetail);
|
||||
String authDetailsJson = JsonSerialization.writeValueAsString(authDetails);
|
||||
|
||||
// Create PAR request
|
||||
HttpPost parRequest = new HttpPost(ctx.openidConfig.getPushedAuthorizationRequestEndpoint());
|
||||
List<NameValuePair> parParameters = new LinkedList<>();
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, getCredentialClientScope().getName()));
|
||||
parParameters.add(new BasicNameValuePair("authorization_details", authDetailsJson));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.STATE, "test-state"));
|
||||
parParameters.add(new BasicNameValuePair(OIDCLoginProtocol.NONCE_PARAM, "test-nonce"));
|
||||
|
||||
UrlEncodedFormEntity parFormEntity = new UrlEncodedFormEntity(parParameters, StandardCharsets.UTF_8);
|
||||
parRequest.setEntity(parFormEntity);
|
||||
|
||||
String requestUri;
|
||||
try (CloseableHttpResponse parResponse = httpClient.execute(parRequest)) {
|
||||
assertEquals(HttpStatus.SC_CREATED, parResponse.getStatusLine().getStatusCode());
|
||||
String parResponseBody = IOUtils.toString(parResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
Map<String, Object> parResult = JsonSerialization.readValue(parResponseBody, Map.class);
|
||||
requestUri = (String) parResult.get("request_uri");
|
||||
assertNotNull("Request URI should not be null", requestUri);
|
||||
}
|
||||
ParResponse parResponse = oauth.pushedAuthorizationRequest()
|
||||
.endpoint(ctx.openidConfig.getPushedAuthorizationRequestEndpoint())
|
||||
.client(oauth.getClientId(), "password")
|
||||
.scopeParam(getCredentialClientScope().getName())
|
||||
.authorizationDetails(authDetails)
|
||||
.state("test-state")
|
||||
.nonce("test-nonce")
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_CREATED, parResponse.getStatusCode());
|
||||
String requestUri = parResponse.getRequestUri();
|
||||
assertNotNull("Request URI should not be null", requestUri);
|
||||
|
||||
// Step 2: Perform authorization with PAR
|
||||
oauth.client(client.getClientId());
|
||||
|
|
@ -303,24 +245,16 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
assertNotNull("Authorization code should not be null", code);
|
||||
|
||||
// Step 3: Exchange authorization code for tokens (should fail because of invalid authorization_details)
|
||||
HttpPost postToken = new HttpPost(ctx.openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> tokenParameters = new LinkedList<>();
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
|
||||
AccessTokenResponse tokenResponse = oauth.accessTokenRequest(code)
|
||||
.endpoint(ctx.openidConfig.getTokenEndpoint())
|
||||
.client(oauth.getClientId(), "password")
|
||||
.send();
|
||||
|
||||
UrlEncodedFormEntity tokenFormEntity = new UrlEncodedFormEntity(tokenParameters, StandardCharsets.UTF_8);
|
||||
postToken.setEntity(tokenFormEntity);
|
||||
|
||||
try (CloseableHttpResponse tokenHttpResponse = httpClient.execute(postToken)) {
|
||||
// Should fail because authorization_details from PAR request cannot be processed
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, tokenHttpResponse.getStatusLine().getStatusCode());
|
||||
String tokenResponseBody = IOUtils.toString(tokenHttpResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
assertTrue("Error message should indicate authorization_details processing failure",
|
||||
tokenResponseBody.contains("authorization_details was used in authorization request but cannot be processed for token response"));
|
||||
}
|
||||
// Should fail because authorization_details from PAR request cannot be processed
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, tokenResponse.getStatusCode());
|
||||
String errorDescription = tokenResponse.getErrorDescription();
|
||||
assertTrue("Error message should indicate authorization_details processing failure",
|
||||
errorDescription != null && errorDescription.contains("authorization_details was used in authorization request but cannot be processed for token response"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -328,27 +262,16 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
Oid4vcTestContext ctx = prepareOid4vcTestContext();
|
||||
|
||||
// Step 1: Create PAR request WITHOUT authorization_details
|
||||
HttpPost parRequest = new HttpPost(ctx.openidConfig.getPushedAuthorizationRequestEndpoint());
|
||||
List<NameValuePair> parParameters = new LinkedList<>();
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, getCredentialClientScope().getName()));
|
||||
parParameters.add(new BasicNameValuePair(OAuth2Constants.STATE, "test-state"));
|
||||
parParameters.add(new BasicNameValuePair(OIDCLoginProtocol.NONCE_PARAM, "test-nonce"));
|
||||
|
||||
UrlEncodedFormEntity parFormEntity = new UrlEncodedFormEntity(parParameters, StandardCharsets.UTF_8);
|
||||
parRequest.setEntity(parFormEntity);
|
||||
|
||||
String requestUri;
|
||||
try (CloseableHttpResponse parResponse = httpClient.execute(parRequest)) {
|
||||
assertEquals(HttpStatus.SC_CREATED, parResponse.getStatusLine().getStatusCode());
|
||||
String parResponseBody = IOUtils.toString(parResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
Map<String, Object> parResult = JsonSerialization.readValue(parResponseBody, Map.class);
|
||||
requestUri = (String) parResult.get("request_uri");
|
||||
assertNotNull("Request URI should not be null", requestUri);
|
||||
}
|
||||
ParResponse parResponse = oauth.pushedAuthorizationRequest()
|
||||
.endpoint(ctx.openidConfig.getPushedAuthorizationRequestEndpoint())
|
||||
.client(oauth.getClientId(), "password")
|
||||
.scopeParam(getCredentialClientScope().getName())
|
||||
.state("test-state")
|
||||
.nonce("test-nonce")
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_CREATED, parResponse.getStatusCode());
|
||||
String requestUri = parResponse.getRequestUri();
|
||||
assertNotNull("Request URI should not be null", requestUri);
|
||||
|
||||
// Step 2: Perform authorization with PAR
|
||||
oauth.client(client.getClientId());
|
||||
|
|
@ -359,26 +282,14 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
assertNotNull("Authorization code should not be null", code);
|
||||
|
||||
// Step 3: Exchange authorization code for tokens
|
||||
HttpPost postToken = new HttpPost(ctx.openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> tokenParameters = new LinkedList<>();
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId()));
|
||||
tokenParameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, "password"));
|
||||
|
||||
UrlEncodedFormEntity tokenFormEntity = new UrlEncodedFormEntity(tokenParameters, StandardCharsets.UTF_8);
|
||||
postToken.setEntity(tokenFormEntity);
|
||||
|
||||
AccessTokenResponse tokenResponse;
|
||||
try (CloseableHttpResponse tokenHttpResponse = httpClient.execute(postToken)) {
|
||||
assertEquals(HttpStatus.SC_OK, tokenHttpResponse.getStatusLine().getStatusCode());
|
||||
String tokenResponseBody = IOUtils.toString(tokenHttpResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
tokenResponse = JsonSerialization.readValue(tokenResponseBody, AccessTokenResponse.class);
|
||||
}
|
||||
AccessTokenResponse tokenResponse = oauth.accessTokenRequest(code)
|
||||
.endpoint(ctx.openidConfig.getTokenEndpoint())
|
||||
.client(oauth.getClientId(), "password")
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode());
|
||||
|
||||
// Step 4: Verify NO authorization_details in token response (since none was in PAR request)
|
||||
List<OID4VCAuthorizationDetailResponse> authDetailsResponse = parseAuthorizationDetails(JsonSerialization.writeValueAsString(tokenResponse));
|
||||
List<OID4VCAuthorizationDetailResponse> authDetailsResponse = tokenResponse.getOid4vcAuthorizationDetails();
|
||||
assertTrue("authorization_details should NOT be present in the response when not used in PAR request",
|
||||
authDetailsResponse == null || authDetailsResponse.isEmpty());
|
||||
}
|
||||
|
|
@ -391,26 +302,4 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
|||
// Default implementation - subclasses should override
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse authorization details from the token response.
|
||||
*/
|
||||
protected List<OID4VCAuthorizationDetailResponse> parseAuthorizationDetails(String responseBody) {
|
||||
try {
|
||||
// Parse the JSON response to extract authorization_details
|
||||
Map<String, Object> responseMap = JsonSerialization.readValue(responseBody, Map.class);
|
||||
Object authDetailsObj = responseMap.get("authorization_details");
|
||||
|
||||
if (authDetailsObj == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Convert to list of OID4VCAuthorizationDetailsResponse
|
||||
return JsonSerialization.readValue(JsonSerialization.writeValueAsString(authDetailsObj),
|
||||
new TypeReference<List<OID4VCAuthorizationDetailResponse>>() {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse authorization_details from response", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -42,8 +42,10 @@ import org.keycloak.services.cors.Cors;
|
|||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.util.TokenUtil;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialOfferResponse;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialOfferUriResponse;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpStatus;
|
||||
|
|
@ -106,24 +108,28 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
// Test credential offer URI endpoint with valid origin
|
||||
String offerUriUrl = getCredentialOfferUriUrl();
|
||||
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUriUrl, VALID_CORS_URL, tokenResponse.getAccessToken())) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertCorsHeaders(response, VALID_CORS_URL);
|
||||
CredentialOfferUriResponse response = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.endpoint(offerUriUrl)
|
||||
.bearerToken(tokenResponse.getAccessToken())
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
// Verify response content
|
||||
String responseBody = getResponseBody(response);
|
||||
CredentialOfferURI offerUri = JsonSerialization.readValue(responseBody, CredentialOfferURI.class);
|
||||
assertNotNull("Credential offer URI should not be null", offerUri.getIssuer());
|
||||
assertNotNull("Nonce should not be null", offerUri.getNonce());
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertCorsHeaders(response, VALID_CORS_URL);
|
||||
|
||||
// Verify CREDENTIAL_OFFER_REQUEST event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST)
|
||||
.client(clientId)
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.USERNAME, "john")
|
||||
.assertEvent();
|
||||
}
|
||||
// Verify response content
|
||||
CredentialOfferURI offerUri = response.getCredentialOfferURI();
|
||||
assertNotNull("Credential offer URI should not be null", offerUri.getIssuer());
|
||||
assertNotNull("Nonce should not be null", offerUri.getNonce());
|
||||
|
||||
// Verify CREDENTIAL_OFFER_REQUEST event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST)
|
||||
.client(clientId)
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.USERNAME, "john")
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -134,11 +140,16 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
// Test credential offer URI endpoint with invalid origin
|
||||
String offerUriUrl = getCredentialOfferUriUrl();
|
||||
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUriUrl, INVALID_CORS_URL, tokenResponse.getAccessToken())) {
|
||||
// Should still return 200 OK but without CORS headers
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertNoCorsHeaders(response);
|
||||
}
|
||||
CredentialOfferUriResponse response = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.endpoint(offerUriUrl)
|
||||
.bearerToken(tokenResponse.getAccessToken())
|
||||
.header("Origin", INVALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
// Should still return 200 OK but without CORS headers
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertNoCorsHeaders(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -176,27 +187,30 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
// Test credential offer endpoint with valid origin
|
||||
String offerUrl = getCredentialOfferUrl(nonce);
|
||||
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUrl, VALID_CORS_URL, null)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertCorsHeadersForSessionEndpoint(response, VALID_CORS_URL);
|
||||
CredentialOfferResponse response = oauth.oid4vc()
|
||||
.credentialOfferRequest()
|
||||
.endpoint(offerUrl)
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
// Verify response content
|
||||
String responseBody = getResponseBody(response);
|
||||
CredentialsOffer offer = JsonSerialization.readValue(responseBody, CredentialsOffer.class);
|
||||
assertNotNull("Credential offer should not be null", offer.getCredentialIssuer());
|
||||
assertNotNull("Credential configuration IDs should not be null", offer.getCredentialConfigurationIds());
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertCorsHeadersForSessionEndpoint(response, VALID_CORS_URL);
|
||||
|
||||
// The credential_type detail contains the credential configuration ID from the offer
|
||||
// We already assert that credentialConfigurationIds is not null and not empty above
|
||||
String expectedCredentialType = offer.getCredentialConfigurationIds().get(0);
|
||||
// Verify response content
|
||||
CredentialsOffer offer = response.getCredentialsOffer();
|
||||
assertNotNull("Credential offer should not be null", offer.getCredentialIssuer());
|
||||
assertNotNull("Credential configuration IDs should not be null", offer.getCredentialConfigurationIds());
|
||||
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST)
|
||||
.client(clientId)
|
||||
.user(AssertEvents.isUUID())
|
||||
.session((String) null) // No session for unauthenticated endpoint
|
||||
.detail(Details.CREDENTIAL_TYPE, expectedCredentialType)
|
||||
.assertEvent();
|
||||
}
|
||||
// The credential_type detail contains the credential configuration ID from the offer
|
||||
// We already assert that credentialConfigurationIds is not null and not empty above
|
||||
String expectedCredentialType = offer.getCredentialConfigurationIds().get(0);
|
||||
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST)
|
||||
.client(clientId)
|
||||
.user(AssertEvents.isUUID())
|
||||
.session((String) null) // No session for unauthenticated endpoint
|
||||
.detail(Details.CREDENTIAL_TYPE, expectedCredentialType)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -208,11 +222,15 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
// Test credential offer endpoint with invalid origin
|
||||
String offerUrl = getCredentialOfferUrl(nonce);
|
||||
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUrl, INVALID_CORS_URL, null)) {
|
||||
// Should still return 200 OK and include CORS headers (allows all origins)
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertCorsHeadersForSessionEndpoint(response, INVALID_CORS_URL);
|
||||
}
|
||||
CredentialOfferResponse response = oauth.oid4vc()
|
||||
.credentialOfferRequest()
|
||||
.endpoint(offerUrl)
|
||||
.header("Origin", INVALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
// Should still return 200 OK and include CORS headers (allows all origins)
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertCorsHeadersForSessionEndpoint(response, INVALID_CORS_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -238,9 +256,10 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
// Test credential offer URI QR code endpoint with valid origin
|
||||
String offerUriUrl = getCredentialOfferUriUrl() + "&type=qr-code";
|
||||
|
||||
// QR code endpoint returns binary data, so we need to use direct HTTP for this test
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUriUrl, VALID_CORS_URL, tokenResponse.getAccessToken())) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertCorsHeaders(response, VALID_CORS_URL);
|
||||
assertCorsHeadersFromCloseableResponse(response, VALID_CORS_URL);
|
||||
|
||||
// Verify response is PNG image
|
||||
String contentType = response.getFirstHeader("Content-Type").getValue();
|
||||
|
|
@ -255,16 +274,24 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
String offerUriUrl = getCredentialOfferUriUrl();
|
||||
|
||||
// Test with first valid origin
|
||||
try (CloseableHttpResponse response1 = makeCorsRequest(offerUriUrl, VALID_CORS_URL, tokenResponse.getAccessToken())) {
|
||||
assertEquals(HttpStatus.SC_OK, response1.getStatusLine().getStatusCode());
|
||||
assertCorsHeaders(response1, VALID_CORS_URL);
|
||||
}
|
||||
CredentialOfferUriResponse response1 = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.endpoint(offerUriUrl)
|
||||
.bearerToken(tokenResponse.getAccessToken())
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, response1.getStatusCode());
|
||||
assertCorsHeaders(response1, VALID_CORS_URL);
|
||||
|
||||
// Test with second valid origin
|
||||
try (CloseableHttpResponse response2 = makeCorsRequest(offerUriUrl, ANOTHER_VALID_CORS_URL, tokenResponse.getAccessToken())) {
|
||||
assertEquals(HttpStatus.SC_OK, response2.getStatusLine().getStatusCode());
|
||||
assertCorsHeaders(response2, ANOTHER_VALID_CORS_URL);
|
||||
}
|
||||
CredentialOfferUriResponse response2 = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.endpoint(offerUriUrl)
|
||||
.bearerToken(tokenResponse.getAccessToken())
|
||||
.header("Origin", ANOTHER_VALID_CORS_URL)
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, response2.getStatusCode());
|
||||
assertCorsHeaders(response2, ANOTHER_VALID_CORS_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -272,12 +299,16 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
// Test credential offer URI endpoint without authentication
|
||||
String offerUriUrl = getCredentialOfferUriUrl();
|
||||
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUriUrl, VALID_CORS_URL, null)) {
|
||||
// Should return 400 Bad Request
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusLine().getStatusCode());
|
||||
// Should still include CORS headers for error responses
|
||||
assertCorsHeaders(response, VALID_CORS_URL);
|
||||
}
|
||||
CredentialOfferUriResponse response = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.endpoint(offerUriUrl)
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
// Should return 400 Bad Request
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode());
|
||||
// Should still include CORS headers for error responses
|
||||
assertCorsHeaders(response, VALID_CORS_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -288,18 +319,23 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
// Test credential offer URI endpoint with invalid credential configuration ID
|
||||
String offerUriUrl = getCredentialOfferUriUrl("invalid-credential-config-id");
|
||||
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUriUrl, VALID_CORS_URL, tokenResponse.getAccessToken())) {
|
||||
// Should return 400 Bad Request
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusLine().getStatusCode());
|
||||
CredentialOfferUriResponse response = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.endpoint(offerUriUrl)
|
||||
.bearerToken(tokenResponse.getAccessToken())
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR)
|
||||
.client(clientId)
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.assertEvent();
|
||||
}
|
||||
// Should return 400 Bad Request
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode());
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR)
|
||||
.client(clientId)
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -331,20 +367,24 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
|
||||
// Try to fetch the expired credential offer
|
||||
String offerUrl = getCredentialOfferUrl(nonce);
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUrl, VALID_CORS_URL, null)) {
|
||||
// Should return 400 Bad Request
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusLine().getStatusCode());
|
||||
CredentialOfferResponse response = oauth.oid4vc()
|
||||
.credentialOfferRequest()
|
||||
.endpoint(offerUrl)
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR)
|
||||
.client((String) null)
|
||||
.user((String) null)
|
||||
.session((String) null)
|
||||
// Storage prunes expired single-use entries before lookup; lookup failure yields INVALID_REQUEST
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("No credential offer state"))
|
||||
.assertEvent();
|
||||
}
|
||||
// Should return 400 Bad Request
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode());
|
||||
|
||||
// Verify VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR event was fired
|
||||
events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR)
|
||||
.client((String) null)
|
||||
.user((String) null)
|
||||
.session((String) null)
|
||||
// Storage prunes expired single-use entries before lookup; lookup failure yields INVALID_REQUEST
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.detail(Details.REASON, Matchers.containsString("No credential offer state"))
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
|
@ -363,14 +403,16 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
private String getNonceFromOfferUri(String accessToken) throws Exception {
|
||||
String offerUriUrl = getCredentialOfferUriUrl();
|
||||
|
||||
try (CloseableHttpResponse response = makeCorsRequest(offerUriUrl, VALID_CORS_URL, accessToken)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
CredentialOfferUriResponse response = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.endpoint(offerUriUrl)
|
||||
.bearerToken(accessToken)
|
||||
.header("Origin", VALID_CORS_URL)
|
||||
.send();
|
||||
|
||||
String responseBody = getResponseBody(response);
|
||||
CredentialOfferURI offerUri = JsonSerialization.readValue(responseBody, CredentialOfferURI.class);
|
||||
|
||||
return offerUri.getNonce();
|
||||
}
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
CredentialOfferURI offerUri = response.getCredentialOfferURI();
|
||||
return offerUri.getNonce();
|
||||
}
|
||||
|
||||
private CloseableHttpResponse makeCorsRequest(String url, String origin, String accessToken) throws IOException {
|
||||
|
|
@ -398,7 +440,62 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
return new String(response.getEntity().getContent().readAllBytes());
|
||||
}
|
||||
|
||||
private void assertCorsHeaders(CloseableHttpResponse response, String expectedOrigin) {
|
||||
private void assertCorsHeaders(AbstractHttpResponse response, String expectedOrigin) {
|
||||
assertNotNull("Access-Control-Allow-Origin header should be present",
|
||||
response.getHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
assertEquals("Access-Control-Allow-Origin should match request origin",
|
||||
expectedOrigin, response.getHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
|
||||
assertNotNull("Access-Control-Allow-Credentials header should be present",
|
||||
response.getHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS));
|
||||
assertEquals("Access-Control-Allow-Credentials should be true",
|
||||
"true", response.getHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS));
|
||||
}
|
||||
|
||||
private void assertCorsHeadersForSessionEndpoint(AbstractHttpResponse response, String expectedOrigin) {
|
||||
assertNotNull("Access-Control-Allow-Origin header should be present",
|
||||
response.getHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
assertEquals("Access-Control-Allow-Origin should match request origin",
|
||||
expectedOrigin, response.getHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
|
||||
// Session-based endpoints don't require credentials since they use nonces for security
|
||||
// and allow all origins, so credentials header should be false for security reasons
|
||||
String credentialsHeader = response.getHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS);
|
||||
assertNotNull("Access-Control-Allow-Credentials header should be present for session endpoints",
|
||||
credentialsHeader);
|
||||
assertEquals("Access-Control-Allow-Credentials should be false when allowing all origins",
|
||||
"false", credentialsHeader);
|
||||
}
|
||||
|
||||
private void assertCorsPreflightHeaders(CloseableHttpResponse response, String expectedOrigin) {
|
||||
assertCorsHeadersFromCloseableResponse(response, expectedOrigin);
|
||||
|
||||
assertNotNull("Access-Control-Allow-Methods header should be present",
|
||||
response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS));
|
||||
|
||||
String allowedMethods = response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS).getValue();
|
||||
Set<String> methods = Arrays.stream(allowedMethods.split(", "))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertTrue("GET should be allowed", methods.contains("GET"));
|
||||
assertTrue("OPTIONS should be allowed", methods.contains("OPTIONS"));
|
||||
}
|
||||
|
||||
private void assertCorsPreflightHeadersForSessionEndpoint(CloseableHttpResponse response, String expectedOrigin) {
|
||||
assertCorsHeadersForSessionEndpointFromCloseableResponse(response, expectedOrigin);
|
||||
|
||||
assertNotNull("Access-Control-Allow-Methods header should be present",
|
||||
response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS));
|
||||
|
||||
String allowedMethods = response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS).getValue();
|
||||
Set<String> methods = Arrays.stream(allowedMethods.split(", "))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertTrue("GET should be allowed", methods.contains("GET"));
|
||||
assertTrue("OPTIONS should be allowed", methods.contains("OPTIONS"));
|
||||
}
|
||||
|
||||
private void assertCorsHeadersFromCloseableResponse(CloseableHttpResponse response, String expectedOrigin) {
|
||||
assertNotNull("Access-Control-Allow-Origin header should be present",
|
||||
response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
assertEquals("Access-Control-Allow-Origin should match request origin",
|
||||
|
|
@ -410,7 +507,7 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
"true", response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS).getValue());
|
||||
}
|
||||
|
||||
private void assertCorsHeadersForSessionEndpoint(CloseableHttpResponse response, String expectedOrigin) {
|
||||
private void assertCorsHeadersForSessionEndpointFromCloseableResponse(CloseableHttpResponse response, String expectedOrigin) {
|
||||
assertNotNull("Access-Control-Allow-Origin header should be present",
|
||||
response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
assertEquals("Access-Control-Allow-Origin should match request origin",
|
||||
|
|
@ -425,36 +522,8 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
|||
"false", credentialsHeader.getValue());
|
||||
}
|
||||
|
||||
private void assertCorsPreflightHeaders(CloseableHttpResponse response, String expectedOrigin) {
|
||||
assertCorsHeaders(response, expectedOrigin);
|
||||
|
||||
assertNotNull("Access-Control-Allow-Methods header should be present",
|
||||
response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS));
|
||||
|
||||
String allowedMethods = response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS).getValue();
|
||||
Set<String> methods = Arrays.stream(allowedMethods.split(", "))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertTrue("GET should be allowed", methods.contains("GET"));
|
||||
assertTrue("OPTIONS should be allowed", methods.contains("OPTIONS"));
|
||||
}
|
||||
|
||||
private void assertCorsPreflightHeadersForSessionEndpoint(CloseableHttpResponse response, String expectedOrigin) {
|
||||
assertCorsHeadersForSessionEndpoint(response, expectedOrigin);
|
||||
|
||||
assertNotNull("Access-Control-Allow-Methods header should be present",
|
||||
response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS));
|
||||
|
||||
String allowedMethods = response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS).getValue();
|
||||
Set<String> methods = Arrays.stream(allowedMethods.split(", "))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertTrue("GET should be allowed", methods.contains("GET"));
|
||||
assertTrue("OPTIONS should be allowed", methods.contains("OPTIONS"));
|
||||
}
|
||||
|
||||
private void assertNoCorsHeaders(CloseableHttpResponse response) {
|
||||
assertNull("Access-Control-Allow-Origin header should not be present", response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
assertNull("Access-Control-Allow-Credentials header should not be present", response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS));
|
||||
private void assertNoCorsHeaders(AbstractHttpResponse response) {
|
||||
assertNull("Access-Control-Allow-Origin header should not be present", response.getHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
assertNull("Access-Control-Allow-Credentials header should not be present", response.getHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import java.util.zip.DeflaterOutputStream;
|
|||
|
||||
import jakarta.ws.rs.client.Client;
|
||||
import jakarta.ws.rs.client.WebTarget;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
|
||||
|
|
@ -103,16 +102,13 @@ import org.keycloak.testsuite.Assert;
|
|||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
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.util.JsonSerialization;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.jboss.logging.Logger;
|
||||
|
|
@ -604,23 +600,15 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
|||
SupportedCredentialConfiguration offeredCredential,
|
||||
CredentialResponseHandler responseHandler,
|
||||
ClientScopeRepresentation expectedClientScope) throws IOException, VerificationException {
|
||||
Oid4vcCredentialResponse credentialRequestResponse = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.endpoint(credentialEndpoint)
|
||||
.bearerToken(token)
|
||||
.credentialConfigurationId(offeredCredential.getId())
|
||||
.send();
|
||||
|
||||
CredentialRequest request = new CredentialRequest();
|
||||
request.setCredentialConfigurationId(offeredCredential.getId());
|
||||
|
||||
StringEntity stringEntity = new StringEntity(JsonSerialization.writeValueAsString(request),
|
||||
ContentType.APPLICATION_JSON);
|
||||
|
||||
HttpPost postCredential = new HttpPost(credentialEndpoint);
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
postCredential.setEntity(stringEntity);
|
||||
|
||||
CredentialResponse credentialResponse;
|
||||
try (CloseableHttpResponse credentialRequestResponse = httpClient.execute(postCredential)) {
|
||||
assertEquals(HttpStatus.SC_OK, credentialRequestResponse.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(credentialRequestResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
credentialResponse = JsonSerialization.readValue(s, CredentialResponse.class);
|
||||
}
|
||||
assertEquals(HttpStatus.SC_OK, credentialRequestResponse.getStatusCode());
|
||||
CredentialResponse credentialResponse = credentialRequestResponse.getCredentialResponse();
|
||||
|
||||
// Use response handler to customize checks based on formats.
|
||||
responseHandler.handleCredentialResponse(credentialResponse, expectedClientScope);
|
||||
|
|
@ -628,25 +616,20 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
|||
|
||||
public CredentialIssuer getCredentialIssuerMetadata() {
|
||||
final String endpoint = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
HttpGet getMetadataRequest = new HttpGet(endpoint);
|
||||
try (CloseableHttpResponse metadataResponse = httpClient.execute(getMetadataRequest)) {
|
||||
assertEquals(HttpStatus.SC_OK, metadataResponse.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(metadataResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
return JsonSerialization.readValue(s, CredentialIssuer.class);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
CredentialIssuerMetadataResponse metadataResponse = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(endpoint)
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, metadataResponse.getStatusCode());
|
||||
return metadataResponse.getMetadata();
|
||||
}
|
||||
|
||||
public OIDCConfigurationRepresentation getAuthorizationMetadata(String authServerUrl) {
|
||||
HttpGet getOpenidConfiguration = new HttpGet(authServerUrl + "/.well-known/openid-configuration");
|
||||
try (CloseableHttpResponse response = httpClient.execute(getOpenidConfiguration)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
return JsonSerialization.readValue(s, OIDCConfigurationRepresentation.class);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
OpenIDProviderConfigurationResponse response = oauth.wellknownRequest()
|
||||
.url(authServerUrl)
|
||||
.send();
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
return response.getOidcConfiguration();
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration getSupportedCredentialConfigurationByScope(CredentialIssuer metadata, String scope) {
|
||||
|
|
|
|||
|
|
@ -71,20 +71,15 @@ import org.keycloak.testsuite.arquillian.SuiteContext;
|
|||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialIssuerMetadataResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.MediaType;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
|
@ -129,137 +124,136 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testUnsignedMetadata() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
public void testUnsignedMetadata() throws IOException {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
|
||||
// Configure realm for unsigned metadata
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "false");
|
||||
});
|
||||
// Configure realm for unsigned metadata
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "false");
|
||||
});
|
||||
|
||||
HttpGet getJsonMetadata = new HttpGet(wellKnownUri);
|
||||
getJsonMetadata.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
|
||||
try (CloseableHttpResponse response = httpClient.execute(getJsonMetadata)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertEquals("Content-Type should be application/json", MediaType.APPLICATION_JSON,
|
||||
response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue());
|
||||
String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer issuer = JsonSerialization.readValue(json, CredentialIssuer.class);
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, issuer.getCredentialIssuer());
|
||||
assertEquals("credential_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/credential",
|
||||
issuer.getCredentialEndpoint());
|
||||
assertEquals("nonce_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/nonce",
|
||||
issuer.getNonceEndpoint());
|
||||
assertNull("deferred_credential_endpoint should be omitted", issuer.getDeferredCredentialEndpoint());
|
||||
assertNotNull("authorization_servers should be present", issuer.getAuthorizationServers());
|
||||
assertNotNull("credential_response_encryption should be present", issuer.getCredentialResponseEncryption());
|
||||
assertNotNull("batch_credential_issuance should be present", issuer.getBatchCredentialIssuance());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process JSON metadata response: " + e.getMessage(), e);
|
||||
}
|
||||
CredentialIssuerMetadataResponse response = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(wellKnownUri)
|
||||
.send();
|
||||
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertEquals("Content-Type should be application/json", MediaType.APPLICATION_JSON,
|
||||
response.getHeader(HttpHeaders.CONTENT_TYPE));
|
||||
|
||||
CredentialIssuer issuer = response.getMetadata();
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, issuer.getCredentialIssuer());
|
||||
assertEquals("credential_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/credential",
|
||||
issuer.getCredentialEndpoint());
|
||||
assertEquals("nonce_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/nonce",
|
||||
issuer.getNonceEndpoint());
|
||||
assertNull("deferred_credential_endpoint should be omitted", issuer.getDeferredCredentialEndpoint());
|
||||
assertNotNull("authorization_servers should be present", issuer.getAuthorizationServers());
|
||||
assertNotNull("credential_response_encryption should be present", issuer.getCredentialResponseEncryption());
|
||||
assertNotNull("batch_credential_issuance should be present", issuer.getBatchCredentialIssuance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignedMetadata() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
public void testSignedMetadata() throws Exception {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
|
||||
// Configure realm for signed metadata
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "true");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ALG_ATTR, "RS256");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_LIFESPAN_ATTR, "3600");
|
||||
});
|
||||
// Configure realm for signed metadata
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "true");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ALG_ATTR, "RS256");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_LIFESPAN_ATTR, "3600");
|
||||
});
|
||||
|
||||
HttpGet getJwtMetadata = new HttpGet(wellKnownUri);
|
||||
getJwtMetadata.addHeader(HttpHeaders.ACCEPT, org.keycloak.utils.MediaType.APPLICATION_JWT);
|
||||
try (CloseableHttpResponse response = httpClient.execute(getJwtMetadata)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertEquals("Content-Type should be application/jwt", org.keycloak.utils.MediaType.APPLICATION_JWT,
|
||||
response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue());
|
||||
String jws = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
assertNotNull("Response should be a JWT string", jws);
|
||||
JWSInput jwsInput = new JWSInput(jws);
|
||||
CredentialIssuerMetadataResponse response = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(wellKnownUri)
|
||||
.header(HttpHeaders.ACCEPT, org.keycloak.utils.MediaType.APPLICATION_JWT)
|
||||
.send();
|
||||
|
||||
// Validate JOSE Header
|
||||
JWSHeader header = jwsInput.getHeader();
|
||||
assertEquals("Algorithm should be RS256", "RS256", header.getAlgorithm().name());
|
||||
assertEquals("Type should be openidvci-issuer-metadata+jwt",
|
||||
SIGNED_METADATA_JWT_TYPE, header.getType());
|
||||
assertNotNull("Key ID should be present", header.getKeyId());
|
||||
assertNotNull("x5c header should be present if certificates are configured", header.getX5c());
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertEquals("Content-Type should be application/jwt", org.keycloak.utils.MediaType.APPLICATION_JWT,
|
||||
response.getHeader(HttpHeaders.CONTENT_TYPE));
|
||||
|
||||
// Validate JWT claims
|
||||
Map<String, Object> claims = JsonSerialization.readValue(jwsInput.getContent(), Map.class);
|
||||
assertEquals("sub should match credential_issuer", expectedIssuer, claims.get("sub"));
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, claims.get("credential_issuer"));
|
||||
assertEquals("iss should match credential_issuer", expectedIssuer, claims.get("iss"));
|
||||
assertNotNull("iat should be present", claims.get("iat"));
|
||||
assertTrue("iat should be a number", claims.get("iat") instanceof Number);
|
||||
assertTrue("iat should be recent", ((Number) claims.get("iat")).longValue() <= Time.currentTime());
|
||||
assertNotNull("exp should be present", claims.get("exp"));
|
||||
assertTrue("exp should be a number", claims.get("exp") instanceof Number);
|
||||
assertTrue("exp should be in the future",
|
||||
((Number) claims.get("exp")).longValue() > Time.currentTime());
|
||||
assertEquals("credential_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/credential",
|
||||
claims.get("credential_endpoint"));
|
||||
assertEquals("nonce_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/nonce",
|
||||
claims.get("nonce_endpoint"));
|
||||
assertFalse("deferred_credential_endpoint should be omitted",
|
||||
claims.containsKey("deferred_credential_endpoint"));
|
||||
assertNotNull("authorization_servers should be present", claims.get("authorization_servers"));
|
||||
assertNotNull("credential_response_encryption should be present", claims.get("credential_response_encryption"));
|
||||
assertNotNull("batch_credential_issuance should be present", claims.get("batch_credential_issuance"));
|
||||
String jws = response.getContent();
|
||||
assertNotNull("Response should be a JWT string", jws);
|
||||
JWSInput jwsInput = new JWSInput(jws);
|
||||
|
||||
// Verify signature
|
||||
byte[] encodedSignatureInput = jwsInput.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8);
|
||||
byte[] signature = jwsInput.getSignature();
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
KeyWrapper keyWrapper = session.keys().getActiveKey(realm, KeyUse.SIG, "RS256");
|
||||
assertNotNull("Active signing key should exist", keyWrapper);
|
||||
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, "RS256");
|
||||
assertNotNull("Signature provider should exist for RS256", signatureProvider);
|
||||
SignatureVerifierContext verifier = signatureProvider.verifier(keyWrapper);
|
||||
boolean isValid = verifier.verify(encodedSignatureInput, signature);
|
||||
assertTrue("JWS signature should be valid", isValid);
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process JWT metadata response: " + e.getMessage(), e);
|
||||
}
|
||||
// Validate JOSE Header
|
||||
JWSHeader header = jwsInput.getHeader();
|
||||
assertEquals("Algorithm should be RS256", "RS256", header.getAlgorithm().name());
|
||||
assertEquals("Type should be openidvci-issuer-metadata+jwt",
|
||||
SIGNED_METADATA_JWT_TYPE, header.getType());
|
||||
assertNotNull("Key ID should be present", header.getKeyId());
|
||||
assertNotNull("x5c header should be present if certificates are configured", header.getX5c());
|
||||
|
||||
// Validate JWT claims
|
||||
Map<String, Object> claims = JsonSerialization.readValue(jwsInput.getContent(), Map.class);
|
||||
assertEquals("sub should match credential_issuer", expectedIssuer, claims.get("sub"));
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, claims.get("credential_issuer"));
|
||||
assertEquals("iss should match credential_issuer", expectedIssuer, claims.get("iss"));
|
||||
assertNotNull("iat should be present", claims.get("iat"));
|
||||
assertTrue("iat should be a number", claims.get("iat") instanceof Number);
|
||||
assertTrue("iat should be recent", ((Number) claims.get("iat")).longValue() <= Time.currentTime());
|
||||
assertNotNull("exp should be present", claims.get("exp"));
|
||||
assertTrue("exp should be a number", claims.get("exp") instanceof Number);
|
||||
assertTrue("exp should be in the future",
|
||||
((Number) claims.get("exp")).longValue() > Time.currentTime());
|
||||
assertEquals("credential_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/credential",
|
||||
claims.get("credential_endpoint"));
|
||||
assertEquals("nonce_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/nonce",
|
||||
claims.get("nonce_endpoint"));
|
||||
assertFalse("deferred_credential_endpoint should be omitted",
|
||||
claims.containsKey("deferred_credential_endpoint"));
|
||||
assertNotNull("authorization_servers should be present", claims.get("authorization_servers"));
|
||||
assertNotNull("credential_response_encryption should be present", claims.get("credential_response_encryption"));
|
||||
assertNotNull("batch_credential_issuance should be present", claims.get("batch_credential_issuance"));
|
||||
|
||||
// Verify signature
|
||||
byte[] encodedSignatureInput = jwsInput.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8);
|
||||
byte[] signature = jwsInput.getSignature();
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
KeyWrapper keyWrapper = session.keys().getActiveKey(realm, KeyUse.SIG, "RS256");
|
||||
assertNotNull("Active signing key should exist", keyWrapper);
|
||||
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, "RS256");
|
||||
assertNotNull("Signature provider should exist for RS256", signatureProvider);
|
||||
SignatureVerifierContext verifier = signatureProvider.verifier(keyWrapper);
|
||||
boolean isValid = verifier.verify(encodedSignatureInput, signature);
|
||||
assertTrue("JWS signature should be valid", isValid);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldServeJwtVcMetadataAtSpecCompliantEndpoint() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
String realm = TEST_REALM_NAME;
|
||||
String wellKnownUri = getSpecCompliantRealmMetadataPath(realm);
|
||||
String expectedIssuer = getRealmPath(realm);
|
||||
String realm = TEST_REALM_NAME;
|
||||
String wellKnownUri = getSpecCompliantRealmMetadataPath(realm);
|
||||
String expectedIssuer = getRealmPath(realm);
|
||||
|
||||
HttpGet get = new HttpGet(wellKnownUri);
|
||||
get.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
|
||||
try {
|
||||
CredentialIssuerMetadataResponse response = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(wellKnownUri)
|
||||
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
|
||||
.send();
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(get)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
String json = response.getContent();
|
||||
|
||||
JWTVCIssuerMetadata metadata = JsonSerialization.readValue(json, JWTVCIssuerMetadata.class);
|
||||
assertNotNull(metadata);
|
||||
assertEquals(expectedIssuer, metadata.getIssuer());
|
||||
assertNotNull("JWKS must be present", metadata.getJwks());
|
||||
|
||||
JWTVCIssuerMetadata metadata = JsonSerialization.readValue(json, JWTVCIssuerMetadata.class);
|
||||
assertNotNull(metadata);
|
||||
assertEquals(expectedIssuer, metadata.getIssuer());
|
||||
assertNotNull("JWKS must be present", metadata.getJwks());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process spec-compliant JWT VC issuer metadata response: " + e.getMessage(), e);
|
||||
}
|
||||
|
|
@ -267,119 +261,117 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
|||
|
||||
@Test
|
||||
public void shouldKeepLegacyJwtVcEndpointWithDeprecationHeaders() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
String realm = TEST_REALM_NAME;
|
||||
String wellKnownUri = getLegacyJwtVcRealmMetadataPath(realm); // legacy JWT VC path
|
||||
String realm = TEST_REALM_NAME;
|
||||
String wellKnownUri = getLegacyJwtVcRealmMetadataPath(realm); // legacy JWT VC path
|
||||
|
||||
HttpGet get = new HttpGet(wellKnownUri);
|
||||
get.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
|
||||
try {
|
||||
CredentialIssuerMetadataResponse response = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(wellKnownUri)
|
||||
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
|
||||
.send();
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
|
||||
String warning = response.getHeader("Warning");
|
||||
String deprecation = response.getHeader("Deprecation");
|
||||
String link = response.getHeader("Link");
|
||||
|
||||
assertNotNull("Warning header should be present", warning);
|
||||
assertTrue("Warning header should mention deprecated endpoint", warning.contains("Deprecated endpoint"));
|
||||
assertNotNull("Deprecation header should be present", deprecation);
|
||||
assertEquals("true", deprecation);
|
||||
assertNotNull("Link header should point to successor", link);
|
||||
assertTrue("Link header should reference spec-compliant endpoint",
|
||||
link.contains(getSpecCompliantRealmMetadataPath(realm)));
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(get)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
Header warning = response.getFirstHeader("Warning");
|
||||
Header deprecation = response.getFirstHeader("Deprecation");
|
||||
Header link = response.getFirstHeader("Link");
|
||||
assertNotNull("Warning header should be present", warning);
|
||||
assertTrue("Warning header should mention deprecated endpoint", warning.getValue().contains("Deprecated endpoint"));
|
||||
assertNotNull("Deprecation header should be present", deprecation);
|
||||
assertEquals("true", deprecation.getValue());
|
||||
assertNotNull("Link header should point to successor", link);
|
||||
assertTrue("Link header should reference spec-compliant endpoint",
|
||||
link.getValue().contains(getSpecCompliantRealmMetadataPath(realm)));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process legacy JWT VC issuer metadata response: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsignedMetadataWhenSignedDisabled() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
public void testUnsignedMetadataWhenSignedDisabled() throws IOException {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
|
||||
// Disable signed metadata
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "false");
|
||||
assertNotNull("Realm should have signed metadata disabled",
|
||||
realm.getAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR));
|
||||
});
|
||||
// Disable signed metadata
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "false");
|
||||
assertNotNull("Realm should have signed metadata disabled",
|
||||
realm.getAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR));
|
||||
});
|
||||
|
||||
HttpGet getUnsignedMetadata = new HttpGet(wellKnownUri);
|
||||
getUnsignedMetadata.addHeader(HttpHeaders.ACCEPT, org.keycloak.utils.MediaType.APPLICATION_JWT);
|
||||
try (CloseableHttpResponse response = httpClient.execute(getUnsignedMetadata)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertEquals("Content-Type should be application/json when signed metadata is disabled",
|
||||
MediaType.APPLICATION_JSON, response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue());
|
||||
String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer issuer = JsonSerialization.readValue(json, CredentialIssuer.class);
|
||||
assertNotNull("Unsigned metadata should return CredentialIssuer", issuer);
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, issuer.getCredentialIssuer());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process unsigned metadata response: " + e.getMessage(), e);
|
||||
}
|
||||
CredentialIssuerMetadataResponse response = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(wellKnownUri)
|
||||
.header(HttpHeaders.ACCEPT, org.keycloak.utils.MediaType.APPLICATION_JWT)
|
||||
.send();
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertEquals("Content-Type should be application/json when signed metadata is disabled",
|
||||
MediaType.APPLICATION_JSON, response.getHeader(HttpHeaders.CONTENT_TYPE));
|
||||
|
||||
CredentialIssuer issuer = response.getMetadata();
|
||||
assertNotNull("Unsigned metadata should return CredentialIssuer", issuer);
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, issuer.getCredentialIssuer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignedMetadataWithInvalidLifespan() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
public void testSignedMetadataWithInvalidLifespan() throws IOException {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
|
||||
// Configure invalid lifespan
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "true");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ALG_ATTR, "RS256");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_LIFESPAN_ATTR, "invalid");
|
||||
});
|
||||
// Configure invalid lifespan
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "true");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ALG_ATTR, "RS256");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_LIFESPAN_ATTR, "invalid");
|
||||
});
|
||||
|
||||
HttpGet getInvalidExpMetadata = new HttpGet(wellKnownUri);
|
||||
getInvalidExpMetadata.addHeader(HttpHeaders.ACCEPT, org.keycloak.utils.MediaType.APPLICATION_JWT);
|
||||
try (CloseableHttpResponse response = httpClient.execute(getInvalidExpMetadata)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertEquals("Content-Type should be application/json due to invalid lifespan",
|
||||
MediaType.APPLICATION_JSON, response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue());
|
||||
String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer issuer = JsonSerialization.readValue(json, CredentialIssuer.class);
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, issuer.getCredentialIssuer());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process invalid lifespan metadata response: " + e.getMessage(), e);
|
||||
}
|
||||
CredentialIssuerMetadataResponse response = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(wellKnownUri)
|
||||
.header(HttpHeaders.ACCEPT, org.keycloak.utils.MediaType.APPLICATION_JWT)
|
||||
.send();
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertEquals("Content-Type should be application/json due to invalid lifespan",
|
||||
MediaType.APPLICATION_JSON, response.getHeader(HttpHeaders.CONTENT_TYPE));
|
||||
|
||||
CredentialIssuer issuer = response.getMetadata();
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, issuer.getCredentialIssuer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignedMetadataWithInvalidAlgorithm() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
public void testSignedMetadataWithInvalidAlgorithm() throws IOException {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
|
||||
// Configure invalid algorithm
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "true");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ALG_ATTR, "INVALID_ALG");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_LIFESPAN_ATTR, "3600");
|
||||
});
|
||||
// Configure invalid algorithm
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ENABLED_ATTR, "true");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_ALG_ATTR, "INVALID_ALG");
|
||||
realm.setAttribute(OID4VCIssuerWellKnownProvider.SIGNED_METADATA_LIFESPAN_ATTR, "3600");
|
||||
});
|
||||
|
||||
HttpGet getJwtMetadata = new HttpGet(wellKnownUri);
|
||||
getJwtMetadata.addHeader(HttpHeaders.ACCEPT, org.keycloak.utils.MediaType.APPLICATION_JWT);
|
||||
try (CloseableHttpResponse response = httpClient.execute(getJwtMetadata)) {
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
assertEquals("Content-Type should be application/json due to invalid algorithm",
|
||||
MediaType.APPLICATION_JSON, response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue());
|
||||
String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer issuer = JsonSerialization.readValue(json, CredentialIssuer.class);
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, issuer.getCredentialIssuer());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process invalid algorithm metadata response: " + e.getMessage(), e);
|
||||
}
|
||||
CredentialIssuerMetadataResponse response = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(wellKnownUri)
|
||||
.header(HttpHeaders.ACCEPT, org.keycloak.utils.MediaType.APPLICATION_JWT)
|
||||
.send();
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, response.getStatusCode());
|
||||
assertEquals("Content-Type should be application/json due to invalid algorithm",
|
||||
MediaType.APPLICATION_JSON, response.getHeader(HttpHeaders.CONTENT_TYPE));
|
||||
|
||||
CredentialIssuer issuer = response.getMetadata();
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
assertEquals("credential_issuer should be set", expectedIssuer, issuer.getCredentialIssuer());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -501,38 +493,37 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
|||
|
||||
@Test
|
||||
public void testIssuerMetadataIncludesEncryptionSupport() throws IOException {
|
||||
try (Client client = AdminClientUtil.createResteasyClient()) {
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
WebTarget oid4vciDiscoveryTarget = client.target(wellKnownUri);
|
||||
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
|
||||
|
||||
try (Response discoveryResponse = oid4vciDiscoveryTarget.request().get()) {
|
||||
CredentialIssuer oid4vciIssuerConfig = JsonSerialization.readValue(
|
||||
discoveryResponse.readEntity(String.class), CredentialIssuer.class);
|
||||
CredentialIssuer oid4vciIssuerConfig = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(wellKnownUri)
|
||||
.send()
|
||||
.getMetadata();
|
||||
|
||||
assertNotNull("Encryption support should be advertised in metadata",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption());
|
||||
assertFalse("Supported algorithms should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().isEmpty());
|
||||
assertFalse("Supported encryption methods should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().isEmpty());
|
||||
assertNotNull("zip_values_supported should be present",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getZipValuesSupported());
|
||||
assertTrue("Supported algorithms should include RSA-OAEP",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().contains("RSA-OAEP"));
|
||||
assertTrue("Supported encryption methods should include A256GCM",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().contains("A256GCM"));
|
||||
assertNotNull("Credential request encryption should be advertised in metadata",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption());
|
||||
assertFalse("Supported encryption methods should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getEncValuesSupported().isEmpty());
|
||||
assertNotNull("zip_values_supported should be present",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getZipValuesSupported());
|
||||
assertTrue("Supported encryption methods should include A256GCM",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getEncValuesSupported().contains("A256GCM"));
|
||||
assertNotNull("JWKS should be present in credential request encryption",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getJwks());
|
||||
|
||||
assertNotNull("Encryption support should be advertised in metadata",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption());
|
||||
assertFalse("Supported algorithms should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().isEmpty());
|
||||
assertFalse("Supported encryption methods should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().isEmpty());
|
||||
assertNotNull("zip_values_supported should be present",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getZipValuesSupported());
|
||||
assertTrue("Supported algorithms should include RSA-OAEP",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().contains("RSA-OAEP"));
|
||||
assertTrue("Supported encryption methods should include A256GCM",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().contains("A256GCM"));
|
||||
assertNotNull("Credential request encryption should be advertised in metadata",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption());
|
||||
assertFalse("Supported encryption methods should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getEncValuesSupported().isEmpty());
|
||||
assertNotNull("zip_values_supported should be present",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getZipValuesSupported());
|
||||
assertTrue("Supported encryption methods should include A256GCM",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getEncValuesSupported().contains("A256GCM"));
|
||||
assertNotNull("JWKS should be present in credential request encryption",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getJwks());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void compareMetadataToClientScope(CredentialIssuer credentialIssuer, ClientScopeRepresentation clientScope) {
|
||||
|
|
@ -826,58 +817,58 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
|||
|
||||
@Test
|
||||
public void testOldOidcDiscoveryCompliantWellKnownUrlWithDeprecationHeaders() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
// Old OIDC Discovery compliant URL
|
||||
String oldWellKnownUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/" + TEST_REALM_NAME + "/.well-known/openid-credential-issuer";
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
// Old OIDC Discovery compliant URL
|
||||
String oldWellKnownUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/" + TEST_REALM_NAME + "/.well-known/openid-credential-issuer";
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
|
||||
HttpGet getMetadata = new HttpGet(oldWellKnownUri);
|
||||
getMetadata.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
|
||||
try {
|
||||
CredentialIssuerMetadataResponse response = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(oldWellKnownUri)
|
||||
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
|
||||
.send();
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(getMetadata)) {
|
||||
// Status & Content-Type
|
||||
assertEquals("Old well-known URL should return 200 OK",
|
||||
HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
// Status & Content-Type
|
||||
assertEquals("Old well-known URL should return 200 OK",
|
||||
HttpStatus.SC_OK, response.getStatusCode());
|
||||
|
||||
String contentType = response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue();
|
||||
assertTrue("Content-Type should be application/json",
|
||||
contentType.startsWith(MediaType.APPLICATION_JSON));
|
||||
String contentType = response.getHeader(HttpHeaders.CONTENT_TYPE);
|
||||
assertTrue("Content-Type should be application/json",
|
||||
contentType.startsWith(MediaType.APPLICATION_JSON));
|
||||
|
||||
// Headers
|
||||
Header warning = response.getFirstHeader("Warning");
|
||||
Header deprecation = response.getFirstHeader("Deprecation");
|
||||
Header link = response.getFirstHeader("Link");
|
||||
// Headers
|
||||
String warning = response.getHeader("Warning");
|
||||
String deprecation = response.getHeader("Deprecation");
|
||||
String link = response.getHeader("Link");
|
||||
|
||||
assertNotNull("Should have deprecation warning header", warning);
|
||||
assertTrue("Warning header should contain deprecation message",
|
||||
warning.getValue().contains("Deprecated endpoint"));
|
||||
assertNotNull("Should have deprecation warning header", warning);
|
||||
assertTrue("Warning header should contain deprecation message",
|
||||
warning.contains("Deprecated endpoint"));
|
||||
|
||||
assertNotNull("Should have deprecation header", deprecation);
|
||||
assertEquals("Deprecation header should be 'true'", "true", deprecation.getValue());
|
||||
assertNotNull("Should have deprecation header", deprecation);
|
||||
assertEquals("Deprecation header should be 'true'", "true", deprecation);
|
||||
|
||||
assertNotNull("Should have successor link header", link);
|
||||
assertTrue("Link header should contain successor-version",
|
||||
link.getValue().contains("successor-version"));
|
||||
assertNotNull("Should have successor link header", link);
|
||||
assertTrue("Link header should contain successor-version",
|
||||
link.contains("successor-version"));
|
||||
|
||||
// Response body
|
||||
String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer issuer = JsonSerialization.readValue(json, CredentialIssuer.class);
|
||||
// Response body
|
||||
CredentialIssuer issuer = response.getMetadata();
|
||||
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
|
||||
assertEquals("credential_issuer should be set",
|
||||
expectedIssuer, issuer.getCredentialIssuer());
|
||||
assertEquals("credential_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/credential", issuer.getCredentialEndpoint());
|
||||
assertEquals("nonce_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/nonce", issuer.getNonceEndpoint());
|
||||
assertNull("deferred_credential_endpoint should be omitted",
|
||||
issuer.getDeferredCredentialEndpoint());
|
||||
assertEquals("credential_issuer should be set",
|
||||
expectedIssuer, issuer.getCredentialIssuer());
|
||||
assertEquals("credential_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/credential", issuer.getCredentialEndpoint());
|
||||
assertEquals("nonce_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/nonce", issuer.getNonceEndpoint());
|
||||
assertNull("deferred_credential_endpoint should be omitted",
|
||||
issuer.getDeferredCredentialEndpoint());
|
||||
|
||||
assertNotNull("authorization_servers should be present", issuer.getAuthorizationServers());
|
||||
assertNotNull("credential_response_encryption should be present", issuer.getCredentialResponseEncryption());
|
||||
assertNotNull("batch_credential_issuance should be present", issuer.getBatchCredentialIssuance());
|
||||
}
|
||||
assertNotNull("authorization_servers should be present", issuer.getAuthorizationServers());
|
||||
assertNotNull("credential_response_encryption should be present", issuer.getCredentialResponseEncryption());
|
||||
assertNotNull("batch_credential_issuance should be present", issuer.getBatchCredentialIssuance());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process old well-known URL response: " + e.getMessage(), e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@
|
|||
package org.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
|
|
@ -32,7 +30,6 @@ import jakarta.ws.rs.core.HttpHeaders;
|
|||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.Time;
|
||||
|
|
@ -66,14 +63,7 @@ import org.keycloak.services.managers.AppAuthManager.BearerTokenAuthenticator;
|
|||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
|
|
@ -426,51 +416,51 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
// 1. Retrieving the credential-offer-uri
|
||||
final String credentialConfigurationId = jwtTypeCredentialClientScope.getAttributes()
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getCredentialOfferUriUrl(credentialConfigurationId));
|
||||
getCredentialOfferURI.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferURIResponse = httpClient.execute(getCredentialOfferURI);
|
||||
CredentialOfferURI credentialOfferURI = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.credentialConfigurationId(credentialConfigurationId)
|
||||
.preAuthorized(true)
|
||||
.username("john")
|
||||
.bearerToken(token)
|
||||
.send()
|
||||
.getCredentialOfferURI();
|
||||
|
||||
assertEquals("A valid offer uri should be returned",
|
||||
HttpStatus.SC_OK,
|
||||
credentialOfferURIResponse.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(credentialOfferURIResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialOfferURI credentialOfferURI = JsonSerialization.readValue(s, CredentialOfferURI.class);
|
||||
assertNotNull("A valid offer uri should be returned", credentialOfferURI);
|
||||
|
||||
// 2. Using the uri to get the actual credential offer
|
||||
HttpGet getCredentialOffer = new HttpGet(credentialOfferURI.getIssuer() + "/" + credentialOfferURI.getNonce());
|
||||
CloseableHttpResponse credentialOfferResponse = httpClient.execute(getCredentialOffer);
|
||||
CredentialsOffer credentialsOffer = oauth.oid4vc()
|
||||
.credentialOfferRequest(credentialOfferURI.getNonce())
|
||||
.bearerToken(token)
|
||||
.send()
|
||||
.getCredentialsOffer();
|
||||
|
||||
assertEquals("A valid offer should be returned", HttpStatus.SC_OK, credentialOfferResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(credentialOfferResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialsOffer credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class);
|
||||
assertNotNull("A valid offer should be returned", credentialsOffer);
|
||||
|
||||
// 3. Get the issuer metadata
|
||||
HttpGet getIssuerMetadata = new HttpGet(credentialsOffer.getIssuerMetadataUrl());
|
||||
CloseableHttpResponse issuerMetadataResponse = httpClient.execute(getIssuerMetadata);
|
||||
assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(issuerMetadataResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer credentialIssuer = JsonSerialization.readValue(s, CredentialIssuer.class);
|
||||
CredentialIssuer credentialIssuer = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(credentialsOffer.getIssuerMetadataUrl())
|
||||
.send()
|
||||
.getMetadata();
|
||||
|
||||
assertNotNull("Issuer metadata should be returned", credentialIssuer);
|
||||
assertEquals("We only expect one authorization server.", 1, credentialIssuer.getAuthorizationServers().size());
|
||||
|
||||
// 4. Get the openid-configuration
|
||||
HttpGet getOpenidConfiguration = new HttpGet(credentialIssuer.getAuthorizationServers().get(0) + "/.well-known/openid-configuration");
|
||||
CloseableHttpResponse openidConfigResponse = httpClient.execute(getOpenidConfiguration);
|
||||
assertEquals(HttpStatus.SC_OK, openidConfigResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(openidConfigResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
OIDCConfigurationRepresentation openidConfig = JsonSerialization.readValue(s, OIDCConfigurationRepresentation.class);
|
||||
OIDCConfigurationRepresentation openidConfig = oauth
|
||||
.wellknownRequest()
|
||||
.url(credentialIssuer.getAuthorizationServers().get(0))
|
||||
.send()
|
||||
.getOidcConfiguration();
|
||||
|
||||
assertNotNull("A token endpoint should be included.", openidConfig.getTokenEndpoint());
|
||||
assertTrue("The pre-authorized code should be supported.", openidConfig.getGrantTypesSupported().contains(PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
|
||||
// 5. Get an access token for the pre-authorized code
|
||||
HttpPost postPreAuthorizedCode = new HttpPost(openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
parameters.add(new BasicNameValuePair(PreAuthorizedCodeGrantTypeFactory.CODE_REQUEST_PARAM, credentialsOffer.getGrants().getPreAuthorizedCode().getPreAuthorizedCode()));
|
||||
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8);
|
||||
postPreAuthorizedCode.setEntity(formEntity);
|
||||
AccessTokenResponse accessTokenResponse = new AccessTokenResponse(httpClient.execute(postPreAuthorizedCode));
|
||||
AccessTokenResponse accessTokenResponse = oauth.oid4vc()
|
||||
.preAuthorizedCodeGrantRequest(credentialsOffer.getGrants().getPreAuthorizedCode().getPreAuthorizedCode())
|
||||
.send();
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, accessTokenResponse.getStatusCode());
|
||||
String theToken = accessTokenResponse.getAccessToken();
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,8 @@
|
|||
|
||||
package org.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
|
@ -67,16 +63,4 @@ public class OID4VCJwtAuthorizationCodeFlowTest extends OID4VCAuthorizationCodeF
|
|||
// Verify it looks like a JWT (contains dots)
|
||||
assertTrue("JWT should contain dots", jwtString.contains("."));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JWSHeader verifyCredentialSignature(Object vcCredential, String expectedSignatureAlgorithm) throws Exception {
|
||||
String vcCredentialString = vcCredential.toString();
|
||||
JWSInput jwsInput = new JWSInput(vcCredentialString);
|
||||
JWSHeader header = jwsInput.getHeader();
|
||||
|
||||
assertEquals(expectedSignatureAlgorithm, header.getRawAlgorithm());
|
||||
oauth.verifyToken(vcCredentialString, JsonWebToken.class);
|
||||
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,10 @@
|
|||
|
||||
package org.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.sdjwt.IssuerSignedJWT;
|
||||
import org.keycloak.sdjwt.vp.SdJwtVP;
|
||||
|
||||
import static org.keycloak.OID4VCConstants.SDJWT_DELIMITER;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
|
@ -71,17 +66,4 @@ public class OID4VCSdJwtAuthorizationCodeFlowTest extends OID4VCAuthorizationCod
|
|||
assertTrue("SD-JWT should contain dots", sdJwtString.contains("."));
|
||||
assertTrue("SD-JWT should contain tilde", sdJwtString.contains(SDJWT_DELIMITER));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JWSHeader verifyCredentialSignature(Object vcCredential, String expectedSignatureAlgorithm) {
|
||||
IssuerSignedJWT issuerSignedJWT = SdJwtVP.of(vcCredential.toString())
|
||||
.getIssuerSignedJWT();
|
||||
JWSHeader header = issuerSignedJWT.getJwsHeader();
|
||||
|
||||
assertEquals(expectedSignatureAlgorithm, header.getRawAlgorithm());
|
||||
|
||||
oauth.verifyToken(issuerSignedJWT.getJws(), JsonWebToken.class);
|
||||
return header;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,18 +17,14 @@
|
|||
package org.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
|
|
@ -66,15 +62,8 @@ import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
|||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
|
|
@ -303,50 +292,51 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
|
||||
// 1. Retrieving the credential-offer-uri
|
||||
final String credentialConfigurationId = clientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getCredentialOfferUriUrl(credentialConfigurationId));
|
||||
getCredentialOfferURI.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferURIResponse = httpClient.execute(getCredentialOfferURI);
|
||||
CredentialOfferURI credentialOfferURI = oauth.oid4vc()
|
||||
.credentialOfferUriRequest()
|
||||
.credentialConfigurationId(credentialConfigurationId)
|
||||
.preAuthorized(true)
|
||||
.username("john")
|
||||
.bearerToken(token)
|
||||
.send()
|
||||
.getCredentialOfferURI();
|
||||
|
||||
assertEquals("A valid offer uri should be returned", HttpStatus.SC_OK, credentialOfferURIResponse.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(credentialOfferURIResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialOfferURI credentialOfferURI = JsonSerialization.readValue(s, CredentialOfferURI.class);
|
||||
assertNotNull("A valid offer uri should be returned", credentialOfferURI);
|
||||
|
||||
// 2. Using the uri to get the actual credential offer
|
||||
HttpGet getCredentialOffer = new HttpGet(credentialOfferURI.getIssuer() + "/" + credentialOfferURI.getNonce());
|
||||
getCredentialOffer.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferResponse = httpClient.execute(getCredentialOffer);
|
||||
CredentialsOffer credentialsOffer = oauth.oid4vc()
|
||||
.credentialOfferRequest(credentialOfferURI.getNonce())
|
||||
.bearerToken(token)
|
||||
.send()
|
||||
.getCredentialsOffer();
|
||||
|
||||
assertEquals("A valid offer should be returned", HttpStatus.SC_OK, credentialOfferResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(credentialOfferResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialsOffer credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class);
|
||||
assertNotNull("A valid offer should be returned", credentialsOffer);
|
||||
|
||||
// 3. Get the issuer metadata
|
||||
HttpGet getIssuerMetadata = new HttpGet(credentialsOffer.getIssuerMetadataUrl());
|
||||
CloseableHttpResponse issuerMetadataResponse = httpClient.execute(getIssuerMetadata);
|
||||
assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(issuerMetadataResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer credentialIssuer = JsonSerialization.readValue(s, CredentialIssuer.class);
|
||||
CredentialIssuer credentialIssuer = oauth.oid4vc()
|
||||
.issuerMetadataRequest()
|
||||
.endpoint(credentialsOffer.getIssuerMetadataUrl())
|
||||
.send()
|
||||
.getMetadata();
|
||||
|
||||
assertNotNull("Issuer metadata should be returned", credentialIssuer);
|
||||
assertEquals("We only expect one authorization server.", 1, credentialIssuer.getAuthorizationServers().size());
|
||||
|
||||
// 4. Get the openid-configuration
|
||||
HttpGet getOpenidConfiguration = new HttpGet(credentialIssuer.getAuthorizationServers().get(0) + "/.well-known/openid-configuration");
|
||||
CloseableHttpResponse openidConfigResponse = httpClient.execute(getOpenidConfiguration);
|
||||
assertEquals(HttpStatus.SC_OK, openidConfigResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(openidConfigResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
OIDCConfigurationRepresentation openidConfig = JsonSerialization.readValue(s, OIDCConfigurationRepresentation.class);
|
||||
OIDCConfigurationRepresentation openidConfig = oauth
|
||||
.wellknownRequest()
|
||||
.url(credentialIssuer.getAuthorizationServers().get(0))
|
||||
.send()
|
||||
.getOidcConfiguration();
|
||||
|
||||
assertNotNull("A token endpoint should be included.", openidConfig.getTokenEndpoint());
|
||||
assertTrue("The pre-authorized code should be supported.", openidConfig.getGrantTypesSupported().contains(PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
|
||||
// 5. Get an access token for the pre-authorized code
|
||||
HttpPost postPreAuthorizedCode = new HttpPost(openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
parameters.add(new BasicNameValuePair(PreAuthorizedCodeGrantTypeFactory.CODE_REQUEST_PARAM, credentialsOffer.getGrants().getPreAuthorizedCode().getPreAuthorizedCode()));
|
||||
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8);
|
||||
postPreAuthorizedCode.setEntity(formEntity);
|
||||
AccessTokenResponse accessTokenResponse = new AccessTokenResponse(httpClient.execute(postPreAuthorizedCode));
|
||||
AccessTokenResponse accessTokenResponse = oauth.oid4vc()
|
||||
.preAuthorizedCodeGrantRequest(credentialsOffer.getGrants().getPreAuthorizedCode().getPreAuthorizedCode())
|
||||
.send();
|
||||
|
||||
assertEquals(HttpStatus.SC_OK, accessTokenResponse.getStatusCode());
|
||||
String theToken = accessTokenResponse.getAccessToken();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue