[OID4VCI] OpenID for Verifiable Credentials support in client settings (#39385)

Closes #32967

Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>


Co-authored-by: Francis Pouatcha <francis.pouatcha@adorsys.com>
This commit is contained in:
forkimenjeckayang
2025-07-14 10:47:10 +01:00
committed by GitHub
parent cabd7cd474
commit a3441689e9
9 changed files with 401 additions and 67 deletions

View File

@@ -3501,3 +3501,7 @@ givenNameClaim=Given name Claim
givenNameClaimHelp=The name of the claim from the JSON document returned by the user profile endpoint representing the user's given name. If not provided, defaults to `given_name`.
familyNameClaim=Family name Claim
familyNameClaimHelp=The name of the claim from the JSON document returned by the user profile endpoint representing the user's family name. If not provided, defaults to `family_name`.
openIdVerifiableCredentials=OpenID for Verifiable Credentials
openIdVerifiableCredentialsHelp=This section is used to configure settings related to OpenID for Verifiable Credential Issuance (OID4VCI).
oid4vciEnabled=Enable OID4VCI
oid4vciEnabledHelp=Enable this option to allow the client to request verifiable credentials from Keycloak's OID4VCI credential endpoint.

View File

@@ -14,6 +14,11 @@ import { ClusteringPanel } from "./advanced/ClusteringPanel";
import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect";
import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig";
import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes";
import { OpenIdVerifiableCredentials } from "./advanced/OpenIdVerifiableCredentials";
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
const PROTOCOL_OIDC = "openid-connect";
const PROTOCOL_OID4VC = "oid4vc";
export const parseResult = (
result: GlobalRequestResult,
@@ -50,7 +55,7 @@ export type AdvancedProps = {
export const AdvancedTab = ({ save, client }: AdvancedProps) => {
const { t } = useTranslation();
const openIdConnect = "openid-connect";
const isFeatureEnabled = useIsFeatureEnabled();
const { setValue } = useFormContext();
const {
@@ -81,7 +86,7 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
},
{
title: t("fineGrainOpenIdConnectConfiguration"),
isHidden: protocol !== openIdConnect,
isHidden: protocol !== PROTOCOL_OIDC,
panel: (
<>
<Text className="pf-v5-u-pb-lg">
@@ -118,7 +123,7 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
},
{
title: t("openIdConnectCompatibilityModes"),
isHidden: protocol !== openIdConnect,
isHidden: protocol !== PROTOCOL_OIDC,
panel: (
<>
<Text className="pf-v5-u-pb-lg">
@@ -140,7 +145,7 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
},
{
title: t("fineGrainSamlEndpointConfig"),
isHidden: protocol === openIdConnect,
isHidden: protocol === PROTOCOL_OIDC,
panel: (
<>
<Text className="pf-v5-u-pb-lg">
@@ -199,6 +204,24 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
</>
),
},
{
title: t("openIdVerifiableCredentials"),
isHidden:
(protocol !== PROTOCOL_OIDC && protocol !== PROTOCOL_OID4VC) ||
!isFeatureEnabled(Feature.OpenId4VCI),
panel: (
<>
<Text className="pf-v5-u-pb-lg">
{t("openIdVerifiableCredentialsHelp")}
</Text>
<OpenIdVerifiableCredentials
client={client}
save={save}
reset={() => resetFields(["oid4vci.enabled"])}
/>
</>
),
},
{
title: t("authenticationOverrides"),
panel: (

View File

@@ -0,0 +1,51 @@
import { Button, ActionGroup } from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../../components/form/FormAccess";
import { convertAttributeNameToForm } from "../../util";
import { FormFields, SaveOptions } from "../ClientDetails";
import ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { DefaultSwitchControl } from "../../components/SwitchControl";
type OpenIdVerifiableCredentialsProps = {
client: ClientRepresentation;
save: (options?: SaveOptions) => void;
reset: () => void;
};
export const OpenIdVerifiableCredentials = ({
save,
reset,
}: OpenIdVerifiableCredentialsProps) => {
const { t } = useTranslation();
return (
<FormAccess role="manage-clients" isHorizontal>
<DefaultSwitchControl
name={convertAttributeNameToForm<FormFields>(
"attributes.oid4vci.enabled",
)}
label={t("oid4vciEnabled")}
labelIcon={t("oid4vciEnabledHelp")}
stringify
/>
<ActionGroup>
<Button
variant="secondary"
id="oid4vciSave"
data-testid="oid4vciSave"
onClick={() => save()}
>
{t("save")}
</Button>
<Button
id="oid4vciRevert"
data-testid="oid4vciRevert"
variant="link"
onClick={reset}
>
{t("revert")}
</Button>
</ActionGroup>
</FormAccess>
);
};

View File

@@ -1,4 +1,4 @@
import { test } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { v4 as uuidv4 } from "uuid";
import adminClient from "../utils/AdminClient";
import { login } from "../utils/login";
@@ -30,6 +30,10 @@ import {
switchOffExcludeSessionStateSwitch,
saveAuthFlowOverride,
revertAuthFlowOverride,
saveOid4vci,
revertOid4vci,
assertOid4vciEnabled,
switchOid4vciEnabled,
} from "./advanced";
test.describe("Advanced tab test", () => {
@@ -129,3 +133,55 @@ test.describe("Client Offline Session Max", () => {
await assertTokenLifespanClientOfflineSessionMaxVisible(page, true);
});
});
test.describe("OpenID for Verifiable Credentials", () => {
const realmName = `oid4vci-test-${uuidv4()}`;
const clientIdOpenIdConnect = `client-oidc-${uuidv4()}`;
test.beforeAll(async () => {
await adminClient.createRealm(realmName, {});
await adminClient.createClient({
clientId: clientIdOpenIdConnect,
realm: realmName,
protocol: "openid-connect",
});
});
test.afterAll(() => adminClient.deleteRealm(realmName));
test.describe("with protocol openid-connect", () => {
test.beforeEach(async ({ page }) => {
await login(page);
await goToRealm(page, realmName);
await goToClients(page);
await clickTableRowItem(page, clientIdOpenIdConnect);
await page.waitForSelector('[data-testid="advancedTab"]', {
state: "visible",
timeout: 10000,
});
await page.getByTestId("advancedTab").click();
});
test("should handle OID4VC section visibility based on feature flag", async ({
page,
}) => {
const toggleSwitch = page.locator("#attributes\\.oid4vci🍺enabled");
const isVisible = await toggleSwitch.isVisible();
if (isVisible) {
await toggleSwitch.scrollIntoViewIfNeeded();
await assertOid4vciEnabled(page, false);
await switchOid4vciEnabled(page, true);
await saveOid4vci(page);
await assertOid4vciEnabled(page, true);
await switchOid4vciEnabled(page, false);
await assertOid4vciEnabled(page, false);
await revertOid4vci(page);
await assertOid4vciEnabled(page, true);
} else {
await expect(toggleSwitch).toBeHidden();
}
});
});
});

View File

@@ -107,6 +107,7 @@ const oAuthMutualSwitch =
"#attributes\\.tls🍺client🍺certificate🍺bound🍺access🍺tokens";
const pushedAuthorizationRequestRequiredSwitch =
"#attributes\\.require🍺pushed🍺authorization🍺requests";
const oid4vciEnabledSwitch = "#attributes\\.oid4vci🍺enabled";
export async function clickAdvancedSwitches(page: Page, toggle = true) {
if (toggle) {
@@ -164,3 +165,27 @@ export async function saveAuthFlowOverride(page: Page) {
export async function revertAuthFlowOverride(page: Page) {
await page.getByTestId("OIDCAuthFlowOverrideRevert").click();
}
export async function switchOid4vciEnabled(page: Page, enable: boolean) {
if (enable) {
await switchOn(page, oid4vciEnabledSwitch);
} else {
await switchOff(page, oid4vciEnabledSwitch);
}
}
export async function assertOid4vciEnabled(page: Page, enabled: boolean) {
if (enabled) {
await expect(page.locator(oid4vciEnabledSwitch)).toBeChecked();
} else {
await expect(page.locator(oid4vciEnabledSwitch)).not.toBeChecked();
}
}
export async function saveOid4vci(page: Page) {
await page.getByTestId("oid4vciSave").click();
}
export async function revertOid4vci(page: Page) {
await page.getByTestId("oid4vciRevert").click();
}

View File

@@ -37,6 +37,9 @@ import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.component.ComponentFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@@ -123,6 +126,9 @@ public class OID4VCIssuerEndpoint {
// lifespan of the preAuthorizedCodes in seconds
private final int preAuthorizedCodeLifeSpan;
// constant for the OID4VCI enabled attribute key
private static final String OID4VCI_ENABLED_ATTRIBUTE_KEY = "oid4vci.enabled";
/**
* Credential builders are responsible for initiating the production of
* credentials in a specific format. Their output is an appropriate credential
@@ -174,6 +180,34 @@ public class OID4VCIssuerEndpoint {
credentialBuilder -> credentialBuilder));
}
/**
* Validates whether the authenticated client is enabled for OID4VCI features.
* <p>
* If the client is not enabled, this method logs the status and throws a
* {@link CorsErrorResponseException} with an appropriate error message.
* </p>
*
* @throws CorsErrorResponseException if the client is not enabled for OID4VCI.
*/
private void checkClientEnabled() {
AuthenticatedClientSessionModel clientSession = getAuthenticatedClientSession();
ClientModel client = clientSession.getClient();
boolean oid4vciEnabled = Boolean.parseBoolean(client.getAttributes().get(OID4VCI_ENABLED_ATTRIBUTE_KEY));
if (!oid4vciEnabled) {
LOGGER.debugf("Client '%s' is not enabled for OID4VCI features.", client.getClientId());
throw new CorsErrorResponseException(
cors,
Errors.INVALID_CLIENT,
"Client not enabled for OID4VCI",
Response.Status.FORBIDDEN
);
}
LOGGER.debugf("Client '%s' is enabled for OID4VCI features.", client.getClientId());
}
/**
* Generates a unique notification ID for use in CredentialResponse.
*
@@ -182,7 +216,7 @@ public class OID4VCIssuerEndpoint {
private String generateNotificationId() {
return SecretGenerator.getInstance().randomString();
}
/**
* the OpenId4VCI nonce-endpoint
*
@@ -213,6 +247,15 @@ public class OID4VCIssuerEndpoint {
AuthenticatedClientSessionModel clientSession = getAuthenticatedClientSession();
// Initialize CORS configuration and validate if the client is enabled for OID4VCI
cors = Cors.builder()
.auth()
.allowedMethods("GET")
.auth()
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
checkClientEnabled();
Map<String, SupportedCredentialConfiguration> credentialsMap = OID4VCIssuerWellKnownProvider.getSupportedCredentials(session);
LOGGER.debugf("Get an offer for %s", vcId);
if (!credentialsMap.containsKey(vcId)) {
@@ -335,6 +378,9 @@ public class OID4VCIssuerEndpoint {
// do first to fail fast on auth
AuthenticationManager.AuthResult authResult = getAuthResult();
// checkClientEnabled call after authentication
checkClientEnabled();
// Both credential_configuration_id and credential_identifier are optional.
// If the credential_configuration_id is present, credential_identifier can't be present.
// But this implementation will tolerate the presence of both, waiting for clarity in specifications.
@@ -345,7 +391,7 @@ public class OID4VCIssuerEndpoint {
// Check if at least one of both is available.
if (requestedCredentialConfigurationId == null && requestedCredentialIdentifier == null) {
LOGGER.debugf("Missing both credential_configuration_id and credential_identifier. " +
"At least one must be specified.");
"At least one must be specified.");
throw new BadRequestException(getErrorResponse(ErrorType.MISSING_CREDENTIAL_IDENTIFIER_AND_CONFIGURATION_ID));
}

View File

@@ -60,6 +60,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.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.representations.JsonWebToken;
@@ -116,6 +117,10 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
protected CloseableHttpClient httpClient;
protected ClientRepresentation client;
protected boolean shouldEnableOid4vci() {
return true;
}
protected static String prepareSessionCode(KeycloakSession session, AppAuthManager.BearerTokenAuthenticator authenticator, String note) {
AuthenticationManager.AuthResult authResult = authenticator.authenticate();
UserSessionModel userSessionModel = authResult.getSession();
@@ -124,14 +129,14 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
String codeId = SecretGenerator.getInstance().randomString();
String nonce = SecretGenerator.getInstance().randomString();
OAuth2Code oAuth2Code = new OAuth2Code(codeId,
Time.currentTime() + 6000,
nonce,
CREDENTIAL_OFFER_URI_CODE_SCOPE,
null,
null,
null,
null,
authenticatedClientSessionModel.getUserSession().getId());
Time.currentTime() + 6000,
nonce,
CREDENTIAL_OFFER_URI_CODE_SCOPE,
null,
null,
null,
null,
authenticatedClientSessionModel.getUserSession().getId());
String oauthCode = OAuth2CodeParser.persistCode(session, authenticatedClientSessionModel, oAuth2Code);
@@ -149,7 +154,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
session,
authenticator,
Map.of(jwtCredentialBuilder.getSupportedFormat(), jwtCredentialBuilder,
sdJwtCredentialBuilder.getSupportedFormat(), sdJwtCredentialBuilder)
sdJwtCredentialBuilder.getSupportedFormat(), sdJwtCredentialBuilder)
);
}
@@ -174,31 +179,34 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
// Register the optional client scopes
sdJwtTypeCredentialClientScope = registerOptionalClientScope(sdJwtTypeCredentialScopeName,
null,
sdJwtTypeCredentialConfigurationIdName,
sdJwtTypeCredentialScopeName,
sdJwtCredentialVct,
Format.SD_JWT_VC,
null);
null,
sdJwtTypeCredentialConfigurationIdName,
sdJwtTypeCredentialScopeName,
sdJwtCredentialVct,
Format.SD_JWT_VC,
null);
jwtTypeCredentialClientScope = registerOptionalClientScope(jwtTypeCredentialScopeName,
TEST_DID.toString(),
jwtTypeCredentialConfigurationIdName,
jwtTypeCredentialScopeName,
null,
Format.JWT_VC,
TEST_CREDENTIAL_MAPPERS_FILE);
TEST_DID.toString(),
jwtTypeCredentialConfigurationIdName,
jwtTypeCredentialScopeName,
null,
Format.JWT_VC,
TEST_CREDENTIAL_MAPPERS_FILE);
minimalJwtTypeCredentialClientScope = registerOptionalClientScope("vc-with-minimal-config",
null,
null,
null,
null,
null,
null);
null,
null,
null,
null,
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());
// Enable OID4VCI for the client by default, but allow tests to override
setClientOid4vciEnabled(clientId, shouldEnableOid4vci());
}
protected String getBearerToken(OAuthClient oAuthClient) {
@@ -217,7 +225,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
oAuthClient.scope(credentialScopeName);
}
AuthorizationEndpointResponse authorizationEndpointResponse = oAuthClient.doLogin("john",
"password");
"password");
return oAuthClient.doAccessTokenRequest(authorizationEndpointResponse.getCode()).getAccessToken();
}
@@ -251,7 +259,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
clientScope.setProtocol(Oid4VciConstants.OID4VC_PROTOCOL);
Map<String, String> attributes =
new HashMap<>(Map.of(ClientScopeModel.INCLUDE_IN_TOKEN_SCOPE, "true",
CredentialScopeModel.EXPIRY_IN_SECONDS, "15"));
CredentialScopeModel.EXPIRY_IN_SECONDS, "15"));
BiConsumer<String, String> addAttribute = (attributeName, value) -> {
if (value != null) {
attributes.put(attributeName, value);
@@ -266,9 +274,9 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
String vcDisplay;
try {
vcDisplay = JsonSerialization.writeValueAsString(List.of(new DisplayObject().setName(credentialConfigurationId)
.setLocale("en-EN"),
new DisplayObject().setName(credentialConfigurationId)
.setLocale("de-DE")));
.setLocale("en-EN"),
new DisplayObject().setName(credentialConfigurationId)
.setLocale("de-DE")));
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -288,8 +296,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
if (protocolMapperReferenceFile == null) {
protocolMappers = getProtocolMappers(scopeName);
addProtocolMappersToClientScope(clientScope, protocolMappers);
}
else {
} else {
protocolMappers = resolveProtocolMappers(protocolMapperReferenceFile);
protocolMappers.add(getStaticClaimMapper(scopeName));
addProtocolMappersToClientScope(clientScope, protocolMappers);
@@ -304,7 +311,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
}
try (InputStream inputStream = getClass().getResourceAsStream(protocolMapperReferenceFile)) {
return JsonSerialization.mapper.readValue(inputStream,
ClientScopeRepresentation.class).getProtocolMappers();
ClientScopeRepresentation.class).getProtocolMappers();
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -320,6 +327,17 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
user.logout();
}
void setClientOid4vciEnabled(String clientId, boolean enabled) {
ClientRepresentation clientRepresentation = adminClient.realm(TEST_REALM_NAME).clients().findByClientId(clientId).get(0);
ClientResource clientResource = adminClient.realm(TEST_REALM_NAME).clients().get(clientRepresentation.getId());
Map<String, String> attributes = new HashMap<>(clientRepresentation.getAttributes() != null ? clientRepresentation.getAttributes() : Map.of());
attributes.put("oid4vci.enabled", String.valueOf(enabled));
clientRepresentation.setAttributes(attributes);
clientResource.update(clientRepresentation);
}
// Tests the AuthZCode complete flow without scope from
// 1. Get authorization code without scope specified by wallet
// 2. Using the code to get access token
@@ -336,8 +354,8 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
try (Client client = AdminClientUtil.createResteasyClient()) {
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
URI oid4vciDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder)
.build(TEST_REALM_NAME,
OID4VCIssuerWellKnownProviderFactory.PROVIDER_ID);
.build(TEST_REALM_NAME,
OID4VCIssuerWellKnownProviderFactory.PROVIDER_ID);
WebTarget oid4vciDiscoveryTarget = client.target(oid4vciDiscoveryUri);
// 1. Get authoriZation code without scope specified by wallet
@@ -347,7 +365,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
// 3. Get the credential configuration id from issuer metadata at .wellKnown
try (Response discoveryResponse = oid4vciDiscoveryTarget.request().get()) {
CredentialIssuer oid4vciIssuerConfig = JsonSerialization.readValue(discoveryResponse.readEntity(String.class),
CredentialIssuer.class);
CredentialIssuer.class);
assertEquals(200, discoveryResponse.getStatus());
assertEquals(getRealmPath(TEST_REALM_NAME), oid4vciIssuerConfig.getCredentialIssuer());
assertEquals(getBasePath(TEST_REALM_NAME) + "credential", oid4vciIssuerConfig.getCredentialEndpoint());
@@ -360,17 +378,17 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
CredentialRequest request = new CredentialRequest();
request.setCredentialConfigurationId(oid4vciIssuerConfig.getCredentialsSupported()
.get(testCredentialConfigurationId)
.getId());
.get(testCredentialConfigurationId)
.getId());
assertEquals(testFormat,
oid4vciIssuerConfig.getCredentialsSupported()
.get(testCredentialConfigurationId)
.getFormat());
oid4vciIssuerConfig.getCredentialsSupported()
.get(testCredentialConfigurationId)
.getFormat());
assertEquals(testCredentialConfigurationId,
oid4vciIssuerConfig.getCredentialsSupported()
.get(testCredentialConfigurationId)
.getId());
oid4vciIssuerConfig.getCredentialsSupported()
.get(testCredentialConfigurationId)
.getId());
c.accept(Map.of(
"accessToken", token,
@@ -401,7 +419,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
request.setCredentialConfigurationId(offeredCredential.getId());
StringEntity stringEntity = new StringEntity(JsonSerialization.writeValueAsString(request),
ContentType.APPLICATION_JSON);
ContentType.APPLICATION_JSON);
HttpPost postCredential = new HttpPost(credentialEndpoint);
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
@@ -440,9 +458,9 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
// Find existing client representation
ClientRepresentation existingClient = testRealm.getClients().stream()
.filter(client -> client.getClientId().equals(clientId))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Client with ID " + clientId + " not found in realm"));
.filter(client -> client.getClientId().equals(clientId))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Client with ID " + clientId + " not found in realm"));
// Add role to existing client
if (testRealm.getRoles() != null) {
@@ -456,15 +474,14 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
return mergedRoles;
}
);
}
else {
} else {
testRealm.getRoles()
.setClient(Map.of(existingClient.getClientId(),
List.of(getRoleRepresentation("testRole", existingClient.getClientId()))));
.setClient(Map.of(existingClient.getClientId(),
List.of(getRoleRepresentation("testRole", existingClient.getClientId()))));
}
List<UserRepresentation> realmUsers = Optional.ofNullable(testRealm.getUsers()).map(ArrayList::new)
.orElse(new ArrayList<>());
.orElse(new ArrayList<>());
realmUsers.add(getUserRepresentation(Map.of(existingClient.getClientId(), List.of("testRole"))));
testRealm.setUsers(realmUsers);
}
@@ -495,7 +512,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
assertNotNull("The first credential in the array should not be null.", credentialObj);
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialObj.getCredential(),
JsonWebToken.class).getToken();
JsonWebToken.class).getToken();
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
VerifiableCredential credential = JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get(
"vc"), VerifiableCredential.class);
@@ -503,12 +520,12 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
assertEquals("john@email.cz", credential.getCredentialSubject().getClaims().get("email"));
assertTrue("The static claim should be set.",
credential.getCredentialSubject().getClaims().containsKey("scope-name"));
credential.getCredentialSubject().getClaims().containsKey("scope-name"));
assertEquals("The static claim should be set.",
clientScope.getName(),
credential.getCredentialSubject().getClaims().get("scope-name"));
clientScope.getName(),
credential.getCredentialSubject().getClaims().get("scope-name"));
assertFalse("Only mappers supported for the requested type should have been evaluated.",
credential.getCredentialSubject().getClaims().containsKey("AnotherCredentialType"));
credential.getCredentialSubject().getClaims().containsKey("AnotherCredentialType"));
}
}
}

View File

@@ -0,0 +1,56 @@
package org.keycloak.testsuite.oid4vc.issuance.signing;
import jakarta.ws.rs.core.Response;
import org.junit.Test;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
import org.keycloak.protocol.oid4vc.model.Format;
import org.keycloak.protocol.oid4vc.model.OfferUriType;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.testsuite.Assert;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
/**
* Tests for OID4VCIssuerEndpoint with OID4VCI disabled.
*/
public class OID4VCJWTIssuerEndpointDisabledTest extends OID4VCIssuerEndpointTest {
@Override
protected boolean shouldEnableOid4vci() {
return false;
}
@Test
public void testClientNotEnabled() {
testWithBearerToken(token -> testingClient.server(TEST_REALM_NAME).run((session) -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(token);
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
// Test getCredentialOfferURI
CorsErrorResponseException offerUriException = Assert.assertThrows(CorsErrorResponseException.class, () ->
issuerEndpoint.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0)
);
assertEquals("Should fail with 403 Forbidden when client is not OID4VCI-enabled",
Response.Status.FORBIDDEN.getStatusCode(), offerUriException.getResponse().getStatus());
// Test requestCredential
CredentialRequest credentialRequest = new CredentialRequest()
.setCredentialIdentifier(jwtTypeCredentialScopeName);
CorsErrorResponseException requestException = Assert.assertThrows(CorsErrorResponseException.class, () ->
issuerEndpoint.requestCredential(credentialRequest)
);
assertEquals("Should fail with 403 Forbidden when client is not OID4VCI-enabled",
Response.Status.FORBIDDEN.getStatusCode(), requestException.getResponse().getStatus());
}));
}
private void testWithBearerToken(Consumer<String> testLogic) {
String token = getBearerToken(oauth);
testLogic.accept(token);
}
}

View File

@@ -0,0 +1,56 @@
package org.keycloak.testsuite.oid4vc.issuance.signing;
import jakarta.ws.rs.core.Response;
import org.junit.Test;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
import org.keycloak.protocol.oid4vc.model.Format;
import org.keycloak.protocol.oid4vc.model.OfferUriType;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.testsuite.Assert;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
/**
* Tests for OID4VCIssuerEndpoint with OID4VCI disabled for SD-JWT format
*/
public class OID4VCSdJwtIssuingEndpointDisabledTest extends OID4VCIssuerEndpointTest {
@Override
protected boolean shouldEnableOid4vci() {
return false;
}
@Test
public void testClientNotEnabled() {
testWithBearerToken(token -> testingClient.server(TEST_REALM_NAME).run((session) -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(token);
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
// Test getCredentialOfferURI
CorsErrorResponseException offerUriException = Assert.assertThrows(CorsErrorResponseException.class, () ->
issuerEndpoint.getCredentialOfferURI("test-credential", OfferUriType.URI, 0, 0)
);
assertEquals("Should fail with 403 Forbidden when client is not OID4VCI-enabled",
Response.Status.FORBIDDEN.getStatusCode(), offerUriException.getResponse().getStatus());
// Test requestCredential
CredentialRequest credentialRequest = new CredentialRequest()
.setCredentialConfigurationId(sdJwtTypeCredentialConfigurationIdName);
CorsErrorResponseException requestException = Assert.assertThrows(CorsErrorResponseException.class, () ->
issuerEndpoint.requestCredential(credentialRequest)
);
assertEquals("Should fail with 403 Forbidden when client is not OID4VCI-enabled",
Response.Status.FORBIDDEN.getStatusCode(), requestException.getResponse().getStatus());
}));
}
private void testWithBearerToken(Consumer<String> testLogic) {
String token = getBearerToken(oauth);
testLogic.accept(token);
}
}