keycloak-uncached

Merge pull request #4269 from stianst/dockerdockerdocker KEYCLOAK-3592

6/29/2017 2:23:47 AM

Changes

Details

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/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/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;
     }
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>
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/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 ffb6a44..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
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/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/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/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
diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index cc8156b..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
 
@@ -454,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):
@@ -466,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/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/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/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/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/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/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index f4a93f3..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} &amp;&amp; ! ${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>
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/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 14eb89c..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
@@ -1935,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");
 });
 
@@ -2129,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 3170052..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
@@ -335,8 +335,38 @@ 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);
     });
 
@@ -346,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/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>