8000 [OID4VCI] Restore support for pre-auth tx_code by tdiesler · Pull Request #47653 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
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 @@ -27,9 +27,7 @@
/**
* This condition determines to which client a client policy is adopted.
* The condition can be evaluated on the events defined in {@link ClientPolicyEvent}.
* It is sufficient for the implementer of this condition to implement methods in which they are interested
* and {@link isEvaluatedOnEvent} method.
*
*
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public interface ClientPolicyConditionProvider<CONFIG extends ClientPolicyConditionConfigurationRepresentation> extends Provider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@
/**
* This executor specifies what action is executed on the client to which a client policy is adopted.
* The executor can be executed on the events defined in {@link ClientPolicyEvent}.
* It is sufficient for the implementer of this executor to implement methods in which they are interested
* and {@link isEvaluatedOnEvent} method.
*
*
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public interface ClientPolicyExecutorProvider<CONFIG extends ClientPolicyExecutorConfigurationRepresentation> extends Provider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ public enum ClientPolicyEvent {

SAML_AUTHN_REQUEST,
SAML_LOGOUT_REQUEST,

// OID4VCI
CREDENTIAL_OFFER_CREATE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.keycloak.protocol.oid4vc.clientpolicy;

public interface CredentialClientPolicies {

/**
* Governs whether the given `credential_configuration_id` requires a Credential Offer
*/
PredicateCredentialClientPolicy VC_POLICY_CREDENTIAL_OFFER_REQUIRED = new PredicateCredentialClientPolicy(
"oid4vci-offer-required", "vc.policy.offer.required", true, false);

/**
* Governs whether Credential Offers with `pre-authorized_code` grant are allowed
*/
PredicateCredentialClientPolicy VC_POLICY_CREDENTIAL_OFFER_PREAUTH_ALLOWED = new PredicateCredentialClientPolicy(
"oid4vci-offer-preauth-allowed", "vc.policy.offer.preauth.allowed", true, true);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.keycloak.protocol.oid4vc.clientpolicy;

import org.keycloak.protocol.oid4vc.model.CredentialScopeRepresentation;


