mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Fix scope validation for realm-level credential definitions in Authorization Code flow (#39148)
Closes #39130 Signed-off-by: Awambeng Rodrick <awambengrodrick@gmail.com> (cherry picked from commit ca3859b0f821587dfc4be31daef77c3a3e273e77)
This commit is contained in:
parent
28ea8bc221
commit
60445d2a9f
@ -292,23 +292,33 @@ public class OID4VCIssuerEndpoint {
|
||||
private void checkScope(CredentialRequest credentialRequestVO) {
|
||||
AuthenticatedClientSessionModel clientSession = getAuthenticatedClientSession();
|
||||
String vcIssuanceFlow = clientSession.getNote(PreAuthorizedCodeGrantType.VC_ISSUANCE_FLOW);
|
||||
|
||||
if (vcIssuanceFlow == null || !vcIssuanceFlow.equals(PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE)) {
|
||||
// authz code flow
|
||||
ClientModel client = clientSession.getClient();
|
||||
// Authorization Code Flow
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
String credentialIdentifier = credentialRequestVO.getCredentialIdentifier();
|
||||
String scope = client.getAttributes().get("vc." + credentialIdentifier + ".scope"); // following credential identifier in client attribute
|
||||
|
||||
String scope = realm.getAttribute("vc." + credentialIdentifier + ".scope");
|
||||
|
||||
AccessToken accessToken = bearerTokenAuthenticator.authenticate().getToken();
|
||||
if (Arrays.stream(accessToken.getScope().split(" ")).sequential().noneMatch(i -> i.equals(scope))) {
|
||||
LOGGER.debugf("Scope check failure: credentialIdentifier = %s, required scope = %s, scope in access token = %s.", credentialIdentifier, scope, accessToken.getScope());
|
||||
throw new CorsErrorResponseException(cors, ErrorType.UNSUPPORTED_CREDENTIAL_TYPE.toString(), "Scope check failure", Response.Status.BAD_REQUEST);
|
||||
if (scope == null || Arrays.stream(accessToken.getScope().split(" "))
|
||||
.noneMatch(tokenScope -> tokenScope.equals(scope))) {
|
||||
LOGGER.debugf("Scope check failure: credentialIdentifier = %s, required scope = %s, scope in access token = %s.",
|
||||
credentialIdentifier, scope, accessToken.getScope());
|
||||
throw new CorsErrorResponseException(cors,
|
||||
ErrorType.UNSUPPORTED_CREDENTIAL_TYPE.toString(),
|
||||
"Scope check failure",
|
||||
Response.Status.BAD_REQUEST);
|
||||
} else {
|
||||
LOGGER.debugf("Scope check success: credentialIdentifier = %s, required scope = %s, scope in access token = %s.", credentialIdentifier, scope, accessToken.getScope());
|
||||
LOGGER.debugf("Scope check success: credentialIdentifier = %s, required scope = %s, scope in access token = %s.",
|
||||
credentialIdentifier, scope, accessToken.getScope());
|
||||
}
|
||||
} else {
|
||||
clientSession.removeNote(PreAuthorizedCodeGrantType.VC_ISSUANCE_FLOW);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a verifiable credential
|
||||
*/
|
||||
|
||||
@ -30,9 +30,11 @@ import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
@ -48,13 +50,14 @@ 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.RealmRepresentation;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -415,16 +418,30 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
@Test
|
||||
public void testCredentialIssuanceWithAuthZCodeWithScopeMatched() throws Exception {
|
||||
testCredentialIssuanceWithAuthZCodeFlow((testClientId, testScope) -> getBearerToken(oauth.clientId(testClientId).openid(false).scope(testScope)),
|
||||
// Set the realm attribute for the required scope
|
||||
RealmResource realm = adminClient.realm(TEST_REALM_NAME);
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
Map<String, String> attributes = rep.getAttributes() != null ? new HashMap<>(rep.getAttributes()) : new HashMap<>();
|
||||
attributes.put("vc.test-credential.scope", "VerifiableCredential");
|
||||
rep.setAttributes(attributes);
|
||||
realm.update(rep);
|
||||
|
||||
testCredentialIssuanceWithAuthZCodeFlow(
|
||||
(testClientId, testScope) -> getBearerToken(oauth.clientId(testClientId).openid(false).scope("VerifiableCredential")),
|
||||
m -> {
|
||||
String accessToken = (String) m.get("accessToken");
|
||||
WebTarget credentialTarget = (WebTarget) m.get("credentialTarget");
|
||||
CredentialRequest credentialRequest = (CredentialRequest) m.get("credentialRequest");
|
||||
assertEquals("Credential identifier should match", "test-credential", credentialRequest.getCredentialIdentifier());
|
||||
|
||||
try (Response response = credentialTarget.request().header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken).post(Entity.json(credentialRequest))) {
|
||||
if (response.getStatus() != 200) {
|
||||
String errorBody = response.readEntity(String.class);
|
||||
System.out.println("Error Response: " + errorBody);
|
||||
}
|
||||
assertEquals(200, response.getStatus());
|
||||
CredentialResponse credentialResponse = JsonSerialization.readValue(response.readEntity(String.class), CredentialResponse.class);
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredential(), JsonWebToken.class).getToken();
|
||||
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
|
||||
|
||||
@ -433,7 +450,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
assertEquals(TEST_DID, credential.getIssuer());
|
||||
assertEquals("john@email.cz", credential.getCredentialSubject().getClaims().get("email"));
|
||||
} catch (IOException | VerificationException e) {
|
||||
Assert.fail();
|
||||
Assert.fail("Failed to process credential response: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -466,4 +483,60 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialIssuanceWithRealmScopeUnmatched() throws Exception {
|
||||
// Set the realm attribute for the required scope
|
||||
RealmResource realm = adminClient.realm(TEST_REALM_NAME);
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
Map<String, String> attributes = rep.getAttributes() != null ? new HashMap<>(rep.getAttributes()) : new HashMap<>();
|
||||
attributes.put("vc.test-credential.scope", "VerifiableCredential");
|
||||
rep.setAttributes(attributes);
|
||||
realm.update(rep);
|
||||
|
||||
// Run the flow with a non-matching scope
|
||||
testCredentialIssuanceWithAuthZCodeFlow((testClientId, testScope) -> getBearerToken(oauth.clientId(testClientId).openid(false).scope("email")),
|
||||
m -> {
|
||||
String accessToken = (String) m.get("accessToken");
|
||||
WebTarget credentialTarget = (WebTarget) m.get("credentialTarget");
|
||||
CredentialRequest credentialRequest = (CredentialRequest) m.get("credentialRequest");
|
||||
|
||||
try (Response response = credentialTarget.request().header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken).post(Entity.json(credentialRequest))) {
|
||||
assertEquals(400, response.getStatus());
|
||||
String errorJson = response.readEntity(String.class);
|
||||
assertNotNull("Error response should not be null", errorJson);
|
||||
assertTrue("Error response should mention UNSUPPORTED_CREDENTIAL_TYPE or scope",
|
||||
errorJson.contains("UNSUPPORTED_CREDENTIAL_TYPE") || errorJson.contains("scope"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialIssuanceWithRealmScopeMissing() throws Exception {
|
||||
// Remove the realm attribute for the required scope
|
||||
RealmResource realm = adminClient.realm(TEST_REALM_NAME);
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
Map<String, String> attributes = rep.getAttributes() != null ? new HashMap<>(rep.getAttributes()) : new HashMap<>();
|
||||
attributes.remove("vc.test-credential.scope");
|
||||
rep.setAttributes(attributes);
|
||||
realm.update(rep);
|
||||
|
||||
// Run the flow with a scope in the access token, but no realm attribute
|
||||
testCredentialIssuanceWithAuthZCodeFlow((testClientId, testScope) -> getBearerToken(oauth.clientId(testClientId).openid(false).scope("VerifiableCredential")),
|
||||
m -> {
|
||||
String accessToken = (String) m.get("accessToken");
|
||||
WebTarget credentialTarget = (WebTarget) m.get("credentialTarget");
|
||||
CredentialRequest credentialRequest = (CredentialRequest) m.get("credentialRequest");
|
||||
|
||||
try (Response response = credentialTarget.request().header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken).post(Entity.json(credentialRequest))) {
|
||||
assertEquals(400, response.getStatus());
|
||||
String errorJson = response.readEntity(String.class);
|
||||
Map<String, Object> errorMap = JsonSerialization.readValue(errorJson, Map.class);
|
||||
assertTrue("Error should contain 'error' field", errorMap.containsKey("error"));
|
||||
assertEquals("UNSUPPORTED_CREDENTIAL_TYPE", errorMap.get("error"));
|
||||
assertEquals("Scope check failure", errorMap.get("error_description"));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user