diff --git a/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyEvent.java b/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyEvent.java index 4093f95e67..b17f8d21cb 100644 --- a/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyEvent.java +++ b/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyEvent.java @@ -31,18 +31,25 @@ public enum ClientPolicyEvent { VIEW, UNREGISTER, AUTHORIZATION_REQUEST, + IMPLICIT_HYBRID_TOKEN_RESPONSE, TOKEN_REQUEST, + TOKEN_RESPONSE, SERVICE_ACCOUNT_TOKEN_REQUEST, + SERVICE_ACCOUNT_TOKEN_RESPONSE, TOKEN_REFRESH, + TOKEN_REFRESH_RESPONSE, TOKEN_REVOKE, TOKEN_INTROSPECT, USERINFO_REQUEST, LOGOUT_REQUEST, BACKCHANNEL_AUTHENTICATION_REQUEST, BACKCHANNEL_TOKEN_REQUEST, + BACKCHANNEL_TOKEN_RESPONSE, PUSHED_AUTHORIZATION_REQUEST, DEVICE_AUTHORIZATION_REQUEST, DEVICE_TOKEN_REQUEST, - RESOURCE_OWNER_PASSWORD_CREDENTIALS_REQUEST + DEVICE_TOKEN_RESPONSE, + RESOURCE_OWNER_PASSWORD_CREDENTIALS_REQUEST, + RESOURCE_OWNER_PASSWORD_CREDENTIALS_RESPONSE } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 00553dc97f..db98a183bf 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -48,7 +48,11 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.adapters.action.PushNotBeforeAction; +import org.keycloak.services.CorsErrorResponseException; 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.AuthenticationSessionManager; import org.keycloak.protocol.oidc.utils.OAuth2Code; @@ -267,6 +271,15 @@ public class OIDCLoginProtocol implements LoginProtocol { 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(); if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index e147e7ec9f..cf5128dea2 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -74,11 +74,16 @@ import org.keycloak.saml.common.util.DocumentUtil; import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.ServicesLogger; import org.keycloak.services.Urls; +import org.keycloak.services.clientpolicy.ClientPolicyContext; import org.keycloak.services.clientpolicy.ClientPolicyException; 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.ServiceAccountTokenResponseContext; 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.TokenResponseContext; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; @@ -113,6 +118,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; 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 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, - String scopeParam, boolean code) { + String scopeParam, boolean code, Function clientPolicyContextGenerator) { AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx); TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager @@ -451,6 +457,15 @@ public class TokenEndpoint { 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; if (code) { try { @@ -506,6 +521,9 @@ public class TokenEndpoint { try { // KEYCLOAK-6771 Certificate Bound Token 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(); if (!responseBuilder.isOfflineToken()) { @@ -525,6 +543,9 @@ public class TokenEndpoint { event.error(Errors.INVALID_TOKEN); 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(); @@ -640,6 +661,13 @@ public class TokenEndpoint { 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() AccessTokenResponse res = responseBuilder.build(); @@ -739,6 +767,13 @@ public class TokenEndpoint { 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() AccessTokenResponse res = null; try { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java index 6a90a8c26d..d1da7a0dc0 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java @@ -45,6 +45,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; 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.BackchannelTokenResponseContext; import org.keycloak.protocol.oidc.grants.ciba.endpoints.CibaRootEndpoint; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.endpoints.TokenEndpoint; @@ -216,7 +217,7 @@ public class CibaGrantType { int authTime = Time.currentTime(); 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);}); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/context/BackchannelTokenResponseContext.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/context/BackchannelTokenResponseContext.java new file mode 100644 index 0000000000..1425d6a3d7 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/context/BackchannelTokenResponseContext.java @@ -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 Takashi Norimatsu + */ +public class BackchannelTokenResponseContext implements ClientPolicyContext { + + private final CIBAAuthenticationRequest parsedRequest; + private final MultivaluedMap requestParameters; + private final ClientSessionContext clientSessionCtx; + private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder; + + public BackchannelTokenResponseContext(CIBAAuthenticationRequest parsedRequest, + MultivaluedMap 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 getRequestParameters() { + return requestParameters; + } + + public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() { + return accessTokenResponseBuilder; + } + + public ClientSessionContext getClientSessionContext() { + return clientSessionCtx; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java index 313dcaaf34..12e9cc5e80 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java @@ -44,6 +44,7 @@ import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; 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.DeviceTokenResponseContext; import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint; import org.keycloak.protocol.oidc.utils.PkceUtils; 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 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);}); } } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceTokenResponseContext.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceTokenResponseContext.java new file mode 100644 index 0000000000..6c279ff45a --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceTokenResponseContext.java @@ -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 Takashi Norimatsu + */ +public class DeviceTokenResponseContext implements ClientPolicyContext { + + private final OAuth2DeviceCodeModel deviceCodeModel; + private final MultivaluedMap requestParameters; + private final AuthenticatedClientSessionModel clientSession; + private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder; + + public DeviceTokenResponseContext(OAuth2DeviceCodeModel deviceCodeModel, + MultivaluedMap 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 getRequestParameters() { + return requestParameters; + } + + public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() { + return accessTokenResponseBuilder; + } + + public AuthenticatedClientSessionModel getClientSession() { + return clientSession; + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientAccessTypeCondition.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientAccessTypeCondition.java index c4e175b9f9..95d10a102c 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientAccessTypeCondition.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientAccessTypeCondition.java @@ -70,8 +70,11 @@ public class ClientAccessTypeCondition extends AbstractClientPolicyConditionProv switch (context.getEvent()) { case AUTHORIZATION_REQUEST: case TOKEN_REQUEST: + case TOKEN_RESPONSE: case SERVICE_ACCOUNT_TOKEN_REQUEST: + case SERVICE_ACCOUNT_TOKEN_RESPONSE: case TOKEN_REFRESH: + case TOKEN_REFRESH_RESPONSE: case TOKEN_REVOKE: case TOKEN_INTROSPECT: case USERINFO_REQUEST: diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientRolesCondition.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientRolesCondition.java index 6977c1ed71..2737c409b3 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientRolesCondition.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientRolesCondition.java @@ -70,14 +70,18 @@ public class ClientRolesCondition extends AbstractClientPolicyConditionProvider< switch (context.getEvent()) { case AUTHORIZATION_REQUEST: case TOKEN_REQUEST: + case TOKEN_RESPONSE: case SERVICE_ACCOUNT_TOKEN_REQUEST: + case SERVICE_ACCOUNT_TOKEN_RESPONSE: case TOKEN_REFRESH: + case TOKEN_REFRESH_RESPONSE: case TOKEN_REVOKE: case TOKEN_INTROSPECT: case USERINFO_REQUEST: case LOGOUT_REQUEST: case BACKCHANNEL_AUTHENTICATION_REQUEST: case BACKCHANNEL_TOKEN_REQUEST: + case BACKCHANNEL_TOKEN_RESPONSE: case PUSHED_AUTHORIZATION_REQUEST: case REGISTERED: case UPDATED: diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientScopesCondition.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientScopesCondition.java index ffd7909668..0c1da6b1df 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientScopesCondition.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientScopesCondition.java @@ -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.clientpolicy.context.BackchannelAuthenticationRequestContext; 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.services.clientpolicy.ClientPolicyContext; import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyVote; import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext; 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.TokenResponseContext; /** * @author Takashi Norimatsu @@ -92,15 +95,24 @@ public class ClientScopesCondition extends AbstractClientPolicyConditionProvider case TOKEN_REQUEST: if (isScopeMatched(((TokenRequestContext)context).getParseResult().getClientSession())) return ClientPolicyVote.YES; return ClientPolicyVote.NO; + case TOKEN_RESPONSE: + if (isScopeMatched(((TokenResponseContext)context).getParseResult().getClientSession())) return ClientPolicyVote.YES; + return ClientPolicyVote.NO; case SERVICE_ACCOUNT_TOKEN_REQUEST: if (isScopeMatched(((ServiceAccountTokenRequestContext)context).getClientSession())) return ClientPolicyVote.YES; return ClientPolicyVote.NO; + case SERVICE_ACCOUNT_TOKEN_RESPONSE: + if (isScopeMatched(((ServiceAccountTokenResponseContext)context).getClientSession())) return ClientPolicyVote.YES; + return ClientPolicyVote.NO; case BACKCHANNEL_AUTHENTICATION_REQUEST: if (isScopeMatched(((BackchannelAuthenticationRequestContext)context).getParsedRequest())) return ClientPolicyVote.YES; return ClientPolicyVote.NO; case BACKCHANNEL_TOKEN_REQUEST: if (isScopeMatched(((BackchannelTokenRequestContext)context).getParsedRequest())) return ClientPolicyVote.YES; return ClientPolicyVote.NO; + case BACKCHANNEL_TOKEN_RESPONSE: + if (isScopeMatched(((BackchannelTokenResponseContext)context).getParsedRequest())) return ClientPolicyVote.YES; + return ClientPolicyVote.NO; default: return ClientPolicyVote.ABSTAIN; } diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/context/ImplicitHybridTokenResponse.java b/services/src/main/java/org/keycloak/services/clientpolicy/context/ImplicitHybridTokenResponse.java new file mode 100644 index 0000000000..8ba5c9165d --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/context/ImplicitHybridTokenResponse.java @@ -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 Takashi Norimatsu + */ +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; + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/context/ResourceOwnerPasswordCredentialsResponseContext.java b/services/src/main/java/org/keycloak/services/clientpolicy/context/ResourceOwnerPasswordCredentialsResponseContext.java new file mode 100644 index 0000000000..a7bccd54fa --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/context/ResourceOwnerPasswordCredentialsResponseContext.java @@ -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 Takashi Norimatsu + */ +public class ResourceOwnerPasswordCredentialsResponseContext implements ClientPolicyContext { + + private final MultivaluedMap params; + private final ClientSessionContext clientSessionCtx; + private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder; + + public ResourceOwnerPasswordCredentialsResponseContext(MultivaluedMap 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 getParams() { + return params; + } + + public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() { + return accessTokenResponseBuilder; + } + + public ClientSessionContext getClientSessionContext() { + return clientSessionCtx; + } + +} + diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/context/ServiceAccountTokenResponseContext.java b/services/src/main/java/org/keycloak/services/clientpolicy/context/ServiceAccountTokenResponseContext.java new file mode 100644 index 0000000000..9c46b77303 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/context/ServiceAccountTokenResponseContext.java @@ -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 Takashi Norimatsu + */ +public class ServiceAccountTokenResponseContext implements ClientPolicyContext { + + private final MultivaluedMap params; + private final AuthenticatedClientSessionModel clientSession; + private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder; + + public ServiceAccountTokenResponseContext(MultivaluedMap 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 getParams() { + return params; + } + + public AuthenticatedClientSessionModel getClientSession() { + return clientSession; + } + + public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() { + return accessTokenResponseBuilder; + } +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/context/TokenRefreshResponseContext.java b/services/src/main/java/org/keycloak/services/clientpolicy/context/TokenRefreshResponseContext.java new file mode 100644 index 0000000000..8f3f8f4c24 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/context/TokenRefreshResponseContext.java @@ -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 Takashi Norimatsu + */ +public class TokenRefreshResponseContext implements ClientPolicyContext { + + private final MultivaluedMap params; + private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder; + + public TokenRefreshResponseContext(MultivaluedMap params, + TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder) { + this.params = params; + this.accessTokenResponseBuilder = accessTokenResponseBuilder; + } + + @Override + public ClientPolicyEvent getEvent() { + return ClientPolicyEvent.TOKEN_REFRESH_RESPONSE; + } + + public MultivaluedMap getParams() { + return params; + } + + public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() { + return accessTokenResponseBuilder; + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/context/TokenResponseContext.java b/services/src/main/java/org/keycloak/services/clientpolicy/context/TokenResponseContext.java new file mode 100644 index 0000000000..bcd66e7e72 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/context/TokenResponseContext.java @@ -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 Takashi Norimatsu + */ +public class TokenResponseContext implements ClientPolicyContext { + + private final MultivaluedMap params; + private final OAuth2CodeParser.ParseResult parseResult; + private final ClientSessionContext clientSessionCtx; + private final TokenManager.AccessTokenResponseBuilder accessTokenResponseBuilder; + + public TokenResponseContext(MultivaluedMap 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 getParams() { + return params; + } + + public OAuth2CodeParser.ParseResult getParseResult() { + return parseResult; + } + + public TokenManager.AccessTokenResponseBuilder getAccessTokenResponseBuilder() { + return accessTokenResponseBuilder; + } + + public ClientSessionContext getClientSessionContext() { + return clientSessionCtx; + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SuppressRefreshTokenRotationExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SuppressRefreshTokenRotationExecutor.java new file mode 100644 index 0000000000..df725c1c13 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SuppressRefreshTokenRotationExecutor.java @@ -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 Takashi Norimatsu + */ +public class SuppressRefreshTokenRotationExecutor implements ClientPolicyExecutorProvider { + + 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; + } + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SuppressRefreshTokenRotationExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SuppressRefreshTokenRotationExecutorFactory.java new file mode 100644 index 0000000000..f1e375ab12 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SuppressRefreshTokenRotationExecutorFactory.java @@ -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 Takashi Norimatsu + */ +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 getConfigProperties() { + return Collections.emptyList(); + } + + @Override + public boolean isSupported() { + return true; + } +} \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory index 67aa86849d..be75d59bd3 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory @@ -17,4 +17,5 @@ org.keycloak.services.clientpolicy.executor.SecureLogoutExecutorFactory org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutorFactory org.keycloak.services.clientpolicy.executor.ClientSecretRotationExecutorFactory org.keycloak.services.clientpolicy.executor.RejectRequestExecutorFactory -org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory \ No newline at end of file +org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory +org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutorFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java index 6684c7a4ad..b27b64cf7e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java @@ -1874,6 +1874,61 @@ public class CIBATest extends AbstractClientPoliciesTest { 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 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 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 public void testSecureCibaAuthenticationRequestSigningAlgorithmEnforceExecutor() throws Exception { // register profiles diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java index a8bd1582ae..0f4d5992a5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java @@ -37,7 +37,6 @@ import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.TextNode; -import com.google.common.collect.ImmutableMap; import org.apache.http.HttpResponse; 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.OIDCConfigAttributes; 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.utils.OIDCResponseType; 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.SecureSigningAlgorithmExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtExecutorFactory; +import org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutorFactory; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; -import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LogoutConfirmPage; 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.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.util.ClientBuilder; -import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.ClientPoliciesUtil; import org.keycloak.testsuite.util.MutualTLSUtils; import org.keycloak.testsuite.util.OAuthClient; -import org.keycloak.testsuite.util.OAuthClient.AuthorizationEndpointResponse; import org.keycloak.testsuite.util.RoleBuilder; import org.keycloak.testsuite.util.ServerURLs; import org.keycloak.testsuite.util.UserBuilder; @@ -2751,6 +2747,252 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { 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 public void testSecureLogoutExecutor() throws Exception { // register profiles diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java index 0400954ce4..88e6989627 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java @@ -317,9 +317,9 @@ public final class ClientPoliciesUtil { } public static TestRaiseExceptionExecutor.Configuration createTestRaiseExeptionExecutorConfig(List events) { - TestRaiseExceptionExecutor.Configuration conf = new TestRaiseExceptionExecutor.Configuration(); - conf.setEvents(events); - return conf; + TestRaiseExceptionExecutor.Configuration conf = new TestRaiseExceptionExecutor.Configuration(); + conf.setEvents(events); + return conf; } public static ClientPolicyConditionConfigurationRepresentation createAnyClientConditionConfig() {