Implement DPoP for all grantTypes (#29967)

fixes #30179
fixes #30181


Signed-off-by: Pascal Knüppel <captain.p.goldfish@gmx.de>
Signed-off-by: Pascal Knüppel <pascal.knueppel@governikus.de>
This commit is contained in:
Pascal Knüppel 2024-07-29 16:30:54 +02:00 committed by GitHub
parent 17c01c9380
commit 94784182df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 405 additions and 171 deletions

View file

@ -152,6 +152,8 @@ public interface OAuth2Constants {
String ISSUER = "iss"; String ISSUER = "iss";
String AUTHENTICATOR_METHOD_REFERENCE = "amr"; String AUTHENTICATOR_METHOD_REFERENCE = "amr";
String CNF = "cnf";
} }

View file

@ -36,7 +36,6 @@ public class RefreshToken extends AccessToken {
/** /**
* Deep copies issuer, subject, issuedFor, sessionState from AccessToken. * Deep copies issuer, subject, issuedFor, sessionState from AccessToken.
* *
* @param token
*/ */
public RefreshToken(AccessToken token) { public RefreshToken(AccessToken token) {
this(); this();
@ -49,6 +48,25 @@ public class RefreshToken extends AccessToken {
this.scope = token.scope; this.scope = token.scope;
} }
/**
* Deep copies issuer, subject, issuedFor, sessionState from AccessToken.
*
* @param token
* @param confirmation optional confirmation parameter that might be processed during authentication but should not
* always be included in the response
*/
public RefreshToken(AccessToken token, Confirmation confirmation) {
this();
this.issuer = token.issuer;
this.subject = token.subject;
this.issuedFor = token.issuedFor;
this.sessionId = token.sessionId;
this.nonce = token.nonce;
this.audience = new String[] { token.issuer };
this.scope = token.scope;
this.confirmation = confirmation;
}
@Override @Override
public TokenCategory getCategory() { public TokenCategory getCategory() {
return TokenCategory.INTERNAL; return TokenCategory.INTERNAL;

View file

@ -33,7 +33,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.representations.dpop.DPoP;
import org.keycloak.services.cors.Cors; import org.keycloak.services.cors.Cors;
/** /**
@ -72,10 +71,9 @@ public interface OAuth2GrantType extends Provider {
protected EventBuilder event; protected EventBuilder event;
protected Cors cors; protected Cors cors;
protected Object tokenManager; protected Object tokenManager;
protected DPoP dPoP;
public Context(KeycloakSession session, Object clientConfig, Map<String, String> clientAuthAttributes, public Context(KeycloakSession session, Object clientConfig, Map<String, String> clientAuthAttributes,
MultivaluedMap<String, String> formParams, EventBuilder event, Cors cors, Object tokenManager, DPoP dPoP) { MultivaluedMap<String, String> formParams, EventBuilder event, Cors cors, Object tokenManager) {
this.session = session; this.session = session;
this.realm = session.getContext().getRealm(); this.realm = session.getContext().getRealm();
this.client = session.getContext().getClient(); this.client = session.getContext().getClient();
@ -89,7 +87,6 @@ public interface OAuth2GrantType extends Provider {
this.event = event; this.event = event;
this.cors = cors; this.cors = cors;
this.tokenManager = tokenManager; this.tokenManager = tokenManager;
this.dPoP = dPoP;
} }
public Context(Context context) { public Context(Context context) {
@ -106,7 +103,6 @@ public interface OAuth2GrantType extends Provider {
this.event = context.event; this.event = context.event;
this.cors = context.cors; this.cors = context.cors;
this.tokenManager = context.tokenManager; this.tokenManager = context.tokenManager;
this.dPoP = context.dPoP;
} }
public void setFormParams(MultivaluedHashMap<String, String> formParams) { public void setFormParams(MultivaluedHashMap<String, String> formParams) {

View file

@ -24,6 +24,7 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.services.util.DPoPUtil;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.AbstractMap; import java.util.AbstractMap;
@ -130,16 +131,20 @@ public class ProtocolMapperUtils {
public static Stream<Entry<ProtocolMapperModel, ProtocolMapper>> getSortedProtocolMappers(KeycloakSession session, ClientSessionContext ctx, Predicate<Entry<ProtocolMapperModel, ProtocolMapper>> filter) { public static Stream<Entry<ProtocolMapperModel, ProtocolMapper>> getSortedProtocolMappers(KeycloakSession session, ClientSessionContext ctx, Predicate<Entry<ProtocolMapperModel, ProtocolMapper>> filter) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
return ctx.getProtocolMappersStream()
.<Entry<ProtocolMapperModel, ProtocolMapper>>map(mapperModel -> { Stream<Entry<ProtocolMapperModel, ProtocolMapper>> protocolMapperStream = //
ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapperModel.getProtocolMapper()); ctx.getProtocolMappersStream()
if (mapper == null) { .<Entry<ProtocolMapperModel, ProtocolMapper>>map(mapperModel -> {
return null; ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapperModel.getProtocolMapper());
} if (mapper == null) {
return new AbstractMap.SimpleEntry<>(mapperModel, mapper); return null;
}) }
.filter(Objects::nonNull) return new AbstractMap.SimpleEntry<>(mapperModel, mapper);
.filter(filter) })
.filter(Objects::nonNull)
.filter(filter);
return Stream.concat(protocolMapperStream, DPoPUtil.getTransientProtocolMapper())
.sorted(Comparator.comparing(ProtocolMapperUtils::compare)); .sorted(Comparator.comparing(ProtocolMapperUtils::compare));
} }

View file

@ -113,6 +113,7 @@ public class DockerAuthV2Protocol implements LoginProtocol {
AtomicReference<DockerResponseToken> finalResponseToken = new AtomicReference<>(responseToken); AtomicReference<DockerResponseToken> finalResponseToken = new AtomicReference<>(responseToken);
ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx, mapper -> ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx, mapper ->
mapper.getValue() instanceof DockerAuthV2AttributeMapper && ((DockerAuthV2AttributeMapper) mapper.getValue()).appliesTo(finalResponseToken.get())) mapper.getValue() instanceof DockerAuthV2AttributeMapper && ((DockerAuthV2AttributeMapper) mapper.getValue()).appliesTo(finalResponseToken.get()))
.filter(mapper -> mapper instanceof DockerAuthV2AttributeMapper)
.forEach(mapper -> finalResponseToken.set(((DockerAuthV2AttributeMapper) mapper.getValue()) .forEach(mapper -> finalResponseToken.set(((DockerAuthV2AttributeMapper) mapper.getValue())
.transformDockerResponseToken(finalResponseToken.get(), mapper.getKey(), session, userSession, clientSession))); .transformDockerResponseToken(finalResponseToken.get(), mapper.getKey(), session, userSession, clientSession)));
responseToken = finalResponseToken.get(); responseToken = finalResponseToken.get();

View file

@ -46,7 +46,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.ImpersonationSessionNote;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -959,7 +958,7 @@ public class TokenManager {
ClientSessionContext clientSessionCtx, UriInfo uriInfo) { ClientSessionContext clientSessionCtx, UriInfo uriInfo) {
AccessToken token = new AccessToken(); AccessToken token = new AccessToken();
token.id(KeycloakModelUtils.generateId()); token.id(KeycloakModelUtils.generateId());
token.type(TokenUtil.TOKEN_TYPE_BEARER); token.type(formatTokenType(client, token));
if (UserSessionModel.SessionPersistenceState.TRANSIENT.equals(session.getPersistenceState())) { if (UserSessionModel.SessionPersistenceState.TRANSIENT.equals(session.getPersistenceState())) {
token.subject(user.getId()); token.subject(user.getId());
} }
@ -1061,7 +1060,7 @@ public class TokenManager {
this.session = session; this.session = session;
this.userSession = userSession; this.userSession = userSession;
this.clientSessionCtx = clientSessionCtx; this.clientSessionCtx = clientSessionCtx;
this.responseTokenType = formatTokenType(client); this.responseTokenType = formatTokenType(client, null);
} }
public AccessToken getAccessToken() { public AccessToken getAccessToken() {
@ -1078,6 +1077,7 @@ public class TokenManager {
public AccessTokenResponseBuilder accessToken(AccessToken accessToken) { public AccessTokenResponseBuilder accessToken(AccessToken accessToken) {
this.accessToken = accessToken; this.accessToken = accessToken;
this.responseTokenType = formatTokenType(client, accessToken);
return this; return this;
} }
public AccessTokenResponseBuilder refreshToken(RefreshToken refreshToken) { public AccessTokenResponseBuilder refreshToken(RefreshToken refreshToken) {
@ -1098,6 +1098,7 @@ public class TokenManager {
public AccessTokenResponseBuilder generateAccessToken() { public AccessTokenResponseBuilder generateAccessToken() {
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
accessToken = createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx); accessToken = createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
responseTokenType = formatTokenType(client, accessToken);
return this; return this;
} }
@ -1136,10 +1137,11 @@ public class TokenManager {
} }
private void generateRefreshToken(boolean offlineTokenRequested) { private void generateRefreshToken(boolean offlineTokenRequested) {
refreshToken = new RefreshToken(accessToken); AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
final AccessToken.Confirmation confirmation = getConfirmation(clientSession, accessToken);
refreshToken = new RefreshToken(accessToken, confirmation);
refreshToken.id(KeycloakModelUtils.generateId()); refreshToken.id(KeycloakModelUtils.generateId());
refreshToken.issuedNow(); refreshToken.issuedNow();
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
clientSession.setTimestamp(refreshToken.getIat().intValue()); clientSession.setTimestamp(refreshToken.getIat().intValue());
UserSessionModel userSession = clientSession.getUserSession(); UserSessionModel userSession = clientSession.getUserSession();
userSession.setLastSessionRefresh(refreshToken.getIat().intValue()); userSession.setLastSessionRefresh(refreshToken.getIat().intValue());
@ -1160,6 +1162,19 @@ public class TokenManager {
} }
} }
/**
* 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 Long getExpiration(boolean offline) { private Long getExpiration(boolean offline) {
long expiration = SessionExpirationUtils.calculateClientSessionIdleTimestamp( long expiration = SessionExpirationUtils.calculateClientSessionIdleTimestamp(
offline, userSession.isRememberMe(), offline, userSession.isRememberMe(),
@ -1314,11 +1329,13 @@ public class TokenManager {
} }
private String formatTokenType(ClientModel client) { private String formatTokenType(ClientModel client, AccessToken accessToken) {
final String tokenType = Optional.ofNullable(accessToken).map(AccessToken::getType)
.orElse(TokenUtil.TOKEN_TYPE_BEARER);
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseLowerCaseInTokenResponse()) { if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseLowerCaseInTokenResponse()) {
return TokenUtil.TOKEN_TYPE_BEARER.toLowerCase(); return tokenType.toLowerCase();
} }
return TokenUtil.TOKEN_TYPE_BEARER; return tokenType;
} }
public static class NotBeforeCheck implements TokenVerifier.Predicate<JsonWebToken> { public static class NotBeforeCheck implements TokenVerifier.Predicate<JsonWebToken> {

View file

@ -48,7 +48,6 @@ import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder; import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
import org.keycloak.protocol.saml.SamlClient; import org.keycloak.protocol.saml.SamlClient;
import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.dpop.DPoP;
import org.keycloak.saml.common.constants.JBossSAMLConstants; import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ConfigurationException;
@ -56,6 +55,7 @@ import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil; import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.cors.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.services.util.DPoPUtil;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
@ -73,7 +73,6 @@ public class TokenEndpoint {
private ClientModel client; private ClientModel client;
private Map<String, String> clientAuthAttributes; private Map<String, String> clientAuthAttributes;
private OIDCAdvancedConfigWrapper clientConfig; private OIDCAdvancedConfigWrapper clientConfig;
private DPoP dPoP;
private final KeycloakSession session; private final KeycloakSession session;
@ -136,7 +135,19 @@ public class TokenEndpoint {
checkParameterDuplicated(); checkParameterDuplicated();
} }
OAuth2GrantType.Context context = new OAuth2GrantType.Context(session, clientConfig, clientAuthAttributes, formParams, event, cors, tokenManager, dPoP); /*
* To request an access token that is bound to a public key using DPoP, the client MUST provide a valid DPoP
* proof JWT in a DPoP header when making an access token request to the authorization server's token endpoint.
* This is applicable for all access token requests regardless of grant type (e.g., the common
* authorization_code and refresh_token grant types and extension grants such as the JWT
* authorization grant [RFC7523])
*/
DPoPUtil.retrieveDPoPHeaderIfPresent(session, clientConfig, event, cors).ifPresent(dPoP -> {
session.setAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, dPoP);
});
OAuth2GrantType.Context context = new OAuth2GrantType.Context(session, clientConfig, clientAuthAttributes,
formParams, event, cors, tokenManager);
return grant.process(context); return grant.process(context);
} }

View file

@ -26,7 +26,6 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.common.Profile;
import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
@ -34,7 +33,6 @@ import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -63,8 +61,6 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase {
public Response process(Context context) { public Response process(Context context) {
setContext(context); setContext(context);
checkAndRetrieveDPoPProof(Profile.isFeatureEnabled(Profile.Feature.DPOP));
String code = formParams.getFirst(OAuth2Constants.CODE); String code = formParams.getFirst(OAuth2Constants.CODE);
if (code == null) { if (code == null) {
String errorMessage = "Missing parameter: " + OAuth2Constants.CODE; String errorMessage = "Missing parameter: " + OAuth2Constants.CODE;

View file

@ -24,12 +24,7 @@ import jakarta.ws.rs.core.Response;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
@ -37,7 +32,6 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.VerificationException;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
@ -46,7 +40,6 @@ import org.keycloak.http.HttpRequest;
import org.keycloak.http.HttpResponse; import org.keycloak.http.HttpResponse;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -58,14 +51,12 @@ import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.rar.AuthorizationRequestContext; import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.dpop.DPoP;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.clientpolicy.ClientPolicyContext; import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.cors.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.services.util.AuthorizationContextUtil; import org.keycloak.services.util.AuthorizationContextUtil;
import org.keycloak.services.util.DPoPUtil;
import org.keycloak.services.util.MtlsHoKTokenUtil; import org.keycloak.services.util.MtlsHoKTokenUtil;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
@ -90,7 +81,6 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
protected EventBuilder event; protected EventBuilder event;
protected Cors cors; protected Cors cors;
protected TokenManager tokenManager; protected TokenManager tokenManager;
protected DPoP dPoP;
protected HttpRequest request; protected HttpRequest request;
protected HttpResponse response; protected HttpResponse response;
protected HttpHeaders headers; protected HttpHeaders headers;
@ -110,7 +100,6 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
this.event = context.event; this.event = context.event;
this.cors = context.cors; this.cors = context.cors;
this.tokenManager = (TokenManager) context.tokenManager; this.tokenManager = (TokenManager) context.tokenManager;
this.dPoP = context.dPoP;
} }
protected Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx, protected Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
@ -125,7 +114,6 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
} }
checkAndBindMtlsHoKToken(responseBuilder, useRefreshToken); checkAndBindMtlsHoKToken(responseBuilder, useRefreshToken);
checkAndBindDPoPToken(responseBuilder, useRefreshToken && client.isPublicClient(), Profile.isFeatureEnabled(Profile.Feature.DPOP));
if (TokenUtil.isOIDCRequest(scopeParam)) { if (TokenUtil.isOIDCRequest(scopeParam)) {
responseBuilder.generateIDToken().generateAccessTokenHash(); responseBuilder.generateIDToken().generateAccessTokenHash();
@ -182,21 +170,6 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
} }
} }
protected void checkAndBindDPoPToken(TokenManager.AccessTokenResponseBuilder responseBuilder, boolean useRefreshToken, boolean isDPoPSupported) {
if (!isDPoPSupported) return;
if (clientConfig.isUseDPoP() || dPoP != null) {
DPoPUtil.bindToken(responseBuilder.getAccessToken(), dPoP);
responseBuilder.getAccessToken().type(DPoPUtil.DPOP_TOKEN_TYPE);
responseBuilder.responseTokenType(DPoPUtil.DPOP_TOKEN_TYPE);
// Bind refresh tokens for public clients, See "Section 5. DPoP Access Token Request" from DPoP specification
if (useRefreshToken) {
DPoPUtil.bindToken(responseBuilder.getRefreshToken(), dPoP);
}
}
}
protected void updateClientSession(AuthenticatedClientSessionModel clientSession) { protected void updateClientSession(AuthenticatedClientSessionModel clientSession) {
if(clientSession == null) { if(clientSession == null) {
@ -227,21 +200,6 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
} }
} }
protected void checkAndRetrieveDPoPProof(boolean isDPoPSupported) {
if (!isDPoPSupported) return;
if (clientConfig.isUseDPoP() || request.getHttpHeaders().getHeaderString(DPoPUtil.DPOP_HTTP_HEADER) != null) {
try {
dPoP = new DPoPUtil.Validator(session).request(request).uriInfo(session.getContext().getUri()).validate();
session.setAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, dPoP);
} catch (VerificationException ex) {
event.detail(Details.REASON, ex.getMessage());
event.error(Errors.INVALID_DPOP_PROOF);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_DPOP_PROOF, ex.getMessage(), Response.Status.BAD_REQUEST);
}
}
}
protected String getRequestedScopes() { protected String getRequestedScopes() {
String scope = formParams.getFirst(OAuth2Constants.SCOPE); String scope = formParams.getFirst(OAuth2Constants.SCOPE);

View file

@ -52,8 +52,6 @@ public class RefreshTokenGrantType extends OAuth2GrantTypeBase {
public Response process(Context context) { public Response process(Context context) {
setContext(context); setContext(context);
checkAndRetrieveDPoPProof(Profile.isFeatureEnabled(Profile.Feature.DPOP));
String refreshToken = formParams.getFirst(OAuth2Constants.REFRESH_TOKEN); String refreshToken = formParams.getFirst(OAuth2Constants.REFRESH_TOKEN);
if (refreshToken == null) { if (refreshToken == null) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST); throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
@ -76,7 +74,6 @@ public class RefreshTokenGrantType extends OAuth2GrantTypeBase {
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.refreshAccessToken(session, session.getContext().getUri(), clientConnection, realm, client, refreshToken, event, headers, request, scopeParameter); TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.refreshAccessToken(session, session.getContext().getUri(), clientConnection, realm, client, refreshToken, event, headers, request, scopeParameter);
checkAndBindMtlsHoKToken(responseBuilder, clientConfig.isUseRefreshToken()); checkAndBindMtlsHoKToken(responseBuilder, clientConfig.isUseRefreshToken());
checkAndBindDPoPToken(responseBuilder, clientConfig.isUseRefreshToken() && (client.isPublicClient() || client.isBearerOnly()), Profile.isFeatureEnabled(Profile.Feature.DPOP));
session.clientPolicy().triggerOnEvent(new TokenRefreshResponseContext(formParams, responseBuilder)); session.clientPolicy().triggerOnEvent(new TokenRefreshResponseContext(formParams, responseBuilder));

View file

@ -26,6 +26,8 @@ import org.keycloak.models.RealmModel;
import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import org.keycloak.util.TokenUtil;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -65,7 +67,7 @@ public class AppAuthManager extends AuthenticationManager {
} }
String bearerPart = split[0]; String bearerPart = split[0];
if (!bearerPart.equalsIgnoreCase(BEARER)){ if (!bearerPart.equalsIgnoreCase(BEARER) && !bearerPart.equalsIgnoreCase(TokenUtil.TOKEN_TYPE_DPOP)){
return null; return null;
} }

View file

@ -19,33 +19,61 @@ package org.keycloak.services.util;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.binary.Hex;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenVerifier; import org.keycloak.TokenVerifier;
import org.keycloak.common.Profile;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureProvider; import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.exceptions.TokenVerificationException; import org.keycloak.exceptions.TokenVerificationException;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.crypto.HashUtils; import org.keycloak.jose.jws.crypto.HashUtils;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.SingleUseObjectProvider; import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenResponseMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
import org.keycloak.protocol.oidc.mappers.TokenIntrospectionTokenMapper;
import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.dpop.DPoP; import org.keycloak.representations.dpop.DPoP;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.cors.Cors;
import org.keycloak.util.JWKSUtils; import org.keycloak.util.JWKSUtils;
import static org.keycloak.utils.StringUtil.isNotBlank; import static org.keycloak.utils.StringUtil.isNotBlank;
@ -60,8 +88,6 @@ public class DPoPUtil {
public static final String DPOP_TOKEN_TYPE = "DPoP"; public static final String DPOP_TOKEN_TYPE = "DPoP";
public static final String DPOP_SCHEME = "DPoP"; public static final String DPOP_SCHEME = "DPoP";
public final static String DPOP_SESSION_ATTRIBUTE = "dpop"; public final static String DPOP_SESSION_ATTRIBUTE = "dpop";
public final static String DPOP_PARAM = "dpop";
public final static String DPOP_THUMBPRINT_NOTE = "dpop.thumbprint";
public static enum Mode { public static enum Mode {
ENABLED, ENABLED,
@ -89,6 +115,62 @@ public class DPoPUtil {
return UriBuilder.fromUri(uri).replaceQuery("").build(); return UriBuilder.fromUri(uri).replaceQuery("").build();
} }
/**
* creates a protocol mapper that cannot be modified by administration users and that is used to bind AccessTokens
* to specific DPoP keys. <br />
* <br />
* NOTE: The binding was solved with a protocol mapper to have generic solution for DPoP on all implemented
* grantTypes, even custom-implemented grantTypes.
*/
public static Stream<Map.Entry<ProtocolMapperModel, ProtocolMapper>> getTransientProtocolMapper() {
final String PROVIDER_ID = DPOP_SCHEME.toLowerCase(Locale.ROOT) + "-protocol-mapper";
ProtocolMapperModel protocolMapperModel = new ProtocolMapperModel();
protocolMapperModel.setId(DPOP_SCHEME);
protocolMapperModel.setName(DPOP_SCHEME);
protocolMapperModel.setProtocolMapper(PROVIDER_ID);
protocolMapperModel.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
Map<String, String> config = new HashMap<>();
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "false");
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "false");
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION, "false");
protocolMapperModel.setConfig(config);
ProtocolMapper dpopProtocolMapper = new DpopProtocolMapper(PROVIDER_ID);
return Stream.of(Map.entry(protocolMapperModel, dpopProtocolMapper));
}
/**
* checks the current request if a DPoP HTTP Header is present and returns it if it is present.
*/
public static Optional<DPoP> retrieveDPoPHeaderIfPresent(KeycloakSession keycloakSession,
OIDCAdvancedConfigWrapper clientConfig,
EventBuilder event,
Cors cors) {
boolean isDPoPSupported = Profile.isFeatureEnabled(Profile.Feature.DPOP);
if (!isDPoPSupported) {
return Optional.empty();
}
HttpRequest request = keycloakSession.getContext().getHttpRequest();
final boolean isClientRequiresDpop = clientConfig.isUseDPoP();
final boolean isDpopHeaderPresent = request.getHttpHeaders().getHeaderString(DPoPUtil.DPOP_HTTP_HEADER) != null;
if (!isClientRequiresDpop && !isDpopHeaderPresent) {
return Optional.empty();
}
try {
DPoP dPoP = new DPoPUtil.Validator(keycloakSession).request(request).uriInfo(keycloakSession.getContext().getUri()).validate();
keycloakSession.setAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, dPoP);
return Optional.of(dPoP);
} catch (VerificationException ex) {
event.detail(Details.REASON, ex.getMessage());
event.error(Errors.INVALID_DPOP_PROOF);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_DPOP_PROOF, ex.getMessage(), Response.Status.BAD_REQUEST);
}
}
private static DPoP validateDPoP(KeycloakSession session, URI uri, String method, String token, String accessToken, int lifetime, int clockSkew) throws VerificationException { private static DPoP validateDPoP(KeycloakSession session, URI uri, String method, String token, String accessToken, int lifetime, int clockSkew) throws VerificationException {
if (token == null || token.trim().equals("")) { if (token == null || token.trim().equals("")) {
@ -150,7 +232,7 @@ public class DPoPUtil {
} catch (DPoPVerificationException ex) { } catch (DPoPVerificationException ex) {
throw ex; throw ex;
} catch (VerificationException ex) { } catch (VerificationException ex) {
throw new VerificationException("DPoP verification failure", ex); throw new VerificationException("DPoP verification failure: " + ex.getMessage(), ex);
} }
} }
@ -387,4 +469,67 @@ public class DPoPUtil {
} }
/**
* a custom protocol mapper that is not meant for configuration in the Admin-UI. This mapper is created on the
* fly for TokenRequests to bind the created generated AccessTokens to the key of the DPoP HTTP Header.
*/
private static final class DpopProtocolMapper extends AbstractOIDCProtocolMapper
implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper, TokenIntrospectionTokenMapper,
OIDCAccessTokenResponseMapper {
private final String providerId;
public DpopProtocolMapper(String providerId) {
this.providerId = providerId;
}
@Override
public String getDisplayCategory() {
return TOKEN_MAPPER_CATEGORY;
}
@Override
public String getDisplayType() {
return DPOP_SCHEME;
}
@Override
public String getHelpText() {
return "not needed";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of(new ProviderConfigProperty("multivalued", ProtocolMapperUtils.MULTIVALUED, "",
ProviderConfigProperty.BOOLEAN_TYPE, false));
}
@Override
public String getId() {
return providerId;
}
@Override
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
boolean isDPoPSupported = Profile.isFeatureEnabled(Profile.Feature.DPOP);
if (!isDPoPSupported) {
return super.transformAccessToken(token, mappingModel, session, userSession, clientSessionCtx);
}
DPoP dPoP = session.getAttribute(DPOP_SESSION_ATTRIBUTE, DPoP.class);
if (dPoP == null) {
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);
}
confirmation.setKeyThumbprint(dPoP.getThumbprint());
// 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);
}
}
} }

