[OID4VCI] Relax CORS policy on credential offer endpoint (#43182)

Closes #43183


Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
Signed-off-by: Awambeng Rodrick <awambengrodrick@gmail.com>
Co-authored-by: Awambeng Rodrick <awambengrodrick@gmail.com>
This commit is contained in:
forkimenjeckayang
2025-11-12 14:25:20 +01:00
committed by GitHub
parent c8c110a049
commit a05ed3154c
3 changed files with 506 additions and 78 deletions

View File

@@ -27,6 +27,7 @@ import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.OPTIONS;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@@ -36,6 +37,9 @@ import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.jboss.logging.Logger;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.crypto.KeyUse;
@@ -172,7 +176,7 @@ public class OID4VCIssuerEndpoint {
private final int preAuthorizedCodeLifeSpan;
// constant for the OID4VCI enabled attribute key
private static final String OID4VCI_ENABLED_ATTRIBUTE_KEY = "oid4vci.enabled";
public static final String OID4VCI_ENABLED_ATTRIBUTE_KEY = "oid4vci.enabled";
/**
* Credential builders are responsible for initiating the production of
@@ -297,6 +301,19 @@ public class OID4VCIssuerEndpoint {
return responseBuilder.build();
}
/**
* Handles CORS preflight requests for credential offer URI endpoint.
* Preflight requests return CORS headers for all origins (standard CORS behavior).
* The actual request will validate origins against client configuration.
*/
@OPTIONS
@Path("credential-offer-uri")
public Response getCredentialOfferURIPreflight() {
configureCors(true);
cors.preflight();
return cors.add(Response.ok());
}
/**
* Provides the URI to the OID4VCI compliant credentials offer
*/
@@ -304,16 +321,10 @@ public class OID4VCIssuerEndpoint {
@Produces({MediaType.APPLICATION_JSON, RESPONSE_TYPE_IMG_PNG})
@Path("credential-offer-uri")
public Response getCredentialOfferURI(@QueryParam("credential_configuration_id") String vcId, @QueryParam("type") @DefaultValue("uri") OfferUriType type, @QueryParam("width") @DefaultValue("200") int width, @QueryParam("height") @DefaultValue("200") int height) {
configureCors(true);
AuthenticatedClientSessionModel clientSession = getAuthenticatedClientSession();
// Initialize CORS configuration and validate if the client is enabled for OID4VCI
cors = Cors.builder()
.auth()
.allowedMethods("GET")
.auth()
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
cors.allowedOrigins(session, clientSession.getClient());
checkClientEnabled();
Map<String, SupportedCredentialConfiguration> credentialsMap = OID4VCIssuerWellKnownProvider.getSupportedCredentials(session);
@@ -321,7 +332,11 @@ public class OID4VCIssuerEndpoint {
if (!credentialsMap.containsKey(vcId)) {
LOGGER.debugf("No credential with id %s exists.", vcId);
LOGGER.debugf("Supported credentials are %s.", credentialsMap);
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST));
throw new CorsErrorResponseException(
cors,
ErrorType.INVALID_CREDENTIAL_REQUEST.toString(),
"Invalid credential configuration ID",
Response.Status.BAD_REQUEST);
}
SupportedCredentialConfiguration supportedCredentialConfiguration = credentialsMap.get(vcId);
@@ -350,14 +365,17 @@ public class OID4VCIssuerEndpoint {
LOGGER.debugf("Stored credential configuration IDs for token processing: %s", credentialConfigIdsJson);
} catch (JsonProcessingException e) {
LOGGER.errorf("Could not convert the offer POJO to JSON: %s", e.getMessage());
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST));
throw new CorsErrorResponseException(
cors,
ErrorType.INVALID_CREDENTIAL_REQUEST.toString(),
"Failed to process credential offer",
Response.Status.BAD_REQUEST);
}
return switch (type) {
case URI -> getOfferUriAsUri(sessionCode);
case QR_CODE -> getOfferUriAsQr(sessionCode, width, height);
};
}
private Response getOfferUriAsUri(String sessionCode) {
@@ -365,10 +383,9 @@ public class OID4VCIssuerEndpoint {
.setIssuer(OID4VCIssuerWellKnownProvider.getIssuer(session.getContext()) + "/protocol/" + OID4VCLoginProtocolFactory.PROTOCOL_ID + "/" + CREDENTIAL_OFFER_PATH)
.setNonce(sessionCode);
return Response.ok()
return cors.add(Response.ok()
.type(MediaType.APPLICATION_JSON)
.entity(credentialOfferURI)
.build();
.entity(credentialOfferURI));
}
private Response getOfferUriAsQr(String sessionCode, int width, int height) {
@@ -378,13 +395,37 @@ public class OID4VCIssuerEndpoint {
BitMatrix bitMatrix = qrCodeWriter.encode("openid-credential-offer://?credential_offer_uri=" + encodedOfferUri, BarcodeFormat.QR_CODE, width, height);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "png", bos);
return Response.ok().type(RESPONSE_TYPE_IMG_PNG).entity(bos.toByteArray()).build();
return cors.add(Response.ok().type(RESPONSE_TYPE_IMG_PNG).entity(bos.toByteArray()));
} catch (WriterException | IOException e) {
LOGGER.warnf("Was not able to create a qr code of dimension %s:%s.", width, height, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Was not able to generate qr.").build();
}
}
/**
* Configures basic CORS for error responses before authentication
*/
private void configureCors(boolean authenticated) {
cors = Cors.builder()
.allowedMethods(HttpGet.METHOD_NAME, HttpOptions.METHOD_NAME)
.allowAllOrigins()
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS, HttpHeaders.CONTENT_TYPE);
if (authenticated) {
cors = cors.auth();
}
}
/**
* Handles CORS preflight requests for credential offer endpoint
*/
@OPTIONS
@Path(CREDENTIAL_OFFER_PATH + "{sessionCode}")
public Response getCredentialOfferPreflight(@PathParam("sessionCode") String sessionCode) {
configureCors(false);
cors.preflight();
return cors.add(Response.ok());
}
/**
* Provides an OID4VCI compliant credential offer
*/
@@ -392,6 +433,8 @@ public class OID4VCIssuerEndpoint {
@Produces(MediaType.APPLICATION_JSON)
@Path(CREDENTIAL_OFFER_PATH + "{sessionCode}")
public Response getCredentialOffer(@PathParam("sessionCode") String sessionCode) {
configureCors(false);
if (sessionCode == null) {
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST));
}
@@ -399,9 +442,8 @@ public class OID4VCIssuerEndpoint {
CredentialsOffer credentialsOffer = getOfferFromSessionCode(sessionCode);
LOGGER.debugf("Responding with offer: %s", credentialsOffer);
return Response.ok()
.entity(credentialsOffer)
.build();
return cors.add(Response.ok()
.entity(credentialsOffer));
}
private void checkScope(CredentialScopeModel requestedCredential) {
@@ -445,7 +487,7 @@ public class OID4VCIssuerEndpoint {
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST, errorMessage));
}
cors = Cors.builder().auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
cors = Cors.builder().auth().allowedMethods(HttpPost.METHOD_NAME).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
CredentialIssuer issuerMetadata = (CredentialIssuer) new OID4VCIssuerWellKnownProvider(session).getConfig();
@@ -953,7 +995,11 @@ public class OID4VCIssuerEndpoint {
getAuthenticatedClientSessionByClient(
authResult.client().getId());
if (clientSession == null) {
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_TOKEN));
throw new CorsErrorResponseException(
cors,
ErrorType.INVALID_TOKEN.toString(),
"Invalid or missing token",
Response.Status.BAD_REQUEST);
}
return clientSession;
}
@@ -961,7 +1007,11 @@ public class OID4VCIssuerEndpoint {
private AuthenticationManager.AuthResult getAuthResult() {
AuthenticationManager.AuthResult authResult = bearerTokenAuthenticator.authenticate();
if (authResult == null) {
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_TOKEN));
throw new CorsErrorResponseException(
cors,
ErrorType.INVALID_TOKEN.toString(),
"Invalid or missing token",
Response.Status.BAD_REQUEST);
}
// Validate DPoP nonce if present in the DPoP proof
@@ -982,7 +1032,11 @@ public class OID4VCIssuerEndpoint {
);
} catch (VerificationException e) {
LOGGER.debugf("DPoP nonce validation failed: %s", e.getMessage());
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_TOKEN));
throw new CorsErrorResponseException(
cors,
ErrorType.INVALID_TOKEN.toString(),
"Invalid or missing token",
Response.Status.BAD_REQUEST);
}
}
}

