diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java index a7ca83e1a6..77c2c45bd7 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java @@ -56,6 +56,7 @@ public class ClientRepresentation { protected Boolean frontchannelLogout; protected String protocol; protected Map attributes; + protected Map authenticationFlowBindingOverrides; protected Boolean fullScopeAllowed; protected Integer nodeReRegistrationTimeout; protected Map registeredNodes; @@ -296,6 +297,14 @@ public class ClientRepresentation { this.attributes = attributes; } + public Map getAuthenticationFlowBindingOverrides() { + return authenticationFlowBindingOverrides; + } + + public void setAuthenticationFlowBindingOverrides(Map authenticationFlowBindingOverrides) { + this.authenticationFlowBindingOverrides = authenticationFlowBindingOverrides; + } + public Integer getNodeReRegistrationTimeout() { return nodeReRegistrationTimeout; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index 14f4c0f76a..a5823ceeed 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -345,6 +345,34 @@ public class ClientAdapter implements ClientModel { return copy; } + @Override + public void setAuthenticationFlowBindingOverride(String name, String value) { + getDelegateForUpdate(); + updated.setAuthenticationFlowBindingOverride(name, value); + + } + + @Override + public void removeAuthenticationFlowBindingOverride(String name) { + getDelegateForUpdate(); + updated.removeAuthenticationFlowBindingOverride(name); + + } + + @Override + public String getAuthenticationFlowBindingOverride(String name) { + if (isUpdated()) return updated.getAuthenticationFlowBindingOverride(name); + return cached.getAuthFlowBindings().get(name); + } + + @Override + public Map getAuthenticationFlowBindingOverrides() { + if (isUpdated()) return updated.getAuthenticationFlowBindingOverrides(); + Map copy = new HashMap(); + copy.putAll(cached.getAuthFlowBindings()); + return copy; + } + @Override public Set getProtocolMappers() { if (isUpdated()) return updated.getProtocolMappers(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedClient.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedClient.java index b4b67293a0..787dc45a9f 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedClient.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedClient.java @@ -46,6 +46,7 @@ public class CachedClient extends AbstractRevisioned implements InRealm { protected String registrationToken; protected String protocol; protected Map attributes = new HashMap(); + protected Map authFlowBindings = new HashMap(); protected boolean publicClient; protected boolean fullScopeAllowed; protected boolean frontchannelLogout; @@ -83,6 +84,7 @@ public class CachedClient extends AbstractRevisioned implements InRealm { enabled = model.isEnabled(); protocol = model.getProtocol(); attributes.putAll(model.getAttributes()); + authFlowBindings.putAll(model.getAuthenticationFlowBindingOverrides()); notBefore = model.getNotBefore(); frontchannelLogout = model.isFrontchannelLogout(); publicClient = model.isPublicClient(); @@ -256,4 +258,8 @@ public class CachedClient extends AbstractRevisioned implements InRealm { public boolean isUseTemplateMappers() { return useTemplateMappers; } + + public Map getAuthFlowBindings() { + return authFlowBindings; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index ac1a7386cd..3a7eabbed2 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -270,6 +270,29 @@ public class ClientAdapter implements ClientModel, JpaModel { } + @Override + public void setAuthenticationFlowBindingOverride(String name, String value) { + entity.getAuthFlowBindings().put(name, value); + + } + + @Override + public void removeAuthenticationFlowBindingOverride(String name) { + entity.getAuthFlowBindings().remove(name); + } + + @Override + public String getAuthenticationFlowBindingOverride(String name) { + return entity.getAuthFlowBindings().get(name); + } + + @Override + public Map getAuthenticationFlowBindingOverrides() { + Map copy = new HashMap<>(); + copy.putAll(entity.getAuthFlowBindings()); + return copy; + } + @Override public void setAttribute(String name, String value) { entity.getAttributes().put(name, value); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index dab3fe4cd1..7f889777af 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -119,6 +119,12 @@ public class ClientEntity { @CollectionTable(name="CLIENT_ATTRIBUTES", joinColumns={ @JoinColumn(name="CLIENT_ID") }) protected Map attributes = new HashMap(); + @ElementCollection + @MapKeyColumn(name="BINDING_NAME") + @Column(name="FLOW_ID", length = 4000) + @CollectionTable(name="CLIENT_AUTH_FLOW_BINDINGS", joinColumns={ @JoinColumn(name="CLIENT_ID") }) + protected Map authFlowBindings = new HashMap(); + @OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE) Collection identityProviders = new ArrayList(); @@ -292,6 +298,14 @@ public class ClientEntity { this.attributes = attributes; } + public Map getAuthFlowBindings() { + return authFlowBindings; + } + + public void setAuthFlowBindings(Map authFlowBindings) { + this.authFlowBindings = authFlowBindings; + } + public String getProtocol() { return protocol; } diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml new file mode 100644 index 0000000000..f7ebc6838b --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index d89aa9696f..fa824e2fdd 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -53,4 +53,5 @@ + diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/AuthenticationFlowResolver.java b/server-spi-private/src/main/java/org/keycloak/models/utils/AuthenticationFlowResolver.java new file mode 100644 index 0000000000..1a27dd8107 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/AuthenticationFlowResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 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.models.utils; + +import org.keycloak.models.AuthenticationFlowBindings; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; +import org.keycloak.sessions.AuthenticationSessionModel; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class AuthenticationFlowResolver { + + public static AuthenticationFlowModel resolveBrowserFlow(AuthenticationSessionModel authSession) { + AuthenticationFlowModel flow = null; + ClientModel client = authSession.getClient(); + String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING); + if (clientFlow != null) { + flow = authSession.getRealm().getAuthenticationFlowById(clientFlow); + if (flow == null) { + throw new ModelException("Client " + client.getClientId() + " has browser flow override, but this flow does not exist"); + } + return flow; + } + return authSession.getRealm().getBrowserFlow(); + } + public static AuthenticationFlowModel resolveDirectGrantFlow(AuthenticationSessionModel authSession) { + AuthenticationFlowModel flow = null; + ClientModel client = authSession.getClient(); + String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING); + if (clientFlow != null) { + flow = authSession.getRealm().getAuthenticationFlowById(clientFlow); + if (flow == null) { + throw new ModelException("Client " + client.getClientId() + " has direct grant flow override, but this flow does not exist"); + } + return flow; + } + return authSession.getRealm().getDirectGrantFlow(); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index aa6b42c42b..0dad16a48d 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -494,6 +494,7 @@ public class ModelToRepresentation { rep.setFrontchannelLogout(clientModel.isFrontchannelLogout()); rep.setProtocol(clientModel.getProtocol()); rep.setAttributes(clientModel.getAttributes()); + rep.setAuthenticationFlowBindingOverrides(clientModel.getAuthenticationFlowBindingOverrides()); rep.setFullScopeAllowed(clientModel.isFullScopeAllowed()); rep.setBearerOnly(clientModel.isBearerOnly()); rep.setConsentRequired(clientModel.isConsentRequired()); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 34ee766fed..5fb5003f2f 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -1084,6 +1084,17 @@ public class RepresentationToModel { } + if (resourceRep.getAuthenticationFlowBindingOverrides() != null) { + for (Map.Entry entry : resourceRep.getAuthenticationFlowBindingOverrides().entrySet()) { + if (entry.getValue() == null || entry.getValue().trim().equals("")) { + continue; + } else { + client.setAuthenticationFlowBindingOverride(entry.getKey(), entry.getValue()); + } + } + } + + if (resourceRep.getRedirectUris() != null) { for (String redirectUri : resourceRep.getRedirectUris()) { client.addRedirectUri(redirectUri); @@ -1201,6 +1212,22 @@ public class RepresentationToModel { resource.setAttribute(entry.getKey(), entry.getValue()); } } + if (rep.getAttributes() != null) { + for (Map.Entry entry : rep.getAttributes().entrySet()) { + resource.setAttribute(entry.getKey(), entry.getValue()); + } + } + + if (rep.getAuthenticationFlowBindingOverrides() != null) { + for (Map.Entry entry : rep.getAuthenticationFlowBindingOverrides().entrySet()) { + if (entry.getValue() == null || entry.getValue().trim().equals("")) { + resource.removeAuthenticationFlowBindingOverride(entry.getKey()); + } else { + resource.setAuthenticationFlowBindingOverride(entry.getKey(), entry.getValue()); + + } + } + } if (rep.getNotBefore() != null) { diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticationFlowBindings.java b/server-spi/src/main/java/org/keycloak/models/AuthenticationFlowBindings.java new file mode 100644 index 0000000000..251547203f --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/AuthenticationFlowBindings.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 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.models; + +/** + * Defines constants for authentication flow bindings. Strings used for lookup + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface AuthenticationFlowBindings { + String BROWSER_BINDING = "browser"; + String DIRECT_GRANT_BINDING = "direct_grant"; +} diff --git a/server-spi/src/main/java/org/keycloak/models/ClientModel.java b/server-spi/src/main/java/org/keycloak/models/ClientModel.java index 5f4403f9f6..aa406cfc05 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientModel.java @@ -118,6 +118,18 @@ public interface ClientModel extends RoleContainerModel, ProtocolMapperContaine String getAttribute(String name); Map getAttributes(); + /** + * Get authentication flow binding override for this client. Allows client to override an authentication flow binding. + * + * @param binding examples are "browser", "direct_grant" + * + * @return + */ + public String getAuthenticationFlowBindingOverride(String binding); + public Map getAuthenticationFlowBindingOverrides(); + public void removeAuthenticationFlowBindingOverride(String binding); + public void setAuthenticationFlowBindingOverride(String binding, String flowId); + boolean isFrontchannelLogout(); void setFrontchannelLogout(boolean flag); diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 7a3c3af7ec..4464ff75cd 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -38,6 +38,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.LoginProtocol; @@ -646,7 +647,7 @@ public class AuthenticationProcessor { AuthenticationProcessor processor = new AuthenticationProcessor(); processor.setAuthenticationSession(clone) .setFlowPath(LoginActionsService.AUTHENTICATE_PATH) - .setFlowId(realm.getBrowserFlow().getId()) + .setFlowId(AuthenticationFlowResolver.resolveBrowserFlow(clone).getId()) .setForwardedErrorMessage(reset.getErrorMessage()) .setForwardedSuccessMessage(reset.getSuccessMessage()) .setConnection(connection) diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java index dbed381d22..619a862bba 100755 --- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java +++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java @@ -30,6 +30,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.services.ErrorPageException; import org.keycloak.services.managers.AuthenticationManager; @@ -107,7 +108,7 @@ public abstract class AuthorizationEndpointBase { * @return response to be returned to the browser */ protected Response handleBrowserAuthenticationRequest(AuthenticationSessionModel authSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) { - AuthenticationFlowModel flow = getAuthenticationFlow(); + AuthenticationFlowModel flow = getAuthenticationFlow(authSession); String flowId = flow.getId(); AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH); event.detail(Details.CODE_ID, authSession.getParentSession().getId()); @@ -149,8 +150,8 @@ public abstract class AuthorizationEndpointBase { } } - protected AuthenticationFlowModel getAuthenticationFlow() { - return realm.getBrowserFlow(); + protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) { + return AuthenticationFlowResolver.resolveBrowserFlow(authSession); } protected void checkSsl() { diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java index 0c9cb79945..6ed777d4b2 100644 --- a/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java @@ -88,7 +88,7 @@ public class DockerEndpoint extends AuthorizationEndpointBase { } @Override - protected AuthenticationFlowModel getAuthenticationFlow() { + protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) { return realm.getDockerAuthenticationFlow(); } 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 ee0ff85b5d..3be96860a0 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 @@ -56,6 +56,7 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.TokenManager; @@ -491,7 +492,7 @@ public class TokenEndpoint { authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); - AuthenticationFlowModel flow = realm.getDirectGrantFlow(); + AuthenticationFlowModel flow = AuthenticationFlowResolver.resolveDirectGrantFlow(authSession); String flowId = flow.getId(); AuthenticationProcessor processor = new AuthenticationProcessor(); processor.setAuthenticationSession(authSession) diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java index c0be2baf97..cb0c3673fc 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java +++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java @@ -147,7 +147,7 @@ public class SamlEcpProfileService extends SamlService { } @Override - protected AuthenticationFlowModel getAuthenticationFlow() { + protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) { for (AuthenticationFlowModel flowModel : realm.getAuthenticationFlows()) { if (flowModel.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) { return flowModel; diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 33a982fb92..c8e3fda0b5 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -55,6 +55,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.FormMessage; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocolFactory; @@ -1121,7 +1122,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal protected Response browserAuthentication(AuthenticationSessionModel authSession, String errorMessage) { this.event.event(EventType.LOGIN); - AuthenticationFlowModel flow = realmModel.getBrowserFlow(); + AuthenticationFlowModel flow = AuthenticationFlowResolver.resolveBrowserFlow(authSession); String flowId = flow.getId(); AuthenticationProcessor processor = new AuthenticationProcessor(); processor.setAuthenticationSession(authSession) diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index f09cfbb2a4..fd171ee420 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -53,6 +53,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.SystemClientUtil; import org.keycloak.protocol.AuthorizationEndpointBase; @@ -252,7 +253,7 @@ public class LoginActionsService { } protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) { - return processFlow(action, execution, authSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor()); + return processFlow(action, execution, authSession, AUTHENTICATE_PATH, AuthenticationFlowResolver.resolveBrowserFlow(authSession), errorMessage, new AuthenticationProcessor()); } protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) { diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/UsernameOnlyAuthenticator.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/UsernameOnlyAuthenticator.java new file mode 100644 index 0000000000..1fd72f4def --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/UsernameOnlyAuthenticator.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016 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.testsuite.forms; + +import org.keycloak.Config; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UsernameOnlyAuthenticator implements Authenticator, AuthenticatorFactory { + public static final String PROVIDER_ID = "testsuite-username"; + + @Override + public void authenticate(AuthenticationFlowContext context) { + String username = context.getHttpRequest().getDecodedFormParameters().getFirst("username"); + UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username); + if (user == null) { + context.failure(AuthenticationFlowError.UNKNOWN_USER); + return; + } + context.setUser(user); + context.success(); + } + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + + } + + @Override + public void action(AuthenticationFlowContext context) { + + } + + @Override + public String getDisplayType() { + return "Testsuite Username Only"; + } + + @Override + public String getReferenceCategory() { + return null; + } + + @Override + public boolean isConfigurable() { + return false; + } + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED + }; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Testsuite Username authenticator. Username parameter sets username"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public void close() { + + } + + @Override + public Authenticator create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index 18317d048c..3b28d99c0e 100755 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -19,4 +19,5 @@ org.keycloak.testsuite.forms.PassThroughAuthenticator org.keycloak.testsuite.forms.PassThroughRegistration org.keycloak.testsuite.forms.ClickThroughAuthenticator org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory -org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory \ No newline at end of file +org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory +org.keycloak.testsuite.forms.UsernameOnlyAuthenticator \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java index f55e90f48e..1fac71ae06 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java @@ -180,6 +180,8 @@ public class ProvidersTest extends AbstractAuthenticationTest { "Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user"); addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru", "Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user"); + addProviderInfo(result, "testsuite-username", "Testsuite Username Only", + "Testsuite Username authenticator. Username parameter sets username"); return result; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/FlowOverrideTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/FlowOverrideTest.java new file mode 100644 index 0000000000..33bb259011 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/FlowOverrideTest.java @@ -0,0 +1,353 @@ +/* + * Copyright 2017 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.testsuite.forms; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; +import org.keycloak.events.Details; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowBindings; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; +import org.keycloak.util.BasicAuthHelper; +import org.openqa.selenium.By; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Test that clients can override auth flows + * + * @author Bill Burke + */ +public class FlowOverrideTest extends AbstractTestRealmKeycloakTest { + + public static final String TEST_APP_DIRECT_OVERRIDE = "test-app-direct-override"; + public static final String TEST_APP_FLOW = "test-app-flow"; + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + @Page + protected ErrorPage errorPage; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(UserResource.class) + .addPackages(true, "org.keycloak.testsuite"); + } + + + @Before + public void setupFlows() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + + ClientModel client = session.realms().getClientByClientId("test-app-flow", realm); + if (client != null) { + return; + } + + client = session.realms().getClientByClientId("test-app", realm); + client.setDirectAccessGrantsEnabled(true); + + + + // Parent flow + AuthenticationFlowModel browser = new AuthenticationFlowModel(); + browser.setAlias("parent-flow"); + browser.setDescription("browser based authentication"); + browser.setProviderId("basic-flow"); + browser.setTopLevel(true); + browser.setBuiltIn(true); + browser = realm.addAuthenticationFlow(browser); + + // Subflow2 + AuthenticationFlowModel subflow2 = new AuthenticationFlowModel(); + subflow2.setTopLevel(false); + subflow2.setBuiltIn(true); + subflow2.setAlias("subflow-2"); + subflow2.setDescription("username+password AND pushButton"); + subflow2.setProviderId("basic-flow"); + subflow2 = realm.addAuthenticationFlow(subflow2); + + AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); + execution.setParentFlow(browser.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setFlowId(subflow2.getId()); + execution.setPriority(20); + execution.setAuthenticatorFlow(true); + realm.addAuthenticatorExecution(execution); + + // Subflow2 - push the button + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow2.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(PushButtonAuthenticatorFactory.PROVIDER_ID); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + + // Subflow2 - username-password + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow2.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(UsernamePasswordFormFactory.PROVIDER_ID); + execution.setPriority(20); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + + client = realm.addClient(TEST_APP_FLOW); + client.setSecret("password"); + client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth"); + client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin"); + client.setEnabled(true); + client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*"); + client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId()); + client.setPublicClient(false); + + // Parent flow + AuthenticationFlowModel directGrant = new AuthenticationFlowModel(); + directGrant.setAlias("direct-override-flow"); + directGrant.setDescription("direct grant based authentication"); + directGrant.setProviderId("basic-flow"); + directGrant.setTopLevel(true); + directGrant.setBuiltIn(true); + directGrant = realm.addAuthenticationFlow(directGrant); + + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(directGrant.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(UsernameOnlyAuthenticator.PROVIDER_ID); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + + client = realm.addClient(TEST_APP_DIRECT_OVERRIDE); + client.setSecret("password"); + client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth"); + client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin"); + client.setEnabled(true); + client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*"); + client.setPublicClient(false); + client.setDirectAccessGrantsEnabled(true); + client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId()); + client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, directGrant.getId()); + + + + }); + } + + //@Test + public void testRunConsole() throws Exception { + Thread.sleep(10000000); + } + + + @Test + public void testWithClientBrowserOverride() throws Exception { + oauth.clientId(TEST_APP_FLOW); + String loginFormUrl = oauth.getLoginFormUrl(); + log.info("loginFormUrl: " + loginFormUrl); + + //Thread.sleep(10000000); + + driver.navigate().to(loginFormUrl); + + Assert.assertEquals("PushTheButton", driver.getTitle()); + + // Push the button. I am redirected to username+password form + driver.findElement(By.name("submit1")).click(); + + + loginPage.assertCurrent(); + + // Fill username+password. I am successfully authenticated + oauth.fillLoginForm("test-user@localhost", "password"); + appPage.assertCurrent(); + + events.expectLogin().client("test-app-flow").detail(Details.USERNAME, "test-user@localhost").assertEvent(); + } + + @Test + public void testNoOverrideBrowser() throws Exception { + String clientId = "test-app"; + testNoOverrideBrowser(clientId); + } + + private void testNoOverrideBrowser(String clientId) { + oauth.clientId(clientId); + String loginFormUrl = oauth.getLoginFormUrl(); + log.info("loginFormUrl: " + loginFormUrl); + + //Thread.sleep(10000000); + + driver.navigate().to(loginFormUrl); + + loginPage.assertCurrent(); + + // Fill username+password. I am successfully authenticated + oauth.fillLoginForm("test-user@localhost", "password"); + appPage.assertCurrent(); + + events.expectLogin().client(clientId).detail(Details.USERNAME, "test-user@localhost").assertEvent(); + } + + @Test + public void testGrantAccessTokenNoOverride() throws Exception { + testDirectGrantNoOverride("test-app"); + } + + private void testDirectGrantNoOverride(String clientId) { + Client httpClient = javax.ws.rs.client.ClientBuilder.newClient(); + String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl(); + WebTarget grantTarget = httpClient.target(grantUri); + + { // test no password + String header = BasicAuthHelper.createHeader(clientId, "password"); + Form form = new Form(); + form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD); + form.param("username", "test-user@localhost"); + Response response = grantTarget.request() + .header(HttpHeaders.AUTHORIZATION, header) + .post(Entity.form(form)); + assertEquals(401, response.getStatus()); + response.close(); + } + + { // test invalid password + String header = BasicAuthHelper.createHeader(clientId, "password"); + Form form = new Form(); + form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD); + form.param("username", "test-user@localhost"); + form.param("password", "invalid"); + Response response = grantTarget.request() + .header(HttpHeaders.AUTHORIZATION, header) + .post(Entity.form(form)); + assertEquals(401, response.getStatus()); + response.close(); + } + + { // test valid password + String header = BasicAuthHelper.createHeader(clientId, "password"); + Form form = new Form(); + form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD); + form.param("username", "test-user@localhost"); + form.param("password", "password"); + Response response = grantTarget.request() + .header(HttpHeaders.AUTHORIZATION, header) + .post(Entity.form(form)); + assertEquals(200, response.getStatus()); + response.close(); + } + + httpClient.close(); + events.clear(); + } + + @Test + public void testGrantAccessTokenWithClientOverride() throws Exception { + String clientId = TEST_APP_DIRECT_OVERRIDE; + Client httpClient = javax.ws.rs.client.ClientBuilder.newClient(); + String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl(); + WebTarget grantTarget = httpClient.target(grantUri); + + { // test no password + String header = BasicAuthHelper.createHeader(clientId, "password"); + Form form = new Form(); + form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD); + form.param("username", "test-user@localhost"); + Response response = grantTarget.request() + .header(HttpHeaders.AUTHORIZATION, header) + .post(Entity.form(form)); + assertEquals(200, response.getStatus()); + response.close(); + } + + httpClient.close(); + events.clear(); + } + + @Test + public void testRestInterface() throws Exception { + ClientsResource clients = adminClient.realm("test").clients(); + List query = clients.findByClientId(TEST_APP_DIRECT_OVERRIDE); + ClientRepresentation clientRep = query.get(0); + String directGrantFlowId = clientRep.getAuthenticationFlowBindingOverrides().get(AuthenticationFlowBindings.DIRECT_GRANT_BINDING); + Assert.assertNotNull(directGrantFlowId); + clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, ""); + clients.get(clientRep.getId()).update(clientRep); + testDirectGrantNoOverride(TEST_APP_DIRECT_OVERRIDE); + clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, directGrantFlowId); + clients.get(clientRep.getId()).update(clientRep); + testGrantAccessTokenWithClientOverride(); + + query = clients.findByClientId(TEST_APP_FLOW); + clientRep = query.get(0); + String browserFlowId = clientRep.getAuthenticationFlowBindingOverrides().get(AuthenticationFlowBindings.BROWSER_BINDING); + Assert.assertNotNull(browserFlowId); + clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.BROWSER_BINDING, ""); + clients.get(clientRep.getId()).update(clientRep); + testNoOverrideBrowser(TEST_APP_FLOW); + clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.BROWSER_BINDING, browserFlowId); + clients.get(clientRep.getId()).update(clientRep); + testWithClientBrowserOverride(); + + } + +}