View file

@ -83,6 +83,8 @@ import org.keycloak.utils.MediaType;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -116,6 +118,9 @@ import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/ */
public class OAuthClient { public class OAuthClient {
private static final Logger logger = LoggerFactory.getLogger(OAuthClient.class);
public static String SERVER_ROOT; public static String SERVER_ROOT;
public static String AUTH_SERVER_ROOT; public static String AUTH_SERVER_ROOT;
public static String APP_ROOT; public static String APP_ROOT;
@ -384,7 +389,7 @@ public class OAuthClient {
} }
driver.findElement(By.name("login")).click(); driver.findElement(By.name("login")).click();
} catch (Throwable t) { } catch (Throwable t) {
System.err.println(src); logger.error("Unexpected page was loaded\n{}", src);
throw t; throw t;
} }
} }
@ -499,7 +504,7 @@ public class OAuthClient {
} }
if (dpopProof != null) { if (dpopProof != null) {
post.addHeader("DPoP", dpopProof); post.addHeader(TokenUtil.TOKEN_TYPE_DPOP, dpopProof);
} }
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8); UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8);
@ -592,6 +597,9 @@ public class OAuthClient {
post.addHeader(header.getKey(), header.getValue()); post.addHeader(header.getKey(), header.getValue());
} }
} }
if (dpopProof != null) {
post.addHeader(TokenUtil.TOKEN_TYPE_DPOP, dpopProof);
}
List<NameValuePair> parameters = new LinkedList<>(); List<NameValuePair> parameters = new LinkedList<>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
@ -748,6 +756,9 @@ public class OAuthClient {
String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret); String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization); post.setHeader("Authorization", authorization);
} }
if (dpopProof != null) {
post.addHeader(TokenUtil.TOKEN_TYPE_DPOP, dpopProof);
}
List<NameValuePair> parameters = new LinkedList<>(); List<NameValuePair> parameters = new LinkedList<>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@ -979,7 +990,7 @@ public class OAuthClient {
} }
if (dpopProof != null) { if (dpopProof != null) {
post.addHeader("DPoP", dpopProof); post.addHeader(TokenUtil.TOKEN_TYPE_DPOP, dpopProof);
} }
UrlEncodedFormEntity formEntity; UrlEncodedFormEntity formEntity;
@ -1033,7 +1044,7 @@ public class OAuthClient {
} }
if (dpopProof != null) { if (dpopProof != null) {
post.addHeader("DPoP", dpopProof); post.addHeader(TokenUtil.TOKEN_TYPE_DPOP, dpopProof);
} }
UrlEncodedFormEntity formEntity; UrlEncodedFormEntity formEntity;
@ -1154,12 +1165,12 @@ public class OAuthClient {
} }
} }
public UserInfoResponse doUserInfoRequestByGet(String accessToken) throws Exception { public UserInfoResponse doUserInfoRequestByGet(OAuthClient.AccessTokenResponse accessTokenResponse) throws Exception {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) { try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
HttpGet get = new HttpGet(getUserInfoUrl()); HttpGet get = new HttpGet(getUserInfoUrl());
get.setHeader("Authorization", "Bearer " + accessToken); get.setHeader("Authorization", accessTokenResponse.getTokenType() + " " + accessTokenResponse.getAccessToken());
if (dpopProof != null) { if (dpopProof != null) {
get.addHeader("DPoP", dpopProof); get.addHeader(TokenUtil.TOKEN_TYPE_DPOP, dpopProof);
} }
return new UserInfoResponse(client.execute(get)); return new UserInfoResponse(client.execute(get));
} catch (IOException ex) { } catch (IOException ex) {

View file

@ -551,7 +551,7 @@ public final class KcOidcBrokerTransientSessionsTest extends AbstractAdvancedBro
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, CONSUMER_BROKER_APP_SECRET); OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, CONSUMER_BROKER_APP_SECRET);
// Check that userInfo can be invoked // Check that userInfo can be invoked
var userInfoResponse = oauth.doUserInfoRequestByGet(tokenResponse.getAccessToken()); var userInfoResponse = oauth.doUserInfoRequestByGet(tokenResponse);
assertThat(userInfoResponse.getUserInfo().getSub(), is(lwUserId)); assertThat(userInfoResponse.getUserInfo().getSub(), is(lwUserId));
assertThat(userInfoResponse.getUserInfo().getPreferredUsername(), is(bc.getUserLogin())); assertThat(userInfoResponse.getUserInfo().getPreferredUsername(), is(bc.getUserLogin()));
assertThat(userInfoResponse.getUserInfo().getEmail(), is(bc.getUserEmail())); assertThat(userInfoResponse.getUserInfo().getEmail(), is(bc.getUserEmail()));
@ -713,7 +713,7 @@ public final class KcOidcBrokerTransientSessionsTest extends AbstractAdvancedBro
userAttrMapper.setName(USER_ATTRIBUTE_NAME); userAttrMapper.setName(USER_ATTRIBUTE_NAME);
userAttrMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); userAttrMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
userAttrMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID); userAttrMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
Map<String, String> userAttrMapperConfig = userAttrMapper.getConfig(); Map<String, String> userAttrMapperConfig = userAttrMapper.getConfig();
userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, USER_ATTRIBUTE_NAME); userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, USER_ATTRIBUTE_NAME);
userAttrMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, USER_ATTRIBUTE_NAME); userAttrMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, USER_ATTRIBUTE_NAME);
@ -728,7 +728,7 @@ public final class KcOidcBrokerTransientSessionsTest extends AbstractAdvancedBro
client.setProtocolMappers(mappers); client.setProtocolMappers(mappers);
return clients; return clients;
} }
@Override @Override
public List<ClientRepresentation> createConsumerClients() { public List<ClientRepresentation> createConsumerClients() {

View file

@ -26,7 +26,6 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.util.SecretGenerator; import org.keycloak.common.util.SecretGenerator;
@ -41,13 +40,12 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.representations.oidc.TokenMetadataRepresentation; import org.keycloak.representations.oidc.TokenMetadataRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory; import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.util.ClientPoliciesUtil; import org.keycloak.testsuite.util.ClientPoliciesUtil;
import org.keycloak.testsuite.util.Matchers; import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -76,7 +74,7 @@ public class OAuth2_1PublicClientTest extends AbstractFAPITest {
private JWK jwkEc; private JWK jwkEc;
private String validRedirectUri;; private String validRedirectUri;
@Before @Before
public void setupValidateRedirectUri() { public void setupValidateRedirectUri() {
@ -157,7 +155,7 @@ public class OAuth2_1PublicClientTest extends AbstractFAPITest {
public void testOAuth2_1ProofKeyForCodeExchange() throws Exception { public void testOAuth2_1ProofKeyForCodeExchange() throws Exception {
String clientId = generateSuffixedName(CLIENT_NAME); String clientId = generateSuffixedName(CLIENT_NAME);
String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) ->
setupValidClientExceptForRedirectUri(clientRep, OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep)) setupValidClientExceptForRedirectUri(clientRep, OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep))
); );
assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(getClientByAdmin(cId)).getPkceCodeChallengeMethod()); assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(getClientByAdmin(cId)).getPkceCodeChallengeMethod());
@ -212,7 +210,7 @@ public class OAuth2_1PublicClientTest extends AbstractFAPITest {
// registration (auto-config) - success // registration (auto-config) - success
String clientId = generateSuffixedName(CLIENT_NAME); String clientId = generateSuffixedName(CLIENT_NAME);
String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) ->
setupValidClientExceptForRedirectUri(clientRep, OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep)) setupValidClientExceptForRedirectUri(clientRep, OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep))
); );
assertTrue(OIDCAdvancedConfigWrapper.fromClientRepresentation(getClientByAdmin(cId)).isUseDPoP()); assertTrue(OIDCAdvancedConfigWrapper.fromClientRepresentation(getClientByAdmin(cId)).isUseDPoP());
@ -243,7 +241,7 @@ public class OAuth2_1PublicClientTest extends AbstractFAPITest {
// userinfo request with DPoP Proof - success // userinfo request with DPoP Proof - success
dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET, oauth.getUserInfoUrl(), (long) Time.currentTime(), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate()); dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET, oauth.getUserInfoUrl(), (long) Time.currentTime(), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate());
oauth.dpopProof(dpopProofEcEncoded); oauth.dpopProof(dpopProofEcEncoded);
OAuthClient.UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response.getAccessToken()); OAuthClient.UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername()); assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername());
oauth.idTokenHint(response.getIdToken()).openLogout(); oauth.idTokenHint(response.getIdToken()).openLogout();
@ -271,7 +269,7 @@ public class OAuth2_1PublicClientTest extends AbstractFAPITest {
updatePolicies(json); updatePolicies(json);
} }
private void setupValidClientExceptForRedirectUri(ClientRepresentation clientRep, OIDCAdvancedConfigWrapper clientConfig ) { private void setupValidClientExceptForRedirectUri(ClientRepresentation clientRep, OIDCAdvancedConfigWrapper clientConfig) {
clientRep.setPublicClient(Boolean.TRUE); clientRep.setPublicClient(Boolean.TRUE);
clientRep.setRedirectUris(Collections.singletonList(validRedirectUri)); clientRep.setRedirectUris(Collections.singletonList(validRedirectUri));
clientRep.setImplicitFlowEnabled(false); clientRep.setImplicitFlowEnabled(false);
@ -279,7 +277,7 @@ public class OAuth2_1PublicClientTest extends AbstractFAPITest {
clientConfig.setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri())); clientConfig.setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
clientConfig.setPkceCodeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); clientConfig.setPkceCodeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
clientConfig.setUseDPoP(true); clientConfig.setUseDPoP(true);
}; }
private void testProhibitedImplicitOrHybridFlow(boolean isOpenid, String responseType, String nonce) { private void testProhibitedImplicitOrHybridFlow(boolean isOpenid, String responseType, String nonce) {
oauth.openid(isOpenid); oauth.openid(isOpenid);
@ -303,4 +301,4 @@ public class OAuth2_1PublicClientTest extends AbstractFAPITest {
private String generateNonce() { private String generateNonce() {
return SecretGenerator.getInstance().randomString(16); return SecretGenerator.getInstance().randomString(16);
} }
} }

View file

@ -17,32 +17,10 @@
package org.keycloak.testsuite.oauth; package org.keycloak.testsuite.oauth;
import static org.hamcrest.MatcherAssert.assertThat; import jakarta.ws.rs.BadRequestException;
import static org.hamcrest.Matchers.emptyOrNullString; import jakarta.ws.rs.HttpMethod;
import static org.hamcrest.Matchers.is; import jakarta.ws.rs.core.Response;
import static org.junit.Assert.assertEquals; import jakarta.ws.rs.core.Response.Status;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientAccessTypeConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createDPoPBindEnforcerExecutorConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createEcJwk;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createRsaJwk;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.generateEcdsaKey;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.generateSignedDPoPProof;
import java.io.IOException;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@ -83,22 +61,44 @@ import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.OAuthClient.UserInfoResponse;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder; import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder; import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder; import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder; import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.OAuthClient.UserInfoResponse;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.util.JWKSUtils; import org.keycloak.util.JWKSUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
import jakarta.ws.rs.core.Response; import java.io.IOException;
import jakarta.ws.rs.core.Response.Status; import java.security.KeyPair;
import jakarta.ws.rs.BadRequestException; import java.util.Arrays;
import jakarta.ws.rs.HttpMethod; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientAccessTypeConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createDPoPBindEnforcerExecutorConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createEcJwk;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createRsaJwk;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.generateEcdsaKey;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.generateSignedDPoPProof;
@EnableFeature(value = Profile.Feature.DPOP, skipRestart = true) @EnableFeature(value = Profile.Feature.DPOP, skipRestart = true)
public class DPoPTest extends AbstractTestRealmKeycloakTest { public class DPoPTest extends AbstractTestRealmKeycloakTest {
@ -110,19 +110,16 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
private static final String TEST_USER_NAME = "test-user@localhost"; private static final String TEST_USER_NAME = "test-user@localhost";
private static final String TEST_USER_PASSWORD = "password"; private static final String TEST_USER_PASSWORD = "password";
private static final String DPOP_JWT_HEADER_TYPE = "dpop+jwt"; private static final String DPOP_JWT_HEADER_TYPE = "dpop+jwt";
@Rule
public AssertEvents events = new AssertEvents(this);
private KeyPair ecKeyPair; private KeyPair ecKeyPair;
private KeyPair rsaKeyPair; private KeyPair rsaKeyPair;
private JWK jwkRsa; private JWK jwkRsa;
private JWK jwkEc; private JWK jwkEc;
private JWSHeader jwsRsaHeader; private JWSHeader jwsRsaHeader;
private JWSHeader jwsEcHeader; private JWSHeader jwsEcHeader;
private ClientRegistration reg; private ClientRegistration reg;
@Rule
public AssertEvents events = new AssertEvents(this);
@Before @Before
public void beforeDPoPTest() throws Exception { public void beforeDPoPTest() throws Exception {
ecKeyPair = generateEcdsaKey("secp256r1"); ecKeyPair = generateEcdsaKey("secp256r1");
@ -155,12 +152,13 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.dpopProof(dpopProofEcEncoded); oauth.dpopProof(dpopProofEcEncoded);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
assertEquals(Status.OK.getStatusCode(), response.getStatusCode()); assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK)jwkEc).getCrv()); jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK) jwkEc).getCrv());
jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK)jwkEc).getX()); jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK) jwkEc).getX());
jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK)jwkEc).getY()); jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK) jwkEc).getY());
String jkt = JWKSUtils.computeThumbprint(jwkEc); String jkt = JWKSUtils.computeThumbprint(jwkEc);
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint()); assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken()); RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
@ -171,6 +169,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
oauth.dpopProof(dpopProofEcEncoded); oauth.dpopProof(dpopProofEcEncoded);
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null); response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null);
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
assertEquals(Status.OK.getStatusCode(), response.getStatusCode()); assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
accessToken = oauth.verifyToken(response.getAccessToken()); accessToken = oauth.verifyToken(response.getAccessToken());
@ -181,9 +180,8 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
// userinfo access // userinfo access
dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate()); dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate());
oauth.dpopProof(dpopProofEcEncoded); oauth.dpopProof(dpopProofEcEncoded);
UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response.getAccessToken()); UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername()); assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername());
oauth.idTokenHint(response.getIdToken()).openLogout(); oauth.idTokenHint(response.getIdToken()).openLogout();
} }
@ -202,11 +200,12 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.dpopProof(dpopProofRsaEncoded); oauth.dpopProof(dpopProofRsaEncoded);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, TEST_CONFIDENTIAL_CLIENT_SECRET); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, TEST_CONFIDENTIAL_CLIENT_SECRET);
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
assertEquals(Status.OK.getStatusCode(), response.getStatusCode()); assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK)jwkRsa).getModulus()); jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK) jwkRsa).getModulus());
jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK)jwkRsa).getPublicExponent()); jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK) jwkRsa).getPublicExponent());
String jkt = JWKSUtils.computeThumbprint(jwkRsa); String jkt = JWKSUtils.computeThumbprint(jwkRsa);
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint()); assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken()); RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
@ -235,10 +234,11 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
oauth.dpopProof(dpopProofRsaEncoded); oauth.dpopProof(dpopProofRsaEncoded);
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET); response = oauth.doRefreshTokenRequest(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET);
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
assertEquals(Status.OK.getStatusCode(), response.getStatusCode()); assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK)jwkRsa).getModulus()); jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK) jwkRsa).getModulus());
jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK)jwkRsa).getPublicExponent()); jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK) jwkRsa).getPublicExponent());
jkt = JWKSUtils.computeThumbprint(jwkRsa); jkt = JWKSUtils.computeThumbprint(jwkRsa);
accessToken = oauth.verifyToken(response.getAccessToken()); accessToken = oauth.verifyToken(response.getAccessToken());
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint()); assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
@ -263,6 +263,8 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
// token-type must be "Bearer" because no DPoP is present within the token-request
assertEquals(TokenUtil.TOKEN_TYPE_BEARER, response.getTokenType());
assertEquals(Status.OK.getStatusCode(), response.getStatusCode()); assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
@ -272,6 +274,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
// token refresh // token refresh
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null); response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null);
assertEquals(TokenUtil.TOKEN_TYPE_BEARER, response.getTokenType());
assertEquals(Status.OK.getStatusCode(), response.getStatusCode()); assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
accessToken = oauth.verifyToken(response.getAccessToken()); accessToken = oauth.verifyToken(response.getAccessToken());
@ -296,9 +299,11 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.dpopProof(dpopProofEcEncoded); oauth.dpopProof(dpopProofEcEncoded);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
// token refresh // token refresh
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null); response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null);
assertNull(response.getTokenType());
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusCode()); assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_DPOP_PROOF, response.getError()); assertEquals(OAuthErrorException.INVALID_DPOP_PROOF, response.getError());
assertEquals("DPoP proof has already been used", response.getErrorDescription()); assertEquals("DPoP proof has already been used", response.getErrorDescription());
@ -317,10 +322,12 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.dpopProof(dpopProofRsaEncoded); oauth.dpopProof(dpopProofRsaEncoded);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, TEST_USER_PASSWORD); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, TEST_USER_PASSWORD);
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
// token refresh // token refresh
oauth.dpopProof(null); oauth.dpopProof(null);
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET); response = oauth.doRefreshTokenRequest(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET);
assertNull(response.getTokenType());
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusCode()); assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_DPOP_PROOF, response.getError()); assertEquals(OAuthErrorException.INVALID_DPOP_PROOF, response.getError());
assertEquals("DPoP proof is missing", response.getErrorDescription()); assertEquals("DPoP proof is missing", response.getErrorDescription());
@ -340,7 +347,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void testDPoPProofWithoutJwk() throws Exception { public void testDPoPProofWithoutJwk() throws Exception {
JWSHeader jwsHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.ES256, DPOP_JWT_HEADER_TYPE, jwkEc.getKeyId(), (JWK)null); JWSHeader jwsHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.ES256, DPOP_JWT_HEADER_TYPE, jwkEc.getKeyId(), (JWK) null);
testDPoPProofFailure(generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsHeader, ecKeyPair.getPrivate()), "No JWK in DPoP header"); testDPoPProofFailure(generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsHeader, ecKeyPair.getPrivate()), "No JWK in DPoP header");
} }
@ -358,7 +365,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void testDPoPProofInvalidSignature() throws Exception { public void testDPoPProofInvalidSignature() throws Exception {
testDPoPProofFailure(generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsEcHeader, rsaKeyPair.getPrivate()), "DPoP verification failure"); testDPoPProofFailure(generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsEcHeader, rsaKeyPair.getPrivate()), "DPoP verification failure: org.keycloak.exceptions.TokenSignatureInvalidException: Invalid token signature");
} }
@Test @Test
@ -376,6 +383,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, TEST_USER_PASSWORD); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, TEST_USER_PASSWORD);
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
oauth.doLogout(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET); oauth.doLogout(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET);
testDPoPProofFailure(dpopProofEcEncoded, "DPoP proof has already been used"); testDPoPProofFailure(dpopProofEcEncoded, "DPoP proof has already been used");
@ -417,7 +425,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
public void testDPoPProofOnUserInfoByConfidentialClient() throws Exception { public void testDPoPProofOnUserInfoByConfidentialClient() throws Exception {
KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048); KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
OAuthClient.AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair); OAuthClient.AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair);
doSuccessfulUserInfoGet(response.getAccessToken(), rsaKeyPair); doSuccessfulUserInfoGet(response, rsaKeyPair);
oauth.doLogout(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET); oauth.doLogout(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET);
} }
@ -429,11 +437,11 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
try { try {
KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048); KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
OAuthClient.AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair); OAuthClient.AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair);
doSuccessfulUserInfoGet(response.getAccessToken(), rsaKeyPair); doSuccessfulUserInfoGet(response, rsaKeyPair);
// delete DPoP proof // delete DPoP proof
oauth.dpopProof(null); oauth.dpopProof(null);
UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response.getAccessToken()); UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(401, userInfoResponse.getStatusCode()); assertEquals(401, userInfoResponse.getStatusCode());
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate")); assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
@ -449,7 +457,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
OAuthClient.AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair); OAuthClient.AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair);
oauth.dpopProof(null); oauth.dpopProof(null);
UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response.getAccessToken()); UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(401, userInfoResponse.getStatusCode()); assertEquals(401, userInfoResponse.getStatusCode());
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate")); assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
@ -466,7 +474,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
// invalid "htu" claim // invalid "htu" claim
String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate()); String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate());
oauth.dpopProof(dpopProofRsaEncoded); oauth.dpopProof(dpopProofRsaEncoded);
UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response.getAccessToken()); UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(401, userInfoResponse.getStatusCode()); assertEquals(401, userInfoResponse.getStatusCode());
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate")); assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
@ -477,10 +485,10 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
public void testMultipleUseDPoPProofOnUserInfo() throws Exception { public void testMultipleUseDPoPProofOnUserInfo() throws Exception {
KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048); KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
OAuthClient.AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair); OAuthClient.AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair);
doSuccessfulUserInfoGet(response.getAccessToken(), rsaKeyPair); doSuccessfulUserInfoGet(response, rsaKeyPair);
// use the same DPoP proof // use the same DPoP proof
UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response.getAccessToken()); UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(401, userInfoResponse.getStatusCode()); assertEquals(401, userInfoResponse.getStatusCode());
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate")); assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
@ -498,7 +506,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
JWSHeader jwsRsaHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.PS256, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa); JWSHeader jwsRsaHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.PS256, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa);
String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate()); String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate());
oauth.dpopProof(dpopProofRsaEncoded); oauth.dpopProof(dpopProofRsaEncoded);
UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response.getAccessToken()); UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(401, userInfoResponse.getStatusCode()); assertEquals(401, userInfoResponse.getStatusCode());
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate")); assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"DPoP proof and token binding verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
@ -603,9 +611,9 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String encodedRefreshToken = response.getRefreshToken(); String encodedRefreshToken = response.getRefreshToken();
String encodedIdToken = response.getIdToken(); String encodedIdToken = response.getIdToken();
AccessToken accessToken = oauth.verifyToken(encodedAccessToken); AccessToken accessToken = oauth.verifyToken(encodedAccessToken);
jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK)jwkEc).getCrv()); jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK) jwkEc).getCrv());
jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK)jwkEc).getX()); jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK) jwkEc).getX());
jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK)jwkEc).getY()); jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK) jwkEc).getY());
String jkt = JWKSUtils.computeThumbprint(jwkEc); String jkt = JWKSUtils.computeThumbprint(jwkEc);
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint()); assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
RefreshToken refreshToken = oauth.parseRefreshToken(encodedRefreshToken); RefreshToken refreshToken = oauth.parseRefreshToken(encodedRefreshToken);
@ -613,14 +621,14 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
// userinfo request without a DPoP proof - fail // userinfo request without a DPoP proof - fail
oauth.dpopProof(null); oauth.dpopProof(null);
OAuthClient.UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(encodedAccessToken); OAuthClient.UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(401, userInfoResponse.getStatusCode()); assertEquals(401, userInfoResponse.getStatusCode());
// userinfo request with a valid DPoP proof - success // userinfo request with a valid DPoP proof - success
jwsEcHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.ES256, DPOP_JWT_HEADER_TYPE, jwkEc.getKeyId(), jwkEc); jwsEcHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.ES256, DPOP_JWT_HEADER_TYPE, jwkEc.getKeyId(), jwkEc);
dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate()); dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate());
oauth.dpopProof(dpopProofEcEncoded); oauth.dpopProof(dpopProofEcEncoded);
userInfoResponse = oauth.doUserInfoRequestByGet(encodedAccessToken); userInfoResponse = oauth.doUserInfoRequestByGet(response);
assertEquals(200, userInfoResponse.getStatusCode()); assertEquals(200, userInfoResponse.getStatusCode());
assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername()); assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername());
@ -639,9 +647,9 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
encodedAccessToken = response.getAccessToken(); encodedAccessToken = response.getAccessToken();
encodedRefreshToken = response.getRefreshToken(); encodedRefreshToken = response.getRefreshToken();
accessToken = oauth.verifyToken(encodedAccessToken); accessToken = oauth.verifyToken(encodedAccessToken);
jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK)jwkEc).getCrv()); jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK) jwkEc).getCrv());
jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK)jwkEc).getX()); jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK) jwkEc).getX());
jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK)jwkEc).getY()); jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK) jwkEc).getY());
jkt = JWKSUtils.computeThumbprint(jwkEc); jkt = JWKSUtils.computeThumbprint(jwkEc);
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint()); assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
refreshToken = oauth.parseRefreshToken(encodedRefreshToken); refreshToken = oauth.parseRefreshToken(encodedRefreshToken);
@ -669,6 +677,66 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
oauth.idTokenHint(encodedIdToken).openLogout(); oauth.idTokenHint(encodedIdToken).openLogout();
} }
@Test
public void testDPoPProofWithClientCredentialsGrant() throws Exception {
modifyClient(TEST_CONFIDENTIAL_CLIENT_ID, (clientRepresentation, configWrapper) -> {
clientRepresentation.setServiceAccountsEnabled(true);
configWrapper.setUseDPoP(true);
});
oauth.clientId(TEST_CONFIDENTIAL_CLIENT_ID);
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
JWK jwkRsa = createRsaJwk(rsaKeyPair.getPublic());
JWSHeader jwsRsaHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.PS256, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa);
String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate());
oauth.dpopProof(dpopProofRsaEncoded);
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest(TEST_CONFIDENTIAL_CLIENT_SECRET);
assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK) jwkRsa).getModulus());
jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK) jwkRsa).getPublicExponent());
String jkt = JWKSUtils.computeThumbprint(jwkRsa);
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
oauth.doLogout(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET);
}
@Test
public void testDPoPProofWithResourceOwnerPasswordCredentialsGrant() throws Exception {
modifyClient(TEST_CONFIDENTIAL_CLIENT_ID, (clientRepresentation, configWrapper) -> {
clientRepresentation.setDirectAccessGrantsEnabled(true);
configWrapper.setUseDPoP(true);
});
oauth.clientId(TEST_CONFIDENTIAL_CLIENT_ID);
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
JWK jwkRsa = createRsaJwk(rsaKeyPair.getPublic());
JWSHeader jwsRsaHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.PS256, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa);
String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate());
oauth.dpopProof(dpopProofRsaEncoded);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(TEST_CONFIDENTIAL_CLIENT_SECRET,
TEST_USER_NAME,
TEST_USER_PASSWORD);
assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK) jwkRsa).getModulus());
jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK) jwkRsa).getPublicExponent());
String jkt = JWKSUtils.computeThumbprint(jwkRsa);
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
oauth.doLogout(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET);
}
private OAuthClient.AccessTokenResponse getDPoPBindAccessToken(KeyPair rsaKeyPair) throws Exception { private OAuthClient.AccessTokenResponse getDPoPBindAccessToken(KeyPair rsaKeyPair) throws Exception {
oauth.clientId(TEST_CONFIDENTIAL_CLIENT_ID); oauth.clientId(TEST_CONFIDENTIAL_CLIENT_ID);
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD); oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
@ -680,23 +748,24 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.dpopProof(dpopProofRsaEncoded); oauth.dpopProof(dpopProofRsaEncoded);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, TEST_CONFIDENTIAL_CLIENT_SECRET); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, TEST_CONFIDENTIAL_CLIENT_SECRET);
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
assertEquals(Status.OK.getStatusCode(), response.getStatusCode()); assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK)jwkRsa).getModulus()); jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK) jwkRsa).getModulus());
jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK)jwkRsa).getPublicExponent()); jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK) jwkRsa).getPublicExponent());
String jkt = JWKSUtils.computeThumbprint(jwkRsa); String jkt = JWKSUtils.computeThumbprint(jwkRsa);
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint()); assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
return response; return response;
} }
private void doSuccessfulUserInfoGet(String accessToken, KeyPair rsaKeyPair) throws Exception { private void doSuccessfulUserInfoGet(OAuthClient.AccessTokenResponse accessTokenResponse, KeyPair rsaKeyPair) throws Exception {
JWK jwkRsa = createRsaJwk(rsaKeyPair.getPublic()); JWK jwkRsa = createRsaJwk(rsaKeyPair.getPublic());
JWSHeader jwsRsaHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.PS256, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa); JWSHeader jwsRsaHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.PS256, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa);
String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate()); String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate());
oauth.dpopProof(dpopProofRsaEncoded); oauth.dpopProof(dpopProofRsaEncoded);
UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(accessToken); UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(accessTokenResponse);
assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername()); assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername());
} }
@ -720,6 +789,14 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
clientResource.update(clientRep); clientResource.update(clientRep);
} }
private void modifyClient(String clientId, BiConsumer<ClientRepresentation, OIDCAdvancedConfigWrapper> modify) {
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientId);
ClientRepresentation clientRep = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
modify.accept(clientRep, configWrapper);
clientResource.update(clientRep);
}
private String createClientByAdmin(String clientName, Consumer<ClientRepresentation> op) throws ClientPolicyException { private String createClientByAdmin(String clientName, Consumer<ClientRepresentation> op) throws ClientPolicyException {
ClientRepresentation clientRep = new ClientRepresentation(); ClientRepresentation clientRep = new ClientRepresentation();
clientRep.setClientId(clientName); clientRep.setClientId(clientName);
@ -763,7 +840,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
private void updatePolicies(String json) throws ClientPolicyException { private void updatePolicies(String json) throws ClientPolicyException {
try { try {
ClientPoliciesRepresentation clientPolicies = json==null ? null : JsonSerialization.readValue(json, ClientPoliciesRepresentation.class); ClientPoliciesRepresentation clientPolicies = json == null ? null : JsonSerialization.readValue(json, ClientPoliciesRepresentation.class);
adminClient.realm(REALM_NAME).clientPoliciesPoliciesResource().updatePolicies(clientPolicies); adminClient.realm(REALM_NAME).clientPoliciesPoliciesResource().updatePolicies(clientPolicies);
} catch (BadRequestException e) { } catch (BadRequestException e) {
throw new ClientPolicyException("update policies failed", e.getResponse().getStatusInfo().toString()); throw new ClientPolicyException("update policies failed", e.getResponse().getStatusInfo().toString());