View File

@@ -0,0 +1,360 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oid4vc.issuance.signing;
import jakarta.ws.rs.core.HttpHeaders;
import org.apache.http.Header;
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.HttpOptions;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.representations.idm.RealmRepresentation;
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.util.JsonSerialization;
import java.io.IOException;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId;
import org.keycloak.common.Profile;
/**
* Test class for CORS functionality on OID4VCI credential offer endpoints.
* Tests both the authenticated credential-offer-uri endpoint and the
* session-based credential-offer/{sessionCode} endpoint.
*
* @author <a href="https://github.com/forkimenjeckayang">Forkim Akwichek</a>
*/
@EnableFeature(value = Profile.Feature.OID4VC_VCI, skipRestart = true)
public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
private static final String VALID_CORS_URL = "http://localtest.me:8180";
private static final String INVALID_CORS_URL = "http://invalid.localtest.me:8180";
private static final String ANOTHER_VALID_CORS_URL = "http://another.localtest.me:8180";
@Rule
public AssertEvents events = new AssertEvents(this);
@Rule
public TokenUtil tokenUtil = new TokenUtil();
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
super.configureTestRealm(testRealm);
// Find the existing client and add web origins for CORS testing
testRealm.getClients().stream()
.filter(client -> client.getClientId().equals(clientId))
.findFirst()
.ifPresent(client -> {
client.setDirectAccessGrantsEnabled(true);
client.setWebOrigins(Arrays.asList(VALID_CORS_URL, ANOTHER_VALID_CORS_URL));
});
}
@Test
public void testCredentialOfferUriCorsValidOrigin() throws Exception {
AccessTokenResponse tokenResponse = getAccessToken();
// 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);
// 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());
}
}
@Test
public void testCredentialOfferUriCorsInvalidOrigin() throws Exception {
AccessTokenResponse tokenResponse = getAccessToken();
// 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);
}
}
@Test
public void testCredentialOfferUriCorsPreflightRequest() throws Exception {
// Test preflight request for credential offer URI endpoint
String offerUriUrl = getCredentialOfferUriUrl();
try (CloseableHttpResponse response = makePreflightRequest(offerUriUrl, VALID_CORS_URL)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
assertCorsPreflightHeaders(response, VALID_CORS_URL);
}
}
@Test
public void testCredentialOfferUriCorsPreflightInvalidOrigin() throws Exception {
// Test preflight request with invalid origin
String offerUriUrl = getCredentialOfferUriUrl();
try (CloseableHttpResponse response = makePreflightRequest(offerUriUrl, INVALID_CORS_URL)) {
// Preflight should succeed and return CORS headers for all origins (standard CORS behavior)
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
assertCorsPreflightHeaders(response, INVALID_CORS_URL);
}
}
@Test
public void testCredentialOfferSessionCorsValidOrigin() throws Exception {
// First get a credential offer URI to obtain a session code
AccessTokenResponse tokenResponse = getAccessToken();
String sessionCode = getSessionCodeFromOfferUri(tokenResponse.getAccessToken());
// Test credential offer endpoint with valid origin
String offerUrl = getCredentialOfferUrl(sessionCode);
try (CloseableHttpResponse response = makeCorsRequest(offerUrl, VALID_CORS_URL, null)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
assertCorsHeadersForSessionEndpoint(response, VALID_CORS_URL);
// 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());
}
}
@Test
public void testCredentialOfferSessionCorsInvalidOrigin() throws Exception {
// First get a credential offer URI to obtain a session code
AccessTokenResponse tokenResponse = getAccessToken();
String sessionCode = getSessionCodeFromOfferUri(tokenResponse.getAccessToken());
// Test credential offer endpoint with invalid origin
String offerUrl = getCredentialOfferUrl(sessionCode);
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);
}
}
@Test
public void testCredentialOfferSessionCorsPreflightRequest() throws Exception {
// First get a credential offer URI to obtain a session code
AccessTokenResponse tokenResponse = getAccessToken();
String sessionCode = getSessionCodeFromOfferUri(tokenResponse.getAccessToken());
// Test preflight request for credential offer endpoint
String offerUrl = getCredentialOfferUrl(sessionCode);
try (CloseableHttpResponse response = makePreflightRequest(offerUrl, VALID_CORS_URL)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
assertCorsPreflightHeadersForSessionEndpoint(response, VALID_CORS_URL);
}
}
@Test
public void testCredentialOfferUriQrCodeCorsValidOrigin() throws Exception {
AccessTokenResponse tokenResponse = getAccessToken();
// Test credential offer URI QR code endpoint with valid origin
String offerUriUrl = getCredentialOfferUriUrl() + "&type=qr-code";
try (CloseableHttpResponse response = makeCorsRequest(offerUriUrl, VALID_CORS_URL, tokenResponse.getAccessToken())) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
assertCorsHeaders(response, VALID_CORS_URL);
// Verify response is PNG image
String contentType = response.getFirstHeader("Content-Type").getValue();
assertTrue("Response should be PNG image", contentType.contains("image/png"));
}
}
@Test
public void testMultipleValidOrigins() throws Exception {
// Test that multiple valid origins work
AccessTokenResponse tokenResponse = getAccessToken();
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);
}
// 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);
}
}
@Test
public void testUnauthenticatedCredentialOfferUri() throws Exception {
// 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);
}
}
// Helper methods
private AccessTokenResponse getAccessToken() throws Exception {
oauth.realm("test");
oauth.client(client.getClientId(), client.getSecret());
return oauth.doPasswordGrantRequest("john", "password");
}
private String getCredentialOfferUriUrl() {
return getBasePath("test") + "credential-offer-uri?credential_configuration_id=" + jwtTypeCredentialConfigurationIdName;
}
private String getCredentialOfferUrl(String sessionCode) {
return getBasePath("test") + "credential-offer/" + sessionCode;
}
private String getSessionCodeFromOfferUri(String accessToken) throws Exception {
String offerUriUrl = getCredentialOfferUriUrl();
try (CloseableHttpResponse response = makeCorsRequest(offerUriUrl, VALID_CORS_URL, accessToken)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
String responseBody = getResponseBody(response);
CredentialOfferURI offerUri = JsonSerialization.readValue(responseBody, CredentialOfferURI.class);
return offerUri.getNonce();
}
}
private CloseableHttpResponse makeCorsRequest(String url, String origin, String accessToken) throws IOException {
HttpGet request = new HttpGet(url);
request.setHeader("Origin", origin);
request.setHeader("Accept", "application/json");
if (accessToken != null) {
request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
}
return httpClient.execute(request);
}
private CloseableHttpResponse makePreflightRequest(String url, String origin) throws IOException {
HttpOptions request = new HttpOptions(url);
request.setHeader("Origin", origin);
request.setHeader("Access-Control-Request-Method", "GET");
request.setHeader("Access-Control-Request-Headers", "Authorization, Accept");
return httpClient.execute(request);
}
private String getResponseBody(CloseableHttpResponse response) throws IOException {
return new String(response.getEntity().getContent().readAllBytes());
}
private void assertCorsHeaders(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",
expectedOrigin, response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN).getValue());
assertNotNull("Access-Control-Allow-Credentials header should be present",
response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertEquals("Access-Control-Allow-Credentials should be true",
"true", response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS).getValue());
}
private void assertCorsHeadersForSessionEndpoint(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",
expectedOrigin, response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN).getValue());
// Session-based endpoints don't require credentials since they use session codes for security
// and allow all origins, so credentials header should be false for security reasons
Header credentialsHeader = response.getFirstHeader(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.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));
}
}

