8000 [OID4VCI] Conformance with OpenID HAIP Profile by tdiesler · Pull Request #49563 · 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 @@ -38,6 +38,7 @@
* @version $Revision: 1 $
*/
public class AccessToken extends IDToken {

public static class Access implements Serializable {
@JsonProperty("roles")
protected Set<String> roles;
Expand Down Expand Up @@ -105,12 +106,16 @@ public void setPermissions(Collection<Permission> permissions) {
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3.1
public static class Confirmation {

@JsonProperty("x5t#S256")
protected String certThumbprint;

@JsonProperty("jkt")
protected String keyThumbprint;

@JsonProperty("cnf_type")
protected String cnfType;

public String getCertThumbprint() {
return certThumbprint;
}
Expand All @@ -121,11 +126,19 @@ public void setCertThumbprint(String certThumbprint) {

public String getKeyThumbprint() {
return keyThumbprint;
}
}

public void setKeyThumbprint(String keyThumbprint) {
public void setKeyThumbprint(String keyThumbprint) {
this.keyThumbprint = keyThumbprint;
}

public String getCnfType() {
return cnfType;
}

public void setCnfType(String cnfType) {
this.cnfType = cnfType;
}
}

@JsonProperty("trusted-certs")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public interface Details {
String ACTION = "action";
String CODE_ID = "code_id";
String REDIRECT_URI = "redirect_uri";
String REQUEST_URI = "request_uri";
String RESPONSE_TYPE = "response_type";
String RESPONSE_MODE = "response_mode";
String GRANT_TYPE = "grant_type";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
import org.keycloak.protocol.oidc.endpoints.request.RequestUriType;
import org.keycloak.protocol.oidc.par.endpoints.request.AuthzEndpointParParser;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ServicesLogger;
Expand Down Expand Up @@ -1243,10 +1246,21 @@ protected Response authenticationComplete() {

String nextRequiredAction = nextRequiredAction();
if (nextRequiredAction != null) {
return AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction);
Response response = AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction);
return response;
} else {
// Authorization servers that enforce one-time use of request_uri values do so at the point of authorization,
// not at the point of visiting the authorization endpoint
String requestUri = authenticationSession.getAuthNote(Details.REQUEST_URI);
RequestUriType requestUriType = Optional.ofNullable(requestUri)
.map(AuthorizationEndpointRequestParserProcessor::getRequestUriType)
.orElse(null);
if (requestUriType == RequestUriType.PAR && AuthzEndpointParParser.removeRequestObject(session, requestUri) == null) {
throw new AuthenticationFlowException("PAR not found, not issued or used multiple times.", AuthenticationFlowError.INTERNAL_ERROR);
}
event.detail(Details.CODE_ID, authenticationSession.getParentSession().getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
return AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event);
Response response = AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event);
return response;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public class AttestationBasedClientAuthenticator extends AbstractClientAuthentic
*/
public static final String OAUTH_CLIENT_ATTESTATION_CONFIG_TRUST_IDPS = "attester_trust_idps";

/** The token confirmation type */
public static final String ABCA_CNF_TYPE = "Client-Attestation";

@Override
public String getId() {
return PROVIDER_ID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenCategory;
import org.keycloak.TokenVerifier;
import org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.ABCAResult;
import org.keycloak.authentication.authenticators.util.AcrStore;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
Expand All @@ -63,6 +64,7 @@
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.http.HttpRequest;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.HashUtils;
Expand Down 8 10BC8 Expand Up @@ -129,14 +131,17 @@
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.tracing.TracingAttributes;
import org.keycloak.tracing.TracingProvider;
import org.keycloak.util.JWKSUtils;
import org.keycloak.util.TokenUtil;

import org.jboss.logging.Logger;

import static org.keycloak.OAuth2Constants.ORGANIZATION;
import static org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator.ABCA_CNF_TYPE;
import static org.keycloak.models.Constants.AUTHORIZATION_DETAILS_RESPONSE;
import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser;
import static org.keycloak.representations.IDToken.NONCE;
import static org.keycloak.services.util.DPoPUtil.DPOP_CNF_TYPE;

/**
* Stateless object that creates tokens and manages oauth access codes
Expand Down Expand Up @@ -172,7 +177,7 @@ public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, C
userSession = sessionManager.findOfflineUserSession(realm, oldToken.getSessionState());
if (userSession != null) {

// Revoke timeouted offline userSession
// Revoke timed out offline userSession
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
sessionManager.revokeOfflineUserSession(userSession);
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active");
Expand Down Expand Up @@ -368,9 +373,24 @@ public RefreshToken verifyRefreshToken(KeycloakSession session, RealmModel realm
}
}

if (Profile.isFeatureEnabled(Feature.CLIENT_AUTH_ABCA)) {
AccessToken.Confirmation cnf = refreshToken.getConfirmation();
if (cnf != null && ABCA_CNF_TYPE.equals(cnf.getCnfType())) {
ABCAResult abcaResult = session.getAttribute(ABCAResult.ABCA_RESULT, ABCAResult.class);
if (abcaResult == null) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token. Client attestation must be used with this refresh token");
}
JWK jwk = abcaResult.getAttestationJwt().getConfirmation().getJwk();
String thumbprint = JWKSUtils.computeThumbprint(jwk);
if (!Objects.equals(thumbprint, cnf.getKeyThumbprint())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Attestation-Based Key missmatch");
}
}
}

if (Profile.isFeatureEnabled(Profile.Feature.DPOP)) {
if (DPoPUtil.isDPoPToken(refreshToken)) {
DPoP dPoP = (DPoP) session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE);
if (client.isPublicClient() && DPoPUtil.isDPoPToken(refreshToken)) {
DPoP dPoP = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class);
if (dPoP == null) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "DPoP proof is missing");
}
Expand Down Expand Up @@ -1203,8 +1223,8 @@ public AccessTokenResponseBuilder generateRefreshToken(RefreshToken oldRefreshTo

private void generateRefreshToken(boolean offlineTokenRequested) {
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
AccessToken.Confirmation confirmation = getConfirmation(clientSession, accessToken);
refreshToken = new RefreshToken(accessToken, confirmation);
AccessToken.Confirmation cnf = getRefreshTokenConfirmation(accessToken);
refreshToken = new RefreshToken(accessToken, cnf);
refreshToken.id(SecretGenerator.getInstance().generateSecureID());
refreshToken.issuedNow();
clientSession.setTimestamp(refreshToken.getIat().intValue());
Expand All @@ -1219,19 +1239,20 @@ private void generateRefreshToken(boolean offlineTokenRequested) {
} else {
refreshToken.exp(getExpiration(false));
}
final ClientModel[] resquestedAudienceClients = clientSessionCtx.getAttribute(Constants.REQUESTED_AUDIENCE_CLIENTS, ClientModel[].class);
if (resquestedAudienceClients != null) {
refreshToken.getOtherClaims().put(Constants.REQUESTED_AUDIENCE, Arrays.stream(resquestedAudienceClients)
final ClientModel[] requestedAudienceClients = clientSessionCtx.getAttribute(Constants.REQUESTED_AUDIENCE_CLIENTS, ClientModel[].class);
if (requestedAudienceClients != null) {
refreshToken.getOtherClaims().put(Constants.REQUESTED_AUDIENCE, Arrays.stream(requestedAudienceClients)
.map(ClientModel::getClientId)
.collect(Collectors.toSet()));
}
Boolean bindOnlyRefreshToken = session.getAttributeOrDefault(DPoPUtil.DPOP_BINDING_ONLY_REFRESH_TOKEN_SESSION_ATTRIBUTE, false);
if (bindOnlyRefreshToken) {
DPoP dPoP = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class);
if (dPoP != null) {
confirmation = new AccessToken.Confirmation();
confirmation.setKeyThumbprint(dPoP.getThumbprint());
refreshToken.setConfirmation(confirmation);
cnf = new AccessToken.Confirmation();
cnf.setKeyThumbprint(dPoP.getThumbprint());
cnf.setCnfType(DPOP_CNF_TYPE);
refreshToken.setConfirmation(cnf);
}
}
}
Expand All @@ -1246,17 +1267,27 @@ public void createOrUpdateOfflineSession() {
sessionManager.createOrUpdateOfflineSession(clientSessionCtx.getClientSession(), userSession);
}

/**
* RFC9449 chapter 5<br/>
* Refresh tokens issued to confidential clients are not bound to the DPoP proof public key because
* they are already sender-constrained with a different existing mechanism.<br/>
* <br/>
* Based on the definition above the confirmation is only returned for public-clients.
*/
private AccessToken.Confirmation getConfirmation(AuthenticatedClientSessionModel clientSession,
AccessToken accessToken) {
final boolean isPublicClient = clientSession.getClient().isPublicClient();
return isPublicClient ? accessToken.getConfirmation() : null;
private AccessToken.Confirmation getRefreshTokenConfirmation(AccessToken accessToken) {
// RFC9449 chapter 5
// Refresh tokens issued to confidential clients are not bound to the DPoP proof public key because
// they are already sender-constrained with a different existing mechanism.
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
if (clientSession.getClient().isPublicClient()) {
AccessToken.Confirmation cnf = accessToken.getConfirmation();
return cnf;
}
// Authorization servers issuing a refresh token in response to a token request using the client attestation
// mechanism MUST bind the refresh token to the Client Instance and its associated public key
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-attestation-based-client-auth-07#section-10.3
ABCAResult abcaResult = session.getAttribute(ABCAResult.ABCA_RESULT, ABCAResult.class);
if (abcaResult != null) {
JWK jwk = abcaResult.getAttestationJwt().getConfirmation().getJwk();
AccessToken.Confirmation cnf = new AccessToken.Confirmation();
cnf.setKeyThumbprint(JWKSUtils.computeThumbprint(jwk));
cnf.setCnfType(ABCA_CNF_TYPE);
return cnf;
}
return null;
}

private Long getExpiration(boolean offline) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,11 +401,12 @@ private void updateAuthenticationSession() {
}
}

private Response buildAuthorizationCodeAuthorizationResponse(String requestUriParam) {
private Response buildAuthorizationCodeAuthorizationResponse(String requestUri) {
this.event.event(EventType.LOGIN);
authenticationSession.setAuthNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
authenticationSession.setAuthNote(Details.REQUEST_URI, requestUri);

RequestUriType requestUriType = Optional.ofNullable(requestUriParam)
RequestUriType requestUriType = Optional.ofNullable(requestUri)
.map(AuthorizationEndpointRequestParserProcessor::getRequestUriType)
.orElse(null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ public AuthzEndpointParParser(KeycloakSession session, ClientModel client, Strin
super(session);
this.session = session;
this.client = client;
Map<String, String> retrievedRequest = removeRequestObject(session, requestUri);
Map<String, String> retrievedRequest = getRequestObject(session, requestUri);
if (retrievedRequest == null) {
throw new RuntimeException("PAR not found. not issued or used multiple times.");
throw new RuntimeException("PAR not found, not issued or used multiple times.");
}

RealmModel realm = session.getContext().getRealm();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public class DPoPUtil {

public static final int DEFAULT_PROOF_LIFETIME = 10;
public static final int DEFAULT_ALLOWED_CLOCK_SKEW = 15; // sec;
public static final String DPOP_CNF_TYPE = "DPoP";
public static final String DPOP_TOKEN_TYPE = "DPoP";
public static final String DPOP_SCHEME = "DPoP";
public final static String DPOP_SESSION_ATTRIBUTE = "dpop";
Expand Down Expand Up @@ -625,13 +626,13 @@ public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel m
if (bindOnlyRefreshToken) {
return super.transformAccessToken(token, mappingModel, session, userSession, clientSessionCtx);
}
AccessToken.Confirmation confirmation = (AccessToken.Confirmation) token.getOtherClaims()
.get(OAuth2Constants.CNF);
if (confirmation == null) {
confirmation = new AccessToken.Confirmation();
token.setConfirmation(confirmation);
AccessToken.Confirmation cnf = (AccessToken.Confirmation) token.getOtherClaims().get(OAuth2Constants.CNF);
if (cnf == null) {
cnf = new AccessToken.Confirmation();
token.setConfirmation(cnf);
}
confirmation.setKeyThumbprint(dPoP.getThumbprint());
cnf.setKeyThumbprint(dPoP.getThumbprint());
cnf.setCnfType(DPOP_CNF_TYPE);
// make sure that the token-type is set to DPoP. This will be resolved if the AccessTokenResponse is built.
token.type(DPOP_TOKEN_TYPE);
return super.transformAccessToken(token, mappingModel, session, userSession, clientSessionCtx);
Expand Down
Loading
Loading
0