Merge remote-tracking branch 'upstream/master' into client-storage-spi

This commit is contained in:
Bill Burke 2018-01-25 10:08:37 -05:00
commit ddad1cb8af
35 changed files with 1217 additions and 21 deletions

View file

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

View file

@ -345,6 +345,34 @@ public class ClientAdapter implements ClientModel {
return copy; 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 @Override
public Set<ProtocolMapperModel> getProtocolMappers() { public Set<ProtocolMapperModel> getProtocolMappers() {
if (isUpdated()) return updated.getProtocolMappers(); if (isUpdated()) return updated.getProtocolMappers();

View file

@ -46,6 +46,7 @@ public class CachedClient extends AbstractRevisioned implements InRealm {
protected String registrationToken; protected String registrationToken;
protected String protocol; protected String protocol;
protected Map<String, String> attributes = new HashMap<String, String>(); protected Map<String, String> attributes = new HashMap<String, String>();
protected Map<String, String> authFlowBindings = new HashMap<String, String>();
protected boolean publicClient; protected boolean publicClient;
protected boolean fullScopeAllowed; protected boolean fullScopeAllowed;
protected boolean frontchannelLogout; protected boolean frontchannelLogout;
@ -83,6 +84,7 @@ public class CachedClient extends AbstractRevisioned implements InRealm {
enabled = model.isEnabled(); enabled = model.isEnabled();
protocol = model.getProtocol(); protocol = model.getProtocol();
attributes.putAll(model.getAttributes()); attributes.putAll(model.getAttributes());
authFlowBindings.putAll(model.getAuthenticationFlowBindingOverrides());
notBefore = model.getNotBefore(); notBefore = model.getNotBefore();
frontchannelLogout = model.isFrontchannelLogout(); frontchannelLogout = model.isFrontchannelLogout();
publicClient = model.isPublicClient(); publicClient = model.isPublicClient();
@ -256,4 +258,8 @@ public class CachedClient extends AbstractRevisioned implements InRealm {
public boolean isUseTemplateMappers() { public boolean isUseTemplateMappers() {
return useTemplateMappers; return useTemplateMappers;
} }
public Map<String, String> getAuthFlowBindings() {
return authFlowBindings;
}
} }

View file

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

View file

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

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2017 Red Hat, Inc. and/or its affiliates
~ * and other contributors as indicated by the @author tags.
~ *
~ * Licensed under the Apache License, Version 2.0 (the "License");
~ * you may not use this file except in compliance with the License.
~ * You may obtain a copy of the License at
~ *
~ * http://www.apache.org/licenses/LICENSE-2.0
~ *
~ * Unless required by applicable law or agreed to in writing, software
~ * distributed under the License is distributed on an "AS IS" BASIS,
~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ * See the License for the specific language governing permissions and
~ * limitations under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="bburke@redhat.com" id="4.0.0-KEYCLOAK-6335">
<createTable tableName="CLIENT_AUTH_FLOW_BINDINGS">
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="FLOW_ID" type="VARCHAR(36)"/>
<column name="BINDING_NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey columnNames="CLIENT_ID, BINDING_NAME" constraintName="C_CLI_FLOW_BIND" tableName="CLIENT_AUTH_FLOW_BINDINGS"/>
</changeSet>
</databaseChangeLog>

View file

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

View file

@ -0,0 +1,57 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.utils;
import org.keycloak.models.AuthenticationFlowBindings;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ModelException;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticationFlowResolver {
public static AuthenticationFlowModel resolveBrowserFlow(AuthenticationSessionModel authSession) {
AuthenticationFlowModel flow = null;
ClientModel client = authSession.getClient();
String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING);
if (clientFlow != null) {
flow = authSession.getRealm().getAuthenticationFlowById(clientFlow);
if (flow == null) {
throw new ModelException("Client " + client.getClientId() + " has browser flow override, but this flow does not exist");
}
return flow;
}
return authSession.getRealm().getBrowserFlow();
}
public static AuthenticationFlowModel resolveDirectGrantFlow(AuthenticationSessionModel authSession) {
AuthenticationFlowModel flow = null;
ClientModel client = authSession.getClient();
String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING);
if (clientFlow != null) {
flow = authSession.getRealm().getAuthenticationFlowById(clientFlow);
if (flow == null) {
throw new ModelException("Client " + client.getClientId() + " has direct grant flow override, but this flow does not exist");
}
return flow;
}
return authSession.getRealm().getDirectGrantFlow();
}
}

View file

@ -494,6 +494,7 @@ public class ModelToRepresentation {
rep.setFrontchannelLogout(clientModel.isFrontchannelLogout()); rep.setFrontchannelLogout(clientModel.isFrontchannelLogout());
rep.setProtocol(clientModel.getProtocol()); rep.setProtocol(clientModel.getProtocol());
rep.setAttributes(clientModel.getAttributes()); rep.setAttributes(clientModel.getAttributes());
rep.setAuthenticationFlowBindingOverrides(clientModel.getAuthenticationFlowBindingOverrides());
rep.setFullScopeAllowed(clientModel.isFullScopeAllowed()); rep.setFullScopeAllowed(clientModel.isFullScopeAllowed());
rep.setBearerOnly(clientModel.isBearerOnly()); rep.setBearerOnly(clientModel.isBearerOnly());
rep.setConsentRequired(clientModel.isConsentRequired()); rep.setConsentRequired(clientModel.isConsentRequired());

View file

@ -1084,6 +1084,17 @@ public class RepresentationToModel {
} }
if (resourceRep.getAuthenticationFlowBindingOverrides() != null) {
for (Map.Entry<String, String> entry : resourceRep.getAuthenticationFlowBindingOverrides().entrySet()) {
if (entry.getValue() == null || entry.getValue().trim().equals("")) {
continue;
} else {
client.setAuthenticationFlowBindingOverride(entry.getKey(), entry.getValue());
}
}
}
if (resourceRep.getRedirectUris() != null) { if (resourceRep.getRedirectUris() != null) {
for (String redirectUri : resourceRep.getRedirectUris()) { for (String redirectUri : resourceRep.getRedirectUris()) {
client.addRedirectUri(redirectUri); client.addRedirectUri(redirectUri);
@ -1201,6 +1212,22 @@ public class RepresentationToModel {
resource.setAttribute(entry.getKey(), entry.getValue()); 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) { if (rep.getNotBefore() != null) {

View file

@ -0,0 +1,28 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models;
/**
* Defines constants for authentication flow bindings. Strings used for lookup
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface AuthenticationFlowBindings {
String BROWSER_BINDING = "browser";
String DIRECT_GRANT_BINDING = "direct_grant";
}

View file

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

View file

@ -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 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() { public String getType() {
return type; return type;
} }

View file

@ -38,6 +38,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
@ -646,7 +647,7 @@ public class AuthenticationProcessor {
AuthenticationProcessor processor = new AuthenticationProcessor(); AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setAuthenticationSession(clone) processor.setAuthenticationSession(clone)
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH) .setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(realm.getBrowserFlow().getId()) .setFlowId(AuthenticationFlowResolver.resolveBrowserFlow(clone).getId())
.setForwardedErrorMessage(reset.getErrorMessage()) .setForwardedErrorMessage(reset.getErrorMessage())
.setForwardedSuccessMessage(reset.getSuccessMessage()) .setForwardedSuccessMessage(reset.getSuccessMessage())
.setConnection(connection) .setConnection(connection)

View file

@ -39,6 +39,22 @@ import java.security.KeyStore;
import java.util.concurrent.TimeUnit; 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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class DefaultHttpClientFactory implements HttpClientFactory { public class DefaultHttpClientFactory implements HttpClientFactory {
@ -127,6 +143,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
String clientKeystore = config.get("client-keystore"); String clientKeystore = config.get("client-keystore");
String clientKeystorePassword = config.get("client-keystore-password"); String clientKeystorePassword = config.get("client-keystore-password");
String clientPrivateKeyPassword = config.get("client-key-password"); String clientPrivateKeyPassword = config.get("client-key-password");
String[] proxyMappings = config.getArray("proxy-mappings");
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class); TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
boolean disableTrustManager = truststoreProvider == null || truststoreProvider.getTruststore() == null; boolean disableTrustManager = truststoreProvider == null || truststoreProvider.getTruststore() == null;
@ -137,13 +154,15 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
: HttpClientBuilder.HostnameVerificationPolicy.valueOf(truststoreProvider.getPolicy().name()); : HttpClientBuilder.HostnameVerificationPolicy.valueOf(truststoreProvider.getPolicy().name());
HttpClientBuilder builder = new HttpClientBuilder(); HttpClientBuilder builder = new HttpClientBuilder();
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
.maxPooledPerRoute(maxPooledPerRoute) .maxPooledPerRoute(maxPooledPerRoute)
.connectionPoolSize(connectionPoolSize) .connectionPoolSize(connectionPoolSize)
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
.disableCookies(disableCookies); .disableCookies(disableCookies)
.proxyMappings(ProxyMappings.valueOf(proxyMappings));
if (disableTrustManager) { if (disableTrustManager) {
// TODO: is it ok to do away with disabling trust manager? // TODO: is it ok to do away with disabling trust manager?

View file

@ -52,7 +52,7 @@ import java.util.concurrent.TimeUnit;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class HttpClientBuilder { public class HttpClientBuilder {
public static enum HostnameVerificationPolicy { public enum HostnameVerificationPolicy {
/** /**
* Hostname verification is not done on the server's certificate * Hostname verification is not done on the server's certificate
*/ */
@ -104,7 +104,7 @@ public class HttpClientBuilder {
protected long establishConnectionTimeout = -1; protected long establishConnectionTimeout = -1;
protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS; protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
protected boolean disableCookies = false; protected boolean disableCookies = false;
protected ProxyMappings proxyMappings;
/** /**
* Socket inactivity timeout * Socket inactivity timeout
@ -208,6 +208,11 @@ public class HttpClientBuilder {
return this; return this;
} }
public HttpClientBuilder proxyMappings(ProxyMappings proxyMappings) {
this.proxyMappings = proxyMappings;
return this;
}
static class VerifierWrapper implements X509HostnameVerifier { static class VerifierWrapper implements X509HostnameVerifier {
protected HostnameVerifier verifier; protected HostnameVerifier verifier;
@ -272,6 +277,7 @@ public class HttpClientBuilder {
tlsContext.init(null, null, null); tlsContext.init(null, null, null);
sslsf = new SSLConnectionSocketFactory(tlsContext, verifier); sslsf = new SSLConnectionSocketFactory(tlsContext, verifier);
} }
RequestConfig requestConfig = RequestConfig.custom() RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout((int) establishConnectionTimeout) .setConnectTimeout((int) establishConnectionTimeout)
.setSocketTimeout((int) socketTimeout).build(); .setSocketTimeout((int) socketTimeout).build();
@ -283,6 +289,11 @@ public class HttpClientBuilder {
.setMaxConnPerRoute(maxPooledPerRoute) .setMaxConnPerRoute(maxPooledPerRoute)
.setConnectionTimeToLive(connectionTTL, connectionTTLUnit); .setConnectionTimeToLive(connectionTTL, connectionTTLUnit);
if (proxyMappings != null && !proxyMappings.isEmpty()) {
builder.setRoutePlanner(new ProxyMappingsAwareRoutePlanner(proxyMappings));
}
if (maxConnectionIdleTime > 0) { if (maxConnectionIdleTime > 0) {
// Will start background cleaner thread // Will start background cleaner thread
builder.evictIdleConnections(maxConnectionIdleTime, maxConnectionIdleTimeUnit); builder.evictIdleConnections(maxConnectionIdleTime, maxConnectionIdleTimeUnit);

View file

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

View file

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

View file

@ -30,6 +30,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.services.ErrorPageException; import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
@ -107,7 +108,7 @@ public abstract class AuthorizationEndpointBase {
* @return response to be returned to the browser * @return response to be returned to the browser
*/ */
protected Response handleBrowserAuthenticationRequest(AuthenticationSessionModel authSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) { protected Response handleBrowserAuthenticationRequest(AuthenticationSessionModel authSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) {
AuthenticationFlowModel flow = getAuthenticationFlow(); AuthenticationFlowModel flow = getAuthenticationFlow(authSession);
String flowId = flow.getId(); String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH); AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
event.detail(Details.CODE_ID, authSession.getParentSession().getId()); event.detail(Details.CODE_ID, authSession.getParentSession().getId());
@ -149,8 +150,8 @@ public abstract class AuthorizationEndpointBase {
} }
} }
protected AuthenticationFlowModel getAuthenticationFlow() { protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) {
return realm.getBrowserFlow(); return AuthenticationFlowResolver.resolveBrowserFlow(authSession);
} }
protected void checkSsl() { protected void checkSsl() {

View file

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

View file

@ -56,6 +56,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager; 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.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
AuthenticationFlowModel flow = realm.getDirectGrantFlow(); AuthenticationFlowModel flow = AuthenticationFlowResolver.resolveDirectGrantFlow(authSession);
String flowId = flow.getId(); String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor(); AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setAuthenticationSession(authSession) processor.setAuthenticationSession(authSession)

View file

@ -147,7 +147,7 @@ public class SamlEcpProfileService extends SamlService {
} }
@Override @Override
protected AuthenticationFlowModel getAuthenticationFlow() { protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) {
for (AuthenticationFlowModel flowModel : realm.getAuthenticationFlows()) { for (AuthenticationFlowModel flowModel : realm.getAuthenticationFlows()) {
if (flowModel.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) { if (flowModel.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) {
return flowModel; return flowModel;

View file

@ -55,6 +55,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.LoginProtocolFactory;
@ -1121,7 +1122,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
protected Response browserAuthentication(AuthenticationSessionModel authSession, String errorMessage) { protected Response browserAuthentication(AuthenticationSessionModel authSession, String errorMessage) {
this.event.event(EventType.LOGIN); this.event.event(EventType.LOGIN);
AuthenticationFlowModel flow = realmModel.getBrowserFlow(); AuthenticationFlowModel flow = AuthenticationFlowResolver.resolveBrowserFlow(authSession);
String flowId = flow.getId(); String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor(); AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setAuthenticationSession(authSession) processor.setAuthenticationSession(authSession)

View file

@ -53,6 +53,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.SystemClientUtil; import org.keycloak.models.utils.SystemClientUtil;
import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.AuthorizationEndpointBase;
@ -252,7 +253,7 @@ public class LoginActionsService {
} }
protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) { 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) { protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {

View file

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

View file

@ -0,0 +1,137 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.forms;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UsernameOnlyAuthenticator implements Authenticator, AuthenticatorFactory {
public static final String PROVIDER_ID = "testsuite-username";
@Override
public void authenticate(AuthenticationFlowContext context) {
String username = context.getHttpRequest().getDecodedFormParameters().getFirst("username");
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
if (user == null) {
context.failure(AuthenticationFlowError.UNKNOWN_USER);
return;
}
context.setUser(user);
context.success();
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void action(AuthenticationFlowContext context) {
}
@Override
public String getDisplayType() {
return "Testsuite Username Only";
}
@Override
public String getReferenceCategory() {
return null;
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
@Override
public String getHelpText() {
return "Testsuite Username authenticator. Username parameter sets username";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}
@Override
public void close() {
}
@Override
public Authenticator create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -19,4 +19,5 @@ org.keycloak.testsuite.forms.PassThroughAuthenticator
org.keycloak.testsuite.forms.PassThroughRegistration org.keycloak.testsuite.forms.PassThroughRegistration
org.keycloak.testsuite.forms.ClickThroughAuthenticator org.keycloak.testsuite.forms.ClickThroughAuthenticator
org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory
org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory
org.keycloak.testsuite.forms.UsernameOnlyAuthenticator

View file

@ -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}")); 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("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("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText()); assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());

View file

@ -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}")); 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("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("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText()); assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
@ -217,9 +217,9 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
totpPage.clickManual(); totpPage.clickManual();
assertEquals("Type: Counter-based", driver.findElement(By.id("kc-totp-type")).getText()); 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("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 { } finally {
rep.setOtpPolicyDigits(6); rep.setOtpPolicyDigits(6);
rep.setOtpPolicyType("totp"); rep.setOtpPolicyType("totp");

View file

@ -180,6 +180,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user"); "Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru", addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru",
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user"); "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; return result;
} }

View file

@ -0,0 +1,353 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.forms;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.events.Details;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowBindings;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.util.BasicAuthHelper;
import org.openqa.selenium.By;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.util.List;
import static org.junit.Assert.assertEquals;
/**
* Test that clients can override auth flows
*
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
public static final String TEST_APP_DIRECT_OVERRIDE = "test-app-direct-override";
public static final String TEST_APP_FLOW = "test-app-flow";
@Rule
public AssertEvents events = new AssertEvents(this);
@Page
protected AppPage appPage;
@Page
protected LoginPage loginPage;
@Page
protected ErrorPage errorPage;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Deployment
public static WebArchive deploy() {
return RunOnServerDeployment.create(UserResource.class)
.addPackages(true, "org.keycloak.testsuite");
}
@Before
public void setupFlows() {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
ClientModel client = session.realms().getClientByClientId("test-app-flow", realm);
if (client != null) {
return;
}
client = session.realms().getClientByClientId("test-app", realm);
client.setDirectAccessGrantsEnabled(true);
// Parent flow
AuthenticationFlowModel browser = new AuthenticationFlowModel();
browser.setAlias("parent-flow");
browser.setDescription("browser based authentication");
browser.setProviderId("basic-flow");
browser.setTopLevel(true);
browser.setBuiltIn(true);
browser = realm.addAuthenticationFlow(browser);
// Subflow2
AuthenticationFlowModel subflow2 = new AuthenticationFlowModel();
subflow2.setTopLevel(false);
subflow2.setBuiltIn(true);
subflow2.setAlias("subflow-2");
subflow2.setDescription("username+password AND pushButton");
subflow2.setProviderId("basic-flow");
subflow2 = realm.addAuthenticationFlow(subflow2);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(browser.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setFlowId(subflow2.getId());
execution.setPriority(20);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
// Subflow2 - push the button
execution = new AuthenticationExecutionModel();
execution.setParentFlow(subflow2.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator(PushButtonAuthenticatorFactory.PROVIDER_ID);
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
// Subflow2 - username-password
execution = new AuthenticationExecutionModel();
execution.setParentFlow(subflow2.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator(UsernamePasswordFormFactory.PROVIDER_ID);
execution.setPriority(20);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
client = realm.addClient(TEST_APP_FLOW);
client.setSecret("password");
client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin");
client.setEnabled(true);
client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*");
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId());
client.setPublicClient(false);
// Parent flow
AuthenticationFlowModel directGrant = new AuthenticationFlowModel();
directGrant.setAlias("direct-override-flow");
directGrant.setDescription("direct grant based authentication");
directGrant.setProviderId("basic-flow");
directGrant.setTopLevel(true);
directGrant.setBuiltIn(true);
directGrant = realm.addAuthenticationFlow(directGrant);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(directGrant.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator(UsernameOnlyAuthenticator.PROVIDER_ID);
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
client = realm.addClient(TEST_APP_DIRECT_OVERRIDE);
client.setSecret("password");
client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin");
client.setEnabled(true);
client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*");
client.setPublicClient(false);
client.setDirectAccessGrantsEnabled(true);
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId());
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, directGrant.getId());
});
}
//@Test
public void testRunConsole() throws Exception {
Thread.sleep(10000000);
}
@Test
public void testWithClientBrowserOverride() throws Exception {
oauth.clientId(TEST_APP_FLOW);
String loginFormUrl = oauth.getLoginFormUrl();
log.info("loginFormUrl: " + loginFormUrl);
//Thread.sleep(10000000);
driver.navigate().to(loginFormUrl);
Assert.assertEquals("PushTheButton", driver.getTitle());
// Push the button. I am redirected to username+password form
driver.findElement(By.name("submit1")).click();
loginPage.assertCurrent();
// Fill username+password. I am successfully authenticated
oauth.fillLoginForm("test-user@localhost", "password");
appPage.assertCurrent();
events.expectLogin().client("test-app-flow").detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
@Test
public void testNoOverrideBrowser() throws Exception {
String clientId = "test-app";
testNoOverrideBrowser(clientId);
}
private void testNoOverrideBrowser(String clientId) {
oauth.clientId(clientId);
String loginFormUrl = oauth.getLoginFormUrl();
log.info("loginFormUrl: " + loginFormUrl);
//Thread.sleep(10000000);
driver.navigate().to(loginFormUrl);
loginPage.assertCurrent();
// Fill username+password. I am successfully authenticated
oauth.fillLoginForm("test-user@localhost", "password");
appPage.assertCurrent();
events.expectLogin().client(clientId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
@Test
public void testGrantAccessTokenNoOverride() throws Exception {
testDirectGrantNoOverride("test-app");
}
private void testDirectGrantNoOverride(String clientId) {
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
{ // test no password
String header = BasicAuthHelper.createHeader(clientId, "password");
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("username", "test-user@localhost");
Response response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, header)
.post(Entity.form(form));
assertEquals(401, response.getStatus());
response.close();
}
{ // test invalid password
String header = BasicAuthHelper.createHeader(clientId, "password");
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("username", "test-user@localhost");
form.param("password", "invalid");
Response response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, header)
.post(Entity.form(form));
assertEquals(401, response.getStatus());
response.close();
}
{ // test valid password
String header = BasicAuthHelper.createHeader(clientId, "password");
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("username", "test-user@localhost");
form.param("password", "password");
Response response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, header)
.post(Entity.form(form));
assertEquals(200, response.getStatus());
response.close();
}
httpClient.close();
events.clear();
}
@Test
public void testGrantAccessTokenWithClientOverride() throws Exception {
String clientId = TEST_APP_DIRECT_OVERRIDE;
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
{ // test no password
String header = BasicAuthHelper.createHeader(clientId, "password");
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("username", "test-user@localhost");
Response response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, header)
.post(Entity.form(form));
assertEquals(200, response.getStatus());
response.close();
}
httpClient.close();
events.clear();
}
@Test
public void testRestInterface() throws Exception {
ClientsResource clients = adminClient.realm("test").clients();
List<ClientRepresentation> query = clients.findByClientId(TEST_APP_DIRECT_OVERRIDE);
ClientRepresentation clientRep = query.get(0);
String directGrantFlowId = clientRep.getAuthenticationFlowBindingOverrides().get(AuthenticationFlowBindings.DIRECT_GRANT_BINDING);
Assert.assertNotNull(directGrantFlowId);
clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, "");
clients.get(clientRep.getId()).update(clientRep);
testDirectGrantNoOverride(TEST_APP_DIRECT_OVERRIDE);
clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, directGrantFlowId);
clients.get(clientRep.getId()).update(clientRep);
testGrantAccessTokenWithClientOverride();
query = clients.findByClientId(TEST_APP_FLOW);
clientRep = query.get(0);
String browserFlowId = clientRep.getAuthenticationFlowBindingOverrides().get(AuthenticationFlowBindings.BROWSER_BINDING);
Assert.assertNotNull(browserFlowId);
clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.BROWSER_BINDING, "");
clients.get(clientRep.getId()).update(clientRep);
testNoOverrideBrowser(TEST_APP_FLOW);
clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.BROWSER_BINDING, browserFlowId);
clients.get(clientRep.getId()).update(clientRep);
testWithClientBrowserOverride();
}
}

View file

@ -114,6 +114,7 @@ totpType=Type
totpAlgorithm=Algorithm totpAlgorithm=Algorithm
totpDigits=Digits totpDigits=Digits
totpInterval=Interval totpInterval=Interval
totpCounter=Counter
missingUsernameMessage=Please specify username. missingUsernameMessage=Please specify username.
missingFirstNameMessage=Please specify first name. missingFirstNameMessage=Please specify first name.

View file

@ -49,9 +49,13 @@
<p>${msg("totpManualStep3")}</p> <p>${msg("totpManualStep3")}</p>
<ul> <ul>
<li id="kc-totp-type">${msg("totpType")}: ${msg("totp." + totp.policy.type)}</li> <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-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> </ul>
</li> </li>
<#else> <#else>

View file

@ -28,9 +28,13 @@
<p>${msg("loginTotpManualStep3")}</p> <p>${msg("loginTotpManualStep3")}</p>
<ul> <ul>
<li id="kc-totp-type">${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}</li> <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-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> </ul>
</li> </li>
<#else> <#else>

View file

@ -80,6 +80,7 @@ loginTotpType=Type
loginTotpAlgorithm=Algorithm loginTotpAlgorithm=Algorithm
loginTotpDigits=Digits loginTotpDigits=Digits
loginTotpInterval=Interval loginTotpInterval=Interval
loginTotpCounter=Counter
loginTotp.totp=Time-based loginTotp.totp=Time-based
loginTotp.hotp=Counter-based loginTotp.hotp=Counter-based