Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {

private static final Logger LOGGER = Logger.getLogger(OID4VCIssuerWellKnownProvider.class);

public static final String PROVIDER_ID = "openid-credential-issuer";

// Realm attributes for signed metadata configuration
public static final String SIGNED_METADATA_ENABLED_ATTR = "oid4vci.signed_metadata.enabled";
public static final String SIGNED_METADATA_LIFESPAN_ATTR = "oid4vci.signed_metadata.lifespan";
public static final String SIGNED_METADATA_ALG_ATTR = "oid4vci.signed_metadata.alg";

Expand Down Expand Up @@ -161,9 +162,8 @@ public Object getMetadataResponse(CredentialIssuer issuer, KeycloakSession sessi
RealmModel realm = session.getContext().getRealm();
String acceptHeader = session.getContext().getRequestHeaders().getHeaderString(HttpHeaders.ACCEPT);
boolean preferJwt = acceptHeader != null && acceptHeader.contains(MediaType.APPLICATION_JWT);
boolean signedMetadataEnabled = Boolean.parseBoolean(realm.getAttribute(SIGNED_METADATA_ENABLED_ATTR));

if (preferJwt && signedMetadataEnabled) {
if (preferJwt) {
Optional<String> signedJwt = generateSignedMetadata(issuer, session);
if (signedJwt.isPresent()) {
return signedJwt.get();
Expand Down Expand Up @@ -235,15 +235,17 @@ public Optional<String> generateSignedMetadata(CredentialIssuer metadata, Keyclo
JsonWebToken jwt = createMetadataJwt(metadata, realm);

// Validate lifespan configuration
String lifespanStr = realm.getAttribute(SIGNED_METADATA_LIFESPAN_ATTR);
if (lifespanStr != null) {
Optional<String> maybeLifespan = Optional.ofNullable(realm.getAttribute(SIGNED_METADATA_LIFESPAN_ATTR));
if (maybeLifespan.isPresent()) {
String lifespanVal = maybeLifespan.get();
try {
long lifespan = Long.parseLong(lifespanStr);
jwt.exp(Time.currentTime() + lifespan);
jwt.exp(Time.currentTime() + Long.parseLong(lifespanVal));
} catch (NumberFormatException e) {
LOGGER.warnf("Invalid lifespan duration for signed metadata: %s. Falling back to unsigned metadata.", lifespanStr);
LOGGER.warnf("Invalid lifespan duration for signed metadata: %s. Falling back to unsigned metadata.", lifespanVal);
return Optional.empty(); // Return empty to indicate fallback to JSON
}
} else {
jwt.exp(Time.currentTime() + 3600L);
}

// Build JWS with proper headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

Expand All @@ -30,6 +31,7 @@
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
Comment thread
tdiesler marked this conversation as resolved.
public class CredentialIssuer {

@JsonProperty("credential_issuer")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.testframework.annotations.InjectAdminClient;
Expand Down Expand Up @@ -118,13 +119,6 @@ public abstract class OID4VCIssuerTestBase {
public static final String minimalJwtTypeCredentialScopeName = "vc-with-minimal-config";
public static final String minimalJwtTypeCredentialConfigurationIdName = "vc-with-minimal-config-id";

protected CredentialScopeRepresentation minimalJwtTypeCredentialScope;
protected CredentialScopeRepresentation jwtTypeCredentialScope;
protected CredentialScopeRepresentation sdJwtTypeCredentialScope;

protected String clientId = "test-app";
protected ClientRepresentation client;

@InjectRealm(config = VCTestRealmConfig.class)
protected ManagedRealm testRealm;

Expand All @@ -149,6 +143,13 @@ public abstract class OID4VCIssuerTestBase {
@InjectKeycloakUrls
protected KeycloakUrls keycloakUrls;

protected CredentialScopeRepresentation minimalJwtTypeCredentialScope;
protected CredentialScopeRepresentation jwtTypeCredentialScope;
protected CredentialScopeRepresentation sdJwtTypeCredentialScope;

protected String clientId = "test-app";
protected ClientRepresentation client;

@TestSetup
public void configureTestRealm() {
RealmResource realmResource = testRealm.admin();
Expand All @@ -162,10 +163,12 @@ public void configureTestRealm() {
@BeforeEach
void beforeEachInternal() {
client = managedClient.admin().toRepresentation();

jwtTypeCredentialScope = requireExistingCredentialScope(jwtTypeCredentialScopeName);
minimalJwtTypeCredentialScope = requireExistingCredentialScope(minimalJwtTypeCredentialScopeName);
sdJwtTypeCredentialScope = requireExistingCredentialScope(sdJwtTypeCredentialScopeName);
oauth.client(OID4VCI_CLIENT_ID, "test-secret");

oauth.client(client.getClientId(), client.getSecret());
enableVerifiableCredentialEvents(testRealm);
}

Expand Down Expand Up @@ -292,6 +295,28 @@ protected AccessTokenResponse getBearerToken(OAuthClient oauthClient, String aut
return tokenResponse;
}

protected String getRealmAttribute(String key) {
RealmRepresentation realm = testRealm.admin().toRepresentation();
Map<String, String> attributes = realm.getAttributesOrEmpty();
return attributes.get(key);
}

protected void setRealmAttributes(Map<String, String> extraAttributes) {
RealmResource realmResource = testRealm.admin();
RealmRepresentation realm = realmResource.toRepresentation();
Map<String, String> attributes = realm.getAttributesOrEmpty();
attributes.putAll(extraAttributes);
realm.setAttributes(attributes);
realmResource.update(realm);
}

protected void setVerifiableCredentialsEnabled(boolean enabled) {
RealmResource realmResource = testRealm.admin();
RealmRepresentation realm = realmResource.toRepresentation();
realm.setVerifiableCredentialsEnabled(enabled);
realmResource.update(realm);
}

protected CredentialScopeRepresentation requireExistingCredentialScope(String scopeName) {
return Optional.ofNullable(getExistingCredentialScope(scopeName))
.orElseThrow(() -> new IllegalStateException("No such credential scope: " + scopeName));
Expand Down
Loading
Loading