public abstract class CredentialClientPolicy<T> {

private final String name;
private final String attrName;
private final Class<T> type;
private final T expectedValue;
private final T defaultValue;

public CredentialClientPolicy(String name, String attrName, Class<T> type, T expectedValue, T defaultValue) {
this.name = name;
this.attrName = attrName;
this.expectedValue = expectedValue;
this.defaultValue = defaultValue;
this.type = type;
}

public String getName() {
return name;
}

public String getAttrName() {
return attrName;
}

public Class<T> getType() {
return type;
}

public T getExpectedValue() {
return expectedValue;
}

public T getDefaultValue() {
return defaultValue;
}

public abstract T getCurrentValue(CredentialScopeRepresentation credScope);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.keycloak.protocol.oid4vc.clientpolicy;

import org.keycloak.models.oid4vci.CredentialScopeModel;
import org.keycloak.protocol.oid4vc.issuance.credentialoffer.CredentialOfferState;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;

public class CredentialClientPolicyContext implements ClientPolicyContext {

private final ClientPolicyEvent event;
private CredentialScopeModel credScopeModel;
private CredentialOfferState offerState;
private boolean evaluatedOnEvent;

public CredentialClientPolicyContext(ClientPolicyEvent event) {
this.event = event;
}

@Override
public ClientPolicyEvent getEvent() {
return event;
}

public CredentialScopeModel getCredentialScopeModel() {
return credScopeModel;
}

public CredentialClientPolicyContext setCredentialScopeModel(CredentialScopeModel credScopeModel) {
this.credScopeModel = credScopeModel;
return this;
}

public CredentialOfferState getCredentialOfferState() {
return offerState;
}

public CredentialClientPolicyContext setCredentialOfferState(CredentialOfferState offerState) {
this.offerState = offerState;
return this;
}

public boolean isEvaluatedOnEvent() {
return evaluatedOnEvent;
}

public void setEvaluatedOnEvent(boolean evaluated) {
this.evaluatedOnEvent = evaluated;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.keycloak.protocol.oid4vc.clientpolicy;

import org.keycloak.OAuthErrorException;
import org.keycloak.events.Errors;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.oid4vci.CredentialScopeModel;
import org.keycloak.protocol.oid4vc.issuance.credentialoffer.CredentialOfferState;
import org.keycloak.protocol.oid4vc.model.CredentialScopeRepresentation;
import org.keycloak.protocol.oid4vc.model.PreAuthorizedCodeGrant;
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
import org.keycloak.representations.idm.ClientPolicyRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;

import org.jspecify.annotations.Nullable;

import static org.keycloak.protocol.oid4vc.clientpolicy.CredentialClientPolicies.VC_POLICY_CREDENTIAL_OFFER_PREAUTH_ALLOWED;
import static org.keycloak.protocol.oid4vc.clientpolicy.CredentialClientPolicies.VC_POLICY_CREDENTIAL_OFFER_REQUIRED;

public class CredentialClientPolicyExecutor implements ClientPolicyExecutorProvider<ClientPolicyExecutorConfigurationRepresentation> {

protected final KeycloakSession session;

public CredentialClientPolicyExecutor(KeycloakSession session) {
this.session = session;
}

@Override
public String getProviderId() {
return CredentialClientPolicyExecutorFactory.PROVIDER_ID;
}

@Override
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
if (context instanceof CredentialClientPolicyContext) {
checkCredentialPolicies((CredentialClientPolicyContext) context);
}
}

private void checkCredentialPolicies(CredentialClientPolicyContext context) throws ClientPolicyException {

ClientModel client = session.getContext().getClient();
if (client == null) {
throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT, "No issuing client");
}
CredentialScopeModel credScope = context.getCredentialScopeModel();
if (credScope == null) {
throw new ClientPolicyException(OAuthErrorException.INVALID_SCOPE, "No client scope");
}

// Check policy 'oid4vci-offer-preauth-allowed'
//
if (context.getEvent() == ClientPolicyEvent.CREDENTIAL_OFFER_CREATE) {

CredentialOfferState offerState = context.getCredentialOfferState();
if (offerState == null)
throw new ClientPolicyException(OAuthErrorException.UNSUPPORTED_GRANT_TYPE, "No credential offer state");

PredicateCredentialClientPolicy preAuthPolicy = VC_POLICY_CREDENTIAL_OFFER_PREAUTH_ALLOWED;
ClientPolicyRepresentation clientPolicy = findClientPolicyByName(session, preAuthPolicy.getName());

// Allowed by client when policy is not there or enabled
boolean allowedByClient = clientPolicy == null || clientPolicy.isEnabled();

for (String grantType : offerState.getCredentialsOffer().getGrants().keySet()) {
if (grantType.equals(PreAuthorizedCodeGrant.PRE_AUTH_GRANT_TYPE)) {

// Allowed by scope when property is undefined or true
// See default value on the policy definition
boolean allowedByScope = preAuthPolicy.validate(new CredentialScopeRepresentation(credScope));

if (!allowedByClient || !allowedByScope) {
throw new ClientPolicyException(Errors.NOT_ALLOWED,
"Pre-Authorized code grant rejected by policy " + preAuthPolicy.getName() + " for scope " + credScope.getName());
}
}
}
context.setEvaluatedOnEvent(true);
}

// Check policy 'oid4vci-offer-required'
//
if (context.getEvent() == ClientPolicyEvent.AUTHORIZATION_REQUEST) {

PredicateCredentialClientPolicy offerRequiredPolicy = VC_POLICY_CREDENTIAL_OFFER_REQUIRED;
ClientPolicyRepresentation clientPolicy = findClientPolicyByName(session, offerRequiredPolicy.getName());

// Required by client when policy is there and enabled
boolean requiredByClient = clientPolicy != null && clientPolicy.isEnabled();

// Required by scope when property is defined and true
// See default value on the policy definition
boolean requiredByScope = offerRequiredPolicy.validate(new CredentialScopeRepresentation(credScope));

CredentialOfferState offerState = context.getCredentialOfferState();
if ((requiredByClient || requiredByScope) && offerState == null) {
throw new ClientPolicyException(Errors.NOT_ALLOWED,
"Authorization request rejected by policy " + offerRequiredPolicy.getName() + " for scope " + credScope.getName());
}
context.setEvaluatedOnEvent(true);
}
}

public static @Nullable ClientPolicyRepresentation findClientPolicyByName(KeycloakSession session, String policyName) throws ClientPolicyException {
RealmModel realm = session.getContext().getRealm();
ClientPolicyRepresentation clientPolicy = session.clientPolicy().getClientPolicies(realm, false).getPolicies().stream()
.filter(cp -> cp.getName().equals(policyName))
.findFirst().orElse(null);
return clientPolicy;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.keycloak.protocol.oid4vc.clientpolicy;

import java.util.Collections;
import java.util.List;

import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory;

public class CredentialClientPolicyExecutorFactory implements ClientPolicyExecutorProviderFactory {

public static final String PROVIDER_ID = "oid4vci-policy-executor";

@Override
public CredentialClientPolicyExecutor create(KeycloakSession session) {
return new CredentialClientPolicyExecutor(session);
}

@Override
public void init(Config.Scope config) {
}

@Override
public void postInit(KeycloakSessionFactory factory) {
}

@Override
public void close() {
}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public String getHelpText() {
return "This executor checks client policies related to the credential offer process";
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.keycloak.protocol.oid4vc.clientpolicy;

import java.util.Objects;
import java.util.Optional;

import org.keycloak.protocol.oid4vc.model.CredentialScopeRepresentation;

public class PredicateCredentialClientPolicy extends CredentialClientPolicy<Boolean> {

public PredicateCredentialClientPolicy(String name, String key, Boolean exp, Boolean def) {
super(name, key, Boolean.class, exp, def);
}

public Boolean getCurrentValue(CredentialScopeRepresentation credScope) {
Boolean scopeValue = Optional.ofNullable(credScope.getAttribute(getAttrName()))
.map(Boolean::parseBoolean)
.orElse(getDefaultValue());
return scopeValue;
}

public Boolean validate(CredentialScopeRepresentation credScope) {
Boolean scopeValue = credScope.getCredentialPolicyValue(this);
return Objects.equals(scopeValue, getExpectedValue());
}

}
Loading
Loading
0