Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2017-06-29 17:37:45 -04:00
commit 999dff353c
173 changed files with 6324 additions and 501 deletions

View file

@ -91,6 +91,8 @@ public class KeycloakDeployment {
// https://tools.ietf.org/html/rfc7636
protected boolean pkce = false;
protected boolean ignoreOAuthQueryParameter;
protected Map<String, String> redirectRewriteRules;
public KeycloakDeployment() {
}
@ -446,4 +448,14 @@ public class KeycloakDeployment {
public boolean isOAuthQueryParameterEnabled() {
return !this.ignoreOAuthQueryParameter;
}
public Map<String, String> getRedirectRewriteRules() {
return redirectRewriteRules;
}
public void setRewriteRedirectRules(Map<String, String> redirectRewriteRules) {
this.redirectRewriteRules = redirectRewriteRules;
}
}

View file

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

View file

@ -25,7 +25,6 @@ import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Encode;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.UriUtils;
import org.keycloak.constants.AdapterConstants;
@ -38,7 +37,10 @@ import org.keycloak.representations.IDToken;
import org.keycloak.util.TokenUtil;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.logging.Level;
/**
@ -141,6 +143,7 @@ public class OAuthRequestAuthenticator {
protected String getRedirectUri(String state) {
String url = getRequestUrl();
log.debugf("callback uri: %s", url);
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
int port = sslRedirectPort();
if (port < 0) {
@ -170,7 +173,7 @@ public class OAuthRequestAuthenticator {
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
.queryParam(OAuth2Constants.REDIRECT_URI, Encode.encodeQueryParamAsIs(url)) // Need to encode uri ourselves as queryParam() will not encode % characters.
.queryParam(OAuth2Constants.REDIRECT_URI, rewrittenRedirectUri(url))
.queryParam(OAuth2Constants.STATE, state)
.queryParam("login", "true");
if(loginHint != null && loginHint.length() > 0){
@ -320,10 +323,11 @@ public class OAuthRequestAuthenticator {
AccessTokenResponse tokenResponse = null;
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
try {
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, rewrittenRedirectUri(strippedOauthParametersRequestUri), httpSessionId);
} catch (ServerRequest.HttpFailure failure) {
log.error("failed to turn code into token");
log.error("status from server: " + failure.getStatus());
@ -375,6 +379,23 @@ public class OAuthRequestAuthenticator {
.replaceQueryParam(OAuth2Constants.STATE, null);
return builder.build().toString();
}
private String rewrittenRedirectUri(String originalUri) {
Map<String, String> rewriteRules = deployment.getRedirectRewriteRules();
if(rewriteRules != null && !rewriteRules.isEmpty()) {
try {
URL url = new URL(originalUri);
Map.Entry<String, String> rule = rewriteRules.entrySet().iterator().next();
StringBuilder redirectUriBuilder = new StringBuilder(url.getProtocol());
redirectUriBuilder.append("://"+ url.getAuthority());
redirectUriBuilder.append(url.getPath().replaceFirst(rule.getKey(), rule.getValue()));
return redirectUriBuilder.toString();
} catch (MalformedURLException ex) {
log.error("Not a valid request url");
throw new RuntimeException(ex);
}
}
return originalUri;
}
}

View file

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

View file

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

View file

@ -54,72 +54,96 @@ import java.util.regex.Pattern;
*/
public class KeycloakOIDCFilter implements Filter {
private final static Logger log = Logger.getLogger("" + KeycloakOIDCFilter.class);
public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern";
public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver";
public static final String CONFIG_FILE_PARAM = "keycloak.config.file";
public static final String CONFIG_PATH_PARAM = "keycloak.config.path";
protected AdapterDeploymentContext deploymentContext;
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
protected NodesRegistrationManagement nodesRegistrationManagement;
protected Pattern skipPattern;
private final static Logger log = Logger.getLogger(""+KeycloakOIDCFilter.class);
private final KeycloakConfigResolver definedconfigResolver;
/**
* Constructor that can be used to define a {@code KeycloakConfigResolver} that will be used at initialization to
* provide the {@code KeycloakDeployment}.
* @param definedconfigResolver the resolver
*/
public KeycloakOIDCFilter(KeycloakConfigResolver definedconfigResolver) {
this.definedconfigResolver = definedconfigResolver;
}
public KeycloakOIDCFilter() {
this(null);
}
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
String skipPatternDefinition = filterConfig.getInitParameter(SKIP_PATTERN_PARAM);
if (skipPatternDefinition != null) {
skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL);
}
String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver");
if (configResolverClass != null) {
try {
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
deploymentContext = new AdapterDeploymentContext(configResolver);
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
} catch (Exception ex) {
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
}
if (definedconfigResolver != null) {
deploymentContext = new AdapterDeploymentContext(definedconfigResolver);
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass());
} else {
String fp = filterConfig.getInitParameter("keycloak.config.file");
InputStream is = null;
if (fp != null) {
String configResolverClass = filterConfig.getInitParameter(CONFIG_RESOLVER_PARAM);
if (configResolverClass != null) {
try {
is = new FileInputStream(fp);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
deploymentContext = new AdapterDeploymentContext(configResolver);
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
} catch (Exception ex) {
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
}
} else {
String path = "/WEB-INF/keycloak.json";
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
if (pathParam != null) path = pathParam;
is = filterConfig.getServletContext().getResourceAsStream(path);
String fp = filterConfig.getInitParameter(CONFIG_FILE_PARAM);
InputStream is = null;
if (fp != null) {
try {
is = new FileInputStream(fp);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
} else {
String path = "/WEB-INF/keycloak.json";
String pathParam = filterConfig.getInitParameter(CONFIG_PATH_PARAM);
if (pathParam != null) path = pathParam;
is = filterConfig.getServletContext().getResourceAsStream(path);
}
KeycloakDeployment kd = createKeycloakDeploymentFrom(is);
deploymentContext = new AdapterDeploymentContext(kd);
log.fine("Keycloak is using a per-deployment configuration.");
}
KeycloakDeployment kd = createKeycloakDeploymentFrom(is);
deploymentContext = new AdapterDeploymentContext(kd);
log.fine("Keycloak is using a per-deployment configuration.");
}
filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
nodesRegistrationManagement = new NodesRegistrationManagement();
}
private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) {
if (is == null) {
log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
return new KeycloakDeployment();
}
return KeycloakDeploymentBuilder.build(is);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
log.fine("Keycloak OIDC Filter");
//System.err.println("Keycloak OIDC Filter: " + ((HttpServletRequest)req).getRequestURL().toString());
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
@ -201,7 +225,7 @@ public class KeycloakOIDCFilter implements Filter {
*
* @param request the request to check
* @return {@code true} if the request should not be handled,
* {@code false} otherwise.
* {@code false} otherwise.
*/
private boolean shouldSkip(HttpServletRequest request) {

View file

@ -37,6 +37,8 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD
public final class KeycloakAdapterConfigService {
private static final String CREDENTIALS_JSON_NAME = "credentials";
private static final String REDIRECT_REWRITE_RULE_JSON_NAME = "redirect-rewrite-rule";
private static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService();
@ -129,6 +131,56 @@ public final class KeycloakAdapterConfigService {
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
return deployment.get(CREDENTIALS_JSON_NAME);
}
public void addRedirectRewriteRule(ModelNode operation, ModelNode model) {
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
if (!redirectRewritesRules.isDefined()) {
redirectRewritesRules = new ModelNode();
}
String redirectRewriteRuleName = redirectRewriteRule(operation);
if (!redirectRewriteRuleName.contains(".")) {
redirectRewritesRules.get(redirectRewriteRuleName).set(model.get("value").asString());
} else {
String[] parts = redirectRewriteRuleName.split("\\.");
String provider = parts[0];
String property = parts[1];
ModelNode redirectRewriteRule = redirectRewritesRules.get(provider);
if (!redirectRewriteRule.isDefined()) {
redirectRewriteRule = new ModelNode();
}
redirectRewriteRule.get(property).set(model.get("value").asString());
redirectRewritesRules.set(provider, redirectRewriteRule);
}
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME).set(redirectRewritesRules);
}
public void removeRedirectRewriteRule(ModelNode operation) {
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
if (!redirectRewritesRules.isDefined()) {
throw new RuntimeException("Can not remove redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
}
String ruleName = credentialNameFromOp(operation);
redirectRewritesRules.remove(ruleName);
}
public void updateRedirectRewriteRule(ModelNode operation, String attrName, ModelNode resolvedValue) {
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
if (!redirectRewritesRules.isDefined()) {
throw new RuntimeException("Can not update redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
}
String ruleName = credentialNameFromOp(operation);
redirectRewritesRules.get(ruleName).set(resolvedValue);
}
private ModelNode redirectRewriteRuleFromOp(ModelNode operation) {
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
return deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME);
}
private String realmNameFromOp(ModelNode operation) {
return valueFromOpAddress(RealmDefinition.TAG_NAME, operation);
@ -141,6 +193,10 @@ public final class KeycloakAdapterConfigService {
private String credentialNameFromOp(ModelNode operation) {
return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation);
}
private String redirectRewriteRule(ModelNode operation) {
return valueFromOpAddress(RedirecRewritetRuleDefinition.TAG_NAME, operation);
}
private String valueFromOpAddress(String addrElement, ModelNode operation) {
String deploymentName = getValueOfAddrElement(operation.get(ADDRESS), addrElement);

View file

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

View file

@ -96,12 +96,17 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
PathElement.pathElement(SecureDeploymentDefinition.TAG_NAME, name));
addSecureDeployment.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
List<ModelNode> credentialsToAdd = new ArrayList<ModelNode>();
List<ModelNode> redirectRulesToAdd = new ArrayList<ModelNode>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName();
if (tagName.equals(CredentialDefinition.TAG_NAME)) {
readCredential(reader, addr, credentialsToAdd);
continue;
}
if (tagName.equals(RedirecRewritetRuleDefinition.TAG_NAME)) {
readRewriteRule(reader, addr, redirectRulesToAdd);
continue;
}
SimpleAttributeDefinition def = SecureDeploymentDefinition.lookup(tagName);
if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + tagName);
@ -111,6 +116,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
// Must add credentials after the deployment is added.
resourcesToAdd.add(addSecureDeployment);
resourcesToAdd.addAll(credentialsToAdd);
resourcesToAdd.addAll(redirectRulesToAdd);
}
public void readCredential(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> credentialsToAdd) throws XMLStreamException {
@ -149,6 +155,43 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
}
}
}
public void readRewriteRule(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> rewriteRuleToToAdd) throws XMLStreamException {
String name = readNameAttribute(reader);
Map<String, String> values = new HashMap<>();
String textValue = null;
while (reader.hasNext()) {
int next = reader.next();
if (next == CHARACTERS) {
// text value of redirect rule element
String text = reader.getText();
if (text == null || text.trim().isEmpty()) {
continue;
}
textValue = text;
} else if (next == START_ELEMENT) {
String key = reader.getLocalName();
reader.next();
String value = reader.getText();
reader.next();
values.put(key, value);
} else if (next == END_ELEMENT) {
break;
}
}
if (textValue != null) {
ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name, textValue);
rewriteRuleToToAdd.add(addRedirectRule);
} else {
for (Map.Entry<String, String> entry : values.entrySet()) {
ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name + "." + entry.getKey(), entry.getValue());
rewriteRuleToToAdd.add(addRedirectRule);
}
}
}
private ModelNode getCredentialToAdd(PathAddress parent, String name, String value) {
ModelNode addCredential = new ModelNode();
@ -158,6 +201,15 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
addCredential.get(CredentialDefinition.VALUE.getName()).set(value);
return addCredential;
}
private ModelNode getRedirectRuleToAdd(PathAddress parent, String name, String value) {
ModelNode addRedirectRule = new ModelNode();
addRedirectRule.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD);
PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(RedirecRewritetRuleDefinition.TAG_NAME, name));
addRedirectRule.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
addRedirectRule.get(RedirecRewritetRuleDefinition.VALUE.getName()).set(value);
return addRedirectRule;
}
// expects that the current tag will have one single attribute called "name"
private String readNameAttribute(XMLExtendedStreamReader reader) throws XMLStreamException {
@ -219,6 +271,11 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
if (credentials.isDefined()) {
writeCredentials(writer, credentials);
}
ModelNode redirectRewriteRule = deploymentElements.get(RedirecRewritetRuleDefinition.TAG_NAME);
if (redirectRewriteRule.isDefined()) {
writeRedirectRules(writer, redirectRewriteRule);
}
writer.writeEndElement();
}
@ -265,6 +322,34 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writer.writeEndElement();
}
}
private void writeRedirectRules(XMLExtendedStreamWriter writer, ModelNode redirectRules) throws XMLStreamException {
Map<String, Object> parsed = new LinkedHashMap<>();
for (Property redirectRule : redirectRules.asPropertyList()) {
String ruleName = redirectRule.getName();
String ruleValue = redirectRule.getValue().get(RedirecRewritetRuleDefinition.VALUE.getName()).asString();
parsed.put(ruleName, ruleValue);
}
for (Map.Entry<String, Object> entry : parsed.entrySet()) {
writer.writeStartElement(RedirecRewritetRuleDefinition.TAG_NAME);
writer.writeAttribute("name", entry.getKey());
Object value = entry.getValue();
if (value instanceof String) {
writeCharacters(writer, (String) value);
} else {
Map<String, String> redirectRulesProps = (Map<String, String>) value;
for (Map.Entry<String, String> prop : redirectRulesProps.entrySet()) {
writer.writeStartElement(prop.getKey());
writeCharacters(writer, prop.getValue());
writer.writeEndElement();
}
}
writer.writeEndElement();
}
}
// code taken from org.jboss.as.controller.AttributeMarshaller
private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException {

View file

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

View file

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

View file

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

View file

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

View file

@ -65,6 +65,7 @@ keycloak.secure-deployment.connection-pool-size=Connection pool size for the cli
keycloak.secure-deployment.resource=Application name
keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token
keycloak.secure-deployment.credentials=Adapter credentials
keycloak.secure-deployment.redirect-rewrite-rule=Apply a rewrite rule for the redirect URI
keycloak.secure-deployment.bearer-only=Bearer Token Auth only
keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication
keycloak.secure-deployment.public-client=Public client
@ -94,4 +95,9 @@ keycloak.secure-deployment.credential=Credential value
keycloak.credential=Credential
keycloak.credential.value=Credential value
keycloak.credential.add=Credential add
keycloak.credential.remove=Credential remove
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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -36,7 +37,7 @@ import java.util.regex.Pattern;
*/
public class Encode
{
private static final String UTF_8 = "UTF-8";
private static final String UTF_8 = StandardCharsets.UTF_8.name();
private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter");
@ -84,9 +85,7 @@ public class Encode
case '@':
continue;
}
StringBuffer sb = new StringBuffer();
sb.append((char) i);
pathEncoding[i] = URLEncoder.encode(sb.toString());
pathEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
pathEncoding[' '] = "%20";
System.arraycopy(pathEncoding, 0, matrixParameterEncoding, 0, pathEncoding.length);
@ -119,9 +118,7 @@ public class Encode
queryNameValueEncoding[i] = "+";
continue;
}
StringBuffer sb = new StringBuffer();
sb.append((char) i);
queryNameValueEncoding[i] = URLEncoder.encode(sb.toString());
queryNameValueEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
/*
@ -159,9 +156,7 @@ public class Encode
queryStringEncoding[i] = "%20";
continue;
}
StringBuffer sb = new StringBuffer();
sb.append((char) i);
queryStringEncoding[i] = URLEncoder.encode(sb.toString());
queryStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i));
}
}
@ -194,7 +189,7 @@ public class Encode
*/
public static String encodeFragment(String value)
{
return encodeValue(value, queryNameValueEncoding);
return encodeValue(value, queryStringEncoding);
}
/**
@ -221,18 +216,19 @@ public class Encode
public static String decodePath(String path)
{
Matcher matcher = encodedCharsMulti.matcher(path);
StringBuffer buf = new StringBuffer();
int start=0;
StringBuilder builder = new StringBuilder();
CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
while (matcher.find())
{
builder.append(path, start, matcher.start());
decoder.reset();
String decoded = decodeBytes(matcher.group(1), decoder);
decoded = decoded.replace("\\", "\\\\");
decoded = decoded.replace("$", "\\$");
matcher.appendReplacement(buf, decoded);
builder.append(decoded);
start = matcher.end();
}
matcher.appendTail(buf);
return buf.toString();
builder.append(path, start, path.length());
return builder.toString();
}
private static String decodeBytes(String enc, CharsetDecoder decoder)
@ -264,7 +260,7 @@ public class Encode
public static String encodeNonCodes(String string)
{
Matcher matcher = nonCodes.matcher(string);
StringBuffer buf = new StringBuffer();
StringBuilder builder = new StringBuilder();
// FYI: we do not use the no-arg matcher.find()
@ -276,29 +272,32 @@ public class Encode
while (matcher.find(idx))
{
int start = matcher.start();
buf.append(string.substring(idx, start));
buf.append("%25");
builder.append(string.substring(idx, start));
builder.append("%25");
idx = start + 1;
}
buf.append(string.substring(idx));
return buf.toString();
builder.append(string.substring(idx));
return builder.toString();
}
private static boolean savePathParams(String segment, StringBuffer newSegment, List<String> params)
public static boolean savePathParams(String segment, StringBuilder newSegment, List<String> params)
{
boolean foundParam = false;
// Regular expressions can have '{' and '}' characters. Replace them to do match
segment = PathHelper.replaceEnclosedCurlyBraces(segment);
Matcher matcher = PathHelper.URI_TEMPLATE_PATTERN.matcher(segment);
int start = 0;
while (matcher.find())
{
newSegment.append(segment, start, matcher.start());
foundParam = true;
String group = matcher.group();
// Regular expressions can have '{' and '}' characters. Recover earlier replacement
params.add(PathHelper.recoverEnclosedCurlyBraces(group));
matcher.appendReplacement(newSegment, "_resteasy_uri_parameter");
newSegment.append("_resteasy_uri_parameter");
start = matcher.end();
}
matcher.appendTail(newSegment);
newSegment.append(segment, start, segment.length());
return foundParam;
}
@ -309,11 +308,11 @@ public class Encode
* @param encoding
* @return
*/
private static String encodeValue(String segment, String[] encoding)
public static String encodeValue(String segment, String[] encoding)
{
ArrayList<String> params = new ArrayList<String>();
boolean foundParam = false;
StringBuffer newSegment = new StringBuffer();
StringBuilder newSegment = new StringBuilder();
if (savePathParams(segment, newSegment, params))
{
foundParam = true;
@ -411,21 +410,21 @@ public class Encode
return encodeFromArray(nameOrValue, queryNameValueEncoding, true);
}
private static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
protected static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
{
StringBuffer result = new StringBuffer();
StringBuilder result = new StringBuilder();
for (int i = 0; i < segment.length(); i++)
{
if (!encodePercent && segment.charAt(i) == '%')
char currentChar = segment.charAt(i);
if (!encodePercent && currentChar == '%')
{
result.append(segment.charAt(i));
result.append(currentChar);
continue;
}
int idx = segment.charAt(i);
String encoding = encode(idx, encodingMap);
String encoding = encode(currentChar, encodingMap);
if (encoding == null)
{
result.append(segment.charAt(i));
result.append(currentChar);
}
else
{
@ -461,20 +460,20 @@ public class Encode
return encoded;
}
private static String pathParamReplacement(String segment, List<String> params)
public static String pathParamReplacement(String segment, List<String> params)
{
StringBuffer newSegment = new StringBuffer();
StringBuilder newSegment = new StringBuilder();
Matcher matcher = PARAM_REPLACEMENT.matcher(segment);
int i = 0;
int start = 0;
while (matcher.find())
{
newSegment.append(segment, start, matcher.start());
String replacement = params.get(i++);
// double encode slashes, so that slashes stay where they are
replacement = replacement.replace("\\", "\\\\");
replacement = replacement.replace("$", "\\$");
matcher.appendReplacement(newSegment, replacement);
newSegment.append(replacement);
start = matcher.end();
}
matcher.appendTail(newSegment);
newSegment.append(segment, start, segment.length());
segment = newSegment.toString();
return segment;
}
@ -505,6 +504,38 @@ public class Encode
}
return decoded;
}
/**
* decode an encoded map
*
* @param map
* @param charset
* @return
*/
public static MultivaluedHashMap<String, String> decode(MultivaluedHashMap<String, String> map, String charset)
{
if (charset == null)
{
charset = UTF_8;
}
MultivaluedHashMap<String, String> decoded = new MultivaluedHashMap<String, String>();
for (Map.Entry<String, List<String>> entry : map.entrySet())
{
List<String> values = entry.getValue();
for (String value : values)
{
try
{
decoded.add(URLDecoder.decode(entry.getKey(), charset), URLDecoder.decode(value, charset));
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
}
}
return decoded;
}
public static MultivaluedHashMap<String, String> encode(MultivaluedHashMap<String, String> map)
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

@ -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();

View file

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

View file

@ -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()

View file

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

View file

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

View file

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

View file

@ -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();

View file

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

View file

@ -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
*

View file

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

View file

@ -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
*/

View file

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

View file

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

View file

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

View file

@ -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()) {

View file

@ -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)) {

View file

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

View file

@ -53,4 +53,8 @@ public interface ProviderFactory<T extends Provider> {
public String getId();
default int order() {
return 0;
}
}

View file

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

View file

@ -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());

