mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -06:00
[OID4VCI] Credential Offer must be created by Issuer not Holder (#44255)
closes #44116 Signed-off-by: Thomas Diesler <tdiesler@ibm.com>
This commit is contained in:
@@ -38,7 +38,9 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||
public class PassThroughClientAuthenticator extends AbstractClientAuthenticator {
|
||||
|
||||
public static final String PROVIDER_ID = "testsuite-client-passthrough";
|
||||
|
||||
public static String clientId = "test-app";
|
||||
public static String namedClientId = "named-test-app";
|
||||
|
||||
// If this parameter is present in the HTTP request, the error will be thrown during authentication
|
||||
public static final String TEST_ERROR_PARAM = "test_error_param";
|
||||
|
||||
@@ -72,7 +72,6 @@ import org.keycloak.events.admin.AuthDetails;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.email.EmailEventListenerProviderFactory;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
@@ -85,9 +84,14 @@ import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.ResetTimeOffsetEvent;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialoffer.CredentialOfferStorage;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialoffer.CredentialOfferStorage.CredentialOfferState;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedGrant;
|
||||
import org.keycloak.protocol.oidc.encode.AccessTokenContext;
|
||||
import org.keycloak.protocol.oidc.encode.TokenContextEncoderProvider;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantType;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.representations.idm.AdminEventRepresentation;
|
||||
@@ -1121,18 +1125,24 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
||||
@NoCache
|
||||
public String getPreAuthorizedCode(@QueryParam("realm") final String realmName, @QueryParam("userSessionId") final String userSessionId, @QueryParam("clientId") final String clientId, @QueryParam("expiration") final int expiration) {
|
||||
RealmModel realm = getRealmByName(realmName);
|
||||
AuthenticatedClientSessionModel ascm = session.sessions()
|
||||
.getUserSession(realm, userSessionId)
|
||||
.getAuthenticatedClientSessions()
|
||||
.values()
|
||||
.stream().filter(acsm -> acsm.getClient().getClientId().equals(clientId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("No authenticatedClientSession found."));
|
||||
return PreAuthorizedCodeGrantType.getPreAuthorizedCode(session, ascm, expiration);
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
|
||||
|
||||
String code = "urn:oid4vci:code:" + UUID.randomUUID();
|
||||
CredentialsOffer credOffer = new CredentialsOffer()
|
||||
.setCredentialIssuer(OID4VCIssuerWellKnownProvider.getIssuer(session.getContext()))
|
||||
.setCredentialConfigurationIds(List.of("oid4vc_natural_person"))
|
||||
.setGrants(new PreAuthorizedGrant().setPreAuthorizedCode(
|
||||
new PreAuthorizedCode().setPreAuthorizedCode(code)));
|
||||
|
||||
String userId = userSession.getUser().getUsername();
|
||||
var offerStorage = session.getProvider(CredentialOfferStorage.class);
|
||||
offerStorage.putOfferState(session, new CredentialOfferState(credOffer, clientId, userId, expiration));
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/email-event-litener-provide/add-events")
|
||||
@Path("/email-event-listener-provide/add-events")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void addEventsToEmailEventListenerProvider(List<EventType> events) {
|
||||
if (events != null && !events.isEmpty()) {
|
||||
@@ -1143,7 +1153,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/email-event-litener-provide/remove-events")
|
||||
@Path("/email-event-listener-provide/remove-events")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void removeEventsToEmailEventListenerProvider(List<EventType> events) {
|
||||
if (events != null && !events.isEmpty()) {
|
||||
|
||||
@@ -457,7 +457,7 @@ public interface TestingResource {
|
||||
* @param events The events to be included
|
||||
*/
|
||||
@POST
|
||||
@Path("/email-event-litener-provide/add-events")
|
||||
@Path("/email-event-listener-provide/add-events")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void addEventsToEmailEventListenerProvider(List<EventType> events);
|
||||
|
||||
@@ -466,7 +466,7 @@ public interface TestingResource {
|
||||
* @param events The events to be removed
|
||||
*/
|
||||
@POST
|
||||
@Path("/email-event-litener-provide/remove-events")
|
||||
@Path("/email-event-listener-provide/remove-events")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void removeEventsToEmailEventListenerProvider(List<EventType> events);
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ public class PreAuthorizedGrantTest extends AbstractTestRealmKeycloakTest {
|
||||
AccessTokenResponse accessTokenResponse = postCode(preAuthorizedCode);
|
||||
|
||||
assertEquals("An access token should have successfully been returned.", HttpStatus.SC_OK, accessTokenResponse.getStatusCode());
|
||||
assertEquals("The correct session should have been used for the pre-authorized code.", userSessionId, accessTokenResponse.getSessionState());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.OAuth2Constants.AUTHORIZATION_DETAILS_PARAM;
|
||||
import static org.keycloak.OAuth2Constants.AUTHORIZATION_DETAILS;
|
||||
|
||||
public class ParEndpointTest {
|
||||
|
||||
@@ -35,12 +35,12 @@ public class ParEndpointTest {
|
||||
public void testFlattenDecodedFormParametersRetainAuthorizationDetails() {
|
||||
var decodedFormParameters = new MultivaluedHashMap<String, String>();
|
||||
String authorizationDetails = "[{\"type\": \"urn:openfinanceuae:account-access-consent:v1.0\",\"foo\":\"bar\"},{\"type\": \"urn:openfinanceuae:account-access-consent:v1.0\",\"gugu\":\"gaga\"}]";
|
||||
decodedFormParameters.put(AUTHORIZATION_DETAILS_PARAM, List.of(authorizationDetails));
|
||||
decodedFormParameters.put(AUTHORIZATION_DETAILS, List.of(authorizationDetails));
|
||||
var params = new HashMap<String, String>();
|
||||
|
||||
ParEndpoint.flattenDecodedFormParametersToParamsMap(decodedFormParameters, params);
|
||||
|
||||
Assert.assertEquals(authorizationDetails, params.get(AUTHORIZATION_DETAILS_PARAM));
|
||||
Assert.assertEquals(authorizationDetails, params.get(AUTHORIZATION_DETAILS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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.jose.jws.JWSInput;
|
||||
import org.keycloak.protocol.oid4vc.model.AuthorizationDetail;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
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;
|
||||
import org.keycloak.testsuite.oid4vc.issuance.signing.OID4VCIssuerEndpointTest;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
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.CREDENTIAL_IDENTIFIERS;
|
||||
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
|
||||
import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE;
|
||||
import static org.keycloak.protocol.oid4vc.model.ErrorType.INVALID_CREDENTIAL_OFFER_REQUEST;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
|
||||
import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId;
|
||||
import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.namedClientId;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
|
||||
/**
|
||||
* Credential Offer Validity Matrix
|
||||
* <p>
|
||||
* +----------+-----------+---------+---------+------------------------------------------------------+
|
||||
* | pre-auth | clientId | userId | Valid | Notes |
|
||||
* +----------+-----------+---------+---------+------------------------------------------------------+
|
||||
* | no | no | no | yes | Generic offer; any logged-in user may redeem. |
|
||||
* | no | no | yes | yes | Offer restricted to a specific user. |
|
||||
* | no | yes | no | yes | Bound to client; user determined at login. |
|
||||
* | no | yes | yes | yes | Bound to both client and user. |
|
||||
* +----------+-----------+---------+---------+------------------------------------------------------+
|
||||
* | yes | no | no | no | Pre-auth requires a user subject; missing userId. |
|
||||
* | yes | no | yes | yes | Pre-auth for a specific user; client issuer defined. |
|
||||
* | yes | yes | no | no | Same as above; userId required. |
|
||||
* | yes | yes | yes | yes | Fully constrained: user + client. |
|
||||
* +----------+-----------+---------+---------+------------------------------------------------------+
|
||||
*/
|
||||
public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
String issUserId = "john";
|
||||
String issClientId = clientId;
|
||||
|
||||
String namedUserId = "alice";
|
||||
|
||||
String credScopeName = jwtTypeCredentialScopeName;
|
||||
String credConfigId = jwtTypeCredentialConfigurationIdName;
|
||||
|
||||
static class OfferTestContext {
|
||||
boolean preAuthorized;
|
||||
String issUser;
|
||||
String issClient;
|
||||
String appUser;
|
||||
String appClient;
|
||||
CredentialIssuer issuerMetadata;
|
||||
OIDCConfigurationRepresentation authorizationMetadata;
|
||||
SupportedCredentialConfiguration supportedCredentialConfiguration;
|
||||
}
|
||||
|
||||
OfferTestContext newTestContext(boolean preAuth, String appClient, String appUser) {
|
||||
var ctx = new OfferTestContext();
|
||||
ctx.preAuthorized = preAuth;
|
||||
ctx.issUser = issUserId;
|
||||
ctx.issClient = issClientId;
|
||||
ctx.appUser = appUser;
|
||||
ctx.appClient = appClient;
|
||||
ctx.issuerMetadata = getCredentialIssuerMetadata();
|
||||
ctx.authorizationMetadata = getAuthorizationMetadata(ctx.issuerMetadata.getAuthorizationServers().get(0));
|
||||
ctx.supportedCredentialConfiguration = ctx.issuerMetadata.getCredentialsSupported().get(credConfigId);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVariousLogins() {
|
||||
assertNotNull(getBearerTokenAndLogout(issClientId, issUserId, "openid"));
|
||||
assertNotNull(getBearerTokenAndLogout(issClientId, namedUserId, "openid"));
|
||||
assertNotNull(getBearerTokenAndLogout(namedClientId, issUserId, "openid"));
|
||||
assertNotNull(getBearerTokenAndLogout(namedClientId, namedUserId, "openid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialWithoutOffer() throws Exception {
|
||||
|
||||
var ctx = newTestContext(false, null, namedUserId);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(credConfigId);
|
||||
authDetail.setLocations(List.of(ctx.issuerMetadata.getCredentialIssuer()));
|
||||
|
||||
// [TODO] Requires Credential scope in AuthorizationRequest although already given in AuthorizationDetails
|
||||
// https://github.com/keycloak/keycloak/issues/44320
|
||||
String accessToken = getBearerToken(issClientId, ctx.appUser, credScopeName, authDetail);
|
||||
|
||||
CredentialResponse credResponse = getCredentialByAuthDetail(ctx, accessToken, authDetail);
|
||||
verifyCredentialResponse(ctx, credResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialOffer_noPreAuth_noClientId_noUserId() throws Exception {
|
||||
runCredentialOfferTest(newTestContext(false, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialOffer_noPreAuth_noClientId_UserId() throws Exception {
|
||||
runCredentialOfferTest(newTestContext(false, null, namedUserId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialOffer_noPreAuth_ClientId_noUserId() throws Exception {
|
||||
runCredentialOfferTest(newTestContext(false, namedClientId, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialOffer_noPreAuth_ClientId_UserId() throws Exception {
|
||||
runCredentialOfferTest(newTestContext(false, namedClientId, namedUserId));
|
||||
}
|
||||
|
||||
// Pre Authorized --------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void testCredentialOffer_PreAuth_noClientId_noUserId() throws Exception {
|
||||
try {
|
||||
runCredentialOfferTest(newTestContext(true, null, null));
|
||||
fail("Expected " + INVALID_CREDENTIAL_OFFER_REQUEST.name());
|
||||
} catch (RuntimeException ex) {
|
||||
List.of(INVALID_CREDENTIAL_OFFER_REQUEST.name(), "Pre-Authorized credential offer requires a target user")
|
||||
.forEach(it -> assertTrue(ex.getMessage() + " does not contain " + it, ex.getMessage().contains(it)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialOffer_PreAuth_noClientId_UserId() throws Exception {
|
||||
runCredentialOfferTest(newTestContext(true, null, namedUserId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialOffer_PreAuth_ClientId_noUserId() throws Exception {
|
||||
try {
|
||||
runCredentialOfferTest(newTestContext(true, namedClientId, null));
|
||||
fail("Expected " + INVALID_CREDENTIAL_OFFER_REQUEST.name());
|
||||
} catch (RuntimeException ex) {
|
||||
List.of(INVALID_CREDENTIAL_OFFER_REQUEST.name(), "Pre-Authorized credential offer requires a target user")
|
||||
.forEach(it -> assertTrue(ex.getMessage() + " does not contain " + it, ex.getMessage().contains(it)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialOffer_PreAuth_ClientId_UserId() throws Exception {
|
||||
runCredentialOfferTest(newTestContext(true, namedClientId, namedUserId));
|
||||
}
|
||||
|
||||
void runCredentialOfferTest(OfferTestContext ctx) throws Exception {
|
||||
|
||||
// Issuer login
|
||||
//
|
||||
String issToken = getBearerToken(ctx.issClient, ctx.issUser, "openid");
|
||||
|
||||
// Exclude scope: <credScope>
|
||||
// Require role: credential-offer-create
|
||||
verifyTokenJwt(ctx, issToken,
|
||||
List.of(), List.of(ctx.supportedCredentialConfiguration.getScope()),
|
||||
List.of(CREDENTIAL_OFFER_CREATE.getName()), List.of());
|
||||
|
||||
// Retrieving the credential-offer-uri
|
||||
//
|
||||
String offerUri = getCredentialOfferUriUrl(ctx, issToken);
|
||||
|
||||
// Issuer logout in order to remove unwanted session state
|
||||
//
|
||||
logout(ctx.issUser);
|
||||
|
||||
try {
|
||||
|
||||
// Using the uri to get the actual credential offer
|
||||
//
|
||||
CredentialsOffer credOffer = getCredentialsOffer(ctx, offerUri);
|
||||
|
||||
if (credOffer.getCredentialConfigurationIds().size() > 1)
|
||||
throw new IllegalStateException("Multiple credential configuration ids not supported in: " + JsonSerialization.valueAsString(credOffer));
|
||||
|
||||
if (ctx.preAuthorized) {
|
||||
|
||||
// Get an access token for the pre-authorized code (PAC)
|
||||
//
|
||||
// For a PAC access token, we treat all scopes and all roles as non-meaningful.
|
||||
// The access token:
|
||||
// 1. has no authenticated user, and therefore cannot carry any user roles
|
||||
// 2. does not perform authorization-based scope filtering
|
||||
// 3. does not derive scopes from the client configuration
|
||||
// 4. does not reflect anything from the credential offer
|
||||
//
|
||||
AccessTokenResponse accessToken = getPreAuthorizedAccessTokenResponse(ctx, credOffer);
|
||||
List<AuthorizationDetail> authDetails = accessToken.getAuthorizationDetails();
|
||||
if (authDetails == null)
|
||||
throw new IllegalStateException("No authorization_details in token response");
|
||||
if (authDetails.size() > 1)
|
||||
throw new IllegalStateException("Multiple authorization_details in token response");
|
||||
|
||||
// Get the credential and verify
|
||||
//
|
||||
CredentialResponse credResponse = getCredentialByAuthDetail(ctx, accessToken.getAccessToken(), authDetails.get(0));
|
||||
verifyCredentialResponse(ctx, credResponse);
|
||||
|
||||
} else {
|
||||
|
||||
String clientId = ctx.appClient != null ? ctx.appClient : namedClientId;
|
||||
String userId = ctx.appUser != null ? ctx.appUser : namedUserId;
|
||||
String credConfigId = credOffer.getCredentialConfigurationIds().get(0);
|
||||
|
||||
SupportedCredentialConfiguration credConfig = ctx.issuerMetadata.getCredentialsSupported().get(credConfigId);
|
||||
String scope = credConfig.getScope();
|
||||
|
||||
String accessToken = getBearerToken(clientId, userId, scope);
|
||||
|
||||
// Get the credential and verify
|
||||
//
|
||||
CredentialResponse credResponse = getCredentialByOffer(ctx, accessToken, credOffer);
|
||||
verifyCredentialResponse(ctx, credResponse);
|
||||
}
|
||||
} finally {
|
||||
if (ctx.appUser != null) {
|
||||
logout(ctx.appUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
private String getBearerToken(String clientId, String username, String scope) {
|
||||
ClientRepresentation client = testRealm().clients().findByClientId(clientId).get(0);
|
||||
if (client.isDirectAccessGrantsEnabled()) {
|
||||
return getBearerTokenDirectAccess(oauth, client, username, scope).getAccessToken();
|
||||
} else {
|
||||
return getBearerTokenCodeFlow(oauth, client, username, scope).getAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
private String getBearerToken(String clientId, String username, String scope, AuthorizationDetail... authDetail) {
|
||||
ClientRepresentation client = testRealm().clients().findByClientId(clientId).get(0);
|
||||
String authCode = getAuthorizationCode(oauth, client, username, scope);
|
||||
return getBearerToken(oauth, authCode, authDetail).getAccessToken();
|
||||
}
|
||||
|
||||
private String getBearerTokenAndLogout(String clientId, String userId, String scope) {
|
||||
String token = getBearerToken(clientId, userId, scope);
|
||||
logout(userId);
|
||||
return token;
|
||||
}
|
||||
|
||||
private void logout(String userId) {
|
||||
findUserByUsernameId(testRealm(), userId).logout();
|
||||
}
|
||||
|
||||
private String getCredentialOfferUriUrl(OfferTestContext ctx, String token) throws Exception {
|
||||
CredentialOfferURI offerURI = getCredentialOfferUri(ctx, token);
|
||||
return offerURI.getIssuer() + offerURI.getNonce();
|
||||
}
|
||||
|
||||
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();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
HttpEntity entity = credentialOfferURIResponse.getEntity();
|
||||
throw new IllegalStateException(EntityUtils.toString(entity));
|
||||
}
|
||||
String s = IOUtils.toString(credentialOfferURIResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialOfferURI credentialOfferURI = JsonSerialization.valueFromString(s, CredentialOfferURI.class);
|
||||
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();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
HttpEntity entity = credentialOfferResponse.getEntity();
|
||||
throw new IllegalStateException(EntityUtils.toString(entity));
|
||||
}
|
||||
String s = IOUtils.toString(credentialOfferResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialsOffer credOffer = JsonSerialization.valueFromString(s, CredentialsOffer.class);
|
||||
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();
|
||||
if (HttpStatus.SC_OK != statusCode) {
|
||||
HttpEntity entity = accessTokenResponse.getEntity();
|
||||
throw new IllegalStateException(EntityUtils.toString(entity));
|
||||
}
|
||||
return new AccessTokenResponse(accessTokenResponse);
|
||||
}
|
||||
|
||||
private CredentialResponse getCredentialByAuthDetail(OfferTestContext ctx, String accessToken, AuthorizationDetail authDetail) throws Exception {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> credIdentifiers = (List<String>) authDetail.getAdditionalFields().get(CREDENTIAL_IDENTIFIERS);
|
||||
var credentialRequest = new CredentialRequest();
|
||||
if (credIdentifiers != null) {
|
||||
if (credIdentifiers.size() > 1)
|
||||
throw new IllegalStateException("Multiple credential ids not supported");
|
||||
credentialRequest.setCredentialIdentifier(credIdentifiers.get(0));
|
||||
} else {
|
||||
if (authDetail.getCredentialConfigurationId() == null)
|
||||
throw new IllegalStateException("No credential_configuration_id in: " + JsonSerialization.valueAsString(authDetail));
|
||||
credentialRequest.setCredentialConfigurationId(authDetail.getCredentialConfigurationId());
|
||||
}
|
||||
return sendCredentialRequest(ctx, accessToken, credentialRequest);
|
||||
}
|
||||
|
||||
private CredentialResponse getCredentialByOffer(OfferTestContext ctx, String accessToken, CredentialsOffer credOffer) throws Exception {
|
||||
List<String> credConfigIds = credOffer.getCredentialConfigurationIds();
|
||||
if (credConfigIds.size() > 1)
|
||||
throw new IllegalStateException("Multiple credential configuration ids not supported in: " + JsonSerialization.valueAsString(credOffer));
|
||||
var credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialConfigurationId(credConfigIds.get(0));
|
||||
return sendCredentialRequest(ctx, accessToken, credentialRequest);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
String s = IOUtils.toString(credentialRequestResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialResponse credentialResponse = JsonSerialization.valueFromString(s, CredentialResponse.class);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void verifyCredentialResponse(OfferTestContext ctx, CredentialResponse credResponse) throws Exception {
|
||||
|
||||
String scope = ctx.supportedCredentialConfiguration.getScope();
|
||||
CredentialResponse.Credential credentialObj = credResponse.getCredentials().get(0);
|
||||
assertNotNull("The first credential in the array should not be null", credentialObj);
|
||||
|
||||
String expUsername = ctx.appUser != null ? ctx.appUser : namedUserId;
|
||||
|
||||
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialObj.getCredential(), JsonWebToken.class).getToken();
|
||||
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
|
||||
Object vc = jsonWebToken.getOtherClaims().get("vc");
|
||||
VerifiableCredential credential = JsonSerialization.mapper.convertValue(vc, VerifiableCredential.class);
|
||||
assertEquals(List.of(scope), credential.getType());
|
||||
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
|
||||
assertEquals(expUsername + "@email.cz", credential.getCredentialSubject().getClaims().get("email"));
|
||||
}
|
||||
|
||||
private void verifyTokenJwt(
|
||||
OfferTestContext ctx,
|
||||
String token,
|
||||
List<String> includeScopes,
|
||||
List<String> excludeScopes,
|
||||
List<String> includeRoles,
|
||||
List<String> excludeRoles
|
||||
) throws Exception {
|
||||
JsonWebToken jwt = JsonSerialization.readValue(new JWSInput(token).getContent(), JsonWebToken.class);
|
||||
List<String> wasScopes = Arrays.stream(((String) jwt.getOtherClaims().get("scope")).split("\\s")).toList();
|
||||
includeScopes.forEach(it -> assertTrue("Missing scope: " + it, wasScopes.contains(it)));
|
||||
excludeScopes.forEach(it -> assertFalse("Invalid scope: " + it, wasScopes.contains(it)));
|
||||
|
||||
List<String> allRoles = new ArrayList<>();
|
||||
Object realmAccess = jwt.getOtherClaims().get("realm_access");
|
||||
if (realmAccess != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var realmRoles = ((Map<String, List<String>>) realmAccess).get("roles");
|
||||
allRoles.addAll(realmRoles);
|
||||
}
|
||||
Object resourceAccess = jwt.getOtherClaims().get("resource_access");
|
||||
if (resourceAccess != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var resourceAccessMapping = (Map<String, Map<String, List<String>>>) resourceAccess;
|
||||
resourceAccessMapping.forEach((k, v) -> {
|
||||
allRoles.addAll(v.get("roles"));
|
||||
});
|
||||
}
|
||||
includeRoles.forEach(it -> assertTrue("Missing role: " + it, allRoles.contains(it)));
|
||||
excludeRoles.forEach(it -> assertFalse("Invalid role: " + it, allRoles.contains(it)));
|
||||
}
|
||||
}
|
||||
@@ -115,15 +115,12 @@ public class OID4VCTargetRoleMapperTest extends OID4VCTest {
|
||||
return mergedRoles;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
testRealm.getRoles()
|
||||
.setClient(Map.of(existingClient.getClientId(),
|
||||
List.of(getRoleRepresentation("testRole", existingClient.getClientId()))));
|
||||
}
|
||||
|
||||
List<UserRepresentation> realmUsers = Optional.ofNullable(testRealm.getUsers()).map(ArrayList::new)
|
||||
.orElse(new ArrayList<>());
|
||||
realmUsers.add(getUserRepresentation(Map.of(existingClient.getClientId(), List.of("testRole"), "newClient", List.of("newRole"))));
|
||||
realmUsers.add(getUserRepresentation("John Doe", List.of(),
|
||||
Map.of(clientId, List.of("testRole"), "newClient", List.of("newRole"))));
|
||||
testRealm.setUsers(realmUsers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
@@ -63,8 +65,6 @@ import static org.junit.Assert.assertNotNull;
|
||||
*/
|
||||
public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEndpointTest {
|
||||
|
||||
public static final String OPENID_CREDENTIAL_TYPE = "openid_credential";
|
||||
|
||||
/**
|
||||
* Test context for OID4VC tests
|
||||
*/
|
||||
@@ -140,7 +140,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
||||
claim.setMandatory(true);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
|
||||
authDetail.setClaims(Arrays.asList(claim));
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
@@ -176,8 +176,8 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
||||
assertNotNull("Credential identifiers should be present", authDetailResponse.getCredentialIdentifiers());
|
||||
assertEquals(1, authDetailResponse.getCredentialIdentifiers().size());
|
||||
|
||||
String credentialIdentifier = authDetailResponse.getCredentialIdentifiers().get(0);
|
||||
assertNotNull("Credential identifier should not be null", credentialIdentifier);
|
||||
String credentialConfigurationId = authDetailResponse.getCredentialConfigurationId();
|
||||
assertNotNull("Credential configuration id should not be null", credentialConfigurationId);
|
||||
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
@@ -185,7 +185,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialIdentifier(credentialIdentifier);
|
||||
credentialRequest.setCredentialConfigurationId(credentialConfigurationId);
|
||||
|
||||
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
|
||||
|
||||
@@ -53,7 +53,7 @@ import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsProcessor.OPENID_CREDENTIAL_TYPE;
|
||||
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -131,7 +131,7 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
||||
claim.setMandatory(true);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(credentialConfigurationId);
|
||||
authDetail.setClaims(List.of(claim));
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
@@ -198,7 +198,7 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
||||
assertEquals("Should have exactly one authorization detail", 1, authDetailsResponse.size());
|
||||
|
||||
OID4VCAuthorizationDetailsResponse authDetailResponse = authDetailsResponse.get(0);
|
||||
assertEquals("Type should be openid_credential", OPENID_CREDENTIAL_TYPE, authDetailResponse.getType());
|
||||
assertEquals("Type should be openid_credential", OPENID_CREDENTIAL, authDetailResponse.getType());
|
||||
assertEquals("Credential configuration ID should match", credentialConfigurationId, authDetailResponse.getCredentialConfigurationId());
|
||||
|
||||
// Verify claims are preserved
|
||||
@@ -229,7 +229,7 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialIdentifier(credentialIdentifier);
|
||||
credentialRequest.setCredentialConfigurationId(credentialConfigurationId);
|
||||
|
||||
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
|
||||
@@ -263,7 +263,7 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
|
||||
// Step 1: Create PAR request with INVALID authorization_details
|
||||
// Create authorization details with INVALID credential configuration ID
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId("INVALID_CONFIG_ID"); // This should cause failure
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
@@ -56,7 +57,7 @@ import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsProcessor.OPENID_CREDENTIAL_TYPE;
|
||||
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -103,7 +104,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
Oid4vcTestContext ctx = new Oid4vcTestContext();
|
||||
|
||||
String credentialConfigurationId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getBasePath(TEST_REALM_NAME) + "credential-offer-uri?credential_configuration_id=" + credentialConfigurationId);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getCredentialOfferUriUrl(credentialConfigurationId));
|
||||
getCredentialOfferURI.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
|
||||
CredentialOfferURI credentialOfferURI;
|
||||
@@ -144,7 +145,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext(token);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
|
||||
@@ -166,7 +167,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
assertNotNull("authorization_details should be present in the response", authDetailsResponse);
|
||||
assertEquals(1, authDetailsResponse.size());
|
||||
OID4VCAuthorizationDetailsResponse authDetailResponse = authDetailsResponse.get(0);
|
||||
assertEquals(OPENID_CREDENTIAL_TYPE, authDetailResponse.getType());
|
||||
assertEquals(OPENID_CREDENTIAL, authDetailResponse.getType());
|
||||
assertEquals(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID), authDetailResponse.getCredentialConfigurationId());
|
||||
assertNotNull(authDetailResponse.getCredentialIdentifiers());
|
||||
assertEquals(1, authDetailResponse.getCredentialIdentifiers().size());
|
||||
@@ -202,7 +203,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
claim.setMandatory(true);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
|
||||
authDetail.setClaims(Arrays.asList(claim));
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
@@ -225,7 +226,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
assertNotNull("authorization_details should be present in the response", authDetailsResponse);
|
||||
assertEquals(1, authDetailsResponse.size());
|
||||
OID4VCAuthorizationDetailsResponse authDetailResponse = authDetailsResponse.get(0);
|
||||
assertEquals(OPENID_CREDENTIAL_TYPE, authDetailResponse.getType());
|
||||
assertEquals(OPENID_CREDENTIAL, authDetailResponse.getType());
|
||||
assertEquals(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID), authDetailResponse.getCredentialConfigurationId());
|
||||
assertNotNull(authDetailResponse.getClaims());
|
||||
assertEquals(1, authDetailResponse.getClaims().size());
|
||||
@@ -257,7 +258,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
claim.setMandatory(false);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
|
||||
authDetail.setClaims(Arrays.asList(claim));
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
@@ -293,7 +294,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
claim.setMandatory(true);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
|
||||
authDetail.setClaims(Arrays.asList(claim));
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
@@ -329,7 +330,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
claim.setMandatory(false);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
|
||||
authDetail.setClaims(Arrays.asList(claim));
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
@@ -369,7 +370,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext(token);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
// Missing credential_configuration_id - should fail
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
|
||||
@@ -403,7 +404,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
claim.setMandatory(false);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
|
||||
authDetail.setClaims(Arrays.asList(claim));
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
@@ -481,7 +482,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
String expectedConfigId = ctx.credentialsOffer.getCredentialConfigurationIds().get(i);
|
||||
OID4VCAuthorizationDetailsResponse authDetailResponse = authDetailsResponse.get(i);
|
||||
|
||||
assertEquals(OPENID_CREDENTIAL_TYPE, authDetailResponse.getType());
|
||||
assertEquals(OPENID_CREDENTIAL, authDetailResponse.getType());
|
||||
assertEquals("Credential configuration ID should match the one from the offer",
|
||||
expectedConfigId, authDetailResponse.getCredentialConfigurationId());
|
||||
assertNotNull("Credential identifiers should be present", authDetailResponse.getCredentialIdentifiers());
|
||||
@@ -502,18 +503,22 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
@Test
|
||||
public void testCompleteFlowWithCredentialOfferBasedAuthorizationDetails() throws Exception {
|
||||
String token = getBearerToken(oauth, client, getCredentialClientScope().getName());
|
||||
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext(token);
|
||||
PreAuthorizedCode preAuthorizedCode = ctx.credentialsOffer.getGrants().getPreAuthorizedCode();
|
||||
|
||||
// Step 1: Request token without authorization_details parameter (no scope needed)
|
||||
HttpPost postPreAuthorizedCode = new HttpPost(ctx.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, ctx.credentialsOffer.getGrants().getPreAuthorizedCode().getPreAuthorizedCode()));
|
||||
parameters.add(new BasicNameValuePair(PreAuthorizedCodeGrantTypeFactory.CODE_REQUEST_PARAM, preAuthorizedCode.getPreAuthorizedCode()));
|
||||
|
||||
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8);
|
||||
postPreAuthorizedCode.setEntity(formEntity);
|
||||
|
||||
String credentialIdentifier = null;
|
||||
String credentialIdentifier;
|
||||
String credentialConfigurationId;
|
||||
OID4VCAuthorizationDetailsResponse authDetailResponse;
|
||||
try (CloseableHttpResponse tokenResponse = httpClient.execute(postPreAuthorizedCode)) {
|
||||
assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(tokenResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
@@ -524,46 +529,94 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
ctx.credentialsOffer.getCredentialConfigurationIds().size(), authDetailsResponse.size());
|
||||
|
||||
// Use the first authorization detail for credential request
|
||||
OID4VCAuthorizationDetailsResponse authDetailResponse = authDetailsResponse.get(0);
|
||||
authDetailResponse = authDetailsResponse.get(0);
|
||||
assertNotNull("Credential identifiers should be present", authDetailResponse.getCredentialIdentifiers());
|
||||
assertEquals(1, authDetailResponse.getCredentialIdentifiers().size());
|
||||
|
||||
credentialIdentifier = authDetailResponse.getCredentialIdentifiers().get(0);
|
||||
assertNotNull("Credential identifier should not be null", credentialIdentifier);
|
||||
|
||||
credentialConfigurationId = authDetailResponse.getCredentialConfigurationId();
|
||||
assertNotNull("Credential configuration id should not be null", credentialConfigurationId);
|
||||
}
|
||||
|
||||
// Step 2: Request the actual credential using ONLY the identifier (no credential_configuration_id)
|
||||
// This tests that the mapping from credential identifier to credential configuration ID works correctly
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
// This tests that the mapping from credential identifier to credential configuration ID works as expected.
|
||||
//
|
||||
// The Pre-Authorized code flow is treated as a separate authentication event.
|
||||
// Even if the underlying user and client match an existing session.
|
||||
// A new user session is created because:
|
||||
// * The pre-auth code is defined as a standalone authentication mechanism.
|
||||
// * It does not assume the caller already has an authenticated session.
|
||||
// * It must guarantee isolation of state tied to the VC issuance flow.
|
||||
{
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialIdentifier(credentialIdentifier);
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialIdentifier(credentialIdentifier);
|
||||
|
||||
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
|
||||
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
|
||||
// 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());
|
||||
// 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());
|
||||
|
||||
// Step 3: Verify that the issued credential structure is valid
|
||||
CredentialResponse.Credential credentialWrapper = parsedResponse.getCredentials().get(0);
|
||||
assertNotNull("Credential wrapper should not be null", credentialWrapper);
|
||||
// Step 3: Verify that the issued credential structure is valid
|
||||
CredentialResponse.Credential credentialWrapper = parsedResponse.getCredentials().get(0);
|
||||
assertNotNull("Credential wrapper should not be null", credentialWrapper);
|
||||
|
||||
// The credential is stored as Object, so we need to cast it
|
||||
Object credentialObj = credentialWrapper.getCredential();
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
// The credential is stored as Object, so we need to cast it
|
||||
Object credentialObj = credentialWrapper.getCredential();
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
|
||||
// Verify the credential structure based on format
|
||||
verifyCredentialStructure(credentialObj);
|
||||
// Verify the credential structure based on format
|
||||
verifyCredentialStructure(credentialObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Request a credential using the credentialConfigurationId
|
||||
//
|
||||
{
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialConfigurationId(credentialConfigurationId);
|
||||
|
||||
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
|
||||
// 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());
|
||||
|
||||
// Step 3: Verify that the issued credential structure is valid
|
||||
CredentialResponse.Credential credentialWrapper = parsedResponse.getCredentials().get(0);
|
||||
assertNotNull("Credential wrapper should not be null", credentialWrapper);
|
||||
|
||||
// The credential is stored as Object, so we need to cast it
|
||||
Object credentialObj = credentialWrapper.getCredential();
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
|
||||
// Verify the credential structure based on format
|
||||
verifyCredentialStructure(credentialObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,7 +656,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
String expectedConfigId = ctx.credentialsOffer.getCredentialConfigurationIds().get(i);
|
||||
|
||||
// Verify structure
|
||||
assertEquals("Type should be openid_credential", OPENID_CREDENTIAL_TYPE, authDetail.getType());
|
||||
assertEquals("Type should be openid_credential", OPENID_CREDENTIAL, authDetail.getType());
|
||||
assertEquals("Credential configuration ID should match the one from the offer",
|
||||
expectedConfigId, authDetail.getCredentialConfigurationId());
|
||||
assertNotNull("Credential identifiers should be present", authDetail.getCredentialIdentifiers());
|
||||
@@ -633,6 +686,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
@Test
|
||||
public void testCompleteFlowWithClaimsValidation() throws Exception {
|
||||
String token = getBearerToken(oauth, client, getCredentialClientScope().getName());
|
||||
String credConfigId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext(token);
|
||||
|
||||
// Step 1: Request token with authorization details containing specific claims
|
||||
@@ -650,13 +704,13 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
claim.setMandatory(true);
|
||||
|
||||
AuthorizationDetail authDetail = new AuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL_TYPE);
|
||||
authDetail.setCredentialConfigurationId(getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID));
|
||||
authDetail.setClaims(Arrays.asList(claim));
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(credConfigId);
|
||||
authDetail.setLocations(Collections.singletonList(ctx.credentialIssuer.getCredentialIssuer()));
|
||||
authDetail.setClaims(List.of(claim));
|
||||
|
||||
List<AuthorizationDetail> authDetails = List.of(authDetail);
|
||||
String authDetailsJson = JsonSerialization.writeValueAsString(authDetails);
|
||||
String authDetailsJson = JsonSerialization.valueAsString(authDetails);
|
||||
|
||||
HttpPost postPreAuthorizedCode = new HttpPost(ctx.openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
@@ -667,6 +721,8 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
postPreAuthorizedCode.setEntity(formEntity);
|
||||
|
||||
String credentialIdentifier;
|
||||
String credentialConfigurationId;
|
||||
OID4VCAuthorizationDetailsResponse authDetailResponse;
|
||||
try (CloseableHttpResponse tokenResponse = httpClient.execute(postPreAuthorizedCode)) {
|
||||
assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(tokenResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
@@ -674,15 +730,18 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
|
||||
assertNotNull("authorization_details should be present in the response", authDetailsResponse);
|
||||
assertEquals(1, authDetailsResponse.size());
|
||||
|
||||
OID4VCAuthorizationDetailsResponse authDetailResponse = authDetailsResponse.get(0);
|
||||
authDetailResponse = authDetailsResponse.get(0);
|
||||
assertNotNull("Credential identifiers should be present", authDetailResponse.getCredentialIdentifiers());
|
||||
assertEquals(1, authDetailResponse.getCredentialIdentifiers().size());
|
||||
|
||||
credentialIdentifier = authDetailResponse.getCredentialIdentifiers().get(0);
|
||||
assertNotNull("Credential identifier should not be null", credentialIdentifier);
|
||||
|
||||
credentialConfigurationId = authDetailResponse.getCredentialConfigurationId();
|
||||
assertNotNull("Credential configuration id should not be null", credentialConfigurationId);
|
||||
}
|
||||
|
||||
// Step 2: Request the actual credential using the identifier
|
||||
// Step 2: Request the actual credential using the identifier and config id
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
postCredential.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
|
||||
@@ -23,7 +23,6 @@ import jakarta.ws.rs.client.Client;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsProcessor;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
@@ -34,7 +33,7 @@ import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsProcessor.OPENID_CREDENTIAL_TYPE;
|
||||
import static org.keycloak.OAuth2Constants.OPENID_CREDENTIAL;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -74,7 +73,7 @@ public class OID4VCAuthorizationDetailsTypesSupportedTest extends OID4VCIssuerEn
|
||||
// Verify that it contains openid_credential
|
||||
List<String> supportedTypes = oauthConfig.getAuthorizationDetailsTypesSupported();
|
||||
assertTrue("authorization_details_types_supported should contain openid_credential",
|
||||
supportedTypes.contains(OID4VCAuthorizationDetailsProcessor.OPENID_CREDENTIAL_TYPE));
|
||||
supportedTypes.contains(OPENID_CREDENTIAL));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -94,7 +93,7 @@ public class OID4VCAuthorizationDetailsTypesSupportedTest extends OID4VCIssuerEn
|
||||
assertNotNull("Authorization server should support authorization_details_types_supported",
|
||||
authServerConfig.getAuthorizationDetailsTypesSupported());
|
||||
assertTrue("Authorization server should support openid_credential",
|
||||
authServerConfig.getAuthorizationDetailsTypesSupported().contains(OPENID_CREDENTIAL_TYPE));
|
||||
authServerConfig.getAuthorizationDetailsTypesSupported().contains(OPENID_CREDENTIAL));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -255,11 +255,7 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest {
|
||||
}
|
||||
|
||||
private String getCredentialOfferUriUrl() {
|
||||
return getBasePath("test") + "credential-offer-uri?credential_configuration_id=" + jwtTypeCredentialConfigurationIdName;
|
||||
}
|
||||
|
||||
private String getCredentialOfferUrl(String sessionCode) {
|
||||
return getBasePath("test") + "credential-offer/" + sessionCode;
|
||||
return getCredentialOfferUriUrl(jwtTypeCredentialConfigurationIdName);
|
||||
}
|
||||
|
||||
private String getSessionCodeFromOfferUri(String accessToken) throws Exception {
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKParser;
|
||||
import org.keycloak.models.KeyManager;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
@@ -75,6 +76,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest
|
||||
@Test
|
||||
public void testRequestCredentialWithEncryption() {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String credConfigId = jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
@@ -93,7 +95,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest
|
||||
PrivateKey privateKey = (PrivateKey) jwkPair.get("privateKey");
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName)
|
||||
.setCredentialConfigurationId(credConfigId)
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setEnc(A256GCM)
|
||||
@@ -169,6 +171,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest
|
||||
@Test
|
||||
public void testEncryptedCredentialRequest() {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String credConfigId = jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
try {
|
||||
@@ -194,7 +197,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest
|
||||
PrivateKey responsePrivateKey = (PrivateKey) jwkPair.get("privateKey");
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName)
|
||||
.setCredentialConfigurationId(credConfigId)
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setEnc(A256GCM)
|
||||
@@ -234,6 +237,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest
|
||||
@Test
|
||||
public void testEncryptedCredentialRequestWithCompression() {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String credConfigId = jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
try {
|
||||
@@ -262,7 +266,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest
|
||||
|
||||
// Create credential request with response encryption parameters
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName)
|
||||
.setCredentialConfigurationId(credConfigId)
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setEnc(A256GCM)
|
||||
@@ -474,6 +478,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest
|
||||
@Test
|
||||
public void testRequestCredentialWithInvalidJWK() throws Throwable {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String credConfigId = jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
@@ -483,7 +488,7 @@ public class OID4VCIssuerEndpointEncryptionTest extends OID4VCIssuerEndpointTest
|
||||
// Invalid JWK (missing modulus but WITH alg parameter)
|
||||
JWK jwk = JWKParser.create().parse("{\"kty\":\"RSA\",\"alg\":\"RSA-OAEP-256\",\"e\":\"AQAB\"}").getJwk();
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName)
|
||||
.setCredentialConfigurationId(credConfigId)
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setEnc("A256GCM")
|
||||
|
||||
@@ -35,6 +35,8 @@ import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
@@ -54,7 +56,7 @@ import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.constants.Oid4VciConstants;
|
||||
import org.keycloak.constants.OID4VCIConstants;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.jose.jwe.JWE;
|
||||
import org.keycloak.jose.jwe.JWEException;
|
||||
@@ -79,6 +81,7 @@ import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2Code;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
@@ -88,6 +91,7 @@ import org.keycloak.representations.idm.ComponentExportRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.RolesRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
@@ -110,12 +114,14 @@ import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.Before;
|
||||
|
||||
import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.A256GCM;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256;
|
||||
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.CREDENTIAL_OFFER_URI_CODE_SCOPE;
|
||||
import static org.keycloak.protocol.oid4vc.model.ProofType.JWT;
|
||||
import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId;
|
||||
import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.namedClientId;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -139,33 +145,31 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
protected static ClientScopeRepresentation minimalJwtTypeCredentialClientScope;
|
||||
|
||||
protected CloseableHttpClient httpClient;
|
||||
|
||||
protected ClientRepresentation client;
|
||||
protected ClientRepresentation namedClient;
|
||||
|
||||
record OAuth2CodeEntry(String key, OAuth2Code code) {}
|
||||
|
||||
protected boolean shouldEnableOid4vci() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static String prepareSessionCode(KeycloakSession session, AppAuthManager.BearerTokenAuthenticator authenticator, String note) {
|
||||
protected static OAuth2CodeEntry prepareSessionCode(KeycloakSession session, AppAuthManager.BearerTokenAuthenticator authenticator, String note) {
|
||||
AuthenticationManager.AuthResult authResult = authenticator.authenticate();
|
||||
UserSessionModel userSessionModel = authResult.session();
|
||||
AuthenticatedClientSessionModel authenticatedClientSessionModel = userSessionModel.getAuthenticatedClientSessionByClient(
|
||||
authResult.client().getId());
|
||||
String codeId = SecretGenerator.getInstance().randomString();
|
||||
String nonce = SecretGenerator.getInstance().randomString();
|
||||
OAuth2Code oAuth2Code = new OAuth2Code(codeId,
|
||||
OAuth2Code oauth2Code = new OAuth2Code(
|
||||
SecretGenerator.getInstance().randomString(),
|
||||
Time.currentTime() + 6000,
|
||||
nonce,
|
||||
SecretGenerator.getInstance().randomString(),
|
||||
CREDENTIAL_OFFER_URI_CODE_SCOPE,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
authenticatedClientSessionModel.getUserSession().getId());
|
||||
|
||||
String oauthCode = OAuth2CodeParser.persistCode(session, authenticatedClientSessionModel, oAuth2Code);
|
||||
|
||||
authenticatedClientSessionModel.setNote(oauthCode, note);
|
||||
return oauthCode;
|
||||
String nonce = OAuth2CodeParser.persistCode(session, authenticatedClientSessionModel, oauth2Code);
|
||||
authenticatedClientSessionModel.setNote(nonce, note);
|
||||
return new OAuth2CodeEntry(nonce, oauth2Code);
|
||||
}
|
||||
|
||||
protected static OID4VCIssuerEndpoint prepareIssuerEndpoint(KeycloakSession session,
|
||||
@@ -201,6 +205,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
CryptoIntegration.init(this.getClass().getClassLoader());
|
||||
httpClient = HttpClientBuilder.create().build();
|
||||
client = testRealm().clients().findByClientId(clientId).get(0);
|
||||
namedClient = testRealm().clients().findByClientId(namedClientId).get(0);
|
||||
|
||||
// Lookup the pre-installed oid4vc_natural_person client scope
|
||||
sdJwtTypeNaturalPersonClientScope = requireExistingClientScope(sdJwtTypeNaturalPersonScopeName);
|
||||
@@ -228,13 +233,23 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
null,
|
||||
null);
|
||||
|
||||
// Assign the registered optional client scopes to the client
|
||||
assignOptionalClientScopeToClient(sdJwtTypeCredentialClientScope.getId(), client.getClientId());
|
||||
assignOptionalClientScopeToClient(jwtTypeCredentialClientScope.getId(), client.getClientId());
|
||||
assignOptionalClientScopeToClient(minimalJwtTypeCredentialClientScope.getId(), client.getClientId());
|
||||
List.of(client, namedClient).forEach(client -> {
|
||||
String clientId = client.getClientId();
|
||||
|
||||
// Enable OID4VCI for the client by default, but allow tests to override
|
||||
setClientOid4vciEnabled(clientId, shouldEnableOid4vci());
|
||||
// Assign the registered optional client scopes to the client
|
||||
assignOptionalClientScopeToClient(sdJwtTypeNaturalPersonClientScope.getId(), clientId);
|
||||
assignOptionalClientScopeToClient(sdJwtTypeCredentialClientScope.getId(), clientId);
|
||||
assignOptionalClientScopeToClient(jwtTypeCredentialClientScope.getId(), clientId);
|
||||
assignOptionalClientScopeToClient(minimalJwtTypeCredentialClientScope.getId(), clientId);
|
||||
|
||||
assignOptionalClientScopeToClient(sdJwtTypeNaturalPersonClientScope.getId(), clientId);
|
||||
assignOptionalClientScopeToClient(sdJwtTypeCredentialClientScope.getId(), clientId);
|
||||
assignOptionalClientScopeToClient(jwtTypeCredentialClientScope.getId(), clientId);
|
||||
assignOptionalClientScopeToClient(minimalJwtTypeCredentialClientScope.getId(), clientId);
|
||||
|
||||
// Enable OID4VCI for the client by default, but allow tests to override
|
||||
setClientOid4vciEnabled(clientId, shouldEnableOid4vci());
|
||||
});
|
||||
}
|
||||
|
||||
private ClientResource findClientByClientId(RealmResource realm, String clientId) {
|
||||
@@ -264,7 +279,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
// Create a new ClientScope if not found
|
||||
ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
|
||||
clientScope.setName(scopeName);
|
||||
clientScope.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
clientScope.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
Map<String, String> attributes =
|
||||
new HashMap<>(Map.of(ClientScopeModel.INCLUDE_IN_TOKEN_SCOPE, "true",
|
||||
CredentialScopeModel.EXPIRY_IN_SECONDS, "15"));
|
||||
@@ -531,6 +546,29 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
return contextRoot + "/auth/.well-known/openid-credential-issuer/realms/" + realm;
|
||||
}
|
||||
|
||||
protected String getCredentialOfferUriUrl(String configId) {
|
||||
return getCredentialOfferUriUrl(configId, true, "john");
|
||||
}
|
||||
|
||||
protected String getCredentialOfferUriUrl(String configId, boolean preAuthorized, String targetUser) {
|
||||
return getCredentialOfferUriUrl(configId, preAuthorized, targetUser, null);
|
||||
}
|
||||
|
||||
protected String getCredentialOfferUriUrl(String configId, Boolean preAuthorized, String appUserId, String appClientId) {
|
||||
String res = getBasePath("test") + "credential-offer-uri?credential_configuration_id=" + configId;
|
||||
if (preAuthorized != null)
|
||||
res += "&pre_authorized=" + preAuthorized;
|
||||
if (appClientId != null)
|
||||
res += "&client_id=" + appClientId;
|
||||
if (appUserId != null)
|
||||
res += "&user_id=" + appUserId;
|
||||
return res;
|
||||
}
|
||||
|
||||
protected String getCredentialOfferUrl(String nonce) {
|
||||
return getBasePath("test") + "credential-offer/" + nonce;
|
||||
}
|
||||
|
||||
protected void requestCredential(String token,
|
||||
String credentialEndpoint,
|
||||
SupportedCredentialConfiguration offeredCredential,
|
||||
@@ -569,6 +607,24 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration getSupportedCredentialConfigurationByScope(CredentialIssuer metadata, String scope) {
|
||||
SupportedCredentialConfiguration result = metadata.getCredentialsSupported().values().stream()
|
||||
.filter(it -> it.getScope().equals(scope))
|
||||
.findFirst().orElse(null);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
if (testRealm.getComponents() == null) {
|
||||
@@ -583,15 +639,15 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
getRsaEncKeyProvider(RSA_OAEP, "enc-key-oaep", 101));
|
||||
|
||||
// Find existing client representation
|
||||
ClientRepresentation existingClient = testRealm.getClients().stream()
|
||||
.filter(client -> client.getClientId().equals(clientId))
|
||||
.findFirst()
|
||||
Map<String, ClientRepresentation> realmClients = testRealm.getClients().stream()
|
||||
.collect(Collectors.toMap(ClientRepresentation::getClientId, Function.identity()));
|
||||
ClientRepresentation existingClient = Optional.ofNullable(realmClients.get(clientId))
|
||||
.orElseThrow(() -> new IllegalStateException("Client with ID " + clientId + " not found in realm"));
|
||||
|
||||
// Add a role to an existing client
|
||||
if (testRealm.getRoles() != null) {
|
||||
Map<String, List<RoleRepresentation>> clientRoles = testRealm.getRoles().getClient();
|
||||
clientRoles.merge(
|
||||
RolesRepresentation realmRoles = testRealm.getRoles();
|
||||
if (realmRoles != null) {
|
||||
realmRoles.getClient().merge(
|
||||
existingClient.getClientId(),
|
||||
List.of(getRoleRepresentation("testRole", existingClient.getClientId())),
|
||||
(existingRoles, newRoles) -> {
|
||||
@@ -600,15 +656,12 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
return mergedRoles;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
testRealm.getRoles()
|
||||
.setClient(Map.of(existingClient.getClientId(),
|
||||
List.of(getRoleRepresentation("testRole", existingClient.getClientId()))));
|
||||
}
|
||||
|
||||
List<UserRepresentation> realmUsers = Optional.ofNullable(testRealm.getUsers()).map(ArrayList::new)
|
||||
.orElse(new ArrayList<>());
|
||||
realmUsers.add(getUserRepresentation(Map.of(existingClient.getClientId(), List.of("testRole"))));
|
||||
Map<String, List<String>> clientRoles = Map.of(clientId, List.of("testRole"));
|
||||
List<UserRepresentation> realmUsers = Optional.ofNullable(testRealm.getUsers()).map(ArrayList::new).orElse(new ArrayList<>());
|
||||
realmUsers.add(getUserRepresentation("John Doe", List.of(CREDENTIAL_OFFER_CREATE.getName()), clientRoles));
|
||||
realmUsers.add(getUserRepresentation("Alice Wonderland", List.of(), Map.of()));
|
||||
testRealm.setUsers(realmUsers);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
@@ -88,7 +87,7 @@ import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.OID4VCConstants.SIGNED_METADATA_JWT_TYPE;
|
||||
import static org.keycloak.constants.Oid4VciConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE;
|
||||
import static org.keycloak.constants.OID4VCIConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.A256GCM;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256;
|
||||
@@ -783,31 +782,4 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void extendConfigureTestRealm(RealmRepresentation testRealm, ClientRepresentation clientRepresentation) {
|
||||
if (testRealm.getComponents() == null) {
|
||||
testRealm.setComponents(new MultivaluedHashMap<>());
|
||||
}
|
||||
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getRsaKeyProvider(RSA_KEY));
|
||||
testRealm.getComponents().add("org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder", getCredentialBuilderProvider(Format.JWT_VC));
|
||||
|
||||
if (testRealm.getClients() != null) {
|
||||
testRealm.getClients().add(clientRepresentation);
|
||||
} else {
|
||||
testRealm.setClients(new ArrayList<>(List.of(clientRepresentation)));
|
||||
}
|
||||
|
||||
if (testRealm.getUsers() != null) {
|
||||
testRealm.getUsers().add(OID4VCTest.getUserRepresentation(Map.of(clientRepresentation.getClientId(), List.of("testRole"))));
|
||||
} else {
|
||||
testRealm.setUsers(new ArrayList<>(List.of(OID4VCTest.getUserRepresentation(Map.of(clientRepresentation.getClientId(), List.of("testRole"))))));
|
||||
}
|
||||
|
||||
if (testRealm.getAttributes() != null) {
|
||||
testRealm.getAttributes().put("issuerDid", TEST_DID.toString());
|
||||
} else {
|
||||
testRealm.setAttributes(new HashMap<>(Map.of("issuerDid", TEST_DID.toString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.OfferUriType;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
@@ -36,7 +35,7 @@ public class OID4VCJWTIssuerEndpointDisabledTest extends OID4VCIssuerEndpointTes
|
||||
|
||||
// Test getCredentialOfferURI
|
||||
CorsErrorResponseException offerUriException = Assert.assertThrows(CorsErrorResponseException.class, () ->
|
||||
issuerEndpoint.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0)
|
||||
issuerEndpoint.getCredentialOfferURI("test-credential")
|
||||
);
|
||||
assertEquals("Should fail with 403 Forbidden when client is not OID4VCI-enabled",
|
||||
Response.Status.FORBIDDEN.getStatusCode(), offerUriException.getResponse().getStatus());
|
||||
|
||||
@@ -35,9 +35,12 @@ 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;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialoffer.CredentialOfferStorage;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialoffer.CredentialOfferStorage.CredentialOfferState;
|
||||
import org.keycloak.protocol.oid4vc.model.Claim;
|
||||
import org.keycloak.protocol.oid4vc.model.ClaimDisplay;
|
||||
import org.keycloak.protocol.oid4vc.model.Claims;
|
||||
@@ -50,7 +53,6 @@ import org.keycloak.protocol.oid4vc.model.ErrorResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.ErrorType;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.JwtProof;
|
||||
import org.keycloak.protocol.oid4vc.model.OfferUriType;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedGrant;
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
@@ -59,9 +61,8 @@ 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.sdjwt.vp.SdJwtVP;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AppAuthManager.BearerTokenAuthenticator;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
@@ -90,19 +91,20 @@ import static org.junit.Assert.fail;
|
||||
* Test from org.keycloak.testsuite.oid4vc.issuance.signing.OID4VCIssuerEndpointTest
|
||||
*/
|
||||
public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
// ----- getCredentialOfferUri
|
||||
|
||||
@Test
|
||||
public void testGetCredentialOfferUriUnsupportedCredential() {
|
||||
String token = getBearerToken(oauth);
|
||||
testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
CorsErrorResponseException exception = Assert.assertThrows(CorsErrorResponseException.class, () ->
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("inexistent-id", OfferUriType.URI, 0, 0)
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("inexistent-id")
|
||||
);
|
||||
assertEquals("Should return BAD_REQUEST", Response.Status.BAD_REQUEST.getStatusCode(),
|
||||
exception.getResponse().getStatus());
|
||||
@@ -112,12 +114,12 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
@Test
|
||||
public void testGetCredentialOfferUriUnauthorized() {
|
||||
testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(null);
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
CorsErrorResponseException exception = Assert.assertThrows(CorsErrorResponseException.class, () ->
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0)
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential", true, "john")
|
||||
);
|
||||
assertEquals("Should return BAD_REQUEST", Response.Status.BAD_REQUEST.getStatusCode(),
|
||||
exception.getResponse().getStatus());
|
||||
@@ -127,12 +129,12 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
@Test
|
||||
public void testGetCredentialOfferUriInvalidToken() {
|
||||
testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new 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)
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential", true, "john")
|
||||
);
|
||||
assertEquals("Should return BAD_REQUEST", Response.Status.BAD_REQUEST.getStatusCode(),
|
||||
exception.getResponse().getStatus());
|
||||
@@ -148,15 +150,11 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run((session) -> {
|
||||
try {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(
|
||||
session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
Response response = oid4VCIssuerEndpoint.getCredentialOfferURI(credentialConfigurationId,
|
||||
OfferUriType.URI,
|
||||
0,
|
||||
0);
|
||||
Response response = oid4VCIssuerEndpoint.getCredentialOfferURI(credentialConfigurationId);
|
||||
|
||||
assertEquals("An offer uri should have been returned.", HttpStatus.SC_OK, response.getStatus());
|
||||
CredentialOfferURI credentialOfferURI = JsonSerialization.mapper.convertValue(response.getEntity(),
|
||||
@@ -177,7 +175,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session) -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(null);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
Response response = issuerEndpoint.getCredentialOffer("nonce");
|
||||
@@ -193,7 +191,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.getCredentialOffer(null);
|
||||
@@ -208,7 +206,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.getCredentialOffer("unpreparedNonce");
|
||||
@@ -223,11 +221,11 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
String sessionCode = prepareSessionCode(session, authenticator, "invalidNote");
|
||||
String nonce = prepareSessionCode(session, authenticator, "invalidNote").key();
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.getCredentialOffer(sessionCode);
|
||||
issuerEndpoint.getCredentialOffer(nonce);
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -238,25 +236,28 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session) -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
CredentialsOffer credentialsOffer = new CredentialsOffer()
|
||||
CredentialsOffer credOffer = new CredentialsOffer()
|
||||
.setCredentialIssuer("the-issuer")
|
||||
.setGrants(new PreAuthorizedGrant().setPreAuthorizedCode(new PreAuthorizedCode().setPreAuthorizedCode("the-code")))
|
||||
.setCredentialConfigurationIds(List.of("credential-configuration-id"));
|
||||
|
||||
String sessionCode = prepareSessionCode(session, authenticator, JsonSerialization.writeValueAsString(credentialsOffer));
|
||||
CredentialOfferStorage offerStorage = session.getProvider(CredentialOfferStorage.class);
|
||||
CredentialOfferState offerState = new CredentialOfferState(credOffer, null, null, Time.currentTime() + 60);
|
||||
offerStorage.putOfferState(session, offerState);
|
||||
|
||||
// The cache transactions need to be committed explicitly in the test. Without that, the OAuth2Code will only be committed to
|
||||
// the cache after .run((session)-> ...)
|
||||
session.getTransactionManager().commit();
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
Response credentialOfferResponse = issuerEndpoint.getCredentialOffer(sessionCode);
|
||||
Response credentialOfferResponse = issuerEndpoint.getCredentialOffer(offerState.getNonce());
|
||||
assertEquals("The offer should have been returned.", HttpStatus.SC_OK, credentialOfferResponse.getStatus());
|
||||
Object credentialOfferEntity = credentialOfferResponse.getEntity();
|
||||
assertNotNull("An actual offer should be in the response.", credentialOfferEntity);
|
||||
|
||||
CredentialsOffer retrievedCredentialsOffer = JsonSerialization.mapper.convertValue(credentialOfferEntity, CredentialsOffer.class);
|
||||
assertEquals("The offer should be the one prepared with for the session.", credentialsOffer, retrievedCredentialsOffer);
|
||||
assertEquals("The offer should be the one prepared with for the session.", credOffer, retrievedCredentialsOffer);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,7 +266,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
@Test
|
||||
public void testRequestCredentialUnauthorized() {
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(null);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
@@ -285,7 +286,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
@Test
|
||||
public void testRequestCredentialInvalidToken() {
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString("token");
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
@@ -311,8 +312,8 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
try {
|
||||
withCausePropagation(() -> {
|
||||
testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator =
|
||||
new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator =
|
||||
new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
|
||||
// Prepare the issue endpoint with no credential builders.
|
||||
@@ -337,7 +338,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
String token = getBearerToken(oauth);
|
||||
withCausePropagation(() -> {
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
@@ -352,14 +353,15 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
@Test
|
||||
public void testRequestCredential() {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String credConfigId = jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName);
|
||||
.setCredentialConfigurationId(credConfigId);
|
||||
|
||||
String requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
|
||||
@@ -392,27 +394,22 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestCredentialWithConfigurationIdNotSet() {
|
||||
public void testRequestCredentialWithNeitherIdSet() {
|
||||
final String scopeName = minimalJwtTypeCredentialClientScope.getName();
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName);
|
||||
|
||||
String requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
|
||||
Response credentialResponse = issuerEndpoint.requestCredential(requestPayload);
|
||||
assertEquals("The credential request should be answered successfully.",
|
||||
HttpStatus.SC_OK, credentialResponse.getStatus());
|
||||
assertNotNull("A credential should be responded.", credentialResponse.getEntity());
|
||||
CredentialResponse credentialResponseVO = JsonSerialization.mapper
|
||||
.convertValue(credentialResponse.getEntity(), CredentialResponse.class);
|
||||
String credentialString = (String) credentialResponseVO.getCredentials().get(0).getCredential();
|
||||
SdJwtVP sdJwtVP = SdJwtVP.of(credentialString);
|
||||
assertNotNull("A valid credential string should have been responded", sdJwtVP);
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
try {
|
||||
String requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
issuerEndpoint.requestCredential(requestPayload);
|
||||
Assert.fail("Expected BadRequestException due to unknown credential identifier");
|
||||
} catch (BadRequestException e) {
|
||||
ErrorResponse error = (ErrorResponse) e.getResponse().getEntity();
|
||||
assertEquals(ErrorType.MISSING_CREDENTIAL_IDENTIFIER_AND_CONFIGURATION_ID, error.getError());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -430,9 +427,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
// 1. Retrieving the credential-offer-uri
|
||||
final String credentialConfigurationId = jwtTypeCredentialClientScope.getAttributes()
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getBasePath(TEST_REALM_NAME)
|
||||
+ "credential-offer-uri?credential_configuration_id="
|
||||
+ credentialConfigurationId);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getCredentialOfferUriUrl(credentialConfigurationId));
|
||||
getCredentialOfferURI.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferURIResponse = httpClient.execute(getCredentialOfferURI);
|
||||
|
||||
@@ -602,15 +597,16 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
@Test
|
||||
public void testRequestCredentialWithNotificationId() {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String credConfigId = jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run((session) -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName);
|
||||
.setCredentialConfigurationId(credConfigId);
|
||||
|
||||
String requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
|
||||
@@ -639,12 +635,13 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
@Test
|
||||
public void testRequestMultipleCredentialsWithProofs() {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String credConfigId = jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
String cNonce = getCNonce();
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
try {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
String issuer = OID4VCIssuerWellKnownProvider.getIssuer(session.getContext());
|
||||
|
||||
@@ -654,7 +651,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
|
||||
CredentialRequest request = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName)
|
||||
.setCredentialConfigurationId(credConfigId)
|
||||
.setProofs(proofs);
|
||||
|
||||
OID4VCIssuerEndpoint endpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
@@ -874,7 +871,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
@@ -901,7 +898,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
@@ -931,7 +928,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
// Prepare endpoint with no credential builders to simulate missing builder for the configured format
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator, Map.of());
|
||||
@@ -963,7 +960,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
BearerTokenAuthenticator authenticator = new BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.OfferUriType;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
@@ -36,7 +35,7 @@ public class OID4VCSdJwtIssuingEndpointDisabledTest extends OID4VCIssuerEndpoint
|
||||
|
||||
// Test getCredentialOfferURI
|
||||
CorsErrorResponseException offerUriException = Assert.assertThrows(CorsErrorResponseException.class, () ->
|
||||
issuerEndpoint.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0)
|
||||
issuerEndpoint.getCredentialOfferURI("test-credential")
|
||||
);
|
||||
assertEquals("Should fail with 403 Forbidden when client is not OID4VCI-enabled",
|
||||
Response.Status.FORBIDDEN.getStatusCode(), offerUriException.getResponse().getStatus());
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.constants.Oid4VciConstants;
|
||||
import org.keycloak.constants.OID4VCIConstants;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
@@ -231,7 +231,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
final String nonceEndpoint = OID4VCIssuerWellKnownProvider.getNonceEndpoint(session.getContext());
|
||||
try {
|
||||
// make the exp-value negative to set the exp-time in the past
|
||||
session.getContext().getRealm().setAttribute(Oid4VciConstants.C_NONCE_LIFETIME_IN_SECONDS, -1);
|
||||
session.getContext().getRealm().setAttribute(OID4VCIConstants.C_NONCE_LIFETIME_IN_SECONDS, -1);
|
||||
String cNonce = cNonceHandler.buildCNonce(List.of(credentialsEndpoint),
|
||||
Map.of(JwtCNonceHandler.SOURCE_ENDPOINT, nonceEndpoint));
|
||||
Proofs proof = new Proofs().setJwt(List.of(generateJwtProof(getCredentialIssuer(session), cNonce)));
|
||||
@@ -241,7 +241,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
testRequestTestCredential(session, clientScope, token, proof);
|
||||
} finally {
|
||||
// make sure other tests are not affected by the changed realm-attribute
|
||||
session.getContext().getRealm().removeAttribute(Oid4VciConstants.C_NONCE_LIFETIME_IN_SECONDS);
|
||||
session.getContext().getRealm().removeAttribute(OID4VCIConstants.C_NONCE_LIFETIME_IN_SECONDS);
|
||||
}
|
||||
})));
|
||||
Assert.fail("Should have thrown an exception");
|
||||
@@ -303,9 +303,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
// 1. Retrieving the credential-offer-uri
|
||||
final String credentialConfigurationId = clientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getBasePath(TEST_REALM_NAME) +
|
||||
"credential-offer-uri?credential_configuration_id=" +
|
||||
credentialConfigurationId);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getCredentialOfferUriUrl(credentialConfigurationId));
|
||||
getCredentialOfferURI.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferURIResponse = httpClient.execute(getCredentialOfferURI);
|
||||
|
||||
@@ -515,7 +513,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
public static ProtocolMapperRepresentation getJtiGeneratedIdMapper() {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName("generated-id-mapper");
|
||||
protocolMapperRepresentation.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-generated-id-mapper");
|
||||
protocolMapperRepresentation.setConfig(Map.of(
|
||||
@@ -530,7 +528,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
.addClientScope(realmModel, jwtTypeCredentialScopeName);
|
||||
credentialScope.setAttribute(CredentialScopeModel.CREDENTIAL_IDENTIFIER,
|
||||
jwtTypeCredentialScopeName);
|
||||
credentialScope.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
credentialScope.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
return credentialScope;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -53,7 +54,7 @@ import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.constants.Oid4VciConstants;
|
||||
import org.keycloak.constants.OID4VCIConstants;
|
||||
import org.keycloak.crypto.ECDSASignatureSignerContext;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
@@ -70,6 +71,7 @@ import org.keycloak.protocol.oid4vc.issuance.VCIssuanceContext;
|
||||
import org.keycloak.protocol.oid4vc.issuance.keybinding.AttestationValidatorUtil;
|
||||
import org.keycloak.protocol.oid4vc.issuance.keybinding.JwtProofValidator;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCIssuedAtTimeClaimMapper;
|
||||
import org.keycloak.protocol.oid4vc.model.AuthorizationDetail;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
@@ -91,7 +93,8 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenRequest;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
@@ -336,7 +339,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName("role-mapper");
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-target-role-mapper");
|
||||
protocolMapperRepresentation.setConfig(
|
||||
Map.of("claim.name", "roles", "clientId", clientId)
|
||||
@@ -347,7 +350,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
public static ProtocolMapperRepresentation getIdMapper() {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName("id-mapper");
|
||||
protocolMapperRepresentation.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-subject-id-mapper");
|
||||
protocolMapperRepresentation.setConfig(Map.of());
|
||||
@@ -357,7 +360,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
public static ProtocolMapperRepresentation getStaticClaimMapper(String scopeName) {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-static-claim-mapper");
|
||||
protocolMapperRepresentation.setConfig(
|
||||
@@ -394,25 +397,36 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
return componentExportRepresentation;
|
||||
}
|
||||
|
||||
public static UserRepresentation getUserRepresentation(Map<String, List<String>> clientRoles) {
|
||||
UserBuilder userBuilder = UserBuilder.create()
|
||||
.id(KeycloakModelUtils.generateId())
|
||||
.username("john")
|
||||
.enabled(true)
|
||||
.email("john@email.cz")
|
||||
.emailVerified(true)
|
||||
.firstName("John")
|
||||
.lastName("Doe")
|
||||
.password("password")
|
||||
.role("account", "manage-account")
|
||||
.role("account", "view-profile");
|
||||
public static UserRepresentation getUserRepresentation(
|
||||
String fullName,
|
||||
List<String> realmRoles,
|
||||
Map<String, List<String>> clientRoles
|
||||
) {
|
||||
String[] nameToks = fullName.split("\\s");
|
||||
String firstName = nameToks[0];
|
||||
String lastName = nameToks[1];
|
||||
String username = firstName.toLowerCase();
|
||||
UserBuilder userBuilder = UserBuilder.create()
|
||||
.id(KeycloakModelUtils.generateId())
|
||||
.username(username)
|
||||
.enabled(true)
|
||||
.email(username + "@email.cz")
|
||||
.emailVerified(true)
|
||||
.firstName(firstName)
|
||||
.lastName(lastName)
|
||||
.password("password")
|
||||
.role("account", "manage-account")
|
||||
.role("account", "view-profile");
|
||||
|
||||
clientRoles.entrySet().forEach(entry -> {
|
||||
entry.getValue().forEach(role -> userBuilder.role(entry.getKey(), role));
|
||||
});
|
||||
|
||||
return userBuilder.build();
|
||||
}
|
||||
// When Keycloak issues a token for a user and client:
|
||||
//
|
||||
// 1. It looks up all effective realm roles and all effective client roles assigned to the user.
|
||||
// 2. The token includes only those roles that the user actually has.
|
||||
//
|
||||
realmRoles.forEach(userBuilder::addRoles);
|
||||
clientRoles.forEach((cid, roles) -> roles.forEach(role -> userBuilder.role(cid, role)));
|
||||
return userBuilder.build();
|
||||
}
|
||||
|
||||
public static RoleRepresentation getRoleRepresentation(String roleName, String clientId) {
|
||||
|
||||
@@ -423,27 +437,62 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
return role;
|
||||
}
|
||||
|
||||
protected String getBearerToken(OAuthClient oAuthClient) {
|
||||
return getBearerToken(oAuthClient, null);
|
||||
protected String getAuthorizationCode(OAuthClient oAuthClient, ClientRepresentation client, String username, String scope) {
|
||||
if (client != null) {
|
||||
oAuthClient.client(client.getClientId(), client.getSecret());
|
||||
}
|
||||
if (scope != null) {
|
||||
oAuthClient.scope(scope);
|
||||
}
|
||||
var authorizationEndpointResponse = oAuthClient.doLogin(username,"password");
|
||||
return authorizationEndpointResponse.getCode();
|
||||
}
|
||||
|
||||
protected String getBearerToken(OAuthClient oauthClient) {
|
||||
return getBearerToken(oauthClient, null);
|
||||
}
|
||||
|
||||
protected String getBearerToken(OAuthClient oAuthClient, ClientRepresentation client) {
|
||||
return getBearerToken(oAuthClient, client, null);
|
||||
protected String getBearerToken(OAuthClient oauthClient, ClientRepresentation client) {
|
||||
return getBearerToken(oauthClient, client, null);
|
||||
}
|
||||
|
||||
protected String getBearerToken(OAuthClient oAuthClient, ClientRepresentation client, String credentialScopeName) {
|
||||
if (client != null) {
|
||||
oAuthClient.client(client.getClientId(), client.getSecret());
|
||||
}
|
||||
if (credentialScopeName != null) {
|
||||
oAuthClient.scope(credentialScopeName);
|
||||
}
|
||||
AuthorizationEndpointResponse authorizationEndpointResponse = oAuthClient.doLogin("john",
|
||||
"password");
|
||||
return oAuthClient.doAccessTokenRequest(authorizationEndpointResponse.getCode()).getAccessToken();
|
||||
protected String getBearerToken(OAuthClient oauthClient, ClientRepresentation client, String scope) {
|
||||
return getBearerToken(oauthClient, client, "john", scope);
|
||||
}
|
||||
|
||||
public static class StaticTimeProvider implements TimeProvider {
|
||||
protected String getBearerToken(OAuthClient oauthClient, ClientRepresentation client, String username, String scope) {
|
||||
return getBearerTokenCodeFlow(oauthClient, client, username, scope).getAccessToken();
|
||||
}
|
||||
|
||||
protected AccessTokenResponse getBearerToken(OAuthClient oauthClient, String authCode, AuthorizationDetail... authDetail) {
|
||||
AccessTokenRequest accessTokenRequest = oauthClient.accessTokenRequest(authCode);
|
||||
if (authDetail != null) {
|
||||
accessTokenRequest.authorizationDetails(Arrays.asList(authDetail));
|
||||
}
|
||||
AccessTokenResponse tokenResponse = accessTokenRequest.send();
|
||||
if (!tokenResponse.isSuccess()) {
|
||||
throw new IllegalStateException(tokenResponse.getErrorDescription());
|
||||
}
|
||||
return tokenResponse;
|
||||
}
|
||||
|
||||
protected AccessTokenResponse getBearerTokenCodeFlow(OAuthClient oauthClient, ClientRepresentation client, String username, String scope) {
|
||||
var authCode = getAuthorizationCode(oauthClient, client, username, scope);
|
||||
return oauthClient.accessTokenRequest(authCode).send();
|
||||
}
|
||||
|
||||
protected AccessTokenResponse getBearerTokenDirectAccess(OAuthClient oauthClient, ClientRepresentation client, String username, String scope) {
|
||||
if (client != null) {
|
||||
oauthClient.client(client.getClientId(), client.getSecret());
|
||||
}
|
||||
if (scope != null) {
|
||||
oauthClient.scope(scope);
|
||||
}
|
||||
var accessTokenResponse = oauthClient.doPasswordGrantRequest(username, "password");
|
||||
return accessTokenResponse;
|
||||
}
|
||||
|
||||
public static class StaticTimeProvider implements TimeProvider {
|
||||
private final int currentTimeInS;
|
||||
|
||||
public StaticTimeProvider(int currentTimeInS) {
|
||||
@@ -464,7 +513,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
protected ProtocolMapperRepresentation getUserAttributeMapper(String subjectProperty, String attributeName) {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName(attributeName + "-mapper");
|
||||
protocolMapperRepresentation.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-user-attribute-mapper");
|
||||
protocolMapperRepresentation.setConfig(
|
||||
@@ -478,7 +527,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
protected ProtocolMapperRepresentation getIssuedAtTimeMapper(String subjectProperty, String truncateToTimeUnit, String valueSource) {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName(subjectProperty + "-oid4vc-issued-at-time-claim-mapper");
|
||||
protocolMapperRepresentation.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-issued-at-time-claim-mapper");
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.constants.Oid4VciConstants;
|
||||
import org.keycloak.constants.OID4VCIConstants;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCIssuedAtTimeClaimMapper;
|
||||
@@ -68,7 +68,7 @@ public class OID4VCTimeNormalizationSdJwtTest extends OID4VCSdJwtIssuingEndpoint
|
||||
ClientScopeRepresentation clientScope = fromJsonString(clientScopeString, ClientScopeRepresentation.class);
|
||||
ProtocolMapperRepresentation pr = new ProtocolMapperRepresentation();
|
||||
pr.setName("iat-from-vc");
|
||||
pr.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
|
||||
pr.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
pr.setProtocolMapper(OID4VCIssuedAtTimeClaimMapper.MAPPER_ID);
|
||||
pr.setConfig(Map.of(
|
||||
OID4VCIssuedAtTimeClaimMapper.CLAIM_NAME, "iat",
|
||||
|
||||
@@ -24,6 +24,7 @@ import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
@@ -55,6 +56,7 @@ public class OID4VCTimeNormalizationTest extends OID4VCJWTIssuerEndpointTest {
|
||||
});
|
||||
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String credConfigId = jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
@@ -64,7 +66,7 @@ public class OID4VCTimeNormalizationTest extends OID4VCJWTIssuerEndpointTest {
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setCredentialIdentifier(scopeName);
|
||||
.setCredentialConfigurationId(credConfigId);
|
||||
|
||||
String requestPayload = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
Response response = issuerEndpoint.requestCredential(requestPayload);
|
||||
|
||||
@@ -405,6 +405,7 @@
|
||||
"clientId": "named-test-app",
|
||||
"name": "My Named Test App",
|
||||
"enabled": true,
|
||||
"directAccessGrantsEnabled": true,
|
||||
"baseUrl": "http://localhost:8180/namedapp/base",
|
||||
"redirectUris": [
|
||||
"http://localhost:8180/namedapp/base/*",
|
||||
|
||||
Reference in New Issue
Block a user