Merge pull request #4932 from patriot1burke/per-client-flow
KEYCLOAK-6335
This commit is contained in:
commit
7c66f76858
23 changed files with 746 additions and 10 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -88,7 +88,7 @@ public class DockerEndpoint extends AuthorizationEndpointBase {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected AuthenticationFlowModel getAuthenticationFlow() {
|
||||
protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) {
|
||||
return realm.getDockerAuthenticationFlow();
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue