Pluggable Features of Token Manager

Closes #12065
This commit is contained in:
Takashi Norimatsu 2022-06-10 05:54:50 +09:00 committed by Marek Posolda
parent c60a34ac06
commit 148c7695ff
21 changed files with 964 additions and 14 deletions

View file

@ -31,18 +31,25 @@ public enum ClientPolicyEvent {
VIEW, VIEW,
UNREGISTER, UNREGISTER,
AUTHORIZATION_REQUEST, AUTHORIZATION_REQUEST,
IMPLICIT_HYBRID_TOKEN_RESPONSE,
TOKEN_REQUEST, TOKEN_REQUEST,
TOKEN_RESPONSE,
SERVICE_ACCOUNT_TOKEN_REQUEST, SERVICE_ACCOUNT_TOKEN_REQUEST,
SERVICE_ACCOUNT_TOKEN_RESPONSE,
TOKEN_REFRESH, TOKEN_REFRESH,
TOKEN_REFRESH_RESPONSE,
TOKEN_REVOKE, TOKEN_REVOKE,
TOKEN_INTROSPECT, TOKEN_INTROSPECT,
USERINFO_REQUEST, USERINFO_REQUEST,
LOGOUT_REQUEST, LOGOUT_REQUEST,
BACKCHANNEL_AUTHENTICATION_REQUEST, BACKCHANNEL_AUTHENTICATION_REQUEST,
BACKCHANNEL_TOKEN_REQUEST, BACKCHANNEL_TOKEN_REQUEST,
BACKCHANNEL_TOKEN_RESPONSE,
PUSHED_AUTHORIZATION_REQUEST, PUSHED_AUTHORIZATION_REQUEST,
DEVICE_AUTHORIZATION_REQUEST, DEVICE_AUTHORIZATION_REQUEST,
DEVICE_TOKEN_REQUEST, DEVICE_TOKEN_REQUEST,
RESOURCE_OWNER_PASSWORD_CREDENTIALS_REQUEST DEVICE_TOKEN_RESPONSE,
RESOURCE_OWNER_PASSWORD_CREDENTIALS_REQUEST,
RESOURCE_OWNER_PASSWORD_CREDENTIALS_RESPONSE
} }

View file

@ -48,7 +48,11 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.ImplicitHybridTokenResponse;
import org.keycloak.services.clientpolicy.context.TokenRefreshContext;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.protocol.oidc.utils.OAuth2Code; import org.keycloak.protocol.oidc.utils.OAuth2Code;
@ -267,6 +271,15 @@ public class OIDCLoginProtocol implements LoginProtocol {
responseBuilder.generateStateHash(state); responseBuilder.generateStateHash(state);
} }
try {
session.clientPolicy().triggerOnEvent(new ImplicitHybridTokenResponse(authSession, clientSessionCtx, responseBuilder));
} catch (ClientPolicyException cpe) {
event.error(cpe.getError());
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, cpe.getError());
return redirectUri.build();
}
AccessTokenResponse res = responseBuilder.build(); AccessTokenResponse res = responseBuilder.build();
if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) { if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {

View file

@ -74,11 +74,16 @@ import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.ResourceOwnerPasswordCredentialsContext; import org.keycloak.services.clientpolicy.context.ResourceOwnerPasswordCredentialsContext;
import org.keycloak.services.clientpolicy.context.ResourceOwnerPasswordCredentialsResponseContext;
import org.keycloak.services.clientpolicy.context.ServiceAccountTokenRequestContext; import org.keycloak.services.clientpolicy.context.ServiceAccountTokenRequestContext;
import org.keycloak.services.clientpolicy.context.ServiceAccountTokenResponseContext;
import org.keycloak.services.clientpolicy.context.TokenRefreshContext; import org.keycloak.services.clientpolicy.context.TokenRefreshContext;
import org.keycloak.services.clientpolicy.context.TokenRefreshResponseContext;
import org.keycloak.services.clientpolicy.context.TokenRequestContext; import org.keycloak.services.clientpolicy.context.TokenRequestContext;
import org.keycloak.services.clientpolicy.context.TokenResponseContext;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
@ -113,6 +118,7 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -431,11 +437,11 @@ public class TokenEndpoint {
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation // Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, codeData.getNonce()); clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, codeData.getNonce());
return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true); return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true, s -> {return new TokenResponseContext(formParams, parseResult, clientSessionCtx, s);});
} }
public Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx, public Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
String scopeParam, boolean code) { String scopeParam, boolean code, Function<TokenManager.AccessTokenResponseBuilder, ClientPolicyContext> clientPolicyContextGenerator) {
AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx); AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager
@ -451,6 +457,15 @@ public class TokenEndpoint {
responseBuilder.generateIDToken().generateAccessTokenHash(); responseBuilder.generateIDToken().generateAccessTokenHash();
} }
if (clientPolicyContextGenerator != null) {
try {
session.clientPolicy().triggerOnEvent(clientPolicyContextGenerator.apply(responseBuilder));
} catch (ClientPolicyException cpe) {
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}
}
AccessTokenResponse res = null; AccessTokenResponse res = null;
if (code) { if (code) {
try { try {
@ -506,6 +521,9 @@ public class TokenEndpoint {
try { try {
// KEYCLOAK-6771 Certificate Bound Token // KEYCLOAK-6771 Certificate Bound Token
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.refreshAccessToken(session, session.getContext().getUri(), clientConnection, realm, client, refreshToken, event, headers, request); TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.refreshAccessToken(session, session.getContext().getUri(), clientConnection, realm, client, refreshToken, event, headers, request);
session.clientPolicy().triggerOnEvent(new TokenRefreshResponseContext(formParams, responseBuilder));
res = responseBuilder.build(); res = responseBuilder.build();
if (!responseBuilder.isOfflineToken()) { if (!responseBuilder.isOfflineToken()) {
@ -525,6 +543,9 @@ public class TokenEndpoint {
event.error(Errors.INVALID_TOKEN); event.error(Errors.INVALID_TOKEN);
throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST); throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
} }
} catch (ClientPolicyException cpe) {
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
} }
event.success(); event.success();
@ -640,6 +661,13 @@ public class TokenEndpoint {
checkMtlsHoKToken(responseBuilder, useRefreshToken); checkMtlsHoKToken(responseBuilder, useRefreshToken);
try {
session.clientPolicy().triggerOnEvent(new ResourceOwnerPasswordCredentialsResponseContext(formParams, clientSessionCtx, responseBuilder));
} catch (ClientPolicyException cpe) {
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}
// TODO : do the same as codeToToken() // TODO : do the same as codeToToken()
AccessTokenResponse res = responseBuilder.build(); AccessTokenResponse res = responseBuilder.build();
@ -739,6 +767,13 @@ public class TokenEndpoint {
responseBuilder.generateIDToken().generateAccessTokenHash(); responseBuilder.generateIDToken().generateAccessTokenHash();
} }
try {
session.clientPolicy().triggerOnEvent(new ServiceAccountTokenResponseContext(formParams, clientSessionCtx.getClientSession(), responseBuilder));
} catch (ClientPolicyException cpe) {
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
}
// TODO : do the same as codeToToken() // TODO : do the same as codeToToken()
AccessTokenResponse res = null; AccessTokenResponse res = null;
try { try {

View file

@ -45,6 +45,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest; import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenRequestContext; import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenRequestContext;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenResponseContext;
import org.keycloak.protocol.oidc.grants.ciba.endpoints.CibaRootEndpoint; import org.keycloak.protocol.oidc.grants.ciba.endpoints.CibaRootEndpoint;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint; import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
@ -216,7 +217,7 @@ public class CibaGrantType {
int authTime = Time.currentTime(); int authTime = Time.currentTime();
userSession.setNote(AuthenticationManager.AUTH_TIME, String.valueOf(authTime)); userSession.setNote(AuthenticationManager.AUTH_TIME, String.valueOf(authTime));
return tokenEndpoint.createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true); return tokenEndpoint.createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true,s -> {return new BackchannelTokenResponseContext(request, formParams, clientSessionCtx, s);});
} }

View file

@ -0,0 +1,69 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class BackchannelTokenResponseContext implements ClientPolicyContext {
private final CIBAAuthenticationRequest parsedRequest;
private final MultivaluedMap<String, String> requestParameters;
private final ClientSessionContext clientSessionCtx;
private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder;
public BackchannelTokenResponseContext(CIBAAuthenticationRequest parsedRequest,
MultivaluedMap<String, String> requestParameters,
ClientSessionContext clientSessionCtx,
TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder) {
this.parsedRequest = parsedRequest;
this.requestParameters = requestParameters;
this.clientSessionCtx = clientSessionCtx;
this.accessTokenResponseBuilder = accessTokenResponseBuilder;
}
@Override
public ClientPolicyEvent getEvent() {
return ClientPolicyEvent.BACKCHANNEL_TOKEN_RESPONSE;
}
public CIBAAuthenticationRequest getParsedRequest() {
return parsedRequest;
}
public MultivaluedMap<String, String> getRequestParameters() {
return requestParameters;
}
public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() {
return accessTokenResponseBuilder;
}
public ClientSessionContext getClientSessionContext() {
return clientSessionCtx;
}
}

View file

@ -44,6 +44,7 @@ import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint; import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceTokenRequestContext; import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceTokenRequestContext;
import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceTokenResponseContext;
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint; import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
import org.keycloak.protocol.oidc.utils.PkceUtils; import org.keycloak.protocol.oidc.utils.PkceUtils;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
@ -341,6 +342,6 @@ public class DeviceGrantType {
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation // Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, deviceCodeModel.getNonce()); clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, deviceCodeModel.getNonce());
return tokenEndpoint.createTokenResponse(user, userSession, clientSessionCtx, scopeParam, false); return tokenEndpoint.createTokenResponse(user, userSession, clientSessionCtx, scopeParam, false, s -> {return new DeviceTokenResponseContext(deviceCodeModel, formParams, clientSession, s);});
} }
} }

View file

@ -0,0 +1,69 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.oidc.grants.device.clientpolicy.context;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.OAuth2DeviceCodeModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class DeviceTokenResponseContext implements ClientPolicyContext {
private final OAuth2DeviceCodeModel deviceCodeModel;
private final MultivaluedMap<String, String> requestParameters;
private final AuthenticatedClientSessionModel clientSession;
private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder;
public DeviceTokenResponseContext(OAuth2DeviceCodeModel deviceCodeModel,
MultivaluedMap<String, String> requestParameters,
AuthenticatedClientSessionModel clientSession,
TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder) {
this.deviceCodeModel = deviceCodeModel;
this.requestParameters = requestParameters;
this.clientSession = clientSession;
this.accessTokenResponseBuilder = accessTokenResponseBuilder;
}
@Override
public ClientPolicyEvent getEvent() {
return ClientPolicyEvent.DEVICE_TOKEN_RESPONSE;
}
public OAuth2DeviceCodeModel getDeviceCodeModel() {
return deviceCodeModel;
}
public MultivaluedMap<String, String> getRequestParameters() {
return requestParameters;
}
public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() {
return accessTokenResponseBuilder;
}
public AuthenticatedClientSessionModel getClientSession() {
return clientSession;
}
}

View file

@ -70,8 +70,11 @@ public class ClientAccessTypeCondition extends AbstractClientPolicyConditionProv
switch (context.getEvent()) { switch (context.getEvent()) {
case AUTHORIZATION_REQUEST: case AUTHORIZATION_REQUEST:
case TOKEN_REQUEST: case TOKEN_REQUEST:
case TOKEN_RESPONSE:
case SERVICE_ACCOUNT_TOKEN_REQUEST: case SERVICE_ACCOUNT_TOKEN_REQUEST:
case SERVICE_ACCOUNT_TOKEN_RESPONSE:
case TOKEN_REFRESH: case TOKEN_REFRESH:
case TOKEN_REFRESH_RESPONSE:
case TOKEN_REVOKE: case TOKEN_REVOKE:
case TOKEN_INTROSPECT: case TOKEN_INTROSPECT:
case USERINFO_REQUEST: case USERINFO_REQUEST:

View file

@ -70,14 +70,18 @@ public class ClientRolesCondition extends AbstractClientPolicyConditionProvider<
switch (context.getEvent()) { switch (context.getEvent()) {
case AUTHORIZATION_REQUEST: case AUTHORIZATION_REQUEST:
case TOKEN_REQUEST: case TOKEN_REQUEST:
case TOKEN_RESPONSE:
case SERVICE_ACCOUNT_TOKEN_REQUEST: case SERVICE_ACCOUNT_TOKEN_REQUEST:
case SERVICE_ACCOUNT_TOKEN_RESPONSE:
case TOKEN_REFRESH: case TOKEN_REFRESH:
case TOKEN_REFRESH_RESPONSE:
case TOKEN_REVOKE: case TOKEN_REVOKE:
case TOKEN_INTROSPECT: case TOKEN_INTROSPECT:
case USERINFO_REQUEST: case USERINFO_REQUEST:
case LOGOUT_REQUEST: case LOGOUT_REQUEST:
case BACKCHANNEL_AUTHENTICATION_REQUEST: case BACKCHANNEL_AUTHENTICATION_REQUEST:
case BACKCHANNEL_TOKEN_REQUEST: case BACKCHANNEL_TOKEN_REQUEST:
case BACKCHANNEL_TOKEN_RESPONSE:
case PUSHED_AUTHORIZATION_REQUEST: case PUSHED_AUTHORIZATION_REQUEST:
case REGISTERED: case REGISTERED:
case UPDATED: case UPDATED:

View file

@ -32,13 +32,16 @@ import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest
import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest; import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelAuthenticationRequestContext; import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelAuthenticationRequestContext;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenRequestContext; import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenRequestContext;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenResponseContext;
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation; import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
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.clientpolicy.ClientPolicyVote; import org.keycloak.services.clientpolicy.ClientPolicyVote;
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext; import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
import org.keycloak.services.clientpolicy.context.ServiceAccountTokenRequestContext; import org.keycloak.services.clientpolicy.context.ServiceAccountTokenRequestContext;
import org.keycloak.services.clientpolicy.context.ServiceAccountTokenResponseContext;
import org.keycloak.services.clientpolicy.context.TokenRequestContext; import org.keycloak.services.clientpolicy.context.TokenRequestContext;
import org.keycloak.services.clientpolicy.context.TokenResponseContext;
/** /**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a> * @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
@ -92,15 +95,24 @@ public class ClientScopesCondition extends AbstractClientPolicyConditionProvider
case TOKEN_REQUEST: case TOKEN_REQUEST:
if (isScopeMatched(((TokenRequestContext)context).getParseResult().getClientSession())) return ClientPolicyVote.YES; if (isScopeMatched(((TokenRequestContext)context).getParseResult().getClientSession())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO; return ClientPolicyVote.NO;
case TOKEN_RESPONSE:
if (isScopeMatched(((TokenResponseContext)context).getParseResult().getClientSession())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
case SERVICE_ACCOUNT_TOKEN_REQUEST: case SERVICE_ACCOUNT_TOKEN_REQUEST:
if (isScopeMatched(((ServiceAccountTokenRequestContext)context).getClientSession())) return ClientPolicyVote.YES; if (isScopeMatched(((ServiceAccountTokenRequestContext)context).getClientSession())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO; return ClientPolicyVote.NO;
case SERVICE_ACCOUNT_TOKEN_RESPONSE:
if (isScopeMatched(((ServiceAccountTokenResponseContext)context).getClientSession())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
case BACKCHANNEL_AUTHENTICATION_REQUEST: case BACKCHANNEL_AUTHENTICATION_REQUEST:
if (isScopeMatched(((BackchannelAuthenticationRequestContext)context).getParsedRequest())) return ClientPolicyVote.YES; if (isScopeMatched(((BackchannelAuthenticationRequestContext)context).getParsedRequest())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO; return ClientPolicyVote.NO;
case BACKCHANNEL_TOKEN_REQUEST: case BACKCHANNEL_TOKEN_REQUEST:
if (isScopeMatched(((BackchannelTokenRequestContext)context).getParsedRequest())) return ClientPolicyVote.YES; if (isScopeMatched(((BackchannelTokenRequestContext)context).getParsedRequest())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO; return ClientPolicyVote.NO;
case BACKCHANNEL_TOKEN_RESPONSE:
if (isScopeMatched(((BackchannelTokenResponseContext)context).getParsedRequest())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
default: default:
return ClientPolicyVote.ABSTAIN; return ClientPolicyVote.ABSTAIN;
} }

View file

@ -0,0 +1,60 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.clientpolicy.context;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class ImplicitHybridTokenResponse implements ClientPolicyContext {
private final AuthenticationSessionModel authSession;
private final ClientSessionContext clientSessionCtx;
private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder;
public ImplicitHybridTokenResponse(AuthenticationSessionModel authSession,
ClientSessionContext clientSessionCtx,
TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder) {
this.authSession = authSession;
this.clientSessionCtx = clientSessionCtx;
this.accessTokenResponseBuilder = accessTokenResponseBuilder;
}
@Override
public ClientPolicyEvent getEvent() {
return ClientPolicyEvent.IMPLICIT_HYBRID_TOKEN_RESPONSE;
}
public AuthenticationSessionModel getAuthenticationSession() {
return authSession;
}
public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() {
return accessTokenResponseBuilder;
}
public ClientSessionContext getClientSessionContext() {
return clientSessionCtx;
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.clientpolicy.context;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class ResourceOwnerPasswordCredentialsResponseContext implements ClientPolicyContext {
private final MultivaluedMap<String, String> params;
private final ClientSessionContext clientSessionCtx;
private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder;
public ResourceOwnerPasswordCredentialsResponseContext(MultivaluedMap<String, String> params,
ClientSessionContext clientSessionCtx,
TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder) {
this.params = params;
this.clientSessionCtx = clientSessionCtx;
this.accessTokenResponseBuilder = accessTokenResponseBuilder;
}
@Override
public ClientPolicyEvent getEvent() {
return ClientPolicyEvent.RESOURCE_OWNER_PASSWORD_CREDENTIALS_RESPONSE;
}
public MultivaluedMap<String, String> getParams() {
return params;
}
public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() {
return accessTokenResponseBuilder;
}
public ClientSessionContext getClientSessionContext() {
return clientSessionCtx;
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.services.clientpolicy.context;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class ServiceAccountTokenResponseContext implements ClientPolicyContext {
private final MultivaluedMap<String, String> params;
private final AuthenticatedClientSessionModel clientSession;
private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder;
public ServiceAccountTokenResponseContext(MultivaluedMap<String, String> params,
AuthenticatedClientSessionModel clientSession,
TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder) {
this.params = params;
this.clientSession = clientSession;
this.accessTokenResponseBuilder = accessTokenResponseBuilder;
}
@Override
public ClientPolicyEvent getEvent() {
return ClientPolicyEvent.SERVICE_ACCOUNT_TOKEN_RESPONSE;
}
public MultivaluedMap<String, String> getParams() {
return params;
}
public AuthenticatedClientSessionModel getClientSession() {
return clientSession;
}
public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() {
return accessTokenResponseBuilder;
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.clientpolicy.context;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class TokenRefreshResponseContext implements ClientPolicyContext {
private final MultivaluedMap<String, String> params;
private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder;
public TokenRefreshResponseContext(MultivaluedMap<String, String> params,
TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder) {
this.params = params;
this.accessTokenResponseBuilder = accessTokenResponseBuilder;
}
@Override
public ClientPolicyEvent getEvent() {
return ClientPolicyEvent.TOKEN_REFRESH_RESPONSE;
}
public MultivaluedMap<String, String> getParams() {
return params;
}
public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() {
return accessTokenResponseBuilder;
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.clientpolicy.context;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class TokenResponseContext implements ClientPolicyContext {
private final MultivaluedMap<String, String> params;
private final OAuth2CodeParser.ParseResult parseResult;
private final ClientSessionContext clientSessionCtx;
private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder;
public TokenResponseContext(MultivaluedMap<String, String> params,
OAuth2CodeParser.ParseResult parseResult,
ClientSessionContext clientSessionCtx,
TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder) {
this.params = params;
this.parseResult = parseResult;
this.clientSessionCtx = clientSessionCtx;
this.accessTokenResponseBuilder = accessTokenResponseBuilder;
}
@Override
public ClientPolicyEvent getEvent() {
return ClientPolicyEvent.TOKEN_RESPONSE;
}
public MultivaluedMap<String, String> getParams() {
return params;
}
public OAuth2CodeParser.ParseResult getParseResult() {
return parseResult;
}
public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() {
return accessTokenResponseBuilder;
}
public ClientSessionContext getClientSessionContext() {
return clientSessionCtx;
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.clientpolicy.executor;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.TokenRefreshResponseContext;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class SuppressRefreshTokenRotationExecutor implements ClientPolicyExecutorProvider<ClientPolicyExecutorConfigurationRepresentation> {
private static final Logger logger = Logger.getLogger(SuppressRefreshTokenRotationExecutor.class);
protected final KeycloakSession session;
public SuppressRefreshTokenRotationExecutor(KeycloakSession session) {
this.session = session;
}
@Override
public String getProviderId() {
return SuppressRefreshTokenRotationExecutorFactory.PROVIDER_ID;
}
@Override
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
ClientPolicyEvent event = context.getEvent();
logger.tracev("Client Policy Trigger Event = {0}", event);
switch (event) {
case TOKEN_REFRESH_RESPONSE:
TokenRefreshResponseContext tokenRefreshResponseContext = (TokenRefreshResponseContext)context;
TokenManager.AccessTokenResponseBuilder builder = tokenRefreshResponseContext.getAccessTokenResponseBuilder();
builder.refreshToken(null); // drop rotated refresh token before building a response of a token refresh request
logger.trace("A rorated refresh token was suppressed.");
break;
default :
return;
}
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.clientpolicy.executor;
import java.util.Collections;
import java.util.List;
import org.keycloak.Config.Scope;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class SuppressRefreshTokenRotationExecutorFactory implements ClientPolicyExecutorProviderFactory {
public static final String PROVIDER_ID = "suppress-refresh-token-rotation";
@Override
public ClientPolicyExecutorProvider create(KeycloakSession session) {
return new SuppressRefreshTokenRotationExecutor(session);
}
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "When token refresh, it does not return a refreshed refresh token to a client.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
@Override
public boolean isSupported() {
return true;
}
}

View file

@ -18,3 +18,4 @@ org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentia
org.keycloak.services.clientpolicy.executor.ClientSecretRotationExecutorFactory org.keycloak.services.clientpolicy.executor.ClientSecretRotationExecutorFactory
org.keycloak.services.clientpolicy.executor.RejectRequestExecutorFactory org.keycloak.services.clientpolicy.executor.RejectRequestExecutorFactory
org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory
org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutorFactory

View file

@ -1874,6 +1874,61 @@ public class CIBATest extends AbstractClientPoliciesTest {
assertThat(tokenRes.getErrorDescription(), is("Exception thrown intentionally")); assertThat(tokenRes.getErrorDescription(), is("Exception thrown intentionally"));
} }
@Test
public void testExtendedClientPolicyInterfacesForBackchannelTokenResponse() throws Exception {
String clientId = generateSuffixedName("confidential-app");
String clientSecret = "app-secret";
String cid = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
clientRep.setStandardFlowEnabled(Boolean.TRUE);
clientRep.setImplicitFlowEnabled(Boolean.TRUE);
clientRep.setPublicClient(Boolean.FALSE);
clientRep.setBearerOnly(Boolean.FALSE);
Map<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "poll");
attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
clientRep.setAttributes(attributes);
});
adminClient.realm(REALM_NAME).clients().get(cid).roles().create(RoleBuilder.create().name(SAMPLE_CLIENT_ROLE).build());
final String bindingMessage = "BASTION";
Map<String, String> additionalParameters = new HashMap<>();
additionalParameters.put("user_device", "mobile");
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, TEST_USER_NAME, bindingMessage, null, null, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(200)));
Assert.assertNotNull(response.getAuthReqId());
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
TestAuthenticationChannelRequest authenticationChannelReq = oidcClientEndpointsResource.getAuthenticationChannel(bindingMessage);
int statusCode = oauth.doAuthenticationChannelCallback(authenticationChannelReq.getBearerToken(), SUCCEED);
assertThat(statusCode, is(equalTo(200)));
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Forste Profilen")
.addExecutor(TestRaiseExceptionExecutorFactory.PROVIDER_ID,
createTestRaiseExeptionExecutorConfig(Arrays.asList(ClientPolicyEvent.BACKCHANNEL_TOKEN_RESPONSE)))
.toRepresentation()
).toString();
updateProfiles(json);
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "A Primeira Politica", Boolean.TRUE)
.addCondition(ClientRolesConditionFactory.PROVIDER_ID,
createClientRolesConditionConfig(Arrays.asList(SAMPLE_CLIENT_ROLE)))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
OAuthClient.AccessTokenResponse tokenRes = oauth.doBackchannelAuthenticationTokenRequest(clientId, clientSecret, response.getAuthReqId());
assertThat(tokenRes.getStatusCode(), is(equalTo(400)));
assertThat(tokenRes.getError(), is(ClientPolicyEvent.BACKCHANNEL_TOKEN_RESPONSE.toString()));
assertThat(tokenRes.getErrorDescription(), is("Exception thrown intentionally"));
}
@Test @Test
public void testSecureCibaAuthenticationRequestSigningAlgorithmEnforceExecutor() throws Exception { public void testSecureCibaAuthenticationRequestSigningAlgorithmEnforceExecutor() throws Exception {
// register profiles // register profiles

View file

@ -37,7 +37,6 @@ import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.ImmutableMap;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
@ -77,7 +76,6 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper;
import org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper; import org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -124,11 +122,11 @@ import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFac
import org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutorFactory;
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.client.resources.TestApplicationResourceUrls; import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LogoutConfirmPage; import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.pages.OAuth2DeviceVerificationPage; import org.keycloak.testsuite.pages.OAuth2DeviceVerificationPage;
@ -138,11 +136,9 @@ import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseException
import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExceptionExecutorFactory; import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExceptionExecutorFactory;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.ClientPoliciesUtil; import org.keycloak.testsuite.util.ClientPoliciesUtil;
import org.keycloak.testsuite.util.MutualTLSUtils; import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.OAuthClient.AuthorizationEndpointResponse;
import org.keycloak.testsuite.util.RoleBuilder; import org.keycloak.testsuite.util.RoleBuilder;
import org.keycloak.testsuite.util.ServerURLs; import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
@ -2751,6 +2747,252 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
assertEquals("Exception thrown intentionally", tokenResponse.getErrorDescription()); assertEquals("Exception thrown intentionally", tokenResponse.getErrorDescription());
} }
@Test
public void testExtendedClientPolicyIntefacesForDeviceTokenResponse() throws Exception {
// Device Authorization Request from device
oauth.realm(REALM_NAME);
oauth.clientId(DEVICE_APP);
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
Assert.assertEquals(200, response.getStatusCode());
assertNotNull(response.getDeviceCode());
assertNotNull(response.getUserCode());
assertNotNull(response.getVerificationUri());
assertNotNull(response.getVerificationUriComplete());
// Verify user code from verification page using browser
openVerificationPage(response.getVerificationUri());
verificationPage.assertCurrent();
verificationPage.submit(response.getUserCode());
loginPage.assertCurrent();
// Do Login
oauth.fillLoginForm("device-login", "password");
// Consent
grantPage.assertCurrent();
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
grantPage.accept();
verificationPage.assertApprovedPage();
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Forste Profilen")
.addExecutor(TestRaiseExceptionExecutorFactory.PROVIDER_ID,
createTestRaiseExeptionExecutorConfig(Arrays.asList(ClientPolicyEvent.DEVICE_TOKEN_RESPONSE)))
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID, createAnyClientConditionConfig())
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
// Token request from device
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
assertEquals(400, tokenResponse.getStatusCode());
assertEquals(ClientPolicyEvent.DEVICE_TOKEN_RESPONSE.toString(), tokenResponse.getError());
assertEquals("Exception thrown intentionally", tokenResponse.getErrorDescription());
}
@Test
public void testExtendedClientPolicyIntefacesForTokenResponse() throws Exception {
// register a confidential client
String clientId = generateSuffixedName(CLIENT_NAME);
String clientSecret = "secret";
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
clientRep.setPublicClient(Boolean.FALSE);
clientRep.setBearerOnly(Boolean.FALSE);
});
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Forste Profilen")
.addExecutor(TestRaiseExceptionExecutorFactory.PROVIDER_ID,
createTestRaiseExeptionExecutorConfig(Arrays.asList(ClientPolicyEvent.TOKEN_RESPONSE)))
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Primera Plitica", Boolean.TRUE)
.addCondition(ClientAccessTypeConditionFactory.PROVIDER_ID,
createClientAccessTypeConditionConfig(Arrays.asList(ClientAccessTypeConditionFactory.TYPE_CONFIDENTIAL)))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
oauth.clientId(clientId);
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
events.expectLogin().client(clientId).assertEvent();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, clientSecret);
assertEquals(400, response.getStatusCode());
assertEquals(ClientPolicyEvent.TOKEN_RESPONSE.toString(), response.getError());
assertEquals("Exception thrown intentionally", response.getErrorDescription());
}
@Test
public void testExtendedClientPolicyIntefacesForTokenRefreshResponse() throws Exception {
String clientId = generateSuffixedName(CLIENT_NAME);
String clientSecret = "secret";
String cid = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
clientRep.setStandardFlowEnabled(Boolean.TRUE);
clientRep.setImplicitFlowEnabled(Boolean.TRUE);
clientRep.setPublicClient(Boolean.FALSE);
});
adminClient.realm(REALM_NAME).clients().get(cid).roles().create(RoleBuilder.create().name(SAMPLE_CLIENT_ROLE).build());
oauth.clientId(clientId);
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
EventRepresentation loginEvent = events.expectLogin().client(clientId).assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = new OAuthClient.AuthorizationEndpointResponse(oauth).getCode();
OAuthClient.AccessTokenResponse res = oauth.doAccessTokenRequest(code, clientSecret);
assertEquals(200, res.getStatusCode());
events.expectCodeToToken(codeId, sessionId).client(clientId).assertEvent();
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
.addExecutor(SuppressRefreshTokenRotationExecutorFactory.PROVIDER_ID, null)
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Den Forste Politikken", Boolean.TRUE)
.addCondition(ClientRolesConditionFactory.PROVIDER_ID,
createClientRolesConditionConfig(Arrays.asList(SAMPLE_CLIENT_ROLE)))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
String refreshTokenString = res.getRefreshToken();
OAuthClient.AccessTokenResponse accessTokenResponseRefreshed = oauth.doRefreshTokenRequest(refreshTokenString, clientSecret);
assertEquals(200, accessTokenResponseRefreshed.getStatusCode());
assertEquals(null, accessTokenResponseRefreshed.getRefreshToken());
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Den Forste Politikken", Boolean.TRUE)
.addCondition(ClientRolesConditionFactory.PROVIDER_ID,
createClientRolesConditionConfig(Arrays.asList("other" + SAMPLE_CLIENT_ROLE)))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
accessTokenResponseRefreshed = oauth.doRefreshTokenRequest(refreshTokenString, clientSecret);
assertEquals(200, accessTokenResponseRefreshed.getStatusCode());
RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(accessTokenResponseRefreshed.getRefreshToken());
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
assertEquals(findUserByUsername(adminClient.realm(REALM_NAME), TEST_USER_NAME).getId(), refreshedRefreshToken.getSubject());
}
@Test
public void testExtendedClientPolicyIntefacesForServiceAccountTokenRequeponse() throws Exception {
String clientId = "service-account-app";
String clientSecret = "app-secret";
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
clientRep.setStandardFlowEnabled(Boolean.FALSE);
clientRep.setImplicitFlowEnabled(Boolean.FALSE);
clientRep.setServiceAccountsEnabled(Boolean.TRUE);
clientRep.setPublicClient(Boolean.FALSE);
clientRep.setBearerOnly(Boolean.FALSE);
});
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Forste Profilen")
.addExecutor(TestRaiseExceptionExecutorFactory.PROVIDER_ID,
createTestRaiseExeptionExecutorConfig(Arrays.asList(ClientPolicyEvent.SERVICE_ACCOUNT_TOKEN_RESPONSE)))
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Het Eerste Beleid", Boolean.TRUE)
.addCondition(ClientScopesConditionFactory.PROVIDER_ID,
createClientScopesConditionConfig(ClientScopesConditionFactory.OPTIONAL, Arrays.asList("offline_access", "microprofile-jwt")))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
String origClientId = oauth.getClientId();
oauth.clientId("service-account-app");
oauth.scope("offline_access");
try {
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("app-secret");
assertEquals(400, response.getStatusCode());
assertEquals(ClientPolicyEvent.SERVICE_ACCOUNT_TOKEN_RESPONSE.toString(), response.getError());
assertEquals("Exception thrown intentionally", response.getErrorDescription());
} finally {
oauth.clientId(origClientId);
}
}
@Test
public void testExtendedClientPolicyIntefacesForResourceOwnerPasswordCredentialsResponse() throws Exception {
String clientId = generateSuffixedName(CLIENT_NAME);
String clientSecret = "secret";
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
clientRep.setStandardFlowEnabled(Boolean.TRUE);
clientRep.setDirectAccessGrantsEnabled(Boolean.TRUE);
clientRep.setPublicClient(Boolean.FALSE);
});
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Forste Profilen")
.addExecutor(TestRaiseExceptionExecutorFactory.PROVIDER_ID,
createTestRaiseExeptionExecutorConfig(Arrays.asList(ClientPolicyEvent.RESOURCE_OWNER_PASSWORD_CREDENTIALS_RESPONSE)))
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Porisii desu", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
createAnyClientConditionConfig())
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(clientSecret, TEST_USER_NAME, TEST_USER_PASSWORD, null);
assertEquals(400, response.getStatusCode());
assertEquals(ClientPolicyEvent.RESOURCE_OWNER_PASSWORD_CREDENTIALS_RESPONSE.toString(), response.getError());
assertEquals("Exception thrown intentionally", response.getErrorDescription());
}
@Test @Test
public void testSecureLogoutExecutor() throws Exception { public void testSecureLogoutExecutor() throws Exception {
// register profiles // register profiles

View file

@ -317,9 +317,9 @@ public final class ClientPoliciesUtil {
} }
public static TestRaiseExceptionExecutor.Configuration createTestRaiseExeptionExecutorConfig(List<ClientPolicyEvent> events) { public static TestRaiseExceptionExecutor.Configuration createTestRaiseExeptionExecutorConfig(List<ClientPolicyEvent> events) {
TestRaiseExceptionExecutor.Configuration conf = new TestRaiseExceptionExecutor.Configuration(); TestRaiseExceptionExecutor.Configuration conf = new TestRaiseExceptionExecutor.Configuration();
conf.setEvents(events); conf.setEvents(events);
return conf; return conf;
} }
public static ClientPolicyConditionConfigurationRepresentation createAnyClientConditionConfig() { public static ClientPolicyConditionConfigurationRepresentation createAnyClientConditionConfig() {