Skip to content

Commit

Permalink
VC issuance in Authz Code flow with considering scope parameter
Browse files Browse the repository at this point in the history
closes keycloak#29725

Signed-off-by: Takashi Norimatsu <[email protected]>
  • Loading branch information
tnorimat committed Jun 15, 2024
1 parent c516405 commit 13cf752
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantType;
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.utils.MediaType;
Expand All @@ -76,6 +78,7 @@
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
Expand Down Expand Up @@ -109,6 +112,8 @@ public class OID4VCIssuerEndpoint {

private final Map<Format, VerifiableCredentialsSigningService> signingServices;

private final boolean isIgnoreScopeCheck;

public OID4VCIssuerEndpoint(KeycloakSession session,
String issuerDid,
Map<Format, VerifiableCredentialsSigningService> signingServices,
Expand All @@ -121,9 +126,24 @@ public OID4VCIssuerEndpoint(KeycloakSession session,
this.issuerDid = issuerDid;
this.signingServices = signingServices;
this.preAuthorizedCodeLifeSpan = preAuthorizedCodeLifeSpan;

this.isIgnoreScopeCheck = false;
}

public OID4VCIssuerEndpoint(KeycloakSession session,
String issuerDid,
Map<Format, VerifiableCredentialsSigningService> signingServices,
AppAuthManager.BearerTokenAuthenticator authenticator,
ObjectMapper objectMapper, TimeProvider timeProvider, int preAuthorizedCodeLifeSpan,
boolean isIgnoreScopeCheck) {
this.session = session;
this.bearerTokenAuthenticator = authenticator;
this.objectMapper = objectMapper;
this.timeProvider = timeProvider;
this.issuerDid = issuerDid;
this.signingServices = signingServices;
this.preAuthorizedCodeLifeSpan = preAuthorizedCodeLifeSpan;
this.isIgnoreScopeCheck = isIgnoreScopeCheck;
}

/**
* Provides the URI to the OID4VCI compliant credentials offer
Expand Down Expand Up @@ -238,6 +258,26 @@ public Response getCredentialOffer(@PathParam("nonce") String nonce) {
.build();
}

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();
String credentialIdentifier = credentialRequestVO.getCredentialIdentifier();
String scope = client.getAttributes().get("vc." + credentialIdentifier + ".scope"); // following credential identifier in client attribute
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 BadRequestException(getErrorResponse(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE));
} else {
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
*/
Expand All @@ -252,6 +292,10 @@ public Response requestCredential(
// do first to fail fast on auth
UserSessionModel userSessionModel = getUserSessionModel();

if (!isIgnoreScopeCheck) {
checkScope(credentialRequestVO);
}

Format requestedFormat = credentialRequestVO.getFormat();
String requestedCredential = credentialRequestVO.getCredentialIdentifier();

Expand Down Expand Up @@ -426,4 +470,4 @@ private VerifiableCredential getVCToSign(List<OID4VCMapper> protocolMappers, Str
LOGGER.debugf("The credential to sign is: %s", vc);
return vc;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class PreAuthorizedCodeGrantType extends OAuth2GrantTypeBase {

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

public static final String VC_ISSUANCE_FLOW = "VC-Issuance-Flow";

@Override
public Response process(Context context) {
LOGGER.debug("Process grant request for preauthorized.");
Expand Down Expand Up @@ -73,6 +75,7 @@ public Response process(Context context) {
AuthenticatedClientSessionModel clientSession = result.getClientSession();
ClientSessionContext sessionContext = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession,
OAuth2Constants.SCOPE_OPENID, session);
clientSession.setNote(VC_ISSUANCE_FLOW, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE);


// set the client as retrieved from the pre-authorized session
Expand Down Expand Up @@ -119,4 +122,4 @@ public static String getPreAuthorizedCode(KeycloakSession session, Authenticated
authenticatedClientSession.getUserSession().getId());
return OAuth2CodeParser.persistCode(session, authenticatedClientSession, oAuth2Code);
}
}
}
Loading

0 comments on commit 13cf752

Please sign in to comment.