Merge pull request #4932 from patriot1burke/per-client-flow

KEYCLOAK-6335
This commit is contained in:
Bill Burke 2018-01-25 09:55:11 -05:00 committed by GitHub
commit 7c66f76858
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 746 additions and 10 deletions

View file

@ -56,6 +56,7 @@ public class ClientRepresentation {
protected Boolean frontchannelLogout;
protected String protocol;
protected Map<String, String> attributes;
protected Map<String, String> authenticationFlowBindingOverrides;
protected Boolean fullScopeAllowed;
protected Integer nodeReRegistrationTimeout;
protected Map<String, Integer> registeredNodes;
@ -296,6 +297,14 @@ public class ClientRepresentation {
this.attributes = attributes;
}
public Map<String, String> getAuthenticationFlowBindingOverrides() {
return authenticationFlowBindingOverrides;
}
public void setAuthenticationFlowBindingOverrides(Map<String, String> authenticationFlowBindingOverrides) {
this.authenticationFlowBindingOverrides = authenticationFlowBindingOverrides;
}
public Integer getNodeReRegistrationTimeout() {
return nodeReRegistrationTimeout;
}

View file

@ -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<String, String> getAuthenticationFlowBindingOverrides() {
if (isUpdated()) return updated.getAuthenticationFlowBindingOverrides();
Map<String, String> copy = new HashMap<String, String>();
copy.putAll(cached.getAuthFlowBindings());
return copy;
}
@Override
public Set<ProtocolMapperModel> getProtocolMappers() {
if (isUpdated()) return updated.getProtocolMappers();

View file

@ -46,6 +46,7 @@ public class CachedClient extends AbstractRevisioned implements InRealm {
protected String registrationToken;
protected String protocol;
protected Map<String, String> attributes = new HashMap<String, String>();
protected Map<String, String> authFlowBindings = new HashMap<String, String>();
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<String, String> getAuthFlowBindings() {
return authFlowBindings;
}
}

View file

@ -270,6 +270,29 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
}
@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<String, String> getAuthenticationFlowBindingOverrides() {
Map<String, String> copy = new HashMap<>();
copy.putAll(entity.getAuthFlowBindings());
return copy;
}
@Override
public void setAttribute(String name, String value) {
entity.getAttributes().put(name, value);

View file

@ -119,6 +119,12 @@ public class ClientEntity {
@CollectionTable(name="CLIENT_ATTRIBUTES", joinColumns={ @JoinColumn(name="CLIENT_ID") })
protected Map<String, String> attributes = new HashMap<String, String>();
@ElementCollection
@MapKeyColumn(name="BINDING_NAME")
@Column(name="FLOW_ID", length = 4000)
@CollectionTable(name="CLIENT_AUTH_FLOW_BINDINGS", joinColumns={ @JoinColumn(name="CLIENT_ID") })
protected Map<String, String> authFlowBindings = new HashMap<String, String>();
@OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE)
Collection<ClientIdentityProviderMappingEntity> identityProviders = new ArrayList<ClientIdentityProviderMappingEntity>();
@ -292,6 +298,14 @@ public class ClientEntity {
this.attributes = attributes;
}
public Map<String, String> getAuthFlowBindings() {
return authFlowBindings;
}
public void setAuthFlowBindings(Map<String, String> authFlowBindings) {
this.authFlowBindings = authFlowBindings;
}
public String getProtocol() {
return protocol;
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="bburke@redhat.com" id="4.0.0-KEYCLOAK-6335">
<createTable tableName="CLIENT_AUTH_FLOW_BINDINGS">
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="FLOW_ID" type="VARCHAR(36)"/>
<column name="BINDING_NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey columnNames="CLIENT_ID, BINDING_NAME" constraintName="C_CLI_FLOW_BIND" tableName="CLIENT_AUTH_FLOW_BINDINGS"/>
</changeSet>
</databaseChangeLog>

View file

@ -53,4 +53,5 @@
<include file="META-INF/jpa-changelog-3.4.0.xml"/>
<include file="META-INF/jpa-changelog-3.4.1.xml"/>
<include file="META-INF/jpa-changelog-3.4.2.xml"/>
<include file="META-INF/jpa-changelog-4.0.0.xml"/>
</databaseChangeLog>

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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();
}
}

View file

@ -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());

View file

@ -1084,6 +1084,17 @@ public class RepresentationToModel {
}
if (resourceRep.getAuthenticationFlowBindingOverrides() != null) {
for (Map.Entry<String, String> 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<String, String> entry : rep.getAttributes().entrySet()) {
resource.setAttribute(entry.getKey(), entry.getValue());
}
}
if (rep.getAuthenticationFlowBindingOverrides() != null) {
for (Map.Entry<String, String> 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) {

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface AuthenticationFlowBindings {
String BROWSER_BINDING = "browser";
String DIRECT_GRANT_BINDING = "direct_grant";
}

View file

@ -118,6 +118,18 @@ public interface ClientModel extends RoleContainerModel, ProtocolMapperContaine
String getAttribute(String name);
Map<String, String> 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<String, String> getAuthenticationFlowBindingOverrides();
public void removeAuthenticationFlowBindingOverride(String binding);
public void setAuthenticationFlowBindingOverride(String binding, String flowId);
boolean isFrontchannelLogout();
void setFrontchannelLogout(boolean flag);

View file

@ -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)

View file

@ -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() {

View file

@ -88,7 +88,7 @@ public class DockerEndpoint extends AuthorizationEndpointBase {
}
@Override
protected AuthenticationFlowModel getAuthenticationFlow() {
protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) {
return realm.getDockerAuthenticationFlow();
}

View file

@ -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)

View file

@ -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;

View file

@ -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)

View file

@ -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) {

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ProviderConfigProperty> 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;
}
}

View file

@ -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
org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory
org.keycloak.testsuite.forms.UsernameOnlyAuthenticator

View file

@ -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;
}

View file

@ -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 <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
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<ClientRepresentation> 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();
}
}