keycloak-aplcache
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java 1(+1 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java 31(+26 -5)
adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java 1(+1 -0)
adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java 84(+54 -30)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java 56(+56 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java 2(+2 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java 85(+85 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java 61(+61 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java 38(+38 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java 44(+44 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java 36(+36 -0)
adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties 8(+7 -1)
adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml 2(+2 -0)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java 5(+2 -3)
distribution/api-docs-dist/pom.xml 30(+23 -7)
distribution/server-dist/pom.xml 12(+12 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java 7(+7 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 6(+6 -0)
pom.xml 2(+1 -1)
server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java 25(+25 -0)
services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java 64(+57 -7)
services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java 37(+37 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java 62(+62 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java 70(+70 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java 35(+35 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java 148(+148 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java 81(+81 -0)
services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java 81(+81 -0)
services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java 52(+52 -0)
services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java 15(+15 -0)
services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java 51(+51 -0)
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java 209(+72 -137)
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java 115(+115 -0)
services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java 5(+4 -1)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java 19(+18 -1)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java 40(+34 -6)
services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java 2(+0 -2)
services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java 1(+1 -0)
services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory 3(+2 -1)
services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider 4(+3 -1)
services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java 193(+193 -0)
services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java 41(+41 -0)
testsuite/integration-arquillian/HOW-TO-RUN.md 106(+104 -2)
testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl 12(+11 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java 10(+10 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java 4(+4 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java 55(+55 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/SetSystemProperty.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java 4(+4 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java 9(+9 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java 9(+9 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java 8(+8 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java 122(+122 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java 89(+87 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java 27(+16 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java 38(+20 -18)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java 81(+70 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java 17(+17 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java 11(+7 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java 200(+200 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java 45(+45 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java 43(+43 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java 87(+87 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java 99(+99 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java 39(+37 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java 68(+68 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem 17(+17 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt 35(+35 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key 51(+51 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker 45(+45 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt 6(+6 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt 4(+4 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json 1315(+1315 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java 56(+56 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java 9(+4 -5)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java 16(+4 -12)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java 7(+4 -3)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java 5(+3 -2)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java 49(+49 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java 4(+1 -3)
themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html 14(+13 -1)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index 45c4557..d5761bc 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -91,6 +91,8 @@ public class KeycloakDeployment {
// https://tools.ietf.org/html/rfc7636
protected boolean pkce = false;
protected boolean ignoreOAuthQueryParameter;
+
+ protected Map<String, String> redirectRewriteRules;
public KeycloakDeployment() {
}
@@ -446,4 +448,14 @@ public class KeycloakDeployment {
public boolean isOAuthQueryParameterEnabled() {
return !this.ignoreOAuthQueryParameter;
}
+
+ public Map<String, String> getRedirectRewriteRules() {
+ return redirectRewriteRules;
+ }
+
+ public void setRewriteRedirectRules(Map<String, String> redirectRewriteRules) {
+ this.redirectRewriteRules = redirectRewriteRules;
+ }
+
+
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index eca6849..7fca1f1 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -116,6 +116,7 @@ public class KeycloakDeploymentBuilder {
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter());
+ deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules());
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
index fe8dfdc..ee3f214 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
@@ -25,7 +25,6 @@ import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.common.VerificationException;
-import org.keycloak.common.util.Encode;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.UriUtils;
import org.keycloak.constants.AdapterConstants;
@@ -38,7 +37,10 @@ import org.keycloak.representations.IDToken;
import org.keycloak.util.TokenUtil;
import java.io.IOException;
-import java.util.concurrent.atomic.AtomicLong;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+import java.util.logging.Level;
/**
@@ -141,6 +143,7 @@ public class OAuthRequestAuthenticator {
protected String getRedirectUri(String state) {
String url = getRequestUrl();
log.debugf("callback uri: %s", url);
+
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
int port = sslRedirectPort();
if (port < 0) {
@@ -170,7 +173,7 @@ public class OAuthRequestAuthenticator {
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
- .queryParam(OAuth2Constants.REDIRECT_URI, Encode.encodeQueryParamAsIs(url)) // Need to encode uri ourselves as queryParam() will not encode % characters.
+ .queryParam(OAuth2Constants.REDIRECT_URI, rewrittenRedirectUri(url))
.queryParam(OAuth2Constants.STATE, state)
.queryParam("login", "true");
if(loginHint != null && loginHint.length() > 0){
@@ -320,10 +323,11 @@ public class OAuthRequestAuthenticator {
AccessTokenResponse tokenResponse = null;
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
+
try {
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
- tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
+ tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, rewrittenRedirectUri(strippedOauthParametersRequestUri), httpSessionId);
} catch (ServerRequest.HttpFailure failure) {
log.error("failed to turn code into token");
log.error("status from server: " + failure.getStatus());
@@ -375,6 +379,23 @@ public class OAuthRequestAuthenticator {
.replaceQueryParam(OAuth2Constants.STATE, null);
return builder.build().toString();
}
-
+
+ private String rewrittenRedirectUri(String originalUri) {
+ Map<String, String> rewriteRules = deployment.getRedirectRewriteRules();
+ if(rewriteRules != null && !rewriteRules.isEmpty()) {
+ try {
+ URL url = new URL(originalUri);
+ Map.Entry<String, String> rule = rewriteRules.entrySet().iterator().next();
+ StringBuilder redirectUriBuilder = new StringBuilder(url.getProtocol());
+ redirectUriBuilder.append("://"+ url.getAuthority());
+ redirectUriBuilder.append(url.getPath().replaceFirst(rule.getKey(), rule.getValue()));
+ return redirectUriBuilder.toString();
+ } catch (MalformedURLException ex) {
+ log.error("Not a valid request url");
+ throw new RuntimeException(ex);
+ }
+ }
+ return originalUri;
+ }
}
diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
index cd191e2..af58b33 100644
--- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
+++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
@@ -75,6 +75,7 @@ public class KeycloakDeploymentBuilderTest {
assertEquals(10, deployment.getTokenMinimumTimeToLive());
assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
assertEquals(120, deployment.getPublicKeyCacheTtl());
+ assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$"));
}
@Test
diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak.json b/adapters/oidc/adapter-core/src/test/resources/keycloak.json
index 521b8a9..e1b8881 100644
--- a/adapters/oidc/adapter-core/src/test/resources/keycloak.json
+++ b/adapters/oidc/adapter-core/src/test/resources/keycloak.json
@@ -33,5 +33,8 @@
"token-minimum-time-to-live": 10,
"min-time-between-jwks-requests": 20,
"public-key-cache-ttl": 120,
- "ignore-oauth-query-parameter": true
+ "ignore-oauth-query-parameter": true,
+ "redirect-rewrite-rules" : {
+ "^/wsmaster/api/(.*)$" : "/api/$1"
+ }
}
\ No newline at end of file
diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
index 2763ff1..c51b9db 100755
--- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
+++ b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
@@ -54,72 +54,96 @@ import java.util.regex.Pattern;
*/
public class KeycloakOIDCFilter implements Filter {
+ private final static Logger log = Logger.getLogger("" + KeycloakOIDCFilter.class);
+
public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern";
+ public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver";
+
+ public static final String CONFIG_FILE_PARAM = "keycloak.config.file";
+
+ public static final String CONFIG_PATH_PARAM = "keycloak.config.path";
+
protected AdapterDeploymentContext deploymentContext;
+
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
+
protected NodesRegistrationManagement nodesRegistrationManagement;
+
protected Pattern skipPattern;
- private final static Logger log = Logger.getLogger(""+KeycloakOIDCFilter.class);
+ private final KeycloakConfigResolver definedconfigResolver;
+
+ /**
+ * Constructor that can be used to define a {@code KeycloakConfigResolver} that will be used at initialization to
+ * provide the {@code KeycloakDeployment}.
+ * @param definedconfigResolver the resolver
+ */
+ public KeycloakOIDCFilter(KeycloakConfigResolver definedconfigResolver) {
+ this.definedconfigResolver = definedconfigResolver;
+ }
+
+ public KeycloakOIDCFilter() {
+ this(null);
+ }
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
-
String skipPatternDefinition = filterConfig.getInitParameter(SKIP_PATTERN_PARAM);
if (skipPatternDefinition != null) {
skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL);
}
- String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver");
- if (configResolverClass != null) {
- try {
- KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
- deploymentContext = new AdapterDeploymentContext(configResolver);
- log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
- } catch (Exception ex) {
- log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
- deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
- }
+ if (definedconfigResolver != null) {
+ deploymentContext = new AdapterDeploymentContext(definedconfigResolver);
+ log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass());
} else {
- String fp = filterConfig.getInitParameter("keycloak.config.file");
- InputStream is = null;
- if (fp != null) {
+ String configResolverClass = filterConfig.getInitParameter(CONFIG_RESOLVER_PARAM);
+ if (configResolverClass != null) {
try {
- is = new FileInputStream(fp);
- } catch (FileNotFoundException e) {
- throw new RuntimeException(e);
+ KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
+ deploymentContext = new AdapterDeploymentContext(configResolver);
+ log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
+ } catch (Exception ex) {
+ log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
+ deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
}
} else {
- String path = "/WEB-INF/keycloak.json";
- String pathParam = filterConfig.getInitParameter("keycloak.config.path");
- if (pathParam != null) path = pathParam;
- is = filterConfig.getServletContext().getResourceAsStream(path);
+ String fp = filterConfig.getInitParameter(CONFIG_FILE_PARAM);
+ InputStream is = null;
+ if (fp != null) {
+ try {
+ is = new FileInputStream(fp);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ String path = "/WEB-INF/keycloak.json";
+ String pathParam = filterConfig.getInitParameter(CONFIG_PATH_PARAM);
+ if (pathParam != null) path = pathParam;
+ is = filterConfig.getServletContext().getResourceAsStream(path);
+ }
+ KeycloakDeployment kd = createKeycloakDeploymentFrom(is);
+ deploymentContext = new AdapterDeploymentContext(kd);
+ log.fine("Keycloak is using a per-deployment configuration.");
}
- KeycloakDeployment kd = createKeycloakDeploymentFrom(is);
- deploymentContext = new AdapterDeploymentContext(kd);
- log.fine("Keycloak is using a per-deployment configuration.");
}
filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
nodesRegistrationManagement = new NodesRegistrationManagement();
}
private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) {
-
if (is == null) {
log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
return new KeycloakDeployment();
}
-
return KeycloakDeploymentBuilder.build(is);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
-
log.fine("Keycloak OIDC Filter");
- //System.err.println("Keycloak OIDC Filter: " + ((HttpServletRequest)req).getRequestURL().toString());
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
@@ -201,7 +225,7 @@ public class KeycloakOIDCFilter implements Filter {
*
* @param request the request to check
* @return {@code true} if the request should not be handled,
- * {@code false} otherwise.
+ * {@code false} otherwise.
*/
private boolean shouldSkip(HttpServletRequest request) {
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java
index e96a5e5..5a71e61 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java
@@ -37,6 +37,8 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD
public final class KeycloakAdapterConfigService {
private static final String CREDENTIALS_JSON_NAME = "credentials";
+
+ private static final String REDIRECT_REWRITE_RULE_JSON_NAME = "redirect-rewrite-rule";
private static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService();
@@ -129,6 +131,56 @@ public final class KeycloakAdapterConfigService {
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
return deployment.get(CREDENTIALS_JSON_NAME);
}
+
+ public void addRedirectRewriteRule(ModelNode operation, ModelNode model) {
+ ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
+ if (!redirectRewritesRules.isDefined()) {
+ redirectRewritesRules = new ModelNode();
+ }
+
+ String redirectRewriteRuleName = redirectRewriteRule(operation);
+ if (!redirectRewriteRuleName.contains(".")) {
+ redirectRewritesRules.get(redirectRewriteRuleName).set(model.get("value").asString());
+ } else {
+ String[] parts = redirectRewriteRuleName.split("\\.");
+ String provider = parts[0];
+ String property = parts[1];
+ ModelNode redirectRewriteRule = redirectRewritesRules.get(provider);
+ if (!redirectRewriteRule.isDefined()) {
+ redirectRewriteRule = new ModelNode();
+ }
+ redirectRewriteRule.get(property).set(model.get("value").asString());
+ redirectRewritesRules.set(provider, redirectRewriteRule);
+ }
+
+ ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
+ deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME).set(redirectRewritesRules);
+ }
+
+ public void removeRedirectRewriteRule(ModelNode operation) {
+ ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
+ if (!redirectRewritesRules.isDefined()) {
+ throw new RuntimeException("Can not remove redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
+ }
+
+ String ruleName = credentialNameFromOp(operation);
+ redirectRewritesRules.remove(ruleName);
+ }
+
+ public void updateRedirectRewriteRule(ModelNode operation, String attrName, ModelNode resolvedValue) {
+ ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
+ if (!redirectRewritesRules.isDefined()) {
+ throw new RuntimeException("Can not update redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
+ }
+
+ String ruleName = credentialNameFromOp(operation);
+ redirectRewritesRules.get(ruleName).set(resolvedValue);
+ }
+
+ private ModelNode redirectRewriteRuleFromOp(ModelNode operation) {
+ ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
+ return deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME);
+ }
private String realmNameFromOp(ModelNode operation) {
return valueFromOpAddress(RealmDefinition.TAG_NAME, operation);
@@ -141,6 +193,10 @@ public final class KeycloakAdapterConfigService {
private String credentialNameFromOp(ModelNode operation) {
return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation);
}
+
+ private String redirectRewriteRule(ModelNode operation) {
+ return valueFromOpAddress(RedirecRewritetRuleDefinition.TAG_NAME, operation);
+ }
private String valueFromOpAddress(String addrElement, ModelNode operation) {
String deploymentName = getValueOfAddrElement(operation.get(ADDRESS), addrElement);
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java
index 541454a..d04e72d 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java
@@ -48,6 +48,7 @@ public class KeycloakExtension implements Extension {
static final RealmDefinition REALM_DEFINITION = new RealmDefinition();
static final SecureDeploymentDefinition SECURE_DEPLOYMENT_DEFINITION = new SecureDeploymentDefinition();
static final CredentialDefinition CREDENTIAL_DEFINITION = new CredentialDefinition();
+ static final RedirecRewritetRuleDefinition REDIRECT_RULE_DEFINITON = new RedirecRewritetRuleDefinition();
public static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) {
StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME);
@@ -77,6 +78,7 @@ public class KeycloakExtension implements Extension {
registration.registerSubModel(REALM_DEFINITION);
ManagementResourceRegistration secureDeploymentRegistration = registration.registerSubModel(SECURE_DEPLOYMENT_DEFINITION);
secureDeploymentRegistration.registerSubModel(CREDENTIAL_DEFINITION);
+ secureDeploymentRegistration.registerSubModel(REDIRECT_RULE_DEFINITON);
subsystem.registerXMLElementWriter(PARSER);
}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java
index d4ddc02..79555e3 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java
@@ -96,12 +96,17 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
PathElement.pathElement(SecureDeploymentDefinition.TAG_NAME, name));
addSecureDeployment.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
List<ModelNode> credentialsToAdd = new ArrayList<ModelNode>();
+ List<ModelNode> redirectRulesToAdd = new ArrayList<ModelNode>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName();
if (tagName.equals(CredentialDefinition.TAG_NAME)) {
readCredential(reader, addr, credentialsToAdd);
continue;
}
+ if (tagName.equals(RedirecRewritetRuleDefinition.TAG_NAME)) {
+ readRewriteRule(reader, addr, redirectRulesToAdd);
+ continue;
+ }
SimpleAttributeDefinition def = SecureDeploymentDefinition.lookup(tagName);
if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + tagName);
@@ -111,6 +116,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
// Must add credentials after the deployment is added.
resourcesToAdd.add(addSecureDeployment);
resourcesToAdd.addAll(credentialsToAdd);
+ resourcesToAdd.addAll(redirectRulesToAdd);
}
public void readCredential(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> credentialsToAdd) throws XMLStreamException {
@@ -149,6 +155,43 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
}
}
}
+
+ public void readRewriteRule(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> rewriteRuleToToAdd) throws XMLStreamException {
+ String name = readNameAttribute(reader);
+
+ Map<String, String> values = new HashMap<>();
+ String textValue = null;
+ while (reader.hasNext()) {
+ int next = reader.next();
+ if (next == CHARACTERS) {
+ // text value of redirect rule element
+ String text = reader.getText();
+ if (text == null || text.trim().isEmpty()) {
+ continue;
+ }
+ textValue = text;
+ } else if (next == START_ELEMENT) {
+ String key = reader.getLocalName();
+ reader.next();
+ String value = reader.getText();
+ reader.next();
+
+ values.put(key, value);
+ } else if (next == END_ELEMENT) {
+ break;
+ }
+ }
+
+ if (textValue != null) {
+ ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name, textValue);
+ rewriteRuleToToAdd.add(addRedirectRule);
+ } else {
+ for (Map.Entry<String, String> entry : values.entrySet()) {
+ ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name + "." + entry.getKey(), entry.getValue());
+ rewriteRuleToToAdd.add(addRedirectRule);
+ }
+ }
+ }
private ModelNode getCredentialToAdd(PathAddress parent, String name, String value) {
ModelNode addCredential = new ModelNode();
@@ -158,6 +201,15 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
addCredential.get(CredentialDefinition.VALUE.getName()).set(value);
return addCredential;
}
+
+ private ModelNode getRedirectRuleToAdd(PathAddress parent, String name, String value) {
+ ModelNode addRedirectRule = new ModelNode();
+ addRedirectRule.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD);
+ PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(RedirecRewritetRuleDefinition.TAG_NAME, name));
+ addRedirectRule.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
+ addRedirectRule.get(RedirecRewritetRuleDefinition.VALUE.getName()).set(value);
+ return addRedirectRule;
+ }
// expects that the current tag will have one single attribute called "name"
private String readNameAttribute(XMLExtendedStreamReader reader) throws XMLStreamException {
@@ -219,6 +271,11 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
if (credentials.isDefined()) {
writeCredentials(writer, credentials);
}
+
+ ModelNode redirectRewriteRule = deploymentElements.get(RedirecRewritetRuleDefinition.TAG_NAME);
+ if (redirectRewriteRule.isDefined()) {
+ writeRedirectRules(writer, redirectRewriteRule);
+ }
writer.writeEndElement();
}
@@ -265,6 +322,34 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writer.writeEndElement();
}
}
+
+ private void writeRedirectRules(XMLExtendedStreamWriter writer, ModelNode redirectRules) throws XMLStreamException {
+ Map<String, Object> parsed = new LinkedHashMap<>();
+ for (Property redirectRule : redirectRules.asPropertyList()) {
+ String ruleName = redirectRule.getName();
+ String ruleValue = redirectRule.getValue().get(RedirecRewritetRuleDefinition.VALUE.getName()).asString();
+ parsed.put(ruleName, ruleValue);
+ }
+
+ for (Map.Entry<String, Object> entry : parsed.entrySet()) {
+ writer.writeStartElement(RedirecRewritetRuleDefinition.TAG_NAME);
+ writer.writeAttribute("name", entry.getKey());
+
+ Object value = entry.getValue();
+ if (value instanceof String) {
+ writeCharacters(writer, (String) value);
+ } else {
+ Map<String, String> redirectRulesProps = (Map<String, String>) value;
+ for (Map.Entry<String, String> prop : redirectRulesProps.entrySet()) {
+ writer.writeStartElement(prop.getKey());
+ writeCharacters(writer, prop.getValue());
+ writer.writeEndElement();
+ }
+ }
+
+ writer.writeEndElement();
+ }
+ }
// code taken from org.jboss.as.controller.AttributeMarshaller
private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException {
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java
new file mode 100644
index 0000000..a9095c7
--- /dev/null
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java
@@ -0,0 +1,61 @@
+/*
+ * 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.subsystem.adapter.extension;
+
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.operations.validation.StringLengthValidator;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+import org.jboss.dmr.ModelType;
+
+/**
+ *
+ * @author sblanc
+ */
+public class RedirecRewritetRuleDefinition extends SimpleResourceDefinition {
+
+ public static final String TAG_NAME = "redirect-rewrite-rule";
+
+ protected static final AttributeDefinition VALUE =
+ new SimpleAttributeDefinitionBuilder("value", ModelType.STRING, false)
+ .setAllowExpression(true)
+ .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true))
+ .build();
+
+ public RedirecRewritetRuleDefinition() {
+ super(PathElement.pathElement(TAG_NAME),
+ KeycloakExtension.getResourceDescriptionResolver(TAG_NAME),
+ new RedirectRewriteRuleAddHandler(VALUE),
+ RedirectRewriteRuleRemoveHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
+ super.registerAttributes(resourceRegistration);
+ resourceRegistration.registerReadWriteAttribute(VALUE, null, new RedirectRewriteRuleReadWriteAttributeHandler());
+ }
+}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java
new file mode 100644
index 0000000..2fc25f7
--- /dev/null
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java
@@ -0,0 +1,38 @@
+/*
+ * 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.subsystem.adapter.extension;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+public class RedirectRewriteRuleAddHandler extends AbstractAddStepHandler {
+
+ public RedirectRewriteRuleAddHandler(AttributeDefinition... attributes) {
+ super(attributes);
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
+ KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
+ ckService.addRedirectRewriteRule(operation, context.resolveExpressions(model));
+ }
+
+}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java
new file mode 100644
index 0000000..171e755
--- /dev/null
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java
@@ -0,0 +1,44 @@
+/*
+ * 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.subsystem.adapter.extension;
+
+import org.jboss.as.controller.AbstractWriteAttributeHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+public class RedirectRewriteRuleReadWriteAttributeHandler extends AbstractWriteAttributeHandler<KeycloakAdapterConfigService> {
+
+ @Override
+ protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName,
+ ModelNode resolvedValue, ModelNode currentValue, AbstractWriteAttributeHandler.HandbackHolder<KeycloakAdapterConfigService> hh) throws OperationFailedException {
+
+ KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
+ ckService.updateRedirectRewriteRule(operation, attributeName, resolvedValue);
+
+ hh.setHandback(ckService);
+
+ return false;
+ }
+
+ @Override
+ protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName,
+ ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException {
+ ckService.updateRedirectRewriteRule(operation, attributeName, valueToRestore);
+ }
+
+}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java
new file mode 100644
index 0000000..de17c96
--- /dev/null
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java
@@ -0,0 +1,36 @@
+/*
+ * 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.subsystem.adapter.extension;
+
+import org.jboss.as.controller.AbstractRemoveStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+public class RedirectRewriteRuleRemoveHandler extends AbstractRemoveStepHandler {
+
+ public static RedirectRewriteRuleRemoveHandler INSTANCE = new RedirectRewriteRuleRemoveHandler();
+
+ private RedirectRewriteRuleRemoveHandler() {}
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
+ KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
+ ckService.removeRedirectRewriteRule(operation);
+ }
+
+}
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
index 1df5979..c9cea77 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties
@@ -65,6 +65,7 @@ keycloak.secure-deployment.connection-pool-size=Connection pool size for the cli
keycloak.secure-deployment.resource=Application name
keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token
keycloak.secure-deployment.credentials=Adapter credentials
+keycloak.secure-deployment.redirect-rewrite-rule=Apply a rewrite rule for the redirect URI
keycloak.secure-deployment.bearer-only=Bearer Token Auth only
keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication
keycloak.secure-deployment.public-client=Public client
@@ -94,4 +95,9 @@ keycloak.secure-deployment.credential=Credential value
keycloak.credential=Credential
keycloak.credential.value=Credential value
keycloak.credential.add=Credential add
-keycloak.credential.remove=Credential remove
\ No newline at end of file
+keycloak.credential.remove=Credential remove
+
+keycloak.redirect-rewrite-rule=redirect-rewrite-rule
+keycloak.redirect-rewrite-rule.value=redirect-rewrite-rule value
+keycloak.redirect-rewrite-rule.add=redirect-rewrite-rule add
+keycloak.redirect-rewrite-rule.remove=redirect-rewrite-rule remove
\ No newline at end of file
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
index 604e6ac..d8f5bc3 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd
@@ -101,6 +101,7 @@
<xs:element name="ssl-required" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="realm-public-key" type="xs:string" minOccurs="1" maxOccurs="1"/>
<xs:element name="credential" type="credential-type" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="redirect-rewrite-rule" type="redirect-rewrite-rule-type" minOccurs="1" maxOccurs="1"/>
<xs:element name="auth-server-url-for-backend-requests" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="always-refresh-token" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="register-node-at-startup" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
@@ -127,4 +128,10 @@
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required" />
</xs:complexType>
+ <xs:complexType name="redirect-rewrite-rule-type" mixed="true">
+ <xs:sequence maxOccurs="unbounded" minOccurs="0">
+ <xs:any processContents="lax"></xs:any>
+ </xs:sequence>
+ <xs:attribute name="name" type="xs:string" use="required" />
+ </xs:complexType>
</xs:schema>
diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
index 3dcb61d..246d768 100755
--- a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
+++ b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml
@@ -53,6 +53,7 @@
<auth-server-url>http://localhost:8080/auth</auth-server-url>
<ssl-required>EXTERNAL</ssl-required>
<credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential>
+ <redirect-rewrite-rule name="^/wsmaster/api/(.*)$">api/$1/</redirect-rewrite-rule>
</secure-deployment>
<secure-deployment name="http-endpoint">
<realm>master</realm>
@@ -66,5 +67,6 @@
<credential name="jwt">
<client-keystore-file>/tmp/keystore.jks</client-keystore-file>
</credential>
+ <redirect-rewrite-rule name="^/wsmaster/api/(.*)$">/api/$1/</redirect-rewrite-rule>
</secure-deployment>
</subsystem>
\ No newline at end of file
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java
index 0858675..b8d5d66 100644
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java
@@ -34,6 +34,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ParsingException;
+import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.core.util.NamespaceContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -65,9 +66,7 @@ public class SamlDescriptorIDPKeysExtractor {
MultivaluedHashMap<String, KeyInfo> res = new MultivaluedHashMap<>();
try {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
- DocumentBuilder builder = factory.newDocumentBuilder();
+ DocumentBuilder builder = DocumentUtil.getDocumentBuilder();
Document doc = builder.parse(stream);
XPathExpression expr = xpath.compile("/m:EntitiesDescriptor/m:EntityDescriptor/m:IDPSSODescriptor/m:KeyDescriptor");
diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java
index 91b0a80..7f97e55 100755
--- a/common/src/main/java/org/keycloak/common/Profile.java
+++ b/common/src/main/java/org/keycloak/common/Profile.java
@@ -35,13 +35,13 @@ import java.util.Set;
public class Profile {
public enum Feature {
- AUTHORIZATION, IMPERSONATION, SCRIPTS
+ AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER
}
private enum ProfileValue {
- PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS),
+ PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER),
PREVIEW,
- COMMUNITY;
+ COMMUNITY(Feature.DOCKER);
private List<Feature> disabled;
diff --git a/common/src/main/java/org/keycloak/common/util/Encode.java b/common/src/main/java/org/keycloak/common/util/Encode.java
index 63b8f36..b195362 100755
--- a/common/src/main/java/org/keycloak/common/util/Encode.java
+++ b/common/src/main/java/org/keycloak/common/util/Encode.java
@@ -24,6 +24,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -36,7 +37,7 @@ import java.util.regex.Pattern;
*/
public class Encode
{
- private static final String UTF_8 = "UTF-8";
+ private static final String UTF_8 = StandardCharsets.UTF_8.name();
private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter");
@@ -84,9 +85,7 @@ public class Encode
case '@':
continue;
}
- StringBuffer sb = new StringBuffer();
- sb.append((char) i);
- pathEncoding[i] = URLEncoder.encode(sb.toString());
+ pathEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
pathEncoding[' '] = "%20";
System.arraycopy(pathEncoding, 0, matrixParameterEncoding, 0, pathEncoding.length);
@@ -119,9 +118,7 @@ public class Encode
queryNameValueEncoding[i] = "+";
continue;
}
- StringBuffer sb = new StringBuffer();
- sb.append((char) i);
- queryNameValueEncoding[i] = URLEncoder.encode(sb.toString());
+ queryNameValueEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
/*
@@ -159,9 +156,7 @@ public class Encode
queryStringEncoding[i] = "%20";
continue;
}
- StringBuffer sb = new StringBuffer();
- sb.append((char) i);
- queryStringEncoding[i] = URLEncoder.encode(sb.toString());
+ queryStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
}
@@ -194,7 +189,7 @@ public class Encode
*/
public static String encodeFragment(String value)
{
- return encodeValue(value, queryNameValueEncoding);
+ return encodeValue(value, queryStringEncoding);
}
/**
@@ -221,18 +216,19 @@ public class Encode
public static String decodePath(String path)
{
Matcher matcher = encodedCharsMulti.matcher(path);
- StringBuffer buf = new StringBuffer();
+ int start=0;
+ StringBuilder builder = new StringBuilder();
CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
while (matcher.find())
{
+ builder.append(path, start, matcher.start());
decoder.reset();
String decoded = decodeBytes(matcher.group(1), decoder);
- decoded = decoded.replace("\\", "\\\\");
- decoded = decoded.replace("$", "\\$");
- matcher.appendReplacement(buf, decoded);
+ builder.append(decoded);
+ start = matcher.end();
}
- matcher.appendTail(buf);
- return buf.toString();
+ builder.append(path, start, path.length());
+ return builder.toString();
}
private static String decodeBytes(String enc, CharsetDecoder decoder)
@@ -264,7 +260,7 @@ public class Encode
public static String encodeNonCodes(String string)
{
Matcher matcher = nonCodes.matcher(string);
- StringBuffer buf = new StringBuffer();
+ StringBuilder builder = new StringBuilder();
// FYI: we do not use the no-arg matcher.find()
@@ -276,29 +272,32 @@ public class Encode
while (matcher.find(idx))
{
int start = matcher.start();
- buf.append(string.substring(idx, start));
- buf.append("%25");
+ builder.append(string.substring(idx, start));
+ builder.append("%25");
idx = start + 1;
}
- buf.append(string.substring(idx));
- return buf.toString();
+ builder.append(string.substring(idx));
+ return builder.toString();
}
- private static boolean savePathParams(String segment, StringBuffer newSegment, List<String> params)
+ public static boolean savePathParams(String segment, StringBuilder newSegment, List<String> params)
{
boolean foundParam = false;
// Regular expressions can have '{' and '}' characters. Replace them to do match
segment = PathHelper.replaceEnclosedCurlyBraces(segment);
Matcher matcher = PathHelper.URI_TEMPLATE_PATTERN.matcher(segment);
+ int start = 0;
while (matcher.find())
{
+ newSegment.append(segment, start, matcher.start());
foundParam = true;
String group = matcher.group();
// Regular expressions can have '{' and '}' characters. Recover earlier replacement
params.add(PathHelper.recoverEnclosedCurlyBraces(group));
- matcher.appendReplacement(newSegment, "_resteasy_uri_parameter");
+ newSegment.append("_resteasy_uri_parameter");
+ start = matcher.end();
}
- matcher.appendTail(newSegment);
+ newSegment.append(segment, start, segment.length());
return foundParam;
}
@@ -309,11 +308,11 @@ public class Encode
* @param encoding
* @return
*/
- private static String encodeValue(String segment, String[] encoding)
+ public static String encodeValue(String segment, String[] encoding)
{
ArrayList<String> params = new ArrayList<String>();
boolean foundParam = false;
- StringBuffer newSegment = new StringBuffer();
+ StringBuilder newSegment = new StringBuilder();
if (savePathParams(segment, newSegment, params))
{
foundParam = true;
@@ -411,21 +410,21 @@ public class Encode
return encodeFromArray(nameOrValue, queryNameValueEncoding, true);
}
- private static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
+ protected static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
{
- StringBuffer result = new StringBuffer();
+ StringBuilder result = new StringBuilder();
for (int i = 0; i < segment.length(); i++)
{
- if (!encodePercent && segment.charAt(i) == '%')
+ char currentChar = segment.charAt(i);
+ if (!encodePercent && currentChar == '%')
{
- result.append(segment.charAt(i));
+ result.append(currentChar);
continue;
}
- int idx = segment.charAt(i);
- String encoding = encode(idx, encodingMap);
+ String encoding = encode(currentChar, encodingMap);
if (encoding == null)
{
- result.append(segment.charAt(i));
+ result.append(currentChar);
}
else
{
@@ -461,20 +460,20 @@ public class Encode
return encoded;
}
- private static String pathParamReplacement(String segment, List<String> params)
+ public static String pathParamReplacement(String segment, List<String> params)
{
- StringBuffer newSegment = new StringBuffer();
+ StringBuilder newSegment = new StringBuilder();
Matcher matcher = PARAM_REPLACEMENT.matcher(segment);
int i = 0;
+ int start = 0;
while (matcher.find())
{
+ newSegment.append(segment, start, matcher.start());
String replacement = params.get(i++);
- // double encode slashes, so that slashes stay where they are
- replacement = replacement.replace("\\", "\\\\");
- replacement = replacement.replace("$", "\\$");
- matcher.appendReplacement(newSegment, replacement);
+ newSegment.append(replacement);
+ start = matcher.end();
}
- matcher.appendTail(newSegment);
+ newSegment.append(segment, start, segment.length());
segment = newSegment.toString();
return segment;
}
@@ -505,6 +504,38 @@ public class Encode
}
return decoded;
}
+
+ /**
+ * decode an encoded map
+ *
+ * @param map
+ * @param charset
+ * @return
+ */
+ public static MultivaluedHashMap<String, String> decode(MultivaluedHashMap<String, String> map, String charset)
+ {
+ if (charset == null)
+ {
+ charset = UTF_8;
+ }
+ MultivaluedHashMap<String, String> decoded = new MultivaluedHashMap<String, String>();
+ for (Map.Entry<String, List<String>> entry : map.entrySet())
+ {
+ List<String> values = entry.getValue();
+ for (String value : values)
+ {
+ try
+ {
+ decoded.add(URLDecoder.decode(entry.getKey(), charset), URLDecoder.decode(value, charset));
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ return decoded;
+ }
public static MultivaluedHashMap<String, String> encode(MultivaluedHashMap<String, String> map)
{
diff --git a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
index f064163..a03c53c 100755
--- a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
+++ b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
@@ -614,7 +614,7 @@ public class KeycloakUriBuilder {
if (value == null) throw new IllegalArgumentException("A passed in value was null");
if (query == null) query = "";
else query += "&";
- query += Encode.encodeQueryParam(name) + "=" + Encode.encodeQueryParam(value.toString());
+ query += Encode.encodeQueryParamAsIs(name) + "=" + Encode.encodeQueryParamAsIs(value.toString());
}
return this;
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
index 4a2b7e2..ebd49ab 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
@@ -62,7 +62,8 @@ public class BaseAdapterConfig extends BaseRealmConfig {
protected boolean publicClient;
@JsonProperty("credentials")
protected Map<String, Object> credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
+ @JsonProperty("redirect-rewrite-rules")
+ protected Map<String, String> redirectRewriteRules;
public boolean isUseResourceRoleMappings() {
return useResourceRoleMappings;
@@ -167,4 +168,14 @@ public class BaseAdapterConfig extends BaseRealmConfig {
public void setPublicClient(boolean publicClient) {
this.publicClient = publicClient;
}
+
+ public Map<String, String> getRedirectRewriteRules() {
+ return redirectRewriteRules;
+ }
+
+ public void setRedirectRewriteRules(Map<String, String> redirectRewriteRules) {
+ this.redirectRewriteRules = redirectRewriteRules;
+ }
+
+
}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java b/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java
new file mode 100644
index 0000000..969bcb0
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java
@@ -0,0 +1,119 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+
+/**
+ * Per the docker auth v2 spec, access is defined like this:
+ *
+ * {
+ * "type": "repository",
+ * "name": "samalba/my-app",
+ * "actions": [
+ * "push",
+ * "pull"
+ * ]
+ * }
+ *
+ */
+public class DockerAccess {
+
+ public static final int ACCESS_TYPE = 0;
+ public static final int REPOSITORY_NAME = 1;
+ public static final int PERMISSIONS = 2;
+ public static final String DECODE_ENCODING = "UTF-8";
+
+ @JsonProperty("type")
+ protected String type;
+ @JsonProperty("name")
+ protected String name;
+ @JsonProperty("actions")
+ protected List<String> actions;
+
+ public DockerAccess() {
+ }
+
+ public DockerAccess(final String scopeParam) {
+ if (scopeParam != null) {
+ try {
+ final String unencoded = URLDecoder.decode(scopeParam, DECODE_ENCODING);
+ final String[] parts = unencoded.split(":");
+ if (parts.length != 3) {
+ throw new IllegalArgumentException(String.format("Expecting input string to have %d parts delineated by a ':' character. " +
+ "Found %d parts: %s", 3, parts.length, unencoded));
+ }
+
+ type = parts[ACCESS_TYPE];
+ name = parts[REPOSITORY_NAME];
+ if (parts[PERMISSIONS] != null) {
+ actions = Arrays.asList(parts[PERMISSIONS].split(","));
+ }
+ } catch (final UnsupportedEncodingException e) {
+ throw new IllegalStateException("Error attempting to decode scope parameter using encoding: " + DECODE_ENCODING);
+ }
+ }
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public DockerAccess setType(final String type) {
+ this.type = type;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public DockerAccess setName(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public List<String> getActions() {
+ return actions;
+ }
+
+ public DockerAccess setActions(final List<String> actions) {
+ this.actions = actions;
+ return this;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerAccess)) return false;
+
+ final DockerAccess that = (DockerAccess) o;
+
+ if (type != null ? !type.equals(that.type) : that.type != null) return false;
+ if (name != null ? !name.equals(that.name) : that.name != null) return false;
+ return actions != null ? actions.equals(that.actions) : that.actions == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = type != null ? type.hashCode() : 0;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (actions != null ? actions.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerAccess{" +
+ "type='" + type + '\'' +
+ ", name='" + name + '\'' +
+ ", actions=" + actions +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerError.java b/core/src/main/java/org/keycloak/representations/docker/DockerError.java
new file mode 100644
index 0000000..b33bb58
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerError.java
@@ -0,0 +1,84 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * JSON Representation of a Docker Error in the following format:
+ *
+ *
+ * {
+ * "code": "UNAUTHORIZED",
+ * "message": "access to the requested resource is not authorized",
+ * "detail": [
+ * {
+ * "Type": "repository",
+ * "Name": "samalba/my-app",
+ * "Action": "pull"
+ * },
+ * {
+ * "Type": "repository",
+ * "Name": "samalba/my-app",
+ * "Action": "push"
+ * }
+ * ]
+ * }
+ */
+public class DockerError {
+
+
+ @JsonProperty("code")
+ private final String errorCode;
+ @JsonProperty("message")
+ private final String message;
+ @JsonProperty("detail")
+ private final List<DockerAccess> dockerErrorDetails;
+
+ public DockerError(final String errorCode, final String message, final List<DockerAccess> dockerErrorDetails) {
+ this.errorCode = errorCode;
+ this.message = message;
+ this.dockerErrorDetails = dockerErrorDetails;
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public List<DockerAccess> getDockerErrorDetails() {
+ return dockerErrorDetails;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerError)) return false;
+
+ final DockerError that = (DockerError) o;
+
+ if (errorCode != that.errorCode) return false;
+ if (message != null ? !message.equals(that.message) : that.message != null) return false;
+ return dockerErrorDetails != null ? dockerErrorDetails.equals(that.dockerErrorDetails) : that.dockerErrorDetails == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = errorCode != null ? errorCode.hashCode() : 0;
+ result = 31 * result + (message != null ? message.hashCode() : 0);
+ result = 31 * result + (dockerErrorDetails != null ? dockerErrorDetails.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerError{" +
+ "errorCode=" + errorCode +
+ ", message='" + message + '\'' +
+ ", dockerErrorDetails=" + dockerErrorDetails +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java b/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java
new file mode 100644
index 0000000..3d961ce
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java
@@ -0,0 +1,38 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public class DockerErrorResponseToken {
+
+
+ @JsonProperty("errors")
+ private final List<DockerError> errorList;
+
+ public DockerErrorResponseToken(final List<DockerError> errorList) {
+ this.errorList = errorList;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerErrorResponseToken)) return false;
+
+ final DockerErrorResponseToken that = (DockerErrorResponseToken) o;
+
+ return errorList != null ? errorList.equals(that.errorList) : that.errorList == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return errorList != null ? errorList.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerErrorResponseToken{" +
+ "errorList=" + errorList +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java b/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java
new file mode 100644
index 0000000..98074fa
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java
@@ -0,0 +1,88 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Creates a response understandable by the docker client in the form:
+ *
+ {
+ "token" : "eyJh...nSQ",
+ "expires_in" : 300,
+ "issued_at" : "2016-09-02T10:56:33Z"
+ }
+ */
+public class DockerResponse {
+
+ @JsonProperty("token")
+ private String token;
+ @JsonProperty("expires_in")
+ private Integer expires_in;
+ @JsonProperty("issued_at")
+ private String issued_at;
+
+ public DockerResponse() {
+ }
+
+ public DockerResponse(final String token, final Integer expires_in, final String issued_at) {
+ this.token = token;
+ this.expires_in = expires_in;
+ this.issued_at = issued_at;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public DockerResponse setToken(final String token) {
+ this.token = token;
+ return this;
+ }
+
+ public Integer getExpires_in() {
+ return expires_in;
+ }
+
+ public DockerResponse setExpires_in(final Integer expires_in) {
+ this.expires_in = expires_in;
+ return this;
+ }
+
+ public String getIssued_at() {
+ return issued_at;
+ }
+
+ public DockerResponse setIssued_at(final String issued_at) {
+ this.issued_at = issued_at;
+ return this;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerResponse)) return false;
+
+ final DockerResponse that = (DockerResponse) o;
+
+ if (token != null ? !token.equals(that.token) : that.token != null) return false;
+ if (expires_in != null ? !expires_in.equals(that.expires_in) : that.expires_in != null) return false;
+ return issued_at != null ? issued_at.equals(that.issued_at) : that.issued_at == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = token != null ? token.hashCode() : 0;
+ result = 31 * result + (expires_in != null ? expires_in.hashCode() : 0);
+ result = 31 * result + (issued_at != null ? issued_at.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerResponse{" +
+ "token='" + token + '\'' +
+ ", expires_in='" + expires_in + '\'' +
+ ", issued_at='" + issued_at + '\'' +
+ '}';
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java b/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java
new file mode 100644
index 0000000..faee452
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java
@@ -0,0 +1,97 @@
+package org.keycloak.representations.docker;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.representations.JsonWebToken;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * * {
+ * "iss": "auth.docker.com",
+ * "sub": "jlhawn",
+ * "aud": "registry.docker.com",
+ * "exp": 1415387315,
+ * "nbf": 1415387015,
+ * "iat": 1415387015,
+ * "jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws",
+ * "access": [
+ * {
+ * "type": "repository",
+ * "name": "samalba/my-app",
+ * "actions": [
+ * "push"
+ * ]
+ * }
+ * ]
+ * }
+ */
+public class DockerResponseToken extends JsonWebToken {
+
+ @JsonProperty("access")
+ protected List<DockerAccess> accessItems = new ArrayList<>();
+
+ public List<DockerAccess> getAccessItems() {
+ return accessItems;
+ }
+
+ @Override
+ public DockerResponseToken id(final String id) {
+ super.id(id);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken expiration(final int expiration) {
+ super.expiration(expiration);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken notBefore(final int notBefore) {
+ super.notBefore(notBefore);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken issuedNow() {
+ super.issuedNow();
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken issuedAt(final int issuedAt) {
+ super.issuedAt(issuedAt);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken issuer(final String issuer) {
+ super.issuer(issuer);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken audience(final String... audience) {
+ super.audience(audience);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken subject(final String subject) {
+ super.subject(subject);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken type(final String type) {
+ super.type(type);
+ return this;
+ }
+
+ @Override
+ public DockerResponseToken issuedFor(final String issuedFor) {
+ super.issuedFor(issuedFor);
+ return this;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
index d597cf3..95c7ca3 100755
--- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
@@ -155,4 +155,101 @@ public class CredentialRepresentation {
public void setConfig(MultivaluedHashMap<String, String> config) {
this.config = config;
}
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((algorithm == null) ? 0 : algorithm.hashCode());
+ result = prime * result + ((config == null) ? 0 : config.hashCode());
+ result = prime * result + ((counter == null) ? 0 : counter.hashCode());
+ result = prime * result + ((createdDate == null) ? 0 : createdDate.hashCode());
+ result = prime * result + ((device == null) ? 0 : device.hashCode());
+ result = prime * result + ((digits == null) ? 0 : digits.hashCode());
+ result = prime * result + ((hashIterations == null) ? 0 : hashIterations.hashCode());
+ result = prime * result + ((hashedSaltedValue == null) ? 0 : hashedSaltedValue.hashCode());
+ result = prime * result + ((period == null) ? 0 : period.hashCode());
+ result = prime * result + ((salt == null) ? 0 : salt.hashCode());
+ result = prime * result + ((temporary == null) ? 0 : temporary.hashCode());
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
+ result = prime * result + ((value == null) ? 0 : value.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ CredentialRepresentation other = (CredentialRepresentation) obj;
+ if (algorithm == null) {
+ if (other.algorithm != null)
+ return false;
+ } else if (!algorithm.equals(other.algorithm))
+ return false;
+ if (config == null) {
+ if (other.config != null)
+ return false;
+ } else if (!config.equals(other.config))
+ return false;
+ if (counter == null) {
+ if (other.counter != null)
+ return false;
+ } else if (!counter.equals(other.counter))
+ return false;
+ if (createdDate == null) {
+ if (other.createdDate != null)
+ return false;
+ } else if (!createdDate.equals(other.createdDate))
+ return false;
+ if (device == null) {
+ if (other.device != null)
+ return false;
+ } else if (!device.equals(other.device))
+ return false;
+ if (digits == null) {
+ if (other.digits != null)
+ return false;
+ } else if (!digits.equals(other.digits))
+ return false;
+ if (hashIterations == null) {
+ if (other.hashIterations != null)
+ return false;
+ } else if (!hashIterations.equals(other.hashIterations))
+ return false;
+ if (hashedSaltedValue == null) {
+ if (other.hashedSaltedValue != null)
+ return false;
+ } else if (!hashedSaltedValue.equals(other.hashedSaltedValue))
+ return false;
+ if (period == null) {
+ if (other.period != null)
+ return false;
+ } else if (!period.equals(other.period))
+ return false;
+ if (salt == null) {
+ if (other.salt != null)
+ return false;
+ } else if (!salt.equals(other.salt))
+ return false;
+ if (temporary == null) {
+ if (other.temporary != null)
+ return false;
+ } else if (!temporary.equals(other.temporary))
+ return false;
+ if (type == null) {
+ if (other.type != null)
+ return false;
+ } else if (!type.equals(other.type))
+ return false;
+ if (value == null) {
+ if (other.value != null)
+ return false;
+ } else if (!value.equals(other.value))
+ return false;
+ return true;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 670e1d8..c3dd733 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -137,6 +137,7 @@ public class RealmRepresentation {
protected String directGrantFlow;
protected String resetCredentialsFlow;
protected String clientAuthenticationFlow;
+ protected String dockerAuthenticationFlow;
protected Map<String, String> attributes;
@@ -884,6 +885,15 @@ public class RealmRepresentation {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
+ public String getDockerAuthenticationFlow() {
+ return dockerAuthenticationFlow;
+ }
+
+ public RealmRepresentation setDockerAuthenticationFlow(final String dockerAuthenticationFlow) {
+ this.dockerAuthenticationFlow = dockerAuthenticationFlow;
+ return this;
+ }
+
public String getKeycloakVersion() {
return keycloakVersion;
}
diff --git a/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java
index e1b704e..8dcf006 100644
--- a/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java
@@ -21,8 +21,18 @@ import java.util.Map;
public class ProviderRepresentation {
+ private int order;
+
private Map<String, String> operationalInfo;
+ public int getOrder() {
+ return order;
+ }
+
+ public void setOrder(int priorityUI) {
+ this.order = priorityUI;
+ }
+
public Map<String, String> getOperationalInfo() {
return operationalInfo;
}
distribution/api-docs-dist/pom.xml 30(+23 -7)
diff --git a/distribution/api-docs-dist/pom.xml b/distribution/api-docs-dist/pom.xml
index a50916a..af5e218 100755
--- a/distribution/api-docs-dist/pom.xml
+++ b/distribution/api-docs-dist/pom.xml
@@ -63,13 +63,6 @@
</executions>
</plugin>
<plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-deploy-plugin</artifactId>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
- <plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
@@ -96,4 +89,27 @@
</plugins>
</build>
+
+ <profiles>
+ <profile>
+ <id>community</id>
+ <activation>
+ <property>
+ <name>!product</name>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
</project>
distribution/server-dist/pom.xml 12(+12 -0)
diff --git a/distribution/server-dist/pom.xml b/distribution/server-dist/pom.xml
index fd21630..b4c6f54 100755
--- a/distribution/server-dist/pom.xml
+++ b/distribution/server-dist/pom.xml
@@ -34,11 +34,23 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-feature-pack</artifactId>
<type>zip</type>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-cli-dist</artifactId>
<type>zip</type>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
</dependencies>
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
index cba7eb3..c6a1edb 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -38,6 +38,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -184,6 +185,12 @@ public interface RealmResource {
@QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential,
@QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("connectionTimeout") String connectionTimeout);
+ @Path("testSMTPConnection/{config}")
+ @POST
+ @NoCache
+ @Consumes(MediaType.APPLICATION_JSON)
+ Response testSMTPConnection(final @PathParam("config") String config) throws Exception;
+
@Path("clear-realm-cache")
@POST
void clearRealmCache();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index 3668d97..160fee5 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -117,6 +117,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected AuthenticationFlowModel directGrantFlow;
protected AuthenticationFlowModel resetCredentialsFlow;
protected AuthenticationFlowModel clientAuthenticationFlow;
+ protected AuthenticationFlowModel dockerAuthenticationFlow;
protected boolean eventsEnabled;
protected long eventsExpiration;
@@ -252,6 +253,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
directGrantFlow = model.getDirectGrantFlow();
resetCredentialsFlow = model.getResetCredentialsFlow();
clientAuthenticationFlow = model.getClientAuthenticationFlow();
+ dockerAuthenticationFlow = model.getDockerAuthenticationFlow();
for (ComponentModel component : model.getComponents()) {
componentsByParentAndType.add(component.getParentId() + component.getProviderType(), component);
@@ -547,6 +549,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return clientAuthenticationFlow;
}
+ public AuthenticationFlowModel getDockerAuthenticationFlow() {
+ return dockerAuthenticationFlow;
+ }
+
public List<String> getDefaultGroups() {
return defaultGroups;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index d1945ad..9925a69 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -1039,6 +1039,18 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
+ public AuthenticationFlowModel getDockerAuthenticationFlow() {
+ if (isUpdated()) return updated.getDockerAuthenticationFlow();
+ return cached.getDockerAuthenticationFlow();
+ }
+
+ @Override
+ public void setDockerAuthenticationFlow(final AuthenticationFlowModel flow) {
+ getDelegateForUpdate();
+ updated.setDockerAuthenticationFlow(flow);
+ }
+
+ @Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
if (isUpdated()) return updated.getAuthenticationFlows();
return cached.getAuthenticationFlowList();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 13988dc..33578e3 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -220,6 +220,8 @@ public class RealmEntity {
@Column(name="CLIENT_AUTH_FLOW")
protected String clientAuthenticationFlow;
+ @Column(name="DOCKER_AUTH_FLOW")
+ protected String dockerAuthenticationFlow;
@Column(name="INTERNATIONALIZATION_ENABLED")
@@ -733,6 +735,15 @@ public class RealmEntity {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
+ public String getDockerAuthenticationFlow() {
+ return dockerAuthenticationFlow;
+ }
+
+ public RealmEntity setDockerAuthenticationFlow(String dockerAuthenticationFlow) {
+ this.dockerAuthenticationFlow = dockerAuthenticationFlow;
+ return this;
+ }
+
public Collection<ClientTemplateEntity> getClientTemplates() {
return clientTemplates;
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index eba62db..cd814f4 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1376,6 +1376,18 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@Override
+ public AuthenticationFlowModel getDockerAuthenticationFlow() {
+ String flowId = realm.getDockerAuthenticationFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setDockerAuthenticationFlow(AuthenticationFlowModel flow) {
+ realm.setDockerAuthenticationFlow(flow.getId());
+ }
+
+ @Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
return realm.getAuthenticationFlows().stream()
.map(this::entityToModel)
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
index bd55645..daa1c50 100644
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
@@ -15,10 +15,14 @@
~ 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="keycloak" id="3.2.0">
+ <addColumn tableName="REALM">
+ <column name="DOCKER_AUTH_FLOW" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ </addColumn>
- <changeSet author="mposolda@redhat.com" id="3.2.0">
<dropPrimaryKey constraintName="CONSTRAINT_OFFL_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION" />
<dropColumn tableName="OFFLINE_CLIENT_SESSION" columnName="CLIENT_SESSION_ID" />
<addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
@@ -38,9 +42,6 @@
<addPrimaryKey columnNames="ID" constraintName="CNSTR_CLIENT_INIT_ACC_PK" tableName="CLIENT_INITIAL_ACCESS"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT_INITIAL_ACCESS" constraintName="FK_CLIENT_INIT_ACC_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
- </changeSet>
-
- <changeSet author="glavoie@gmail.com" id="3.2.0.idx">
<createIndex indexName="IDX_ASSOC_POL_ASSOC_POL_ID" tableName="ASSOCIATED_POLICY">
<column name="ASSOCIATED_POLICY_ID" type="VARCHAR(36)"/>
</createIndex>
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index d5b108d..2b47da7 100755
--- a/pom.xml
+++ b/pom.xml
@@ -45,7 +45,7 @@
<jboss.as.version>7.2.0.Final</jboss.as.version>
<wildfly.version>11.0.0.Alpha1</wildfly.version>
<wildfly.build-tools.version>1.2.2.Final</wildfly.build-tools.version>
- <eap.version>7.1.0.Beta1-redhat-2</eap.version>
+ <eap.version>7.1.0.Beta1-redhat-5</eap.version>
<eap.build-tools.version>1.2.2.Final</eap.build-tools.version>
<wildfly.core.version>3.0.0.Beta11</wildfly.core.version>
diff --git a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
index 86b3ecb..be74b74 100755
--- a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
@@ -345,7 +345,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
logger.debugv("saml document: {0}", documentAsString);
byte[] responseBytes = documentAsString.getBytes(GeneralConstants.SAML_CHARSET);
- return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
+ return RedirectBindingUtil.deflateBase64Encode(responseBytes);
}
@@ -370,7 +370,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
} catch (InvalidKeyException | SignatureException e) {
throw new ProcessingException(e);
}
- String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
+ String encodedSig = RedirectBindingUtil.base64Encode(sig);
builder.queryParam(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY, encodedSig);
}
return builder.build();
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
index f516124..7177322 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
@@ -511,7 +511,7 @@ public class DocumentUtil {
};
- private static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
+ public static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
DocumentBuilder res = XML_DOCUMENT_BUILDER.get();
res.reset();
return res;
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
index 587113c..9c0938f 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
@@ -61,6 +61,19 @@ public class RedirectBindingUtil {
}
/**
+ * On the byte array, apply base64 encoding
+ *
+ * @param stringToEncode
+ *
+ * @return
+ *
+ * @throws IOException
+ */
+ public static String base64Encode(byte[] stringToEncode) throws IOException {
+ return Base64.encodeBytes(stringToEncode, Base64.DONT_BREAK_LINES);
+ }
+
+ /**
* On the byte array, apply base64 encoding following by URL encoding
*
* @param stringToEncode
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index f6484d6..ff1cfd7 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -251,6 +251,9 @@ public interface RealmModel extends RoleContainerModel {
AuthenticationFlowModel getClientAuthenticationFlow();
void setClientAuthenticationFlow(AuthenticationFlowModel flow);
+ AuthenticationFlowModel getDockerAuthenticationFlow();
+ void setDockerAuthenticationFlow(AuthenticationFlowModel flow);
+
List<AuthenticationFlowModel> getAuthenticationFlows();
AuthenticationFlowModel getFlowByAlias(String alias);
AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model);
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java b/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java
index 3cf8d2c..5c83253 100755
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java
@@ -53,4 +53,8 @@ public interface ProviderFactory<T extends Provider> {
public String getId();
+ default int order() {
+ return 0;
+ }
+
}
diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
index 7ea2b49..a12b028 100755
--- a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
@@ -17,15 +17,15 @@
package org.keycloak.email;
-import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
+import java.util.Map;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface EmailSenderProvider extends Provider {
- void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
-
+ void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
index 1cc6151..da245fc 100755
--- a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
@@ -22,6 +22,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
+import java.util.Map;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -47,6 +49,15 @@ public interface EmailTemplateProvider extends Provider {
public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
/**
+ * Test SMTP connection with current logged in user
+ *
+ * @param config SMTP server configuration
+ * @param user SMTP recipient
+ * @throws EmailException
+ */
+ public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException;
+
+ /**
* Send to confirm that user wants to link his account with identity broker link
*/
void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException;
diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java
index 17cd0ac..98686af 100644
--- a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java
+++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java
@@ -27,11 +27,8 @@ import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.DefaultAuthenticationFlows;
-/**
- * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
- * @version $Revision: 1 $
- */
public class MigrateTo3_2_0 implements Migration {
public static final ModelVersion VERSION = new ModelVersion("3.2.0");
@@ -44,6 +41,10 @@ public class MigrateTo3_2_0 implements Migration {
realm.setPasswordPolicy(builder.remove(PasswordPolicy.HASH_ITERATIONS_ID).build(session));
}
+ if (realm.getDockerAuthenticationFlow() == null) {
+ DefaultAuthenticationFlows.dockerAuthenticationFlow(realm);
+ }
+
ClientModel realmAccess = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
if (realmAccess != null) {
addRoles(realmAccess);
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index b028814..8030da6 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -42,6 +42,7 @@ public class DefaultAuthenticationFlows {
public static final String RESET_CREDENTIALS_FLOW = "reset credentials";
public static final String LOGIN_FORMS_FLOW = "forms";
public static final String SAML_ECP_FLOW = "saml ecp";
+ public static final String DOCKER_AUTH = "docker auth";
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
@@ -58,6 +59,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
+ if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
}
public static void migrateFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
@@ -67,6 +69,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
+ if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
}
public static void registrationFlow(RealmModel realm) {
@@ -528,4 +531,26 @@ public class DefaultAuthenticationFlows {
realm.addAuthenticatorExecution(execution);
}
+
+ public static void dockerAuthenticationFlow(final RealmModel realm) {
+ AuthenticationFlowModel dockerAuthFlow = new AuthenticationFlowModel();
+
+ dockerAuthFlow.setAlias(DOCKER_AUTH);
+ dockerAuthFlow.setDescription("Used by Docker clients to authenticate against the IDP");
+ dockerAuthFlow.setProviderId("basic-flow");
+ dockerAuthFlow.setTopLevel(true);
+ dockerAuthFlow.setBuiltIn(true);
+ dockerAuthFlow = realm.addAuthenticationFlow(dockerAuthFlow);
+ realm.setDockerAuthenticationFlow(dockerAuthFlow);
+
+ AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+
+ execution.setParentFlow(dockerAuthFlow.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator("docker-http-basic-authenticator");
+ execution.setPriority(10);
+ execution.setAuthenticatorFlow(false);
+
+ realm.addAuthenticatorExecution(execution);
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index b454460..dc69fc8 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -489,6 +489,7 @@ public final class KeycloakModelUtils {
if ((realmFlow = realm.getClientAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getDirectGrantFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getResetCredentialsFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
+ if ((realmFlow = realm.getDockerAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
for (IdentityProviderModel idp : realm.getIdentityProviders()) {
if (model.getId().equals(idp.getFirstBrokerLoginFlowId())) return true;
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 6a7aeaa..6b7016f 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
@@ -35,6 +35,7 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
@@ -325,6 +326,7 @@ public class ModelToRepresentation {
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
if (realm.getResetCredentialsFlow() != null) rep.setResetCredentialsFlow(realm.getResetCredentialsFlow().getAlias());
if (realm.getClientAuthenticationFlow() != null) rep.setClientAuthenticationFlow(realm.getClientAuthenticationFlow().getAlias());
+ if (realm.getDockerAuthenticationFlow() != null) rep.setDockerAuthenticationFlow(realm.getDockerAuthenticationFlow().getAlias());
List<String> defaultRoles = realm.getDefaultRoles();
if (!defaultRoles.isEmpty()) {
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 4a4b4fb..a18c27a 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
@@ -614,6 +614,18 @@ public class RepresentationToModel {
}
}
+ // Added in 3.2
+ if (rep.getDockerAuthenticationFlow() == null) {
+ AuthenticationFlowModel dockerAuthenticationFlow = newRealm.getFlowByAlias(DefaultAuthenticationFlows.DOCKER_AUTH);
+ if (dockerAuthenticationFlow == null) {
+ DefaultAuthenticationFlows.dockerAuthenticationFlow(newRealm);
+ } else {
+ newRealm.setDockerAuthenticationFlow(dockerAuthenticationFlow);
+ }
+ } else {
+ newRealm.setDockerAuthenticationFlow(newRealm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
+ }
+
DefaultAuthenticationFlows.addIdentityProviderAuthenticator(newRealm, defaultProvider);
}
@@ -898,6 +910,9 @@ public class RepresentationToModel {
if (rep.getClientAuthenticationFlow() != null) {
realm.setClientAuthenticationFlow(realm.getFlowByAlias(rep.getClientAuthenticationFlow()));
}
+ if (rep.getDockerAuthenticationFlow() != null) {
+ realm.setDockerAuthenticationFlow(realm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
+ }
}
// Basic realm stuff
@@ -1201,6 +1216,7 @@ public class RepresentationToModel {
if (rep.isUseTemplateScope() != null) resource.setUseTemplateScope(rep.isUseTemplateScope());
if (rep.isUseTemplateMappers() != null) resource.setUseTemplateMappers(rep.isUseTemplateMappers());
+ if (rep.getSecret() != null) resource.setSecret(rep.getSecret());
if (rep.getClientTemplate() != null) {
if (rep.getClientTemplate().equals(ClientTemplateRepresentation.NONE)) {
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index 4451b8c..ce5b77f 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -54,7 +54,6 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
-import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
@@ -88,10 +87,13 @@ import java.util.List;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
-import org.w3c.dom.Document;
+import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
import org.w3c.dom.Element;
import java.util.*;
+import javax.xml.crypto.dsig.XMLSignature;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -517,6 +519,17 @@ public class SAMLEndpoint {
protected class PostBinding extends Binding {
@Override
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
+ NodeList nl = documentHolder.getSamlDocument().getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
+ boolean anyElementSigned = (nl != null && nl.getLength() > 0);
+ if ((! anyElementSigned) && (documentHolder.getSamlObject() instanceof ResponseType)) {
+ ResponseType responseType = (ResponseType) documentHolder.getSamlObject();
+ List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
+ if (! assertions.isEmpty() ) {
+ // Only relax verification if the response is an authnresponse and contains (encrypted/plaintext) assertion.
+ // In that case, signature is validated on assertion element
+ return;
+ }
+ }
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKeyLocator());
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
index 4d200a0..c6d999c 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
@@ -27,6 +27,24 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
public static final XmlKeyInfoKeyNameTransformer DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER = XmlKeyInfoKeyNameTransformer.NONE;
+ public static final String ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "addExtensionsElementWithKeyInfo";
+ public static final String BACKCHANNEL_SUPPORTED = "backchannelSupported";
+ public static final String ENCRYPTION_PUBLIC_KEY = "encryptionPublicKey";
+ public static final String FORCE_AUTHN = "forceAuthn";
+ public static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
+ public static final String POST_BINDING_AUTHN_REQUEST = "postBindingAuthnRequest";
+ public static final String POST_BINDING_LOGOUT = "postBindingLogout";
+ public static final String POST_BINDING_RESPONSE = "postBindingResponse";
+ public static final String SIGNATURE_ALGORITHM = "signatureAlgorithm";
+ public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
+ public static final String SINGLE_LOGOUT_SERVICE_URL = "singleLogoutServiceUrl";
+ public static final String SINGLE_SIGN_ON_SERVICE_URL = "singleSignOnServiceUrl";
+ public static final String VALIDATE_SIGNATURE = "validateSignature";
+ public static final String WANT_ASSERTIONS_ENCRYPTED = "wantAssertionsEncrypted";
+ public static final String WANT_ASSERTIONS_SIGNED = "wantAssertionsSigned";
+ public static final String WANT_AUTHN_REQUESTS_SIGNED = "wantAuthnRequestsSigned";
+ public static final String XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER = "xmlSigKeyInfoKeyNameTransformer";
+
public SAMLIdentityProviderConfig() {
}
@@ -35,35 +53,35 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
}
public String getSingleSignOnServiceUrl() {
- return getConfig().get("singleSignOnServiceUrl");
+ return getConfig().get(SINGLE_SIGN_ON_SERVICE_URL);
}
public void setSingleSignOnServiceUrl(String singleSignOnServiceUrl) {
- getConfig().put("singleSignOnServiceUrl", singleSignOnServiceUrl);
+ getConfig().put(SINGLE_SIGN_ON_SERVICE_URL, singleSignOnServiceUrl);
}
public String getSingleLogoutServiceUrl() {
- return getConfig().get("singleLogoutServiceUrl");
+ return getConfig().get(SINGLE_LOGOUT_SERVICE_URL);
}
public void setSingleLogoutServiceUrl(String singleLogoutServiceUrl) {
- getConfig().put("singleLogoutServiceUrl", singleLogoutServiceUrl);
+ getConfig().put(SINGLE_LOGOUT_SERVICE_URL, singleLogoutServiceUrl);
}
public boolean isValidateSignature() {
- return Boolean.valueOf(getConfig().get("validateSignature"));
+ return Boolean.valueOf(getConfig().get(VALIDATE_SIGNATURE));
}
public void setValidateSignature(boolean validateSignature) {
- getConfig().put("validateSignature", String.valueOf(validateSignature));
+ getConfig().put(VALIDATE_SIGNATURE, String.valueOf(validateSignature));
}
public boolean isForceAuthn() {
- return Boolean.valueOf(getConfig().get("forceAuthn"));
+ return Boolean.valueOf(getConfig().get(FORCE_AUTHN));
}
public void setForceAuthn(boolean forceAuthn) {
- getConfig().put("forceAuthn", String.valueOf(forceAuthn));
+ getConfig().put(FORCE_AUTHN, String.valueOf(forceAuthn));
}
/**
@@ -103,82 +121,80 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
return crt.split(",");
}
- public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
-
public String getNameIDPolicyFormat() {
- return getConfig().get("nameIDPolicyFormat");
+ return getConfig().get(NAME_ID_POLICY_FORMAT);
}
public void setNameIDPolicyFormat(String nameIDPolicyFormat) {
- getConfig().put("nameIDPolicyFormat", nameIDPolicyFormat);
+ getConfig().put(NAME_ID_POLICY_FORMAT, nameIDPolicyFormat);
}
public boolean isWantAuthnRequestsSigned() {
- return Boolean.valueOf(getConfig().get("wantAuthnRequestsSigned"));
+ return Boolean.valueOf(getConfig().get(WANT_AUTHN_REQUESTS_SIGNED));
}
public void setWantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) {
- getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
+ getConfig().put(WANT_AUTHN_REQUESTS_SIGNED, String.valueOf(wantAuthnRequestsSigned));
}
public boolean isWantAssertionsSigned() {
- return Boolean.valueOf(getConfig().get("wantAssertionsSigned"));
+ return Boolean.valueOf(getConfig().get(WANT_ASSERTIONS_SIGNED));
}
public void setWantAssertionsSigned(boolean wantAssertionsSigned) {
- getConfig().put("wantAssertionsSigned", String.valueOf(wantAssertionsSigned));
+ getConfig().put(WANT_ASSERTIONS_SIGNED, String.valueOf(wantAssertionsSigned));
}
public boolean isWantAssertionsEncrypted() {
- return Boolean.valueOf(getConfig().get("wantAssertionsEncrypted"));
+ return Boolean.valueOf(getConfig().get(WANT_ASSERTIONS_ENCRYPTED));
}
public void setWantAssertionsEncrypted(boolean wantAssertionsEncrypted) {
- getConfig().put("wantAssertionsEncrypted", String.valueOf(wantAssertionsEncrypted));
+ getConfig().put(WANT_ASSERTIONS_ENCRYPTED, String.valueOf(wantAssertionsEncrypted));
}
public boolean isAddExtensionsElementWithKeyInfo() {
- return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo"));
+ return Boolean.valueOf(getConfig().get(ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
}
public void setAddExtensionsElementWithKeyInfo(boolean addExtensionsElementWithKeyInfo) {
- getConfig().put("addExtensionsElementWithKeyInfo", String.valueOf(addExtensionsElementWithKeyInfo));
+ getConfig().put(ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, String.valueOf(addExtensionsElementWithKeyInfo));
}
public String getSignatureAlgorithm() {
- return getConfig().get("signatureAlgorithm");
+ return getConfig().get(SIGNATURE_ALGORITHM);
}
public void setSignatureAlgorithm(String signatureAlgorithm) {
- getConfig().put("signatureAlgorithm", signatureAlgorithm);
+ getConfig().put(SIGNATURE_ALGORITHM, signatureAlgorithm);
}
public String getEncryptionPublicKey() {
- return getConfig().get("encryptionPublicKey");
+ return getConfig().get(ENCRYPTION_PUBLIC_KEY);
}
public void setEncryptionPublicKey(String encryptionPublicKey) {
- getConfig().put("encryptionPublicKey", encryptionPublicKey);
+ getConfig().put(ENCRYPTION_PUBLIC_KEY, encryptionPublicKey);
}
public boolean isPostBindingAuthnRequest() {
- return Boolean.valueOf(getConfig().get("postBindingAuthnRequest"));
+ return Boolean.valueOf(getConfig().get(POST_BINDING_AUTHN_REQUEST));
}
public void setPostBindingAuthnRequest(boolean postBindingAuthnRequest) {
- getConfig().put("postBindingAuthnRequest", String.valueOf(postBindingAuthnRequest));
+ getConfig().put(POST_BINDING_AUTHN_REQUEST, String.valueOf(postBindingAuthnRequest));
}
public boolean isPostBindingResponse() {
- return Boolean.valueOf(getConfig().get("postBindingResponse"));
+ return Boolean.valueOf(getConfig().get(POST_BINDING_RESPONSE));
}
public void setPostBindingResponse(boolean postBindingResponse) {
- getConfig().put("postBindingResponse", String.valueOf(postBindingResponse));
+ getConfig().put(POST_BINDING_RESPONSE, String.valueOf(postBindingResponse));
}
public boolean isPostBindingLogout() {
- String postBindingLogout = getConfig().get("postBindingLogout");
+ String postBindingLogout = getConfig().get(POST_BINDING_LOGOUT);
if (postBindingLogout == null) {
// To maintain unchanged behavior when adding this field, we set the inital value to equal that
// of the binding for the response:
@@ -188,15 +204,15 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
}
public void setPostBindingLogout(boolean postBindingLogout) {
- getConfig().put("postBindingLogout", String.valueOf(postBindingLogout));
+ getConfig().put(POST_BINDING_LOGOUT, String.valueOf(postBindingLogout));
}
public boolean isBackchannelSupported() {
- return Boolean.valueOf(getConfig().get("backchannelSupported"));
+ return Boolean.valueOf(getConfig().get(BACKCHANNEL_SUPPORTED));
}
public void setBackchannelSupported(boolean backchannel) {
- getConfig().put("backchannelSupported", String.valueOf(backchannel));
+ getConfig().put(BACKCHANNEL_SUPPORTED, String.valueOf(backchannel));
}
/**
@@ -204,11 +220,11 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
* @return Configured ransformer of {@link #DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER} if not set.
*/
public XmlKeyInfoKeyNameTransformer getXmlSigKeyInfoKeyNameTransformer() {
- return XmlKeyInfoKeyNameTransformer.from(getConfig().get("xmlSigKeyInfoKeyNameTransformer"), DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
+ return XmlKeyInfoKeyNameTransformer.from(getConfig().get(XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER), DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
}
public void setXmlSigKeyInfoKeyNameTransformer(XmlKeyInfoKeyNameTransformer xmlSigKeyInfoKeyNameTransformer) {
- getConfig().put("xmlSigKeyInfoKeyNameTransformer",
+ getConfig().put(XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER,
xmlSigKeyInfoKeyNameTransformer == null
? null
: xmlSigKeyInfoKeyNameTransformer.name());
diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
index 7477d84..ca3575c 100644
--- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
+++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
@@ -20,7 +20,6 @@ package org.keycloak.email;
import com.sun.mail.smtp.SMTPMessage;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.truststore.HostnameVerificationPolicy;
@@ -57,20 +56,22 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
@Override
- public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
+ public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
Transport transport = null;
try {
String address = retrieveEmailAddress(user);
- Map<String, String> config = realm.getSmtpConfig();
Properties props = new Properties();
- props.setProperty("mail.smtp.host", config.get("host"));
+
+ if (config.containsKey("host")) {
+ props.setProperty("mail.smtp.host", config.get("host"));
+ }
boolean auth = "true".equals(config.get("auth"));
boolean ssl = "true".equals(config.get("ssl"));
boolean starttls = "true".equals(config.get("starttls"));
- if (config.containsKey("port")) {
+ if (config.containsKey("port") && config.get("port") != null) {
props.setProperty("mail.smtp.port", config.get("port"));
}
@@ -103,13 +104,13 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
Multipart multipart = new MimeMultipart("alternative");
- if(textBody != null) {
+ if (textBody != null) {
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(textBody, "UTF-8");
multipart.addBodyPart(textPart);
}
- if(htmlBody != null) {
+ if (htmlBody != null) {
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlBody, "text/html; charset=UTF-8");
multipart.addBodyPart(htmlPart);
@@ -153,13 +154,16 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
}
- protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException {
+ protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, EmailException {
+ if (email == null || "".equals(email.trim())) {
+ throw new EmailException("Please provide a valid address", null);
+ }
if (displayName == null || "".equals(displayName.trim())) {
return new InternetAddress(email);
}
return new InternetAddress(email, displayName, "utf-8");
}
-
+
protected String retrieveEmailAddress(UserModel user) {
return user.getEmail();
}
diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
index 5105eae..abc23a1 100755
--- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
+++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
@@ -108,6 +108,19 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
}
@Override
+ public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException {
+ setRealm(session.getContext().getRealm());
+ setUser(user);
+
+ Map<String, Object> attributes = new HashMap<String, Object>();
+ attributes.put("user", new ProfileBean(user));
+ attributes.put("realmName", realm.getName());
+
+ EmailTemplate email = processTemplate("emailTestSubject", Collections.emptyList(), "email-test.ftl", attributes);
+ send(config, email.getSubject(), email.getTextBody(), email.getHtmlBody());
+ }
+
+ @Override
public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
attributes.put("user", new ProfileBean(user));
@@ -156,7 +169,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
send(subjectKey, Collections.emptyList(), template, attributes);
}
- private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
+ private EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
try {
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
@@ -168,27 +181,39 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
String textTemplate = String.format("text/%s", template);
String textBody;
try {
- textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
+ textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
} catch (final FreeMarkerException e ) {
- textBody = null;
+ textBody = null;
}
String htmlTemplate = String.format("html/%s", template);
String htmlBody;
try {
- htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
+ htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
} catch (final FreeMarkerException e ) {
- htmlBody = null;
+ htmlBody = null;
}
- send(subject, textBody, htmlBody);
+ return new EmailTemplate(subject, textBody, htmlBody);
+ } catch (Exception e) {
+ throw new EmailException("Failed to template email", e);
+ }
+ }
+ private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
+ try {
+ EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
+ send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
} catch (Exception e) {
throw new EmailException("Failed to template email", e);
}
}
private void send(String subject, String textBody, String htmlBody) throws EmailException {
+ send(realm.getSmtpConfig(), subject, textBody, htmlBody);
+ }
+
+ private void send(Map<String, String> config, String subject, String textBody, String htmlBody) throws EmailException {
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
- emailSender.send(realm, user, subject, textBody, htmlBody);
+ emailSender.send(config, user, subject, textBody, htmlBody);
}
@Override
@@ -203,4 +228,29 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
return sb.toString();
}
+ private class EmailTemplate {
+
+ private String subject;
+ private String textBody;
+ private String htmlBody;
+
+ public EmailTemplate(String subject, String textBody, String htmlBody) {
+ this.subject = subject;
+ this.textBody = textBody;
+ this.htmlBody = htmlBody;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public String getTextBody() {
+ return textBody;
+ }
+
+ public String getHtmlBody() {
+ return htmlBody;
+ }
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
index 9c1e5a5..11d44af 100755
--- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
+++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
@@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
@@ -29,9 +30,11 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol.Error;
+import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
@@ -62,7 +65,7 @@ public abstract class AuthorizationEndpointBase {
@Context
protected HttpHeaders headers;
@Context
- protected HttpRequest request;
+ protected HttpRequest httpRequest;
@Context
protected KeycloakSession session;
@Context
@@ -84,7 +87,7 @@ public abstract class AuthorizationEndpointBase {
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
- .setRequest(request);
+ .setRequest(httpRequest);
authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath);
@@ -147,6 +150,19 @@ public abstract class AuthorizationEndpointBase {
return realm.getBrowserFlow();
}
+ protected void checkSsl() {
+ if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
+ event.error(Errors.SSL_REQUIRED);
+ throw new ErrorPageException(session, Messages.HTTPS_REQUIRED);
+ }
+ }
+
+ protected void checkRealm() {
+ if (!realm.isEnabled()) {
+ event.error(Errors.REALM_DISABLED);
+ throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
+ }
+ }
protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java
new file mode 100644
index 0000000..b2c2b37
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java
@@ -0,0 +1,76 @@
+package org.keycloak.protocol.docker;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.specimpl.ResponseBuilderImpl;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.events.Errors;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator;
+import org.keycloak.representations.docker.DockerAccess;
+import org.keycloak.representations.docker.DockerError;
+import org.keycloak.representations.docker.DockerErrorResponseToken;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Optional;
+
+public class DockerAuthenticator extends HttpBasicAuthenticator {
+ private static final Logger logger = Logger.getLogger(DockerAuthenticator.class);
+
+ public static final String ID = "docker-http-basic-authenticator";
+
+ @Override
+ protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) {
+ invalidUserAction(context, realm, user.getUsername(), context.getSession().getContext().resolveLocale(user));
+ }
+
+ @Override
+ protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String userId) {
+ final String localeString = Optional.ofNullable(realm.getDefaultLocale()).orElse(Locale.ENGLISH.toString());
+ invalidUserAction(context, realm, userId, new Locale(localeString));
+ }
+
+ @Override
+ protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) {
+ context.getEvent().user(user);
+ context.getEvent().error(Errors.USER_DISABLED);
+
+ final DockerError error = new DockerError("UNAUTHORIZED","Invalid username or password.",
+ Collections.singletonList(new DockerAccess(context.getAuthenticationSession().getClientNote(DockerAuthV2Protocol.SCOPE_PARAM))));
+
+ context.failure(AuthenticationFlowError.USER_DISABLED, new ResponseBuilderImpl()
+ .status(Response.Status.UNAUTHORIZED)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .entity(new DockerErrorResponseToken(Collections.singletonList(error)))
+ .build());
+ }
+
+ /**
+ * For Docker protocol the same error message will be returned for invalid credentials and incorrect user name. For SAML
+ * ECP, there is a different behavior for each.
+ */
+ private void invalidUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String userId, final Locale locale) {
+ context.getEvent().user(userId);
+ context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+
+ final DockerError error = new DockerError("UNAUTHORIZED","Invalid username or password.",
+ Collections.singletonList(new DockerAccess(context.getAuthenticationSession().getClientNote(DockerAuthV2Protocol.SCOPE_PARAM))));
+
+ context.failure(AuthenticationFlowError.INVALID_USER, new ResponseBuilderImpl()
+ .status(Response.Status.UNAUTHORIZED)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .entity(new DockerErrorResponseToken(Collections.singletonList(error)))
+ .build());
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java
new file mode 100644
index 0000000..9bba9c4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java
@@ -0,0 +1,84 @@
+package org.keycloak.protocol.docker;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.common.Profile;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.EnvironmentDependentProviderFactory;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.keycloak.models.AuthenticationExecutionModel.Requirement;
+
+public class DockerAuthenticatorFactory implements AuthenticatorFactory {
+
+ @Override
+ public String getHelpText() {
+ return "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Docker Authenticator";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return "docker";
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ private static final Requirement[] REQUIREMENT_CHOICES = {
+ Requirement.REQUIRED,
+ };
+
+ @Override
+ public Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return new DockerAuthenticator();
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return DockerAuthenticator.ID;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java
new file mode 100644
index 0000000..3a7a324
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java
@@ -0,0 +1,184 @@
+package org.keycloak.protocol.docker;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.specimpl.ResponseBuilderImpl;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeyManager;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper;
+import org.keycloak.representations.docker.DockerResponse;
+import org.keycloak.representations.docker.DockerResponseToken;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.util.TokenUtil;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Set;
+
+public class DockerAuthV2Protocol implements LoginProtocol {
+ protected static final Logger logger = Logger.getLogger(DockerEndpoint.class);
+
+ public static final String LOGIN_PROTOCOL = "docker-v2";
+ public static final String ACCOUNT_PARAM = "account";
+ public static final String SERVICE_PARAM = "service";
+ public static final String SCOPE_PARAM = "scope";
+ public static final String ISSUER = "docker.iss"; // don't want to overlap with OIDC notes
+ public static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+ private KeycloakSession session;
+ private RealmModel realm;
+ private UriInfo uriInfo;
+ private HttpHeaders headers;
+ private EventBuilder event;
+
+ public DockerAuthV2Protocol() {
+ }
+
+ public DockerAuthV2Protocol(final KeycloakSession session, final RealmModel realm, final UriInfo uriInfo, final HttpHeaders headers, final EventBuilder event) {
+ this.session = session;
+ this.realm = realm;
+ this.uriInfo = uriInfo;
+ this.headers = headers;
+ this.event = event;
+ }
+
+ @Override
+ public LoginProtocol setSession(final KeycloakSession session) {
+ this.session = session;
+ return this;
+ }
+
+ @Override
+ public LoginProtocol setRealm(final RealmModel realm) {
+ this.realm = realm;
+ return this;
+ }
+
+ @Override
+ public LoginProtocol setUriInfo(final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ return this;
+ }
+
+ @Override
+ public LoginProtocol setHttpHeaders(final HttpHeaders headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ @Override
+ public LoginProtocol setEventBuilder(final EventBuilder event) {
+ this.event = event;
+ return this;
+ }
+
+ @Override
+ public Response authenticated(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
+ // First, create a base response token with realm + user values populated
+ final ClientModel client = clientSession.getClient();
+ DockerResponseToken responseToken = new DockerResponseToken()
+ .id(KeycloakModelUtils.generateId())
+ .type(TokenUtil.TOKEN_TYPE_BEARER)
+ .issuer(clientSession.getNote(DockerAuthV2Protocol.ISSUER))
+ .subject(userSession.getUser().getUsername())
+ .issuedNow()
+ .audience(client.getClientId())
+ .issuedFor(client.getClientId());
+
+ // since realm access token is given in seconds
+ final int accessTokenLifespan = realm.getAccessTokenLifespan();
+ responseToken.notBefore(responseToken.getIssuedAt())
+ .expiration(responseToken.getIssuedAt() + accessTokenLifespan);
+
+ // Next, allow mappers to decorate the token to add/remove scopes as appropriate
+ final ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
+ final Set<ProtocolMapperModel> mappings = accessCode.getRequestedProtocolMappers();
+ for (final ProtocolMapperModel mapping : mappings) {
+ final ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
+ if (mapper instanceof DockerAuthV2AttributeMapper) {
+ final DockerAuthV2AttributeMapper dockerAttributeMapper = (DockerAuthV2AttributeMapper) mapper;
+ if (dockerAttributeMapper.appliesTo(responseToken)) {
+ responseToken = dockerAttributeMapper.transformDockerResponseToken(responseToken, mapping, session, userSession, clientSession);
+ }
+ }
+ }
+
+ try {
+ // Finally, construct the response to the docker client with the token + metadata
+ if (event.getEvent() != null && EventType.LOGIN.equals(event.getEvent().getType())) {
+ final KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm);
+ final String encodedToken = new JWSBuilder()
+ .kid(new DockerKeyIdentifier(activeKey.getPublicKey()).toString())
+ .type("JWT")
+ .jsonContent(responseToken)
+ .rsa256(activeKey.getPrivateKey());
+ final String expiresInIso8601String = new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(new Date(responseToken.getIssuedAt() * 1000L));
+
+ final DockerResponse responseEntity = new DockerResponse()
+ .setToken(encodedToken)
+ .setExpires_in(accessTokenLifespan)
+ .setIssued_at(expiresInIso8601String);
+ return new ResponseBuilderImpl().status(Response.Status.OK).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).entity(responseEntity).build();
+ } else {
+ logger.errorv("Unable to handle request for event type {0}. Currently only LOGIN event types are supported by docker protocol.", event.getEvent() == null ? "null" : event.getEvent().getType());
+ throw new ErrorResponseException("invalid_request", "Event type not supported", Response.Status.BAD_REQUEST);
+ }
+ } catch (final InstantiationException e) {
+ logger.errorv("Error attempting to create Key ID for Docker JOSE header: ", e.getMessage());
+ throw new ErrorResponseException("token_error", "Unable to construct JOSE header for JWT", Response.Status.INTERNAL_SERVER_ERROR);
+ }
+
+ }
+
+ @Override
+ public Response sendError(final AuthenticationSessionModel clientSession, final LoginProtocol.Error error) {
+ return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ }
+
+ @Override
+ public void backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
+ errorResponse(userSession, "backchannelLogout");
+
+ }
+
+ @Override
+ public Response frontchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
+ return errorResponse(userSession, "frontchannelLogout");
+ }
+
+ @Override
+ public Response finishLogout(final UserSessionModel userSession) {
+ return errorResponse(userSession, "finishLogout");
+ }
+
+ @Override
+ public boolean requireReauthentication(final UserSessionModel userSession, final AuthenticationSessionModel clientSession) {
+ return true;
+ }
+
+ private Response errorResponse(final UserSessionModel userSession, final String methodName) {
+ logger.errorv("User {0} attempted to invoke unsupported method {1} on docker protocol.", userSession.getUser().getUsername(), methodName);
+ throw new ErrorResponseException("invalid_request", String.format("Attempted to invoke unsupported docker method %s", methodName), Response.Status.BAD_REQUEST);
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java
new file mode 100644
index 0000000..be4c6c0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java
@@ -0,0 +1,86 @@
+package org.keycloak.protocol.docker;
+
+import org.keycloak.common.Profile;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.AbstractLoginProtocolFactory;
+import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper;
+import org.keycloak.provider.EnvironmentDependentProviderFactory;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class DockerAuthV2ProtocolFactory extends AbstractLoginProtocolFactory implements EnvironmentDependentProviderFactory {
+
+ static List<ProtocolMapperModel> builtins = new ArrayList<>();
+ static List<ProtocolMapperModel> defaultBuiltins = new ArrayList<>();
+
+ static {
+ final ProtocolMapperModel addAllRequestedScopeMapper = new ProtocolMapperModel();
+ addAllRequestedScopeMapper.setName(AllowAllDockerProtocolMapper.PROVIDER_ID);
+ addAllRequestedScopeMapper.setProtocolMapper(AllowAllDockerProtocolMapper.PROVIDER_ID);
+ addAllRequestedScopeMapper.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
+ addAllRequestedScopeMapper.setConsentRequired(false);
+ addAllRequestedScopeMapper.setConfig(Collections.EMPTY_MAP);
+ builtins.add(addAllRequestedScopeMapper);
+ defaultBuiltins.add(addAllRequestedScopeMapper);
+ }
+
+ @Override
+ protected void addDefaults(final ClientModel client) {
+ defaultBuiltins.forEach(builtinMapper -> client.addProtocolMapper(builtinMapper));
+ }
+
+ @Override
+ public List<ProtocolMapperModel> getBuiltinMappers() {
+ return builtins;
+ }
+
+ @Override
+ public List<ProtocolMapperModel> getDefaultBuiltinMappers() {
+ return defaultBuiltins;
+ }
+
+ @Override
+ public Object createProtocolEndpoint(final RealmModel realm, final EventBuilder event) {
+ return new DockerV2LoginProtocolService(realm, event);
+ }
+
+ @Override
+ public void setupClientDefaults(final ClientRepresentation rep, final ClientModel newClient) {
+ // no-op
+ }
+
+ @Override
+ public void setupTemplateDefaults(final ClientTemplateRepresentation clientRep, final ClientTemplateModel newClient) {
+ // no-op
+ }
+
+ @Override
+ public LoginProtocol create(final KeycloakSession session) {
+ return new DockerAuthV2Protocol().setSession(session);
+ }
+
+ @Override
+ public String getId() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return Profile.isFeatureEnabled(Profile.Feature.DOCKER);
+ }
+
+ @Override
+ public int order() {
+ return -100;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java
new file mode 100644
index 0000000..8cf50e8
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java
@@ -0,0 +1,103 @@
+package org.keycloak.protocol.docker;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.Profile;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.AuthorizationEndpointBase;
+import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
+import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.Urls;
+import org.keycloak.services.util.CacheControlUtil;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.utils.ProfileHelper;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+/**
+ * Implements a docker-client understandable format.
+ */
+public class DockerEndpoint extends AuthorizationEndpointBase {
+ protected static final Logger logger = Logger.getLogger(DockerEndpoint.class);
+
+ private final EventType login;
+ private String account;
+ private String service;
+ private String scope;
+ private ClientModel client;
+ private AuthenticationSessionModel authenticationSession;
+
+ public DockerEndpoint(final RealmModel realm, final EventBuilder event, final EventType login) {
+ super(realm, event);
+ this.login = login;
+ }
+
+ @GET
+ public Response build() {
+ ProfileHelper.requireFeature(Profile.Feature.DOCKER);
+
+ final MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
+
+ account = params.getFirst(DockerAuthV2Protocol.ACCOUNT_PARAM);
+ if (account == null) {
+ logger.debug("Account parameter not provided by docker auth. This is techincally required, but not actually used since " +
+ "username is provided by Basic auth header.");
+ }
+ service = params.getFirst(DockerAuthV2Protocol.SERVICE_PARAM);
+ if (service == null) {
+ throw new ErrorResponseException("invalid_request", "service parameter must be provided", Response.Status.BAD_REQUEST);
+ }
+ client = realm.getClientByClientId(service);
+ if (client == null) {
+ logger.errorv("Failed to lookup client given by service={0} parameter for realm: {1}.", service, realm.getName());
+ throw new ErrorResponseException("invalid_client", "Client specified by 'service' parameter does not exist", Response.Status.BAD_REQUEST);
+ }
+ scope = params.getFirst(DockerAuthV2Protocol.SCOPE_PARAM);
+
+ checkSsl();
+ checkRealm();
+
+ final AuthorizationEndpointRequest authRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
+ AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, authRequest.getState());
+ if (checks.response != null) {
+ return checks.response;
+ }
+
+ authenticationSession = checks.authSession;
+ updateAuthenticationSession();
+
+ // So back button doesn't work
+ CacheControlUtil.noBackButtonCacheControlHeader();
+
+ return handleBrowserAuthenticationRequest(authenticationSession, new DockerAuthV2Protocol(session, realm, uriInfo, headers, event.event(login)), false, false);
+ }
+
+ private void updateAuthenticationSession() {
+ authenticationSession.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
+ authenticationSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name());
+
+ // Docker specific stuff
+ authenticationSession.setClientNote(DockerAuthV2Protocol.ACCOUNT_PARAM, account);
+ authenticationSession.setClientNote(DockerAuthV2Protocol.SERVICE_PARAM, service);
+ authenticationSession.setClientNote(DockerAuthV2Protocol.SCOPE_PARAM, scope);
+ authenticationSession.setClientNote(DockerAuthV2Protocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+
+ }
+
+ @Override
+ protected AuthenticationFlowModel getAuthenticationFlow() {
+ return realm.getDockerAuthenticationFlow();
+ }
+
+ @Override
+ protected boolean isNewRequest(final AuthenticationSessionModel authSession, final ClientModel clientFromRequest, final String requestState) {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java b/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java
new file mode 100644
index 0000000..384f218
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java
@@ -0,0 +1,127 @@
+package org.keycloak.protocol.docker;
+
+import org.keycloak.models.utils.Base32;
+
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+/**
+ * The “kid” field has to be in a libtrust fingerprint compatible format. Such a format can be generated by following steps:
+ * 1) Take the DER encoded public key which the JWT token was signed against.
+ * 2) Create a SHA256 hash out of it and truncate to 240bits.
+ * 3) Split the result into 12 base32 encoded groups with : as delimiter.
+ *
+ * Ex: "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"
+ *
+ * @see https://docs.docker.com/registry/spec/auth/jwt/
+ * @see https://github.com/docker/libtrust/blob/master/key.go#L24
+ */
+public class DockerKeyIdentifier {
+
+ private final String identifier;
+
+ public DockerKeyIdentifier(final Key key) throws InstantiationException {
+ try {
+ final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+ final byte[] hashed = sha256.digest(key.getEncoded());
+ final byte[] hashedTruncated = truncateToBitLength(240, hashed);
+ final String base32Id = Base32.encode(hashedTruncated);
+ identifier = byteStream(base32Id.getBytes()).collect(new DelimitingCollector());
+ } catch (final NoSuchAlgorithmException e) {
+ throw new InstantiationException("Could not instantiate docker key identifier, no SHA-256 algorithm available.");
+ }
+ }
+
+ // ugh.
+ private Stream<Byte> byteStream(final byte[] bytes) {
+ final Collection<Byte> colectionedBytes = new ArrayList<>();
+ for (final byte aByte : bytes) {
+ colectionedBytes.add(aByte);
+ }
+
+ return colectionedBytes.stream();
+ }
+
+ private byte[] truncateToBitLength(final int bitLength, final byte[] arrayToTruncate) {
+ if (bitLength % 8 != 0) {
+ throw new IllegalArgumentException("Bit length for truncation of byte array given as a number not divisible by 8");
+ }
+
+ final int numberOfBytes = bitLength / 8;
+ return Arrays.copyOfRange(arrayToTruncate, 0, numberOfBytes);
+ }
+
+ @Override
+ public String toString() {
+ return identifier;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DockerKeyIdentifier)) return false;
+
+ final DockerKeyIdentifier that = (DockerKeyIdentifier) o;
+
+ return identifier != null ? identifier.equals(that.identifier) : that.identifier == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return identifier != null ? identifier.hashCode() : 0;
+ }
+
+ // Could probably be generalized with size and delimiter arguments, but leaving it here for now until someone else needs it.
+ public static class DelimitingCollector implements Collector<Byte, StringBuilder, String> {
+
+ @Override
+ public Supplier<StringBuilder> supplier() {
+ return () -> new StringBuilder();
+ }
+
+ @Override
+ public BiConsumer<StringBuilder, Byte> accumulator() {
+ return ((stringBuilder, aByte) -> {
+ if (needsDelimiter(4, ":", stringBuilder)) {
+ stringBuilder.append(":");
+ }
+
+ stringBuilder.append(new String(new byte[]{aByte}));
+ });
+ }
+
+ private static boolean needsDelimiter(final int maxLength, final String delimiter, final StringBuilder builder) {
+ final int lastDelimiter = builder.lastIndexOf(delimiter);
+ final int charsSinceLastDelimiter = builder.length() - lastDelimiter;
+ return charsSinceLastDelimiter > maxLength;
+ }
+
+ @Override
+ public BinaryOperator<StringBuilder> combiner() {
+ return ((left, right) -> new StringBuilder(left.toString()).append(right.toString()));
+ }
+
+ @Override
+ public Function<StringBuilder, String> finisher() {
+ return StringBuilder::toString;
+ }
+
+ @Override
+ public Set<Characteristics> characteristics() {
+ return Collections.emptySet();
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java
new file mode 100644
index 0000000..a0dad58
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java
@@ -0,0 +1,70 @@
+package org.keycloak.protocol.docker;
+
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.common.Profile;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.utils.ProfileHelper;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+public class DockerV2LoginProtocolService {
+
+ private final RealmModel realm;
+ private final TokenManager tokenManager;
+ private final EventBuilder event;
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Context
+ private KeycloakSession session;
+
+ @Context
+ private HttpHeaders headers;
+
+ public DockerV2LoginProtocolService(final RealmModel realm, final EventBuilder event) {
+ this.realm = realm;
+ this.tokenManager = new TokenManager();
+ this.event = event;
+ }
+
+ public static UriBuilder authProtocolBaseUrl(final UriInfo uriInfo) {
+ final UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
+ return authProtocolBaseUrl(baseUriBuilder);
+ }
+
+ public static UriBuilder authProtocolBaseUrl(final UriBuilder baseUriBuilder) {
+ return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + DockerAuthV2Protocol.LOGIN_PROTOCOL);
+ }
+
+ public static UriBuilder authUrl(final UriInfo uriInfo) {
+ final UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
+ return authUrl(baseUriBuilder);
+ }
+
+ public static UriBuilder authUrl(final UriBuilder baseUriBuilder) {
+ final UriBuilder uriBuilder = authProtocolBaseUrl(baseUriBuilder);
+ return uriBuilder.path(DockerV2LoginProtocolService.class, "auth");
+ }
+
+ /**
+ * Authorization endpoint
+ */
+ @Path("auth")
+ public Object auth() {
+ ProfileHelper.requireFeature(Profile.Feature.DOCKER);
+
+ final DockerEndpoint endpoint = new DockerEndpoint(realm, event, EventType.LOGIN);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java
new file mode 100644
index 0000000..6687089
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java
@@ -0,0 +1,37 @@
+package org.keycloak.protocol.docker.installation.compose;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.Base64;
+
+public final class DockerCertFileUtils {
+ public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
+ public static final String END_CERT = "-----END CERTIFICATE-----";
+ public static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
+ public static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
+ public final static String LINE_SEPERATOR = System.getProperty("line.separator");
+
+ private DockerCertFileUtils() {
+ }
+
+ public static String formatCrtFileContents(final Certificate certificate) throws CertificateEncodingException {
+ return encodeAndPrettify(BEGIN_CERT, certificate.getEncoded(), END_CERT);
+ }
+
+ public static String formatPrivateKeyContents(final PrivateKey privateKey) {
+ return encodeAndPrettify(BEGIN_PRIVATE_KEY, privateKey.getEncoded(), END_PRIVATE_KEY);
+ }
+
+ public static String formatPublicKeyContents(final PublicKey publicKey) {
+ return encodeAndPrettify(BEGIN_CERT, publicKey.getEncoded(), END_CERT);
+ }
+
+ private static String encodeAndPrettify(final String header, final byte[] rawCrtText, final String footer) {
+ final Base64.Encoder encoder = Base64.getMimeEncoder(64, LINE_SEPERATOR.getBytes());
+ final String encodedCertText = new String(encoder.encode(rawCrtText));
+ final String prettified_cert = header + LINE_SEPERATOR + encodedCertText + LINE_SEPERATOR + footer;
+ return prettified_cert;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java
new file mode 100644
index 0000000..9d607f4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java
@@ -0,0 +1,62 @@
+package org.keycloak.protocol.docker.installation.compose;
+
+import org.keycloak.common.util.CertificateUtils;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.AbstractMap;
+import java.util.Map;
+
+public class DockerComposeCertsDirectory {
+
+ private final String directoryName;
+ private final Map.Entry<String, byte[]> localhostCertFile;
+ private final Map.Entry<String, byte[]> localhostKeyFile;
+ private final Map.Entry<String, byte[]> idpTrustChainFile;
+
+ public DockerComposeCertsDirectory(final String directoryName, final Certificate realmCert, final String registryCertFilename, final String registryKeyFilename, final String idpCertTrustChainFilename, final String realmName) {
+ this.directoryName = directoryName;
+
+ final KeyPairGenerator keyGen;
+ try {
+ keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048, new SecureRandom());
+
+ final KeyPair keypair = keyGen.generateKeyPair();
+ final PrivateKey privateKey = keypair.getPrivate();
+ final Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, realmName);
+
+ localhostCertFile = new AbstractMap.SimpleImmutableEntry<>(registryCertFilename, DockerCertFileUtils.formatCrtFileContents(certificate).getBytes());
+ localhostKeyFile = new AbstractMap.SimpleImmutableEntry<>(registryKeyFilename, DockerCertFileUtils.formatPrivateKeyContents(privateKey).getBytes());
+ idpTrustChainFile = new AbstractMap.SimpleEntry<>(idpCertTrustChainFilename, DockerCertFileUtils.formatCrtFileContents(realmCert).getBytes());
+
+ } catch (final NoSuchAlgorithmException e) {
+ // TODO throw error here descritively
+ throw new RuntimeException(e);
+ } catch (final CertificateEncodingException e) {
+ // TODO throw error here descritively
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String getDirectoryName() {
+ return directoryName;
+ }
+
+ public Map.Entry<String, byte[]> getLocalhostCertFile() {
+ return localhostCertFile;
+ }
+
+ public Map.Entry<String, byte[]> getLocalhostKeyFile() {
+ return localhostKeyFile;
+ }
+
+ public Map.Entry<String, byte[]> getIdpTrustChainFile() {
+ return idpTrustChainFile;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java
new file mode 100644
index 0000000..1630ffa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java
@@ -0,0 +1,70 @@
+package org.keycloak.protocol.docker.installation.compose;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.net.URL;
+
+/**
+ * Representation of the docker-compose.yaml file
+ */
+public class DockerComposeYamlFile {
+
+ private final String registryDataDirName;
+ private final String localCertDirName;
+ private final String containerCertPath;
+ private final String localhostCrtFileName;
+ private final String localhostKeyFileName;
+ private final String authServerTrustChainFileName;
+ private final URL authServerUrl;
+ private final String realmName;
+ private final String serviceId;
+
+ /**
+ * @param registryDataDirName Directory name to be used for both the container's storage directory, as well as the local data directory name
+ * @param localCertDirName Name of the (relative) local directory that holds the certs
+ * @param containerCertPath Path at which the local certs directory should be mounted on the container
+ * @param localhostCrtFileName SSL Cert file name for the registry
+ * @param localhostKeyFileName SSL Key file name for the registry
+ * @param authServerTrustChainFileName IDP trust chain, used for auth token validation
+ * @param authServerUrl Root URL for Keycloak, commonly something like http://localhost:8080/auth for dev environments
+ * @param realmName Name of the realm for which the docker client is configured
+ * @param serviceId Docker's Service ID, corresponds to Keycloak's client ID
+ */
+ public DockerComposeYamlFile(final String registryDataDirName, final String localCertDirName, final String containerCertPath, final String localhostCrtFileName, final String localhostKeyFileName, final String authServerTrustChainFileName, final URL authServerUrl, final String realmName, final String serviceId) {
+ this.registryDataDirName = registryDataDirName;
+ this.localCertDirName = localCertDirName;
+ this.containerCertPath = containerCertPath;
+ this.localhostCrtFileName = localhostCrtFileName;
+ this.localhostKeyFileName = localhostKeyFileName;
+ this.authServerTrustChainFileName = authServerTrustChainFileName;
+ this.authServerUrl = authServerUrl;
+ this.realmName = realmName;
+ this.serviceId = serviceId;
+ }
+
+ public byte[] generateDockerComposeFileBytes() {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final PrintWriter writer = new PrintWriter(output);
+
+ writer.print("registry:\n");
+ writer.print(" image: registry:2\n");
+ writer.print(" ports:\n");
+ writer.print(" - 127.0.0.1:5000:5000\n");
+ writer.print(" environment:\n");
+ writer.print(" REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /" + registryDataDirName + "\n");
+ writer.print(" REGISTRY_HTTP_TLS_CERTIFICATE: " + containerCertPath + "/" + localhostCrtFileName + "\n");
+ writer.print(" REGISTRY_HTTP_TLS_KEY: " + containerCertPath + "/" + localhostKeyFileName + "\n");
+ writer.print(" REGISTRY_AUTH_TOKEN_REALM: " + authServerUrl + "/realms/" + realmName + "/protocol/docker-v2/auth\n");
+ writer.print(" REGISTRY_AUTH_TOKEN_SERVICE: " + serviceId + "\n");
+ writer.print(" REGISTRY_AUTH_TOKEN_ISSUER: " + authServerUrl + "/realms/" + realmName + "\n");
+ writer.print(" REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: " + containerCertPath + "/" + authServerTrustChainFileName + "\n");
+ writer.print(" volumes:\n");
+ writer.print(" - ./" + registryDataDirName + ":/" + registryDataDirName + ":z\n");
+ writer.print(" - ./" + localCertDirName + ":" + containerCertPath + ":z");
+
+ writer.flush();
+ writer.close();
+
+ return output.toByteArray();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java
new file mode 100644
index 0000000..a4d0ee2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java
@@ -0,0 +1,35 @@
+package org.keycloak.protocol.docker.installation.compose;
+
+import java.net.URL;
+import java.security.cert.Certificate;
+
+public class DockerComposeZipContent {
+
+ private final DockerComposeYamlFile yamlFile;
+ private final String dataDirectoryName;
+ private final DockerComposeCertsDirectory certsDirectory;
+
+ public DockerComposeZipContent(final Certificate realmCert, final URL realmBaseUrl, final String realmName, final String clientId) {
+ final String dataDirectoryName = "data";
+ final String certsDirectoryName = "certs";
+ final String registryCertFilename = "localhost.crt";
+ final String registryKeyFilename = "localhost.key";
+ final String idpCertTrustChainFilename = "localhost_trust_chain.pem";
+
+ this.yamlFile = new DockerComposeYamlFile(dataDirectoryName, certsDirectoryName, "/opt/" + certsDirectoryName, registryCertFilename, registryKeyFilename, idpCertTrustChainFilename, realmBaseUrl, realmName, clientId);
+ this.dataDirectoryName = dataDirectoryName;
+ this.certsDirectory = new DockerComposeCertsDirectory(certsDirectoryName, realmCert, registryCertFilename, registryKeyFilename, idpCertTrustChainFilename, realmName);
+ }
+
+ public DockerComposeYamlFile getYamlFile() {
+ return yamlFile;
+ }
+
+ public String getDataDirectoryName() {
+ return dataDirectoryName;
+ }
+
+ public DockerComposeCertsDirectory getCertsDirectory() {
+ return certsDirectory;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java
new file mode 100644
index 0000000..72ade31
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java
@@ -0,0 +1,148 @@
+package org.keycloak.protocol.docker.installation;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.ClientInstallationProvider;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+import org.keycloak.protocol.docker.installation.compose.DockerComposeZipContent;
+
+import javax.ws.rs.core.Response;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URL;
+import java.security.cert.Certificate;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class DockerComposeYamlInstallationProvider implements ClientInstallationProvider {
+ private static Logger log = Logger.getLogger(DockerComposeYamlInstallationProvider.class);
+
+ public static final String ROOT_DIR = "keycloak-docker-compose-yaml/";
+
+ @Override
+ public ClientInstallationProvider create(final KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return "docker-v2-compose-yaml";
+ }
+
+ @Override
+ public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
+ final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ final ZipOutputStream zipOutput = new ZipOutputStream(byteStream);
+
+ try {
+ return generateInstallation(zipOutput, byteStream, session.keys().getActiveRsaKey(realm).getCertificate(), session.getContext().getAuthServerUrl().toURL(), realm.getName(), client.getClientId());
+ } catch (final IOException e) {
+ try {
+ zipOutput.close();
+ } catch (final IOException ex) {
+ // do nothing, already in an exception
+ }
+ try {
+ byteStream.close();
+ } catch (final IOException ex) {
+ // do nothing, already in an exception
+ }
+ throw new RuntimeException("Error occurred during attempt to generate docker-compose yaml installation files", e);
+ }
+ }
+
+ public Response generateInstallation(final ZipOutputStream zipOutput, final ByteArrayOutputStream byteStream, final Certificate realmCert, final URL realmBaseURl,
+ final String realmName, final String clientName) throws IOException {
+ final DockerComposeZipContent zipContent = new DockerComposeZipContent(realmCert, realmBaseURl, realmName, clientName);
+
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR));
+
+ // Write docker compose file
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "docker-compose.yaml"));
+ zipOutput.write(zipContent.getYamlFile().generateDockerComposeFileBytes());
+ zipOutput.closeEntry();
+
+ // Write data directory
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/"));
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/.gitignore"));
+ zipOutput.write("*".getBytes());
+ zipOutput.closeEntry();
+
+ // Write certificates
+ final String certsDirectory = ROOT_DIR + zipContent.getCertsDirectory().getDirectoryName() + "/";
+ zipOutput.putNextEntry(new ZipEntry(certsDirectory));
+ zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostCertFile().getKey()));
+ zipOutput.write(zipContent.getCertsDirectory().getLocalhostCertFile().getValue());
+ zipOutput.closeEntry();
+ zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostKeyFile().getKey()));
+ zipOutput.write(zipContent.getCertsDirectory().getLocalhostKeyFile().getValue());
+ zipOutput.closeEntry();
+ zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getIdpTrustChainFile().getKey()));
+ zipOutput.write(zipContent.getCertsDirectory().getIdpTrustChainFile().getValue());
+ zipOutput.closeEntry();
+
+ // Write README to .zip
+ zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "README.md"));
+ final String readmeContent = new BufferedReader(new InputStreamReader(DockerComposeYamlInstallationProvider.class.getResourceAsStream("/DockerComposeYamlReadme.md"))).lines().collect(Collectors.joining("\n"));
+ zipOutput.write(readmeContent.getBytes());
+ zipOutput.closeEntry();
+
+ zipOutput.close();
+ byteStream.close();
+
+ return Response.ok(byteStream.toByteArray(), getMediaType()).build();
+ }
+
+ @Override
+ public String getProtocol() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Docker Compose YAML";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Produces a zip file that can be used to stand up a development registry on localhost";
+ }
+
+ @Override
+ public String getFilename() {
+ return "keycloak-docker-compose-yaml.zip";
+ }
+
+ @Override
+ public String getMediaType() {
+ return "application/zip";
+ }
+
+ @Override
+ public boolean isDownloadOnly() {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java
new file mode 100644
index 0000000..ba4440a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java
@@ -0,0 +1,81 @@
+package org.keycloak.protocol.docker.installation;
+
+import org.keycloak.Config;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.ClientInstallationProvider;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+public class DockerRegistryConfigFileInstallationProvider implements ClientInstallationProvider {
+
+ @Override
+ public ClientInstallationProvider create(final KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return "docker-v2-registry-config-file";
+ }
+
+ @Override
+ public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
+ final StringBuilder responseString = new StringBuilder("auth:\n")
+ .append(" token:\n")
+ .append(" realm: ").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("/protocol/").append(DockerAuthV2Protocol.LOGIN_PROTOCOL).append("/auth\n")
+ .append(" service: ").append(client.getClientId()).append("\n")
+ .append(" issuer: ").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("\n");
+ return Response.ok(responseString.toString(), MediaType.TEXT_PLAIN_TYPE).build();
+ }
+
+ @Override
+ public String getProtocol() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Registry Config File";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Provides a registry configuration file snippet for use with this client";
+ }
+
+ @Override
+ public String getFilename() {
+ return "config.yml";
+ }
+
+ @Override
+ public String getMediaType() {
+ return MediaType.TEXT_PLAIN;
+ }
+
+ @Override
+ public boolean isDownloadOnly() {
+ return false;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java
new file mode 100644
index 0000000..055d9ac
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java
@@ -0,0 +1,81 @@
+package org.keycloak.protocol.docker.installation;
+
+import org.keycloak.Config;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.ClientInstallationProvider;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+public class DockerVariableOverrideInstallationProvider implements ClientInstallationProvider {
+
+ @Override
+ public ClientInstallationProvider create(final KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return "docker-v2-variable-override";
+ }
+
+ // TODO "auth" is not guaranteed to be the endpoint, fix it
+ @Override
+ public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
+ final StringBuilder builder = new StringBuilder()
+ .append("-e REGISTRY_AUTH_TOKEN_REALM=").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("/protocol/").append(DockerAuthV2Protocol.LOGIN_PROTOCOL).append("/auth \\\n")
+ .append("-e REGISTRY_AUTH_TOKEN_SERVICE=").append(client.getClientId()).append(" \\\n")
+ .append("-e REGISTRY_AUTH_TOKEN_ISSUER=").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append(" \\\n");
+ return Response.ok(builder.toString(), MediaType.TEXT_PLAIN_TYPE).build();
+ }
+
+ @Override
+ public String getProtocol() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Variable Override";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Configures environment variable overrides, typically used with a docker-compose.yaml configuration for a docker registry";
+ }
+
+ @Override
+ public String getFilename() {
+ return "docker-env.txt";
+ }
+
+ @Override
+ public String getMediaType() {
+ return MediaType.TEXT_PLAIN;
+ }
+
+ @Override
+ public boolean isDownloadOnly() {
+ return false;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java
new file mode 100644
index 0000000..398eeb6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java
@@ -0,0 +1,52 @@
+package org.keycloak.protocol.docker.mapper;
+
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+import org.keycloak.representations.docker.DockerAccess;
+import org.keycloak.representations.docker.DockerResponseToken;
+
+/**
+ * Populates token with requested scope. If more scopes are present than what has been requested, they will be removed.
+ */
+public class AllowAllDockerProtocolMapper extends DockerAuthV2ProtocolMapper implements DockerAuthV2AttributeMapper {
+
+ public static final String PROVIDER_ID = "docker-v2-allow-all-mapper";
+
+ @Override
+ public String getDisplayType() {
+ return "Allow All";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Allows all grants, returning the full set of requested access attributes as permitted attributes.";
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public boolean appliesTo(final DockerResponseToken responseToken) {
+ return true;
+ }
+
+ @Override
+ public DockerResponseToken transformDockerResponseToken(final DockerResponseToken responseToken, final ProtocolMapperModel mappingModel,
+ final KeycloakSession session, final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
+
+ responseToken.getAccessItems().clear();
+
+ final String requestedScope = clientSession.getNote(DockerAuthV2Protocol.SCOPE_PARAM);
+ if (requestedScope != null) {
+ final DockerAccess allRequestedAccess = new DockerAccess(requestedScope);
+ responseToken.getAccessItems().add(allRequestedAccess);
+ }
+
+ return responseToken;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java
new file mode 100644
index 0000000..320686b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java
@@ -0,0 +1,15 @@
+package org.keycloak.protocol.docker.mapper;
+
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.docker.DockerResponseToken;
+
+public interface DockerAuthV2AttributeMapper {
+
+ boolean appliesTo(DockerResponseToken responseToken);
+
+ DockerResponseToken transformDockerResponseToken(DockerResponseToken responseToken, ProtocolMapperModel mappingModel,
+ KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
+}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java
new file mode 100644
index 0000000..69ccd00
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java
@@ -0,0 +1,51 @@
+package org.keycloak.protocol.docker.mapper;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.Collections;
+import java.util.List;
+
+public abstract class DockerAuthV2ProtocolMapper implements ProtocolMapper {
+
+ public static final String DOCKER_AUTH_V2_CATEGORY = "Docker Auth Mapper";
+
+ @Override
+ public String getProtocol() {
+ return DockerAuthV2Protocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return DOCKER_AUTH_V2_CATEGORY;
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public final ProtocolMapper create(final KeycloakSession session) {
+ throw new UnsupportedOperationException("The create method is not supported by this mapper");
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ // no-op
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 3a7e4c0..402be4c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -49,7 +49,6 @@ import org.keycloak.util.TokenUtil;
import javax.ws.rs.GET;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
-
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -169,21 +168,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return this;
}
-
- private void checkSsl() {
- if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
- event.error(Errors.SSL_REQUIRED);
- throw new ErrorPageException(session, Messages.HTTPS_REQUIRED);
- }
- }
-
- private void checkRealm() {
- if (!realm.isEnabled()) {
- event.error(Errors.REALM_DISABLED);
- throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
- }
- }
-
private void checkClient(String clientId) {
if (clientId == null) {
event.error(Errors.INVALID_REQUEST);
@@ -288,24 +272,24 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private Response checkPKCEParams() {
String codeChallenge = request.getCodeChallenge();
String codeChallengeMethod = request.getCodeChallengeMethod();
-
+
// PKCE not adopted to OAuth2 Implicit Grant and OIDC Implicit Flow,
// adopted to OAuth2 Authorization Code Grant and OIDC Authorization Code Flow, Hybrid Flow
// Namely, flows using authorization code.
if (parsedResponseType.isImplicitFlow()) return null;
-
+
if (codeChallenge == null && codeChallengeMethod != null) {
logger.info("PKCE supporting Client without code challenge");
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge");
}
-
+
// based on code_challenge value decide whether this client(RP) supports PKCE
if (codeChallenge == null) {
logger.debug("PKCE non-supporting Client");
return null;
}
-
+
if (codeChallengeMethod != null) {
// https://tools.ietf.org/html/rfc7636#section-4.2
// plain or S256
@@ -319,13 +303,13 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
// default code_challenge_method is plane
codeChallengeMethod = OIDCLoginProtocol.PKCE_METHOD_PLAIN;
}
-
+
if (!isValidPkceCodeChallenge(codeChallenge)) {
logger.infof("PKCE supporting Client with invalid code challenge specified in PKCE, codeChallenge = %s", codeChallenge);
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge");
}
-
+
return null;
}
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 4870415..14d5570 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
@@ -258,6 +258,7 @@ public class TokenEndpoint {
}
event.user(userSession.getUser());
+
event.session(userSession.getId());
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
index 1e9b3e2..c156092 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
@@ -26,8 +26,10 @@ import org.keycloak.representations.IDToken;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
/**
* Set the 'name' claim to be first + last name.
@@ -73,9 +75,12 @@ public class FullNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
- String first = user.getFirstName() == null ? "" : user.getFirstName() + " ";
- String last = user.getLastName() == null ? "" : user.getLastName();
- token.getOtherClaims().put("name", first + last);
+ List<String> parts = new LinkedList<>();
+ Optional.ofNullable(user.getFirstName()).filter(s -> !s.isEmpty()).ifPresent(parts::add);
+ Optional.ofNullable(user.getLastName()).filter(s -> !s.isEmpty()).ifPresent(parts::add);
+ if (!parts.isEmpty()) {
+ token.getOtherClaims().put("name", String.join(" ", parts));
+ }
}
public static ProtocolMapperModel create(String name,
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
index f2fdd0c..9027ed5 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
@@ -91,7 +91,7 @@ public abstract class OIDCRedirectUriBuilder {
@Override
public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
- String param = paramName + "=" + Encode.encodeQueryParam(paramValue);
+ String param = paramName + "=" + Encode.encodeQueryParamAsIs(paramValue);
if (fragment == null) {
fragment = new StringBuilder(param);
} else {
diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
old mode 100755
new mode 100644
index f21eff3..f6821b6
--- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
@@ -1,192 +1,127 @@
-/*
- * 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.protocol.saml.profile.ecp.authenticator;
import org.jboss.resteasy.spi.HttpRequest;
-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.common.util.Base64;
import org.keycloak.events.Errors;
-import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
-import org.keycloak.provider.ProviderConfigProperty;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
-/**
- * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
- */
-public class HttpBasicAuthenticator implements AuthenticatorFactory {
+public class HttpBasicAuthenticator implements Authenticator {
- public static final String PROVIDER_ID = "http-basic-authenticator";
+ private static final String BASIC = "Basic";
+ private static final String BASIC_PREFIX = BASIC + " ";
@Override
- public String getDisplayType() {
- return "HTTP Basic Authentication";
+ public void authenticate(final AuthenticationFlowContext context) {
+ final HttpRequest httpRequest = context.getHttpRequest();
+ final HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
+ final String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
+
+ context.attempted();
+
+ if (usernameAndPassword != null) {
+ final RealmModel realm = context.getRealm();
+ final String username = usernameAndPassword[0];
+ final UserModel user = context.getSession().users().getUserByUsername(username, realm);
+
+ if (user != null) {
+ final String password = usernameAndPassword[1];
+ final boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
+
+ if (valid) {
+ if (user.isEnabled()) {
+ userSuccessAction(context, user);
+ } else {
+ userDisabledAction(context, realm, user);
+ }
+ } else {
+ notValidCredentialsAction(context, realm, user);
+ }
+ } else {
+ nullUserAction(context, realm, username);
+ }
+ }
}
- @Override
- public String getReferenceCategory() {
- return null;
+ protected void userSuccessAction(AuthenticationFlowContext context, UserModel user) {
+ context.getAuthenticationSession().setAuthenticatedUser(user);
+ context.success();
}
- @Override
- public boolean isConfigurable() {
- return false;
+ protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) {
+ userSuccessAction(context, user);
}
- @Override
- public Requirement[] getRequirementChoices() {
- return new Requirement[0];
+ protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String user) {
+ // no-op by default
}
- @Override
- public boolean isUserSetupAllowed() {
- return false;
+ protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) {
+ context.getEvent().user(user);
+ context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+ context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
+ .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
+ .build());
}
- @Override
- public String getHelpText() {
- return "Validates username and password from Authorization HTTP header";
- }
+ private String[] getUsernameAndPassword(final HttpHeaders httpHeaders) {
+ final List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
- @Override
- public List<ProviderConfigProperty> getConfigProperties() {
- return null;
- }
+ if (authHeaders == null || authHeaders.size() == 0) {
+ return null;
+ }
- @Override
- public Authenticator create(KeycloakSession session) {
- return new Authenticator() {
-
- private static final String BASIC = "Basic";
- private static final String BASIC_PREFIX = BASIC + " ";
-
- @Override
- public void authenticate(AuthenticationFlowContext context) {
- HttpRequest httpRequest = context.getHttpRequest();
- HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
- String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
-
- context.attempted();
-
- if (usernameAndPassword != null) {
- RealmModel realm = context.getRealm();
- UserModel user = context.getSession().users().getUserByUsername(usernameAndPassword[0], realm);
-
- if (user != null) {
- String password = usernameAndPassword[1];
- boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
-
- if (valid) {
- context.getAuthenticationSession().setAuthenticatedUser(user);
- context.success();
- } else {
- context.getEvent().user(user);
- context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
- context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
- .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
- .build());
- }
- }
- }
- }
+ String credentials = null;
- private String[] getUsernameAndPassword(HttpHeaders httpHeaders) {
- List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
+ for (final String authHeader : authHeaders) {
+ if (authHeader.startsWith(BASIC_PREFIX)) {
+ final String[] split = authHeader.trim().split("\\s+");
- if (authHeaders == null || authHeaders.size() == 0) {
- return null;
- }
-
- String credentials = null;
-
- for (String authHeader : authHeaders) {
- if (authHeader.startsWith(BASIC_PREFIX)) {
- String[] split = authHeader.trim().split("\\s+");
-
- if (split == null || split.length != 2) return null;
-
- credentials = split[1];
- }
- }
-
- try {
- return new String(Base64.decode(credentials)).split(":");
- } catch (IOException e) {
- throw new RuntimeException("Failed to parse credentials.", e);
- }
- }
-
- @Override
- public void action(AuthenticationFlowContext context) {
-
- }
-
- @Override
- public boolean requiresUser() {
- return false;
- }
-
- @Override
- public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
- return false;
- }
-
- @Override
- public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+ if (split == null || split.length != 2) return null;
+ credentials = split[1];
}
+ }
- @Override
- public void close() {
-
- }
- };
+ try {
+ return new String(Base64.decode(credentials)).split(":");
+ } catch (final IOException e) {
+ throw new RuntimeException("Failed to parse credentials.", e);
+ }
}
@Override
- public void init(Config.Scope config) {
+ public void action(final AuthenticationFlowContext context) {
}
@Override
- public void postInit(KeycloakSessionFactory factory) {
+ public boolean requiresUser() {
+ return false;
+ }
+ @Override
+ public boolean configuredFor(final KeycloakSession session, final RealmModel realm, final UserModel user) {
+ return false;
}
@Override
- public void close() {
+ public void setRequiredActions(final KeycloakSession session, final RealmModel realm, final UserModel user) {
}
@Override
- public String getId() {
- return PROVIDER_ID;
+ public void close() {
+
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java
new file mode 100755
index 0000000..01adca2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java
@@ -0,0 +1,115 @@
+/*
+ * 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.protocol.saml.profile.ecp.authenticator;
+
+import org.jboss.resteasy.spi.HttpRequest;
+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.common.util.Base64;
+import org.keycloak.events.Errors;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationExecutionModel.Requirement;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpBasicAuthenticatorFactory implements AuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "http-basic-authenticator";
+
+ @Override
+ public String getDisplayType() {
+ return "HTTP Basic Authentication";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return "basic";
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.REQUIRED,
+ Requirement.ALTERNATIVE,
+ Requirement.OPTIONAL,
+ AuthenticationExecutionModel.Requirement.DISABLED
+ };
+
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Validates username and password from Authorization HTTP header";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return null;
+ }
+
+ @Override
+ public Authenticator create(final KeycloakSession session) {
+ return new HttpBasicAuthenticator();
+ }
+
+ @Override
+ public void init(final Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index a8218c1..caa9709 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -193,7 +193,7 @@ public class SamlProtocol implements LoginProtocol {
if (samlClient.requiresEncryption()) {
PublicKey publicKey;
try {
- publicKey = SamlProtocolUtils.getEncryptionValidationKey(client);
+ publicKey = SamlProtocolUtils.getEncryptionKey(client);
} catch (Exception e) {
logger.error("failed", e);
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
@@ -457,7 +457,7 @@ public class SamlProtocol implements LoginProtocol {
if (samlClient.requiresEncryption()) {
PublicKey publicKey = null;
try {
- publicKey = SamlProtocolUtils.getEncryptionValidationKey(client);
+ publicKey = SamlProtocolUtils.getEncryptionKey(client);
} catch (Exception e) {
logger.error("failed", e);
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
index 026a54a..7ab97a4 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
@@ -103,7 +103,7 @@ public class SamlProtocolUtils {
* @return Public key for encryption.
* @throws VerificationException
*/
- public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
+ public static PublicKey getEncryptionKey(ClientModel client) throws VerificationException {
return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
index da693aa..d6683f1 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
@@ -97,9 +97,12 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
auth.requireView(client);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
+ if (client.getSecret() != null) {
+ rep.setSecret(client.getSecret());
+ }
if (auth.isRegistrationAccessToken()) {
- String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client, auth.getRegistrationAuth());
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateTokenSignature(session, auth);
rep.setRegistrationAccessToken(registrationAccessToken);
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
index dfed5aa..88986b5 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
@@ -60,6 +60,8 @@ public class ClientRegistrationAuth {
private RealmModel realm;
private JsonWebToken jwt;
private ClientInitialAccessModel initialAccessModel;
+ private String kid;
+ private String token;
public ClientRegistrationAuth(KeycloakSession session, ClientRegistrationProvider provider, EventBuilder event) {
this.session = session;
@@ -81,10 +83,13 @@ public class ClientRegistrationAuth {
return;
}
- ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, split[1]);
+ token = split[1];
+
+ ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, token);
if (tokenVerification.getError() != null) {
throw unauthorized(tokenVerification.getError().getMessage());
}
+ kid = tokenVerification.getKid();
jwt = tokenVerification.getJwt();
if (isInitialAccessToken()) {
@@ -95,6 +100,18 @@ public class ClientRegistrationAuth {
}
}
+ public String getToken() {
+ return token;
+ }
+
+ public String getKid() {
+ return kid;
+ }
+
+ public JsonWebToken getJwt() {
+ return jwt;
+ }
+
private boolean isBearerToken() {
return jwt != null && TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
index e2d4846..270ca2a 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
@@ -44,6 +44,27 @@ public class ClientRegistrationTokenUtils {
public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken";
public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken";
+ public static String updateTokenSignature(KeycloakSession session, ClientRegistrationAuth auth) {
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(session.getContext().getRealm());
+
+ if (keys.getKid().equals(auth.getKid())) {
+ return auth.getToken();
+ } else {
+ RegistrationAccessToken regToken = new RegistrationAccessToken();
+ regToken.setRegistrationAuth(auth.getRegistrationAuth().toString().toLowerCase());
+
+ regToken.type(auth.getJwt().getType());
+ regToken.id(auth.getJwt().getId());
+ regToken.issuedAt(Time.currentTime());
+ regToken.expiration(0);
+ regToken.issuer(auth.getJwt().getIssuer());
+ regToken.audience(auth.getJwt().getIssuer());
+
+ String token = new JWSBuilder().kid(keys.getKid()).jsonContent(regToken).rsa256(keys.getPrivateKey());
+ return token;
+ }
+ }
+
public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client, RegistrationAuth registrationAuth) {
return updateRegistrationAccessToken(session, session.getContext().getRealm(), session.getContext().getUri(), client, registrationAuth);
}
@@ -75,7 +96,8 @@ public class ClientRegistrationTokenUtils {
return TokenVerification.error(new RuntimeException("Invalid token", e));
}
- PublicKey publicKey = session.keys().getRsaPublicKey(realm, input.getHeader().getKeyId());
+ String kid = input.getHeader().getKeyId();
+ PublicKey publicKey = session.keys().getRsaPublicKey(realm, kid);
if (!RSAProvider.verify(input, publicKey)) {
return TokenVerification.error(new RuntimeException("Failed verify token"));
@@ -102,7 +124,7 @@ public class ClientRegistrationTokenUtils {
return TokenVerification.error(new RuntimeException("Invalid type of token"));
}
- return TokenVerification.success(jwt);
+ return TokenVerification.success(kid, jwt);
}
private static String setupToken(JsonWebToken jwt, KeycloakSession session, RealmModel realm, UriInfo uri, String id, String type, int expiration) {
@@ -127,22 +149,28 @@ public class ClientRegistrationTokenUtils {
protected static class TokenVerification {
+ private final String kid;
private final JsonWebToken jwt;
private final RuntimeException error;
- public static TokenVerification success(JsonWebToken jwt) {
- return new TokenVerification(jwt, null);
+ public static TokenVerification success(String kid, JsonWebToken jwt) {
+ return new TokenVerification(kid, jwt, null);
}
public static TokenVerification error(RuntimeException error) {
- return new TokenVerification(null, error);
+ return new TokenVerification(null,null, error);
}
- private TokenVerification(JsonWebToken jwt, RuntimeException error) {
+ private TokenVerification(String kid, JsonWebToken jwt, RuntimeException error) {
+ this.kid = kid;
this.jwt = jwt;
this.error = error;
}
+ public String getKid() {
+ return kid;
+ }
+
public JsonWebToken getJwt() {
return jwt;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java
index eca5ca1..bad5bc4 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java
@@ -17,8 +17,6 @@
package org.keycloak.services.clientregistration.policy;
-import org.keycloak.services.clientregistration.RegistrationAccessToken;
-
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
index 6c17794..a391c1d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
@@ -129,6 +129,7 @@ public class ServerInfoAdminResource {
for (String name : providerIds) {
ProviderRepresentation provider = new ProviderRepresentation();
ProviderFactory<?> pi = session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name);
+ provider.setOrder(pi.order());
if (ServerInfoAwareProviderFactory.class.isAssignableFrom(pi.getClass())) {
provider.setOperationalInfo(((ServerInfoAwareProviderFactory) pi).getOperationalInfo());
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 28392f7..ebc89be 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.services.resources.admin;
+import com.fasterxml.jackson.core.type.TypeReference;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
@@ -29,6 +30,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Event;
import org.keycloak.events.EventQuery;
import org.keycloak.events.EventStoreProvider;
@@ -50,6 +52,7 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache;
@@ -102,9 +105,9 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.regex.PatternSyntaxException;
import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
+import static org.keycloak.util.JsonSerialization.readValue;
/**
* Base resource class for the admin REST api of one realm
@@ -811,6 +814,35 @@ public class RealmAdminResource {
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
}
+ /**
+ * Test SMTP connection with current logged in user
+ *
+ * @param config SMTP server configuration
+ * @return
+ * @throws Exception
+ */
+ @Path("testSMTPConnection/{config}")
+ @POST
+ @NoCache
+ public Response testSMTPConnection(final @PathParam("config") String config) throws Exception {
+ Map<String, String> settings = readValue(config, new TypeReference<Map<String, String>>() {
+ });
+
+ try {
+ UserModel user = auth.adminAuth().getUser();
+ if (user.getEmail() == null) {
+ return ErrorResponse.error("Logged in user does not have an e-mail.", Response.Status.INTERNAL_SERVER_ERROR);
+ }
+ session.getProvider(EmailTemplateProvider.class).sendSmtpTestEmail(settings, user);
+ } catch (Exception e) {
+ e.printStackTrace();
+ logger.errorf("Failed to send email \n %s", e.getCause());
+ return ErrorResponse.error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR);
+ }
+
+ return Response.noContent().build();
+ }
+
@Path("identity-provider")
public IdentityProvidersResource getIdentityProviderResource() {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 6ed677f..c7b9945 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -52,6 +52,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.provider.ProviderFactory;
@@ -162,8 +163,9 @@ public class UsersResource {
try {
UserModel user = session.users().addUser(realm, rep.getUsername());
Set<String> emptySet = Collections.emptySet();
- UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false);
+ UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false);
+ RepresentationToModel.createCredentials(rep, session, realm, user);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success();
if (session.getTransactionManager().isActive()) {
diff --git a/services/src/main/resources/DockerComposeYamlReadme.md b/services/src/main/resources/DockerComposeYamlReadme.md
new file mode 100644
index 0000000..84dff48
--- /dev/null
+++ b/services/src/main/resources/DockerComposeYamlReadme.md
@@ -0,0 +1,23 @@
+# Docker Compose YAML Installation
+-----------------------------------
+
+*NOTE:* This installation method is intended for development use only. Please don't ever let this anywhere near prod!
+
+## Keycloak Realm Assumptions:
+ - Client configuration has not changed since the installtion files were generated. If you change your client configuration, be sure to grab a re-generated installtion .zip from the 'Installation' tab.
+ - Keycloak server is started with the 'docker' feature enabled. I.E. -Dkeycloak.profile.feature.docker=enabled
+
+## Running the Installation:
+ - Spin up a fully functional docker registry with:
+
+ docker-compose up
+
+ - Now you can login against the registry and perform normal operations:
+
+ docker login -u $username -p $password localhost:5000
+
+ docker pull centos:7
+ docker tag centos:7 localhost:5000/centos:7
+ docker push localhost:5000/centos:7
+
+ ** Remember that users for the `docker login` command must be configured and available in the keycloak realm that hosts the docker client.
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index 208f16d..2b11382 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -34,6 +34,7 @@ org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFac
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
-org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator
+org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
+org.keycloak.protocol.docker.DockerAuthenticatorFactory
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
index a0d8052..f38a5c2 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
@@ -22,4 +22,6 @@ org.keycloak.protocol.saml.installation.SamlSPDescriptorClientInstallation
org.keycloak.protocol.saml.installation.SamlIDPDescriptorClientInstallation
org.keycloak.protocol.saml.installation.ModAuthMellonClientInstallation
org.keycloak.protocol.saml.installation.KeycloakSamlSubsystemInstallation
-
+org.keycloak.protocol.docker.installation.DockerVariableOverrideInstallationProvider
+org.keycloak.protocol.docker.installation.DockerRegistryConfigFileInstallationProvider
+org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
index 38e1b5a..e954f2e 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
@@ -16,4 +16,5 @@
#
org.keycloak.protocol.oidc.OIDCLoginProtocolFactory
-org.keycloak.protocol.saml.SamlProtocolFactory
\ No newline at end of file
+org.keycloak.protocol.saml.SamlProtocolFactory
+org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
index 04f090e..95b79cf 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
@@ -35,4 +35,5 @@ org.keycloak.protocol.saml.mappers.GroupMembershipMapper
org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper
org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper
org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper
+org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper
diff --git a/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java
new file mode 100644
index 0000000..a5f494c
--- /dev/null
+++ b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java
@@ -0,0 +1,193 @@
+package org.keycloak.procotol.docker.installation;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.common.util.CertificateUtils;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider;
+
+import javax.ws.rs.core.Response;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.junit.Assert.fail;
+import static org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider.ROOT_DIR;
+
+public class DockerComposeYamlInstallationProviderTest {
+
+ DockerComposeYamlInstallationProvider installationProvider;
+ static Certificate certificate;
+
+ @BeforeClass
+ public static void setUp_beforeClass() throws NoSuchAlgorithmException {
+ final KeyPairGenerator keyGen;
+ keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048, new SecureRandom());
+
+ final KeyPair keypair = keyGen.generateKeyPair();
+ certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, "test-realm");
+ }
+
+ @Before
+ public void setUp() {
+ installationProvider = new DockerComposeYamlInstallationProvider();
+ }
+
+ private Response fireInstallationProvider() throws IOException {
+ ByteArrayOutputStream byteStream = null;
+ ZipOutputStream zipOutput = null;
+ byteStream = new ByteArrayOutputStream();
+ zipOutput = new ZipOutputStream(byteStream);
+
+ return installationProvider.generateInstallation(zipOutput, byteStream, certificate, new URL("http://localhost:8080/auth"), "docker-test", "docker-registry");
+ }
+
+ @Test
+ @Ignore // Used only for smoke testing
+ public void writeToRealZip() throws IOException {
+ final Response response = fireInstallationProvider();
+ final byte[] responseBytes = (byte[]) response.getEntity();
+ FileUtils.writeByteArrayToFile(new File("target/keycloak-docker-compose-yaml.zip"), responseBytes);
+ }
+
+ @Test
+ public void testAllTheZipThings() throws Exception {
+ final Response response = fireInstallationProvider();
+ assertThat("compose YAML returned non-ok response", response.getStatus(), equalTo(Response.Status.OK.getStatusCode()));
+
+ shouldIncludeDockerComposeYamlInZip(getZipResponseFromInstallProvider(response));
+ shouldIncludeReadmeInZip(getZipResponseFromInstallProvider(response));
+ shouldWriteBlankDataDirectoryInZip(getZipResponseFromInstallProvider(response));
+ shouldWriteCertDirectoryInZip(getZipResponseFromInstallProvider(response));
+ shouldWriteSslCertificateInZip(getZipResponseFromInstallProvider(response));
+ shouldWritePrivateKeyInZip(getZipResponseFromInstallProvider(response));
+ }
+
+ public void shouldIncludeDockerComposeYamlInZip(ZipInputStream zipInput) throws Exception {
+ final Optional<String> dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "docker-compose.yaml");
+
+ assertThat("Could not find docker-compose.yaml file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true));
+ final boolean zipFileContentEqualsTestFile = IOUtils.contentEquals(new ByteArrayInputStream(dockerComposeFileContents.get().getBytes()), new FileInputStream("src/test/resources/docker-compose-expected.yaml"));
+ assertThat("Invalid docker-compose file contents: \n" + dockerComposeFileContents.get(), zipFileContentEqualsTestFile, equalTo(true));
+ }
+
+ public void shouldIncludeReadmeInZip(ZipInputStream zipInput) throws Exception {
+ final Optional<String> dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "README.md");
+
+ assertThat("Could not find README.md file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true));
+ }
+
+ public void shouldWriteBlankDataDirectoryInZip(ZipInputStream zipInput) throws Exception {
+ ZipEntry zipEntry;
+ boolean dataDirFound = false;
+ while ((zipEntry = zipInput.getNextEntry()) != null) {
+ try {
+ if (zipEntry.getName().equals(ROOT_DIR + "data/")) {
+ dataDirFound = true;
+ assertThat("Zip entry for data directory is not the correct type", zipEntry.isDirectory(), equalTo(true));
+ }
+ } finally {
+ zipInput.closeEntry();
+ }
+ }
+
+ assertThat("Could not find data directory", dataDirFound, equalTo(true));
+ }
+
+ public void shouldWriteCertDirectoryInZip(ZipInputStream zipInput) throws Exception {
+ ZipEntry zipEntry;
+ boolean certsDirFound = false;
+ while ((zipEntry = zipInput.getNextEntry()) != null) {
+ try {
+ if (zipEntry.getName().equals(ROOT_DIR + "certs/")) {
+ certsDirFound = true;
+ assertThat("Zip entry for cert directory is not the correct type", zipEntry.isDirectory(), equalTo(true));
+ }
+ } finally {
+ zipInput.closeEntry();
+ }
+ }
+
+ assertThat("Could not find cert directory", certsDirFound, equalTo(true));
+ }
+
+ public void shouldWriteSslCertificateInZip(ZipInputStream zipInput) throws Exception {
+ final Optional<String> localhostCertificateFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.crt");
+
+ assertThat("Could not find localhost certificate", localhostCertificateFileContents.isPresent(), equalTo(true));
+ final X509Certificate x509Certificate = PemUtils.decodeCertificate(localhostCertificateFileContents.get());
+ assertThat("Invalid x509 given by docker-compose YAML", x509Certificate, notNullValue());
+ }
+
+ public void shouldWritePrivateKeyInZip(ZipInputStream zipInput) throws Exception {
+ final Optional<String> localhostPrivateKeyFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.key");
+
+ assertThat("Could not find localhost private key", localhostPrivateKeyFileContents.isPresent(), equalTo(true));
+ final PrivateKey privateKey = PemUtils.decodePrivateKey(localhostPrivateKeyFileContents.get());
+ assertThat("Invalid private Key given by docker-compose YAML", privateKey, notNullValue());
+ }
+
+ private ZipInputStream getZipResponseFromInstallProvider(Response response) throws IOException {
+ final Object responseEntity = response.getEntity();
+ if (!(responseEntity instanceof byte[])) {
+ fail("Recieved non-byte[] entity for docker-compose YAML installation response");
+ }
+
+ return new ZipInputStream(new ByteArrayInputStream((byte[]) responseEntity));
+ }
+
+ private static Optional<String> getFileContents(final ZipInputStream zipInputStream, final String fileName) throws IOException {
+ ZipEntry zipEntry;
+ while ((zipEntry = zipInputStream.getNextEntry()) != null) {
+ try {
+ if (zipEntry.getName().equals(fileName)) {
+ return Optional.of(readBytesToString(zipInputStream));
+ }
+ } finally {
+ zipInputStream.closeEntry();
+ }
+ }
+
+ // fall-through case if file name not found:
+ return Optional.empty();
+ }
+
+ private static String readBytesToString(final InputStream inputStream) throws IOException {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final byte[] buffer = new byte[4096];
+ int bytesRead;
+
+ try {
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ output.write(buffer, 0, bytesRead);
+ }
+ } finally {
+ output.close();
+ }
+
+ return new String(output.toByteArray());
+ }
+}
diff --git a/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java
new file mode 100644
index 0000000..0fa8cb9
--- /dev/null
+++ b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java
@@ -0,0 +1,41 @@
+package org.keycloak.procotol.docker.installation;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.models.utils.Base32;
+import org.keycloak.protocol.docker.DockerKeyIdentifier;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Docker gets really unhappy if the key identifier is not in the format documented here:
+ * @see https://github.com/docker/libtrust/blob/master/key.go#L24
+ */
+public class DockerKeyIdentifierTest {
+
+ String keyIdentifierString;
+ PublicKey publicKey;
+
+ @Before
+ public void shouldBlah() throws Exception {
+ final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048, new SecureRandom());
+
+ final KeyPair keypair = keyGen.generateKeyPair();
+ publicKey = keypair.getPublic();
+ final DockerKeyIdentifier identifier = new DockerKeyIdentifier(publicKey);
+ keyIdentifierString = identifier.toString();
+ }
+
+ @Test
+ public void shoulProduceExpectedKeyFormat() {
+ assertThat("Every 4 chars are not delimted by colon", keyIdentifierString.matches("([\\w]{4}:){11}[\\w]{4}"), equalTo(true));
+ }
+}
diff --git a/services/src/test/resources/docker-compose-expected.yaml b/services/src/test/resources/docker-compose-expected.yaml
new file mode 100644
index 0000000..3c912de
--- /dev/null
+++ b/services/src/test/resources/docker-compose-expected.yaml
@@ -0,0 +1,15 @@
+registry:
+ image: registry:2
+ ports:
+ - 127.0.0.1:5000:5000
+ environment:
+ REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
+ REGISTRY_HTTP_TLS_CERTIFICATE: /opt/certs/localhost.crt
+ REGISTRY_HTTP_TLS_KEY: /opt/certs/localhost.key
+ REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080/auth/realms/docker-test/protocol/docker-v2/auth
+ REGISTRY_AUTH_TOKEN_SERVICE: docker-registry
+ REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/realms/docker-test
+ REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem
+ volumes:
+ - ./data:/data:z
+ - ./certs:/opt/certs:z
\ No newline at end of file
testsuite/integration-arquillian/HOW-TO-RUN.md 106(+104 -2)
diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index cd818c2..225d709 100644
--- a/testsuite/integration-arquillian/HOW-TO-RUN.md
+++ b/testsuite/integration-arquillian/HOW-TO-RUN.md
@@ -61,7 +61,7 @@ More info: http://javahowto.blogspot.cz/2010/09/java-agentlibjdwp-for-attaching.
Analogically, there is the same behaviour for JBoss based app server as for auth server. The default port is set to 5006. There are app server properties.
-Dapp.server.debug.port=$PORT
- -Dapp.server.debug.suspend=y
+ -Dapp.server.debug.suspend=y
## Testsuite logging
@@ -262,6 +262,8 @@ The UI tests are focused on the Admin Console as well as on some login scenarios
The tests also use some constants placed in [test-constants.properties](tests/base/src/test/resources/test-constants.properties). A different file can be specified by `-Dtestsuite.constants=path/to/different-test-constants.properties`
+In case a custom `settings.xml` is used for Maven, you need to specify it also in `-Dkie.maven.settings.custom=path/to/settings.xml`.
+
#### Execution example
```
mvn -f testsuite/integration-arquillian/tests/other/console/pom.xml \
@@ -452,7 +454,7 @@ First compile the Infinispan/JDG test server via the following command:
`mvn -Pcache-server-infinispan -f testsuite/integration-arquillian -DskipTests clean install`
or
-
+
`mvn -Pcache-server-jdg -f testsuite/integration-arquillian -DskipTests clean install`
Then you can run the tests using the following command (adjust the test specification according to your needs):
@@ -464,3 +466,103 @@ or
`mvn -Pcache-server-jdg -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test`
_Someone using IntelliJ IDEA, please describe steps for that IDE_
+
+## Run Docker Authentication test
+
+First, validate that your machine has a valid docker installation and that it is available to the JVM running the test.
+The exact steps to configure Docker depend on the operating system.
+
+By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand.
+The exact command line arguments depend on the operating system.
+
+### General guidelines
+
+If docker daemon doesn't run locally, or if you're not running on Linux, you may need
+ to determine the IP of the bridge interface or local interface that Docker daemon can use to connect to Keycloak Server.
+ Then specify that IP as additional system property called *host.ip*, for example:
+
+ -Dhost.ip=192.168.64.1
+
+If using Docker for Mac, you can create an alias for your local network interface:
+
+ sudo ifconfig lo0 alias 10.200.10.1/24
+
+Then pass the IP as *host.ip*:
+
+ -Dhost.ip=10.200.10.1
+
+
+If you're running a Docker fork that always lists a host component of an image on `docker images` (e.g. Fedora / RHEL Docker)
+use `-Ddocker.io-prefix-explicit=true` argument when running the test.
+
+
+### Fedora
+
+On Fedora one way to set up Docker server is the following:
+
+ # install docker
+ sudo dnf install docker
+
+ # configure docker
+ # remove --selinux-enabled from OPTIONS
+ sudo vi /etc/sysconfig/docker
+
+ # create docker group and add your user (so docker wouldn't need root permissions)
+ sudo groupadd docker && sudo gpasswd -a ${USER} docker && sudo systemctl restart docker
+ newgrp docker
+
+ # you need to login again after this
+
+
+ # make sure Docker is available
+ docker pull registry:2
+
+You may also need to add an iptables rule to allow container to host traffic
+
+ sudo iptables -I INPUT -i docker0 -j ACCEPT
+
+Then, run the test passing `-Ddocker.io-prefix-explicit=true`:
+
+ mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
+ clean test \
+ -Dtest=DockerClientTest \
+ -Dkeycloak.profile.feature.docker=enabled \
+ -Ddocker.io-prefix-explicit=true
+
+
+### macOS
+
+On macOS all you need to do is install Docker for Mac, start it up, and check that it works:
+
+ # make sure Docker is available
+ docker pull registry:2
+
+Be especially careful to restart Docker server after every sleep / suspend to ensure system clock of Docker VM is synchronized with
+that of the host operating system - Docker for Mac runs inside a VM.
+
+
+Then, run the test passing `-Dhost.ip=IP` where IP corresponds to en0 interface or an alias for localhost:
+
+ mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
+ clean test \
+ -Dtest=DockerClientTest \
+ -Dkeycloak.profile.feature.docker=enabled \
+ -Dhost.ip=10.200.10.1
+
+
+
+### Running Docker test against Keycloak Server distribution
+
+Make sure to build the distribution:
+
+ mvn clean install -f distribution
+
+Then, before running the test, setup Keycloak Server distribution for the tests:
+
+ mvn -f testsuite/integration-arquillian/servers/pom.xml \
+ clean install \
+ -Pauth-server-wildfly
+
+When running the test, add the following arguments to the command line:
+
+ -Pauth-server-wildfly -Pauth-server-enable-disable-feature -Dfeature.name=docker -Dfeature.value=enabled
diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml
index 3775738..9b23cd1 100644
--- a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml
@@ -309,7 +309,7 @@
</goals>
<configuration>
<executable>${common.resources}/install-patch.${script.suffix}</executable>
- <workingDirectory>${app.server.home}/bin</workingDirectory>
+ <workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
<environmentVariables>
<JAVA_HOME>${app.server.java.home}</JAVA_HOME>
<JBOSS_HOME>${app.server.jboss.home}</JBOSS_HOME>
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl
index d104e37..276f389 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl
@@ -36,6 +36,15 @@
</provider>
</spi>
</xsl:variable>
+ <xsl:variable name="samlPortsDefinition">
+ <spi name="login-protocol">
+ <provider name="saml" enabled="true">
+ <properties>
+ <property name="knownProtocols" value="["http=${{auth.server.http.port}}","https=${{auth.server.https.port}}"]"/>
+ </properties>
+ </provider>
+ </spi>
+ </xsl:variable>
<xsl:variable name="themeModuleDefinition">
<modules>
<module>org.keycloak.testsuite.integration-arquillian-testsuite-providers</module>
@@ -60,11 +69,12 @@
</xsl:copy>
</xsl:template>
- <!--inject truststore-->
+ <!--inject truststore and SAML port-protocol mappings-->
<xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsKS)]">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
<xsl:copy-of select="$truststoreDefinition"/>
+ <xsl:copy-of select="$samlPortsDefinition"/>
</xsl:copy>
</xsl:template>
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
index 775aa5a..cf8b3b2 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
@@ -100,7 +100,6 @@
<outputDirectory>${project.build.directory}/unpacked</outputDirectory>
</artifactItem>
</artifactItems>
- <excludes>**/product.conf</excludes>
</configuration>
</execution>
<execution>
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index caa76aa..59116e8 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -88,6 +88,28 @@
<artifactId>greenmail</artifactId>
<scope>compile</scope>
</dependency>
+ <!--<dependency>-->
+ <!--<groupId>com.spotify</groupId>-->
+ <!--<artifactId>docker-client</artifactId>-->
+ <!--<version>8.3.2</version>-->
+ <!--<scope>test</scope>-->
+ <!--<exclusions>-->
+ <!--<exclusion>-->
+ <!--<groupId>javax.ws.rs</groupId>-->
+ <!--<artifactId>javax.ws.rs-api</artifactId>-->
+ <!--</exclusion>-->
+ <!--<exclusion>-->
+ <!--<groupId>com.github.jnr</groupId>-->
+ <!--<artifactId>jnr-unixsocket</artifactId>-->
+ <!--</exclusion>-->
+ <!--</exclusions>-->
+ <!--</dependency>-->
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <version>1.2.1</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
index e8852bc..f9d557b 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
@@ -39,6 +39,7 @@ public class URLProvider extends URLResourceProvider {
protected final Logger log = Logger.getLogger(this.getClass());
+ public static final String BOUND_TO_ALL = "0.0.0.0";
public static final String LOCALHOST_ADDRESS = "127.0.0.1";
public static final String LOCALHOST_HOSTNAME = "localhost";
@@ -59,6 +60,7 @@ public class URLProvider extends URLResourceProvider {
if (url != null) {
try {
url = fixLocalhost(url);
+ url = fixBoundToAll(url);
url = removeTrailingSlash(url);
if (appServerSslRequired) {
url = fixSsl(url);
@@ -111,6 +113,14 @@ public class URLProvider extends URLResourceProvider {
return url;
}
+ public URL fixBoundToAll(URL url) throws MalformedURLException {
+ URL fixedUrl = url;
+ if (url.getHost().contains(BOUND_TO_ALL)) {
+ fixedUrl = new URL(fixedUrl.toExternalForm().replace(BOUND_TO_ALL, LOCALHOST_HOSTNAME));
+ }
+ return fixedUrl;
+ }
+
public URL fixLocalhost(URL url) throws MalformedURLException {
URL fixedUrl = url;
if (url.getHost().contains(LOCALHOST_ADDRESS)) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java
index 9e582d8..bc089a9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java
@@ -16,7 +16,9 @@
*/
package org.keycloak.testsuite.console.page.fragment;
+import org.jboss.arquillian.graphene.fragment.Root;
import org.keycloak.testsuite.page.AbstractAlert;
+import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -44,6 +46,9 @@ public class AdminConsoleAlert extends AbstractAlert {
public void close() {
closeButton.click();
+ WaitUtils.pause(500); // Sometimes, when a test is too fast,
+ // one of the consecutive alerts is not displayed;
+ // to prevent this we need to slow down a bit
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
index ad71d38..84b8282 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
@@ -25,6 +25,10 @@ import org.keycloak.common.Profile;
*/
public class ProfileAssume {
+ public static void assumeFeatureEnabled(Profile.Feature feature) {
+ Assume.assumeTrue("Ignoring test as " + feature.name() + " is not enabled", Profile.isFeatureEnabled(feature));
+ }
+
public static void assumePreview() {
Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", !Profile.getName().equals("product"));
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java
new file mode 100644
index 0000000..b8eb487
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java
@@ -0,0 +1,55 @@
+package org.keycloak.testsuite.updaters;
+
+import org.keycloak.admin.client.resource.IdentityProviderResource;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import java.io.Closeable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class IdentityProviderAttributeUpdater {
+
+ private final Map<String, String> originalAttributes = new HashMap<>();
+
+ private final IdentityProviderResource identityProviderResource;
+
+ private final IdentityProviderRepresentation rep;
+
+ public IdentityProviderAttributeUpdater(IdentityProviderResource identityProviderResource) {
+ this.identityProviderResource = identityProviderResource;
+ this.rep = identityProviderResource.toRepresentation();
+ if (this.rep.getConfig() == null) {
+ this.rep.setConfig(new HashMap<>());
+ }
+ }
+
+ public IdentityProviderAttributeUpdater setAttribute(String name, String value) {
+ if (! originalAttributes.containsKey(name)) {
+ this.originalAttributes.put(name, this.rep.getConfig().put(name, value));
+ } else {
+ this.rep.getConfig().put(name, value);
+ }
+ return this;
+ }
+
+ public IdentityProviderAttributeUpdater removeAttribute(String name) {
+ if (! originalAttributes.containsKey(name)) {
+ this.originalAttributes.put(name, this.rep.getConfig().put(name, null));
+ } else {
+ this.rep.getConfig().put(name, null);
+ }
+ return this;
+ }
+
+ public Closeable update() {
+ identityProviderResource.update(rep);
+
+ return () -> {
+ rep.getConfig().putAll(originalAttributes);
+ identityProviderResource.update(rep);
+ };
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
index bc0b787..7ebaa1d 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
@@ -43,6 +43,10 @@ public class GreenMailRule extends ExternalResource {
greenMail.start();
}
+ public void credentials(String username, String password) {
+ greenMail.setUser(username, password);
+ }
+
@Override
protected void after() {
if (greenMail != null) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
index 420fad8..26c398c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.adapter.servlet;
+import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
index 0481518..57fe6de 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
@@ -19,6 +19,8 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Assert;
import org.junit.Test;
+import org.keycloak.models.utils.DefaultAuthenticationFlows;
+import org.keycloak.protocol.docker.DockerAuthenticator;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
@@ -155,6 +157,13 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
addExecInfo(execs, "OTP", "direct-grant-validate-otp", false, 0, 2, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
expected.add(new FlowExecutions(flow, execs));
+ flow = newFlow("docker auth", "Used by Docker clients to authenticate against the IDP", "basic-flow", true, true);
+ addExecExport(flow, null, false, "docker-http-basic-authenticator", false, null, REQUIRED, 10);
+
+ execs = new LinkedList<>();
+ addExecInfo(execs, "Docker Authenticator", "docker-http-basic-authenticator", false, 0, 0, REQUIRED, null, new String[]{REQUIRED});
+ expected.add(new FlowExecutions(flow, execs));
+
flow = newFlow("first broker login", "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"basic-flow", true, true);
addExecExport(flow, null, false, "idp-review-profile", false, "review profile config", REQUIRED, 10);
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 e13794d..f55e90f 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
@@ -151,6 +151,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates the password supplied as a 'password' form parameter in direct grant request");
addProviderInfo(result, "direct-grant-validate-username", "Username Validation",
"Validates the username supplied as a 'username' form parameter in direct grant request");
+ addProviderInfo(result, "docker-http-basic-authenticator", "Docker Authenticator", "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure");
addProviderInfo(result, "expected-param-authenticator", "TEST: Expected Parameter",
"You will be approved if you send query string parameter 'foo' with expected value.");
addProviderInfo(result, "http-basic-authenticator", "HTTP Basic Authentication", "Validates username and password from Authorization HTTP header");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
index 555b38c..6f463c9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
@@ -16,9 +16,12 @@
*/
package org.keycloak.testsuite.admin;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.AuthorizationProviderFactory;
import org.keycloak.authorization.model.Resource;
@@ -48,6 +51,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.AdminClientUtil;
import javax.ws.rs.ClientErrorException;
@@ -65,6 +69,11 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
public static final String CLIENT_NAME = "application";
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(FineGrainAdminUnitTest.class);
+ }
+
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation testRealmRep = new RealmRepresentation();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
index 7c6ced5..3318a6d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
@@ -16,6 +16,8 @@
*/
package org.keycloak.testsuite.admin;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
@@ -40,6 +42,7 @@ import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.AdminClientUtil;
import javax.ws.rs.ClientErrorException;
@@ -58,6 +61,11 @@ public class IllegalAdminUpgradeTest extends AbstractKeycloakTest {
public static final String CLIENT_NAME = "application";
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(FineGrainAdminUnitTest.class);
+ }
+
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation testRealmRep = new RealmRepresentation();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java
new file mode 100644
index 0000000..303cfd6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.admin;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.GreenMailRule;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import javax.mail.internet.MimeMessage;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.keycloak.util.JsonSerialization.writeValueAsPrettyString;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
+ */
+public class SMTPConnectionTest extends AbstractKeycloakTest {
+
+ @Rule
+ public GreenMailRule greenMailRule = new GreenMailRule();
+ private RealmResource realm;
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ }
+
+ @Before
+ public void before() {
+ realm = adminClient.realm("master");
+ List<UserRepresentation> admin = realm.users().search("admin", 0, 1);
+ UserRepresentation user = UserBuilder.edit(admin.get(0)).email("admin@localhost").build();
+ realm.users().get(user.getId()).update(user);
+ }
+
+ private String settings(String host, String port, String from, String auth, String ssl, String starttls,
+ String username, String password) throws Exception {
+ Map<String, String> config = new HashMap<>();
+ config.put("host", host);
+ config.put("port", port);
+ config.put("from", from);
+ config.put("auth", auth);
+ config.put("ssl", ssl);
+ config.put("starttls", starttls);
+ config.put("user", username);
+ config.put("password", password);
+ return writeValueAsPrettyString(config);
+ }
+
+ @Test
+ public void testWithEmptySettings() throws Exception {
+ Response response = realm.testSMTPConnection(settings(null, null, null, null, null, null,
+ null, null));
+ assertStatus(response, 500);
+ }
+
+ @Test
+ public void testWithProperSettings() throws Exception {
+ Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", null, null, null,
+ null, null));
+ assertStatus(response, 204);
+ assertMailReceived();
+ }
+
+ @Test
+ public void testWithAuthEnabledCredentialsEmpty() throws Exception {
+ Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null,
+ null, null));
+ assertStatus(response, 500);
+ }
+
+ @Test
+ public void testWithAuthEnabledValidCredentials() throws Exception {
+ greenMailRule.credentials("admin@localhost", "admin");
+ Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null,
+ "admin@localhost", "admin"));
+ assertStatus(response, 204);
+ }
+
+ private void assertStatus(Response response, int status) {
+ assertEquals(status, response.getStatus());
+ response.close();
+ }
+
+ private void assertMailReceived() {
+ if (greenMailRule.getReceivedMessages().length == 1) {
+ try {
+ MimeMessage message = greenMailRule.getReceivedMessages()[0];
+ assertEquals("[KEYCLOAK] - SMTP test message", message.getSubject());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else {
+ fail("E-mail was not received");
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 567b284..8314be0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -18,9 +18,11 @@
package org.keycloak.testsuite.admin;
import org.hamcrest.Matchers;
+import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
@@ -29,9 +31,13 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleMappingResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.common.util.Base64;
+import org.keycloak.credential.CredentialModel;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.Constants;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
@@ -44,10 +50,12 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.GreenMailRule;
@@ -65,12 +73,15 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
-
import java.util.concurrent.atomic.AtomicInteger;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -90,7 +101,6 @@ public class UserTest extends AbstractAdminTest {
@Page
protected LoginPasswordUpdatePage passwordUpdatePage;
-
@ArquillianResource
protected OAuthClient oAuthClient;
@@ -103,6 +113,14 @@ public class UserTest extends AbstractAdminTest {
@Page
protected LoginPage loginPage;
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(
+ AbstractAdminTest.class,
+ AbstractTestRealmKeycloakTest.class,
+ UserResource.class);
+ }
+
public String createUser() {
return createUser("user1", "user1@localhost");
}
@@ -169,6 +187,73 @@ public class UserTest extends AbstractAdminTest {
}
@Test
+ public void createUserWithHashedCredentials() {
+ UserRepresentation user = new UserRepresentation();
+ user.setUsername("user_creds");
+ user.setEmail("email@localhost");
+
+ CredentialRepresentation hashedPassword = new CredentialRepresentation();
+ hashedPassword.setAlgorithm("my-algorithm");
+ hashedPassword.setCounter(11);
+ hashedPassword.setCreatedDate(1001l);
+ hashedPassword.setDevice("deviceX");
+ hashedPassword.setDigits(6);
+ hashedPassword.setHashIterations(22);
+ hashedPassword.setHashedSaltedValue("ABC");
+ hashedPassword.setPeriod(99);
+ hashedPassword.setSalt(Base64.encodeBytes("theSalt".getBytes()));
+ hashedPassword.setType(CredentialRepresentation.PASSWORD);
+
+ user.setCredentials(Arrays.asList(hashedPassword));
+
+ createUser(user);
+
+ CredentialModel credentialHashed = fetchCredentials("user_creds");
+ assertNotNull("Expecting credential", credentialHashed);
+ assertEquals("my-algorithm", credentialHashed.getAlgorithm());
+ assertEquals(11, credentialHashed.getCounter());
+ assertEquals(Long.valueOf(1001), credentialHashed.getCreatedDate());
+ assertEquals("deviceX", credentialHashed.getDevice());
+ assertEquals(6, credentialHashed.getDigits());
+ assertEquals(22, credentialHashed.getHashIterations());
+ assertEquals("ABC", credentialHashed.getValue());
+ assertEquals(99, credentialHashed.getPeriod());
+ assertEquals("theSalt", new String(credentialHashed.getSalt()));
+ assertEquals(CredentialRepresentation.PASSWORD, credentialHashed.getType());
+ }
+
+ @Test
+ public void createUserWithRawCredentials() {
+ UserRepresentation user = new UserRepresentation();
+ user.setUsername("user_rawpw");
+ user.setEmail("email.raw@localhost");
+
+ CredentialRepresentation rawPassword = new CredentialRepresentation();
+ rawPassword.setValue("ABCD");
+ rawPassword.setType(CredentialRepresentation.PASSWORD);
+ user.setCredentials(Arrays.asList(rawPassword));
+
+ createUser(user);
+
+ CredentialModel credential = fetchCredentials("user_rawpw");
+ assertNotNull("Expecting credential", credential);
+ assertEquals(PasswordPolicy.HASH_ALGORITHM_DEFAULT, credential.getAlgorithm());
+ assertEquals(PasswordPolicy.HASH_ITERATIONS_DEFAULT, credential.getHashIterations());
+ assertNotEquals("ABCD", credential.getValue());
+ assertEquals(CredentialRepresentation.PASSWORD, credential.getType());
+ }
+
+ private CredentialModel fetchCredentials(String username) {
+ return getTestingClient().server(REALM_NAME).fetch(session -> {
+ RealmModel realm = session.getContext().getRealm();
+ UserModel user = session.users().getUserByUsername(username, realm);
+ List<CredentialModel> storedCredentialsByType = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialRepresentation.PASSWORD);
+ System.out.println(storedCredentialsByType.size());
+ return storedCredentialsByType.get(0);
+ }, CredentialModel.class);
+ }
+
+ @Test
public void createDuplicatedUser3() {
createUser();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
index 6500ad0..6332dd0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
@@ -1,6 +1,5 @@
package org.keycloak.testsuite.broker;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -14,7 +13,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
-import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.ConsentPage;
import org.keycloak.testsuite.util.*;
@@ -40,6 +38,8 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.*;
public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
+ protected IdentityProviderResource identityProviderResource;
+
@Before
public void beforeBrokerTest() {
log.debug("creating user for realm " + bc.providerRealmName());
@@ -61,7 +61,8 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
log.debug("adding identity provider to realm " + bc.consumerRealmName());
RealmResource realm = adminClient.realm(bc.consumerRealmName());
- realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext));
+ realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext)).close();
+ identityProviderResource = realm.identityProviders().get(bc.getIDPAlias());
// addClients
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
@@ -70,7 +71,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.providerRealmName());
- providerRealm.clients().create(client);
+ providerRealm.clients().create(client).close();
}
}
@@ -80,7 +81,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.consumerRealmName());
- consumerRealm.clients().create(client);
+ consumerRealm.clients().create(client).close();
}
}
@@ -90,6 +91,12 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
@Test
public void testLogInAsUserInIDP() {
+ loginUser();
+
+ testSingleLogout();
+ }
+
+ protected void loginUser() {
driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
log.debug("Clicking social " + bc.getIDPAlias());
@@ -98,16 +105,16 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
waitForPage(driver, "log in to");
Assert.assertTrue("Driver should be on the provider realm page right now",
- driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
+ driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
log.debug("Logging in");
accountLoginPage.login(bc.getUserLogin(), bc.getUserPassword());
waitForPage(driver, "update account information");
- Assert.assertTrue(updateAccountInformationPage.isCurrent());
+ updateAccountInformationPage.assertCurrent();
Assert.assertTrue("We must be on correct realm right now",
- driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
+ driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
log.debug("Updating info on updateAccount page");
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
@@ -128,9 +135,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
}
Assert.assertTrue("There must be user " + bc.getUserLogin() + " in realm " + bc.consumerRealmName(),
- isUserFound);
-
- testSingleLogout();
+ isUserFound);
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
index e5f5b8d..da0bc2b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
@@ -6,6 +6,7 @@
package org.keycloak.testsuite.broker;
import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper;
@@ -22,6 +23,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import static org.keycloak.broker.saml.SAMLIdentityProviderConfig.*;
import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
import static org.keycloak.testsuite.broker.BrokerTestTools.*;
@@ -63,17 +65,17 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
Map<String, String> attributes = new HashMap<>();
- attributes.put("saml.authnstatement", "true");
- attributes.put("saml_single_logout_service_url_post",
+ attributes.put(SamlConfigAttributes.SAML_AUTHNSTATEMENT, "true");
+ attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE,
getAuthRoot(suiteContext) + "/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_SAML_ALIAS + "/endpoint");
- attributes.put("saml_assertion_consumer_url_post",
+ attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE,
getAuthRoot(suiteContext) + "/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_SAML_ALIAS + "/endpoint");
- attributes.put("saml_force_name_id_format", "true");
- attributes.put("saml_name_id_format", "username");
- attributes.put("saml.assertion.signature", "false");
- attributes.put("saml.server.signature", "false");
- attributes.put("saml.client.signature", "false");
- attributes.put("saml.encrypt", "false");
+ attributes.put(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, "true");
+ attributes.put(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username");
+ attributes.put(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "false");
+ attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false");
+ attributes.put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false");
+ attributes.put(SamlConfigAttributes.SAML_ENCRYPT, "false");
client.setAttributes(attributes);
@@ -133,15 +135,15 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
Map<String, String> config = idp.getConfig();
- config.put("singleSignOnServiceUrl", getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
- config.put("singleLogoutServiceUrl", getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
- config.put("nameIDPolicyFormat", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
- config.put("forceAuthn", "true");
- config.put("postBindingResponse", "true");
- config.put("postBindingAuthnRequest", "true");
- config.put("validateSignature", "false");
- config.put("wantAuthnRequestsSigned", "false");
- config.put("backchannelSupported", "true");
+ config.put(SINGLE_SIGN_ON_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
+ config.put(SINGLE_LOGOUT_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
+ config.put(NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
+ config.put(FORCE_AUTHN, "true");
+ config.put(POST_BINDING_RESPONSE, "true");
+ config.put(POST_BINDING_AUTHN_REQUEST, "true");
+ config.put(VALIDATE_SIGNATURE, "false");
+ config.put(WANT_AUTHN_REQUESTS_SIGNED, "false");
+ config.put(BACKCHANNEL_SUPPORTED, "true");
return idp;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java
index b4825cd..ef23a9a 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java
@@ -1,19 +1,29 @@
package org.keycloak.testsuite.broker;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
+import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.arquillian.SuiteContext;
+import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
+import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater;
+import java.io.Closeable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Test;
import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
+import static org.keycloak.testsuite.broker.BrokerTestTools.encodeUrl;
public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
- public static class KcSamlSignedBrokerConfiguration extends KcSamlBrokerConfiguration {
+ public class KcSamlSignedBrokerConfiguration extends KcSamlBrokerConfiguration {
@Override
public RealmRepresentation createProviderRealm() {
@@ -39,6 +49,9 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
public List<ClientRepresentation> createProviderClients(SuiteContext suiteContext) {
List<ClientRepresentation> clientRepresentationList = super.createProviderClients(suiteContext);
+ String consumerCert = adminClient.realm(consumerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate();
+ Assert.assertThat(consumerCert, Matchers.notNullValue());
+
for (ClientRepresentation client : clientRepresentationList) {
client.setClientAuthenticatorType("client-secret");
client.setSurrogateAuthRequired(false);
@@ -49,12 +62,11 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
client.setAttributes(attributes);
}
- attributes.put("saml.assertion.signature", "true");
- attributes.put("saml.server.signature", "true");
- attributes.put("saml.client.signature", "true");
- attributes.put("saml.signature.algorithm", "RSA_SHA256");
- attributes.put("saml.signing.private.key", IDP_SAML_SIGN_KEY);
- attributes.put("saml.signing.certificate", IDP_SAML_SIGN_CERT);
+ attributes.put(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "true");
+ attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true");
+ attributes.put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "true");
+ attributes.put(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, "RSA_SHA256");
+ attributes.put(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, consumerCert);
}
return clientRepresentationList;
@@ -64,11 +76,15 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
IdentityProviderRepresentation result = super.setUpIdentityProvider(suiteContext);
+ String providerCert = adminClient.realm(providerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate();
+ Assert.assertThat(providerCert, Matchers.notNullValue());
+
Map<String, String> config = result.getConfig();
- config.put("validateSignature", "true");
- config.put("wantAuthnRequestsSigned", "true");
- config.put("signingCertificate", IDP_SAML_SIGN_CERT);
+ config.put(SAMLIdentityProviderConfig.VALIDATE_SIGNATURE, "true");
+ config.put(SAMLIdentityProviderConfig.WANT_ASSERTIONS_SIGNED, "true");
+ config.put(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "true");
+ config.put(SAMLIdentityProviderConfig.SIGNING_CERTIFICATE_KEY, providerCert);
return result;
}
@@ -76,7 +92,50 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
- return KcSamlSignedBrokerConfiguration.INSTANCE;
+ return new KcSamlSignedBrokerConfiguration();
}
+ @Test
+ public void testSignedEncryptedAssertions() throws Exception {
+ ClientRepresentation client = adminClient.realm(bc.providerRealmName())
+ .clients()
+ .findByClientId(bc.getIDPClientIdInProviderRealm(suiteContext))
+ .get(0);
+
+ final ClientResource clientResource = realmsResouce().realm(bc.providerRealmName()).clients().get(client.getId());
+ Assert.assertThat(clientResource, Matchers.notNullValue());
+
+ String providerCert = adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate();
+ Assert.assertThat(providerCert, Matchers.notNullValue());
+
+ String consumerCert = adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate();
+ Assert.assertThat(consumerCert, Matchers.notNullValue());
+
+ try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource)
+ .setAttribute(SAMLIdentityProviderConfig.VALIDATE_SIGNATURE, "true")
+ .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_SIGNED, "true")
+ .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, "true")
+ .setAttribute(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "false")
+ .setAttribute(SAMLIdentityProviderConfig.SIGNING_CERTIFICATE_KEY, providerCert)
+ .update();
+ Closeable clientUpdater = new ClientAttributeUpdater(clientResource)
+ .setAttribute(SamlConfigAttributes.SAML_ENCRYPT, "true")
+ .setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, consumerCert)
+ .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false") // only sign assertions
+ .setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "true")
+ .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
+ .update())
+ {
+ // Login should pass because assertion is signed.
+ loginUser();
+
+ // Logout should fail because logout response is not signed.
+ driver.navigate().to(BrokerTestTools.getAuthRoot(suiteContext)
+ + "/auth/realms/" + bc.providerRealmName()
+ + "/protocol/" + "openid-connect"
+ + "/logout?redirect_uri=" + encodeUrl(getAccountUrl(bc.providerRealmName())));
+
+ errorPage.assertCurrent();
+ }
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java
index 40d755a..e171cca 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java
@@ -455,7 +455,7 @@ public abstract class AbstractRegCliTest extends AbstractCliTest {
ClientRepresentation client3 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("clientId", "test-client", client3.getClientId());
- Assert.assertNotEquals("registrationAccessToken in returned json is different than one returned by create",
+ Assert.assertEquals("registrationAccessToken in returned json is different than one returned by create",
client.getRegistrationAccessToken(), client3.getRegistrationAccessToken());
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
index d590322..86edae7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
@@ -187,6 +187,23 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
}
@Test
+ public void updateClientSecret() throws ClientRegistrationException {
+ authManageClients();
+
+ registerClient();
+
+ ClientRepresentation client = reg.get(CLIENT_ID);
+ assertNotNull(client.getSecret());
+ client.setSecret("mysecret");
+
+ reg.update(client);
+
+ ClientRepresentation updatedClient = reg.get(CLIENT_ID);
+
+ assertEquals("mysecret", updatedClient.getSecret());
+ }
+
+ @Test
public void updateClientAsAdminWithCreateOnly() throws ClientRegistrationException {
authCreateClients();
try {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
index 4b4c9ba..57f71b2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -139,7 +139,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
OIDCClientRepresentation rep = reg.oidc().get(response.getClientId());
assertNotNull(rep);
- assertNotEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
+ assertEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
assertTrue(CollectionUtil.collectionEquals(Arrays.asList("code", "none"), response.getResponseTypes()));
assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes()));
assertNotNull(response.getClientSecret());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
index 3eb0d7e..d7ea7f1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
@@ -82,13 +82,16 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
@Test
public void getClientWithRegistrationToken() throws ClientRegistrationException {
+ setTimeOffset(10);
+
ClientRepresentation rep = reg.get(client.getClientId());
assertNotNull(rep);
- assertNotEquals(client.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
- // check registration access token is updated
- assertRead(client.getClientId(), client.getRegistrationAccessToken(), false);
- assertRead(client.getClientId(), rep.getRegistrationAccessToken(), true);
+ assertEquals(client.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
+ assertNotNull(rep.getRegistrationAccessToken());
+
+ // KEYCLOAK-4984 check registration access token is not updated
+ assertRead(client.getClientId(), client.getRegistrationAccessToken(), true);
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java
new file mode 100644
index 0000000..f947d9e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java
@@ -0,0 +1,200 @@
+package org.keycloak.testsuite.docker;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.keycloak.common.Profile;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.ProfileAssume;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
+import org.rnorth.ducttape.unreliables.Unreliables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.shaded.com.github.dockerjava.api.model.ContainerNetwork;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assume.assumeTrue;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+
+public class DockerClientTest extends AbstractKeycloakTest {
+ public static final Logger LOGGER = LoggerFactory.getLogger(DockerClientTest.class);
+
+ public static final String REALM_ID = "docker-test-realm";
+ public static final String AUTH_FLOW = "docker-basic-auth-flow";
+ public static final String CLIENT_ID = "docker-test-client";
+ public static final String DOCKER_USER = "docker-user";
+ public static final String DOCKER_USER_PASSWORD = "password";
+
+ public static final String REGISTRY_HOSTNAME = "registry.localdomain";
+ public static final Integer REGISTRY_PORT = 5000;
+ public static final String MINIMUM_DOCKER_VERSION = "1.8.0";
+ public static final String IMAGE_NAME = "busybox";
+
+ private GenericContainer dockerRegistryContainer = null;
+ private GenericContainer dockerClientContainer = null;
+
+ private static String hostIp;
+
+ @BeforeClass
+ public static void verifyEnvironment() {
+ ProfileAssume.assumeFeatureEnabled(Profile.Feature.DOCKER);
+
+ final Optional<DockerVersion> dockerVersion = new DockerHostVersionSupplier().get();
+ assumeTrue("Could not determine docker version for host machine. It either is not present or accessible to the JVM running the test harness.", dockerVersion.isPresent());
+ assumeTrue("Docker client on host machine is not a supported version. Please upgrade and try again.", DockerVersion.COMPARATOR.compare(dockerVersion.get(), DockerVersion.parseVersionString(MINIMUM_DOCKER_VERSION)) >= 0);
+ LOGGER.debug("Discovered valid docker client on host. version: {}", dockerVersion);
+
+ hostIp = System.getProperty("host.ip");
+
+ if (hostIp == null) {
+ final Optional<String> foundHostIp = new DockerHostIpSupplier().get();
+ if (foundHostIp.isPresent()) {
+ hostIp = foundHostIp.get();
+ }
+ }
+ Assert.assertNotNull("Could not resolve host machine's IP address for docker adapter, and 'host.ip' system poperty not set. Client will not be able to authenticate against the keycloak server!", hostIp);
+ }
+
+ @Override
+ public void addTestRealms(final List<RealmRepresentation> testRealms) {
+ final RealmRepresentation dockerRealm = loadJson(getClass().getResourceAsStream("/docker-test-realm.json"), RealmRepresentation.class);
+
+ /**
+ * TODO fix test harness/importer NPEs when attempting to create realm from scratch.
+ * Need to fix those, would be preferred to do this programmatically such that we don't have to keep realm elements
+ * (I.E. certs, realm url) in sync with a flat file
+ *
+ * final RealmRepresentation dockerRealm = DockerTestRealmSetup.createRealm(REALM_ID);
+ * DockerTestRealmSetup.configureDockerAuthenticationFlow(dockerRealm, AUTH_FLOW);
+ */
+
+ DockerTestRealmSetup.configureDockerRegistryClient(dockerRealm, CLIENT_ID);
+ DockerTestRealmSetup.configureUser(dockerRealm, DOCKER_USER, DOCKER_USER_PASSWORD);
+
+ testRealms.add(dockerRealm);
+ }
+
+ @Override
+ public void beforeAbstractKeycloakTest() throws Exception {
+ super.beforeAbstractKeycloakTest();
+
+ final Map<String, String> environment = new HashMap<>();
+ environment.put("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp");
+ environment.put("REGISTRY_HTTP_TLS_CERTIFICATE", "/opt/certs/localhost.crt");
+ environment.put("REGISTRY_HTTP_TLS_KEY", "/opt/certs/localhost.key");
+ environment.put("REGISTRY_AUTH_TOKEN_REALM", "http://" + hostIp + ":8180/auth/realms/docker-test-realm/protocol/docker-v2/auth");
+ environment.put("REGISTRY_AUTH_TOKEN_SERVICE", CLIENT_ID);
+ environment.put("REGISTRY_AUTH_TOKEN_ISSUER", "http://" + hostIp + ":8180/auth/realms/docker-test-realm");
+ environment.put("REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE", "/opt/certs/docker-realm-public-key.pem");
+ environment.put("INSECURE_REGISTRY", "--insecure-registry " + REGISTRY_HOSTNAME + ":" + REGISTRY_PORT);
+
+ String dockerioPrefix = Boolean.parseBoolean(System.getProperty("docker.io-prefix-explicit")) ? "docker.io/" : "";
+
+ // TODO this required me to turn selinux off :(. Add BindMode options for :z and :Z. Make selinux enforcing again!
+ dockerRegistryContainer = new GenericContainer(dockerioPrefix + "registry:2")
+ .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/certs", "/opt/certs", BindMode.READ_ONLY)
+ .withEnv(environment)
+ .withPrivilegedMode(true);
+ dockerRegistryContainer.start();
+ dockerRegistryContainer.followOutput(new Slf4jLogConsumer(LOGGER));
+
+ dockerClientContainer = new GenericContainer(
+ new ImageFromDockerfile()
+ .withDockerfileFromBuilder(dockerfileBuilder -> {
+ dockerfileBuilder.from("centos/systemd:latest")
+ .run("yum", "install", "-y", "docker", "iptables", ";", "yum", "clean", "all")
+ .cmd("/usr/sbin/init")
+ .volume("/sys/fs/cgroup")
+ .build();
+ })
+ )
+ .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt", "/opt/docker/certs.d/" + REGISTRY_HOSTNAME + "/localhost.crt", BindMode.READ_ONLY)
+ .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker", "/etc/sysconfig/docker", BindMode.READ_WRITE)
+ .withPrivilegedMode(true);
+
+ final Optional<ContainerNetwork> network = dockerRegistryContainer.getContainerInfo().getNetworkSettings().getNetworks().values().stream().findFirst();
+ assumeTrue("Could not find a network adapter whereby the docker client container could connect to host!", network.isPresent());
+ dockerClientContainer.withExtraHost(REGISTRY_HOSTNAME, network.get().getIpAddress());
+
+ dockerClientContainer.start();
+ dockerClientContainer.followOutput(new Slf4jLogConsumer(LOGGER));
+
+ int i = 0;
+ String stdErr = "";
+ while (i++ < 30) {
+ log.infof("Trying to start docker service; attempt: %d", i);
+ stdErr = dockerClientContainer.execInContainer("systemctl", "start", "docker.service").getStderr();
+ if (stdErr.isEmpty()) {
+ break;
+ }
+ else {
+ log.info("systemctl failed: " + stdErr);
+ }
+ WaitUtils.pause(1000);
+ }
+
+ assumeTrue("Cannot start docker service!", stdErr.isEmpty());
+
+ log.info("Waiting for docker service...");
+ validateDockerStarted();
+ log.info("Docker service successfully started");
+ }
+
+ private void validateDockerStarted() {
+ final Callable<Boolean> checkStrategy = () -> {
+ try {
+ final String commandResult = dockerClientContainer.execInContainer("docker", "ps").getStderr();
+ return !commandResult.contains("Cannot connect");
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (Exception e) {
+ return false;
+ }
+ };
+
+ Unreliables.retryUntilTrue(30, TimeUnit.SECONDS, () -> RateLimiterBuilder.newBuilder().withRate(1, TimeUnit.SECONDS).withConstantThroughput().build().getWhenReady(() -> {
+ try {
+ return checkStrategy.call();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+
+ @Test
+ public void shouldPerformDockerAuthAgainstRegistry() throws Exception {
+ Container.ExecResult dockerLoginResult = dockerClientContainer.execInContainer("docker", "login", "-u", DOCKER_USER, "-p", DOCKER_USER_PASSWORD, REGISTRY_HOSTNAME + ":" + REGISTRY_PORT);
+ printNonEmpties(dockerLoginResult.getStdout(), dockerLoginResult.getStderr());
+ assertThat(dockerLoginResult.getStdout(), containsString("Login Succeeded"));
+ }
+
+ private static void printNonEmpties(final String... results) {
+ Arrays.stream(results)
+ .forEachOrdered(DockerClientTest::printNonEmpty);
+ }
+
+ private static void printNonEmpty(final String result) {
+ if (nullOrEmpty.negate().test(result)) {
+ LOGGER.info(result);
+ }
+ }
+
+ public static final Predicate<String> nullOrEmpty = string -> string == null || string.isEmpty();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java
new file mode 100644
index 0000000..b73471c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java
@@ -0,0 +1,45 @@
+package org.keycloak.testsuite.docker;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+/**
+ * Docker doesn't provide a static/reliable way to grab the host machine's IP.
+ * <p>
+ * this currently just returns the first address for the bridge adapter starting with 'docker'. Not the most elegant solution,
+ * but I'm open to suggestions.
+ *
+ * @see https://github.com/moby/moby/issues/1143 and related issues referenced therein.
+ */
+public class DockerHostIpSupplier implements Supplier<Optional<String>> {
+
+ @Override
+ public Optional<String> get() {
+ final Enumeration<NetworkInterface> networkInterfaces;
+ try {
+ networkInterfaces = NetworkInterface.getNetworkInterfaces();
+ } catch (SocketException e) {
+ return Optional.empty();
+ }
+
+ return Collections.list(networkInterfaces).stream()
+ .filter(networkInterface -> networkInterface.getDisplayName().startsWith("docker"))
+ .flatMap(networkInterface -> Collections.list(networkInterface.getInetAddresses()).stream())
+ .map(InetAddress::getHostAddress)
+ .filter(DockerHostIpSupplier::looksLikeIpv4Address)
+ .findFirst();
+ }
+
+ public static boolean looksLikeIpv4Address(final String ip) {
+ return IPv4RegexPattern.matcher(ip).matches();
+ }
+
+ private static final Pattern IPv4RegexPattern = Pattern.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java
new file mode 100644
index 0000000..eac0092
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java
@@ -0,0 +1,43 @@
+package org.keycloak.testsuite.docker;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class DockerHostVersionSupplier implements Supplier<Optional<DockerVersion>> {
+ private static final Logger log = LoggerFactory.getLogger(DockerHostVersionSupplier.class);
+
+ @Override
+ public Optional<DockerVersion> get() {
+ try {
+ Process process = new ProcessBuilder()
+ .command("docker", "version", "--format", "'{{.Client.Version}}'")
+ .start();
+
+ final BufferedReader stdout = getReader(process, Process::getInputStream);
+ final BufferedReader err = getReader(process, Process::getErrorStream);
+
+ int exitCode = process.waitFor();
+ if (exitCode == 0) {
+ final String versionString = stdout.lines().collect(Collectors.joining()).replaceAll("'", "");
+ return Optional.ofNullable(DockerVersion.parseVersionString(versionString));
+ }
+ } catch (IOException | InterruptedException e) {
+ log.error("Could not determine host machine's docker version: ", e);
+ }
+
+ return Optional.empty();
+ }
+
+ private static BufferedReader getReader(final Process process, final Function<Process, InputStream> streamSelector) {
+ return new BufferedReader(new InputStreamReader(streamSelector.apply(process)));
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java
new file mode 100644
index 0000000..727af1d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java
@@ -0,0 +1,87 @@
+package org.keycloak.testsuite.docker;
+
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.protocol.docker.DockerAuthV2Protocol;
+import org.keycloak.protocol.docker.DockerAuthenticator;
+import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public final class DockerTestRealmSetup {
+
+ private DockerTestRealmSetup() {
+ }
+
+ public static RealmRepresentation createRealm(final String realmId) {
+ final RealmRepresentation createdRealm = new RealmRepresentation();
+ createdRealm.setId(UUID.randomUUID().toString());
+ createdRealm.setRealm(realmId);
+ createdRealm.setEnabled(true);
+ createdRealm.setAuthenticatorConfig(new ArrayList<>());
+
+ return createdRealm;
+ }
+
+ public static void configureDockerAuthenticationFlow(final RealmRepresentation dockerRealm, final String authFlowAlais) {
+ final AuthenticationFlowRepresentation dockerBasicAuthFlow = new AuthenticationFlowRepresentation();
+ dockerBasicAuthFlow.setId(UUID.randomUUID().toString());
+ dockerBasicAuthFlow.setAlias(authFlowAlais);
+ dockerBasicAuthFlow.setProviderId("basic-flow");
+ dockerBasicAuthFlow.setTopLevel(true);
+ dockerBasicAuthFlow.setBuiltIn(false);
+
+ final AuthenticationExecutionExportRepresentation dockerBasicAuthExecution = new AuthenticationExecutionExportRepresentation();
+ dockerBasicAuthExecution.setAuthenticator(DockerAuthenticator.ID);
+ dockerBasicAuthExecution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name());
+ dockerBasicAuthExecution.setPriority(0);
+ dockerBasicAuthExecution.setUserSetupAllowed(false);
+ dockerBasicAuthExecution.setAutheticatorFlow(false);
+
+ final List<AuthenticationExecutionExportRepresentation> authenticationExecutions = Optional.ofNullable(dockerBasicAuthFlow.getAuthenticationExecutions()).orElse(new ArrayList<>());
+ authenticationExecutions.add(dockerBasicAuthExecution);
+ dockerBasicAuthFlow.setAuthenticationExecutions(authenticationExecutions);
+
+ final List<AuthenticationFlowRepresentation> authenticationFlows = Optional.ofNullable(dockerRealm.getAuthenticationFlows()).orElse(new ArrayList<>());
+ authenticationFlows.add(dockerBasicAuthFlow);
+ dockerRealm.setAuthenticationFlows(authenticationFlows);
+ dockerRealm.setBrowserFlow(dockerBasicAuthFlow.getAlias());
+ }
+
+
+ public static void configureDockerRegistryClient(final RealmRepresentation dockerRealm, final String clientId) {
+ final ClientRepresentation dockerClient = new ClientRepresentation();
+ dockerClient.setClientId(clientId);
+ dockerClient.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
+ dockerClient.setEnabled(true);
+
+ final List<ClientRepresentation> clients = Optional.ofNullable(dockerRealm.getClients()).orElse(new ArrayList<>());
+ clients.add(dockerClient);
+ dockerRealm.setClients(clients);
+ }
+
+ public static void configureUser(final RealmRepresentation dockerRealm, final String username, final String password) {
+ final UserRepresentation dockerUser = new UserRepresentation();
+ dockerUser.setUsername(username);
+ dockerUser.setEnabled(true);
+ dockerUser.setEmail("docker-users@localhost.localdomain");
+ dockerUser.setFirstName("docker");
+ dockerUser.setLastName("user");
+
+ final CredentialRepresentation dockerUserCreds = new CredentialRepresentation();
+ dockerUserCreds.setType(CredentialRepresentation.PASSWORD);
+ dockerUserCreds.setValue(password);
+ dockerUser.setCredentials(Collections.singletonList(dockerUserCreds));
+
+ dockerRealm.setUsers(Collections.singletonList(dockerUser));
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java
new file mode 100644
index 0000000..7182c54
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java
@@ -0,0 +1,99 @@
+package org.keycloak.testsuite.docker;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class DockerVersion {
+
+ public static final Integer MAJOR_VERSION_INDEX = 0;
+ public static final Integer MINOR_VERSION_INDEX = 1;
+ public static final Integer PATCH_VERSION_INDEX = 2;
+
+ private final Integer major;
+ private final Integer minor;
+ private final Integer patch;
+
+ public static final Comparator<DockerVersion> COMPARATOR = (lhs, rhs) -> Comparator.comparing(DockerVersion::getMajor)
+ .thenComparing(Comparator.comparing(DockerVersion::getMinor)
+ .thenComparing(Comparator.comparing(DockerVersion::getPatch)))
+ .compare(lhs, rhs);
+
+ /**
+ * Major version is required. minor and patch versions will be assumed '0' if not provided.
+ */
+ public DockerVersion(final Integer major, final Optional<Integer> minor, final Optional<Integer> patch) {
+ Objects.requireNonNull(major, "Invalid docker version - no major release number given");
+
+ this.major = major;
+ this.minor = minor.orElse(0);
+ this.patch = patch.orElse(0);
+ }
+
+ /**
+ * @param versionString given in the form '1.12.6'
+ */
+ public static DockerVersion parseVersionString(final String versionString) {
+ Objects.requireNonNull(versionString, "Cannot parse null docker version string");
+
+ final List<Integer> versionNumberList = Arrays.stream(stripDashAndEdition(versionString).trim().split("\\."))
+ .map(Integer::parseInt)
+ .collect(Collectors.toList());
+
+ return new DockerVersion(versionNumberList.get(MAJOR_VERSION_INDEX),
+ Optional.ofNullable(versionNumberList.get(MINOR_VERSION_INDEX)),
+ Optional.ofNullable(versionNumberList.get(PATCH_VERSION_INDEX)));
+ }
+
+ private static String stripDashAndEdition(final String versionString) {
+ if (versionString.contains("-")) {
+ return versionString.substring(0, versionString.indexOf("-"));
+ }
+
+ return versionString;
+ }
+
+ public Integer getMajor() {
+ return major;
+ }
+
+ public Integer getMinor() {
+ return minor;
+ }
+
+ public Integer getPatch() {
+ return patch;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DockerVersion that = (DockerVersion) o;
+
+ if (major != null ? !major.equals(that.major) : that.major != null) return false;
+ if (minor != null ? !minor.equals(that.minor) : that.minor != null) return false;
+ return patch != null ? patch.equals(that.patch) : that.patch == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = major != null ? major.hashCode() : 0;
+ result = 31 * result + (minor != null ? minor.hashCode() : 0);
+ result = 31 * result + (patch != null ? patch.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DockerVersion{" +
+ "major=" + major +
+ ", minor=" + minor +
+ ", patch=" + patch +
+ '}';
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
index 068c426..9700a29 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
@@ -23,6 +23,9 @@ import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.RSATokenVerifier;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistration;
+import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.MultivaluedHashMap;
@@ -31,6 +34,8 @@ import org.keycloak.keys.Attributes;
import org.keycloak.keys.GeneratedHmacKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.ImportedRsaKeyProviderFactory;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
@@ -41,11 +46,11 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserInfoClientUtil;
-import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.KeyPair;
@@ -127,12 +132,27 @@ public class KeyRotationTest extends AbstractKeycloakTest {
assertTokenSignature(key1, response.getAccessToken());
assertTokenSignature(key1, response.getRefreshToken());
+ // Create client with keys #1
+ ClientInitialAccessCreatePresentation initialToken = new ClientInitialAccessCreatePresentation();
+ initialToken.setCount(100);
+ initialToken.setExpiration(0);
+ ClientInitialAccessPresentation accessRep = adminClient.realm("test").clientInitialAccess().create(initialToken);
+ String initialAccessToken = accessRep.getToken();
+
+ ClientRegistration reg = ClientRegistration.create().url(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", "test").build();
+ reg.auth(Auth.token(initialAccessToken));
+ ClientRepresentation clientRep = reg.create(ClientBuilder.create().clientId("test").build());
+
// Userinfo with keys #1
assertUserInfo(response.getAccessToken(), 200);
// Token introspection with keys #1
assertTokenIntrospection(response.getAccessToken(), true);
+ // Get client with keys #1 - registration access token should not have changed
+ ClientRepresentation clientRep2 = reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test");
+ assertEquals(clientRep.getRegistrationAccessToken(), clientRep2.getRegistrationAccessToken());
+
// Create keys #2
PublicKey key2 = createKeys2();
@@ -148,6 +168,10 @@ public class KeyRotationTest extends AbstractKeycloakTest {
// Token introspection with keys #2
assertTokenIntrospection(response.getAccessToken(), true);
+ // Get client with keys #2 - registration access token should be changed
+ ClientRepresentation clientRep3 = reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test");
+ assertNotEquals(clientRep.getRegistrationAccessToken(), clientRep3.getRegistrationAccessToken());
+
// Drop key #1
dropKeys1();
@@ -162,6 +186,17 @@ public class KeyRotationTest extends AbstractKeycloakTest {
// Token introspection with keys #1 dropped
assertTokenIntrospection(response.getAccessToken(), true);
+ // Get client with keys #1 - should fail
+ try {
+ reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test");
+ fail("Expected to fail");
+ } catch (ClientRegistrationException e) {
+ }
+
+ // Get client with keys #2 - should succeed
+ ClientRepresentation clientRep4 = reg.auth(Auth.token(clientRep3.getRegistrationAccessToken())).get("test");
+ assertNotEquals(clientRep2.getRegistrationAccessToken(), clientRep4.getRegistrationAccessToken());
+
// Drop key #2
dropKeys2();
@@ -292,7 +327,7 @@ public class KeyRotationTest extends AbstractKeycloakTest {
}
private void assertUserInfo(String token, int expectedStatus) {
- Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(ClientBuilder.newClient(), token);
+ Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(javax.ws.rs.client.ClientBuilder.newClient(), token);
assertEquals(expectedStatus, userInfoResponse.getStatus());
userInfoResponse.close();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
index a769687..cfdf0b7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
@@ -59,6 +59,7 @@ import org.keycloak.testsuite.runonserver.RunHelpers;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.OAuthClient;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
@@ -216,6 +217,20 @@ public class MigrationTest extends AbstractKeycloakTest {
private void testMigrationTo3_2_0() {
assertNull(masterRealm.toRepresentation().getPasswordPolicy());
assertNull(migrationRealm.toRepresentation().getPasswordPolicy());
+
+ testDockerAuthenticationFlow(masterRealm, migrationRealm);
+ }
+
+ private void testDockerAuthenticationFlow(RealmResource... realms) {
+ for (RealmResource realm : realms) {
+ AuthenticationFlowRepresentation flow = null;
+ for (AuthenticationFlowRepresentation f : realm.flows().getFlows()) {
+ if (DefaultAuthenticationFlows.DOCKER_AUTH.equals(f.getAlias())) {
+ flow = f;
+ }
+ }
+ assertNotNull(flow);
+ }
}
private void testRoleManageAccountLinks(RealmResource... realms) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java
new file mode 100644
index 0000000..db410d5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.oauth;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.util.OAuthClient;
+
+public class OAuthRedirectUriStateTest extends AbstractTestRealmKeycloakTest {
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ @Before
+ public void clientConfiguration() {
+ oauth.clientId("test-app");
+ oauth.responseType(OIDCResponseType.CODE);
+ oauth.stateParamRandom();
+ }
+
+ void assertStateReflected(String state) {
+ oauth.stateParamHardcoded(state);
+
+ OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
+ Assert.assertNotNull(response.getCode());
+
+ URL url;
+ try {
+ url = new URL(driver.getCurrentUrl());
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ Assert.assertTrue(url.getQuery().contains("state=" + state));
+ }
+
+ @Test
+ public void testSimpleStateParameter() {
+ assertStateReflected("VeryLittleGravitasIndeed");
+ }
+
+ @Test
+ public void testJsonStateParameter() {
+ assertStateReflected("%7B%22csrf_token%22%3A%2B%22hlvZNIsWyqdkEhbjlQIia0ty2YY4TXat%22%2C%2B%22destination%22%3A%2B%22eyJhbGciOiJIUzI1NiJ9.Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9wcml2YXRlIg.T18WeIV29komDl8jav-3bSnUZDlMD8VOfIrd2ikP5zE%22%7D");
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index 58ef272..8a3d8bc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -50,7 +50,7 @@
<container qualifier="auth-server-undertow" mode="suite" >
<configuration>
<property name="enabled">${auth.server.undertow} && ! ${auth.server.undertow.crossdc}</property>
- <property name="bindAddress">localhost</property>
+ <property name="bindAddress">0.0.0.0</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
<property name="bindHttpPort">${auth.server.http.port}</property>
<property name="remoteMode">${undertow.remote}</property>
@@ -68,6 +68,8 @@
<property name="jbossArguments">
-Djboss.socket.binding.port-offset=${auth.server.port.offset}
-Djboss.bind.address=0.0.0.0
+ -Dauth.server.http.port=${auth.server.http.port}
+ -Dauth.server.https.port=${auth.server.https.port}
${adapter.test.props}
${migration.import.properties}
${auth.server.profile}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem
new file mode 100644
index 0000000..a7493f1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICsTCCAZkCBgFbaSTAdjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFkb2Nr
+ZXItdGVzdC1yZWFsbTAeFw0xNzA0MTMyMTA2MDdaFw0yNzA0MTMyMTA3NDdaMBwx
+GjAYBgNVBAMMEWRvY2tlci10ZXN0LXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7r
+oLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E
++eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJ
+FLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlw
+fcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMD
+AxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABMA0GCSqGSIb3DQEBCwUA
+A4IBAQB2u9hP3S1bP4+FBwOLPwI3p7WrWBlt2CgwTiyuXvV7u9GLiXqCDUWZd3dS
+ks9vU4Y4NdVyToY4q9YFJ3oAQXlfRw2Yi6e/0nSPpU25o52TWwREnRY98fjVy1eC
+5K2GRwSu79HZKeqA0Tg/ONvGOrlYO1KPbWZGg9NcwAGeILkNdfI82w0KZTpTy+f5
+ATtV30pFkDNT0gfayFmDQvw3EgcD/x0/vI3PlnHLLGprV/ZlBmFWo0vk8iUBwP1Y
+bTA0XqKasITFXJaPeZWzNMCjR1NxDqlIq095uX04E5XGS6XGJKS9PanvGXidk5xM
+gI7xwKE6jaxD9pspYPRgv66528Dc
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt
new file mode 100644
index 0000000..6b50a04
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGBTCCA+2gAwIBAgIJALfo8UyCLlnkMA0GCSqGSIb3DQEBCwUAMIGYMQswCQYD
+VQQGEwJVUzEXMBUGA1UECAwOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcMB1JhbGVp
+Z2gxFjAUBgNVBAoMDVJlZCBIYXQsIEluYy4xJzAlBgNVBAsMHklkZW50aXR5IGFu
+ZCBBY2Nlc3MgTWFuYWdlbWVudDEdMBsGA1UEAwwUcmVnaXN0cnkubG9jYWxkb21h
+aW4wHhcNMTcwNDIwMDMwNzMwWhcNMjAwMTE0MDMwNzMwWjCBmDELMAkGA1UEBhMC
+VVMxFzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMRAwDgYDVQQHDAdSYWxlaWdoMRYw
+FAYDVQQKDA1SZWQgSGF0LCBJbmMuMScwJQYDVQQLDB5JZGVudGl0eSBhbmQgQWNj
+ZXNzIE1hbmFnZW1lbnQxHTAbBgNVBAMMFHJlZ2lzdHJ5LmxvY2FsZG9tYWluMIIC
+IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyIKYO7gYA9T8PpqTf2Lad81X
+cHzhiRYvvzUDgR4UD1NummWPnl2sPjdlQayM/TZ7p6gserdLjms336tvU/6GOIjv
+v10uvDsFVxafuASY1tQSlrFLwF2NwavVOWlPhdlYLvOUnT/zk7fWKRFy7WXp6hD5
+RAkI4+ywuhS6eiZy3wIv/04VjFGYAB1x3NfHVwSuo+cjz/UvI3sU1i0LR+aOSRoP
+9GM8OBpaTxRu/vEHd3k0A2FLP3sJYzkSD6A0p+nqbMfrPKRuZEjDYvBad4KemAl2
+5GRxNeZkJUk0CX2QK2cqr6xOa7598Nr+3ejv99Iiga5r2VlSSdsbV3U9j3RoZY48
+J0RvSgsVeeYqE93SUsVKhSoN4UIdhiVoDCvLtuIeqfQjehowent03OwDUiYw0TeV
+GqmcN54Ki6v+EWSNqY2h01wcbMuQw6PDQ/mn1pz7f/ZAt9T0fop6ml4Mg4nud9S9
+b/Y9+XfuJlPKwZIgQEtrpSfLveOBmWYRu9/rSX9YtHx+pyzbWDtwrF0O9Z/pO+T4
+qOMmfc2ltjzRMFKK6JZFhFVHQP0AKsxLChQrzoHr5k7Rmcn+iGtmqD4tWtzgEQvA
+umhNsm4nrR92hB97yxw3WC9gGvJlBIi/swrCxiKCJDklxCZtVCmqwMFx/bzXu3pH
+sKwYv3poURR9NZb7kDcCAwEAAaNQME4wHQYDVR0OBBYEFNhH71tQSivnjfCHd7pt
+3Qo50DCZMB8GA1UdIwQYMBaAFNhH71tQSivnjfCHd7pt3Qo50DCZMAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAGSCDF/l/ExabQ1DfoKoRCmVoslnK+M1
+0TuDtfss2zqF89BPLBNBKdfp7r1OV4fp465HMpd2ovUkuijLjrIf78+I4AFEv60s
+Z7NKMYEULpvBZ3RY7INr9CoNcWGvnfC/h782axjyI6ZW6I2v717FcciI6su0Eg+k
+kF6+c+cVLmhKLi7hnC9mlN0JMUcOt3cBuZ8NvCHwW6VFmv8hsxt8Z18JcY6aPZE8
+32XzdgcU/U9OAhv1iMEuoGAqQatCHAmA3FOpfI9LjVOxW0LZgHWKX7OEyDEZ+7Ed
+DbEpD73bmTp89lvFcT0UEAcWkRpD+VSozgYEzSeNmzKks2ngl37SlG2YQ23UzgYS
+alGcUEJFBmWr9pJUN+tDPzbtmlrEw9pA6xYZMTDgAQSRHGQK/5lISuzEIMR0nh3q
+Hyhmamlg+zkF415gYKUwh96NgalIc+Y9B4vnSpOv7b+ZFXoubBD2Wk5oi0Ziyog0
+J8YcbLQ8ZhINRvDyNv0iWHNachIzO1/N5G5H8hjibLkH+tpFBSs3uCiwTi+L/MlD
+Pqc0A6Slyi8TnJJDFCDaa3xU321dkvyhGmPeqiyIK+dpJO1FI3OU0rZeGGcyc+K6
+SnDRByp0HQt9W/8Aw+kXjUoI8LOYeR/7Ctd+Tqf11TDxmw9w9LSIEhiYeEJQCxTc
+Dk72PkeTi1zO
+-----END CERTIFICATE-----
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key
new file mode 100644
index 0000000..22a3986
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEAyIKYO7gYA9T8PpqTf2Lad81XcHzhiRYvvzUDgR4UD1NummWP
+nl2sPjdlQayM/TZ7p6gserdLjms336tvU/6GOIjvv10uvDsFVxafuASY1tQSlrFL
+wF2NwavVOWlPhdlYLvOUnT/zk7fWKRFy7WXp6hD5RAkI4+ywuhS6eiZy3wIv/04V
+jFGYAB1x3NfHVwSuo+cjz/UvI3sU1i0LR+aOSRoP9GM8OBpaTxRu/vEHd3k0A2FL
+P3sJYzkSD6A0p+nqbMfrPKRuZEjDYvBad4KemAl25GRxNeZkJUk0CX2QK2cqr6xO
+a7598Nr+3ejv99Iiga5r2VlSSdsbV3U9j3RoZY48J0RvSgsVeeYqE93SUsVKhSoN
+4UIdhiVoDCvLtuIeqfQjehowent03OwDUiYw0TeVGqmcN54Ki6v+EWSNqY2h01wc
+bMuQw6PDQ/mn1pz7f/ZAt9T0fop6ml4Mg4nud9S9b/Y9+XfuJlPKwZIgQEtrpSfL
+veOBmWYRu9/rSX9YtHx+pyzbWDtwrF0O9Z/pO+T4qOMmfc2ltjzRMFKK6JZFhFVH
+QP0AKsxLChQrzoHr5k7Rmcn+iGtmqD4tWtzgEQvAumhNsm4nrR92hB97yxw3WC9g
+GvJlBIi/swrCxiKCJDklxCZtVCmqwMFx/bzXu3pHsKwYv3poURR9NZb7kDcCAwEA
+AQKCAgEAsPuM0dGZ6O/7QmsAXEVuHqbyUkj4bh9WP8jUcgiRnkF/c+rHTPrTyQru
+Znye6fZISWFI+XyGxYvgAp54osQbxxUfwWLHmL/j484FZtEv8xe33Klb+szZDiTV
+DVrmJXgFvVOlTvOe1TlEYHWVYvQ89yzKSIJNBZnrGCSpwJ3lcPCmWwyaOoPezeMv
+mMYhnq50VBn2Y13AoOnIJ5AUz/8yglXt1UIuajrgkcKwgnlPpOYnwgAEAmFglONQ
+DNjVAY2YLTJ9ccaV5hDP3anXwHtb70kTV19NCk11AfBObT4Wniju5acKhVHcKley
+9T7haXZinOLPMUcFOkmbJaRHlTMj3UgnF4k2iJJ7NyY3lAAIedlZ3EFNwpa68Roo
+WClNAJIV6KYRExOZfqeRyR09loTnynPgxkMR4N4oLJHCiTtReXW5Y1HAYbT+iVHC
+Ox1ob/INuZ1VoumDfn6bRqFdK8LldjBwVqRecSad/dg84BtjTB/po81aUpSRENEV
+aZP+jOT9kZbybACh8FdF8u7mxgL+x7Xidng3SKRJi5whQJNmQ62QkzTFMPVXCqlO
+ABsz2a/Zw7swyetg9uApoTTCeK1P0V/MrcEVTIGmcABfBYAVMBj1S2SH1xgAr20P
+IR3SOpPtiNYhIIOnfyQQ3qVudsaSOAJH26I7QLnMyBqOId0Js9ECggEBAOSrGSfT
+bm7OhGu1ZcTmlS17kjsUUYn1Uy30vV5e7uhpQGmr4rKVWYkNeZa5qtJossY3z+4H
+9fZAqJWH2Cr/4pqnfz4GqK+qE56fFdbyHzHKLZOXZGdp9fQzlLsEi9JVYgv+nAPR
+MHS7WeMTUlFc+P3pP6Btyhk/x7YfZnnlatFYlsNJVzUVdblrG6wSVZGpmxcNIeM2
+UeGG78aDBZQdKUO+xuh6MFW20lU165QC1JfGE+NRawqvgSD09F3MGkEwJuD8XEBg
+/rOwNUg8/ayQhd1EgRGQOiDgqfXSpsF101HPUSX/HDC41KG3gTKTc/Vw+ac5ID1r
+b3PKExEXCicDgCkCggEBAOB55eVsRZHBHeBjhqemH8SxWUfSCbx17cGbs7sw95Rs
+3wYci7ABC8wbvG5UDNPd3BI2IV5bJWYOlbVv+Y1FjNHamQjiSXgB3g6RzvaM0bVP
+1Rvn7EvQF87XIKEdo3uHtvpSVBDHYq/DtDyE9wwaNctxBgJwThVXVYINsp+leGsD
+uGVMAsUP01vMNdHJBk/ANPvYxUkDOCtlDDV8cyaFVJAq4/A1h4crv39S/6ZY/RWo
+LQpYnA47pfKZzxvtDQsnVTmolQ8x4yAX5bQrpKAt/hIJhzKdeCglgVr9cq/7sNOO
+kDLZzPLlFPRX1gOHTpDlucNxxlIjPh2h+3CCCPUzGV8CggEAYGmDgbczqKSKUJ96
++Tn/S93+GcrHVlOJbqbx8Qg10ugNsIA4ZPNzfMWhrls6GtzqA4kkskfI/LrmWaWd
+DwQ0luBoVc6Y8PfUrdyFaMtNO8Dy1nfObYvPl9bnrrKMAXLelBAV18YrmAwmKgfL
+fWKl2OivWwTvYRXzLmau3lZMY1fmuRADJO6XZEY0tKhGS9Qm/+EZmKMeguhR0HEN
+uRVSgK2/T+W0227p3+OMICvRVuy9FesOJsM4vpyJK8MSjsmums3MV5iNy1VQIdUV
+X9zPlCt9/9m/qH0RLARVKtxy7Ntsa4jUafaEMGseniRtj97CZC9B2KOjqj5ZK6t7
+LFfdgQKCAQEAtu6gC3dQupdGYba55aXb/c8Jkx34ET2JpF3e+o3NNYgDuFdK/wPb
+OVrhFIgqa/5BehXi26IruB/qoRG/rQEg4WPjkvnWJZZgAD+TChl4TOniIfu+9Yl/
+3XAzhxlAQUs4MoclOwdBxTsXhrpVGefCLyjMXPBosbuaU4IWL0QJ/ivp+aMYHr/m
+3shsk6nfGt7oTtU48WdOPw76BByHOr0tTM+nMfptmBpu1LQu4sFifmOvUN8lTfQO
+KMZvobJtDsnfCj34O4nMLjtLVqi6YE8a3lgldXoekZj+8cfZztCuKbnkiYw1GTzW
+9skd/4Ik5LBR0pTFqepOlJeM8QMHics6wQKCAQA+6RvPk2/b8OJArrFHkhNbfqpf
+Sa/BvRam8azo2MGgOZWVm/yAGHvoVgOaq2H1DrrDh6qBlzZULpwFD+XeuuzYrLs2
+mYr2LFZdeQtd95V7oASdM0OlFatzKPOoLrHwNc4ztwNz0sMrjTYxDG07mp/3Ixz7
+koUPinV636wZUmvwHiUTlD4E2db+fslDhBUc+HV/4MXihvMSA3D8Mum9SttMABYJ
+L0lBzexfVL8oyYvft/tGwV9LwrlFpzndnX6ZZvgJUqzBPx/+exuZjnTwD3N70SN+
+T0TwL0tsVE5clxVdv5xlm5WIW4kQKglRoJnVB1TnpFddRRu/QD8S+e/S6G4w
+-----END RSA PRIVATE KEY-----
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml
new file mode 100644
index 0000000..53702a6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml
@@ -0,0 +1,15 @@
+registry:
+ image: registry:2
+ ports:
+ - 127.0.0.1:5000:5000
+ environment:
+ REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
+ REGISTRY_HTTP_TLS_CERTIFICATE: /opt/certs/localhost.crt
+ REGISTRY_HTTP_TLS_KEY: /opt/certs/localhost.key
+ REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080/auth/realms/docker-test-realm/protocol/docker-v2/auth
+ REGISTRY_AUTH_TOKEN_SERVICE: docker-test-client
+ REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/realms/docker-test-realm
+ REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem
+ volumes:
+ - ./data:/data:z
+ - ./certs:/opt/certs:z
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker
new file mode 100644
index 0000000..433cbc5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker
@@ -0,0 +1,45 @@
+# /etc/sysconfig/docker
+
+# Modify these options if you want to change the way the docker daemon runs
+OPTIONS='--selinux-enabled --log-driver=journald --signature-verification=false'
+if [ -z "${DOCKER_CERT_PATH}" ]; then
+ DOCKER_CERT_PATH=/etc/docker
+fi
+
+# If you want to add your own registry to be used for docker search and docker
+# pull use the ADD_REGISTRY option to list a set of registries, each prepended
+# with --add-registry flag. The first registry added will be the first registry
+# searched.
+# ADD_REGISTRY='--add-registry registry.access.redhat.com'
+
+# If you want to block registries from being used, uncomment the BLOCK_REGISTRY
+# option and give it a set of registries, each prepended with --block-registry
+# flag. For example adding docker.io will stop users from downloading images
+# from docker.io
+# BLOCK_REGISTRY='--block-registry'
+
+# If you have a registry secured with https but do not have proper certs
+# distributed, you can tell docker to not look for full authorization by
+# adding the registry to the INSECURE_REGISTRY line and uncommenting it.
+INSECURE_REGISTRY='--insecure-registry registry.localdomain:5000'
+
+# On an SELinux system, if you remove the --selinux-enabled option, you
+# also need to turn on the docker_transition_unconfined boolean.
+# setsebool -P docker_transition_unconfined 1
+
+# Location used for temporary files, such as those created by
+# docker load and build operations. Default is /var/lib/docker/tmp
+# Can be overriden by setting the following environment variable.
+# DOCKER_TMPDIR=/var/tmp
+
+# Controls the /etc/cron.daily/docker-logrotate cron job status.
+# To disable, uncomment the line below.
+# LOGROTATE=false
+#
+
+# docker-latest daemon can be used by starting the docker-latest unitfile.
+# To use docker-latest client, uncomment below lines
+#DOCKERBINARY=/usr/bin/docker-latest
+#DOCKERDBINARY=/usr/bin/dockerd-latest
+#DOCKER_CONTAINERD_BINARY=/usr/bin/docker-containerd-latest
+#DOCKER_CONTAINERD_SHIM_BINARY=/usr/bin/docker-containerd-shim-latest
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt
new file mode 100644
index 0000000..fe1af61
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt
@@ -0,0 +1,6 @@
+auth:
+ token:
+ realm: http://localhost:8080/auth/auth/realms/docker-test-realm/protocol/docker-v2/auth
+ service: docker-test-client
+ issuer: http://localhost:8080/auth/auth/realms/docker-test-realm
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt
new file mode 100644
index 0000000..7fd8485
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt
@@ -0,0 +1,4 @@
+-e REGISTRY_AUTH_TOKEN_REALM=http://localhost:8080/auth/auth/realms/docker-test-realm/protocol/docker-v2/auth \
+-e REGISTRY_AUTH_TOKEN_SERVICE: docker-test-client \
+-e REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/auth/realms/docker-test-realm \
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json
new file mode 100644
index 0000000..9f9d2ff
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json
@@ -0,0 +1,1315 @@
+{
+ "id" : "docker-test-realm",
+ "realm" : "docker-test-realm",
+ "notBefore" : 0,
+ "revokeRefreshToken" : false,
+ "accessTokenLifespan" : 300,
+ "accessTokenLifespanForImplicitFlow" : 900,
+ "ssoSessionIdleTimeout" : 1800,
+ "ssoSessionMaxLifespan" : 36000,
+ "offlineSessionIdleTimeout" : 2592000,
+ "accessCodeLifespan" : 60,
+ "accessCodeLifespanUserAction" : 300,
+ "accessCodeLifespanLogin" : 1800,
+ "enabled" : true,
+ "sslRequired" : "external",
+ "registrationAllowed" : false,
+ "registrationEmailAsUsername" : false,
+ "rememberMe" : false,
+ "verifyEmail" : false,
+ "loginWithEmailAllowed" : true,
+ "duplicateEmailsAllowed" : false,
+ "resetPasswordAllowed" : false,
+ "editUsernameAllowed" : false,
+ "bruteForceProtected" : false,
+ "maxFailureWaitSeconds" : 900,
+ "minimumQuickLoginWaitSeconds" : 60,
+ "waitIncrementSeconds" : 60,
+ "quickLoginCheckMilliSeconds" : 1000,
+ "maxDeltaTimeSeconds" : 43200,
+ "failureFactor" : 30,
+ "roles" : {
+ "realm" : [ {
+ "id" : "dbcbd18f-52cb-4e45-9372-7e2bbf255729",
+ "name" : "uma_authorization",
+ "description" : "${role_uma_authorization}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : false,
+ "containerId" : "docker-test-realm"
+ }, {
+ "id" : "834687f7-29ce-43a2-a5f7-55c965026827",
+ "name" : "offline_access",
+ "description" : "${role_offline-access}",
+ "scopeParamRequired" : true,
+ "composite" : false,
+ "clientRole" : false,
+ "containerId" : "docker-test-realm"
+ } ],
+ "client" : {
+ "realm-management" : [ {
+ "id" : "11956a41-328d-4cec-a98c-f77fe6accda3",
+ "name" : "create-client",
+ "description" : "${role_create-client}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "e65e7810-359b-429d-9389-c1cd041915fd",
+ "name" : "view-clients",
+ "description" : "${role_view-clients}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "43d747fc-76c3-4a06-a492-44dea5a07edb",
+ "name" : "manage-clients",
+ "description" : "${role_manage-clients}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "de324c4c-34ea-467b-b851-cca912d1cf60",
+ "name" : "view-authorization",
+ "description" : "${role_view-authorization}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "b0f25ef8-404b-4370-a981-ca155eae6b83",
+ "name" : "manage-identity-providers",
+ "description" : "${role_manage-identity-providers}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "c16eb517-5416-4b86-b86d-c312d3b98e09",
+ "name" : "impersonation",
+ "description" : "${role_impersonation}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "1526f875-2d04-453a-aa29-979f61d1013c",
+ "name" : "view-events",
+ "description" : "${role_view-events}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "7043cd10-a2b0-4568-8295-9840c9c2fa43",
+ "name" : "view-realm",
+ "description" : "${role_view-realm}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "23bb0cd9-2c0e-4510-96af-73f0ba1251df",
+ "name" : "manage-realm",
+ "description" : "${role_manage-realm}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "eff4c8dd-0c53-41ca-8013-336b9c19f55b",
+ "name" : "manage-authorization",
+ "description" : "${role_manage-authorization}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "41cead2f-ed3f-4add-8fd2-ceaf3e20daf5",
+ "name" : "realm-admin",
+ "description" : "${role_realm-admin}",
+ "scopeParamRequired" : false,
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "realm-management" : [ "create-client", "view-clients", "manage-clients", "view-authorization", "manage-identity-providers", "impersonation", "view-events", "view-realm", "manage-realm", "manage-authorization", "view-users", "manage-events", "manage-users", "view-identity-providers" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "025e57f5-73a2-4382-b6e7-ea2f447f86a5",
+ "name" : "view-users",
+ "description" : "${role_view-users}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "86fce514-f5f4-4c7d-ae07-56caaeffe272",
+ "name" : "manage-events",
+ "description" : "${role_manage-events}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "8f49cc1a-a3f1-4185-982e-765617c1ac88",
+ "name" : "manage-users",
+ "description" : "${role_manage-users}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ }, {
+ "id" : "e8d7cf8e-b970-4ada-a8b5-58b7d5fcc4e8",
+ "name" : "view-identity-providers",
+ "description" : "${role_view-identity-providers}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1"
+ } ],
+ "security-admin-console" : [ ],
+ "admin-cli" : [ ],
+ "broker" : [ {
+ "id" : "f0eb6730-f5ed-4216-a9db-d87fee982b08",
+ "name" : "read-token",
+ "description" : "${role_read-token}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "f85d993b-f251-4f9c-87f9-6586cb7bb830"
+ } ],
+ "account" : [ {
+ "id" : "8a34db5e-26fb-4be0-ba09-d4e92bc9dd88",
+ "name" : "view-profile",
+ "description" : "${role_view-profile}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9"
+ }, {
+ "id" : "5aef5567-004e-4a18-8ee4-b8a6d5fa0c85",
+ "name" : "manage-account",
+ "description" : "${role_manage-account}",
+ "scopeParamRequired" : false,
+ "composite" : true,
+ "composites" : {
+ "client" : {
+ "account" : [ "manage-account-links" ]
+ }
+ },
+ "clientRole" : true,
+ "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9"
+ }, {
+ "id" : "3bf09e38-5f0d-41c8-adc2-1dba1cf5d819",
+ "name" : "manage-account-links",
+ "description" : "${role_manage-account-links}",
+ "scopeParamRequired" : false,
+ "composite" : false,
+ "clientRole" : true,
+ "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9"
+ } ]
+ }
+ },
+ "groups" : [ ],
+ "defaultRoles" : [ "offline_access", "uma_authorization" ],
+ "requiredCredentials" : [ "password" ],
+ "passwordPolicy" : "hashIterations(20000)",
+ "otpPolicyType" : "totp",
+ "otpPolicyAlgorithm" : "HmacSHA1",
+ "otpPolicyInitialCounter" : 0,
+ "otpPolicyDigits" : 6,
+ "otpPolicyLookAheadWindow" : 1,
+ "otpPolicyPeriod" : 30,
+ "users" : [ {
+ "id" : "a413b2e2-5cff-43e4-ac6e-ab307e8c0652",
+ "createdTimestamp" : 1492117705870,
+ "username" : "user1",
+ "enabled" : true,
+ "totp" : false,
+ "emailVerified" : false,
+ "firstName" : "User",
+ "lastName" : "One",
+ "email" : "user1@redhat.com",
+ "credentials" : [ {
+ "type" : "password",
+ "hashedSaltedValue" : "A1B2lKKJ2npPjSoFo653q2H8Wu/CNoAVD9pYUnAJwMb0AJzAfXGkdX6eHSUEyUK1cDGVfn6iX/JRNo5XyoSH2w==",
+ "salt" : "5X0JI44mCfleW8qR08II1A==",
+ "hashIterations" : 20000,
+ "counter" : 0,
+ "algorithm" : "pbkdf2",
+ "digits" : 0,
+ "period" : 0,
+ "createdDate" : 1492117716198,
+ "config" : { }
+ } ],
+ "disableableCredentialTypes" : [ "password" ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "uma_authorization", "offline_access" ],
+ "clientRoles" : {
+ "account" : [ "view-profile", "manage-account" ]
+ },
+ "groups" : [ ]
+ } ],
+ "clientScopeMappings" : {
+ "realm-management" : [ {
+ "client" : "admin-cli",
+ "roles" : [ "realm-admin" ]
+ }, {
+ "client" : "security-admin-console",
+ "roles" : [ "realm-admin" ]
+ } ]
+ },
+ "clients" : [ {
+ "id" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9",
+ "clientId" : "account",
+ "name" : "${client_account}",
+ "baseUrl" : "/auth/realms/docker-test-realm/account",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "e4f21dc6-959f-4248-8e04-4fb606d9ceaf",
+ "defaultRoles" : [ "view-profile", "manage-account" ],
+ "redirectUris" : [ "/auth/realms/docker-test-realm/account/*" ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "d0e8f6a9-9442-443e-af03-7d31545af866",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "2afbd4f6-e9bc-45d1-92ee-1c4dc9c099d5",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "1bd8d67f-3aac-42cc-8dba-e676a2b41bb1",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "d7df006b-686a-41a8-958b-2525b9c48ff2",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "93bee57d-79e3-42fb-87da-71c05963aa49",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ }, {
+ "id" : "297ecd2f-4440-48aa-82aa-74901588f7c1",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ }, {
+ "id" : "ac4d45a0-c127-4ba3-b243-49cc570a9871",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ }, {
+ "id" : "e0105ad8-27c3-471d-99c3-244762847563",
+ "clientId" : "admin-cli",
+ "name" : "${client_admin-cli}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "72ff8162-b891-4ba3-9501-68e2e34d7cf0",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : false,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : true,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : true,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "c61ba5ee-a8e1-409c-9898-cb8b9697eb26",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "879e8a4f-e4e9-402d-b867-59171fbcb370",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "ee335ebe-a3bd-426a-9622-268ad583fe67",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ }, {
+ "id" : "628083f3-62f0-454a-bc35-80728893513b",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ }, {
+ "id" : "48efdb06-c88b-478f-9009-65bac264de00",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "5683455d-bcaf-41ca-8b0e-da15dfd48753",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "31d6698d-10f0-4fd9-b7f3-c4bc23b507dc",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ }, {
+ "id" : "f85d993b-f251-4f9c-87f9-6586cb7bb830",
+ "clientId" : "broker",
+ "name" : "${client_broker}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "1fbd3ca1-203f-4074-b1d5-b0c6c2739ea4",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "9b754f6b-0a03-4db5-80f9-3c4f656e0828",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "384701c9-c08a-483f-8f44-b288c8694fe3",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "c2e767e6-7744-457b-8dea-e6f170a5122c",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "796cc4cd-b7a5-4255-bf8b-3b99db7532ee",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ }, {
+ "id" : "528ba572-1438-4afc-88c7-02f5e511d433",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "a14f7e92-23ea-444f-8bb8-f2dfb1f255dc",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ }, {
+ "id" : "724e61f0-b490-46b1-b063-2ee122e4ac7a",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ }, {
+ "id" : "2d61e404-7444-4fad-8386-06b811b5f7c1",
+ "clientId" : "realm-management",
+ "name" : "${client_realm-management}",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "403c5eae-8c79-4cfc-ba00-4bb2bfbaaf92",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : true,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "098aeaab-76f1-4742-8522-27e8c178e596",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "fbc2f08d-d6a0-49ad-9b61-601eec42d46f",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ }, {
+ "id" : "b39438d3-a149-4e0f-a3a1-87c441d05123",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "06706c9d-1f71-4cc8-afca-daea4e9fe9e8",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "bcf14207-1f8e-4e53-8d2b-59939e82f8c4",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "91e78da7-b049-41a5-9a22-1f833755c41b",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "3949f934-b86b-4e70-bcc4-52db0288d55b",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ }, {
+ "id" : "7d4ec353-1cf7-43a1-af4d-218fd9dd37ed",
+ "clientId" : "security-admin-console",
+ "name" : "${client_security-admin-console}",
+ "baseUrl" : "/auth/admin/docker-test-realm/console/index.html",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "a0e6ebf9-58fa-472c-a853-64c16c2f8ad8",
+ "redirectUris" : [ "/auth/admin/docker-test-realm/console/*" ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : true,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : false,
+ "publicClient" : true,
+ "frontchannelLogout" : false,
+ "attributes" : { },
+ "fullScopeAllowed" : false,
+ "nodeReRegistrationTimeout" : 0,
+ "protocolMappers" : [ {
+ "id" : "c501a7bc-171b-4ce6-8d91-3f69ae32591d",
+ "name" : "given name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${givenName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "firstName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "given_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "bce6f7a9-b86d-4f5f-a262-f01e235b5622",
+ "name" : "locale",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "consentRequired" : false,
+ "consentText" : "${locale}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "locale",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "locale",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "9d28d5da-53f2-49f9-b0c0-ae3a51f5ac92",
+ "name" : "role list",
+ "protocol" : "saml",
+ "protocolMapper" : "saml-role-list-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "single" : "false",
+ "attribute.nameformat" : "Basic",
+ "attribute.name" : "Role"
+ }
+ }, {
+ "id" : "00183de0-af80-47c5-807f-a62366b2e1b6",
+ "name" : "email",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${email}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "email",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "email",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "31eccf32-3e16-44f2-b727-27c5cb2e9554",
+ "name" : "family name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${familyName}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "lastName",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "family_name",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "c26c0dc9-4cba-42f0-80e4-1f2363084b95",
+ "name" : "docker-v2-allow-all-mapper",
+ "protocol" : "docker-v2",
+ "protocolMapper" : "docker-v2-allow-all-mapper",
+ "consentRequired" : false,
+ "config" : { }
+ }, {
+ "id" : "db4d11d2-e243-4df7-811f-e4622b49950b",
+ "name" : "username",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usermodel-property-mapper",
+ "consentRequired" : true,
+ "consentText" : "${username}",
+ "config" : {
+ "userinfo.token.claim" : "true",
+ "user.attribute" : "username",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "preferred_username",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "id" : "e6d398a7-dbec-480f-93c4-8a9d1bfbad24",
+ "name" : "full name",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-full-name-mapper",
+ "consentRequired" : true,
+ "consentText" : "${fullName}",
+ "config" : {
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
+ } ],
+ "useTemplateConfig" : false,
+ "useTemplateScope" : false,
+ "useTemplateMappers" : false
+ } ],
+ "clientTemplates" : [ ],
+ "browserSecurityHeaders" : {
+ "xContentTypeOptions" : "nosniff",
+ "xRobotsTag" : "none",
+ "xFrameOptions" : "SAMEORIGIN",
+ "contentSecurityPolicy" : "frame-src 'self'"
+ },
+ "smtpServer" : { },
+ "eventsEnabled" : false,
+ "eventsListeners" : [ "jboss-logging" ],
+ "enabledEventTypes" : [ ],
+ "adminEventsEnabled" : false,
+ "adminEventsDetailsEnabled" : false,
+ "components" : {
+ "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ {
+ "id" : "7f9cbf76-3ecb-49ed-850b-f2fce4ecc87f",
+ "name" : "Trusted Hosts",
+ "providerId" : "trusted-hosts",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "host-sending-registration-request-must-match" : [ "true" ],
+ "client-uris-must-match" : [ "true" ]
+ }
+ }, {
+ "id" : "ea2db337-b9d9-463b-abea-0c5dadb5b5f0",
+ "name" : "Consent Required",
+ "providerId" : "consent-required",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : { }
+ }, {
+ "id" : "2d6e7a94-d73c-4f54-b9ea-64f563f5f8fa",
+ "name" : "Full Scope Disabled",
+ "providerId" : "scope",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : { }
+ }, {
+ "id" : "16f6705e-f671-4fde-ba7d-6254e404b503",
+ "name" : "Max Clients Limit",
+ "providerId" : "max-clients",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "max-clients" : [ "200" ]
+ }
+ }, {
+ "id" : "e4baf3d7-e7af-48d0-890d-11304927be69",
+ "name" : "Allowed Protocol Mapper Types",
+ "providerId" : "allowed-protocol-mappers",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : {
+ "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ],
+ "consent-required-for-all-mappers" : [ "true" ]
+ }
+ }, {
+ "id" : "c27ecc77-c0c3-462e-b803-33432c9a7813",
+ "name" : "Allowed Client Templates",
+ "providerId" : "allowed-client-templates",
+ "subType" : "anonymous",
+ "subComponents" : { },
+ "config" : { }
+ }, {
+ "id" : "18bdc70c-5475-4ae4-8606-d52a6397a125",
+ "name" : "Allowed Protocol Mapper Types",
+ "providerId" : "allowed-protocol-mappers",
+ "subType" : "authenticated",
+ "subComponents" : { },
+ "config" : {
+ "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ],
+ "consent-required-for-all-mappers" : [ "true" ]
+ }
+ }, {
+ "id" : "95fd260b-36e9-4df5-aa6b-6c3b8138c766",
+ "name" : "Allowed Client Templates",
+ "providerId" : "allowed-client-templates",
+ "subType" : "authenticated",
+ "subComponents" : { },
+ "config" : { }
+ } ],
+ "org.keycloak.keys.KeyProvider" : [ {
+ "id" : "9dc7e4c1-5bc2-4756-9486-fb64a06582ad",
+ "name" : "rsa-generated",
+ "providerId" : "rsa-generated",
+ "subComponents" : { },
+ "config" : {
+ "privateKey" : [ "MIIEowIBAAKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7roLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJFLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlwfcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMDAxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABAoIBAQCLigz0Q41OVlDt+ALQAYMj4lr8DgtcprRzQ8Tggu31hqom5Pv3woa+5OSuh9LjGY1OD/f1zLWkZI/kdcarx2I8m29rtUfU9QobcPhyXcqa7Y5DZlV/IHj5YUjqi8txMz0aOlhlcXa3qHz9eXlX18wN0SKuu4vJCQzWnEH4DS9ZTwXAp4uZUkOIUHIkACcRPBGBVHCNvwneLA7tPi5E1TK2fvlgyHOvbsomBh385WKrO6HFBmjV9XsMx3QU1EjRaXSpELdIDUR9Z8rgVg08nZ8z3LZ9UNHHdiAXoCm5oqqf8zP5gL6U79vybvjerCpx2AX60UkhpuHeUmZQQMcylLLhAoGBAP4xdt/gkBsC+9faAw3o9VW/6RsdW7ussptnt50Ymi/mlE8qHNe0oSbkGAhqdqCjAV0+cgygn2krOM+OUF/Lq87kBgRE0fAqaarEAryT/DrmvroNrp3Lnif9/kAcEWo8WhpIPgspqzVy7byAFR29/sdbVby2C37OeFYpw0ad/UVdAoGBAJRylgu59wM5ekrmJqNd326J+RLg76abF9TpW3Ka5CY12NgI60ZxRFBfncZKJCTovmoZgE89RHdz7n4ghxVg8D9ThPY7Kh4flAq8SIqAqmb2b7hkfyEMOgGpdwQq1T7uIcIefwYivLpb62C8cSK7leLXJ/wMza5bo8m5fD3t+a2VAoGAJZxqC2wtxmFlpCWU6Bz9GAgCVMm+RgGil8375Bm8zrOeZCxGAkCuy5NaXvxpuxEDZamUtHuburLzf/p9t/7p1/3zSfRo39FWuzavdPmsi4aS1/KoUJ7NMvupABFnHkH5zwO7cmli9NChjo+hEDqJlTPVdsu03bltIsqhIzTDQd0CgYAQ8owCxrZWnedCScg7emoZupK+/wMdKDOuUP3ptZk6a4dYEpyZrDC6ZFAk5S3/MLscbdDiOwJoCMo/iAMkA68p66UQX2zNh5llKF23wjyyCIx0prSE11p/+hLmXOV/i7w65zRlRO368KeMobbg2j2gaiPceLG6qCeozg5LG7IXiQKBgALwLpGKaIixsIaAD1Bzd5cLaKdPGXPyaJwG5xqog58XGVcHklGQRnaN/B3vlrHBgI/NGZNt83bWamCTVlN+A0q9AnMxGHXZHzL21lx6bNiZXX+3DVDm88m+ODPebZXxSZQRNjBrw1KotqUyyhzkbIjfE8752ofb4T+veViHkjW2" ],
+ "certificate" : [ "MIICsTCCAZkCBgFbaSTAdjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFkb2NrZXItdGVzdC1yZWFsbTAeFw0xNzA0MTMyMTA2MDdaFw0yNzA0MTMyMTA3NDdaMBwxGjAYBgNVBAMMEWRvY2tlci10ZXN0LXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7roLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJFLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlwfcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMDAxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB2u9hP3S1bP4+FBwOLPwI3p7WrWBlt2CgwTiyuXvV7u9GLiXqCDUWZd3dSks9vU4Y4NdVyToY4q9YFJ3oAQXlfRw2Yi6e/0nSPpU25o52TWwREnRY98fjVy1eC5K2GRwSu79HZKeqA0Tg/ONvGOrlYO1KPbWZGg9NcwAGeILkNdfI82w0KZTpTy+f5ATtV30pFkDNT0gfayFmDQvw3EgcD/x0/vI3PlnHLLGprV/ZlBmFWo0vk8iUBwP1YbTA0XqKasITFXJaPeZWzNMCjR1NxDqlIq095uX04E5XGS6XGJKS9PanvGXidk5xMgI7xwKE6jaxD9pspYPRgv66528Dc" ],
+ "priority" : [ "100" ]
+ }
+ }, {
+ "id" : "ae58bc1e-c60e-4889-986d-ea5648ea5989",
+ "name" : "hmac-generated",
+ "providerId" : "hmac-generated",
+ "subComponents" : { },
+ "config" : {
+ "kid" : [ "5a0c54c4-fb3d-4b2c-8e1a-9bebb6251b6f" ],
+ "secret" : [ "-5XJ1f5410LDE1XIvQsvAuwwm4CdEyd6Rco0E3EsxG4" ],
+ "priority" : [ "100" ]
+ }
+ } ]
+ },
+ "internationalizationEnabled" : false,
+ "supportedLocales" : [ ],
+ "authenticationFlows" : [ {
+ "id" : "6a3d3800-bea6-4fc4-958f-65365d23c33b",
+ "alias" : "Handle Existing Account",
+ "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "idp-confirm-link",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "idp-email-verification",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "requirement" : "ALTERNATIVE",
+ "priority" : 30,
+ "flowAlias" : "Verify Existing Account by Re-authentication",
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : true
+ } ]
+ }, {
+ "id" : "41de318f-6434-443a-bcf0-6632568f32b0",
+ "alias" : "Verify Existing Account by Re-authentication",
+ "description" : "Reauthentication of existing account",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "idp-username-password-form",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "auth-otp-form",
+ "requirement" : "OPTIONAL",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "8b2f90df-5a09-49b6-b978-acbb74a60670",
+ "alias" : "browser",
+ "description" : "browser based authentication",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "auth-cookie",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "auth-spnego",
+ "requirement" : "DISABLED",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "identity-provider-redirector",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 25,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "requirement" : "ALTERNATIVE",
+ "priority" : 30,
+ "flowAlias" : "forms",
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : true
+ } ]
+ }, {
+ "id" : "6d0cba98-a1d9-4ca4-a877-ffe0d2c7f667",
+ "alias" : "clients",
+ "description" : "Base authentication for clients",
+ "providerId" : "client-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "client-secret",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "client-jwt",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "8c752045-bd44-48fc-ae36-816625897545",
+ "alias" : "direct grant",
+ "description" : "OpenID Connect Resource Owner Grant",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "direct-grant-validate-username",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "direct-grant-validate-password",
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "direct-grant-validate-otp",
+ "requirement" : "OPTIONAL",
+ "priority" : 30,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "7c8e6906-6b5f-4766-b80d-f23b56595992",
+ "alias" : "docker-basic-auth-flow",
+ "description" : "",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : false,
+ "authenticationExecutions" : [ {
+ "authenticator" : "docker-http-basic-authenticator",
+ "requirement" : "REQUIRED",
+ "priority" : 0,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "a41036cf-e368-46e0-9cf3-a96908c53609",
+ "alias" : "first broker login",
+ "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticatorConfig" : "review profile config",
+ "authenticator" : "idp-review-profile",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticatorConfig" : "create unique user config",
+ "authenticator" : "idp-create-user-if-unique",
+ "requirement" : "ALTERNATIVE",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "requirement" : "ALTERNATIVE",
+ "priority" : 30,
+ "flowAlias" : "Handle Existing Account",
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : true
+ } ]
+ }, {
+ "id" : "49c349cc-f11e-461c-98e2-546327175ca4",
+ "alias" : "forms",
+ "description" : "Username, password, otp and other auth forms.",
+ "providerId" : "basic-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "auth-username-password-form",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "auth-otp-form",
+ "requirement" : "OPTIONAL",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "2445867e-f9eb-46cc-8f68-c15d6cf962e4",
+ "alias" : "registration",
+ "description" : "registration flow",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "registration-page-form",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "flowAlias" : "registration form",
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : true
+ } ]
+ }, {
+ "id" : "83a735c2-cf61-49fa-879b-e9b0ed5bb9e9",
+ "alias" : "registration form",
+ "description" : "registration form",
+ "providerId" : "form-flow",
+ "topLevel" : false,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "registration-user-creation",
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "registration-profile-action",
+ "requirement" : "REQUIRED",
+ "priority" : 40,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "registration-password-action",
+ "requirement" : "REQUIRED",
+ "priority" : 50,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "registration-recaptcha-action",
+ "requirement" : "DISABLED",
+ "priority" : 60,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "32acb7cb-af8f-42b2-bd34-9ff534d87121",
+ "alias" : "reset credentials",
+ "description" : "Reset credentials for a user if they forgot their password or something",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "reset-credentials-choose-user",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "reset-credential-email",
+ "requirement" : "REQUIRED",
+ "priority" : 20,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "reset-password",
+ "requirement" : "REQUIRED",
+ "priority" : 30,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ }, {
+ "authenticator" : "reset-otp",
+ "requirement" : "OPTIONAL",
+ "priority" : 40,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ }, {
+ "id" : "1c67b912-70f4-4182-b055-08c3d6bb23c8",
+ "alias" : "saml ecp",
+ "description" : "SAML ECP Profile Authentication Flow",
+ "providerId" : "basic-flow",
+ "topLevel" : true,
+ "builtIn" : true,
+ "authenticationExecutions" : [ {
+ "authenticator" : "http-basic-authenticator",
+ "requirement" : "REQUIRED",
+ "priority" : 10,
+ "userSetupAllowed" : false,
+ "autheticatorFlow" : false
+ } ]
+ } ],
+ "authenticatorConfig" : [ {
+ "id" : "30fd72e5-eb98-4ae5-a695-c959ec626ac6",
+ "alias" : "create unique user config",
+ "config" : {
+ "require.password.update.after.registration" : "false"
+ }
+ }, {
+ "id" : "e0ea82a7-98d7-4ffb-8444-8d240a94d83b",
+ "alias" : "review profile config",
+ "config" : {
+ "update.profile.on.first.login" : "missing"
+ }
+ } ],
+ "requiredActions" : [ {
+ "alias" : "CONFIGURE_TOTP",
+ "name" : "Configure OTP",
+ "providerId" : "CONFIGURE_TOTP",
+ "enabled" : true,
+ "defaultAction" : false,
+ "config" : { }
+ }, {
+ "alias" : "UPDATE_PASSWORD",
+ "name" : "Update Password",
+ "providerId" : "UPDATE_PASSWORD",
+ "enabled" : true,
+ "defaultAction" : false,
+ "config" : { }
+ }, {
+ "alias" : "UPDATE_PROFILE",
+ "name" : "Update Profile",
+ "providerId" : "UPDATE_PROFILE",
+ "enabled" : true,
+ "defaultAction" : false,
+ "config" : { }
+ }, {
+ "alias" : "VERIFY_EMAIL",
+ "name" : "Verify Email",
+ "providerId" : "VERIFY_EMAIL",
+ "enabled" : true,
+ "defaultAction" : false,
+ "config" : { }
+ }, {
+ "alias" : "terms_and_conditions",
+ "name" : "Terms and Conditions",
+ "providerId" : "terms_and_conditions",
+ "enabled" : false,
+ "defaultAction" : false,
+ "config" : { }
+ } ],
+ "browserFlow" : "docker-basic-auth-flow",
+ "registrationFlow" : "registration",
+ "directGrantFlow" : "direct grant",
+ "resetCredentialsFlow" : "reset credentials",
+ "clientAuthenticationFlow" : "clients",
+ "attributes" : {
+ "_browser_header.xFrameOptions" : "SAMEORIGIN",
+ "failureFactor" : "30",
+ "quickLoginCheckMilliSeconds" : "1000",
+ "maxDeltaTimeSeconds" : "43200",
+ "_browser_header.xContentTypeOptions" : "nosniff",
+ "_browser_header.xRobotsTag" : "none",
+ "bruteForceProtected" : "false",
+ "maxFailureWaitSeconds" : "900",
+ "_browser_header.contentSecurityPolicy" : "frame-src 'self'",
+ "minimumQuickLoginWaitSeconds" : "60",
+ "waitIncrementSeconds" : "60"
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java
index 7e4c29c..09f5f1c 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java
@@ -85,4 +85,60 @@ public class RequiredActions extends Authentication {
public void setUpdateProfileDefaultAction(boolean value) {
setRequiredActionDefaultValue(UPDATE_PROFILE, value);
}
+
+ private boolean getRequiredActionValue(String id) {
+ WaitUtils.waitUntilElement(requiredActionTable).is().present();
+
+ WebElement checkbox = requiredActionTable.findElement(By.id(id));
+
+ return checkbox.isSelected();
+ }
+
+ private boolean getRequiredActionEnabledValue(String id) {
+ return getRequiredActionValue(id + ENABLED);
+ }
+
+ private boolean getRequiredActionDefaultValue(String id) {
+ return getRequiredActionValue(id + DEFAULT);
+ }
+
+ public boolean getTermsAndConditionEnabled() {
+ return getRequiredActionEnabledValue(TERMS_AND_CONDITIONS);
+ }
+
+ public boolean getTermsAndConditionDefaultAction() {
+ return getRequiredActionDefaultValue(TERMS_AND_CONDITIONS);
+ }
+
+ public boolean getVerifyEmailEnabled() {
+ return getRequiredActionEnabledValue(VERIFY_EMAIL);
+ }
+
+ public boolean getVerifyEmailDefaultAction() {
+ return getRequiredActionDefaultValue(VERIFY_EMAIL);
+ }
+
+ public boolean getUpdatePasswordEnabled() {
+ return getRequiredActionEnabledValue(UPDATE_PASSWORD);
+ }
+
+ public boolean getUpdatePasswordDefaultAction() {
+ return getRequiredActionDefaultValue(UPDATE_PASSWORD);
+ }
+
+ public boolean getConfigureTotpEnabled() {
+ return getRequiredActionEnabledValue(CONFIGURE_TOTP);
+ }
+
+ public boolean getConfigureTotpDefaultAction() {
+ return getRequiredActionDefaultValue(CONFIGURE_TOTP);
+ }
+
+ public boolean getUpdateProfileEnabled() {
+ return getRequiredActionEnabledValue(UPDATE_PROFILE);
+ }
+
+ public boolean getUpdateProfileDefaultAction() {
+ return getRequiredActionDefaultValue(UPDATE_PROFILE);
+ }
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java
index a6c9527..fee3da3 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java
@@ -25,6 +25,7 @@ import org.keycloak.representations.idm.authorization.ResourcePermissionRepresen
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.testsuite.console.page.clients.authorization.policy.PolicyTypeUI;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -58,11 +59,9 @@ public class Permissions extends Form {
if ("resource".equals(type)) {
resourcePermission.form().populate((ResourcePermissionRepresentation) expected);
- resourcePermission.form().save();
return (P) resourcePermission;
} else if ("scope".equals(type)) {
scopePermission.form().populate((ScopePermissionRepresentation) expected);
- scopePermission.form().save();
return (P) scopePermission;
}
@@ -73,7 +72,7 @@ public class Permissions extends Form {
for (WebElement row : permissions().rows()) {
PolicyRepresentation actual = permissions().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
String type = representation.getType();
@@ -92,7 +91,7 @@ public class Permissions extends Form {
for (WebElement row : permissions().rows()) {
PolicyRepresentation actual = permissions().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
String type = actual.getType();
if ("resource".equals(type)) {
@@ -109,7 +108,7 @@ public class Permissions extends Form {
for (WebElement row : permissions().rows()) {
PolicyRepresentation actual = permissions().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
String type = actual.getType();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java
index cf39523..e31ac2a 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.permission;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.console.page.fragment.OnOffSwitch;
import org.keycloak.testsuite.page.Form;
@@ -48,8 +49,8 @@ public class ResourcePermissionForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
@FindBy(id = "s2id_policies")
private MultipleStringSelect2 policySelect;
@@ -78,7 +79,7 @@ public class ResourcePermissionForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public ResourcePermissionRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java
index f16cd5c..deb7f06 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java
@@ -21,6 +21,7 @@ import java.util.function.Function;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.console.page.fragment.SingleStringSelect2;
import org.keycloak.testsuite.page.Form;
@@ -45,8 +46,8 @@ public class ScopePermissionForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
@FindBy(id = "s2id_policies")
private MultipleStringSelect2 policySelect;
@@ -81,7 +82,7 @@ public class ScopePermissionForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public ScopePermissionRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java
index 5e7170d..12c4289 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java
@@ -20,6 +20,7 @@ import java.util.Set;
import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.WebElement;
@@ -46,8 +47,8 @@ public class AggregatePolicyForm extends Form {
@FindBy(id = "s2id_policies")
private MultipleStringSelect2 policySelect;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(AggregatePolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -83,7 +84,7 @@ public class AggregatePolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public AggregatePolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java
index 9095a32..cedacee 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java
@@ -25,6 +25,7 @@ import java.util.stream.Collectors;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.By;
@@ -52,8 +53,8 @@ public class ClientPolicyForm extends Form {
@FindBy(id = "s2id_clients")
private ClientSelect clientsInput;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(ClientPolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -67,7 +68,7 @@ public class ClientPolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public ClientPolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java
index e83585b..9c1c1ea 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy;
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebElement;
@@ -41,8 +42,8 @@ public class JSPolicyForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(JSPolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -58,7 +59,7 @@ public class JSPolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public JSPolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
index 7be563e..7ac4b52 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
@@ -30,6 +30,7 @@ import org.keycloak.representations.idm.authorization.RulePolicyRepresentation;
import org.keycloak.representations.idm.authorization.TimePolicyRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -81,31 +82,24 @@ public class Policies extends Form {
if ("role".equals(type)) {
rolePolicy.form().populate((RolePolicyRepresentation) expected);
- rolePolicy.form().save();
return (P) rolePolicy;
} else if ("user".equals(type)) {
userPolicy.form().populate((UserPolicyRepresentation) expected);
- userPolicy.form().save();
return (P) userPolicy;
} else if ("aggregate".equals(type)) {
aggregatePolicy.form().populate((AggregatePolicyRepresentation) expected);
- aggregatePolicy.form().save();
return (P) aggregatePolicy;
} else if ("js".equals(type)) {
jsPolicy.form().populate((JSPolicyRepresentation) expected);
- jsPolicy.form().save();
return (P) jsPolicy;
} else if ("time".equals(type)) {
timePolicy.form().populate((TimePolicyRepresentation) expected);
- timePolicy.form().save();
return (P) timePolicy;
} else if ("rules".equals(type)) {
rulePolicy.form().populate((RulePolicyRepresentation) expected);
- rulePolicy.form().save();
return (P) rulePolicy;
} else if ("client".equals(type)) {
clientPolicy.form().populate((ClientPolicyRepresentation) expected);
- clientPolicy.form().save();
return (P) clientPolicy;
} else if ("group".equals(type)) {
groupPolicy.form().populate((GroupPolicyRepresentation) expected);
@@ -120,7 +114,7 @@ public class Policies extends Form {
for (WebElement row : policies().rows()) {
PolicyRepresentation actual = policies().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
String type = representation.getType();
@@ -151,8 +145,7 @@ public class Policies extends Form {
for (WebElement row : policies().rows()) {
PolicyRepresentation actual = policies().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
- WaitUtils.waitForPageToLoad(driver);
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
String type = actual.getType();
if ("role".equals(type)) {
return (P) rolePolicy;
@@ -180,8 +173,7 @@ public class Policies extends Form {
for (WebElement row : policies().rows()) {
PolicyRepresentation actual = policies().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
- WaitUtils.waitForPageToLoad(driver);
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
String type = actual.getType();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java
index 8b6f114..f917678 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
import org.keycloak.testsuite.console.page.fragment.AbstractMultipleSelect2;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
@@ -60,8 +61,8 @@ public class RolePolicyForm extends Form {
@FindBy(id = "s2id_clientRoles")
private ClientRoleSelect clientRoleSelect;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(RolePolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -115,7 +116,7 @@ public class RolePolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public RolePolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java
index 17b4d46..0ba43f1 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.RulePolicyRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement;
@@ -62,8 +63,8 @@ public class RulePolicyForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
@FindBy(id = "resolveModule")
private WebElement resolveModuleButton;
@@ -92,7 +93,7 @@ public class RulePolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public RulePolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java
index 5c31f33..47be24d 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy;
import org.keycloak.representations.idm.authorization.TimePolicyRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -77,8 +78,8 @@ public class TimePolicyForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(TimePolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -102,7 +103,7 @@ public class TimePolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public TimePolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java
index e403d1b..ec24ace 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java
@@ -25,6 +25,7 @@ import java.util.stream.Collectors;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.By;
@@ -52,8 +53,8 @@ public class UserPolicyForm extends Form {
@FindBy(id = "s2id_users")
private UserSelect usersInput;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(UserPolicyRepresentation expected) {
setInputValue(name, expected.getName());
@@ -67,7 +68,7 @@ public class UserPolicyForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public UserPolicyRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
index 8f4a66f..c4d2b2b 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
@@ -23,6 +23,7 @@ import java.util.Set;
import org.jboss.arquillian.graphene.fragment.Root;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By;
@@ -54,8 +55,8 @@ public class ResourceForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
@FindBy(id = "s2id_scopes")
private ScopesInput scopesInput;
@@ -94,7 +95,7 @@ public class ResourceForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
public ResourceRepresentation toRepresentation() {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java
index 280af3f..0290bc1 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java
@@ -21,6 +21,7 @@ import static org.openqa.selenium.By.tagName;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -52,7 +53,7 @@ public class Resources extends Form {
for (WebElement row : resources().rows()) {
ResourceRepresentation actual = resources().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
resource.form().populate(representation);
return;
@@ -64,7 +65,7 @@ public class Resources extends Form {
for (WebElement row : resources().rows()) {
ResourceRepresentation actual = resources().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
resource.form().delete();
return;
@@ -76,7 +77,7 @@ public class Resources extends Form {
for (WebElement row : resources().rows()) {
ResourceRepresentation actual = resources().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
WaitUtils.waitForPageToLoad(driver);
return resource;
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
index ed01a2b..29ec514 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.console.page.clients.authorization.scope;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -35,8 +36,8 @@ public class ScopeForm extends Form {
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
private WebElement deleteButton;
- @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
- private WebElement confirmDelete;
+ @FindBy(xpath = "//div[@class='modal-dialog']")
+ protected ModalDialog modalDialog;
public void populate(ScopeRepresentation expected) {
setInputValue(name, expected.getName());
@@ -46,6 +47,6 @@ public class ScopeForm extends Form {
public void delete() {
deleteButton.click();
- confirmDelete.click();
+ modalDialog.confirmDeletion();
}
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
index a59869c..4e706e7 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
@@ -21,6 +21,7 @@ import static org.openqa.selenium.By.tagName;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.URLUtils;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -51,7 +52,7 @@ public class Scopes extends Form {
for (WebElement row : scopes().rows()) {
ScopeRepresentation actual = scopes().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
scope.form().populate(representation);
}
}
@@ -61,7 +62,7 @@ public class Scopes extends Form {
for (WebElement row : scopes().rows()) {
ScopeRepresentation actual = scopes().toRepresentation(row);
if (actual.getName().equalsIgnoreCase(name)) {
- row.findElements(tagName("a")).get(0).click();
+ URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true);
scope.form().delete();
}
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java
index b8217f8..b879e18 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java
@@ -21,8 +21,11 @@ import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.auth.page.login.Registration;
import org.keycloak.testsuite.console.AbstractConsoleTest;
+import org.keycloak.testsuite.console.page.AdminConsoleRealm;
import org.keycloak.testsuite.console.page.authentication.RequiredActions;
import org.keycloak.testsuite.console.page.realm.LoginSettings;
import org.openqa.selenium.By;
@@ -72,6 +75,52 @@ public class RequiredActionsTest extends AbstractConsoleTest {
}
@Test
+ public void defaultCheckboxUncheckableWhenEnabledIsFalse() {
+ requiredActionsPage.setTermsAndConditionEnabled(false);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled());
+ requiredActionsPage.setTermsAndConditionDefaultAction(true);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ }
+
+ @Test
+ public void defaultCheckboxUncheckedWhenEnabledBecomesFalse() {
+ requiredActionsPage.setTermsAndConditionEnabled(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ requiredActionsPage.setTermsAndConditionDefaultAction(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(false);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ assertAlertSuccess();
+ }
+
+ @Test
+ public void defaultCheckboxKeepsValueWhenEnabledIsToggled() {
+ requiredActionsPage.setTermsAndConditionEnabled(true);
+ requiredActionsPage.setTermsAndConditionDefaultAction(false);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(false);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+
+ requiredActionsPage.setTermsAndConditionDefaultAction(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(false);
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction());
+ requiredActionsPage.setTermsAndConditionEnabled(true);
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled());
+ Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction());
+
+ assertAlertSuccess();
+ }
+
+ @Test
public void configureTotpDefaultActionTest() {
requiredActionsPage.setConfigureTotpDefaultAction(true);
assertAlertSuccess();
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java
index 0d8e6b2..eaa45de 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java
@@ -23,6 +23,7 @@ package org.keycloak.testsuite.console.clients;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@@ -92,7 +93,6 @@ public class ClientMappersOIDCTest extends AbstractClientTest {
assertEquals("oidc-hardcoded-role-mapper", found.getProtocolMapper());
Map<String, String> config = found.getConfig();
- assertEquals(1, config.size());
assertEquals("offline_access", config.get("role"));
//edit
@@ -164,8 +164,6 @@ public class ClientMappersOIDCTest extends AbstractClientTest {
assertEquals("oidc-usersessionmodel-note-mapper", found.getProtocolMapper());
Map<String, String> config = found.getConfig();
- assertNull(config.get("id.token.claim"));
- assertNull(config.get("access.token.claim"));
assertEquals("claim name", config.get("claim.name"));
assertEquals("session note", config.get("user.session.note"));
assertEquals("int", config.get("jsonType.label"));
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 28a985c..73412e9 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -833,6 +833,8 @@ reset-credentials=Reset Credentials
reset-credentials.tooltip=Select the flow you want to use when the user has forgotten their credentials.
client-authentication=Client Authentication
client-authentication.tooltip=Select the flow you want to use for authentication of clients.
+docker-auth=Docker Authentication
+docker-auth.tooptip=Select the flow you want to use for authenticatoin against a docker client.
new=New
copy=Copy
add-execution=Add execution
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index c4e870f..15c86dc 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -1709,8 +1709,8 @@ module.config([ '$routeProvider', function($routeProvider) {
flows : function(AuthenticationFlowsLoader) {
return AuthenticationFlowsLoader();
},
- serverInfo : function(ServerInfo) {
- return ServerInfo.delay;
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
}
},
controller : 'RealmFlowBindingCtrl'
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 515eb99..db29ebe 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -814,7 +814,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
"bearer-only"
];
- $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
+ $scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.templates = [ {name:'NONE'}];
for (var i = 0; i < templates.length; i++) {
@@ -1240,7 +1240,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
});
module.controller('CreateClientCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) {
- $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
+ $scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.create = true;
$scope.templates = [ {name:'NONE'}];
var templateNameMap = new Object();
@@ -1915,7 +1915,7 @@ module.controller('ClientTemplateListCtrl', function($scope, realm, templates, C
});
module.controller('ClientTemplateDetailCtrl', function($scope, realm, template, $route, serverInfo, ClientTemplate, $location, $modal, Dialog, Notifications) {
- $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
+ $scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.realm = realm;
$scope.create = !template.name;
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index ab4e72a..1daf307 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1449,7 +1449,7 @@ module.controller('RoleDetailCtrl', function($scope, realm, role, roles, clients
$http, $location, Notifications, Dialog);
});
-module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications) {
+module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications, RealmSMTPConnectionTester) {
console.log('RealmSMTPSettingsCtrl');
var booleanSmtpAtts = ["auth","ssl","starttls"];
@@ -1484,6 +1484,25 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real
$scope.changed = false;
};
+ var initSMTPTest = function() {
+ return {
+ realm: $scope.realm.realm,
+ config: JSON.stringify(realm.smtpServer)
+ };
+ };
+
+ $scope.testConnection = function() {
+ RealmSMTPConnectionTester.send(initSMTPTest(), function() {
+ Notifications.success("SMTP connection successful. E-mail was sent!");
+ }, function(errorResponse) {
+ if (error.data.errorMessage) {
+ Notifications.error(error.data.errorMessage);
+ } else {
+ Notifications.error('Unexpected error during SMTP validation');
+ }
+ });
+ };
+
/* Convert string attributes containing a boolean to actual boolean type + convert an integer string (port) to integer. */
function typeObject(obj){
for (var att in obj){
@@ -1916,6 +1935,8 @@ module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm
}
}
+ $scope.profileInfo = serverInfo.profileInfo;
+
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings");
});
@@ -2110,6 +2131,9 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
} else if (realm.clientAuthenticationFlow == $scope.flow.alias) {
Notifications.error("Cannot remove flow, it is currently being used as the client authentication flow.");
+ } else if (realm.dockerAuthenticationFlow == $scope.flow.alias) {
+ Notifications.error("Cannot remove flow, it is currently being used as the docker authentication flow.");
+
} else {
AuthenticationFlows.remove({realm: realm.realm, flow: $scope.flow.id}, function () {
$location.url("/realms/" + realm.realm + '/authentication/flows/' + flows[0].alias);
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index e850b3b..e671c13 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -320,12 +320,53 @@ module.factory('RealmLDAPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
});
+module.factory('RealmSMTPConnectionTester', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/testSMTPConnection/:config', {
+ realm : '@realm',
+ config : '@config'
+ }, {
+ send: {
+ method: 'POST'
+ }
+ });
+});
+
module.service('ServerInfo', function($resource, $q, $http) {
var info = {};
var delay = $q.defer();
- $http.get(authUrl + '/admin/serverinfo').success(function(data) {
+ function copyInfo(data, info) {
angular.copy(data, info);
+
+ info.listProviderIds = function(spi) {
+ var providers = info.providers[spi].providers;
+ var ids = Object.keys(providers);
+ ids.sort(function(a, b) {
+ var s1;
+ var s2;
+
+ if (providers[a].order != providers[b].order) {
+ s1 = providers[b].order;
+ s2 = providers[a].order;
+ } else {
+ s1 = a;
+ s2 = b;
+ }
+
+ if (s1 < s2) {
+ return -1;
+ } else if (s1 > s2) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+ return ids;
+ }
+ }
+
+ $http.get(authUrl + '/admin/serverinfo').success(function(data) {
+ copyInfo(data, info);
delay.resolve(info);
});
@@ -335,7 +376,7 @@ module.service('ServerInfo', function($resource, $q, $http) {
},
reload: function() {
$http.get(authUrl + '/admin/serverinfo').success(function(data) {
- angular.copy(data, info);
+ copyInfo(data, info);
});
},
promise: delay.promise
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html b/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
index 8a9d0e1..0ef489b 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
@@ -47,7 +47,7 @@
</div>
<div class="form-group">
- <label for="resetCredentials" class="col-md-2 control-label">{{:: 'client-authentication' | translate}}</label>
+ <label for="clientAuthentication" class="col-md-2 control-label">{{:: 'client-authentication' | translate}}</label>
<div class="col-md-2">
<div>
<select id="clientAuthentication" ng-model="realm.clientAuthenticationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in clientFlows">
@@ -57,6 +57,18 @@
<kc-tooltip>{{:: 'client-authentication.tooltip' | translate}}</kc-tooltip>
</div>
+
+ <div class="form-group" data-ng-show="profileInfo.disabledFeatures.indexOf('DOCKER') == -1">
+ <label for="dockerAuth" class="col-md-2 control-label">{{:: 'docker-auth' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="dockerAuth" ng-model="realm.dockerAuthenticationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in flows">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'docker-auth.tooltip' | translate}}</kc-tooltip>
+ </div>
+
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index cd6e271..979c713 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -37,7 +37,7 @@
</div>
<kc-tooltip>{{:: 'client.enabled.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group clearfix block">
+ <div class="form-group clearfix block" data-ng-show="protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="consentRequired">{{:: 'consent-required' | translate}}</label>
<div class="col-sm-6">
<input ng-model="clientEdit.consentRequired" name="consentRequired" id="consentRequired" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
@@ -239,7 +239,7 @@
<kc-tooltip>{{:: 'name-id-format.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="!clientEdit.bearerOnly">
+ <div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="rootUrl">{{:: 'root-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="rootUrl" id="rootUrl" data-ng-model="clientEdit.rootUrl">
@@ -247,7 +247,7 @@
<kc-tooltip>{{:: 'root-url.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group clearfix block" data-ng-hide="clientEdit.bearerOnly || (!clientEdit.standardFlowEnabled && !clientEdit.implicitFlowEnabled)">
+ <div class="form-group clearfix block" data-ng-hide="clientEdit.bearerOnly || (!clientEdit.standardFlowEnabled && !clientEdit.implicitFlowEnabled) || protocol == 'docker-v2'">
<label class="col-md-2 control-label" for="newRedirectUri"><span class="required" data-ng-show="protocol != 'saml'">*</span> {{:: 'valid-redirect-uris' | translate}}</label>
<div class="col-sm-6">
@@ -269,14 +269,14 @@
<kc-tooltip>{{:: 'valid-redirect-uris.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="!clientEdit.bearerOnly">
+ <div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="baseUrl">{{:: 'base-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="baseUrl" id="baseUrl" data-ng-model="clientEdit.baseUrl">
</div>
<kc-tooltip>{{:: 'base-url.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-hide="protocol == 'saml'">
+ <div class="form-group" data-ng-hide="protocol == 'saml' || protocol == 'docker-v2'">
<label class="col-md-2 control-label" for="adminUrl">{{:: 'admin-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="adminUrl" id="adminUrl"
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
index 5d3c68e..43df761 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
@@ -10,6 +10,9 @@
<div class="col-md-6">
<input class="form-control" id="smtpHost" type="text" ng-model="realm.smtpServer.host" placeholder="{{:: 'smtp-host' | translate}}" required>
</div>
+ <div class="col-sm-4">
+ <a class="btn btn-primary" data-ng-click="testConnection()">{{:: 'test-connection' | translate}}</a>
+ </div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="smtpPort">{{:: 'port' | translate}}</label>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html b/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
index 45a5be6..17dcc05 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
@@ -21,7 +21,7 @@
<tr ng-repeat="requiredAction in requiredActions | orderBy : 'name'" data-ng-show="requiredActions.length > 0">
<td>{{requiredAction.name}}</td>
<td><input type="checkbox" ng-model="requiredAction.enabled" ng-change="updateRequiredAction(requiredAction)" id="{{requiredAction.alias}}.enabled"></td>
- <td><input type="checkbox" ng-model="requiredAction.defaultAction" ng-change="updateRequiredAction(requiredAction)" id="{{requiredAction.alias}}.defaultAction"></td>
+ <td><input type="checkbox" ng-model="requiredAction.defaultAction" ng-change="updateRequiredAction(requiredAction)" ng-disabled="!requiredAction.enabled" ng-checked="requiredAction.enabled && requiredAction.defaultAction" id="{{requiredAction.alias}}.defaultAction"></td>
</tr>
<tr data-ng-show="requiredActions.length == 0">
<td>{{:: 'no-required-actions-configured' | translate}}</td>
@@ -31,4 +31,4 @@
</div>
-<kc-menu></kc-menu>
\ No newline at end of file
+<kc-menu></kc-menu>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index e5b7c21..4155b46 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -8,7 +8,10 @@
<ul class="nav nav-tabs" data-ng-hide="create && !path[4]">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{:: 'settings' | translate}}</a></li>
- <li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!disableCredentialsTab && !client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a></li>
+ <li ng-class="{active: path[4] == 'credentials'}"
+ data-ng-show="!client.publicClient && client.protocol == 'openid-connect'">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a>
+ </li>
<li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">{{:: 'saml-keys' | translate}}</a></li>
<li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
<li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
@@ -19,8 +22,13 @@
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/scope-mappings">{{:: 'scope' | translate}}</a>
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
</li>
- <li ng-class="{active: path[4] == 'authz'}" data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('AUTHORIZATION') == -1 && !disableAuthorizationTab && client.authorizationServicesEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
- <li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a></li>
+ <li ng-class="{active: path[4] == 'authz'}"
+ data-ng-show="serverInfo.profileInfo.previewEnabled && !disableAuthorizationTab && client.authorizationServicesEnabled">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' |
+ translate}}</a></li>
+ <li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2'"><a
+ href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a>
+ </li>
<!-- <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
<li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/sessions">{{:: 'sessions' | translate}}</a>
@@ -39,7 +47,7 @@
<kc-tooltip>{{:: 'installation.tooltip' | translate}}</kc-tooltip>
</li>
- <li ng-class="{active: path[4] == 'service-account-roles'}" data-ng-show="!disableServiceAccountRolesTab && client.serviceAccountsEnabled && !(client.bearerOnly || client.publicClient)">
+ <li ng-class="{active: path[4] == 'service-account-roles'}" data-ng-show="client.serviceAccountsEnabled && !(client.bearerOnly || client.publicClient)">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-account-roles">{{:: 'service-account-roles' | translate}}</a>
<kc-tooltip>{{:: 'service-account-roles.tooltip' | translate}}</kc-tooltip>
</li>
@@ -48,4 +56,4 @@
<kc-tooltip>{{:: 'manage-permissions-client.tooltip' | translate}}</kc-tooltip>
</li>
</ul>
-</div>
\ No newline at end of file
+</div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html
index 7bba535..6328c4e 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html
@@ -12,7 +12,7 @@
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers">{{:: 'mappers' | translate}}</a>
<kc-tooltip>{{:: 'mappers.tooltip' | translate}}</kc-tooltip>
</li>
- <li ng-class="{active: path[4] == 'scope-mappings'}" >
+ <li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="client.protocol != 'docker-v2'">
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/scope-mappings">{{:: 'scope' | translate}}</a>
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
</li>
diff --git a/themes/src/main/resources/theme/base/email/html/email-test.ftl b/themes/src/main/resources/theme/base/email/html/email-test.ftl
new file mode 100644
index 0000000..604415d
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/html/email-test.ftl
@@ -0,0 +1,5 @@
+<html>
+<body>
+${msg("emailTestBodyHtml",realmName)}
+</body>
+</html>
diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
index 9281bb7..8a0ae92 100755
--- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
@@ -1,6 +1,9 @@
emailVerificationSubject=Verify email
emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message.
emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">Link to e-mail address verification</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p>
+emailTestSubject=[KEYCLOAK] - SMTP test message
+emailTestBody=This is a test message
+emailTestBodyHtml=<p>This is a test message</p>
identityProviderLinkSubject=Link {0}
identityProviderLinkBody=Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {4} minutes.\n\nIf you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.
identityProviderLinkBodyHtml=<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2} . If this was you, click the link below to link accounts</p><p><a href="{3}">Link to confirm account linking</a></p><p>This link will expire within {4} minutes.</p><p>If you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>
diff --git a/themes/src/main/resources/theme/base/email/text/email-test.ftl b/themes/src/main/resources/theme/base/email/text/email-test.ftl
new file mode 100644
index 0000000..19942c7
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/text/email-test.ftl
@@ -0,0 +1 @@
+${msg("emailTestBody", realmName)}
\ No newline at end of file
diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties
index f8c7145..9068321 100755
--- a/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties
+++ b/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties
@@ -1,7 +1,15 @@
emailVerificationSubject=E-Mail verifizieren
-passwordResetSubject=Passwort zur\u00FCcksetzen
emailVerificationBody=Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Falls Sie das waren, dann klicken Sie auf den Link, um die E-Mail Adresse zu verifizieren.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.
emailVerificationBodyHtml=<p>Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Falls das Sie waren, klicken Sie auf den Link, um die E-Mail Adresse zu verifizieren.</p><p><a href="{0}">{0}</a></p><p>Dieser Link wird in {1} Minuten ablaufen.</p><p>Falls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.</p>
+identityProviderLinkSubject=Link {0}
+identityProviderLinkBody=Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{3}\n\n Die G\u00FCltigkeit des Links wird in {4} Minuten verfallen.\n\nSollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} \u00FCber {0} erm\u00F6glicht.
+identityProviderLinkBodyHtml=<p>Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.</p><p><a href="{3}">Link zur Best\u00E4tigung der Kontoverkn\u00FCpfung</a></p><p>Die G\u00FCltigkeit des Links wird in {4} Minuten verfallen.</p><p>Sollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} \u00FCber {0} erm\u00F6glicht.</p>
+passwordResetSubject=Passwort zur\u00FCcksetzen
+passwordResetBody=Es wurde eine \u00C4nderung der Anmeldeinformationen f\u00FCr Ihren Account {2} angefordert. Wenn Sie diese \u00C4nderung beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{0}\n\nDie G\u00FCltigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie keine \u00C4nderung vollziehen wollen k\u00F6nnen Sie diese Nachricht ignorieren und an Ihrem Account wird nichts ge\u00E4ndert.
+passwordResetBodyHtml=<p>Es wurde eine \u00C4nderung der Anmeldeinformationen f\u00FCr Ihren Account {2} angefordert. Wenn Sie diese \u00C4nderung beantragt haben, klicken Sie auf den unten stehenden Link.</p><p><a href="{0}">Link zum Zur\u00FCcksetzen von Anmeldeinformationen</a></p><p>Die G\u00FCltigkeit des Links wird in {1} Minuten verfallen.</p><p>Sollten Sie keine \u00C4nderung vollziehen wollen k\u00F6nnen Sie diese Nachricht ignorieren und an Ihrem Account wird nichts ge\u00E4ndert.</p>
+executeActionsSubject=Aktualisieren Sie Ihr Konto
+executeActionsBody=Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.\n\n{0}\n\nDie G\u00FCltigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unver\u00E4ndert.
+executeActionsBodyHtml=<p>Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.</p><p><a href="{0}">Link zum Account-Update</a></p><p>Die G\u00FCltigkeit des Links wird in {1} Minuten verfallen.</p><p>Sollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unver\u00E4ndert.</p>
eventLoginErrorSubject=Fehlgeschlagene Anmeldung
eventLoginErrorBody=Jemand hat um {0} von {1} versucht, sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.
eventLoginErrorBodyHtml=<p>Jemand hat um {0} von {1} versucht, sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>
diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties
index 087170a..a60ffe3 100644
--- a/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties
+++ b/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties
@@ -1,16 +1,16 @@
# encoding: utf-8
emailVerificationSubject=Eメールの確認
emailVerificationBody=このメールアドレスで {2} アカウントが作成されたました。以下のリンクをクリックしてメールアドレスの確認を完了してください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\nもしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。
-emailVerificationBodyHtml=<p>このメールアドレスで {2} アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。</p><p><a href="{0}">{0}</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>もしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。</p>
+emailVerificationBodyHtml=<p>このメールアドレスで {2} アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。</p><p><a href="{0}">メールアドレスの確認</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>もしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。</p>
identityProviderLinkSubject=リンク {0}
identityProviderLinkBody=あなたの "{1}" アカウントと {2} ユーザーの "{0}" アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。\n\n{3}\n\nこのリンクは {4} 分間だけ有効です。\n\nもしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0} 経由で {1} にログインすることができるようになります。
-identityProviderLinkBodyHtml=<p>あなたの <b>{1}</b> アカウントと {2} ユーザーの <b>{0}</b> アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。</p><p><a href="{3}">{3}</a></p><p>このリンクは {4} 分間だけ有効です。</p><p>もしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0} 経由で {1} にログインすることができるようになります。</p>
+identityProviderLinkBodyHtml=<p>あなたの <b>{1}</b> アカウントと {2} ユーザーの <b>{0}</b> アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。</p><p><a href="{3}">アカウントリンクの確認</a></p><p>このリンクは {4} 分間だけ有効です。</p><p>もしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0} 経由で {1} にログインすることができるようになります。</p>
passwordResetSubject=パスワードのリセット
-passwordResetBody=あなたの {2} アカウントのクレデンシャルの変更が要求されています。以下のリンクをクリックしてクレデンシャルのリセットを行ってください。\n\n{0}\n\nこのリンクとコードは {1} 分間だけ有効です。\n\nもしクレデンシャルのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。
-passwordResetBodyHtml=<p>あなたの {2} アカウントのクレデンシャルの変更が要求されています。以下のリンクをクリックしてクレデンシャルのリセットを行ってください。</p><p><a href="{0}">{0}</a></p><p>このリンクとコードは {1} 分間だけ有効です。</p><p>もしクレデンシャルのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。</p>
+passwordResetBody=あなたの {2} アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\nもしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。
+passwordResetBodyHtml=<p>あなたの {2} アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。</p><p><a href="{0}">パスワードのリセット</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>もしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。</p>
executeActionsSubject=アカウントの更新
executeActionsBody=管理者よりあなたの {2} アカウントの更新が要求されています。以下のリンクをクリックしてこのプロセスを開始してください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\n管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。
-executeActionsBodyHtml=<p>管理者よりあなたの {2} アカウントの更新が要求されています。以下のリンクをクリックしてこのプロセスを開始してください。</p><p><a href="{0}">{0}</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。</p>
+executeActionsBodyHtml=<p>管理者よりあなたの {2} アカウントの更新が要求されています。以下のリンクをクリックしてこのプロセスを開始してください。</p><p><a href="{0}">アカウントの更新</a></p><p>このリンクは {1} 分間だけ有効です。</p><p>管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。</p>
eventLoginErrorSubject=ログインエラー
eventLoginErrorBody={0} に {1} からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は、管理者に連絡してください。
eventLoginErrorBodyHtml=<p>{0} に {1} からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は管理者に連絡してください。</p>