Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
999dff353c
173 changed files with 6324 additions and 501 deletions
|
@ -92,6 +92,8 @@ public class KeycloakDeployment {
|
|||
protected boolean pkce = false;
|
||||
protected boolean ignoreOAuthQueryParameter;
|
||||
|
||||
protected Map<String, String> redirectRewriteRules;
|
||||
|
||||
public KeycloakDeployment() {
|
||||
}
|
||||
|
||||
|
@ -446,4 +448,14 @@ public class KeycloakDeployment {
|
|||
public boolean isOAuthQueryParameterEnabled() {
|
||||
return !this.ignoreOAuthQueryParameter;
|
||||
}
|
||||
|
||||
public Map<String, String> getRedirectRewriteRules() {
|
||||
return redirectRewriteRules;
|
||||
}
|
||||
|
||||
public void setRewriteRedirectRules(Map<String, String> redirectRewriteRules) {
|
||||
this.redirectRewriteRules = redirectRewriteRules;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -116,6 +116,7 @@ public class KeycloakDeploymentBuilder {
|
|||
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
|
||||
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
|
||||
deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter());
|
||||
deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules());
|
||||
|
||||
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
|
||||
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
|
||||
|
|
|
@ -25,7 +25,6 @@ import org.keycloak.adapters.spi.AuthChallenge;
|
|||
import org.keycloak.adapters.spi.AuthOutcome;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.Encode;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
|
@ -38,7 +37,10 @@ import org.keycloak.representations.IDToken;
|
|||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -141,6 +143,7 @@ public class OAuthRequestAuthenticator {
|
|||
protected String getRedirectUri(String state) {
|
||||
String url = getRequestUrl();
|
||||
log.debugf("callback uri: %s", url);
|
||||
|
||||
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
||||
int port = sslRedirectPort();
|
||||
if (port < 0) {
|
||||
|
@ -170,7 +173,7 @@ public class OAuthRequestAuthenticator {
|
|||
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
|
||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, Encode.encodeQueryParamAsIs(url)) // Need to encode uri ourselves as queryParam() will not encode % characters.
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, rewrittenRedirectUri(url))
|
||||
.queryParam(OAuth2Constants.STATE, state)
|
||||
.queryParam("login", "true");
|
||||
if(loginHint != null && loginHint.length() > 0){
|
||||
|
@ -320,10 +323,11 @@ public class OAuthRequestAuthenticator {
|
|||
|
||||
AccessTokenResponse tokenResponse = null;
|
||||
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
|
||||
|
||||
try {
|
||||
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
|
||||
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
|
||||
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
|
||||
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, rewrittenRedirectUri(strippedOauthParametersRequestUri), httpSessionId);
|
||||
} catch (ServerRequest.HttpFailure failure) {
|
||||
log.error("failed to turn code into token");
|
||||
log.error("status from server: " + failure.getStatus());
|
||||
|
@ -376,5 +380,22 @@ public class OAuthRequestAuthenticator {
|
|||
return builder.build().toString();
|
||||
}
|
||||
|
||||
private String rewrittenRedirectUri(String originalUri) {
|
||||
Map<String, String> rewriteRules = deployment.getRedirectRewriteRules();
|
||||
if(rewriteRules != null && !rewriteRules.isEmpty()) {
|
||||
try {
|
||||
URL url = new URL(originalUri);
|
||||
Map.Entry<String, String> rule = rewriteRules.entrySet().iterator().next();
|
||||
StringBuilder redirectUriBuilder = new StringBuilder(url.getProtocol());
|
||||
redirectUriBuilder.append("://"+ url.getAuthority());
|
||||
redirectUriBuilder.append(url.getPath().replaceFirst(rule.getKey(), rule.getValue()));
|
||||
return redirectUriBuilder.toString();
|
||||
} catch (MalformedURLException ex) {
|
||||
log.error("Not a valid request url");
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
return originalUri;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ public class KeycloakDeploymentBuilderTest {
|
|||
assertEquals(10, deployment.getTokenMinimumTimeToLive());
|
||||
assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
|
||||
assertEquals(120, deployment.getPublicKeyCacheTtl());
|
||||
assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -33,5 +33,8 @@
|
|||
"token-minimum-time-to-live": 10,
|
||||
"min-time-between-jwks-requests": 20,
|
||||
"public-key-cache-ttl": 120,
|
||||
"ignore-oauth-query-parameter": true
|
||||
"ignore-oauth-query-parameter": true,
|
||||
"redirect-rewrite-rules" : {
|
||||
"^/wsmaster/api/(.*)$" : "/api/$1"
|
||||
}
|
||||
}
|
|
@ -54,24 +54,51 @@ import java.util.regex.Pattern;
|
|||
*/
|
||||
public class KeycloakOIDCFilter implements Filter {
|
||||
|
||||
private final static Logger log = Logger.getLogger("" + KeycloakOIDCFilter.class);
|
||||
|
||||
public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern";
|
||||
|
||||
public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver";
|
||||
|
||||
public static final String CONFIG_FILE_PARAM = "keycloak.config.file";
|
||||
|
||||
public static final String CONFIG_PATH_PARAM = "keycloak.config.path";
|
||||
|
||||
protected AdapterDeploymentContext deploymentContext;
|
||||
|
||||
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
||||
|
||||
protected NodesRegistrationManagement nodesRegistrationManagement;
|
||||
|
||||
protected Pattern skipPattern;
|
||||
|
||||
private final static Logger log = Logger.getLogger(""+KeycloakOIDCFilter.class);
|
||||
private final KeycloakConfigResolver definedconfigResolver;
|
||||
|
||||
/**
|
||||
* Constructor that can be used to define a {@code KeycloakConfigResolver} that will be used at initialization to
|
||||
* provide the {@code KeycloakDeployment}.
|
||||
* @param definedconfigResolver the resolver
|
||||
*/
|
||||
public KeycloakOIDCFilter(KeycloakConfigResolver definedconfigResolver) {
|
||||
this.definedconfigResolver = definedconfigResolver;
|
||||
}
|
||||
|
||||
public KeycloakOIDCFilter() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final FilterConfig filterConfig) throws ServletException {
|
||||
|
||||
String skipPatternDefinition = filterConfig.getInitParameter(SKIP_PATTERN_PARAM);
|
||||
if (skipPatternDefinition != null) {
|
||||
skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL);
|
||||
}
|
||||
|
||||
String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver");
|
||||
if (definedconfigResolver != null) {
|
||||
deploymentContext = new AdapterDeploymentContext(definedconfigResolver);
|
||||
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass());
|
||||
} else {
|
||||
String configResolverClass = filterConfig.getInitParameter(CONFIG_RESOLVER_PARAM);
|
||||
if (configResolverClass != null) {
|
||||
try {
|
||||
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
|
||||
|
@ -82,7 +109,7 @@ public class KeycloakOIDCFilter implements Filter {
|
|||
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
||||
}
|
||||
} else {
|
||||
String fp = filterConfig.getInitParameter("keycloak.config.file");
|
||||
String fp = filterConfig.getInitParameter(CONFIG_FILE_PARAM);
|
||||
InputStream is = null;
|
||||
if (fp != null) {
|
||||
try {
|
||||
|
@ -92,7 +119,7 @@ public class KeycloakOIDCFilter implements Filter {
|
|||
}
|
||||
} else {
|
||||
String path = "/WEB-INF/keycloak.json";
|
||||
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
|
||||
String pathParam = filterConfig.getInitParameter(CONFIG_PATH_PARAM);
|
||||
if (pathParam != null) path = pathParam;
|
||||
is = filterConfig.getServletContext().getResourceAsStream(path);
|
||||
}
|
||||
|
@ -100,26 +127,23 @@ public class KeycloakOIDCFilter implements Filter {
|
|||
deploymentContext = new AdapterDeploymentContext(kd);
|
||||
log.fine("Keycloak is using a per-deployment configuration.");
|
||||
}
|
||||
}
|
||||
filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
|
||||
nodesRegistrationManagement = new NodesRegistrationManagement();
|
||||
}
|
||||
|
||||
private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) {
|
||||
|
||||
if (is == null) {
|
||||
log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
||||
return new KeycloakDeployment();
|
||||
}
|
||||
|
||||
return KeycloakDeploymentBuilder.build(is);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
|
||||
|
||||
log.fine("Keycloak OIDC Filter");
|
||||
//System.err.println("Keycloak OIDC Filter: " + ((HttpServletRequest)req).getRequestURL().toString());
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
HttpServletResponse response = (HttpServletResponse) res;
|
||||
|
||||
|
|
|
@ -38,6 +38,8 @@ public final class KeycloakAdapterConfigService {
|
|||
|
||||
private static final String CREDENTIALS_JSON_NAME = "credentials";
|
||||
|
||||
private static final String REDIRECT_REWRITE_RULE_JSON_NAME = "redirect-rewrite-rule";
|
||||
|
||||
private static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService();
|
||||
|
||||
public static KeycloakAdapterConfigService getInstance() {
|
||||
|
@ -130,6 +132,56 @@ public final class KeycloakAdapterConfigService {
|
|||
return deployment.get(CREDENTIALS_JSON_NAME);
|
||||
}
|
||||
|
||||
public void addRedirectRewriteRule(ModelNode operation, ModelNode model) {
|
||||
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
|
||||
if (!redirectRewritesRules.isDefined()) {
|
||||
redirectRewritesRules = new ModelNode();
|
||||
}
|
||||
|
||||
String redirectRewriteRuleName = redirectRewriteRule(operation);
|
||||
if (!redirectRewriteRuleName.contains(".")) {
|
||||
redirectRewritesRules.get(redirectRewriteRuleName).set(model.get("value").asString());
|
||||
} else {
|
||||
String[] parts = redirectRewriteRuleName.split("\\.");
|
||||
String provider = parts[0];
|
||||
String property = parts[1];
|
||||
ModelNode redirectRewriteRule = redirectRewritesRules.get(provider);
|
||||
if (!redirectRewriteRule.isDefined()) {
|
||||
redirectRewriteRule = new ModelNode();
|
||||
}
|
||||
redirectRewriteRule.get(property).set(model.get("value").asString());
|
||||
redirectRewritesRules.set(provider, redirectRewriteRule);
|
||||
}
|
||||
|
||||
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
|
||||
deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME).set(redirectRewritesRules);
|
||||
}
|
||||
|
||||
public void removeRedirectRewriteRule(ModelNode operation) {
|
||||
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
|
||||
if (!redirectRewritesRules.isDefined()) {
|
||||
throw new RuntimeException("Can not remove redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
|
||||
}
|
||||
|
||||
String ruleName = credentialNameFromOp(operation);
|
||||
redirectRewritesRules.remove(ruleName);
|
||||
}
|
||||
|
||||
public void updateRedirectRewriteRule(ModelNode operation, String attrName, ModelNode resolvedValue) {
|
||||
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
|
||||
if (!redirectRewritesRules.isDefined()) {
|
||||
throw new RuntimeException("Can not update redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
|
||||
}
|
||||
|
||||
String ruleName = credentialNameFromOp(operation);
|
||||
redirectRewritesRules.get(ruleName).set(resolvedValue);
|
||||
}
|
||||
|
||||
private ModelNode redirectRewriteRuleFromOp(ModelNode operation) {
|
||||
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
|
||||
return deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME);
|
||||
}
|
||||
|
||||
private String realmNameFromOp(ModelNode operation) {
|
||||
return valueFromOpAddress(RealmDefinition.TAG_NAME, operation);
|
||||
}
|
||||
|
@ -142,6 +194,10 @@ public final class KeycloakAdapterConfigService {
|
|||
return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation);
|
||||
}
|
||||
|
||||
private String redirectRewriteRule(ModelNode operation) {
|
||||
return valueFromOpAddress(RedirecRewritetRuleDefinition.TAG_NAME, operation);
|
||||
}
|
||||
|
||||
private String valueFromOpAddress(String addrElement, ModelNode operation) {
|
||||
String deploymentName = getValueOfAddrElement(operation.get(ADDRESS), addrElement);
|
||||
if (deploymentName == null) throw new RuntimeException("Can't find '" + addrElement + "' in address " + operation.toString());
|
||||
|
|
|
@ -48,6 +48,7 @@ public class KeycloakExtension implements Extension {
|
|||
static final RealmDefinition REALM_DEFINITION = new RealmDefinition();
|
||||
static final SecureDeploymentDefinition SECURE_DEPLOYMENT_DEFINITION = new SecureDeploymentDefinition();
|
||||
static final CredentialDefinition CREDENTIAL_DEFINITION = new CredentialDefinition();
|
||||
static final RedirecRewritetRuleDefinition REDIRECT_RULE_DEFINITON = new RedirecRewritetRuleDefinition();
|
||||
|
||||
public static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) {
|
||||
StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME);
|
||||
|
@ -77,6 +78,7 @@ public class KeycloakExtension implements Extension {
|
|||
registration.registerSubModel(REALM_DEFINITION);
|
||||
ManagementResourceRegistration secureDeploymentRegistration = registration.registerSubModel(SECURE_DEPLOYMENT_DEFINITION);
|
||||
secureDeploymentRegistration.registerSubModel(CREDENTIAL_DEFINITION);
|
||||
secureDeploymentRegistration.registerSubModel(REDIRECT_RULE_DEFINITON);
|
||||
|
||||
subsystem.registerXMLElementWriter(PARSER);
|
||||
}
|
||||
|
|
|
@ -96,12 +96,17 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
PathElement.pathElement(SecureDeploymentDefinition.TAG_NAME, name));
|
||||
addSecureDeployment.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
|
||||
List<ModelNode> credentialsToAdd = new ArrayList<ModelNode>();
|
||||
List<ModelNode> redirectRulesToAdd = new ArrayList<ModelNode>();
|
||||
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
|
||||
String tagName = reader.getLocalName();
|
||||
if (tagName.equals(CredentialDefinition.TAG_NAME)) {
|
||||
readCredential(reader, addr, credentialsToAdd);
|
||||
continue;
|
||||
}
|
||||
if (tagName.equals(RedirecRewritetRuleDefinition.TAG_NAME)) {
|
||||
readRewriteRule(reader, addr, redirectRulesToAdd);
|
||||
continue;
|
||||
}
|
||||
|
||||
SimpleAttributeDefinition def = SecureDeploymentDefinition.lookup(tagName);
|
||||
if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + tagName);
|
||||
|
@ -111,6 +116,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
// Must add credentials after the deployment is added.
|
||||
resourcesToAdd.add(addSecureDeployment);
|
||||
resourcesToAdd.addAll(credentialsToAdd);
|
||||
resourcesToAdd.addAll(redirectRulesToAdd);
|
||||
}
|
||||
|
||||
public void readCredential(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> credentialsToAdd) throws XMLStreamException {
|
||||
|
@ -150,6 +156,43 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
}
|
||||
}
|
||||
|
||||
public void readRewriteRule(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> rewriteRuleToToAdd) throws XMLStreamException {
|
||||
String name = readNameAttribute(reader);
|
||||
|
||||
Map<String, String> values = new HashMap<>();
|
||||
String textValue = null;
|
||||
while (reader.hasNext()) {
|
||||
int next = reader.next();
|
||||
if (next == CHARACTERS) {
|
||||
// text value of redirect rule element
|
||||
String text = reader.getText();
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
textValue = text;
|
||||
} else if (next == START_ELEMENT) {
|
||||
String key = reader.getLocalName();
|
||||
reader.next();
|
||||
String value = reader.getText();
|
||||
reader.next();
|
||||
|
||||
values.put(key, value);
|
||||
} else if (next == END_ELEMENT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (textValue != null) {
|
||||
ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name, textValue);
|
||||
rewriteRuleToToAdd.add(addRedirectRule);
|
||||
} else {
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name + "." + entry.getKey(), entry.getValue());
|
||||
rewriteRuleToToAdd.add(addRedirectRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ModelNode getCredentialToAdd(PathAddress parent, String name, String value) {
|
||||
ModelNode addCredential = new ModelNode();
|
||||
addCredential.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD);
|
||||
|
@ -159,6 +202,15 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
return addCredential;
|
||||
}
|
||||
|
||||
private ModelNode getRedirectRuleToAdd(PathAddress parent, String name, String value) {
|
||||
ModelNode addRedirectRule = new ModelNode();
|
||||
addRedirectRule.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD);
|
||||
PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(RedirecRewritetRuleDefinition.TAG_NAME, name));
|
||||
addRedirectRule.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
|
||||
addRedirectRule.get(RedirecRewritetRuleDefinition.VALUE.getName()).set(value);
|
||||
return addRedirectRule;
|
||||
}
|
||||
|
||||
// expects that the current tag will have one single attribute called "name"
|
||||
private String readNameAttribute(XMLExtendedStreamReader reader) throws XMLStreamException {
|
||||
String name = null;
|
||||
|
@ -220,6 +272,11 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
writeCredentials(writer, credentials);
|
||||
}
|
||||
|
||||
ModelNode redirectRewriteRule = deploymentElements.get(RedirecRewritetRuleDefinition.TAG_NAME);
|
||||
if (redirectRewriteRule.isDefined()) {
|
||||
writeRedirectRules(writer, redirectRewriteRule);
|
||||
}
|
||||
|
||||
writer.writeEndElement();
|
||||
}
|
||||
}
|
||||
|
@ -266,6 +323,34 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
}
|
||||
}
|
||||
|
||||
private void writeRedirectRules(XMLExtendedStreamWriter writer, ModelNode redirectRules) throws XMLStreamException {
|
||||
Map<String, Object> parsed = new LinkedHashMap<>();
|
||||
for (Property redirectRule : redirectRules.asPropertyList()) {
|
||||
String ruleName = redirectRule.getName();
|
||||
String ruleValue = redirectRule.getValue().get(RedirecRewritetRuleDefinition.VALUE.getName()).asString();
|
||||
parsed.put(ruleName, ruleValue);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Object> entry : parsed.entrySet()) {
|
||||
writer.writeStartElement(RedirecRewritetRuleDefinition.TAG_NAME);
|
||||
writer.writeAttribute("name", entry.getKey());
|
||||
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof String) {
|
||||
writeCharacters(writer, (String) value);
|
||||
} else {
|
||||
Map<String, String> redirectRulesProps = (Map<String, String>) value;
|
||||
for (Map.Entry<String, String> prop : redirectRulesProps.entrySet()) {
|
||||
writer.writeStartElement(prop.getKey());
|
||||
writeCharacters(writer, prop.getValue());
|
||||
writer.writeEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
writer.writeEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
// code taken from org.jboss.as.controller.AttributeMarshaller
|
||||
private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException {
|
||||
if (content.indexOf('\n') > -1) {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.subsystem.adapter.extension;
|
||||
|
||||
import org.jboss.as.controller.AttributeDefinition;
|
||||
import org.jboss.as.controller.PathElement;
|
||||
import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
|
||||
import org.jboss.as.controller.SimpleResourceDefinition;
|
||||
import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
|
||||
import org.jboss.as.controller.operations.validation.StringLengthValidator;
|
||||
import org.jboss.as.controller.registry.ManagementResourceRegistration;
|
||||
import org.jboss.dmr.ModelType;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author sblanc
|
||||
*/
|
||||
public class RedirecRewritetRuleDefinition extends SimpleResourceDefinition {
|
||||
|
||||
public static final String TAG_NAME = "redirect-rewrite-rule";
|
||||
|
||||
protected static final AttributeDefinition VALUE =
|
||||
new SimpleAttributeDefinitionBuilder("value", ModelType.STRING, false)
|
||||
.setAllowExpression(true)
|
||||
.setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true))
|
||||
.build();
|
||||
|
||||
public RedirecRewritetRuleDefinition() {
|
||||
super(PathElement.pathElement(TAG_NAME),
|
||||
KeycloakExtension.getResourceDescriptionResolver(TAG_NAME),
|
||||
new RedirectRewriteRuleAddHandler(VALUE),
|
||||
RedirectRewriteRuleRemoveHandler.INSTANCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOperations(ManagementResourceRegistration resourceRegistration) {
|
||||
super.registerOperations(resourceRegistration);
|
||||
resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
|
||||
super.registerAttributes(resourceRegistration);
|
||||
resourceRegistration.registerReadWriteAttribute(VALUE, null, new RedirectRewriteRuleReadWriteAttributeHandler());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.subsystem.adapter.extension;
|
||||
|
||||
import org.jboss.as.controller.AbstractAddStepHandler;
|
||||
import org.jboss.as.controller.AttributeDefinition;
|
||||
import org.jboss.as.controller.OperationContext;
|
||||
import org.jboss.as.controller.OperationFailedException;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
|
||||
public class RedirectRewriteRuleAddHandler extends AbstractAddStepHandler {
|
||||
|
||||
public RedirectRewriteRuleAddHandler(AttributeDefinition... attributes) {
|
||||
super(attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
|
||||
KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
|
||||
ckService.addRedirectRewriteRule(operation, context.resolveExpressions(model));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.subsystem.adapter.extension;
|
||||
|
||||
import org.jboss.as.controller.AbstractWriteAttributeHandler;
|
||||
import org.jboss.as.controller.OperationContext;
|
||||
import org.jboss.as.controller.OperationFailedException;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
|
||||
public class RedirectRewriteRuleReadWriteAttributeHandler extends AbstractWriteAttributeHandler<KeycloakAdapterConfigService> {
|
||||
|
||||
@Override
|
||||
protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName,
|
||||
ModelNode resolvedValue, ModelNode currentValue, AbstractWriteAttributeHandler.HandbackHolder<KeycloakAdapterConfigService> hh) throws OperationFailedException {
|
||||
|
||||
KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
|
||||
ckService.updateRedirectRewriteRule(operation, attributeName, resolvedValue);
|
||||
|
||||
hh.setHandback(ckService);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName,
|
||||
ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException {
|
||||
ckService.updateRedirectRewriteRule(operation, attributeName, valueToRestore);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.subsystem.adapter.extension;
|
||||
|
||||
import org.jboss.as.controller.AbstractRemoveStepHandler;
|
||||
import org.jboss.as.controller.OperationContext;
|
||||
import org.jboss.as.controller.OperationFailedException;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
|
||||
public class RedirectRewriteRuleRemoveHandler extends AbstractRemoveStepHandler {
|
||||
|
||||
public static RedirectRewriteRuleRemoveHandler INSTANCE = new RedirectRewriteRuleRemoveHandler();
|
||||
|
||||
private RedirectRewriteRuleRemoveHandler() {}
|
||||
|
||||
@Override
|
||||
protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
|
||||
KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
|
||||
ckService.removeRedirectRewriteRule(operation);
|
||||
}
|
||||
|
||||
}
|
|
@ -65,6 +65,7 @@ keycloak.secure-deployment.connection-pool-size=Connection pool size for the cli
|
|||
keycloak.secure-deployment.resource=Application name
|
||||
keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token
|
||||
keycloak.secure-deployment.credentials=Adapter credentials
|
||||
keycloak.secure-deployment.redirect-rewrite-rule=Apply a rewrite rule for the redirect URI
|
||||
keycloak.secure-deployment.bearer-only=Bearer Token Auth only
|
||||
keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication
|
||||
keycloak.secure-deployment.public-client=Public client
|
||||
|
@ -95,3 +96,8 @@ keycloak.credential=Credential
|
|||
keycloak.credential.value=Credential value
|
||||
keycloak.credential.add=Credential add
|
||||
keycloak.credential.remove=Credential remove
|
||||
|
||||
keycloak.redirect-rewrite-rule=redirect-rewrite-rule
|
||||
keycloak.redirect-rewrite-rule.value=redirect-rewrite-rule value
|
||||
keycloak.redirect-rewrite-rule.add=redirect-rewrite-rule add
|
||||
keycloak.redirect-rewrite-rule.remove=redirect-rewrite-rule remove
|
|
@ -101,6 +101,7 @@
|
|||
<xs:element name="ssl-required" type="xs:string" minOccurs="0" maxOccurs="1" />
|
||||
<xs:element name="realm-public-key" type="xs:string" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="credential" type="credential-type" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="redirect-rewrite-rule" type="redirect-rewrite-rule-type" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="auth-server-url-for-backend-requests" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="always-refresh-token" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="register-node-at-startup" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
|
||||
|
@ -127,4 +128,10 @@
|
|||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required" />
|
||||
</xs:complexType>
|
||||
<xs:complexType name="redirect-rewrite-rule-type" mixed="true">
|
||||
<xs:sequence maxOccurs="unbounded" minOccurs="0">
|
||||
<xs:any processContents="lax"></xs:any>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required" />
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
<auth-server-url>http://localhost:8080/auth</auth-server-url>
|
||||
<ssl-required>EXTERNAL</ssl-required>
|
||||
<credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential>
|
||||
<redirect-rewrite-rule name="^/wsmaster/api/(.*)$">api/$1/</redirect-rewrite-rule>
|
||||
</secure-deployment>
|
||||
<secure-deployment name="http-endpoint">
|
||||
<realm>master</realm>
|
||||
|
@ -66,5 +67,6 @@
|
|||
<credential name="jwt">
|
||||
<client-keystore-file>/tmp/keystore.jks</client-keystore-file>
|
||||
</credential>
|
||||
<redirect-rewrite-rule name="^/wsmaster/api/(.*)$">/api/$1/</redirect-rewrite-rule>
|
||||
</secure-deployment>
|
||||
</subsystem>
|
|
@ -34,6 +34,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
|
|||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.processing.core.util.NamespaceContext;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
@ -65,9 +66,7 @@ public class SamlDescriptorIDPKeysExtractor {
|
|||
MultivaluedHashMap<String, KeyInfo> res = new MultivaluedHashMap<>();
|
||||
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
DocumentBuilder builder = DocumentUtil.getDocumentBuilder();
|
||||
Document doc = builder.parse(stream);
|
||||
|
||||
XPathExpression expr = xpath.compile("/m:EntitiesDescriptor/m:EntityDescriptor/m:IDPSSODescriptor/m:KeyDescriptor");
|
||||
|
|
|
@ -35,13 +35,13 @@ import java.util.Set;
|
|||
public class Profile {
|
||||
|
||||
public enum Feature {
|
||||
AUTHORIZATION, IMPERSONATION, SCRIPTS
|
||||
AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER
|
||||
}
|
||||
|
||||
private enum ProfileValue {
|
||||
PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS),
|
||||
PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER),
|
||||
PREVIEW,
|
||||
COMMUNITY;
|
||||
COMMUNITY(Feature.DOCKER);
|
||||
|
||||
private List<Feature> disabled;
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.nio.ByteBuffer;
|
|||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -36,7 +37,7 @@ import java.util.regex.Pattern;
|
|||
*/
|
||||
public class Encode
|
||||
{
|
||||
private static final String UTF_8 = "UTF-8";
|
||||
private static final String UTF_8 = StandardCharsets.UTF_8.name();
|
||||
|
||||
private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter");
|
||||
|
||||
|
@ -84,9 +85,7 @@ public class Encode
|
|||
case '@':
|
||||
continue;
|
||||
}
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append((char) i);
|
||||
pathEncoding[i] = URLEncoder.encode(sb.toString());
|
||||
pathEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
|
||||
}
|
||||
pathEncoding[' '] = "%20";
|
||||
System.arraycopy(pathEncoding, 0, matrixParameterEncoding, 0, pathEncoding.length);
|
||||
|
@ -119,9 +118,7 @@ public class Encode
|
|||
queryNameValueEncoding[i] = "+";
|
||||
continue;
|
||||
}
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append((char) i);
|
||||
queryNameValueEncoding[i] = URLEncoder.encode(sb.toString());
|
||||
queryNameValueEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -159,9 +156,7 @@ public class Encode
|
|||
queryStringEncoding[i] = "%20";
|
||||
continue;
|
||||
}
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append((char) i);
|
||||
queryStringEncoding[i] = URLEncoder.encode(sb.toString());
|
||||
queryStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,7 +189,7 @@ public class Encode
|
|||
*/
|
||||
public static String encodeFragment(String value)
|
||||
{
|
||||
return encodeValue(value, queryNameValueEncoding);
|
||||
return encodeValue(value, queryStringEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -221,18 +216,19 @@ public class Encode
|
|||
public static String decodePath(String path)
|
||||
{
|
||||
Matcher matcher = encodedCharsMulti.matcher(path);
|
||||
StringBuffer buf = new StringBuffer();
|
||||
int start=0;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
|
||||
while (matcher.find())
|
||||
{
|
||||
builder.append(path, start, matcher.start());
|
||||
decoder.reset();
|
||||
String decoded = decodeBytes(matcher.group(1), decoder);
|
||||
decoded = decoded.replace("\\", "\\\\");
|
||||
decoded = decoded.replace("$", "\\$");
|
||||
matcher.appendReplacement(buf, decoded);
|
||||
builder.append(decoded);
|
||||
start = matcher.end();
|
||||
}
|
||||
matcher.appendTail(buf);
|
||||
return buf.toString();
|
||||
builder.append(path, start, path.length());
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String decodeBytes(String enc, CharsetDecoder decoder)
|
||||
|
@ -264,7 +260,7 @@ public class Encode
|
|||
public static String encodeNonCodes(String string)
|
||||
{
|
||||
Matcher matcher = nonCodes.matcher(string);
|
||||
StringBuffer buf = new StringBuffer();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
|
||||
// FYI: we do not use the no-arg matcher.find()
|
||||
|
@ -276,29 +272,32 @@ public class Encode
|
|||
while (matcher.find(idx))
|
||||
{
|
||||
int start = matcher.start();
|
||||
buf.append(string.substring(idx, start));
|
||||
buf.append("%25");
|
||||
builder.append(string.substring(idx, start));
|
||||
builder.append("%25");
|
||||
idx = start + 1;
|
||||
}
|
||||
buf.append(string.substring(idx));
|
||||
return buf.toString();
|
||||
builder.append(string.substring(idx));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static boolean savePathParams(String segment, StringBuffer newSegment, List<String> params)
|
||||
public static boolean savePathParams(String segment, StringBuilder newSegment, List<String> params)
|
||||
{
|
||||
boolean foundParam = false;
|
||||
// Regular expressions can have '{' and '}' characters. Replace them to do match
|
||||
segment = PathHelper.replaceEnclosedCurlyBraces(segment);
|
||||
Matcher matcher = PathHelper.URI_TEMPLATE_PATTERN.matcher(segment);
|
||||
int start = 0;
|
||||
while (matcher.find())
|
||||
{
|
||||
newSegment.append(segment, start, matcher.start());
|
||||
foundParam = true;
|
||||
String group = matcher.group();
|
||||
// Regular expressions can have '{' and '}' characters. Recover earlier replacement
|
||||
params.add(PathHelper.recoverEnclosedCurlyBraces(group));
|
||||
matcher.appendReplacement(newSegment, "_resteasy_uri_parameter");
|
||||
newSegment.append("_resteasy_uri_parameter");
|
||||
start = matcher.end();
|
||||
}
|
||||
matcher.appendTail(newSegment);
|
||||
newSegment.append(segment, start, segment.length());
|
||||
return foundParam;
|
||||
}
|
||||
|
||||
|
@ -309,11 +308,11 @@ public class Encode
|
|||
* @param encoding
|
||||
* @return
|
||||
*/
|
||||
private static String encodeValue(String segment, String[] encoding)
|
||||
public static String encodeValue(String segment, String[] encoding)
|
||||
{
|
||||
ArrayList<String> params = new ArrayList<String>();
|
||||
boolean foundParam = false;
|
||||
StringBuffer newSegment = new StringBuffer();
|
||||
StringBuilder newSegment = new StringBuilder();
|
||||
if (savePathParams(segment, newSegment, params))
|
||||
{
|
||||
foundParam = true;
|
||||
|
@ -411,21 +410,21 @@ public class Encode
|
|||
return encodeFromArray(nameOrValue, queryNameValueEncoding, true);
|
||||
}
|
||||
|
||||
private static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
|
||||
protected static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
|
||||
{
|
||||
StringBuffer result = new StringBuffer();
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < segment.length(); i++)
|
||||
{
|
||||
if (!encodePercent && segment.charAt(i) == '%')
|
||||
char currentChar = segment.charAt(i);
|
||||
if (!encodePercent && currentChar == '%')
|
||||
{
|
||||
result.append(segment.charAt(i));
|
||||
result.append(currentChar);
|
||||
continue;
|
||||
}
|
||||
int idx = segment.charAt(i);
|
||||
String encoding = encode(idx, encodingMap);
|
||||
String encoding = encode(currentChar, encodingMap);
|
||||
if (encoding == null)
|
||||
{
|
||||
result.append(segment.charAt(i));
|
||||
result.append(currentChar);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -461,20 +460,20 @@ public class Encode
|
|||
return encoded;
|
||||
}
|
||||
|
||||
private static String pathParamReplacement(String segment, List<String> params)
|
||||
public static String pathParamReplacement(String segment, List<String> params)
|
||||
{
|
||||
StringBuffer newSegment = new StringBuffer();
|
||||
StringBuilder newSegment = new StringBuilder();
|
||||
Matcher matcher = PARAM_REPLACEMENT.matcher(segment);
|
||||
int i = 0;
|
||||
int start = 0;
|
||||
while (matcher.find())
|
||||
{
|
||||
newSegment.append(segment, start, matcher.start());
|
||||
String replacement = params.get(i++);
|
||||
// double encode slashes, so that slashes stay where they are
|
||||
replacement = replacement.replace("\\", "\\\\");
|
||||
replacement = replacement.replace("$", "\\$");
|
||||
matcher.appendReplacement(newSegment, replacement);
|
||||
newSegment.append(replacement);
|
||||
start = matcher.end();
|
||||
}
|
||||
matcher.appendTail(newSegment);
|
||||
newSegment.append(segment, start, segment.length());
|
||||
segment = newSegment.toString();
|
||||
return segment;
|
||||
}
|
||||
|
@ -506,6 +505,38 @@ public class Encode
|
|||
return decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* decode an encoded map
|
||||
*
|
||||
* @param map
|
||||
* @param charset
|
||||
* @return
|
||||
*/
|
||||
public static MultivaluedHashMap<String, String> decode(MultivaluedHashMap<String, String> map, String charset)
|
||||
{
|
||||
if (charset == null)
|
||||
{
|
||||
charset = UTF_8;
|
||||
}
|
||||
MultivaluedHashMap<String, String> decoded = new MultivaluedHashMap<String, String>();
|
||||
for (Map.Entry<String, List<String>> entry : map.entrySet())
|
||||
{
|
||||
List<String> values = entry.getValue();
|
||||
for (String value : values)
|
||||
{
|
||||
try
|
||||
{
|
||||
decoded.add(URLDecoder.decode(entry.getKey(), charset), URLDecoder.decode(value, charset));
|
||||
}
|
||||
catch (UnsupportedEncodingException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
public static MultivaluedHashMap<String, String> encode(MultivaluedHashMap<String, String> map)
|
||||
{
|
||||
MultivaluedHashMap<String, String> decoded = new MultivaluedHashMap<String, String>();
|
||||
|
|
|
@ -614,7 +614,7 @@ public class KeycloakUriBuilder {
|
|||
if (value == null) throw new IllegalArgumentException("A passed in value was null");
|
||||
if (query == null) query = "";
|
||||
else query += "&";
|
||||
query += Encode.encodeQueryParam(name) + "=" + Encode.encodeQueryParam(value.toString());
|
||||
query += Encode.encodeQueryParamAsIs(name) + "=" + Encode.encodeQueryParamAsIs(value.toString());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -62,7 +62,8 @@ public class BaseAdapterConfig extends BaseRealmConfig {
|
|||
protected boolean publicClient;
|
||||
@JsonProperty("credentials")
|
||||
protected Map<String, Object> credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
@JsonProperty("redirect-rewrite-rules")
|
||||
protected Map<String, String> redirectRewriteRules;
|
||||
|
||||
public boolean isUseResourceRoleMappings() {
|
||||
return useResourceRoleMappings;
|
||||
|
@ -167,4 +168,14 @@ public class BaseAdapterConfig extends BaseRealmConfig {
|
|||
public void setPublicClient(boolean publicClient) {
|
||||
this.publicClient = publicClient;
|
||||
}
|
||||
|
||||
public Map<String, String> getRedirectRewriteRules() {
|
||||
return redirectRewriteRules;
|
||||
}
|
||||
|
||||
public void setRedirectRewriteRules(Map<String, String> redirectRewriteRules) {
|
||||
this.redirectRewriteRules = redirectRewriteRules;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
package org.keycloak.representations.docker;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* Per the docker auth v2 spec, access is defined like this:
|
||||
*
|
||||
* {
|
||||
* "type": "repository",
|
||||
* "name": "samalba/my-app",
|
||||
* "actions": [
|
||||
* "push",
|
||||
* "pull"
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
*/
|
||||
public class DockerAccess {
|
||||
|
||||
public static final int ACCESS_TYPE = 0;
|
||||
public static final int REPOSITORY_NAME = 1;
|
||||
public static final int PERMISSIONS = 2;
|
||||
public static final String DECODE_ENCODING = "UTF-8";
|
||||
|
||||
@JsonProperty("type")
|
||||
protected String type;
|
||||
@JsonProperty("name")
|
||||
protected String name;
|
||||
@JsonProperty("actions")
|
||||
protected List<String> actions;
|
||||
|
||||
public DockerAccess() {
|
||||
}
|
||||
|
||||
public DockerAccess(final String scopeParam) {
|
||||
if (scopeParam != null) {
|
||||
try {
|
||||
final String unencoded = URLDecoder.decode(scopeParam, DECODE_ENCODING);
|
||||
final String[] parts = unencoded.split(":");
|
||||
if (parts.length != 3) {
|
||||
throw new IllegalArgumentException(String.format("Expecting input string to have %d parts delineated by a ':' character. " +
|
||||
"Found %d parts: %s", 3, parts.length, unencoded));
|
||||
}
|
||||
|
||||
type = parts[ACCESS_TYPE];
|
||||
name = parts[REPOSITORY_NAME];
|
||||
if (parts[PERMISSIONS] != null) {
|
||||
actions = Arrays.asList(parts[PERMISSIONS].split(","));
|
||||
}
|
||||
} catch (final UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException("Error attempting to decode scope parameter using encoding: " + DECODE_ENCODING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public DockerAccess setType(final String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public DockerAccess setName(final String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getActions() {
|
||||
return actions;
|
||||
}
|
||||
|
||||
public DockerAccess setActions(final List<String> actions) {
|
||||
this.actions = actions;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DockerAccess)) return false;
|
||||
|
||||
final DockerAccess that = (DockerAccess) o;
|
||||
|
||||
if (type != null ? !type.equals(that.type) : that.type != null) return false;
|
||||
if (name != null ? !name.equals(that.name) : that.name != null) return false;
|
||||
return actions != null ? actions.equals(that.actions) : that.actions == null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = type != null ? type.hashCode() : 0;
|
||||
result = 31 * result + (name != null ? name.hashCode() : 0);
|
||||
result = 31 * result + (actions != null ? actions.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DockerAccess{" +
|
||||
"type='" + type + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", actions=" + actions +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package org.keycloak.representations.docker;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JSON Representation of a Docker Error in the following format:
|
||||
*
|
||||
*
|
||||
* {
|
||||
* "code": "UNAUTHORIZED",
|
||||
* "message": "access to the requested resource is not authorized",
|
||||
* "detail": [
|
||||
* {
|
||||
* "Type": "repository",
|
||||
* "Name": "samalba/my-app",
|
||||
* "Action": "pull"
|
||||
* },
|
||||
* {
|
||||
* "Type": "repository",
|
||||
* "Name": "samalba/my-app",
|
||||
* "Action": "push"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
public class DockerError {
|
||||
|
||||
|
||||
@JsonProperty("code")
|
||||
private final String errorCode;
|
||||
@JsonProperty("message")
|
||||
private final String message;
|
||||
@JsonProperty("detail")
|
||||
private final List<DockerAccess> dockerErrorDetails;
|
||||
|
||||
public DockerError(final String errorCode, final String message, final List<DockerAccess> dockerErrorDetails) {
|
||||
this.errorCode = errorCode;
|
||||
this.message = message;
|
||||
this.dockerErrorDetails = dockerErrorDetails;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public List<DockerAccess> getDockerErrorDetails() {
|
||||
return dockerErrorDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DockerError)) return false;
|
||||
|
||||
final DockerError that = (DockerError) o;
|
||||
|
||||
if (errorCode != that.errorCode) return false;
|
||||
if (message != null ? !message.equals(that.message) : that.message != null) return false;
|
||||
return dockerErrorDetails != null ? dockerErrorDetails.equals(that.dockerErrorDetails) : that.dockerErrorDetails == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = errorCode != null ? errorCode.hashCode() : 0;
|
||||
result = 31 * result + (message != null ? message.hashCode() : 0);
|
||||
result = 31 * result + (dockerErrorDetails != null ? dockerErrorDetails.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DockerError{" +
|
||||
"errorCode=" + errorCode +
|
||||
", message='" + message + '\'' +
|
||||
", dockerErrorDetails=" + dockerErrorDetails +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package org.keycloak.representations.docker;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DockerErrorResponseToken {
|
||||
|
||||
|
||||
@JsonProperty("errors")
|
||||
private final List<DockerError> errorList;
|
||||
|
||||
public DockerErrorResponseToken(final List<DockerError> errorList) {
|
||||
this.errorList = errorList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DockerErrorResponseToken)) return false;
|
||||
|
||||
final DockerErrorResponseToken that = (DockerErrorResponseToken) o;
|
||||
|
||||
return errorList != null ? errorList.equals(that.errorList) : that.errorList == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return errorList != null ? errorList.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DockerErrorResponseToken{" +
|
||||
"errorList=" + errorList +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package org.keycloak.representations.docker;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* * {
|
||||
* "iss": "auth.docker.com",
|
||||
* "sub": "jlhawn",
|
||||
* "aud": "registry.docker.com",
|
||||
* "exp": 1415387315,
|
||||
* "nbf": 1415387015,
|
||||
* "iat": 1415387015,
|
||||
* "jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws",
|
||||
* "access": [
|
||||
* {
|
||||
* "type": "repository",
|
||||
* "name": "samalba/my-app",
|
||||
* "actions": [
|
||||
* "push"
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
public class DockerResponseToken extends JsonWebToken {
|
||||
|
||||
@JsonProperty("access")
|
||||
protected List<DockerAccess> accessItems = new ArrayList<>();
|
||||
|
||||
public List<DockerAccess> getAccessItems() {
|
||||
return accessItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken id(final String id) {
|
||||
super.id(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken expiration(final int expiration) {
|
||||
super.expiration(expiration);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken notBefore(final int notBefore) {
|
||||
super.notBefore(notBefore);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken issuedNow() {
|
||||
super.issuedNow();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken issuedAt(final int issuedAt) {
|
||||
super.issuedAt(issuedAt);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken issuer(final String issuer) {
|
||||
super.issuer(issuer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken audience(final String... audience) {
|
||||
super.audience(audience);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken subject(final String subject) {
|
||||
super.subject(subject);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken type(final String type) {
|
||||
super.type(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DockerResponseToken issuedFor(final String issuedFor) {
|
||||
super.issuedFor(issuedFor);
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -155,4 +155,101 @@ public class CredentialRepresentation {
|
|||
public void setConfig(MultivaluedHashMap<String, String> config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((algorithm == null) ? 0 : algorithm.hashCode());
|
||||
result = prime * result + ((config == null) ? 0 : config.hashCode());
|
||||
result = prime * result + ((counter == null) ? 0 : counter.hashCode());
|
||||
result = prime * result + ((createdDate == null) ? 0 : createdDate.hashCode());
|
||||
result = prime * result + ((device == null) ? 0 : device.hashCode());
|
||||
result = prime * result + ((digits == null) ? 0 : digits.hashCode());
|
||||
result = prime * result + ((hashIterations == null) ? 0 : hashIterations.hashCode());
|
||||
result = prime * result + ((hashedSaltedValue == null) ? 0 : hashedSaltedValue.hashCode());
|
||||
result = prime * result + ((period == null) ? 0 : period.hashCode());
|
||||
result = prime * result + ((salt == null) ? 0 : salt.hashCode());
|
||||
result = prime * result + ((temporary == null) ? 0 : temporary.hashCode());
|
||||
result = prime * result + ((type == null) ? 0 : type.hashCode());
|
||||
result = prime * result + ((value == null) ? 0 : value.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
CredentialRepresentation other = (CredentialRepresentation) obj;
|
||||
if (algorithm == null) {
|
||||
if (other.algorithm != null)
|
||||
return false;
|
||||
} else if (!algorithm.equals(other.algorithm))
|
||||
return false;
|
||||
if (config == null) {
|
||||
if (other.config != null)
|
||||
return false;
|
||||
} else if (!config.equals(other.config))
|
||||
return false;
|
||||
if (counter == null) {
|
||||
if (other.counter != null)
|
||||
return false;
|
||||
} else if (!counter.equals(other.counter))
|
||||
return false;
|
||||
if (createdDate == null) {
|
||||
if (other.createdDate != null)
|
||||
return false;
|
||||
} else if (!createdDate.equals(other.createdDate))
|
||||
return false;
|
||||
if (device == null) {
|
||||
if (other.device != null)
|
||||
return false;
|
||||
} else if (!device.equals(other.device))
|
||||
return false;
|
||||
if (digits == null) {
|
||||
if (other.digits != null)
|
||||
return false;
|
||||
} else if (!digits.equals(other.digits))
|
||||
return false;
|
||||
if (hashIterations == null) {
|
||||
if (other.hashIterations != null)
|
||||
return false;
|
||||
} else if (!hashIterations.equals(other.hashIterations))
|
||||
return false;
|
||||
if (hashedSaltedValue == null) {
|
||||
if (other.hashedSaltedValue != null)
|
||||
return false;
|
||||
} else if (!hashedSaltedValue.equals(other.hashedSaltedValue))
|
||||
return false;
|
||||
if (period == null) {
|
||||
if (other.period != null)
|
||||
return false;
|
||||
} else if (!period.equals(other.period))
|
||||
return false;
|
||||
if (salt == null) {
|
||||
if (other.salt != null)
|
||||
return false;
|
||||
} else if (!salt.equals(other.salt))
|
||||
return false;
|
||||
if (temporary == null) {
|
||||
if (other.temporary != null)
|
||||
return false;
|
||||
} else if (!temporary.equals(other.temporary))
|
||||
return false;
|
||||
if (type == null) {
|
||||
if (other.type != null)
|
||||
return false;
|
||||
} else if (!type.equals(other.type))
|
||||
return false;
|
||||
if (value == null) {
|
||||
if (other.value != null)
|
||||
return false;
|
||||
} else if (!value.equals(other.value))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,6 +137,7 @@ public class RealmRepresentation {
|
|||
protected String directGrantFlow;
|
||||
protected String resetCredentialsFlow;
|
||||
protected String clientAuthenticationFlow;
|
||||
protected String dockerAuthenticationFlow;
|
||||
|
||||
protected Map<String, String> attributes;
|
||||
|
||||
|
@ -884,6 +885,15 @@ public class RealmRepresentation {
|
|||
this.clientAuthenticationFlow = clientAuthenticationFlow;
|
||||
}
|
||||
|
||||
public String getDockerAuthenticationFlow() {
|
||||
return dockerAuthenticationFlow;
|
||||
}
|
||||
|
||||
public RealmRepresentation setDockerAuthenticationFlow(final String dockerAuthenticationFlow) {
|
||||
this.dockerAuthenticationFlow = dockerAuthenticationFlow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getKeycloakVersion() {
|
||||
return keycloakVersion;
|
||||
}
|
||||
|
|
|
@ -21,8 +21,18 @@ import java.util.Map;
|
|||
|
||||
public class ProviderRepresentation {
|
||||
|
||||
private int order;
|
||||
|
||||
private Map<String, String> operationalInfo;
|
||||
|
||||
public int getOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public void setOrder(int priorityUI) {
|
||||
this.order = priorityUI;
|
||||
}
|
||||
|
||||
public Map<String, String> getOperationalInfo() {
|
||||
return operationalInfo;
|
||||
}
|
||||
|
|
|
@ -62,13 +62,6 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
|
@ -96,4 +89,27 @@
|
|||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>community</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>!product</name>
|
||||
</property>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -34,11 +34,23 @@
|
|||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-feature-pack</artifactId>
|
||||
<type>zip</type>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-client-cli-dist</artifactId>
|
||||
<type>zip</type>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import javax.ws.rs.Produces;
|
|||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -184,6 +185,12 @@ public interface RealmResource {
|
|||
@QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential,
|
||||
@QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("connectionTimeout") String connectionTimeout);
|
||||
|
||||
@Path("testSMTPConnection/{config}")
|
||||
@POST
|
||||
@NoCache
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Response testSMTPConnection(final @PathParam("config") String config) throws Exception;
|
||||
|
||||
@Path("clear-realm-cache")
|
||||
@POST
|
||||
void clearRealmCache();
|
||||
|
|
|
@ -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<AuthenticationFlowModel> getAuthenticationFlows() {
|
||||
if (isUpdated()) return updated.getAuthenticationFlows();
|
||||
|
|
|
@ -117,6 +117,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
protected AuthenticationFlowModel directGrantFlow;
|
||||
protected AuthenticationFlowModel resetCredentialsFlow;
|
||||
protected AuthenticationFlowModel clientAuthenticationFlow;
|
||||
protected AuthenticationFlowModel dockerAuthenticationFlow;
|
||||
|
||||
protected boolean eventsEnabled;
|
||||
protected long eventsExpiration;
|
||||
|
@ -252,6 +253,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
directGrantFlow = model.getDirectGrantFlow();
|
||||
resetCredentialsFlow = model.getResetCredentialsFlow();
|
||||
clientAuthenticationFlow = model.getClientAuthenticationFlow();
|
||||
dockerAuthenticationFlow = model.getDockerAuthenticationFlow();
|
||||
|
||||
for (ComponentModel component : model.getComponents()) {
|
||||
componentsByParentAndType.add(component.getParentId() + component.getProviderType(), component);
|
||||
|
@ -547,6 +549,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
return clientAuthenticationFlow;
|
||||
}
|
||||
|
||||
public AuthenticationFlowModel getDockerAuthenticationFlow() {
|
||||
return dockerAuthenticationFlow;
|
||||
}
|
||||
|
||||
public List<String> getDefaultGroups() {
|
||||
return defaultGroups;
|
||||
}
|
||||
|
|
|
@ -1375,6 +1375,18 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
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<AuthenticationFlowModel> getAuthenticationFlows() {
|
||||
return realm.getAuthenticationFlows().stream()
|
||||
|
|
|
@ -220,6 +220,8 @@ public class RealmEntity {
|
|||
@Column(name="CLIENT_AUTH_FLOW")
|
||||
protected String clientAuthenticationFlow;
|
||||
|
||||
@Column(name="DOCKER_AUTH_FLOW")
|
||||
protected String dockerAuthenticationFlow;
|
||||
|
||||
|
||||
@Column(name="INTERNATIONALIZATION_ENABLED")
|
||||
|
@ -733,6 +735,15 @@ public class RealmEntity {
|
|||
this.clientAuthenticationFlow = clientAuthenticationFlow;
|
||||
}
|
||||
|
||||
public String getDockerAuthenticationFlow() {
|
||||
return dockerAuthenticationFlow;
|
||||
}
|
||||
|
||||
public RealmEntity setDockerAuthenticationFlow(String dockerAuthenticationFlow) {
|
||||
this.dockerAuthenticationFlow = dockerAuthenticationFlow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Collection<ClientTemplateEntity> getClientTemplates() {
|
||||
return clientTemplates;
|
||||
}
|
||||
|
|
|
@ -15,10 +15,14 @@
|
|||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="keycloak" id="3.2.0">
|
||||
<addColumn tableName="REALM">
|
||||
<column name="DOCKER_AUTH_FLOW" type="VARCHAR(36)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
|
||||
<changeSet author="mposolda@redhat.com" id="3.2.0">
|
||||
<dropPrimaryKey constraintName="CONSTRAINT_OFFL_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION" />
|
||||
<dropColumn tableName="OFFLINE_CLIENT_SESSION" columnName="CLIENT_SESSION_ID" />
|
||||
<addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
|
||||
|
@ -38,9 +42,6 @@
|
|||
<addPrimaryKey columnNames="ID" constraintName="CNSTR_CLIENT_INIT_ACC_PK" tableName="CLIENT_INITIAL_ACCESS"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT_INITIAL_ACCESS" constraintName="FK_CLIENT_INIT_ACC_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="glavoie@gmail.com" id="3.2.0.idx">
|
||||
<createIndex indexName="IDX_ASSOC_POL_ASSOC_POL_ID" tableName="ASSOCIATED_POLICY">
|
||||
<column name="ASSOCIATED_POLICY_ID" type="VARCHAR(36)"/>
|
||||
</createIndex>
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -45,7 +45,7 @@
|
|||
<jboss.as.version>7.2.0.Final</jboss.as.version>
|
||||
<wildfly.version>11.0.0.Alpha1</wildfly.version>
|
||||
<wildfly.build-tools.version>1.2.2.Final</wildfly.build-tools.version>
|
||||
<eap.version>7.1.0.Beta1-redhat-2</eap.version>
|
||||
<eap.version>7.1.0.Beta1-redhat-5</eap.version>
|
||||
<eap.build-tools.version>1.2.2.Final</eap.build-tools.version>
|
||||
<wildfly.core.version>3.0.0.Beta11</wildfly.core.version>
|
||||
|
||||
|
|
|
@ -345,7 +345,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
|||
logger.debugv("saml document: {0}", documentAsString);
|
||||
byte[] responseBytes = documentAsString.getBytes(GeneralConstants.SAML_CHARSET);
|
||||
|
||||
return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
|
||||
return RedirectBindingUtil.deflateBase64Encode(responseBytes);
|
||||
}
|
||||
|
||||
|
||||
|
@ -370,7 +370,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
|||
} catch (InvalidKeyException | SignatureException e) {
|
||||
throw new ProcessingException(e);
|
||||
}
|
||||
String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
|
||||
String encodedSig = RedirectBindingUtil.base64Encode(sig);
|
||||
builder.queryParam(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY, encodedSig);
|
||||
}
|
||||
return builder.build();
|
||||
|
|
|
@ -511,7 +511,7 @@ public class DocumentUtil {
|
|||
|
||||
};
|
||||
|
||||
private static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
|
||||
public static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
|
||||
DocumentBuilder res = XML_DOCUMENT_BUILDER.get();
|
||||
res.reset();
|
||||
return res;
|
||||
|
|
|
@ -60,6 +60,19 @@ public class RedirectBindingUtil {
|
|||
return URLDecoder.decode(str, GeneralConstants.SAML_CHARSET_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* On the byte array, apply base64 encoding
|
||||
*
|
||||
* @param stringToEncode
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public static String base64Encode(byte[] stringToEncode) throws IOException {
|
||||
return Base64.encodeBytes(stringToEncode, Base64.DONT_BREAK_LINES);
|
||||
}
|
||||
|
||||
/**
|
||||
* On the byte array, apply base64 encoding following by URL encoding
|
||||
*
|
||||
|
|
|
@ -17,15 +17,15 @@
|
|||
|
||||
package org.keycloak.email;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface EmailSenderProvider extends Provider {
|
||||
|
||||
void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
|
||||
|
||||
void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
|
@ -46,6 +48,15 @@ public interface EmailTemplateProvider extends Provider {
|
|||
*/
|
||||
public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
|
||||
|
||||
/**
|
||||
* Test SMTP connection with current logged in user
|
||||
*
|
||||
* @param config SMTP server configuration
|
||||
* @param user SMTP recipient
|
||||
* @throws EmailException
|
||||
*/
|
||||
public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException;
|
||||
|
||||
/**
|
||||
* Send to confirm that user wants to link his account with identity broker link
|
||||
*/
|
||||
|
|
|
@ -27,11 +27,8 @@ import org.keycloak.migration.ModelVersion;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class MigrateTo3_2_0 implements Migration {
|
||||
|
||||
public static final ModelVersion VERSION = new ModelVersion("3.2.0");
|
||||
|
@ -44,6 +41,10 @@ public class MigrateTo3_2_0 implements Migration {
|
|||
realm.setPasswordPolicy(builder.remove(PasswordPolicy.HASH_ITERATIONS_ID).build(session));
|
||||
}
|
||||
|
||||
if (realm.getDockerAuthenticationFlow() == null) {
|
||||
DefaultAuthenticationFlows.dockerAuthenticationFlow(realm);
|
||||
}
|
||||
|
||||
ClientModel realmAccess = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
|
||||
if (realmAccess != null) {
|
||||
addRoles(realmAccess);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.keycloak.authorization.model.ResourceServer;
|
|||
import org.keycloak.authorization.model.Scope;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
||||
import org.keycloak.authorization.store.ResourceStore;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
@ -325,6 +326,7 @@ public class ModelToRepresentation {
|
|||
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
|
||||
if (realm.getResetCredentialsFlow() != null) rep.setResetCredentialsFlow(realm.getResetCredentialsFlow().getAlias());
|
||||
if (realm.getClientAuthenticationFlow() != null) rep.setClientAuthenticationFlow(realm.getClientAuthenticationFlow().getAlias());
|
||||
if (realm.getDockerAuthenticationFlow() != null) rep.setDockerAuthenticationFlow(realm.getDockerAuthenticationFlow().getAlias());
|
||||
|
||||
List<String> defaultRoles = realm.getDefaultRoles();
|
||||
if (!defaultRoles.isEmpty()) {
|
||||
|
|
|
@ -614,6 +614,18 @@ public class RepresentationToModel {
|
|||
}
|
||||
}
|
||||
|
||||
// Added in 3.2
|
||||
if (rep.getDockerAuthenticationFlow() == null) {
|
||||
AuthenticationFlowModel dockerAuthenticationFlow = newRealm.getFlowByAlias(DefaultAuthenticationFlows.DOCKER_AUTH);
|
||||
if (dockerAuthenticationFlow == null) {
|
||||
DefaultAuthenticationFlows.dockerAuthenticationFlow(newRealm);
|
||||
} else {
|
||||
newRealm.setDockerAuthenticationFlow(dockerAuthenticationFlow);
|
||||
}
|
||||
} else {
|
||||
newRealm.setDockerAuthenticationFlow(newRealm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
|
||||
}
|
||||
|
||||
DefaultAuthenticationFlows.addIdentityProviderAuthenticator(newRealm, defaultProvider);
|
||||
}
|
||||
|
||||
|
@ -898,6 +910,9 @@ public class RepresentationToModel {
|
|||
if (rep.getClientAuthenticationFlow() != null) {
|
||||
realm.setClientAuthenticationFlow(realm.getFlowByAlias(rep.getClientAuthenticationFlow()));
|
||||
}
|
||||
if (rep.getDockerAuthenticationFlow() != null) {
|
||||
realm.setDockerAuthenticationFlow(realm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
|
||||
}
|
||||
}
|
||||
|
||||
// Basic realm stuff
|
||||
|
@ -1201,6 +1216,7 @@ public class RepresentationToModel {
|
|||
if (rep.isUseTemplateScope() != null) resource.setUseTemplateScope(rep.isUseTemplateScope());
|
||||
if (rep.isUseTemplateMappers() != null) resource.setUseTemplateMappers(rep.isUseTemplateMappers());
|
||||
|
||||
if (rep.getSecret() != null) resource.setSecret(rep.getSecret());
|
||||
|
||||
if (rep.getClientTemplate() != null) {
|
||||
if (rep.getClientTemplate().equals(ClientTemplateRepresentation.NONE)) {
|
||||
|
|
|
@ -251,6 +251,9 @@ public interface RealmModel extends RoleContainerModel {
|
|||
AuthenticationFlowModel getClientAuthenticationFlow();
|
||||
void setClientAuthenticationFlow(AuthenticationFlowModel flow);
|
||||
|
||||
AuthenticationFlowModel getDockerAuthenticationFlow();
|
||||
void setDockerAuthenticationFlow(AuthenticationFlowModel flow);
|
||||
|
||||
List<AuthenticationFlowModel> getAuthenticationFlows();
|
||||
AuthenticationFlowModel getFlowByAlias(String alias);
|
||||
AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model);
|
||||
|
|
|
@ -53,4 +53,8 @@ public interface ProviderFactory<T extends Provider> {
|
|||
|
||||
public String getId();
|
||||
|
||||
default int order() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
|||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
|
||||
|
@ -88,10 +87,13 @@ import java.util.List;
|
|||
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||
import org.w3c.dom.Document;
|
||||
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.util.*;
|
||||
import javax.xml.crypto.dsig.XMLSignature;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -517,6 +519,17 @@ public class SAMLEndpoint {
|
|||
protected class PostBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
NodeList nl = documentHolder.getSamlDocument().getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
||||
boolean anyElementSigned = (nl != null && nl.getLength() > 0);
|
||||
if ((! anyElementSigned) && (documentHolder.getSamlObject() instanceof ResponseType)) {
|
||||
ResponseType responseType = (ResponseType) documentHolder.getSamlObject();
|
||||
List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
|
||||
if (! assertions.isEmpty() ) {
|
||||
// Only relax verification if the response is an authnresponse and contains (encrypted/plaintext) assertion.
|
||||
// In that case, signature is validated on assertion element
|
||||
return;
|
||||
}
|
||||
}
|
||||
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKeyLocator());
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,24 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
|||
|
||||
public static final XmlKeyInfoKeyNameTransformer DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER = XmlKeyInfoKeyNameTransformer.NONE;
|
||||
|
||||
public static final String ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "addExtensionsElementWithKeyInfo";
|
||||
public static final String BACKCHANNEL_SUPPORTED = "backchannelSupported";
|
||||
public static final String ENCRYPTION_PUBLIC_KEY = "encryptionPublicKey";
|
||||
public static final String FORCE_AUTHN = "forceAuthn";
|
||||
public static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
|
||||
public static final String POST_BINDING_AUTHN_REQUEST = "postBindingAuthnRequest";
|
||||
public static final String POST_BINDING_LOGOUT = "postBindingLogout";
|
||||
public static final String POST_BINDING_RESPONSE = "postBindingResponse";
|
||||
public static final String SIGNATURE_ALGORITHM = "signatureAlgorithm";
|
||||
public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
|
||||
public static final String SINGLE_LOGOUT_SERVICE_URL = "singleLogoutServiceUrl";
|
||||
public static final String SINGLE_SIGN_ON_SERVICE_URL = "singleSignOnServiceUrl";
|
||||
public static final String VALIDATE_SIGNATURE = "validateSignature";
|
||||
public static final String WANT_ASSERTIONS_ENCRYPTED = "wantAssertionsEncrypted";
|
||||
public static final String WANT_ASSERTIONS_SIGNED = "wantAssertionsSigned";
|
||||
public static final String WANT_AUTHN_REQUESTS_SIGNED = "wantAuthnRequestsSigned";
|
||||
public static final String XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER = "xmlSigKeyInfoKeyNameTransformer";
|
||||
|
||||
public SAMLIdentityProviderConfig() {
|
||||
}
|
||||
|
||||
|
@ -35,35 +53,35 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
|||
}
|
||||
|
||||
public String getSingleSignOnServiceUrl() {
|
||||
return getConfig().get("singleSignOnServiceUrl");
|
||||
return getConfig().get(SINGLE_SIGN_ON_SERVICE_URL);
|
||||
}
|
||||
|
||||
public void setSingleSignOnServiceUrl(String singleSignOnServiceUrl) {
|
||||
getConfig().put("singleSignOnServiceUrl", singleSignOnServiceUrl);
|
||||
getConfig().put(SINGLE_SIGN_ON_SERVICE_URL, singleSignOnServiceUrl);
|
||||
}
|
||||
|
||||
public String getSingleLogoutServiceUrl() {
|
||||
return getConfig().get("singleLogoutServiceUrl");
|
||||
return getConfig().get(SINGLE_LOGOUT_SERVICE_URL);
|
||||
}
|
||||
|
||||
public void setSingleLogoutServiceUrl(String singleLogoutServiceUrl) {
|
||||
getConfig().put("singleLogoutServiceUrl", singleLogoutServiceUrl);
|
||||
getConfig().put(SINGLE_LOGOUT_SERVICE_URL, singleLogoutServiceUrl);
|
||||
}
|
||||
|
||||
public boolean isValidateSignature() {
|
||||
return Boolean.valueOf(getConfig().get("validateSignature"));
|
||||
return Boolean.valueOf(getConfig().get(VALIDATE_SIGNATURE));
|
||||
}
|
||||
|
||||
public void setValidateSignature(boolean validateSignature) {
|
||||
getConfig().put("validateSignature", String.valueOf(validateSignature));
|
||||
getConfig().put(VALIDATE_SIGNATURE, String.valueOf(validateSignature));
|
||||
}
|
||||
|
||||
public boolean isForceAuthn() {
|
||||
return Boolean.valueOf(getConfig().get("forceAuthn"));
|
||||
return Boolean.valueOf(getConfig().get(FORCE_AUTHN));
|
||||
}
|
||||
|
||||
public void setForceAuthn(boolean forceAuthn) {
|
||||
getConfig().put("forceAuthn", String.valueOf(forceAuthn));
|
||||
getConfig().put(FORCE_AUTHN, String.valueOf(forceAuthn));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,82 +121,80 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
|||
return crt.split(",");
|
||||
}
|
||||
|
||||
public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
|
||||
|
||||
public String getNameIDPolicyFormat() {
|
||||
return getConfig().get("nameIDPolicyFormat");
|
||||
return getConfig().get(NAME_ID_POLICY_FORMAT);
|
||||
}
|
||||
|
||||
public void setNameIDPolicyFormat(String nameIDPolicyFormat) {
|
||||
getConfig().put("nameIDPolicyFormat", nameIDPolicyFormat);
|
||||
getConfig().put(NAME_ID_POLICY_FORMAT, nameIDPolicyFormat);
|
||||
}
|
||||
|
||||
public boolean isWantAuthnRequestsSigned() {
|
||||
return Boolean.valueOf(getConfig().get("wantAuthnRequestsSigned"));
|
||||
return Boolean.valueOf(getConfig().get(WANT_AUTHN_REQUESTS_SIGNED));
|
||||
}
|
||||
|
||||
public void setWantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) {
|
||||
getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
|
||||
getConfig().put(WANT_AUTHN_REQUESTS_SIGNED, String.valueOf(wantAuthnRequestsSigned));
|
||||
}
|
||||
|
||||
public boolean isWantAssertionsSigned() {
|
||||
return Boolean.valueOf(getConfig().get("wantAssertionsSigned"));
|
||||
return Boolean.valueOf(getConfig().get(WANT_ASSERTIONS_SIGNED));
|
||||
}
|
||||
|
||||
public void setWantAssertionsSigned(boolean wantAssertionsSigned) {
|
||||
getConfig().put("wantAssertionsSigned", String.valueOf(wantAssertionsSigned));
|
||||
getConfig().put(WANT_ASSERTIONS_SIGNED, String.valueOf(wantAssertionsSigned));
|
||||
}
|
||||
|
||||
public boolean isWantAssertionsEncrypted() {
|
||||
return Boolean.valueOf(getConfig().get("wantAssertionsEncrypted"));
|
||||
return Boolean.valueOf(getConfig().get(WANT_ASSERTIONS_ENCRYPTED));
|
||||
}
|
||||
|
||||
public void setWantAssertionsEncrypted(boolean wantAssertionsEncrypted) {
|
||||
getConfig().put("wantAssertionsEncrypted", String.valueOf(wantAssertionsEncrypted));
|
||||
getConfig().put(WANT_ASSERTIONS_ENCRYPTED, String.valueOf(wantAssertionsEncrypted));
|
||||
}
|
||||
|
||||
public boolean isAddExtensionsElementWithKeyInfo() {
|
||||
return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo"));
|
||||
return Boolean.valueOf(getConfig().get(ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
|
||||
}
|
||||
|
||||
public void setAddExtensionsElementWithKeyInfo(boolean addExtensionsElementWithKeyInfo) {
|
||||
getConfig().put("addExtensionsElementWithKeyInfo", String.valueOf(addExtensionsElementWithKeyInfo));
|
||||
getConfig().put(ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, String.valueOf(addExtensionsElementWithKeyInfo));
|
||||
}
|
||||
|
||||
public String getSignatureAlgorithm() {
|
||||
return getConfig().get("signatureAlgorithm");
|
||||
return getConfig().get(SIGNATURE_ALGORITHM);
|
||||
}
|
||||
|
||||
public void setSignatureAlgorithm(String signatureAlgorithm) {
|
||||
getConfig().put("signatureAlgorithm", signatureAlgorithm);
|
||||
getConfig().put(SIGNATURE_ALGORITHM, signatureAlgorithm);
|
||||
}
|
||||
|
||||
public String getEncryptionPublicKey() {
|
||||
return getConfig().get("encryptionPublicKey");
|
||||
return getConfig().get(ENCRYPTION_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
public void setEncryptionPublicKey(String encryptionPublicKey) {
|
||||
getConfig().put("encryptionPublicKey", encryptionPublicKey);
|
||||
getConfig().put(ENCRYPTION_PUBLIC_KEY, encryptionPublicKey);
|
||||
}
|
||||
|
||||
public boolean isPostBindingAuthnRequest() {
|
||||
return Boolean.valueOf(getConfig().get("postBindingAuthnRequest"));
|
||||
return Boolean.valueOf(getConfig().get(POST_BINDING_AUTHN_REQUEST));
|
||||
}
|
||||
|
||||
public void setPostBindingAuthnRequest(boolean postBindingAuthnRequest) {
|
||||
getConfig().put("postBindingAuthnRequest", String.valueOf(postBindingAuthnRequest));
|
||||
getConfig().put(POST_BINDING_AUTHN_REQUEST, String.valueOf(postBindingAuthnRequest));
|
||||
}
|
||||
|
||||
public boolean isPostBindingResponse() {
|
||||
return Boolean.valueOf(getConfig().get("postBindingResponse"));
|
||||
return Boolean.valueOf(getConfig().get(POST_BINDING_RESPONSE));
|
||||
}
|
||||
|
||||
public void setPostBindingResponse(boolean postBindingResponse) {
|
||||
getConfig().put("postBindingResponse", String.valueOf(postBindingResponse));
|
||||
getConfig().put(POST_BINDING_RESPONSE, String.valueOf(postBindingResponse));
|
||||
}
|
||||
|
||||
public boolean isPostBindingLogout() {
|
||||
String postBindingLogout = getConfig().get("postBindingLogout");
|
||||
String postBindingLogout = getConfig().get(POST_BINDING_LOGOUT);
|
||||
if (postBindingLogout == null) {
|
||||
// To maintain unchanged behavior when adding this field, we set the inital value to equal that
|
||||
// of the binding for the response:
|
||||
|
@ -188,15 +204,15 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
|||
}
|
||||
|
||||
public void setPostBindingLogout(boolean postBindingLogout) {
|
||||
getConfig().put("postBindingLogout", String.valueOf(postBindingLogout));
|
||||
getConfig().put(POST_BINDING_LOGOUT, String.valueOf(postBindingLogout));
|
||||
}
|
||||
|
||||
public boolean isBackchannelSupported() {
|
||||
return Boolean.valueOf(getConfig().get("backchannelSupported"));
|
||||
return Boolean.valueOf(getConfig().get(BACKCHANNEL_SUPPORTED));
|
||||
}
|
||||
|
||||
public void setBackchannelSupported(boolean backchannel) {
|
||||
getConfig().put("backchannelSupported", String.valueOf(backchannel));
|
||||
getConfig().put(BACKCHANNEL_SUPPORTED, String.valueOf(backchannel));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,11 +220,11 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
|||
* @return Configured ransformer of {@link #DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER} if not set.
|
||||
*/
|
||||
public XmlKeyInfoKeyNameTransformer getXmlSigKeyInfoKeyNameTransformer() {
|
||||
return XmlKeyInfoKeyNameTransformer.from(getConfig().get("xmlSigKeyInfoKeyNameTransformer"), DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
|
||||
return XmlKeyInfoKeyNameTransformer.from(getConfig().get(XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER), DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
|
||||
}
|
||||
|
||||
public void setXmlSigKeyInfoKeyNameTransformer(XmlKeyInfoKeyNameTransformer xmlSigKeyInfoKeyNameTransformer) {
|
||||
getConfig().put("xmlSigKeyInfoKeyNameTransformer",
|
||||
getConfig().put(XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER,
|
||||
xmlSigKeyInfoKeyNameTransformer == null
|
||||
? null
|
||||
: xmlSigKeyInfoKeyNameTransformer.name());
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.keycloak.email;
|
|||
import com.sun.mail.smtp.SMTPMessage;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.truststore.HostnameVerificationPolicy;
|
||||
|
@ -57,20 +56,22 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
|
||||
public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
|
||||
Transport transport = null;
|
||||
try {
|
||||
String address = retrieveEmailAddress(user);
|
||||
Map<String, String> config = realm.getSmtpConfig();
|
||||
|
||||
Properties props = new Properties();
|
||||
|
||||
if (config.containsKey("host")) {
|
||||
props.setProperty("mail.smtp.host", config.get("host"));
|
||||
}
|
||||
|
||||
boolean auth = "true".equals(config.get("auth"));
|
||||
boolean ssl = "true".equals(config.get("ssl"));
|
||||
boolean starttls = "true".equals(config.get("starttls"));
|
||||
|
||||
if (config.containsKey("port")) {
|
||||
if (config.containsKey("port") && config.get("port") != null) {
|
||||
props.setProperty("mail.smtp.port", config.get("port"));
|
||||
}
|
||||
|
||||
|
@ -153,7 +154,10 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
|
|||
}
|
||||
}
|
||||
|
||||
protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException {
|
||||
protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, EmailException {
|
||||
if (email == null || "".equals(email.trim())) {
|
||||
throw new EmailException("Please provide a valid address", null);
|
||||
}
|
||||
if (displayName == null || "".equals(displayName.trim())) {
|
||||
return new InternetAddress(email);
|
||||
}
|
||||
|
|
|
@ -107,6 +107,19 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
|
|||
send("passwordResetSubject", "password-reset.ftl", attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException {
|
||||
setRealm(session.getContext().getRealm());
|
||||
setUser(user);
|
||||
|
||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||
attributes.put("user", new ProfileBean(user));
|
||||
attributes.put("realmName", realm.getName());
|
||||
|
||||
EmailTemplate email = processTemplate("emailTestSubject", Collections.emptyList(), "email-test.ftl", attributes);
|
||||
send(config, email.getSubject(), email.getTextBody(), email.getHtmlBody());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException {
|
||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||
|
@ -156,7 +169,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
|
|||
send(subjectKey, Collections.emptyList(), template, attributes);
|
||||
}
|
||||
|
||||
private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
|
||||
private EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
|
||||
try {
|
||||
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
|
||||
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
|
||||
|
@ -180,15 +193,27 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
|
|||
htmlBody = null;
|
||||
}
|
||||
|
||||
send(subject, textBody, htmlBody);
|
||||
return new EmailTemplate(subject, textBody, htmlBody);
|
||||
} catch (Exception e) {
|
||||
throw new EmailException("Failed to template email", e);
|
||||
}
|
||||
}
|
||||
private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
|
||||
try {
|
||||
EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
|
||||
send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
|
||||
} catch (Exception e) {
|
||||
throw new EmailException("Failed to template email", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void send(String subject, String textBody, String htmlBody) throws EmailException {
|
||||
send(realm.getSmtpConfig(), subject, textBody, htmlBody);
|
||||
}
|
||||
|
||||
private void send(Map<String, String> config, String subject, String textBody, String htmlBody) throws EmailException {
|
||||
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
|
||||
emailSender.send(realm, user, subject, textBody, htmlBody);
|
||||
emailSender.send(config, user, subject, textBody, htmlBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -203,4 +228,29 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
private class EmailTemplate {
|
||||
|
||||
private String subject;
|
||||
private String textBody;
|
||||
private String htmlBody;
|
||||
|
||||
public EmailTemplate(String subject, String textBody, String htmlBody) {
|
||||
this.subject = subject;
|
||||
this.textBody = textBody;
|
||||
this.htmlBody = htmlBody;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getTextBody() {
|
||||
return textBody;
|
||||
}
|
||||
|
||||
public String getHtmlBody() {
|
||||
return htmlBody;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
package org.keycloak.protocol.docker;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.specimpl.ResponseBuilderImpl;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeyManager;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper;
|
||||
import org.keycloak.representations.docker.DockerResponse;
|
||||
import org.keycloak.representations.docker.DockerResponseToken;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
|
||||
public class DockerAuthV2Protocol implements LoginProtocol {
|
||||
protected static final Logger logger = Logger.getLogger(DockerEndpoint.class);
|
||||
|
||||
public static final String LOGIN_PROTOCOL = "docker-v2";
|
||||
public static final String ACCOUNT_PARAM = "account";
|
||||
public static final String SERVICE_PARAM = "service";
|
||||
public static final String SCOPE_PARAM = "scope";
|
||||
public static final String ISSUER = "docker.iss"; // don't want to overlap with OIDC notes
|
||||
public static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
||||
|
||||
private KeycloakSession session;
|
||||
private RealmModel realm;
|
||||
private UriInfo uriInfo;
|
||||
private HttpHeaders headers;
|
||||
private EventBuilder event;
|
||||
|
||||
public DockerAuthV2Protocol() {
|
||||
}
|
||||
|
||||
public DockerAuthV2Protocol(final KeycloakSession session, final RealmModel realm, final UriInfo uriInfo, final HttpHeaders headers, final EventBuilder event) {
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
this.uriInfo = uriInfo;
|
||||
this.headers = headers;
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginProtocol setSession(final KeycloakSession session) {
|
||||
this.session = session;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginProtocol setRealm(final RealmModel realm) {
|
||||
this.realm = realm;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginProtocol setUriInfo(final UriInfo uriInfo) {
|
||||
this.uriInfo = uriInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginProtocol setHttpHeaders(final HttpHeaders headers) {
|
||||
this.headers = headers;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginProtocol setEventBuilder(final EventBuilder event) {
|
||||
this.event = event;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response authenticated(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
|
||||
// First, create a base response token with realm + user values populated
|
||||
final ClientModel client = clientSession.getClient();
|
||||
DockerResponseToken responseToken = new DockerResponseToken()
|
||||
.id(KeycloakModelUtils.generateId())
|
||||
.type(TokenUtil.TOKEN_TYPE_BEARER)
|
||||
.issuer(clientSession.getNote(DockerAuthV2Protocol.ISSUER))
|
||||
.subject(userSession.getUser().getUsername())
|
||||
.issuedNow()
|
||||
.audience(client.getClientId())
|
||||
.issuedFor(client.getClientId());
|
||||
|
||||
// since realm access token is given in seconds
|
||||
final int accessTokenLifespan = realm.getAccessTokenLifespan();
|
||||
responseToken.notBefore(responseToken.getIssuedAt())
|
||||
.expiration(responseToken.getIssuedAt() + accessTokenLifespan);
|
||||
|
||||
// Next, allow mappers to decorate the token to add/remove scopes as appropriate
|
||||
final ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
|
||||
final Set<ProtocolMapperModel> mappings = accessCode.getRequestedProtocolMappers();
|
||||
for (final ProtocolMapperModel mapping : mappings) {
|
||||
final ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
|
||||
if (mapper instanceof DockerAuthV2AttributeMapper) {
|
||||
final DockerAuthV2AttributeMapper dockerAttributeMapper = (DockerAuthV2AttributeMapper) mapper;
|
||||
if (dockerAttributeMapper.appliesTo(responseToken)) {
|
||||
responseToken = dockerAttributeMapper.transformDockerResponseToken(responseToken, mapping, session, userSession, clientSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Finally, construct the response to the docker client with the token + metadata
|
||||
if (event.getEvent() != null && EventType.LOGIN.equals(event.getEvent().getType())) {
|
||||
final KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm);
|
||||
final String encodedToken = new JWSBuilder()
|
||||
.kid(new DockerKeyIdentifier(activeKey.getPublicKey()).toString())
|
||||
.type("JWT")
|
||||
.jsonContent(responseToken)
|
||||
.rsa256(activeKey.getPrivateKey());
|
||||
final String expiresInIso8601String = new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(new Date(responseToken.getIssuedAt() * 1000L));
|
||||
|
||||
final DockerResponse responseEntity = new DockerResponse()
|
||||
.setToken(encodedToken)
|
||||
.setExpires_in(accessTokenLifespan)
|
||||
.setIssued_at(expiresInIso8601String);
|
||||
return new ResponseBuilderImpl().status(Response.Status.OK).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).entity(responseEntity).build();
|
||||
} else {
|
||||
logger.errorv("Unable to handle request for event type {0}. Currently only LOGIN event types are supported by docker protocol.", event.getEvent() == null ? "null" : event.getEvent().getType());
|
||||
throw new ErrorResponseException("invalid_request", "Event type not supported", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
} catch (final InstantiationException e) {
|
||||
logger.errorv("Error attempting to create Key ID for Docker JOSE header: ", e.getMessage());
|
||||
throw new ErrorResponseException("token_error", "Unable to construct JOSE header for JWT", Response.Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response sendError(final AuthenticationSessionModel clientSession, final LoginProtocol.Error error) {
|
||||
return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
|
||||
errorResponse(userSession, "backchannelLogout");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response frontchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
|
||||
return errorResponse(userSession, "frontchannelLogout");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response finishLogout(final UserSessionModel userSession) {
|
||||
return errorResponse(userSession, "finishLogout");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requireReauthentication(final UserSessionModel userSession, final AuthenticationSessionModel clientSession) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private Response errorResponse(final UserSessionModel userSession, final String methodName) {
|
||||
logger.errorv("User {0} attempted to invoke unsupported method {1} on docker protocol.", userSession.getUser().getUsername(), methodName);
|
||||
throw new ErrorResponseException("invalid_request", String.format("Attempted to invoke unsupported docker method %s", methodName), Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package org.keycloak.protocol.docker;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientTemplateModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.AbstractLoginProtocolFactory;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientTemplateRepresentation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class DockerAuthV2ProtocolFactory extends AbstractLoginProtocolFactory implements EnvironmentDependentProviderFactory {
|
||||
|
||||
static List<ProtocolMapperModel> builtins = new ArrayList<>();
|
||||
static List<ProtocolMapperModel> defaultBuiltins = new ArrayList<>();
|
||||
|
||||
static {
|
||||
final ProtocolMapperModel addAllRequestedScopeMapper = new ProtocolMapperModel();
|
||||
addAllRequestedScopeMapper.setName(AllowAllDockerProtocolMapper.PROVIDER_ID);
|
||||
addAllRequestedScopeMapper.setProtocolMapper(AllowAllDockerProtocolMapper.PROVIDER_ID);
|
||||
addAllRequestedScopeMapper.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
|
||||
addAllRequestedScopeMapper.setConsentRequired(false);
|
||||
addAllRequestedScopeMapper.setConfig(Collections.EMPTY_MAP);
|
||||
builtins.add(addAllRequestedScopeMapper);
|
||||
defaultBuiltins.add(addAllRequestedScopeMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addDefaults(final ClientModel client) {
|
||||
defaultBuiltins.forEach(builtinMapper -> client.addProtocolMapper(builtinMapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProtocolMapperModel> getBuiltinMappers() {
|
||||
return builtins;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProtocolMapperModel> getDefaultBuiltinMappers() {
|
||||
return defaultBuiltins;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object createProtocolEndpoint(final RealmModel realm, final EventBuilder event) {
|
||||
return new DockerV2LoginProtocolService(realm, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupClientDefaults(final ClientRepresentation rep, final ClientModel newClient) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupTemplateDefaults(final ClientTemplateRepresentation clientRep, final ClientTemplateModel newClient) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginProtocol create(final KeycloakSession session) {
|
||||
return new DockerAuthV2Protocol().setSession(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return DockerAuthV2Protocol.LOGIN_PROTOCOL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.DOCKER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
return -100;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package org.keycloak.protocol.docker;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
||||
|
||||
public class DockerAuthenticatorFactory implements AuthenticatorFactory {
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Docker Authenticator";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "docker";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final Requirement[] REQUIREMENT_CHOICES = {
|
||||
Requirement.REQUIRED,
|
||||
};
|
||||
|
||||
@Override
|
||||
public Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return new DockerAuthenticator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return DockerAuthenticator.ID;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package org.keycloak.protocol.docker;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.utils.ProfileHelper;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
/**
|
||||
* Implements a docker-client understandable format.
|
||||
*/
|
||||
public class DockerEndpoint extends AuthorizationEndpointBase {
|
||||
protected static final Logger logger = Logger.getLogger(DockerEndpoint.class);
|
||||
|
||||
private final EventType login;
|
||||
private String account;
|
||||
private String service;
|
||||
private String scope;
|
||||
private ClientModel client;
|
||||
private AuthenticationSessionModel authenticationSession;
|
||||
|
||||
public DockerEndpoint(final RealmModel realm, final EventBuilder event, final EventType login) {
|
||||
super(realm, event);
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
@GET
|
||||
public Response build() {
|
||||
ProfileHelper.requireFeature(Profile.Feature.DOCKER);
|
||||
|
||||
final MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
|
||||
|
||||
account = params.getFirst(DockerAuthV2Protocol.ACCOUNT_PARAM);
|
||||
if (account == null) {
|
||||
logger.debug("Account parameter not provided by docker auth. This is techincally required, but not actually used since " +
|
||||
"username is provided by Basic auth header.");
|
||||
}
|
||||
service = params.getFirst(DockerAuthV2Protocol.SERVICE_PARAM);
|
||||
if (service == null) {
|
||||
throw new ErrorResponseException("invalid_request", "service parameter must be provided", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
client = realm.getClientByClientId(service);
|
||||
if (client == null) {
|
||||
logger.errorv("Failed to lookup client given by service={0} parameter for realm: {1}.", service, realm.getName());
|
||||
throw new ErrorResponseException("invalid_client", "Client specified by 'service' parameter does not exist", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
scope = params.getFirst(DockerAuthV2Protocol.SCOPE_PARAM);
|
||||
|
||||
checkSsl();
|
||||
checkRealm();
|
||||
|
||||
final AuthorizationEndpointRequest authRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
|
||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, authRequest.getState());
|
||||
if (checks.response != null) {
|
||||
return checks.response;
|
||||
}
|
||||
|
||||
authenticationSession = checks.authSession;
|
||||
updateAuthenticationSession();
|
||||
|
||||
// So back button doesn't work
|
||||
CacheControlUtil.noBackButtonCacheControlHeader();
|
||||
|
||||
return handleBrowserAuthenticationRequest(authenticationSession, new DockerAuthV2Protocol(session, realm, uriInfo, headers, event.event(login)), false, false);
|
||||
}
|
||||
|
||||
private void updateAuthenticationSession() {
|
||||
authenticationSession.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
|
||||
authenticationSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name());
|
||||
|
||||
// Docker specific stuff
|
||||
authenticationSession.setClientNote(DockerAuthV2Protocol.ACCOUNT_PARAM, account);
|
||||
authenticationSession.setClientNote(DockerAuthV2Protocol.SERVICE_PARAM, service);
|
||||
authenticationSession.setClientNote(DockerAuthV2Protocol.SCOPE_PARAM, scope);
|
||||
authenticationSession.setClientNote(DockerAuthV2Protocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthenticationFlowModel getAuthenticationFlow() {
|
||||
return realm.getDockerAuthenticationFlow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewRequest(final AuthenticationSessionModel authSession, final ClientModel clientFromRequest, final String requestState) {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package org.keycloak.protocol.docker;
|
||||
|
||||
import org.keycloak.models.utils.Base32;
|
||||
|
||||
import java.security.Key;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* The “kid” field has to be in a libtrust fingerprint compatible format. Such a format can be generated by following steps:
|
||||
* 1) Take the DER encoded public key which the JWT token was signed against.
|
||||
* 2) Create a SHA256 hash out of it and truncate to 240bits.
|
||||
* 3) Split the result into 12 base32 encoded groups with : as delimiter.
|
||||
*
|
||||
* Ex: "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"
|
||||
*
|
||||
* @see https://docs.docker.com/registry/spec/auth/jwt/
|
||||
* @see https://github.com/docker/libtrust/blob/master/key.go#L24
|
||||
*/
|
||||
public class DockerKeyIdentifier {
|
||||
|
||||
private final String identifier;
|
||||
|
||||
public DockerKeyIdentifier(final Key key) throws InstantiationException {
|
||||
try {
|
||||
final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
final byte[] hashed = sha256.digest(key.getEncoded());
|
||||
final byte[] hashedTruncated = truncateToBitLength(240, hashed);
|
||||
final String base32Id = Base32.encode(hashedTruncated);
|
||||
identifier = byteStream(base32Id.getBytes()).collect(new DelimitingCollector());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new InstantiationException("Could not instantiate docker key identifier, no SHA-256 algorithm available.");
|
||||
}
|
||||
}
|
||||
|
||||
// ugh.
|
||||
private Stream<Byte> byteStream(final byte[] bytes) {
|
||||
final Collection<Byte> colectionedBytes = new ArrayList<>();
|
||||
for (final byte aByte : bytes) {
|
||||
colectionedBytes.add(aByte);
|
||||
}
|
||||
|
||||
return colectionedBytes.stream();
|
||||
}
|
||||
|
||||
private byte[] truncateToBitLength(final int bitLength, final byte[] arrayToTruncate) {
|
||||
if (bitLength % 8 != 0) {
|
||||
throw new IllegalArgumentException("Bit length for truncation of byte array given as a number not divisible by 8");
|
||||
}
|
||||
|
||||
final int numberOfBytes = bitLength / 8;
|
||||
return Arrays.copyOfRange(arrayToTruncate, 0, numberOfBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DockerKeyIdentifier)) return false;
|
||||
|
||||
final DockerKeyIdentifier that = (DockerKeyIdentifier) o;
|
||||
|
||||
return identifier != null ? identifier.equals(that.identifier) : that.identifier == null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return identifier != null ? identifier.hashCode() : 0;
|
||||
}
|
||||
|
||||
// Could probably be generalized with size and delimiter arguments, but leaving it here for now until someone else needs it.
|
||||
public static class DelimitingCollector implements Collector<Byte, StringBuilder, String> {
|
||||
|
||||
@Override
|
||||
public Supplier<StringBuilder> supplier() {
|
||||
return () -> new StringBuilder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiConsumer<StringBuilder, Byte> accumulator() {
|
||||
return ((stringBuilder, aByte) -> {
|
||||
if (needsDelimiter(4, ":", stringBuilder)) {
|
||||
stringBuilder.append(":");
|
||||
}
|
||||
|
||||
stringBuilder.append(new String(new byte[]{aByte}));
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean needsDelimiter(final int maxLength, final String delimiter, final StringBuilder builder) {
|
||||
final int lastDelimiter = builder.lastIndexOf(delimiter);
|
||||
final int charsSinceLastDelimiter = builder.length() - lastDelimiter;
|
||||
return charsSinceLastDelimiter > maxLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryOperator<StringBuilder> combiner() {
|
||||
return ((left, right) -> new StringBuilder(left.toString()).append(right.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<StringBuilder, String> finisher() {
|
||||
return StringBuilder::toString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package org.keycloak.protocol.docker.installation.compose;
|
||||
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class DockerComposeCertsDirectory {
|
||||
|
||||
private final String directoryName;
|
||||
private final Map.Entry<String, byte[]> localhostCertFile;
|
||||
private final Map.Entry<String, byte[]> localhostKeyFile;
|
||||
private final Map.Entry<String, byte[]> idpTrustChainFile;
|
||||
|
||||
public DockerComposeCertsDirectory(final String directoryName, final Certificate realmCert, final String registryCertFilename, final String registryKeyFilename, final String idpCertTrustChainFilename, final String realmName) {
|
||||
this.directoryName = directoryName;
|
||||
|
||||
final KeyPairGenerator keyGen;
|
||||
try {
|
||||
keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048, new SecureRandom());
|
||||
|
||||
final KeyPair keypair = keyGen.generateKeyPair();
|
||||
final PrivateKey privateKey = keypair.getPrivate();
|
||||
final Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, realmName);
|
||||
|
||||
localhostCertFile = new AbstractMap.SimpleImmutableEntry<>(registryCertFilename, DockerCertFileUtils.formatCrtFileContents(certificate).getBytes());
|
||||
localhostKeyFile = new AbstractMap.SimpleImmutableEntry<>(registryKeyFilename, DockerCertFileUtils.formatPrivateKeyContents(privateKey).getBytes());
|
||||
idpTrustChainFile = new AbstractMap.SimpleEntry<>(idpCertTrustChainFilename, DockerCertFileUtils.formatCrtFileContents(realmCert).getBytes());
|
||||
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
// TODO throw error here descritively
|
||||
throw new RuntimeException(e);
|
||||
} catch (final CertificateEncodingException e) {
|
||||
// TODO throw error here descritively
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getDirectoryName() {
|
||||
return directoryName;
|
||||
}
|
||||
|
||||
public Map.Entry<String, byte[]> getLocalhostCertFile() {
|
||||
return localhostCertFile;
|
||||
}
|
||||
|
||||
public Map.Entry<String, byte[]> getLocalhostKeyFile() {
|
||||
return localhostKeyFile;
|
||||
}
|
||||
|
||||
public Map.Entry<String, byte[]> getIdpTrustChainFile() {
|
||||
return idpTrustChainFile;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package org.keycloak.protocol.docker.mapper;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.docker.DockerAuthV2Protocol;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class DockerAuthV2ProtocolMapper implements ProtocolMapper {
|
||||
|
||||
public static final String DOCKER_AUTH_V2_CATEGORY = "Docker Auth Mapper";
|
||||
|
||||
@Override
|
||||
public String getProtocol() {
|
||||
return DockerAuthV2Protocol.LOGIN_PROTOCOL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return DOCKER_AUTH_V2_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ProtocolMapper create(final KeycloakSession session) {
|
||||
throw new UnsupportedOperationException("The create method is not supported by this mapper");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(final KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -258,6 +258,7 @@ public class TokenEndpoint {
|
|||
}
|
||||
|
||||
event.user(userSession.getUser());
|
||||
|
||||
event.session(userSession.getId());
|
||||
|
||||
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
||||
|
|
|
@ -26,8 +26,10 @@ import org.keycloak.representations.IDToken;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Set the 'name' claim to be first + last name.
|
||||
|
@ -73,9 +75,12 @@ public class FullNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc
|
|||
|
||||
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
|
||||
UserModel user = userSession.getUser();
|
||||
String first = user.getFirstName() == null ? "" : user.getFirstName() + " ";
|
||||
String last = user.getLastName() == null ? "" : user.getLastName();
|
||||
token.getOtherClaims().put("name", first + last);
|
||||
List<String> parts = new LinkedList<>();
|
||||
Optional.ofNullable(user.getFirstName()).filter(s -> !s.isEmpty()).ifPresent(parts::add);
|
||||
Optional.ofNullable(user.getLastName()).filter(s -> !s.isEmpty()).ifPresent(parts::add);
|
||||
if (!parts.isEmpty()) {
|
||||
token.getOtherClaims().put("name", String.join(" ", parts));
|
||||
}
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel create(String name,
|
||||
|
|
|
@ -91,7 +91,7 @@ public abstract class OIDCRedirectUriBuilder {
|
|||
|
||||
@Override
|
||||
public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
|
||||
String param = paramName + "=" + Encode.encodeQueryParam(paramValue);
|
||||
String param = paramName + "=" + Encode.encodeQueryParamAsIs(paramValue);
|
||||
if (fragment == null) {
|
||||
fragment = new StringBuilder(param);
|
||||
} else {
|
||||
|
|
|
@ -193,7 +193,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
if (samlClient.requiresEncryption()) {
|
||||
PublicKey publicKey;
|
||||
try {
|
||||
publicKey = SamlProtocolUtils.getEncryptionValidationKey(client);
|
||||
publicKey = SamlProtocolUtils.getEncryptionKey(client);
|
||||
} catch (Exception e) {
|
||||
logger.error("failed", e);
|
||||
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
|
||||
|
@ -457,7 +457,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
if (samlClient.requiresEncryption()) {
|
||||
PublicKey publicKey = null;
|
||||
try {
|
||||
publicKey = SamlProtocolUtils.getEncryptionValidationKey(client);
|
||||
publicKey = SamlProtocolUtils.getEncryptionKey(client);
|
||||
} catch (Exception e) {
|
||||
logger.error("failed", e);
|
||||
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
|
||||
|
|
|
@ -103,7 +103,7 @@ public class SamlProtocolUtils {
|
|||
* @return Public key for encryption.
|
||||
* @throws VerificationException
|
||||
*/
|
||||
public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
|
||||
public static PublicKey getEncryptionKey(ClientModel client) throws VerificationException {
|
||||
return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
|
||||
}
|
||||
|
||||
|
|
153
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
Executable file → Normal file
153
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
Executable file → Normal file
|
@ -1,124 +1,81 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.protocol.saml.profile.ecp.authenticator;
|
||||
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class HttpBasicAuthenticator implements AuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "http-basic-authenticator";
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "HTTP Basic Authentication";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Requirement[] getRequirementChoices() {
|
||||
return new Requirement[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Validates username and password from Authorization HTTP header";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return new Authenticator() {
|
||||
public class HttpBasicAuthenticator implements 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);
|
||||
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) {
|
||||
RealmModel realm = context.getRealm();
|
||||
UserModel user = context.getSession().users().getUserByUsername(usernameAndPassword[0], realm);
|
||||
final RealmModel realm = context.getRealm();
|
||||
final String username = usernameAndPassword[0];
|
||||
final UserModel user = context.getSession().users().getUserByUsername(username, realm);
|
||||
|
||||
if (user != null) {
|
||||
String password = usernameAndPassword[1];
|
||||
boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
|
||||
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();
|
||||
} else {
|
||||
}
|
||||
|
||||
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(HttpHeaders httpHeaders) {
|
||||
List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
|
||||
private String[] getUsernameAndPassword(final HttpHeaders httpHeaders) {
|
||||
final List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
if (authHeaders == null || authHeaders.size() == 0) {
|
||||
return null;
|
||||
|
@ -126,9 +83,9 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory {
|
|||
|
||||
String credentials = null;
|
||||
|
||||
for (String authHeader : authHeaders) {
|
||||
for (final String authHeader : authHeaders) {
|
||||
if (authHeader.startsWith(BASIC_PREFIX)) {
|
||||
String[] split = authHeader.trim().split("\\s+");
|
||||
final String[] split = authHeader.trim().split("\\s+");
|
||||
|
||||
if (split == null || split.length != 2) return null;
|
||||
|
||||
|
@ -138,13 +95,13 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory {
|
|||
|
||||
try {
|
||||
return new String(Base64.decode(credentials)).split(":");
|
||||
} catch (IOException e) {
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException("Failed to parse credentials.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
public void action(final AuthenticationFlowContext context) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -154,12 +111,12 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
public boolean configuredFor(final KeycloakSession session, final RealmModel realm, final UserModel user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
public void setRequiredActions(final KeycloakSession session, final RealmModel realm, final UserModel user) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -167,26 +124,4 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory {
|
|||
public void close() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.protocol.saml.profile.ecp.authenticator;
|
||||
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class HttpBasicAuthenticatorFactory implements AuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "http-basic-authenticator";
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "HTTP Basic Authentication";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "basic";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
Requirement.ALTERNATIVE,
|
||||
Requirement.OPTIONAL,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED
|
||||
};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Validates username and password from Authorization HTTP header";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(final KeycloakSession session) {
|
||||
return new HttpBasicAuthenticator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(final KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -97,9 +97,12 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
auth.requireView(client);
|
||||
|
||||
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
|
||||
if (client.getSecret() != null) {
|
||||
rep.setSecret(client.getSecret());
|
||||
}
|
||||
|
||||
if (auth.isRegistrationAccessToken()) {
|
||||
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client, auth.getRegistrationAuth());
|
||||
String registrationAccessToken = ClientRegistrationTokenUtils.updateTokenSignature(session, auth);
|
||||
rep.setRegistrationAccessToken(registrationAccessToken);
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,8 @@ public class ClientRegistrationAuth {
|
|||
private RealmModel realm;
|
||||
private JsonWebToken jwt;
|
||||
private ClientInitialAccessModel initialAccessModel;
|
||||
private String kid;
|
||||
private String token;
|
||||
|
||||
public ClientRegistrationAuth(KeycloakSession session, ClientRegistrationProvider provider, EventBuilder event) {
|
||||
this.session = session;
|
||||
|
@ -81,10 +83,13 @@ public class ClientRegistrationAuth {
|
|||
return;
|
||||
}
|
||||
|
||||
ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, split[1]);
|
||||
token = split[1];
|
||||
|
||||
ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, token);
|
||||
if (tokenVerification.getError() != null) {
|
||||
throw unauthorized(tokenVerification.getError().getMessage());
|
||||
}
|
||||
kid = tokenVerification.getKid();
|
||||
jwt = tokenVerification.getJwt();
|
||||
|
||||
if (isInitialAccessToken()) {
|
||||
|
@ -95,6 +100,18 @@ public class ClientRegistrationAuth {
|
|||
}
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public String getKid() {
|
||||
return kid;
|
||||
}
|
||||
|
||||
public JsonWebToken getJwt() {
|
||||
return jwt;
|
||||
}
|
||||
|
||||
private boolean isBearerToken() {
|
||||
return jwt != null && TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
|
||||
}
|
||||
|
|
|
@ -44,6 +44,27 @@ public class ClientRegistrationTokenUtils {
|
|||
public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken";
|
||||
public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken";
|
||||
|
||||
public static String updateTokenSignature(KeycloakSession session, ClientRegistrationAuth auth) {
|
||||
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(session.getContext().getRealm());
|
||||
|
||||
if (keys.getKid().equals(auth.getKid())) {
|
||||
return auth.getToken();
|
||||
} else {
|
||||
RegistrationAccessToken regToken = new RegistrationAccessToken();
|
||||
regToken.setRegistrationAuth(auth.getRegistrationAuth().toString().toLowerCase());
|
||||
|
||||
regToken.type(auth.getJwt().getType());
|
||||
regToken.id(auth.getJwt().getId());
|
||||
regToken.issuedAt(Time.currentTime());
|
||||
regToken.expiration(0);
|
||||
regToken.issuer(auth.getJwt().getIssuer());
|
||||
regToken.audience(auth.getJwt().getIssuer());
|
||||
|
||||
String token = new JWSBuilder().kid(keys.getKid()).jsonContent(regToken).rsa256(keys.getPrivateKey());
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client, RegistrationAuth registrationAuth) {
|
||||
return updateRegistrationAccessToken(session, session.getContext().getRealm(), session.getContext().getUri(), client, registrationAuth);
|
||||
}
|
||||
|
@ -75,7 +96,8 @@ public class ClientRegistrationTokenUtils {
|
|||
return TokenVerification.error(new RuntimeException("Invalid token", e));
|
||||
}
|
||||
|
||||
PublicKey publicKey = session.keys().getRsaPublicKey(realm, input.getHeader().getKeyId());
|
||||
String kid = input.getHeader().getKeyId();
|
||||
PublicKey publicKey = session.keys().getRsaPublicKey(realm, kid);
|
||||
|
||||
if (!RSAProvider.verify(input, publicKey)) {
|
||||
return TokenVerification.error(new RuntimeException("Failed verify token"));
|
||||
|
@ -102,7 +124,7 @@ public class ClientRegistrationTokenUtils {
|
|||
return TokenVerification.error(new RuntimeException("Invalid type of token"));
|
||||
}
|
||||
|
||||
return TokenVerification.success(jwt);
|
||||
return TokenVerification.success(kid, jwt);
|
||||
}
|
||||
|
||||
private static String setupToken(JsonWebToken jwt, KeycloakSession session, RealmModel realm, UriInfo uri, String id, String type, int expiration) {
|
||||
|
@ -127,22 +149,28 @@ public class ClientRegistrationTokenUtils {
|
|||
|
||||
protected static class TokenVerification {
|
||||
|
||||
private final String kid;
|
||||
private final JsonWebToken jwt;
|
||||
private final RuntimeException error;
|
||||
|
||||
public static TokenVerification success(JsonWebToken jwt) {
|
||||
return new TokenVerification(jwt, null);
|
||||
public static TokenVerification success(String kid, JsonWebToken jwt) {
|
||||
return new TokenVerification(kid, jwt, null);
|
||||
}
|
||||
|
||||
public static TokenVerification error(RuntimeException error) {
|
||||
return new TokenVerification(null, error);
|
||||
return new TokenVerification(null,null, error);
|
||||
}
|
||||
|
||||
private TokenVerification(JsonWebToken jwt, RuntimeException error) {
|
||||
private TokenVerification(String kid, JsonWebToken jwt, RuntimeException error) {
|
||||
this.kid = kid;
|
||||
this.jwt = jwt;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public String getKid() {
|
||||
return kid;
|
||||
}
|
||||
|
||||
public JsonWebToken getJwt() {
|
||||
return jwt;
|
||||
}
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package org.keycloak.services.clientregistration.policy;
|
||||
|
||||
import org.keycloak.services.clientregistration.RegistrationAccessToken;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.BadRequestException;
|
||||
|
@ -29,6 +30,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
|||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.email.EmailTemplateProvider;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.EventQuery;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
|
@ -50,6 +52,7 @@ import org.keycloak.models.LDAPConstants;
|
|||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.cache.CacheRealmProvider;
|
||||
import org.keycloak.models.cache.UserCache;
|
||||
|
@ -102,9 +105,9 @@ import java.util.HashMap;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
|
||||
import static org.keycloak.util.JsonSerialization.readValue;
|
||||
|
||||
/**
|
||||
* Base resource class for the admin REST api of one realm
|
||||
|
@ -811,6 +814,35 @@ public class RealmAdminResource {
|
|||
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SMTP connection with current logged in user
|
||||
*
|
||||
* @param config SMTP server configuration
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@Path("testSMTPConnection/{config}")
|
||||
@POST
|
||||
@NoCache
|
||||
public Response testSMTPConnection(final @PathParam("config") String config) throws Exception {
|
||||
Map<String, String> settings = readValue(config, new TypeReference<Map<String, String>>() {
|
||||
});
|
||||
|
||||
try {
|
||||
UserModel user = auth.adminAuth().getUser();
|
||||
if (user.getEmail() == null) {
|
||||
return ErrorResponse.error("Logged in user does not have an e-mail.", Response.Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
session.getProvider(EmailTemplateProvider.class).sendSmtpTestEmail(settings, user);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.errorf("Failed to send email \n %s", e.getCause());
|
||||
return ErrorResponse.error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@Path("identity-provider")
|
||||
public IdentityProvidersResource getIdentityProviderResource() {
|
||||
return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
|
||||
|
|
|
@ -52,6 +52,7 @@ import org.keycloak.models.UserSessionModel;
|
|||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
@ -162,8 +163,9 @@ public class UsersResource {
|
|||
try {
|
||||
UserModel user = session.users().addUser(realm, rep.getUsername());
|
||||
Set<String> emptySet = Collections.emptySet();
|
||||
UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false);
|
||||
|
||||
UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false);
|
||||
RepresentationToModel.createCredentials(rep, session, realm, user);
|
||||
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success();
|
||||
|
||||
if (session.getTransactionManager().isActive()) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
23
services/src/main/resources/DockerComposeYamlReadme.md
Normal file
23
services/src/main/resources/DockerComposeYamlReadme.md
Normal file
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,3 +17,4 @@
|
|||
|
||||
org.keycloak.protocol.oidc.OIDCLoginProtocolFactory
|
||||
org.keycloak.protocol.saml.SamlProtocolFactory
|
||||
org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
package org.keycloak.procotol.docker.installation;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.hamcrest.core.IsNull.notNullValue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider.ROOT_DIR;
|
||||
|
||||
public class DockerComposeYamlInstallationProviderTest {
|
||||
|
||||
DockerComposeYamlInstallationProvider installationProvider;
|
||||
static Certificate certificate;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp_beforeClass() throws NoSuchAlgorithmException {
|
||||
final KeyPairGenerator keyGen;
|
||||
keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048, new SecureRandom());
|
||||
|
||||
final KeyPair keypair = keyGen.generateKeyPair();
|
||||
certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, "test-realm");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
installationProvider = new DockerComposeYamlInstallationProvider();
|
||||
}
|
||||
|
||||
private Response fireInstallationProvider() throws IOException {
|
||||
ByteArrayOutputStream byteStream = null;
|
||||
ZipOutputStream zipOutput = null;
|
||||
byteStream = new ByteArrayOutputStream();
|
||||
zipOutput = new ZipOutputStream(byteStream);
|
||||
|
||||
return installationProvider.generateInstallation(zipOutput, byteStream, certificate, new URL("http://localhost:8080/auth"), "docker-test", "docker-registry");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // Used only for smoke testing
|
||||
public void writeToRealZip() throws IOException {
|
||||
final Response response = fireInstallationProvider();
|
||||
final byte[] responseBytes = (byte[]) response.getEntity();
|
||||
FileUtils.writeByteArrayToFile(new File("target/keycloak-docker-compose-yaml.zip"), responseBytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllTheZipThings() throws Exception {
|
||||
final Response response = fireInstallationProvider();
|
||||
assertThat("compose YAML returned non-ok response", response.getStatus(), equalTo(Response.Status.OK.getStatusCode()));
|
||||
|
||||
shouldIncludeDockerComposeYamlInZip(getZipResponseFromInstallProvider(response));
|
||||
shouldIncludeReadmeInZip(getZipResponseFromInstallProvider(response));
|
||||
shouldWriteBlankDataDirectoryInZip(getZipResponseFromInstallProvider(response));
|
||||
shouldWriteCertDirectoryInZip(getZipResponseFromInstallProvider(response));
|
||||
shouldWriteSslCertificateInZip(getZipResponseFromInstallProvider(response));
|
||||
shouldWritePrivateKeyInZip(getZipResponseFromInstallProvider(response));
|
||||
}
|
||||
|
||||
public void shouldIncludeDockerComposeYamlInZip(ZipInputStream zipInput) throws Exception {
|
||||
final Optional<String> dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "docker-compose.yaml");
|
||||
|
||||
assertThat("Could not find docker-compose.yaml file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true));
|
||||
final boolean zipFileContentEqualsTestFile = IOUtils.contentEquals(new ByteArrayInputStream(dockerComposeFileContents.get().getBytes()), new FileInputStream("src/test/resources/docker-compose-expected.yaml"));
|
||||
assertThat("Invalid docker-compose file contents: \n" + dockerComposeFileContents.get(), zipFileContentEqualsTestFile, equalTo(true));
|
||||
}
|
||||
|
||||
public void shouldIncludeReadmeInZip(ZipInputStream zipInput) throws Exception {
|
||||
final Optional<String> dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "README.md");
|
||||
|
||||
assertThat("Could not find README.md file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true));
|
||||
}
|
||||
|
||||
public void shouldWriteBlankDataDirectoryInZip(ZipInputStream zipInput) throws Exception {
|
||||
ZipEntry zipEntry;
|
||||
boolean dataDirFound = false;
|
||||
while ((zipEntry = zipInput.getNextEntry()) != null) {
|
||||
try {
|
||||
if (zipEntry.getName().equals(ROOT_DIR + "data/")) {
|
||||
dataDirFound = true;
|
||||
assertThat("Zip entry for data directory is not the correct type", zipEntry.isDirectory(), equalTo(true));
|
||||
}
|
||||
} finally {
|
||||
zipInput.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
assertThat("Could not find data directory", dataDirFound, equalTo(true));
|
||||
}
|
||||
|
||||
public void shouldWriteCertDirectoryInZip(ZipInputStream zipInput) throws Exception {
|
||||
ZipEntry zipEntry;
|
||||
boolean certsDirFound = false;
|
||||
while ((zipEntry = zipInput.getNextEntry()) != null) {
|
||||
try {
|
||||
if (zipEntry.getName().equals(ROOT_DIR + "certs/")) {
|
||||
certsDirFound = true;
|
||||
assertThat("Zip entry for cert directory is not the correct type", zipEntry.isDirectory(), equalTo(true));
|
||||
}
|
||||
} finally {
|
||||
zipInput.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
assertThat("Could not find cert directory", certsDirFound, equalTo(true));
|
||||
}
|
||||
|
||||
public void shouldWriteSslCertificateInZip(ZipInputStream zipInput) throws Exception {
|
||||
final Optional<String> localhostCertificateFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.crt");
|
||||
|
||||
assertThat("Could not find localhost certificate", localhostCertificateFileContents.isPresent(), equalTo(true));
|
||||
final X509Certificate x509Certificate = PemUtils.decodeCertificate(localhostCertificateFileContents.get());
|
||||
assertThat("Invalid x509 given by docker-compose YAML", x509Certificate, notNullValue());
|
||||
}
|
||||
|
||||
public void shouldWritePrivateKeyInZip(ZipInputStream zipInput) throws Exception {
|
||||
final Optional<String> localhostPrivateKeyFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.key");
|
||||
|
||||
assertThat("Could not find localhost private key", localhostPrivateKeyFileContents.isPresent(), equalTo(true));
|
||||
final PrivateKey privateKey = PemUtils.decodePrivateKey(localhostPrivateKeyFileContents.get());
|
||||
assertThat("Invalid private Key given by docker-compose YAML", privateKey, notNullValue());
|
||||
}
|
||||
|
||||
private ZipInputStream getZipResponseFromInstallProvider(Response response) throws IOException {
|
||||
final Object responseEntity = response.getEntity();
|
||||
if (!(responseEntity instanceof byte[])) {
|
||||
fail("Recieved non-byte[] entity for docker-compose YAML installation response");
|
||||
}
|
||||
|
||||
return new ZipInputStream(new ByteArrayInputStream((byte[]) responseEntity));
|
||||
}
|
||||
|
||||
private static Optional<String> getFileContents(final ZipInputStream zipInputStream, final String fileName) throws IOException {
|
||||
ZipEntry zipEntry;
|
||||
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
||||
try {
|
||||
if (zipEntry.getName().equals(fileName)) {
|
||||
return Optional.of(readBytesToString(zipInputStream));
|
||||
}
|
||||
} finally {
|
||||
zipInputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// fall-through case if file name not found:
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static String readBytesToString(final InputStream inputStream) throws IOException {
|
||||
final ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
final byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
|
||||
try {
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} finally {
|
||||
output.close();
|
||||
}
|
||||
|
||||
return new String(output.toByteArray());
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
15
services/src/test/resources/docker-compose-expected.yaml
Normal file
15
services/src/test/resources/docker-compose-expected.yaml
Normal file
|
@ -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
|
|
@ -262,6 +262,8 @@ The UI tests are focused on the Admin Console as well as on some login scenarios
|
|||
|
||||
The tests also use some constants placed in [test-constants.properties](tests/base/src/test/resources/test-constants.properties). A different file can be specified by `-Dtestsuite.constants=path/to/different-test-constants.properties`
|
||||
|
||||
In case a custom `settings.xml` is used for Maven, you need to specify it also in `-Dkie.maven.settings.custom=path/to/settings.xml`.
|
||||
|
||||
#### Execution example
|
||||
```
|
||||
mvn -f testsuite/integration-arquillian/tests/other/console/pom.xml \
|
||||
|
@ -464,3 +466,103 @@ or
|
|||
`mvn -Pcache-server-jdg -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test`
|
||||
|
||||
_Someone using IntelliJ IDEA, please describe steps for that IDE_
|
||||
|
||||
## Run Docker Authentication test
|
||||
|
||||
First, validate that your machine has a valid docker installation and that it is available to the JVM running the test.
|
||||
The exact steps to configure Docker depend on the operating system.
|
||||
|
||||
By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand.
|
||||
The exact command line arguments depend on the operating system.
|
||||
|
||||
### General guidelines
|
||||
|
||||
If docker daemon doesn't run locally, or if you're not running on Linux, you may need
|
||||
to determine the IP of the bridge interface or local interface that Docker daemon can use to connect to Keycloak Server.
|
||||
Then specify that IP as additional system property called *host.ip*, for example:
|
||||
|
||||
-Dhost.ip=192.168.64.1
|
||||
|
||||
If using Docker for Mac, you can create an alias for your local network interface:
|
||||
|
||||
sudo ifconfig lo0 alias 10.200.10.1/24
|
||||
|
||||
Then pass the IP as *host.ip*:
|
||||
|
||||
-Dhost.ip=10.200.10.1
|
||||
|
||||
|
||||
If you're running a Docker fork that always lists a host component of an image on `docker images` (e.g. Fedora / RHEL Docker)
|
||||
use `-Ddocker.io-prefix-explicit=true` argument when running the test.
|
||||
|
||||
|
||||
### Fedora
|
||||
|
||||
On Fedora one way to set up Docker server is the following:
|
||||
|
||||
# install docker
|
||||
sudo dnf install docker
|
||||
|
||||
# configure docker
|
||||
# remove --selinux-enabled from OPTIONS
|
||||
sudo vi /etc/sysconfig/docker
|
||||
|
||||
# create docker group and add your user (so docker wouldn't need root permissions)
|
||||
sudo groupadd docker && sudo gpasswd -a ${USER} docker && sudo systemctl restart docker
|
||||
newgrp docker
|
||||
|
||||
# you need to login again after this
|
||||
|
||||
|
||||
# make sure Docker is available
|
||||
docker pull registry:2
|
||||
|
||||
You may also need to add an iptables rule to allow container to host traffic
|
||||
|
||||
sudo iptables -I INPUT -i docker0 -j ACCEPT
|
||||
|
||||
Then, run the test passing `-Ddocker.io-prefix-explicit=true`:
|
||||
|
||||
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
|
||||
clean test \
|
||||
-Dtest=DockerClientTest \
|
||||
-Dkeycloak.profile.feature.docker=enabled \
|
||||
-Ddocker.io-prefix-explicit=true
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
On macOS all you need to do is install Docker for Mac, start it up, and check that it works:
|
||||
|
||||
# make sure Docker is available
|
||||
docker pull registry:2
|
||||
|
||||
Be especially careful to restart Docker server after every sleep / suspend to ensure system clock of Docker VM is synchronized with
|
||||
that of the host operating system - Docker for Mac runs inside a VM.
|
||||
|
||||
|
||||
Then, run the test passing `-Dhost.ip=IP` where IP corresponds to en0 interface or an alias for localhost:
|
||||
|
||||
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
|
||||
clean test \
|
||||
-Dtest=DockerClientTest \
|
||||
-Dkeycloak.profile.feature.docker=enabled \
|
||||
-Dhost.ip=10.200.10.1
|
||||
|
||||
|
||||
|
||||
### Running Docker test against Keycloak Server distribution
|
||||
|
||||
Make sure to build the distribution:
|
||||
|
||||
mvn clean install -f distribution
|
||||
|
||||
Then, before running the test, setup Keycloak Server distribution for the tests:
|
||||
|
||||
mvn -f testsuite/integration-arquillian/servers/pom.xml \
|
||||
clean install \
|
||||
-Pauth-server-wildfly
|
||||
|
||||
When running the test, add the following arguments to the command line:
|
||||
|
||||
-Pauth-server-wildfly -Pauth-server-enable-disable-feature -Dfeature.name=docker -Dfeature.value=enabled
|
||||
|
|
|
@ -309,7 +309,7 @@
|
|||
</goals>
|
||||
<configuration>
|
||||
<executable>${common.resources}/install-patch.${script.suffix}</executable>
|
||||
<workingDirectory>${app.server.home}/bin</workingDirectory>
|
||||
<workingDirectory>${app.server.jboss.home}/bin</workingDirectory>
|
||||
<environmentVariables>
|
||||
<JAVA_HOME>${app.server.java.home}</JAVA_HOME>
|
||||
<JBOSS_HOME>${app.server.jboss.home}</JBOSS_HOME>
|
||||
|
|
|
@ -36,6 +36,15 @@
|
|||
</provider>
|
||||
</spi>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="samlPortsDefinition">
|
||||
<spi name="login-protocol">
|
||||
<provider name="saml" enabled="true">
|
||||
<properties>
|
||||
<property name="knownProtocols" value="["http=${{auth.server.http.port}}","https=${{auth.server.https.port}}"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="themeModuleDefinition">
|
||||
<modules>
|
||||
<module>org.keycloak.testsuite.integration-arquillian-testsuite-providers</module>
|
||||
|
@ -60,11 +69,12 @@
|
|||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
<!--inject truststore-->
|
||||
<!--inject truststore and SAML port-protocol mappings-->
|
||||
<xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsKS)]">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()" />
|
||||
<xsl:copy-of select="$truststoreDefinition"/>
|
||||
<xsl:copy-of select="$samlPortsDefinition"/>
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
|
|
|
@ -100,7 +100,6 @@
|
|||
<outputDirectory>${project.build.directory}/unpacked</outputDirectory>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
<excludes>**/product.conf</excludes>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
|
|
|
@ -88,6 +88,28 @@
|
|||
<artifactId>greenmail</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>com.spotify</groupId>-->
|
||||
<!--<artifactId>docker-client</artifactId>-->
|
||||
<!--<version>8.3.2</version>-->
|
||||
<!--<scope>test</scope>-->
|
||||
<!--<exclusions>-->
|
||||
<!--<exclusion>-->
|
||||
<!--<groupId>javax.ws.rs</groupId>-->
|
||||
<!--<artifactId>javax.ws.rs-api</artifactId>-->
|
||||
<!--</exclusion>-->
|
||||
<!--<exclusion>-->
|
||||
<!--<groupId>com.github.jnr</groupId>-->
|
||||
<!--<artifactId>jnr-unixsocket</artifactId>-->
|
||||
<!--</exclusion>-->
|
||||
<!--</exclusions>-->
|
||||
<!--</dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers</artifactId>
|
||||
<version>1.2.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue