keycloak-uncached
Changes
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedClient.java 6(+6 -0)
server-spi-private/src/main/java/org/keycloak/models/utils/AuthenticationFlowResolver.java 57(+57 -0)
services/src/main/java/org/keycloak/connections/httpclient/ProxyMappingsAwareRoutePlanner.java 53(+53 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/UsernameOnlyAuthenticator.java 137(+137 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java 6(+3 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java 2(+2 -0)
Details
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 a7ca83e..77c2c45 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<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;
}
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 2207153..01cfd8c 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
@@ -346,6 +346,34 @@ public class ClientAdapter implements ClientModel {
}
@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();
return cached.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 b4b6729..787dc45 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<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;
+ }
}
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 ac1a738..3a7eabb 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
@@ -271,6 +271,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);
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 dab3fe4..7f88977 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<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;
}
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 0000000..f7ebc68
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml
@@ -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>
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 d89aa96..fa824e2 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 @@
<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>
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 0000000..2515472
--- /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 <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";
+}
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 5f4403f..aa406cf 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<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);
diff --git a/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java b/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java
index 83a27fc..ca5f986 100755
--- a/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java
+++ b/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java
@@ -68,6 +68,10 @@ public class OTPPolicy implements Serializable {
public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(UserCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1, 30);
+ public String getAlgorithmKey() {
+ return algToKeyUriAlg.containsKey(algorithm) ? algToKeyUriAlg.get(algorithm) : algorithm;
+ }
+
public String getType() {
return type;
}
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 0000000..1a27dd8
--- /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 <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();
+ }
+}
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 aa6b42c..0dad16a 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 34ee766..5fb5003 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<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) {
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 7a3c3af..4464ff7 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/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
index 217581b..7e6b4c1 100755
--- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
+++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
@@ -39,6 +39,22 @@ import java.security.KeyStore;
import java.util.concurrent.TimeUnit;
/**
+ * The default {@link HttpClientFactory} for {@link HttpClientProvider HttpClientProvider's} used by Keycloak for outbound HTTP calls.
+ * <p>
+ * The constructed clients can be configured via Keycloaks SPI configuration, e.g. {@code standalone.xml, standalone-ha.xml, domain.xml}.
+ * </p>
+ * <p>
+ * Examples for jboss-cli
+ * </p>
+ * <pre>
+ * {@code
+ *
+ * /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:add(enabled=true)
+ * /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.connection-pool-size,value=128)
+ * /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.proxy-mappings,value=[".*\\.(google|googleapis)\\.com;http://www-proxy.acme.corp.com:8080",".*\\.acme\\.corp\\.com;NO_PROXY",".*;http://fallback:8080"])
+ * }
+ * </pre>
+ * </p>
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DefaultHttpClientFactory implements HttpClientFactory {
@@ -127,6 +143,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
String clientKeystore = config.get("client-keystore");
String clientKeystorePassword = config.get("client-keystore-password");
String clientPrivateKeyPassword = config.get("client-key-password");
+ String[] proxyMappings = config.getArray("proxy-mappings");
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
boolean disableTrustManager = truststoreProvider == null || truststoreProvider.getTruststore() == null;
@@ -137,13 +154,15 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
: HttpClientBuilder.HostnameVerificationPolicy.valueOf(truststoreProvider.getPolicy().name());
HttpClientBuilder builder = new HttpClientBuilder();
+
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
.maxPooledPerRoute(maxPooledPerRoute)
.connectionPoolSize(connectionPoolSize)
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
- .disableCookies(disableCookies);
+ .disableCookies(disableCookies)
+ .proxyMappings(ProxyMappings.valueOf(proxyMappings));
if (disableTrustManager) {
// TODO: is it ok to do away with disabling trust manager?
diff --git a/services/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java b/services/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java
index e4ac52b..e5bebf2 100755
--- a/services/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java
+++ b/services/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java
@@ -52,7 +52,7 @@ import java.util.concurrent.TimeUnit;
* @version $Revision: 1 $
*/
public class HttpClientBuilder {
- public static enum HostnameVerificationPolicy {
+ public enum HostnameVerificationPolicy {
/**
* Hostname verification is not done on the server's certificate
*/
@@ -104,7 +104,7 @@ public class HttpClientBuilder {
protected long establishConnectionTimeout = -1;
protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
protected boolean disableCookies = false;
-
+ protected ProxyMappings proxyMappings;
/**
* Socket inactivity timeout
@@ -208,6 +208,11 @@ public class HttpClientBuilder {
return this;
}
+ public HttpClientBuilder proxyMappings(ProxyMappings proxyMappings) {
+ this.proxyMappings = proxyMappings;
+ return this;
+ }
+
static class VerifierWrapper implements X509HostnameVerifier {
protected HostnameVerifier verifier;
@@ -272,6 +277,7 @@ public class HttpClientBuilder {
tlsContext.init(null, null, null);
sslsf = new SSLConnectionSocketFactory(tlsContext, verifier);
}
+
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout((int) establishConnectionTimeout)
.setSocketTimeout((int) socketTimeout).build();
@@ -283,6 +289,11 @@ public class HttpClientBuilder {
.setMaxConnPerRoute(maxPooledPerRoute)
.setConnectionTimeToLive(connectionTTL, connectionTTLUnit);
+
+ if (proxyMappings != null && !proxyMappings.isEmpty()) {
+ builder.setRoutePlanner(new ProxyMappingsAwareRoutePlanner(proxyMappings));
+ }
+
if (maxConnectionIdleTime > 0) {
// Will start background cleaner thread
builder.evictIdleConnections(maxConnectionIdleTime, maxConnectionIdleTimeUnit);
diff --git a/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java
new file mode 100644
index 0000000..c534cb8
--- /dev/null
+++ b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java
@@ -0,0 +1,192 @@
+/*
+ * 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.connections.httpclient;
+
+import org.apache.http.HttpHost;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * {@link ProxyMappings} describes an ordered mapping for hostname regex patterns to a {@link HttpHost} proxy.
+ * <p>
+ * Mappings can be created via {@link #valueOf(String...)} or {@link #valueOf(List)}.
+ * For a description of the mapping format see {@link ProxyMapping#valueOf(String)}
+ *
+ * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
+ */
+public class ProxyMappings {
+
+ private static final ProxyMappings EMPTY_MAPPING = valueOf(Collections.emptyList());
+
+ private final List<ProxyMapping> entries;
+
+ /**
+ * Creates a {@link ProxyMappings} from the provided {@link ProxyMapping Entries}.
+ *
+ * @param entries
+ */
+ public ProxyMappings(List<ProxyMapping> entries) {
+ this.entries = Collections.unmodifiableList(entries);
+ }
+
+ /**
+ * Creates a new {@link ProxyMappings} from the provided {@code List} of proxy mapping strings.
+ * <p>
+ *
+ * @param proxyMappings
+ */
+ public static ProxyMappings valueOf(List<String> proxyMappings) {
+
+ if (proxyMappings == null || proxyMappings.isEmpty()) {
+ return EMPTY_MAPPING;
+ }
+
+ List<ProxyMapping> entries = proxyMappings.stream() //
+ .map(ProxyMapping::valueOf) //
+ .collect(Collectors.toList());
+
+ return new ProxyMappings(entries);
+ }
+
+ /**
+ * Creates a new {@link ProxyMappings} from the provided {@code String[]} of proxy mapping strings.
+ *
+ * @param proxyMappings
+ * @return
+ * @see #valueOf(List)
+ * @see ProxyMapping#valueOf(String...)
+ */
+ public static ProxyMappings valueOf(String... proxyMappings) {
+
+ if (proxyMappings == null || proxyMappings.length == 0) {
+ return EMPTY_MAPPING;
+ }
+
+ return valueOf(Arrays.asList(proxyMappings));
+ }
+
+
+ public boolean isEmpty() {
+ return this.entries.isEmpty();
+ }
+
+ /**
+ * @param hostname
+ * @return the {@link HttpHost} proxy associated with the first matching hostname {@link Pattern}
+ * or {@literal null} if none matches.
+ */
+ public HttpHost getProxyFor(String hostname) {
+
+ Objects.requireNonNull(hostname, "hostname");
+
+ return entries.stream() //
+ .filter(e -> e.matches(hostname)) //
+ .findFirst() //
+ .map(ProxyMapping::getProxy) //
+ .orElse(null);
+ }
+
+ /**
+ * {@link ProxyMapping} describes a Proxy Mapping with a Hostname {@link Pattern}
+ * that is mapped to a proxy {@link HttpHost}.
+ */
+ public static class ProxyMapping {
+
+ public static final String NO_PROXY = "NO_PROXY";
+ private static final String DELIMITER = ";";
+
+ private final Pattern hostnamePattern;
+
+ private final HttpHost proxy;
+
+ public ProxyMapping(Pattern hostnamePattern, HttpHost proxy) {
+ this.hostnamePattern = hostnamePattern;
+ this.proxy = proxy;
+ }
+
+ public Pattern getHostnamePattern() {
+ return hostnamePattern;
+ }
+
+ public HttpHost getProxy() {
+ return proxy;
+ }
+
+ public boolean matches(String hostname) {
+ return getHostnamePattern().matcher(hostname).matches();
+ }
+
+ /**
+ * Parses a mapping string into an {@link ProxyMapping}.
+ * <p>
+ * A proxy mapping string must have the following format: {@code hostnameRegex;www-proxy-uri}
+ * with semicolon as a delimiter.</p>
+ * <p>
+ * If no proxy should be used for a host pattern then use {@code NO_PROXY} as www-proxy-uri.
+ * </p>
+ * <p>Examples:
+ * <pre>
+ * {@code
+ *
+ * .*\.(google\.com|googleapis\.com);http://www-proxy.acme.corp.com:8080
+ * .*\.acme\.corp\.com;NO_PROXY
+ * .*;http://fallback:8080
+ * }
+ * </pre>
+ * </p>
+ *
+ * @param mapping
+ * @return
+ */
+ public static ProxyMapping valueOf(String mapping) {
+
+ String[] mappingTokens = mapping.split(DELIMITER);
+
+ String hostPatternRegex = mappingTokens[0];
+ String proxyUriString = mappingTokens[1];
+
+ Pattern hostPattern = Pattern.compile(hostPatternRegex);
+ HttpHost proxyHost = toProxyHost(proxyUriString);
+
+ return new ProxyMapping(hostPattern, proxyHost);
+ }
+
+ private static HttpHost toProxyHost(String proxyUriString) {
+
+ if (NO_PROXY.equals(proxyUriString)) {
+ return null;
+ }
+
+ URI uri = URI.create(proxyUriString);
+ return new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
+ }
+
+ @Override
+ public String toString() {
+ return "ProxyMapping{" +
+ "hostnamePattern=" + hostnamePattern +
+ ", proxy=" + proxy +
+ '}';
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappingsAwareRoutePlanner.java b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappingsAwareRoutePlanner.java
new file mode 100644
index 0000000..353a5bb
--- /dev/null
+++ b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappingsAwareRoutePlanner.java
@@ -0,0 +1,53 @@
+/*
+ * 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.connections.httpclient;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.impl.conn.DefaultRoutePlanner;
+import org.apache.http.impl.conn.DefaultSchemePortResolver;
+import org.apache.http.protocol.HttpContext;
+import org.jboss.logging.Logger;
+
+/**
+ * A {@link DefaultRoutePlanner} that determines the proxy to use for a given target hostname by consulting
+ * the given {@link ProxyMappings}.
+ *
+ * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
+ * @see ProxyMappings
+ */
+public class ProxyMappingsAwareRoutePlanner extends DefaultRoutePlanner {
+
+ private static final Logger LOG = Logger.getLogger(ProxyMappingsAwareRoutePlanner.class);
+
+ private final ProxyMappings proxyMappings;
+
+ public ProxyMappingsAwareRoutePlanner(ProxyMappings proxyMappings) {
+ super(DefaultSchemePortResolver.INSTANCE);
+ this.proxyMappings = proxyMappings;
+ }
+
+ @Override
+ protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException {
+
+ HttpHost proxy = proxyMappings.getProxyFor(target.getHostName());
+ LOG.debugf("Returning proxy=%s for targetHost=%s", proxy, target.getHostName());
+
+ return proxy;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
index dbed381..619a862 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 0c9cb79..6ed777d 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 ee0ff85..3be9686 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 c0be2ba..cb0c367 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 33a982f..c8e3fda 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 f09cfbb..fd171ee 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/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java b/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java
new file mode 100644
index 0000000..3841521
--- /dev/null
+++ b/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.connections.httpclient;
+
+import org.apache.http.HttpHost;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Tests for {@link ProxyMappings}.
+ *
+ * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
+ */
+public class ProxyMappingsTest {
+
+ private static final List<String> DEFAULT_MAPPINGS = Arrays.asList( //
+ ".*\\.(google|googleapis)\\.com;http://proxy1:8080", //
+ ".*\\.facebook\\.com;http://proxy2:8080" //
+ );
+
+ private static final List<String> MAPPINGS_WITH_FALLBACK = new ArrayList<>();
+
+ private static final List<String> MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION = new ArrayList<>();
+
+ static {
+ MAPPINGS_WITH_FALLBACK.addAll(DEFAULT_MAPPINGS);
+ MAPPINGS_WITH_FALLBACK.add(".*;http://fallback:8080");
+ }
+
+ static {
+ MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION.addAll(DEFAULT_MAPPINGS);
+ MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION.add(".*\\.acme\\.corp\\.com;NO_PROXY");
+ MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION.add(".*;http://fallback:8080");
+ }
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ ProxyMappings proxyMappings;
+
+ @Before
+ public void setup() {
+ proxyMappings = ProxyMappings.valueOf(DEFAULT_MAPPINGS);
+ }
+
+ @Test
+ public void proxyMappingFromEmptyListShouldBeEmpty() {
+ assertThat(new ProxyMappings(new ArrayList<>()).isEmpty(), is(true));
+ }
+
+ @Test
+ public void shouldReturnProxy1ForConfiguredProxyMapping() {
+
+ HttpHost proxy = proxyMappings.getProxyFor("account.google.com");
+ assertThat(proxy, is(notNullValue()));
+ assertThat(proxy.getHostName(), is("proxy1"));
+ }
+
+ @Test
+ public void shouldReturnProxy1ForConfiguredProxyMappingAlternative() {
+
+ HttpHost proxy = proxyMappings.getProxyFor("www.googleapis.com");
+ assertThat(proxy, is(notNullValue()));
+ assertThat(proxy.getHostName(), is("proxy1"));
+ }
+
+ @Test
+ public void shouldReturnProxy1ForConfiguredProxyMappingWithSubDomain() {
+
+ HttpHost proxy = proxyMappings.getProxyFor("awesome.account.google.com");
+ assertThat(proxy, is(notNullValue()));
+ assertThat(proxy.getHostName(), is("proxy1"));
+ }
+
+ @Test
+ public void shouldReturnProxy2ForConfiguredProxyMapping() {
+
+ HttpHost proxy = proxyMappings.getProxyFor("login.facebook.com");
+ assertThat(proxy, is(notNullValue()));
+ assertThat(proxy.getHostName(), is("proxy2"));
+ }
+
+ @Test
+ public void shouldReturnNoProxyForUnknownHost() {
+
+ HttpHost proxy = proxyMappings.getProxyFor("login.microsoft.com");
+ assertThat(proxy, is(nullValue()));
+ }
+
+ @Test
+ public void shouldRejectNull() {
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("hostname");
+
+ proxyMappings.getProxyFor(null);
+ }
+
+ @Test
+ public void shouldReturnFallbackForNotExplicitlyMappedHostname() {
+
+ ProxyMappings proxyMappingsWithFallback = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK);
+
+ HttpHost proxy = proxyMappingsWithFallback.getProxyFor("login.salesforce.com");
+ assertThat(proxy.getHostName(), is("fallback"));
+ }
+
+ @Test
+ public void shouldReturnCorrectProxyOrFallback() {
+
+ ProxyMappings proxyMappingsWithFallback = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK);
+
+ HttpHost forGoogle = proxyMappingsWithFallback.getProxyFor("login.google.com");
+ assertThat(forGoogle.getHostName(), is("proxy1"));
+
+ HttpHost forFacebook = proxyMappingsWithFallback.getProxyFor("login.facebook.com");
+ assertThat(forFacebook.getHostName(), is("proxy2"));
+
+ HttpHost forMicrosoft = proxyMappingsWithFallback.getProxyFor("login.microsoft.com");
+ assertThat(forMicrosoft.getHostName(), is("fallback"));
+
+ HttpHost forSalesForce = proxyMappingsWithFallback.getProxyFor("login.salesforce.com");
+ assertThat(forSalesForce.getHostName(), is("fallback"));
+ }
+
+ @Test
+ public void shouldReturnFallbackForNotExplicitlyMappedHostnameAndHonorProxyExceptions() {
+
+ ProxyMappings proxyMappingsWithFallbackAndProxyException = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION);
+
+ HttpHost forGoogle = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.google.com");
+ assertThat(forGoogle.getHostName(), is("proxy1"));
+
+ HttpHost forFacebook = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.facebook.com");
+ assertThat(forFacebook.getHostName(), is("proxy2"));
+
+ HttpHost forAcmeCorp = proxyMappingsWithFallbackAndProxyException.getProxyFor("myapp.acme.corp.com");
+ assertThat(forAcmeCorp, is(nullValue()));
+
+ HttpHost forMicrosoft = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.microsoft.com");
+ assertThat(forMicrosoft.getHostName(), is("fallback"));
+
+ HttpHost forSalesForce = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.salesforce.com");
+ assertThat(forSalesForce.getHostName(), is("fallback"));
+ }
+}
\ No newline at end of file
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 0000000..1fd72f4
--- /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 <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;
+ }
+}
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 18317d0..3b28d99 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/account/AccountFormServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
index ca8a270..a942ec4 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
@@ -837,7 +837,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
assertTrue(driver.findElement(By.id("kc-totp-secret-key")).getText().matches("[\\w]{4}( [\\w]{4}){7}"));
assertEquals("Type: Time-based", driver.findElement(By.id("kc-totp-type")).getText());
- assertEquals("Algorithm: HmacSHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
+ assertEquals("Algorithm: SHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
assertEquals("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
index f42a146..c2ba191 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
@@ -177,7 +177,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
assertTrue(driver.findElement(By.id("kc-totp-secret-key")).getText().matches("[\\w]{4}( [\\w]{4}){7}"));
assertEquals("Type: Time-based", driver.findElement(By.id("kc-totp-type")).getText());
- assertEquals("Algorithm: HmacSHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
+ assertEquals("Algorithm: SHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
assertEquals("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
@@ -217,9 +217,9 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
totpPage.clickManual();
assertEquals("Type: Counter-based", driver.findElement(By.id("kc-totp-type")).getText());
- assertEquals("Algorithm: HmacSHA256", driver.findElement(By.id("kc-totp-algorithm")).getText());
+ assertEquals("Algorithm: SHA256", driver.findElement(By.id("kc-totp-algorithm")).getText());
assertEquals("Digits: 8", driver.findElement(By.id("kc-totp-digits")).getText());
- assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
+ assertEquals("Counter: 0", driver.findElement(By.id("kc-totp-counter")).getText());
} finally {
rep.setOtpPolicyDigits(6);
rep.setOtpPolicyType("totp");
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 f55e90f..1fac71a 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 0000000..33bb259
--- /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 <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();
+
+ }
+
+}
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
index b943edc..44f4bf2 100755
--- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -114,6 +114,7 @@ totpType=Type
totpAlgorithm=Algorithm
totpDigits=Digits
totpInterval=Interval
+totpCounter=Counter
missingUsernameMessage=Please specify username.
missingFirstNameMessage=Please specify first name.
diff --git a/themes/src/main/resources/theme/base/account/totp.ftl b/themes/src/main/resources/theme/base/account/totp.ftl
index e94eb81..8ed3fda 100755
--- a/themes/src/main/resources/theme/base/account/totp.ftl
+++ b/themes/src/main/resources/theme/base/account/totp.ftl
@@ -49,9 +49,13 @@
<p>${msg("totpManualStep3")}</p>
<ul>
<li id="kc-totp-type">${msg("totpType")}: ${msg("totp." + totp.policy.type)}</li>
- <li id="kc-totp-algorithm">${msg("totpAlgorithm")}: ${totp.policy.algorithm}</li>
+ <li id="kc-totp-algorithm">${msg("totpAlgorithm")}: ${totp.policy.getAlgorithmKey()}</li>
<li id="kc-totp-digits">${msg("totpDigits")}: ${totp.policy.digits}</li>
- <li id="kc-totp-period">${msg("totpInterval")}: ${totp.policy.period}</li>
+ <#if totp.policy.type = "totp">
+ <li id="kc-totp-period">${msg("totpInterval")}: ${totp.policy.period}</li>
+ <#elseif totp.policy.type = "hotp">
+ <li id="kc-totp-counter">${msg("totpCounter")}: ${totp.policy.initialCounter}</li>
+ </#if>
</ul>
</li>
<#else>
diff --git a/themes/src/main/resources/theme/base/login/login-config-totp.ftl b/themes/src/main/resources/theme/base/login/login-config-totp.ftl
index 24383ec..bd53b27 100755
--- a/themes/src/main/resources/theme/base/login/login-config-totp.ftl
+++ b/themes/src/main/resources/theme/base/login/login-config-totp.ftl
@@ -28,9 +28,13 @@
<p>${msg("loginTotpManualStep3")}</p>
<ul>
<li id="kc-totp-type">${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}</li>
- <li id="kc-totp-algorithm">${msg("loginTotpAlgorithm")}: ${totp.policy.algorithm}</li>
+ <li id="kc-totp-algorithm">${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}</li>
<li id="kc-totp-digits">${msg("loginTotpDigits")}: ${totp.policy.digits}</li>
- <li id="kc-totp-period">${msg("loginTotpInterval")}: ${totp.policy.period}</li>
+ <#if totp.policy.type = "totp">
+ <li id="kc-totp-period">${msg("loginTotpInterval")}: ${totp.policy.period}</li>
+ <#elseif totp.policy.type = "hotp">
+ <li id="kc-totp-counter">${msg("loginTotpCounter")}: ${totp.policy.initialCounter}</li>
+ </#if>
</ul>
</li>
<#else>
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 7c5a22d..e6eb261 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -80,6 +80,7 @@ loginTotpType=Type
loginTotpAlgorithm=Algorithm
loginTotpDigits=Digits
loginTotpInterval=Interval
+loginTotpCounter=Counter
loginTotp.totp=Time-based
loginTotp.hotp=Counter-based