diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 91b0a806cc..7f97e55b8a 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 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 0000000000..969bcb03ab --- /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 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 getActions() { + return actions; + } + + public DockerAccess setActions(final List 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 0000000000..b33bb58749 --- /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 dockerErrorDetails; + + public DockerError(final String errorCode, final String message, final List dockerErrorDetails) { + this.errorCode = errorCode; + this.message = message; + this.dockerErrorDetails = dockerErrorDetails; + } + + public String getErrorCode() { + return errorCode; + } + + public String getMessage() { + return message; + } + + public List 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 0000000000..3d961ce946 --- /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 errorList; + + public DockerErrorResponseToken(final List 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 0000000000..98074fa689 --- /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 0000000000..faee452c5b --- /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 accessItems = new ArrayList<>(); + + public List 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 670e1d8bde..c3dd733262 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 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 e1b704e206..8dcf00631f 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 operationalInfo; + public int getOrder() { + return order; + } + + public void setOrder(int priorityUI) { + this.order = priorityUI; + } + public Map getOperationalInfo() { return operationalInfo; } 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 d1945ad42a..9925a690a0 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 @@ -1038,6 +1038,18 @@ public class RealmAdapter implements CachedRealmModel { updated.setClientAuthenticationFlow(flow); } + @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 getAuthenticationFlows() { if (isUpdated()) return updated.getAuthenticationFlows(); 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 3668d9740e..160fee56b6 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 getDefaultGroups() { return defaultGroups; } 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 eba62db5c9..cd814f4605 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 @@ -1375,6 +1375,18 @@ public class RealmAdapter implements RealmModel, JpaModel { realm.setClientAuthenticationFlow(flow.getId()); } + @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 getAuthenticationFlows() { return realm.getAuthenticationFlows().stream() 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 13988dcd12..33578e3156 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 getClientTemplates() { return clientTemplates; } 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 bd55645295..daa1c5040f 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. --> - + + + + + + - @@ -38,9 +42,6 @@ - - - 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 17cd0ac53c..98686af7a0 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 Bill Burke - * @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 b02881406e..8030da6c97 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 b454460325..dc69fc82de 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 6a7aeaa165..6b7016ff5f 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 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 ffb6a445c7..a18c27a087 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/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index f6484d6f5b..ff1cfd78bb 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 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 3cf8d2cab7..5c83253a8d 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 { public String getId(); + default int order() { + return 0; + } + } diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java index 9c1e5a5e8e..11d44af84a 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/DockerAuthV2Protocol.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java new file mode 100644 index 0000000000..3a7a3247ba --- /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 accessCode = new ClientSessionCode<>(session, realm, clientSession); + final Set 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 0000000000..be4c6c0bac --- /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 builtins = new ArrayList<>(); + static List 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 getBuiltinMappers() { + return builtins; + } + + @Override + public List 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/DockerAuthenticator.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java new file mode 100644 index 0000000000..b2c2b37886 --- /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 0000000000..9bba9c490c --- /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 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/DockerEndpoint.java b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java new file mode 100644 index 0000000000..8cf50e8bc6 --- /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 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 0000000000..384f2182a5 --- /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 byteStream(final byte[] bytes) { + final Collection 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 { + + @Override + public Supplier supplier() { + return () -> new StringBuilder(); + } + + @Override + public BiConsumer 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 combiner() { + return ((left, right) -> new StringBuilder(left.toString()).append(right.toString())); + } + + @Override + public Function finisher() { + return StringBuilder::toString; + } + + @Override + public Set 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 0000000000..a0dad58129 --- /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/DockerComposeYamlInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java new file mode 100644 index 0000000000..72ade3116c --- /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 0000000000..ba4440a21c --- /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 0000000000..055d9ac043 --- /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/installation/compose/DockerCertFileUtils.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java new file mode 100644 index 0000000000..66870899db --- /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 0000000000..9d607f4be5 --- /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 localhostCertFile; + private final Map.Entry localhostKeyFile; + private final Map.Entry 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 getLocalhostCertFile() { + return localhostCertFile; + } + + public Map.Entry getLocalhostKeyFile() { + return localhostKeyFile; + } + + public Map.Entry 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 0000000000..1630ffaec0 --- /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 0000000000..a4d0ee2012 --- /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/mapper/AllowAllDockerProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java new file mode 100644 index 0000000000..398eeb61e1 --- /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 0000000000..320686be96 --- /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 0000000000..69ccd004ed --- /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 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 3a7e4c0e36..402be4cb03 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 f21eff3bf0..f6821b6331 --- 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,182 +1,122 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package org.keycloak.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 Pedro Igor - */ -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); + } + } + } + + protected void userSuccessAction(AuthenticationFlowContext context, UserModel user) { + context.getAuthenticationSession().setAuthenticatedUser(user); + context.success(); + } + + protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) { + userSuccessAction(context, user); + } + + protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String user) { + // no-op by default + } + + 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()); + } + + private String[] getUsernameAndPassword(final HttpHeaders httpHeaders) { + final List authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION); + + if (authHeaders == null || authHeaders.size() == 0) { + return null; + } + + String credentials = null; + + for (final String authHeader : authHeaders) { + if (authHeader.startsWith(BASIC_PREFIX)) { + final 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 (final IOException e) { + throw new RuntimeException("Failed to parse credentials.", e); + } } @Override - public String getReferenceCategory() { - return null; + public void action(final AuthenticationFlowContext context) { + } @Override - public boolean isConfigurable() { + public boolean requiresUser() { return false; } @Override - public Requirement[] getRequirementChoices() { - return new Requirement[0]; - } - - @Override - public boolean isUserSetupAllowed() { + public boolean configuredFor(final KeycloakSession session, final RealmModel realm, final UserModel user) { return false; } @Override - public String getHelpText() { - return "Validates username and password from Authorization HTTP header"; - } - - @Override - public List getConfigProperties() { - 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()); - } - } - } - } - - private String[] getUsernameAndPassword(HttpHeaders httpHeaders) { - List authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION); - - 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) { - - } - - @Override - public void close() { - - } - }; - } - - @Override - public void init(Config.Scope config) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { + public void setRequiredActions(final KeycloakSession session, final RealmModel realm, final UserModel user) { } @@ -184,9 +124,4 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory { public void close() { } - - @Override - public String getId() { - return PROVIDER_ID; - } } 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 0000000000..01adca2dc0 --- /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 Pedro Igor + */ +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 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 6c1779471a..a391c1d445 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 0000000000..84dff48460 --- /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 208f16dade..2b11382be1 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 a0d8052082..f38a5c2366 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 38e1b5a918..e954f2ee7a 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 04f090ecf4..95b79cfb01 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 0000000000..a5f494c20c --- /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 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 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 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 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 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 0000000000..0fa8cb9888 --- /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 0000000000..3c912de3f9 --- /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 cc8156bb5d..225d709338 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 caa76aa32f..59116e841a 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -88,6 +88,28 @@ greenmail compile + + + + + + + + + + + + + + + + + + org.testcontainers + testcontainers + 1.2.1 + test + 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 ad71d385f0..84b8282a1d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java @@ -25,6 +25,10 @@ import org.keycloak.common.Profile; */ public class ProfileAssume { + public static void assumeFeatureEnabled(Profile.Feature feature) { + Assume.assumeTrue("Ignoring test as " + feature.name() + " is not enabled", Profile.isFeatureEnabled(feature)); + } + public static void assumePreview() { Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", !Profile.getName().equals("product")); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java index e8852bc151..f9d557b3f4 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/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 0481518518..57fe6de7bc 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 e13794dc8e..f55e90f48e 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 0000000000..f947d9e2d5 --- /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 = 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 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 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 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 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 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 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 0000000000..b73471c7e4 --- /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. + *

+ * 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> { + + @Override + public Optional get() { + final Enumeration 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 0000000000..eac009228e --- /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> { + private static final Logger log = LoggerFactory.getLogger(DockerHostVersionSupplier.class); + + @Override + public Optional 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 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 0000000000..727af1dbb8 --- /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 authenticationExecutions = Optional.ofNullable(dockerBasicAuthFlow.getAuthenticationExecutions()).orElse(new ArrayList<>()); + authenticationExecutions.add(dockerBasicAuthExecution); + dockerBasicAuthFlow.setAuthenticationExecutions(authenticationExecutions); + + final List 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 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 0000000000..7182c54ecd --- /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 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 minor, final Optional 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 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 a769687f0d..cfdf0b7979 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 f4a93f3f16..8a3d8bc933 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 @@ ${auth.server.undertow} && ! ${auth.server.undertow.crossdc} - localhost + 0.0.0.0 org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow ${auth.server.http.port} ${undertow.remote} 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 0000000000..9f9d2ff03a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json @@ -0,0 +1,1315 @@ +{ + "id" : "docker-test-realm", + "realm" : "docker-test-realm", + "notBefore" : 0, + "revokeRefreshToken" : false, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "offlineSessionIdleTimeout" : 2592000, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "dbcbd18f-52cb-4e45-9372-7e2bbf255729", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : false, + "containerId" : "docker-test-realm" + }, { + "id" : "834687f7-29ce-43a2-a5f7-55c965026827", + "name" : "offline_access", + "description" : "${role_offline-access}", + "scopeParamRequired" : true, + "composite" : false, + "clientRole" : false, + "containerId" : "docker-test-realm" + } ], + "client" : { + "realm-management" : [ { + "id" : "11956a41-328d-4cec-a98c-f77fe6accda3", + "name" : "create-client", + "description" : "${role_create-client}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "e65e7810-359b-429d-9389-c1cd041915fd", + "name" : "view-clients", + "description" : "${role_view-clients}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "43d747fc-76c3-4a06-a492-44dea5a07edb", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "de324c4c-34ea-467b-b851-cca912d1cf60", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "b0f25ef8-404b-4370-a981-ca155eae6b83", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "c16eb517-5416-4b86-b86d-c312d3b98e09", + "name" : "impersonation", + "description" : "${role_impersonation}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "1526f875-2d04-453a-aa29-979f61d1013c", + "name" : "view-events", + "description" : "${role_view-events}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "7043cd10-a2b0-4568-8295-9840c9c2fa43", + "name" : "view-realm", + "description" : "${role_view-realm}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "23bb0cd9-2c0e-4510-96af-73f0ba1251df", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "eff4c8dd-0c53-41ca-8013-336b9c19f55b", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "41cead2f-ed3f-4add-8fd2-ceaf3e20daf5", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "scopeParamRequired" : false, + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "create-client", "view-clients", "manage-clients", "view-authorization", "manage-identity-providers", "impersonation", "view-events", "view-realm", "manage-realm", "manage-authorization", "view-users", "manage-events", "manage-users", "view-identity-providers" ] + } + }, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "025e57f5-73a2-4382-b6e7-ea2f447f86a5", + "name" : "view-users", + "description" : "${role_view-users}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "86fce514-f5f4-4c7d-ae07-56caaeffe272", + "name" : "manage-events", + "description" : "${role_manage-events}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "8f49cc1a-a3f1-4185-982e-765617c1ac88", + "name" : "manage-users", + "description" : "${role_manage-users}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "e8d7cf8e-b970-4ada-a8b5-58b7d5fcc4e8", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "broker" : [ { + "id" : "f0eb6730-f5ed-4216-a9db-d87fee982b08", + "name" : "read-token", + "description" : "${role_read-token}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "f85d993b-f251-4f9c-87f9-6586cb7bb830" + } ], + "account" : [ { + "id" : "8a34db5e-26fb-4be0-ba09-d4e92bc9dd88", + "name" : "view-profile", + "description" : "${role_view-profile}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9" + }, { + "id" : "5aef5567-004e-4a18-8ee4-b8a6d5fa0c85", + "name" : "manage-account", + "description" : "${role_manage-account}", + "scopeParamRequired" : false, + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9" + }, { + "id" : "3bf09e38-5f0d-41c8-adc2-1dba1cf5d819", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9" + } ] + } + }, + "groups" : [ ], + "defaultRoles" : [ "offline_access", "uma_authorization" ], + "requiredCredentials" : [ "password" ], + "passwordPolicy" : "hashIterations(20000)", + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "users" : [ { + "id" : "a413b2e2-5cff-43e4-ac6e-ab307e8c0652", + "createdTimestamp" : 1492117705870, + "username" : "user1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "User", + "lastName" : "One", + "email" : "user1@redhat.com", + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "A1B2lKKJ2npPjSoFo653q2H8Wu/CNoAVD9pYUnAJwMb0AJzAfXGkdX6eHSUEyUK1cDGVfn6iX/JRNo5XyoSH2w==", + "salt" : "5X0JI44mCfleW8qR08II1A==", + "hashIterations" : 20000, + "counter" : 0, + "algorithm" : "pbkdf2", + "digits" : 0, + "period" : 0, + "createdDate" : 1492117716198, + "config" : { } + } ], + "disableableCredentialTypes" : [ "password" ], + "requiredActions" : [ ], + "realmRoles" : [ "uma_authorization", "offline_access" ], + "clientRoles" : { + "account" : [ "view-profile", "manage-account" ] + }, + "groups" : [ ] + } ], + "clientScopeMappings" : { + "realm-management" : [ { + "client" : "admin-cli", + "roles" : [ "realm-admin" ] + }, { + "client" : "security-admin-console", + "roles" : [ "realm-admin" ] + } ] + }, + "clients" : [ { + "id" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9", + "clientId" : "account", + "name" : "${client_account}", + "baseUrl" : "/auth/realms/docker-test-realm/account", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "e4f21dc6-959f-4248-8e04-4fb606d9ceaf", + "defaultRoles" : [ "view-profile", "manage-account" ], + "redirectUris" : [ "/auth/realms/docker-test-realm/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "d0e8f6a9-9442-443e-af03-7d31545af866", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "2afbd4f6-e9bc-45d1-92ee-1c4dc9c099d5", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "1bd8d67f-3aac-42cc-8dba-e676a2b41bb1", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "d7df006b-686a-41a8-958b-2525b9c48ff2", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "93bee57d-79e3-42fb-87da-71c05963aa49", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + }, { + "id" : "297ecd2f-4440-48aa-82aa-74901588f7c1", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "ac4d45a0-c127-4ba3-b243-49cc570a9871", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + }, { + "id" : "e0105ad8-27c3-471d-99c3-244762847563", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "72ff8162-b891-4ba3-9501-68e2e34d7cf0", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "c61ba5ee-a8e1-409c-9898-cb8b9697eb26", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "879e8a4f-e4e9-402d-b867-59171fbcb370", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "ee335ebe-a3bd-426a-9622-268ad583fe67", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + }, { + "id" : "628083f3-62f0-454a-bc35-80728893513b", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "48efdb06-c88b-478f-9009-65bac264de00", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "5683455d-bcaf-41ca-8b0e-da15dfd48753", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "31d6698d-10f0-4fd9-b7f3-c4bc23b507dc", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + }, { + "id" : "f85d993b-f251-4f9c-87f9-6586cb7bb830", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "1fbd3ca1-203f-4074-b1d5-b0c6c2739ea4", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "9b754f6b-0a03-4db5-80f9-3c4f656e0828", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "384701c9-c08a-483f-8f44-b288c8694fe3", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "c2e767e6-7744-457b-8dea-e6f170a5122c", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "796cc4cd-b7a5-4255-bf8b-3b99db7532ee", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + }, { + "id" : "528ba572-1438-4afc-88c7-02f5e511d433", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "a14f7e92-23ea-444f-8bb8-f2dfb1f255dc", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "724e61f0-b490-46b1-b063-2ee122e4ac7a", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + }, { + "id" : "2d61e404-7444-4fad-8386-06b811b5f7c1", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "403c5eae-8c79-4cfc-ba00-4bb2bfbaaf92", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "098aeaab-76f1-4742-8522-27e8c178e596", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "fbc2f08d-d6a0-49ad-9b61-601eec42d46f", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "b39438d3-a149-4e0f-a3a1-87c441d05123", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "06706c9d-1f71-4cc8-afca-daea4e9fe9e8", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "bcf14207-1f8e-4e53-8d2b-59939e82f8c4", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "91e78da7-b049-41a5-9a22-1f833755c41b", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "3949f934-b86b-4e70-bcc4-52db0288d55b", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + }, { + "id" : "7d4ec353-1cf7-43a1-af4d-218fd9dd37ed", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "baseUrl" : "/auth/admin/docker-test-realm/console/index.html", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "a0e6ebf9-58fa-472c-a853-64c16c2f8ad8", + "redirectUris" : [ "/auth/admin/docker-test-realm/console/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "c501a7bc-171b-4ce6-8d91-3f69ae32591d", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "bce6f7a9-b86d-4f5f-a262-f01e235b5622", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "consentText" : "${locale}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "9d28d5da-53f2-49f9-b0c0-ae3a51f5ac92", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + }, { + "id" : "00183de0-af80-47c5-807f-a62366b2e1b6", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "31eccf32-3e16-44f2-b727-27c5cb2e9554", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "c26c0dc9-4cba-42f0-80e4-1f2363084b95", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "db4d11d2-e243-4df7-811f-e4622b49950b", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "e6d398a7-dbec-480f-93c4-8a9d1bfbad24", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + } ], + "clientTemplates" : [ ], + "browserSecurityHeaders" : { + "xContentTypeOptions" : "nosniff", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "7f9cbf76-3ecb-49ed-850b-f2fce4ecc87f", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "ea2db337-b9d9-463b-abea-0c5dadb5b5f0", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "2d6e7a94-d73c-4f54-b9ea-64f563f5f8fa", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "16f6705e-f671-4fde-ba7d-6254e404b503", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "e4baf3d7-e7af-48d0-890d-11304927be69", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ], + "consent-required-for-all-mappers" : [ "true" ] + } + }, { + "id" : "c27ecc77-c0c3-462e-b803-33432c9a7813", + "name" : "Allowed Client Templates", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "18bdc70c-5475-4ae4-8606-d52a6397a125", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ], + "consent-required-for-all-mappers" : [ "true" ] + } + }, { + "id" : "95fd260b-36e9-4df5-aa6b-6c3b8138c766", + "name" : "Allowed Client Templates", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "9dc7e4c1-5bc2-4756-9486-fb64a06582ad", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7roLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJFLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlwfcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMDAxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABAoIBAQCLigz0Q41OVlDt+ALQAYMj4lr8DgtcprRzQ8Tggu31hqom5Pv3woa+5OSuh9LjGY1OD/f1zLWkZI/kdcarx2I8m29rtUfU9QobcPhyXcqa7Y5DZlV/IHj5YUjqi8txMz0aOlhlcXa3qHz9eXlX18wN0SKuu4vJCQzWnEH4DS9ZTwXAp4uZUkOIUHIkACcRPBGBVHCNvwneLA7tPi5E1TK2fvlgyHOvbsomBh385WKrO6HFBmjV9XsMx3QU1EjRaXSpELdIDUR9Z8rgVg08nZ8z3LZ9UNHHdiAXoCm5oqqf8zP5gL6U79vybvjerCpx2AX60UkhpuHeUmZQQMcylLLhAoGBAP4xdt/gkBsC+9faAw3o9VW/6RsdW7ussptnt50Ymi/mlE8qHNe0oSbkGAhqdqCjAV0+cgygn2krOM+OUF/Lq87kBgRE0fAqaarEAryT/DrmvroNrp3Lnif9/kAcEWo8WhpIPgspqzVy7byAFR29/sdbVby2C37OeFYpw0ad/UVdAoGBAJRylgu59wM5ekrmJqNd326J+RLg76abF9TpW3Ka5CY12NgI60ZxRFBfncZKJCTovmoZgE89RHdz7n4ghxVg8D9ThPY7Kh4flAq8SIqAqmb2b7hkfyEMOgGpdwQq1T7uIcIefwYivLpb62C8cSK7leLXJ/wMza5bo8m5fD3t+a2VAoGAJZxqC2wtxmFlpCWU6Bz9GAgCVMm+RgGil8375Bm8zrOeZCxGAkCuy5NaXvxpuxEDZamUtHuburLzf/p9t/7p1/3zSfRo39FWuzavdPmsi4aS1/KoUJ7NMvupABFnHkH5zwO7cmli9NChjo+hEDqJlTPVdsu03bltIsqhIzTDQd0CgYAQ8owCxrZWnedCScg7emoZupK+/wMdKDOuUP3ptZk6a4dYEpyZrDC6ZFAk5S3/MLscbdDiOwJoCMo/iAMkA68p66UQX2zNh5llKF23wjyyCIx0prSE11p/+hLmXOV/i7w65zRlRO368KeMobbg2j2gaiPceLG6qCeozg5LG7IXiQKBgALwLpGKaIixsIaAD1Bzd5cLaKdPGXPyaJwG5xqog58XGVcHklGQRnaN/B3vlrHBgI/NGZNt83bWamCTVlN+A0q9AnMxGHXZHzL21lx6bNiZXX+3DVDm88m+ODPebZXxSZQRNjBrw1KotqUyyhzkbIjfE8752ofb4T+veViHkjW2" ], + "certificate" : [ "MIICsTCCAZkCBgFbaSTAdjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFkb2NrZXItdGVzdC1yZWFsbTAeFw0xNzA0MTMyMTA2MDdaFw0yNzA0MTMyMTA3NDdaMBwxGjAYBgNVBAMMEWRvY2tlci10ZXN0LXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7roLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJFLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlwfcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMDAxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB2u9hP3S1bP4+FBwOLPwI3p7WrWBlt2CgwTiyuXvV7u9GLiXqCDUWZd3dSks9vU4Y4NdVyToY4q9YFJ3oAQXlfRw2Yi6e/0nSPpU25o52TWwREnRY98fjVy1eC5K2GRwSu79HZKeqA0Tg/ONvGOrlYO1KPbWZGg9NcwAGeILkNdfI82w0KZTpTy+f5ATtV30pFkDNT0gfayFmDQvw3EgcD/x0/vI3PlnHLLGprV/ZlBmFWo0vk8iUBwP1YbTA0XqKasITFXJaPeZWzNMCjR1NxDqlIq095uX04E5XGS6XGJKS9PanvGXidk5xMgI7xwKE6jaxD9pspYPRgv66528Dc" ], + "priority" : [ "100" ] + } + }, { + "id" : "ae58bc1e-c60e-4889-986d-ea5648ea5989", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "5a0c54c4-fb3d-4b2c-8e1a-9bebb6251b6f" ], + "secret" : [ "-5XJ1f5410LDE1XIvQsvAuwwm4CdEyd6Rco0E3EsxG4" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "6a3d3800-bea6-4fc4-958f-65365d23c33b", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "idp-email-verification", + "requirement" : "ALTERNATIVE", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 30, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "41de318f-6434-443a-bcf0-6632568f32b0", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-otp-form", + "requirement" : "OPTIONAL", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "8b2f90df-5a09-49b6-b978-acbb74a60670", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-spnego", + "requirement" : "DISABLED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "identity-provider-redirector", + "requirement" : "ALTERNATIVE", + "priority" : 25, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 30, + "flowAlias" : "forms", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "6d0cba98-a1d9-4ca4-a877-ffe0d2c7f667", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "client-jwt", + "requirement" : "ALTERNATIVE", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "8c752045-bd44-48fc-ae36-816625897545", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "direct-grant-validate-password", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "requirement" : "OPTIONAL", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "7c8e6906-6b5f-4766-b80d-f23b56595992", + "alias" : "docker-basic-auth-flow", + "description" : "", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : false, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "requirement" : "REQUIRED", + "priority" : 0, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "a41036cf-e368-46e0-9cf3-a96908c53609", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "requirement" : "ALTERNATIVE", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 30, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "49c349cc-f11e-461c-98e2-546327175ca4", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-otp-form", + "requirement" : "OPTIONAL", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "2445867e-f9eb-46cc-8f68-c15d6cf962e4", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "requirement" : "REQUIRED", + "priority" : 10, + "flowAlias" : "registration form", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "83a735c2-cf61-49fa-879b-e9b0ed5bb9e9", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-profile-action", + "requirement" : "REQUIRED", + "priority" : 40, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-password-action", + "requirement" : "REQUIRED", + "priority" : 50, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-recaptcha-action", + "requirement" : "DISABLED", + "priority" : 60, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "32acb7cb-af8f-42b2-bd34-9ff534d87121", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-credential-email", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-password", + "requirement" : "REQUIRED", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-otp", + "requirement" : "OPTIONAL", + "priority" : 40, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "1c67b912-70f4-4182-b055-08c3d6bb23c8", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "30fd72e5-eb98-4ae5-a695-c959ec626ac6", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "e0ea82a7-98d7-4ffb-8444-8d240a94d83b", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "config" : { } + }, { + "alias" : "terms_and_conditions", + "name" : "Terms and Conditions", + "providerId" : "terms_and_conditions", + "enabled" : false, + "defaultAction" : false, + "config" : { } + } ], + "browserFlow" : "docker-basic-auth-flow", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "attributes" : { + "_browser_header.xFrameOptions" : "SAMEORIGIN", + "failureFactor" : "30", + "quickLoginCheckMilliSeconds" : "1000", + "maxDeltaTimeSeconds" : "43200", + "_browser_header.xContentTypeOptions" : "nosniff", + "_browser_header.xRobotsTag" : "none", + "bruteForceProtected" : "false", + "maxFailureWaitSeconds" : "900", + "_browser_header.contentSecurityPolicy" : "frame-src 'self'", + "minimumQuickLoginWaitSeconds" : "60", + "waitIncrementSeconds" : "60" + } +} diff --git a/testsuite/integration-arquillian/tests/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 0000000000..a7493f1f52 --- /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 0000000000..6b50a0459c --- /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 0000000000..22a39863af --- /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 0000000000..53702a6fd5 --- /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 0000000000..433cbc58dd --- /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 0000000000..fe1af6104a --- /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 0000000000..7fd8485cce --- /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/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 28a985c516..73412e983d 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 c4e870fdac..15c86dc580 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 515eb99720..db29ebef20 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 14eb89c523..1daf30741b 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 3170052646..e671c13d3a 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 8a9d0e1ecd..0ef489b3db 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 @@

- +
+ +
+
+ {{:: 'docker-auth.tooltip' | translate}} +
+
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 cd6e2717be..979c713f21 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 @@
{{:: 'client.enabled.tooltip' | translate}}
-
+
@@ -239,7 +239,7 @@ {{:: 'name-id-format.tooltip' | translate}}
-
+
@@ -247,7 +247,7 @@ {{:: 'root-url.tooltip' | translate}}
-
+
@@ -269,14 +269,14 @@ {{:: 'valid-redirect-uris.tooltip' | translate}}
-
+
{{:: 'base-url.tooltip' | translate}}
-
+
{{:: 'mappers' | translate}} {{:: 'mappers.tooltip' | translate}} -
  • +
  • {{:: 'scope' | translate}} {{:: 'scope.tooltip' | translate}}
  • 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 e5b7c21c70..4155b4692d 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 @@ -
    \ No newline at end of file +