View File

@@ -60,6 +60,7 @@ import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.sdjwt.vp.SdJwtVP;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.util.JsonSerialization;
@@ -87,41 +88,51 @@ import static org.junit.Assert.fail;
public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
// ----- getCredentialOfferUri
@Test(expected = BadRequestException.class)
public void testGetCredentialOfferUriUnsupportedCredential() throws Throwable {
@Test
public void testGetCredentialOfferUriUnsupportedCredential() {
String token = getBearerToken(oauth);
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME)
.run((session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(token);
testingClient.server(TEST_REALM_NAME).run((session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(token);
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
oid4VCIssuerEndpoint.getCredentialOfferURI("inexistent-id", OfferUriType.URI, 0, 0);
})));
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
CorsErrorResponseException exception = Assert.assertThrows(CorsErrorResponseException.class, () ->
oid4VCIssuerEndpoint.getCredentialOfferURI("inexistent-id", OfferUriType.URI, 0, 0)
);
assertEquals("Should return BAD_REQUEST", Response.Status.BAD_REQUEST.getStatusCode(),
exception.getResponse().getStatus());
}));
}
@Test(expected = BadRequestException.class)
public void testGetCredentialOfferUriUnauthorized() throws Throwable {
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME)
.run((session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(null);
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0);
})));
@Test
public void testGetCredentialOfferUriUnauthorized() {
testingClient.server(TEST_REALM_NAME).run((session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(null);
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
CorsErrorResponseException exception = Assert.assertThrows(CorsErrorResponseException.class, () ->
oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0)
);
assertEquals("Should return BAD_REQUEST", Response.Status.BAD_REQUEST.getStatusCode(),
exception.getResponse().getStatus());
}));
}
@Test(expected = BadRequestException.class)
public void testGetCredentialOfferUriInvalidToken() throws Throwable {
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME)
.run((session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString("invalid-token");
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
Response response = oid4VCIssuerEndpoint
.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0);
assertEquals(MediaType.APPLICATION_JSON_TYPE, response.getMediaType());
})));
@Test
public void testGetCredentialOfferUriInvalidToken() {
testingClient.server(TEST_REALM_NAME).run((session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString("invalid-token");
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
CorsErrorResponseException exception = Assert.assertThrows(CorsErrorResponseException.class, () ->
oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0)
);
assertEquals("Should return BAD_REQUEST", Response.Status.BAD_REQUEST.getStatusCode(),
exception.getResponse().getStatus());
}));
}
@Test
@@ -247,39 +258,42 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
// ----- requestCredential
@Test(expected = BadRequestException.class)
public void testRequestCredentialUnauthorized() throws Throwable {
withCausePropagation(() -> {
testingClient.server(TEST_REALM_NAME).run(session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(null);
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
CredentialRequest credentialRequest = new CredentialRequest()
.setCredentialIdentifier("test-credential");
@Test
public void testRequestCredentialUnauthorized() {
testingClient.server(TEST_REALM_NAME).run(session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(null);
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
CredentialRequest credentialRequest = new CredentialRequest()
.setCredentialIdentifier("test-credential");
String requestPayload;
requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
String requestPayload;
requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
Response response = issuerEndpoint.requestCredential(requestPayload);
assertEquals(MediaType.APPLICATION_JSON_TYPE, response.getMediaType());
});
CorsErrorResponseException exception = Assert.assertThrows(CorsErrorResponseException.class, () ->
issuerEndpoint.requestCredential(requestPayload)
);
assertEquals("Should return BAD_REQUEST", Response.Status.BAD_REQUEST.getStatusCode(),
exception.getResponse().getStatus());
});
}
@Test(expected = BadRequestException.class)
public void testRequestCredentialInvalidToken() throws Throwable {
withCausePropagation(() -> {
testingClient.server(TEST_REALM_NAME).run(session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString("token");
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
CredentialRequest credentialRequest = new CredentialRequest()
.setCredentialIdentifier("test-credential");
@Test
public void testRequestCredentialInvalidToken() {
testingClient.server(TEST_REALM_NAME).run(session -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString("token");
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
CredentialRequest credentialRequest = new CredentialRequest()
.setCredentialIdentifier("test-credential");
String requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
String requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
issuerEndpoint.requestCredential(requestPayload);
});
CorsErrorResponseException exception = Assert.assertThrows(CorsErrorResponseException.class, () ->
issuerEndpoint.requestCredential(requestPayload)
);
assertEquals("Should return BAD_REQUEST", Response.Status.BAD_REQUEST.getStatusCode(),
exception.getResponse().getStatus());
});
}