View file

@ -20,7 +20,6 @@ package org.keycloak.email;
import com.sun.mail.smtp.SMTPMessage;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.truststore.HostnameVerificationPolicy;
@ -57,20 +56,22 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
@Override
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
Transport transport = null;
try {
String address = retrieveEmailAddress(user);
Map<String, String> config = realm.getSmtpConfig();
Properties props = new Properties();
props.setProperty("mail.smtp.host", config.get("host"));
if (config.containsKey("host")) {
props.setProperty("mail.smtp.host", config.get("host"));
}
boolean auth = "true".equals(config.get("auth"));
boolean ssl = "true".equals(config.get("ssl"));
boolean starttls = "true".equals(config.get("starttls"));
if (config.containsKey("port")) {
if (config.containsKey("port") && config.get("port") != null) {
props.setProperty("mail.smtp.port", config.get("port"));
}
@ -103,13 +104,13 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
Multipart multipart = new MimeMultipart("alternative");
if(textBody != null) {
if (textBody != null) {
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(textBody, "UTF-8");
multipart.addBodyPart(textPart);
}
if(htmlBody != null) {
if (htmlBody != null) {
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlBody, "text/html; charset=UTF-8");
multipart.addBodyPart(htmlPart);
@ -153,13 +154,16 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
}
protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException {
protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, EmailException {
if (email == null || "".equals(email.trim())) {
throw new EmailException("Please provide a valid address", null);
}
if (displayName == null || "".equals(displayName.trim())) {
return new InternetAddress(email);
}
return new InternetAddress(email, displayName, "utf-8");
}
protected String retrieveEmailAddress(UserModel user) {
return user.getEmail();
}

View file

@ -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);
@ -168,27 +181,39 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
String textTemplate = String.format("text/%s", template);
String textBody;
try {
textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
} catch (final FreeMarkerException e ) {
textBody = null;
textBody = null;
}
String htmlTemplate = String.format("html/%s", template);
String htmlBody;
try {
htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
} catch (final FreeMarkerException e ) {
htmlBody = null;
htmlBody = null;
}
send(subject, textBody, htmlBody);
return new EmailTemplate(subject, textBody, htmlBody);
} catch (Exception e) {
throw new EmailException("Failed to template email", e);
}
}
private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
try {
EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
} catch (Exception e) {
throw new EmailException("Failed to template email", e);
}
}
private void send(String subject, String textBody, String htmlBody) throws EmailException {
send(realm.getSmtpConfig(), subject, textBody, htmlBody);
}
private void send(Map<String, String> config, String subject, String textBody, String htmlBody) throws EmailException {
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
emailSender.send(realm, user, subject, textBody, htmlBody);
emailSender.send(config, user, subject, textBody, htmlBody);
}
@Override
@ -203,4 +228,29 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
return sb.toString();
}
private class EmailTemplate {
private String subject;
private String textBody;
private String htmlBody;
public EmailTemplate(String subject, String textBody, String htmlBody) {
this.subject = subject;
this.textBody = textBody;
this.htmlBody = htmlBody;
}
public String getSubject() {
return subject;
}
public String getTextBody() {
return textBody;
}
public String getHtmlBody() {
return htmlBody;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -49,7 +49,6 @@ import org.keycloak.util.TokenUtil;
import javax.ws.rs.GET;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -169,21 +168,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return this;
}
private void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
event.error(Errors.SSL_REQUIRED);
throw new ErrorPageException(session, Messages.HTTPS_REQUIRED);
}
}
private void checkRealm() {
if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED);
throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
}
}
private void checkClient(String clientId) {
if (clientId == null) {
event.error(Errors.INVALID_REQUEST);
@ -288,24 +272,24 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private Response checkPKCEParams() {
String codeChallenge = request.getCodeChallenge();
String codeChallengeMethod = request.getCodeChallengeMethod();
// PKCE not adopted to OAuth2 Implicit Grant and OIDC Implicit Flow,
// adopted to OAuth2 Authorization Code Grant and OIDC Authorization Code Flow, Hybrid Flow
// Namely, flows using authorization code.
if (parsedResponseType.isImplicitFlow()) return null;
if (codeChallenge == null && codeChallengeMethod != null) {
logger.info("PKCE supporting Client without code challenge");
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge");
}
// based on code_challenge value decide whether this client(RP) supports PKCE
if (codeChallenge == null) {
logger.debug("PKCE non-supporting Client");
return null;
}
if (codeChallengeMethod != null) {
// https://tools.ietf.org/html/rfc7636#section-4.2
// plain or S256
@ -319,13 +303,13 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
// default code_challenge_method is plane
codeChallengeMethod = OIDCLoginProtocol.PKCE_METHOD_PLAIN;
}
if (!isValidPkceCodeChallenge(codeChallenge)) {
logger.infof("PKCE supporting Client with invalid code challenge specified in PKCE, codeChallenge = %s", codeChallenge);
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge");
}
return null;
}

View file

@ -258,6 +258,7 @@ public class TokenEndpoint {
}
event.user(userSession.getUser());
event.session(userSession.getId());
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);

View file

@ -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,

View file

@ -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 {

View file

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

View file

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

View file

@ -1,182 +1,122 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.saml.profile.ecp.authenticator;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.common.util.Base64;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class HttpBasicAuthenticator implements AuthenticatorFactory {
public class HttpBasicAuthenticator implements Authenticator {
public static final String PROVIDER_ID = "http-basic-authenticator";
private static final String BASIC = "Basic";
private static final String BASIC_PREFIX = BASIC + " ";
@Override
public String getDisplayType() {
return "HTTP Basic Authentication";
public void authenticate(final AuthenticationFlowContext context) {
final HttpRequest httpRequest = context.getHttpRequest();
final HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
final String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
context.attempted();
if (usernameAndPassword != null) {
final RealmModel realm = context.getRealm();
final String username = usernameAndPassword[0];
final UserModel user = context.getSession().users().getUserByUsername(username, realm);
if (user != null) {
final String password = usernameAndPassword[1];
final boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
if (valid) {
if (user.isEnabled()) {
userSuccessAction(context, user);
} else {
userDisabledAction(context, realm, user);
}
} else {
notValidCredentialsAction(context, realm, user);
}
} else {
nullUserAction(context, realm, username);
}
}
}
protected void userSuccessAction(AuthenticationFlowContext context, UserModel user) {
context.getAuthenticationSession().setAuthenticatedUser(user);
context.success();
}
protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) {
userSuccessAction(context, user);
}
protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String user) {
// no-op by default
}
protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) {
context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
.build());
}
private String[] getUsernameAndPassword(final HttpHeaders httpHeaders) {
final List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
if (authHeaders == null || authHeaders.size() == 0) {
return null;
}
String credentials = null;
for (final String authHeader : authHeaders) {
if (authHeader.startsWith(BASIC_PREFIX)) {
final String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) return null;
credentials = split[1];
}
}
try {
return new String(Base64.decode(credentials)).split(":");
} catch (final IOException e) {
throw new RuntimeException("Failed to parse credentials.", e);
}
}
@Override
public String getReferenceCategory() {
return null;
public void action(final AuthenticationFlowContext context) {
}
@Override
public boolean isConfigurable() {
public boolean requiresUser() {
return false;
}
@Override
public Requirement[] getRequirementChoices() {
return new Requirement[0];
}
@Override
public boolean isUserSetupAllowed() {
public boolean configuredFor(final KeycloakSession session, final RealmModel realm, final UserModel user) {
return false;
}
@Override
public String getHelpText() {
return "Validates username and password from Authorization HTTP header";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}
@Override
public Authenticator create(KeycloakSession session) {
return new Authenticator() {
private static final String BASIC = "Basic";
private static final String BASIC_PREFIX = BASIC + " ";
@Override
public void authenticate(AuthenticationFlowContext context) {
HttpRequest httpRequest = context.getHttpRequest();
HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
context.attempted();
if (usernameAndPassword != null) {
RealmModel realm = context.getRealm();
UserModel user = context.getSession().users().getUserByUsername(usernameAndPassword[0], realm);
if (user != null) {
String password = usernameAndPassword[1];
boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
if (valid) {
context.getAuthenticationSession().setAuthenticatedUser(user);
context.success();
} else {
context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
.build());
}
}
}
}
private String[] getUsernameAndPassword(HttpHeaders httpHeaders) {
List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
if (authHeaders == null || authHeaders.size() == 0) {
return null;
}
String credentials = null;
for (String authHeader : authHeaders) {
if (authHeader.startsWith(BASIC_PREFIX)) {
String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) return null;
credentials = split[1];
}
}
try {
return new String(Base64.decode(credentials)).split(":");
} catch (IOException e) {
throw new RuntimeException("Failed to parse credentials.", e);
}
}
@Override
public void action(AuthenticationFlowContext context) {
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
};
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
public void setRequiredActions(final KeycloakSession session, final RealmModel realm, final UserModel user) {
}
@ -184,9 +124,4 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory {
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
*/

View file

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

View file

@ -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()) {

View file

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

View 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.

View file

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

View file

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

View file

@ -16,4 +16,5 @@
#
org.keycloak.protocol.oidc.OIDCLoginProtocolFactory
org.keycloak.protocol.saml.SamlProtocolFactory
org.keycloak.protocol.saml.SamlProtocolFactory
org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory

View file

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

View file

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

View file

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

View 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

View file

@ -61,7 +61,7 @@ More info: http://javahowto.blogspot.cz/2010/09/java-agentlibjdwp-for-attaching.
Analogically, there is the same behaviour for JBoss based app server as for auth server. The default port is set to 5006. There are app server properties.
-Dapp.server.debug.port=$PORT
-Dapp.server.debug.suspend=y
-Dapp.server.debug.suspend=y
## Testsuite logging
@ -262,6 +262,8 @@ The UI tests are focused on the Admin Console as well as on some login scenarios
The tests also use some constants placed in [test-constants.properties](tests/base/src/test/resources/test-constants.properties). A different file can be specified by `-Dtestsuite.constants=path/to/different-test-constants.properties`
In case a custom `settings.xml` is used for Maven, you need to specify it also in `-Dkie.maven.settings.custom=path/to/settings.xml`.
#### Execution example
```
mvn -f testsuite/integration-arquillian/tests/other/console/pom.xml \
@ -452,7 +454,7 @@ First compile the Infinispan/JDG test server via the following command:
`mvn -Pcache-server-infinispan -f testsuite/integration-arquillian -DskipTests clean install`
or
`mvn -Pcache-server-jdg -f testsuite/integration-arquillian -DskipTests clean install`
Then you can run the tests using the following command (adjust the test specification according to your needs):
@ -464,3 +466,103 @@ or
`mvn -Pcache-server-jdg -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test`
_Someone using IntelliJ IDEA, please describe steps for that IDE_
## Run Docker Authentication test
First, validate that your machine has a valid docker installation and that it is available to the JVM running the test.
The exact steps to configure Docker depend on the operating system.
By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand.
The exact command line arguments depend on the operating system.
### General guidelines
If docker daemon doesn't run locally, or if you're not running on Linux, you may need
to determine the IP of the bridge interface or local interface that Docker daemon can use to connect to Keycloak Server.
Then specify that IP as additional system property called *host.ip*, for example:
-Dhost.ip=192.168.64.1
If using Docker for Mac, you can create an alias for your local network interface:
sudo ifconfig lo0 alias 10.200.10.1/24
Then pass the IP as *host.ip*:
-Dhost.ip=10.200.10.1
If you're running a Docker fork that always lists a host component of an image on `docker images` (e.g. Fedora / RHEL Docker)
use `-Ddocker.io-prefix-explicit=true` argument when running the test.
### Fedora
On Fedora one way to set up Docker server is the following:
# install docker
sudo dnf install docker
# configure docker
# remove --selinux-enabled from OPTIONS
sudo vi /etc/sysconfig/docker
# create docker group and add your user (so docker wouldn't need root permissions)
sudo groupadd docker && sudo gpasswd -a ${USER} docker && sudo systemctl restart docker
newgrp docker
# you need to login again after this
# make sure Docker is available
docker pull registry:2
You may also need to add an iptables rule to allow container to host traffic
sudo iptables -I INPUT -i docker0 -j ACCEPT
Then, run the test passing `-Ddocker.io-prefix-explicit=true`:
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
clean test \
-Dtest=DockerClientTest \
-Dkeycloak.profile.feature.docker=enabled \
-Ddocker.io-prefix-explicit=true
### macOS
On macOS all you need to do is install Docker for Mac, start it up, and check that it works:
# make sure Docker is available
docker pull registry:2
Be especially careful to restart Docker server after every sleep / suspend to ensure system clock of Docker VM is synchronized with
that of the host operating system - Docker for Mac runs inside a VM.
Then, run the test passing `-Dhost.ip=IP` where IP corresponds to en0 interface or an alias for localhost:
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
clean test \
-Dtest=DockerClientTest \
-Dkeycloak.profile.feature.docker=enabled \
-Dhost.ip=10.200.10.1
### Running Docker test against Keycloak Server distribution
Make sure to build the distribution:
mvn clean install -f distribution
Then, before running the test, setup Keycloak Server distribution for the tests:
mvn -f testsuite/integration-arquillian/servers/pom.xml \
clean install \
-Pauth-server-wildfly
When running the test, add the following arguments to the command line:
-Pauth-server-wildfly -Pauth-server-enable-disable-feature -Dfeature.name=docker -Dfeature.value=enabled

View file

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

View file

@ -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="[&quot;http=${{auth.server.http.port}}&quot;,&quot;https=${{auth.server.https.port}}&quot;]"/>
</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>

View file

@ -100,7 +100,6 @@
<outputDirectory>${project.build.directory}/unpacked</outputDirectory>
</artifactItem>
</artifactItems>
<excludes>**/product.conf</excludes>
</configuration>
</execution>
<execution>

View file

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