Merge pull request #3472 from hmlnarik/KEYCLOAK-1881-saml-key-rotation
Keycloak 1881 - SAML key/cert rotation for IdP
This commit is contained in:
commit
292777259e
121 changed files with 4327 additions and 446 deletions
|
@ -38,7 +38,7 @@ import org.apache.http.params.BasicHttpParams;
|
||||||
import org.apache.http.params.HttpConnectionParams;
|
import org.apache.http.params.HttpConnectionParams;
|
||||||
import org.keycloak.common.util.EnvUtil;
|
import org.keycloak.common.util.EnvUtil;
|
||||||
import org.keycloak.common.util.KeystoreUtil;
|
import org.keycloak.common.util.KeystoreUtil;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.keycloak.representations.adapters.config.AdapterHttpClientConfig;
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
|
@ -333,7 +333,7 @@ public class HttpClientBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpClient build(AdapterConfig adapterConfig) {
|
public HttpClient build(AdapterHttpClientConfig adapterConfig) {
|
||||||
disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
|
disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
|
||||||
|
|
||||||
String truststorePath = adapterConfig.getTruststore();
|
String truststorePath = adapterConfig.getTruststore();
|
||||||
|
@ -379,13 +379,13 @@ public class HttpClientBuilder {
|
||||||
/**
|
/**
|
||||||
* Configures a the proxy to use for auth-server requests if provided.
|
* Configures a the proxy to use for auth-server requests if provided.
|
||||||
* <p>
|
* <p>
|
||||||
* If the given {@link AdapterConfig} contains the attribute {@code proxy-url} we use the
|
* If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the
|
||||||
* given URL as a proxy server, otherwise the proxy configuration is ignored.
|
* given URL as a proxy server, otherwise the proxy configuration is ignored.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param adapterConfig
|
* @param adapterConfig
|
||||||
*/
|
*/
|
||||||
private void configureProxyForAuthServerIfProvided(AdapterConfig adapterConfig) {
|
private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) {
|
||||||
|
|
||||||
if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
|
if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
|
0
adapters/saml/core/nbproject/project.properties
Normal file
0
adapters/saml/core/nbproject/project.properties
Normal file
|
@ -34,6 +34,7 @@
|
||||||
<timestamp>${maven.build.timestamp}</timestamp>
|
<timestamp>${maven.build.timestamp}</timestamp>
|
||||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
|
@ -70,6 +71,11 @@
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.cloned;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options relevant for configuring http client that can be used by adapter.
|
||||||
|
*
|
||||||
|
* NOTE: keep in sync with core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java until unified.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public interface AdapterHttpClientConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns truststore filename.
|
||||||
|
*/
|
||||||
|
public String getTruststore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns truststore password.
|
||||||
|
*/
|
||||||
|
public String getTruststorePassword();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns keystore with client keys.
|
||||||
|
*/
|
||||||
|
public String getClientKeystore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns keystore password.
|
||||||
|
*/
|
||||||
|
public String getClientKeystorePassword();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns boolean flag whether any hostname verification is done on the server's
|
||||||
|
* certificate, {@code true} means that verification is not done.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isAllowAnyHostname();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns boolean flag whether any trust management and hostname verification is done.
|
||||||
|
* <p>
|
||||||
|
* <i>NOTE</i> Disabling trust manager is a security hole, so only set this option
|
||||||
|
* if you cannot or do not want to verify the identity of the
|
||||||
|
* host you are communicating with.
|
||||||
|
*/
|
||||||
|
public boolean isDisableTrustManager();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns size of connection pool.
|
||||||
|
*/
|
||||||
|
public int getConnectionPoolSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns URL of HTTP proxy.
|
||||||
|
*/
|
||||||
|
public String getProxyUrl();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.cloned;
|
||||||
|
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||||
|
import org.apache.http.HttpStatus;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.keycloak.adapters.saml.descriptor.parsers.SamlDescriptorIDPKeysExtractor;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
|
||||||
|
*/
|
||||||
|
public class HttpAdapterUtils {
|
||||||
|
|
||||||
|
public static MultivaluedHashMap<String, KeyInfo> downloadKeysFromSamlDescriptor(HttpClient client, String descriptorUrl) throws HttpClientAdapterException {
|
||||||
|
try {
|
||||||
|
HttpGet httpRequest = new HttpGet(descriptorUrl);
|
||||||
|
HttpResponse response = client.execute(httpRequest);
|
||||||
|
int status = response.getStatusLine().getStatusCode();
|
||||||
|
if (status != HttpStatus.SC_OK) {
|
||||||
|
EntityUtils.consumeQuietly(response.getEntity());
|
||||||
|
throw new HttpClientAdapterException("Unexpected status = " + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
if (entity == null) {
|
||||||
|
throw new HttpClientAdapterException("There was no entity.");
|
||||||
|
}
|
||||||
|
|
||||||
|
MultivaluedHashMap<String, KeyInfo> res;
|
||||||
|
try (InputStream is = entity.getContent()) {
|
||||||
|
res = extractKeysFromSamlDescriptor(is);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityUtils.consumeQuietly(entity);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (IOException | ParsingException e) {
|
||||||
|
throw new HttpClientAdapterException("IO error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses SAML descriptor and extracts keys from it.
|
||||||
|
* @param xmlStream
|
||||||
|
* @return List of KeyInfo objects containing keys from the descriptor.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static MultivaluedHashMap<String, KeyInfo> extractKeysFromSamlDescriptor(InputStream xmlStream) throws ParsingException {
|
||||||
|
Object res = new SamlDescriptorIDPKeysExtractor().parse(xmlStream);
|
||||||
|
return (MultivaluedHashMap<String, KeyInfo>) res;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.cloned;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class HttpClientAdapterException extends Exception {
|
||||||
|
|
||||||
|
public HttpClientAdapterException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientAdapterException(String message, Throwable t) {
|
||||||
|
super(message, t);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,396 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.cloned;
|
||||||
|
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.apache.http.client.CookieStore;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.conn.ClientConnectionManager;
|
||||||
|
import org.apache.http.conn.params.ConnRoutePNames;
|
||||||
|
import org.apache.http.conn.scheme.PlainSocketFactory;
|
||||||
|
import org.apache.http.conn.scheme.Scheme;
|
||||||
|
import org.apache.http.conn.scheme.SchemeRegistry;
|
||||||
|
import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
|
||||||
|
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
|
||||||
|
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||||
|
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||||
|
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||||
|
import org.apache.http.cookie.Cookie;
|
||||||
|
import org.apache.http.impl.client.DefaultHttpClient;
|
||||||
|
import org.apache.http.impl.conn.SingleClientConnManager;
|
||||||
|
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
||||||
|
import org.apache.http.params.BasicHttpParams;
|
||||||
|
import org.apache.http.params.HttpConnectionParams;
|
||||||
|
import org.keycloak.common.util.EnvUtil;
|
||||||
|
import org.keycloak.common.util.KeystoreUtil;
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction for creating HttpClients. Allows SSL configuration.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class HttpClientBuilder {
|
||||||
|
public static enum HostnameVerificationPolicy {
|
||||||
|
/**
|
||||||
|
* Hostname verification is not done on the server's certificate
|
||||||
|
*/
|
||||||
|
ANY,
|
||||||
|
/**
|
||||||
|
* Allows wildcards in subdomain names i.e. *.foo.com
|
||||||
|
*/
|
||||||
|
WILDCARD,
|
||||||
|
/**
|
||||||
|
* CN must match hostname connecting to
|
||||||
|
*/
|
||||||
|
STRICT
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
private static class PassthroughTrustManager implements X509TrustManager {
|
||||||
|
public void checkClientTrusted(X509Certificate[] chain,
|
||||||
|
String authType) throws CertificateException {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkServerTrusted(X509Certificate[] chain,
|
||||||
|
String authType) throws CertificateException {
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeyStore truststore;
|
||||||
|
protected KeyStore clientKeyStore;
|
||||||
|
protected String clientPrivateKeyPassword;
|
||||||
|
protected boolean disableTrustManager;
|
||||||
|
protected boolean disableCookieCache = true;
|
||||||
|
protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD;
|
||||||
|
protected SSLContext sslContext;
|
||||||
|
protected int connectionPoolSize = 100;
|
||||||
|
protected int maxPooledPerRoute = 0;
|
||||||
|
protected long connectionTTL = -1;
|
||||||
|
protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
|
||||||
|
protected HostnameVerifier verifier = null;
|
||||||
|
protected long socketTimeout = -1;
|
||||||
|
protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS;
|
||||||
|
protected long establishConnectionTimeout = -1;
|
||||||
|
protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
|
||||||
|
protected HttpHost proxyHost;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket inactivity timeout
|
||||||
|
*
|
||||||
|
* @param timeout
|
||||||
|
* @param unit
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public HttpClientBuilder socketTimeout(long timeout, TimeUnit unit) {
|
||||||
|
this.socketTimeout = timeout;
|
||||||
|
this.socketTimeoutUnits = unit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When trying to make an initial socket connection, what is the timeout?
|
||||||
|
*
|
||||||
|
* @param timeout
|
||||||
|
* @param unit
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public HttpClientBuilder establishConnectionTimeout(long timeout, TimeUnit unit) {
|
||||||
|
this.establishConnectionTimeout = timeout;
|
||||||
|
this.establishConnectionTimeoutUnits = unit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder connectionTTL(long ttl, TimeUnit unit) {
|
||||||
|
this.connectionTTL = ttl;
|
||||||
|
this.connectionTTLUnit = unit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder maxPooledPerRoute(int maxPooledPerRoute) {
|
||||||
|
this.maxPooledPerRoute = maxPooledPerRoute;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder connectionPoolSize(int connectionPoolSize) {
|
||||||
|
this.connectionPoolSize = connectionPoolSize;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable trust management and hostname verification. <i>NOTE</i> this is a security
|
||||||
|
* hole, so only set this option if you cannot or do not want to verify the identity of the
|
||||||
|
* host you are communicating with.
|
||||||
|
*/
|
||||||
|
public HttpClientBuilder disableTrustManager() {
|
||||||
|
this.disableTrustManager = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder disableCookieCache() {
|
||||||
|
this.disableCookieCache = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSL policy used to verify hostnames
|
||||||
|
*
|
||||||
|
* @param policy
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public HttpClientBuilder hostnameVerification(HostnameVerificationPolicy policy) {
|
||||||
|
this.policy = policy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public HttpClientBuilder sslContext(SSLContext sslContext) {
|
||||||
|
this.sslContext = sslContext;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder trustStore(KeyStore truststore) {
|
||||||
|
this.truststore = truststore;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder keyStore(KeyStore keyStore, String password) {
|
||||||
|
this.clientKeyStore = keyStore;
|
||||||
|
this.clientPrivateKeyPassword = password;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder keyStore(KeyStore keyStore, char[] password) {
|
||||||
|
this.clientKeyStore = keyStore;
|
||||||
|
this.clientPrivateKeyPassword = new String(password);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class VerifierWrapper implements X509HostnameVerifier {
|
||||||
|
protected HostnameVerifier verifier;
|
||||||
|
|
||||||
|
VerifierWrapper(HostnameVerifier verifier) {
|
||||||
|
this.verifier = verifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(String host, SSLSocket ssl) throws IOException {
|
||||||
|
if (!verifier.verify(host, ssl.getSession())) throw new SSLException("Hostname verification failure");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(String host, X509Certificate cert) throws SSLException {
|
||||||
|
throw new SSLException("This verification path not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
|
||||||
|
throw new SSLException("This verification path not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verify(String s, SSLSession sslSession) {
|
||||||
|
return verifier.verify(s, sslSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient build() {
|
||||||
|
X509HostnameVerifier verifier = null;
|
||||||
|
if (this.verifier != null) verifier = new VerifierWrapper(this.verifier);
|
||||||
|
else {
|
||||||
|
switch (policy) {
|
||||||
|
case ANY:
|
||||||
|
verifier = new AllowAllHostnameVerifier();
|
||||||
|
break;
|
||||||
|
case WILDCARD:
|
||||||
|
verifier = new BrowserCompatHostnameVerifier();
|
||||||
|
break;
|
||||||
|
case STRICT:
|
||||||
|
verifier = new StrictHostnameVerifier();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
SSLSocketFactory sslsf = null;
|
||||||
|
SSLContext theContext = sslContext;
|
||||||
|
if (disableTrustManager) {
|
||||||
|
theContext = SSLContext.getInstance("SSL");
|
||||||
|
theContext.init(null, new TrustManager[]{new PassthroughTrustManager()},
|
||||||
|
new SecureRandom());
|
||||||
|
verifier = new AllowAllHostnameVerifier();
|
||||||
|
sslsf = new SniSSLSocketFactory(theContext, verifier);
|
||||||
|
} else if (theContext != null) {
|
||||||
|
sslsf = new SniSSLSocketFactory(theContext, verifier);
|
||||||
|
} else if (clientKeyStore != null || truststore != null) {
|
||||||
|
sslsf = new SniSSLSocketFactory(SSLSocketFactory.TLS, clientKeyStore, clientPrivateKeyPassword, truststore, null, verifier);
|
||||||
|
} else {
|
||||||
|
final SSLContext tlsContext = SSLContext.getInstance(SSLSocketFactory.TLS);
|
||||||
|
tlsContext.init(null, null, null);
|
||||||
|
sslsf = new SniSSLSocketFactory(tlsContext, verifier);
|
||||||
|
}
|
||||||
|
SchemeRegistry registry = new SchemeRegistry();
|
||||||
|
registry.register(
|
||||||
|
new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
|
||||||
|
Scheme httpsScheme = new Scheme("https", 443, sslsf);
|
||||||
|
registry.register(httpsScheme);
|
||||||
|
ClientConnectionManager cm = null;
|
||||||
|
if (connectionPoolSize > 0) {
|
||||||
|
ThreadSafeClientConnManager tcm = new ThreadSafeClientConnManager(registry, connectionTTL, connectionTTLUnit);
|
||||||
|
tcm.setMaxTotal(connectionPoolSize);
|
||||||
|
if (maxPooledPerRoute == 0) maxPooledPerRoute = connectionPoolSize;
|
||||||
|
tcm.setDefaultMaxPerRoute(maxPooledPerRoute);
|
||||||
|
cm = tcm;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cm = new SingleClientConnManager(registry);
|
||||||
|
}
|
||||||
|
BasicHttpParams params = new BasicHttpParams();
|
||||||
|
|
||||||
|
if (proxyHost != null) {
|
||||||
|
params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socketTimeout > -1) {
|
||||||
|
HttpConnectionParams.setSoTimeout(params, (int) socketTimeoutUnits.toMillis(socketTimeout));
|
||||||
|
|
||||||
|
}
|
||||||
|
if (establishConnectionTimeout > -1) {
|
||||||
|
HttpConnectionParams.setConnectionTimeout(params, (int) establishConnectionTimeoutUnits.toMillis(establishConnectionTimeout));
|
||||||
|
}
|
||||||
|
DefaultHttpClient client = new DefaultHttpClient(cm, params);
|
||||||
|
|
||||||
|
if (disableCookieCache) {
|
||||||
|
client.setCookieStore(new CookieStore() {
|
||||||
|
@Override
|
||||||
|
public void addCookie(Cookie cookie) {
|
||||||
|
//To change body of implemented methods use File | Settings | File Templates.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Cookie> getCookies() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean clearExpired(Date date) {
|
||||||
|
return false; //To change body of implemented methods use File | Settings | File Templates.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
//To change body of implemented methods use File | Settings | File Templates.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient build(AdapterHttpClientConfig adapterConfig) {
|
||||||
|
disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
|
||||||
|
|
||||||
|
String truststorePath = adapterConfig.getTruststore();
|
||||||
|
if (truststorePath != null) {
|
||||||
|
truststorePath = EnvUtil.replace(truststorePath);
|
||||||
|
String truststorePassword = adapterConfig.getTruststorePassword();
|
||||||
|
try {
|
||||||
|
this.truststore = KeystoreUtil.loadKeyStore(truststorePath, truststorePassword);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to load truststore", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String clientKeystore = adapterConfig.getClientKeystore();
|
||||||
|
if (clientKeystore != null) {
|
||||||
|
clientKeystore = EnvUtil.replace(clientKeystore);
|
||||||
|
String clientKeystorePassword = adapterConfig.getClientKeystorePassword();
|
||||||
|
try {
|
||||||
|
KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword);
|
||||||
|
keyStore(clientCertKeystore, clientKeystorePassword);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to load keystore", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int size = 10;
|
||||||
|
if (adapterConfig.getConnectionPoolSize() > 0)
|
||||||
|
size = adapterConfig.getConnectionPoolSize();
|
||||||
|
HttpClientBuilder.HostnameVerificationPolicy policy = HttpClientBuilder.HostnameVerificationPolicy.WILDCARD;
|
||||||
|
if (adapterConfig.isAllowAnyHostname())
|
||||||
|
policy = HttpClientBuilder.HostnameVerificationPolicy.ANY;
|
||||||
|
connectionPoolSize(size);
|
||||||
|
hostnameVerification(policy);
|
||||||
|
if (adapterConfig.isDisableTrustManager()) {
|
||||||
|
disableTrustManager();
|
||||||
|
} else {
|
||||||
|
trustStore(truststore);
|
||||||
|
}
|
||||||
|
|
||||||
|
configureProxyForAuthServerIfProvided(adapterConfig);
|
||||||
|
|
||||||
|
return build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures a the proxy to use for auth-server requests if provided.
|
||||||
|
* <p>
|
||||||
|
* If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the
|
||||||
|
* given URL as a proxy server, otherwise the proxy configuration is ignored.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param adapterConfig
|
||||||
|
*/
|
||||||
|
private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) {
|
||||||
|
|
||||||
|
if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
URI uri = URI.create(adapterConfig.getProxyUrl());
|
||||||
|
this.proxyHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.cloned;
|
||||||
|
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.apache.http.conn.scheme.HostNameResolver;
|
||||||
|
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||||
|
import org.apache.http.conn.ssl.TrustStrategy;
|
||||||
|
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||||
|
import org.apache.http.protocol.HttpContext;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.security.AccessController;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivilegedActionException;
|
||||||
|
import java.security.PrivilegedExceptionAction;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.UnrecoverableKeyException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSLSocketFactory that uses Server Name Indication (SNI) TLS extension.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Originally copied from <b>keycloak-adapter-core</b> project.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
* @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
|
||||||
|
*/
|
||||||
|
public class SniSSLSocketFactory extends SSLSocketFactory {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(SniSSLSocketFactory.class.getName());
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, HostNameResolver nameResolver) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||||
|
super(algorithm, keystore, keyPassword, truststore, random, nameResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||||
|
super(algorithm, keystore, keyPassword, truststore, random, trustStrategy, hostnameVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||||
|
super(algorithm, keystore, keyPassword, truststore, random, hostnameVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(KeyStore keystore, String keystorePassword, KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||||
|
super(keystore, keystorePassword, truststore);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(KeyStore keystore, String keystorePassword) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||||
|
super(keystore, keystorePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||||
|
super(truststore);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||||
|
super(trustStrategy, hostnameVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(TrustStrategy trustStrategy) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||||
|
super(trustStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(SSLContext sslContext) {
|
||||||
|
super(sslContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(SSLContext sslContext, HostNameResolver nameResolver) {
|
||||||
|
super(sslContext, nameResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(SSLContext sslContext, X509HostnameVerifier hostnameVerifier) {
|
||||||
|
super(sslContext, hostnameVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(SSLContext sslContext, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) {
|
||||||
|
super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier) {
|
||||||
|
super(socketfactory, hostnameVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) {
|
||||||
|
super(socketfactory, supportedProtocols, supportedCipherSuites, hostnameVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException {
|
||||||
|
return super.connectSocket(connectTimeout, applySNI(socket, host.getHostName()), host, remoteAddress, localAddress, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createLayeredSocket(Socket socket, String target, int port, HttpContext context) throws IOException {
|
||||||
|
return super.createLayeredSocket(applySNI(socket, target), target, port, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Socket applySNI(final Socket socket, String hostname) {
|
||||||
|
if (socket instanceof SSLSocket) {
|
||||||
|
try {
|
||||||
|
Method setHostMethod = AccessController.doPrivileged(new PrivilegedExceptionAction<Method>() {
|
||||||
|
@Override
|
||||||
|
public Method run() throws NoSuchMethodException {
|
||||||
|
return socket.getClass().getMethod("setHost", String.class);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setHostMethod.invoke(socket, hostname);
|
||||||
|
LOG.log(Level.FINEST, "Applied SNI to socket for host {0}", hostname);
|
||||||
|
} catch (PrivilegedActionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||||
|
LOG.log(Level.WARNING, "Failed to apply SNI to SSLSocket", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,7 +79,9 @@ public abstract class AbstractInitiateLogin implements AuthChallenge {
|
||||||
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.signWith(keypair);
|
binding.signWith(null, keypair);
|
||||||
|
// TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
|
||||||
|
// <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
|
||||||
binding.signDocument();
|
binding.signDocument();
|
||||||
}
|
}
|
||||||
return binding;
|
return binding;
|
||||||
|
|
|
@ -23,7 +23,14 @@ import org.keycloak.saml.SignatureAlgorithm;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.keycloak.adapters.saml.rotation.SamlDescriptorPublicKeyLocator;
|
||||||
|
import org.keycloak.rotation.CompositeKeyLocator;
|
||||||
|
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -179,10 +186,15 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
|
|
||||||
public static class DefaultIDP implements IDP {
|
public static class DefaultIDP implements IDP {
|
||||||
|
|
||||||
|
private static final int DEFAULT_CACHE_TTL = 24 * 60 * 60;
|
||||||
|
|
||||||
private String entityID;
|
private String entityID;
|
||||||
private PublicKey signatureValidationKey;
|
private final CompositeKeyLocator signatureValidationKeyLocator = new CompositeKeyLocator();
|
||||||
private SingleSignOnService singleSignOnService;
|
private SingleSignOnService singleSignOnService;
|
||||||
private SingleLogoutService singleLogoutService;
|
private SingleLogoutService singleLogoutService;
|
||||||
|
private final List<PublicKey> signatureValidationKeys = new LinkedList<>();
|
||||||
|
private int minTimeBetweenDescriptorRequests;
|
||||||
|
private HttpClient client;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getEntityID() {
|
public String getEntityID() {
|
||||||
|
@ -200,16 +212,25 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PublicKey getSignatureValidationKey() {
|
public KeyLocator getSignatureValidationKeyLocator() {
|
||||||
return signatureValidationKey;
|
return this.signatureValidationKeyLocator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMinTimeBetweenDescriptorRequests() {
|
||||||
|
return minTimeBetweenDescriptorRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMinTimeBetweenDescriptorRequests(int minTimeBetweenDescriptorRequests) {
|
||||||
|
this.minTimeBetweenDescriptorRequests = minTimeBetweenDescriptorRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEntityID(String entityID) {
|
public void setEntityID(String entityID) {
|
||||||
this.entityID = entityID;
|
this.entityID = entityID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSignatureValidationKey(PublicKey signatureValidationKey) {
|
public void addSignatureValidationKey(PublicKey signatureValidationKey) {
|
||||||
this.signatureValidationKey = signatureValidationKey;
|
this.signatureValidationKeys.add(signatureValidationKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSingleSignOnService(SingleSignOnService singleSignOnService) {
|
public void setSingleSignOnService(SingleSignOnService singleSignOnService) {
|
||||||
|
@ -219,6 +240,31 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
public void setSingleLogoutService(SingleLogoutService singleLogoutService) {
|
public void setSingleLogoutService(SingleLogoutService singleLogoutService) {
|
||||||
this.singleLogoutService = singleLogoutService;
|
this.singleLogoutService = singleLogoutService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void refreshKeyLocatorConfiguration() {
|
||||||
|
this.signatureValidationKeyLocator.clear();
|
||||||
|
|
||||||
|
// When key is set, use that (and only that), otherwise configure dynamic key locator
|
||||||
|
if (! this.signatureValidationKeys.isEmpty()) {
|
||||||
|
this.signatureValidationKeyLocator.add(new HardcodedKeyLocator(this.signatureValidationKeys));
|
||||||
|
} else if (this.singleSignOnService != null) {
|
||||||
|
String samlDescriptorUrl = singleSignOnService.getRequestBindingUrl() + "/descriptor";
|
||||||
|
HttpClient httpClient = getClient();
|
||||||
|
SamlDescriptorPublicKeyLocator samlDescriptorPublicKeyLocator =
|
||||||
|
new SamlDescriptorPublicKeyLocator(
|
||||||
|
samlDescriptorUrl, this.minTimeBetweenDescriptorRequests, DEFAULT_CACHE_TTL, httpClient);
|
||||||
|
this.signatureValidationKeyLocator.add(samlDescriptorPublicKeyLocator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient getClient() {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClient(HttpClient client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IDP idp;
|
private IDP idp;
|
||||||
|
|
|
@ -22,14 +22,18 @@ import org.keycloak.saml.SignatureAlgorithm;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Represents SAML deployment configuration.
|
||||||
|
*
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public interface SamlDeployment {
|
public interface SamlDeployment {
|
||||||
|
|
||||||
enum Binding {
|
enum Binding {
|
||||||
POST,
|
POST,
|
||||||
REDIRECT;
|
REDIRECT;
|
||||||
|
@ -41,20 +45,68 @@ public interface SamlDeployment {
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IDP {
|
public interface IDP {
|
||||||
|
/**
|
||||||
|
* Returns entity identifier of this IdP.
|
||||||
|
* @return see description.
|
||||||
|
*/
|
||||||
String getEntityID();
|
String getEntityID();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Single sign on service configuration for this IdP.
|
||||||
|
* @return see description.
|
||||||
|
*/
|
||||||
SingleSignOnService getSingleSignOnService();
|
SingleSignOnService getSingleSignOnService();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Single logout service configuration for this IdP.
|
||||||
|
* @return see description.
|
||||||
|
*/
|
||||||
SingleLogoutService getSingleLogoutService();
|
SingleLogoutService getSingleLogoutService();
|
||||||
PublicKey getSignatureValidationKey();
|
|
||||||
|
/**
|
||||||
|
* Returns {@link KeyLocator} looking up public keys used for validation of IdP signatures.
|
||||||
|
* @return see description.
|
||||||
|
*/
|
||||||
|
KeyLocator getSignatureValidationKeyLocator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns minimum time (in seconds) between issuing requests to IdP SAML descriptor.
|
||||||
|
* Used e.g. by {@link KeyLocator} looking up public keys for validation of IdP signatures
|
||||||
|
* to prevent too frequent requests.
|
||||||
|
*
|
||||||
|
* @return see description.
|
||||||
|
*/
|
||||||
|
int getMinTimeBetweenDescriptorRequests();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link HttpClient} instance that will be used for http communication with this IdP.
|
||||||
|
* @return see description
|
||||||
|
*/
|
||||||
|
HttpClient getClient();
|
||||||
|
|
||||||
public interface SingleSignOnService {
|
public interface SingleSignOnService {
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the requests to IdP need to be signed by SP key.
|
||||||
|
* @return see dscription
|
||||||
|
*/
|
||||||
boolean signRequest();
|
boolean signRequest();
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the complete response message from IdP should
|
||||||
|
* be checked for valid signature.
|
||||||
|
* @return see dscription
|
||||||
|
*/
|
||||||
boolean validateResponseSignature();
|
boolean validateResponseSignature();
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if individual assertions in response from IdP should
|
||||||
|
* be checked for valid signature.
|
||||||
|
* @return see dscription
|
||||||
|
*/
|
||||||
boolean validateAssertionSignature();
|
boolean validateAssertionSignature();
|
||||||
Binding getRequestBinding();
|
Binding getRequestBinding();
|
||||||
Binding getResponseBinding();
|
Binding getResponseBinding();
|
||||||
String getRequestBindingUrl();
|
String getRequestBindingUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface SingleLogoutService {
|
public interface SingleLogoutService {
|
||||||
boolean validateRequestSignature();
|
boolean validateRequestSignature();
|
||||||
boolean validateResponseSignature();
|
boolean validateResponseSignature();
|
||||||
|
@ -67,10 +119,19 @@ public interface SamlDeployment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Identity Provider configuration for this SAML deployment.
|
||||||
|
* @return see description.
|
||||||
|
*/
|
||||||
public IDP getIDP();
|
public IDP getIDP();
|
||||||
|
|
||||||
public boolean isConfigured();
|
public boolean isConfigured();
|
||||||
SslRequired getSslRequired();
|
SslRequired getSslRequired();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns entity identifier of this SP.
|
||||||
|
* @return see description.
|
||||||
|
*/
|
||||||
String getEntityID();
|
String getEntityID();
|
||||||
String getNameIDPolicyFormat();
|
String getNameIDPolicyFormat();
|
||||||
boolean isForceAuthentication();
|
boolean isForceAuthentication();
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.adapters.saml.config;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.keycloak.adapters.cloned.AdapterHttpClientConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -157,12 +158,97 @@ public class IDP implements Serializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class HttpClientConfig implements AdapterHttpClientConfig {
|
||||||
|
|
||||||
|
private String truststore;
|
||||||
|
private String truststorePassword;
|
||||||
|
private String clientKeystore;
|
||||||
|
private String clientKeystorePassword;
|
||||||
|
private boolean allowAnyHostname;
|
||||||
|
private boolean disableTrustManager;
|
||||||
|
private int connectionPoolSize;
|
||||||
|
private String proxyUrl;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTruststore() {
|
||||||
|
return truststore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTruststore(String truststore) {
|
||||||
|
this.truststore = truststore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTruststorePassword() {
|
||||||
|
return truststorePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTruststorePassword(String truststorePassword) {
|
||||||
|
this.truststorePassword = truststorePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientKeystore() {
|
||||||
|
return clientKeystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientKeystore(String clientKeystore) {
|
||||||
|
this.clientKeystore = clientKeystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientKeystorePassword() {
|
||||||
|
return clientKeystorePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientKeystorePassword(String clientKeystorePassword) {
|
||||||
|
this.clientKeystorePassword = clientKeystorePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAllowAnyHostname() {
|
||||||
|
return allowAnyHostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowAnyHostname(boolean allowAnyHostname) {
|
||||||
|
this.allowAnyHostname = allowAnyHostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDisableTrustManager() {
|
||||||
|
return disableTrustManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisableTrustManager(boolean disableTrustManager) {
|
||||||
|
this.disableTrustManager = disableTrustManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getConnectionPoolSize() {
|
||||||
|
return connectionPoolSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectionPoolSize(int connectionPoolSize) {
|
||||||
|
this.connectionPoolSize = connectionPoolSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProxyUrl() {
|
||||||
|
return proxyUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProxyUrl(String proxyUrl) {
|
||||||
|
this.proxyUrl = proxyUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String entityID;
|
private String entityID;
|
||||||
private String signatureAlgorithm;
|
private String signatureAlgorithm;
|
||||||
private String signatureCanonicalizationMethod;
|
private String signatureCanonicalizationMethod;
|
||||||
private SingleSignOnService singleSignOnService;
|
private SingleSignOnService singleSignOnService;
|
||||||
private SingleLogoutService singleLogoutService;
|
private SingleLogoutService singleLogoutService;
|
||||||
private List<Key> keys;
|
private List<Key> keys;
|
||||||
|
private AdapterHttpClientConfig httpClientConfig = new HttpClientConfig();
|
||||||
|
|
||||||
public String getEntityID() {
|
public String getEntityID() {
|
||||||
return entityID;
|
return entityID;
|
||||||
|
@ -212,4 +298,12 @@ public class IDP implements Serializable {
|
||||||
this.signatureCanonicalizationMethod = signatureCanonicalizationMethod;
|
this.signatureCanonicalizationMethod = signatureCanonicalizationMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AdapterHttpClientConfig getHttpClientConfig() {
|
||||||
|
return httpClientConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHttpClientConfig(AdapterHttpClientConfig httpClientConfig) {
|
||||||
|
this.httpClientConfig = httpClientConfig;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,4 +72,15 @@ public class ConfigXmlConstants {
|
||||||
public static final String VALIDATE_REQUEST_SIGNATURE_ATTR = "validateRequestSignature";
|
public static final String VALIDATE_REQUEST_SIGNATURE_ATTR = "validateRequestSignature";
|
||||||
public static final String POST_BINDING_URL_ATTR = "postBindingUrl";
|
public static final String POST_BINDING_URL_ATTR = "postBindingUrl";
|
||||||
public static final String REDIRECT_BINDING_URL_ATTR = "redirectBindingUrl";
|
public static final String REDIRECT_BINDING_URL_ATTR = "redirectBindingUrl";
|
||||||
|
|
||||||
|
public static final String HTTP_CLIENT_ELEMENT = "HttpClient";
|
||||||
|
public static final String ALLOW_ANY_HOSTNAME_ATTR = "allowAnyHostname";
|
||||||
|
public static final String CLIENT_KEYSTORE_ATTR = "clientKeystore";
|
||||||
|
public static final String CLIENT_KEYSTORE_PASSWORD_ATTR = "clientKeystorePassword";
|
||||||
|
public static final String CONNECTION_POOL_SIZE_ATTR = "connectionPoolSize";
|
||||||
|
public static final String DISABLE_TRUST_MANAGER_ATTR = "disableTrustManager";
|
||||||
|
public static final String PROXY_URL_ATTR = "proxyUrl";
|
||||||
|
public static final String TRUSTSTORE_ATTR = "truststore";
|
||||||
|
public static final String TRUSTSTORE_PASSWORD_ATTR = "truststorePassword";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import java.security.PublicKey;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.keycloak.adapters.cloned.HttpClientBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -178,6 +179,19 @@ public class DeploymentBuilder {
|
||||||
if (sp.getIdp().getKeys() != null) {
|
if (sp.getIdp().getKeys() != null) {
|
||||||
for (Key key : sp.getIdp().getKeys()) {
|
for (Key key : sp.getIdp().getKeys()) {
|
||||||
if (key.isSigning()) {
|
if (key.isSigning()) {
|
||||||
|
processSigningKey(idp, key, resourceLoader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idp.setClient(new HttpClientBuilder().build(sp.getIdp().getHttpClientConfig()));
|
||||||
|
idp.refreshKeyLocatorConfiguration();
|
||||||
|
|
||||||
|
return deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processSigningKey(DefaultSamlDeployment.DefaultIDP idp, Key key, ResourceLoader resourceLoader) throws RuntimeException {
|
||||||
|
PublicKey publicKey;
|
||||||
if (key.getKeystore() != null) {
|
if (key.getKeystore() != null) {
|
||||||
KeyStore keyStore = loadKeystore(resourceLoader, key);
|
KeyStore keyStore = loadKeystore(resourceLoader, key);
|
||||||
Certificate cert = null;
|
Certificate cert = null;
|
||||||
|
@ -186,27 +200,18 @@ public class DeploymentBuilder {
|
||||||
} catch (KeyStoreException e) {
|
} catch (KeyStoreException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
idp.setSignatureValidationKey(cert.getPublicKey());
|
publicKey = cert.getPublicKey();
|
||||||
} else {
|
} else {
|
||||||
if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
|
if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
|
||||||
throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
|
throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
|
||||||
}
|
}
|
||||||
try {
|
publicKey = getPublicKeyFromPem(key);
|
||||||
PublicKey publicKey = getPublicKeyFromPem(key);
|
|
||||||
idp.setSignatureValidationKey(publicKey);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
idp.addSignatureValidationKey(publicKey);
|
||||||
return deployment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static PublicKey getPublicKeyFromPem(Key key) throws Exception {
|
protected static PublicKey getPublicKeyFromPem(Key key) {
|
||||||
PublicKey publicKey;
|
PublicKey publicKey;
|
||||||
if (key.getPublicKeyPem() != null) {
|
if (key.getPublicKeyPem() != null) {
|
||||||
publicKey = PemUtils.decodePublicKey(key.getPublicKeyPem().trim());
|
publicKey = PemUtils.decodePublicKey(key.getPublicKeyPem().trim());
|
||||||
|
|
|
@ -29,6 +29,10 @@ import javax.xml.stream.events.EndElement;
|
||||||
import javax.xml.stream.events.StartElement;
|
import javax.xml.stream.events.StartElement;
|
||||||
import javax.xml.stream.events.XMLEvent;
|
import javax.xml.stream.events.XMLEvent;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.keycloak.adapters.saml.config.IDP.HttpClientConfig;
|
||||||
|
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getAttributeValue;
|
||||||
|
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getBooleanAttributeValue;
|
||||||
|
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getIntegerAttributeValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -41,16 +45,16 @@ public class IDPXmlParser extends AbstractParser {
|
||||||
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
StaxParserUtil.validate(startElement, ConfigXmlConstants.IDP_ELEMENT);
|
StaxParserUtil.validate(startElement, ConfigXmlConstants.IDP_ELEMENT);
|
||||||
IDP idp = new IDP();
|
IDP idp = new IDP();
|
||||||
String entityID = SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.ENTITY_ID_ATTR);
|
String entityID = getAttributeValue(startElement, ConfigXmlConstants.ENTITY_ID_ATTR);
|
||||||
if (entityID == null) {
|
if (entityID == null) {
|
||||||
throw new ParsingException("entityID must be set on IDP");
|
throw new ParsingException("entityID must be set on IDP");
|
||||||
|
|
||||||
}
|
}
|
||||||
idp.setEntityID(entityID);
|
idp.setEntityID(entityID);
|
||||||
|
|
||||||
boolean signaturesRequired = SPXmlParser.getBooleanAttributeValue(startElement, ConfigXmlConstants.SIGNATURES_REQUIRED_ATTR);
|
boolean signaturesRequired = getBooleanAttributeValue(startElement, ConfigXmlConstants.SIGNATURES_REQUIRED_ATTR);
|
||||||
idp.setSignatureCanonicalizationMethod(SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_CANONICALIZATION_METHOD_ATTR));
|
idp.setSignatureCanonicalizationMethod(getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_CANONICALIZATION_METHOD_ATTR));
|
||||||
idp.setSignatureAlgorithm(SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_ALGORITHM_ATTR));
|
idp.setSignatureAlgorithm(getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_ALGORITHM_ATTR));
|
||||||
while (xmlEventReader.hasNext()) {
|
while (xmlEventReader.hasNext()) {
|
||||||
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
||||||
if (xmlEvent == null)
|
if (xmlEvent == null)
|
||||||
|
@ -75,6 +79,10 @@ public class IDPXmlParser extends AbstractParser {
|
||||||
IDP.SingleLogoutService slo = parseSingleLogoutService(xmlEventReader, signaturesRequired);
|
IDP.SingleLogoutService slo = parseSingleLogoutService(xmlEventReader, signaturesRequired);
|
||||||
idp.setSingleLogoutService(slo);
|
idp.setSingleLogoutService(slo);
|
||||||
|
|
||||||
|
} else if (tag.equals(ConfigXmlConstants.HTTP_CLIENT_ELEMENT)) {
|
||||||
|
HttpClientConfig config = parseHttpClientElement(xmlEventReader);
|
||||||
|
idp.setHttpClientConfig(config);
|
||||||
|
|
||||||
} else if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
|
} else if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
|
||||||
KeysXmlParser parser = new KeysXmlParser();
|
KeysXmlParser parser = new KeysXmlParser();
|
||||||
List<Key> keys = (List<Key>)parser.parse(xmlEventReader);
|
List<Key> keys = (List<Key>)parser.parse(xmlEventReader);
|
||||||
|
@ -90,29 +98,63 @@ public class IDPXmlParser extends AbstractParser {
|
||||||
protected IDP.SingleLogoutService parseSingleLogoutService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
|
protected IDP.SingleLogoutService parseSingleLogoutService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
|
||||||
IDP.SingleLogoutService slo = new IDP.SingleLogoutService();
|
IDP.SingleLogoutService slo = new IDP.SingleLogoutService();
|
||||||
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
slo.setSignRequest(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
|
slo.setSignRequest(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
|
||||||
slo.setValidateResponseSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
|
slo.setValidateResponseSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
|
||||||
slo.setValidateRequestSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_REQUEST_SIGNATURE_ATTR, signaturesRequired));
|
slo.setValidateRequestSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_REQUEST_SIGNATURE_ATTR, signaturesRequired));
|
||||||
slo.setRequestBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
|
slo.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
|
||||||
slo.setResponseBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
|
slo.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
|
||||||
slo.setSignResponse(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_RESPONSE_ATTR, signaturesRequired));
|
slo.setSignResponse(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_RESPONSE_ATTR, signaturesRequired));
|
||||||
slo.setPostBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.POST_BINDING_URL_ATTR));
|
slo.setPostBindingUrl(getAttributeValue(element, ConfigXmlConstants.POST_BINDING_URL_ATTR));
|
||||||
slo.setRedirectBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REDIRECT_BINDING_URL_ATTR));
|
slo.setRedirectBindingUrl(getAttributeValue(element, ConfigXmlConstants.REDIRECT_BINDING_URL_ATTR));
|
||||||
return slo;
|
return slo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IDP.SingleSignOnService parseSingleSignOnService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
|
protected IDP.SingleSignOnService parseSingleSignOnService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
|
||||||
IDP.SingleSignOnService sso = new IDP.SingleSignOnService();
|
IDP.SingleSignOnService sso = new IDP.SingleSignOnService();
|
||||||
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
sso.setSignRequest(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
|
sso.setSignRequest(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
|
||||||
sso.setValidateResponseSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
|
sso.setValidateResponseSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
|
||||||
sso.setValidateAssertionSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_ASSERTION_SIGNATURE_ATTR));
|
sso.setValidateAssertionSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_ASSERTION_SIGNATURE_ATTR));
|
||||||
sso.setRequestBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
|
sso.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
|
||||||
sso.setResponseBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
|
sso.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
|
||||||
sso.setBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR));
|
sso.setBindingUrl(getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR));
|
||||||
return sso;
|
return sso;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HttpClientConfig parseHttpClientElement(XMLEventReader xmlEventReader) throws ParsingException {
|
||||||
|
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
|
StaxParserUtil.validate(startElement, ConfigXmlConstants.HTTP_CLIENT_ELEMENT);
|
||||||
|
HttpClientConfig config = new HttpClientConfig();
|
||||||
|
|
||||||
|
config.setAllowAnyHostname(getBooleanAttributeValue(startElement, ConfigXmlConstants.ALLOW_ANY_HOSTNAME_ATTR, false));
|
||||||
|
config.setClientKeystore(getAttributeValue(startElement, ConfigXmlConstants.CLIENT_KEYSTORE_ATTR));
|
||||||
|
config.setClientKeystorePassword(getAttributeValue(startElement, ConfigXmlConstants.CLIENT_KEYSTORE_PASSWORD_ATTR));
|
||||||
|
config.setConnectionPoolSize(getIntegerAttributeValue(startElement, ConfigXmlConstants.CONNECTION_POOL_SIZE_ATTR, 0));
|
||||||
|
config.setDisableTrustManager(getBooleanAttributeValue(startElement, ConfigXmlConstants.ALLOW_ANY_HOSTNAME_ATTR, false));
|
||||||
|
config.setProxyUrl(getAttributeValue(startElement, ConfigXmlConstants.PROXY_URL_ATTR));
|
||||||
|
config.setTruststore(getAttributeValue(startElement, ConfigXmlConstants.TRUSTSTORE_ATTR));
|
||||||
|
config.setTruststorePassword(getAttributeValue(startElement, ConfigXmlConstants.TRUSTSTORE_PASSWORD_ATTR));
|
||||||
|
|
||||||
|
while (xmlEventReader.hasNext()) {
|
||||||
|
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
||||||
|
if (xmlEvent == null)
|
||||||
|
break;
|
||||||
|
if (xmlEvent instanceof EndElement) {
|
||||||
|
EndElement endElement = (EndElement) StaxParserUtil.getNextEvent(xmlEventReader);
|
||||||
|
String endElementName = StaxParserUtil.getEndElementName(endElement);
|
||||||
|
if (endElementName.equals(ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT))
|
||||||
|
break;
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String tag = StaxParserUtil.getStartElementName(startElement);
|
||||||
|
StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(QName qname) {
|
public boolean supports(QName qname) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -48,6 +48,13 @@ public class SPXmlParser extends AbstractParser {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getIntegerAttributeValue(StartElement startElement, String tag, int defaultValue) {
|
||||||
|
String result = getAttributeValue(startElement, tag);
|
||||||
|
if (result == null)
|
||||||
|
return defaultValue;
|
||||||
|
return Integer.valueOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
|
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
|
||||||
String result = getAttributeValue(startElement, tag);
|
String result = getAttributeValue(startElement, tag);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.saml.descriptor.parsers;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import javax.xml.crypto.MarshalException;
|
||||||
|
import javax.xml.crypto.dom.DOMStructure;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.xpath.XPath;
|
||||||
|
import javax.xml.xpath.XPathConstants;
|
||||||
|
import javax.xml.xpath.XPathExpression;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
import javax.xml.xpath.XPathFactory;
|
||||||
|
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.processing.core.util.NamespaceContext;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Goes through the given XML file and extracts names, certificates and keys from the KeyInfo elements.
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class SamlDescriptorIDPKeysExtractor {
|
||||||
|
|
||||||
|
private static final NamespaceContext NS_CONTEXT = new NamespaceContext();
|
||||||
|
static {
|
||||||
|
NS_CONTEXT.addNsUriPair("m", JBossSAMLURIConstants.METADATA_NSURI.get());
|
||||||
|
NS_CONTEXT.addNsUriPair("dsig", JBossSAMLURIConstants.XMLDSIG_NSURI.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private final KeyInfoFactory kif = KeyInfoFactory.getInstance();
|
||||||
|
|
||||||
|
private final XPathFactory xPathfactory = XPathFactory.newInstance();
|
||||||
|
private final XPath xpath = xPathfactory.newXPath();
|
||||||
|
{
|
||||||
|
xpath.setNamespaceContext(NS_CONTEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MultivaluedHashMap<String, KeyInfo> parse(InputStream stream) throws ParsingException {
|
||||||
|
MultivaluedHashMap<String, KeyInfo> res = new MultivaluedHashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(true);
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document doc = builder.parse(stream);
|
||||||
|
|
||||||
|
XPathExpression expr = xpath.compile("/m:EntitiesDescriptor/m:EntityDescriptor/m:IDPSSODescriptor/m:KeyDescriptor");
|
||||||
|
NodeList keyDescriptors = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
|
||||||
|
for (int i = 0; i < keyDescriptors.getLength(); i ++) {
|
||||||
|
Node keyDescriptor = keyDescriptors.item(i);
|
||||||
|
Element keyDescriptorEl = (Element) keyDescriptor;
|
||||||
|
KeyInfo ki = processKeyDescriptor(keyDescriptorEl);
|
||||||
|
if (ki != null) {
|
||||||
|
String use = keyDescriptorEl.getAttribute(JBossSAMLConstants.USE.get());
|
||||||
|
res.add(use, ki);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SAXException | IOException | ParserConfigurationException | MarshalException | XPathExpressionException e) {
|
||||||
|
throw new ParsingException("Error parsing SAML descriptor", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyInfo processKeyDescriptor(Element keyDescriptor) throws MarshalException {
|
||||||
|
NodeList childNodes = keyDescriptor.getElementsByTagNameNS(JBossSAMLURIConstants.XMLDSIG_NSURI.get(), JBossSAMLConstants.KEY_INFO.get());
|
||||||
|
|
||||||
|
if (childNodes.getLength() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Node keyInfoNode = childNodes.item(0);
|
||||||
|
return (keyInfoNode == null) ? null : kif.unmarshalKeyInfo(new DOMStructure(keyInfoNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -64,11 +64,20 @@ import org.w3c.dom.Node;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -257,13 +266,44 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
|
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
|
||||||
|
KeyLocator signatureValidationKey = deployment.getIDP().getSignatureValidationKeyLocator();
|
||||||
if (postBinding) {
|
if (postBinding) {
|
||||||
verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
|
verifyPostBindingSignature(holder.getSamlDocument(), signatureValidationKey);
|
||||||
} else {
|
} else {
|
||||||
verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
|
String keyId = getMessageSigningKeyId(holder.getSamlObject());
|
||||||
|
verifyRedirectBindingSignature(paramKey, signatureValidationKey, keyId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getMessageSigningKeyId(SAML2Object doc) {
|
||||||
|
final ExtensionsType extensions;
|
||||||
|
if (doc instanceof RequestAbstractType) {
|
||||||
|
extensions = ((RequestAbstractType) doc).getExtensions();
|
||||||
|
} else if (doc instanceof StatusResponseType) {
|
||||||
|
extensions = ((StatusResponseType) doc).getExtensions();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensions == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object ext : extensions.getAny()) {
|
||||||
|
if (! (ext instanceof Element)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String res = KeycloakKeySamlExtensionGenerator.getMessageSigningKeyIdFromElement((Element) ext);
|
||||||
|
|
||||||
|
if (res != null) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
|
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
|
||||||
if(statusCode != null && statusCode.getValue()!=null){
|
if(statusCode != null && statusCode.getValue()!=null){
|
||||||
String v = statusCode.getValue().toString();
|
String v = statusCode.getValue().toString();
|
||||||
|
@ -473,10 +513,10 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
|
public void verifyPostBindingSignature(Document document, KeyLocator keyLocator) throws VerificationException {
|
||||||
SAML2Signature saml2Signature = new SAML2Signature();
|
SAML2Signature saml2Signature = new SAML2Signature();
|
||||||
try {
|
try {
|
||||||
if (!saml2Signature.validate(document, publicKey)) {
|
if (!saml2Signature.validate(document, keyLocator)) {
|
||||||
throw new VerificationException("Invalid signature on document");
|
throw new VerificationException("Invalid signature on document");
|
||||||
}
|
}
|
||||||
} catch (ProcessingException e) {
|
} catch (ProcessingException e) {
|
||||||
|
@ -484,7 +524,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
|
private void verifyRedirectBindingSignature(String paramKey, KeyLocator keyLocator, String keyId) throws VerificationException {
|
||||||
String request = facade.getRequest().getQueryParamValue(paramKey);
|
String request = facade.getRequest().getQueryParamValue(paramKey);
|
||||||
String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
||||||
String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
|
String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
|
||||||
|
@ -511,16 +551,80 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||||
try {
|
try {
|
||||||
//byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
|
//byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
|
||||||
byte[] decodedSignature = Base64.decode(signature);
|
byte[] decodedSignature = Base64.decode(signature);
|
||||||
|
byte[] rawQueryBytes = rawQuery.getBytes("UTF-8");
|
||||||
|
|
||||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
|
||||||
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
|
|
||||||
validator.initVerify(publicKey);
|
if (! validateRedirectBindingSignature(signatureAlgorithm, rawQueryBytes, decodedSignature, keyLocator, keyId)) {
|
||||||
validator.update(rawQuery.getBytes("UTF-8"));
|
|
||||||
if (!validator.verify(decodedSignature)) {
|
|
||||||
throw new VerificationException("Invalid query param signature");
|
throw new VerificationException("Invalid query param signature");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new VerificationException(e);
|
throw new VerificationException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean validateRedirectBindingSignature(SignatureAlgorithm sigAlg, byte[] rawQueryBytes, byte[] decodedSignature, KeyLocator locator, String keyId)
|
||||||
|
throws KeyManagementException, VerificationException {
|
||||||
|
try {
|
||||||
|
Key key;
|
||||||
|
try {
|
||||||
|
key = locator.getKey(keyId);
|
||||||
|
boolean keyLocated = key != null;
|
||||||
|
|
||||||
|
if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyLocated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (KeyManagementException ex) {
|
||||||
|
}
|
||||||
|
} catch (SignatureException ex) {
|
||||||
|
log.debug("Verification failed for key %s: %s", keyId, ex);
|
||||||
|
log.trace(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locator instanceof Iterable) {
|
||||||
|
Iterable<Key> availableKeys = (Iterable<Key>) locator;
|
||||||
|
|
||||||
|
log.trace("Trying hard to validate XML signature using all available keys.");
|
||||||
|
|
||||||
|
for (Key key : availableKeys) {
|
||||||
|
try {
|
||||||
|
if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (SignatureException ex) {
|
||||||
|
log.debug("Verification failed: %s", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateRedirectBindingSignatureForKey(SignatureAlgorithm sigAlg, byte[] rawQueryBytes, byte[] decodedSignature, Key key)
|
||||||
|
throws SignatureException {
|
||||||
|
if (key == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! (key instanceof PublicKey)) {
|
||||||
|
log.warnf("Unusable key for signature validation: %s", key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Signature signature = sigAlg.createSignature(); // todo plugin signature alg
|
||||||
|
try {
|
||||||
|
signature.initVerify((PublicKey) key);
|
||||||
|
} catch (InvalidKeyException ex) {
|
||||||
|
log.warnf(ex, "Unusable key for signature validation: %s", key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
signature.update(rawQueryBytes);
|
||||||
|
|
||||||
|
return signature.verify(decodedSignature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,8 +82,10 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
|
||||||
if (deployment.getSignatureCanonicalizationMethod() != null)
|
if (deployment.getSignatureCanonicalizationMethod() != null)
|
||||||
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
||||||
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
|
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
|
||||||
.signWith(deployment.getSigningKeyPair())
|
.signWith(null, deployment.getSigningKeyPair())
|
||||||
.signDocument();
|
.signDocument();
|
||||||
|
// TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
|
||||||
|
// <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,8 +115,10 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
|
||||||
if (deployment.getSignatureCanonicalizationMethod() != null)
|
if (deployment.getSignatureCanonicalizationMethod() != null)
|
||||||
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
||||||
binding.signatureAlgorithm(deployment.getSignatureAlgorithm());
|
binding.signatureAlgorithm(deployment.getSignatureAlgorithm());
|
||||||
binding.signWith(deployment.getSigningKeyPair())
|
binding.signWith(null, deployment.getSigningKeyPair())
|
||||||
.signDocument();
|
.signDocument();
|
||||||
|
// TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
|
||||||
|
// <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.relayState("logout");
|
binding.relayState("logout");
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.saml.rotation;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyName;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.adapters.cloned.HttpAdapterUtils;
|
||||||
|
import org.keycloak.adapters.cloned.HttpClientAdapterException;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
import org.keycloak.saml.processing.api.util.KeyInfoTools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class defines a {@link KeyLocator} that looks up public keys and certificates in IdP's
|
||||||
|
* SAML descriptor (i.e. http://{host}/auth/realms/{realm}/protocol/saml/descriptor).
|
||||||
|
*
|
||||||
|
* Based on {@code JWKPublicKeyLocator}.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class SamlDescriptorPublicKeyLocator implements KeyLocator, Iterable<PublicKey> {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(SamlDescriptorPublicKeyLocator.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time between two subsequent requests (in seconds).
|
||||||
|
*/
|
||||||
|
private final int minTimeBetweenDescriptorRequests;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to live for cache entries (in seconds).
|
||||||
|
*/
|
||||||
|
private final int cacheEntryTtl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target descriptor URL.
|
||||||
|
*/
|
||||||
|
private final String descriptorUrl;
|
||||||
|
|
||||||
|
private final Map<String, PublicKey> publicKeyCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final HttpClient client;
|
||||||
|
|
||||||
|
private volatile int lastRequestTime = 0;
|
||||||
|
|
||||||
|
public SamlDescriptorPublicKeyLocator(String descriptorUrl, int minTimeBetweenDescriptorRequests, int cacheEntryTtl, HttpClient httpClient) {
|
||||||
|
this.minTimeBetweenDescriptorRequests = minTimeBetweenDescriptorRequests <= 0
|
||||||
|
? 20
|
||||||
|
: minTimeBetweenDescriptorRequests;
|
||||||
|
|
||||||
|
this.descriptorUrl = descriptorUrl;
|
||||||
|
this.cacheEntryTtl = cacheEntryTtl;
|
||||||
|
|
||||||
|
this.client = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Key getKey(String kid) throws KeyManagementException {
|
||||||
|
if (kid == null) {
|
||||||
|
LOG.debugf("Invalid key id: %s", kid);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.tracef("Requested key id: %s", kid);
|
||||||
|
|
||||||
|
int currentTime = Time.currentTime();
|
||||||
|
|
||||||
|
PublicKey res;
|
||||||
|
if (currentTime > this.lastRequestTime + this.cacheEntryTtl) {
|
||||||
|
LOG.debugf("Performing regular cache cleanup.");
|
||||||
|
res = refreshCertificateCacheAndGet(kid);
|
||||||
|
} else {
|
||||||
|
res = publicKeyCache.get(kid);
|
||||||
|
|
||||||
|
if (res == null) {
|
||||||
|
if (currentTime > this.lastRequestTime + this.minTimeBetweenDescriptorRequests) {
|
||||||
|
res = refreshCertificateCacheAndGet(kid);
|
||||||
|
} else {
|
||||||
|
LOG.debugf("Won't send request to realm SAML descriptor url, timeout not expired. Last request time was %d", lastRequestTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void refreshKeyCache() {
|
||||||
|
LOG.info("Forcing key cache cleanup and refresh.");
|
||||||
|
this.publicKeyCache.clear();
|
||||||
|
refreshCertificateCacheAndGet(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized PublicKey refreshCertificateCacheAndGet(String kid) {
|
||||||
|
if (this.descriptorUrl == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastRequestTime = Time.currentTime();
|
||||||
|
|
||||||
|
LOG.debugf("Refreshing public key cache from %s", this.descriptorUrl);
|
||||||
|
List<KeyInfo> signingCerts;
|
||||||
|
try {
|
||||||
|
MultivaluedHashMap<String, KeyInfo> certs = HttpAdapterUtils.downloadKeysFromSamlDescriptor(client, this.descriptorUrl);
|
||||||
|
signingCerts = certs.get(KeyTypes.SIGNING.value());
|
||||||
|
} catch (HttpClientAdapterException ex) {
|
||||||
|
LOG.error("Could not refresh certificates from the server", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signingCerts == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debugf("Certificates retrieved from server, filling public key cache");
|
||||||
|
|
||||||
|
// Only clear cache after it is certain that the SAML descriptor has been read successfully
|
||||||
|
this.publicKeyCache.clear();
|
||||||
|
|
||||||
|
for (KeyInfo ki : signingCerts) {
|
||||||
|
KeyName keyName = KeyInfoTools.getKeyName(ki);
|
||||||
|
X509Certificate x509certificate = KeyInfoTools.getX509Certificate(ki);
|
||||||
|
if (x509certificate != null && keyName != null) {
|
||||||
|
LOG.tracef("Registering signing certificate %s", keyName.getName());
|
||||||
|
this.publicKeyCache.put(keyName.getName(), x509certificate.getPublicKey());
|
||||||
|
} else {
|
||||||
|
LOG.tracef("Ignoring certificate %s: %s", keyName, x509certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (kid == null ? null : this.publicKeyCache.get(kid));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Keys retrieved from SAML descriptor at " + descriptorUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<PublicKey> iterator() {
|
||||||
|
if (this.publicKeyCache.isEmpty()) {
|
||||||
|
refreshCertificateCacheAndGet(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.publicKeyCache.values().iterator();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<xs:schema version="1.0"
|
||||||
|
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||||
|
xmlns="urn:keycloak:saml:adapter"
|
||||||
|
targetNamespace="urn:keycloak:saml:adapter"
|
||||||
|
elementFormDefault="qualified"
|
||||||
|
attributeFormDefault="unqualified">
|
||||||
|
|
||||||
|
<xs:element name="keycloak-saml-adapter" type="adapter-type"/>
|
||||||
|
<xs:complexType name="adapter-type">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
<![CDATA[
|
||||||
|
The Keycloak SAML Adapter keycloak-saml.xml config file
|
||||||
|
]]>
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="SP" maxOccurs="1" minOccurs="0" type="sp-type"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="sp-type">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="PrincipalNameMapping" type="principal-name-mapping-type" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="RoleIdentifiers" type="role-identifiers-type" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="IDP" type="idp-type" minOccurs="1" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="entityID" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="sslPolicy" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="logoutPage" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="isPassive" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="keys-type">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="Key" type="key-type" minOccurs="1" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="key-type">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="KeyStore" maxOccurs="1" minOccurs="0" type="key-store-type"/>
|
||||||
|
<xs:element name="PrivateKeyPem" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="PublicKeyPem" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="CertificatePem" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="signing" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="encryption" type="xs:boolean" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="key-store-type">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="PrivateKey" maxOccurs="1" minOccurs="0" type="private-key-type"/>
|
||||||
|
<xs:element name="Certificate" type="certificate-type" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="file" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="resource" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="password" type="xs:string" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="private-key-type">
|
||||||
|
<xs:attribute name="alias" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="password" type="xs:string" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="certificate-type">
|
||||||
|
<xs:attribute name="alias" type="xs:string" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="principal-name-mapping-type">
|
||||||
|
<xs:attribute name="policy" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="attribute" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="role-identifiers-type">
|
||||||
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element name="Attribute" maxOccurs="unbounded" minOccurs="0" type="attribute-type"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="attribute-type">
|
||||||
|
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="idp-type">
|
||||||
|
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element name="SingleSignOnService" maxOccurs="1" minOccurs="1" type="sign-on-type"/>
|
||||||
|
<xs:element name="SingleLogoutService" type="logout-type" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="HttpClient" type="http-client-type" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="entityID" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="signaturesRequired" type="xs:boolean" use="required"/>
|
||||||
|
<xs:attribute name="signatureAlgorithm" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="encryption" type="xs:boolean" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="sign-on-type">
|
||||||
|
<xs:attribute name="signRequest" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="requestBinding" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="responseBinding" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="bindingUrl" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="logout-type">
|
||||||
|
<xs:attribute name="signRequest" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="signResponse" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="requestBinding" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="responseBinding" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="postBindingUrl" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="redirectBindingUrl" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="http-client-type">
|
||||||
|
<xs:attribute name="allowAnyHostname" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="clientKeystore" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="clientKeystorePassword" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="connectionPoolSize" type="xs:int" use="optional"/>
|
||||||
|
<xs:attribute name="disableTrustManager" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="proxyUrl" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="truststore" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="truststorePassword" type="xs:string" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
</xs:schema>
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* To change this license header, choose License Headers in Project Properties.
|
||||||
|
* To change this template file, choose Tools | Templates
|
||||||
|
* and open the template in the editor.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.cloned;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyName;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.X509Data;
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
import org.keycloak.adapters.saml.config.parsers.ConfigXmlConstants;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||||
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class HttpAdapterUtilsTest {
|
||||||
|
|
||||||
|
private <T> T getContent(List<Object> objects, Class<T> clazz) {
|
||||||
|
for (Object o : objects) {
|
||||||
|
if (clazz.isInstance(o)) {
|
||||||
|
return (T) o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractKeysFromSamlDescriptor() throws ParsingException {
|
||||||
|
InputStream xmlStream = HttpAdapterUtilsTest.class.getResourceAsStream("saml-descriptor-valid.xml");
|
||||||
|
MultivaluedHashMap<String, KeyInfo> res = HttpAdapterUtils.extractKeysFromSamlDescriptor(xmlStream);
|
||||||
|
|
||||||
|
assertThat(res, notNullValue());
|
||||||
|
assertThat(res.keySet(), hasItems(KeyTypes.SIGNING.value()));
|
||||||
|
assertThat(res.get(ConfigXmlConstants.SIGNING_ATTR), notNullValue());
|
||||||
|
assertThat(res.get(ConfigXmlConstants.SIGNING_ATTR).size(), equalTo(2));
|
||||||
|
|
||||||
|
KeyInfo ki;
|
||||||
|
KeyName keyName;
|
||||||
|
X509Data x509data;
|
||||||
|
X509Certificate x509certificate;
|
||||||
|
|
||||||
|
ki = res.get(ConfigXmlConstants.SIGNING_ATTR).get(0);
|
||||||
|
assertThat(ki.getContent().size(), equalTo(2));
|
||||||
|
assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(X509Data.class)));
|
||||||
|
assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(KeyName.class)));
|
||||||
|
|
||||||
|
keyName = getContent(ki.getContent(), KeyName.class);
|
||||||
|
assertThat(keyName.getName(), equalTo("rJkJlvowmv1Id74GznieaAC5jU5QQp_ILzuG-GsweTI"));
|
||||||
|
|
||||||
|
x509data = getContent(ki.getContent(), X509Data.class);
|
||||||
|
assertThat(x509data, notNullValue());
|
||||||
|
x509certificate = getContent(x509data.getContent(), X509Certificate.class);
|
||||||
|
assertThat(x509certificate, notNullValue());
|
||||||
|
assertThat(x509certificate.getSigAlgName(), equalTo("SHA256withRSA"));
|
||||||
|
|
||||||
|
ki = res.get(ConfigXmlConstants.SIGNING_ATTR).get(1);
|
||||||
|
assertThat(ki.getContent().size(), equalTo(2));
|
||||||
|
assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(X509Data.class)));
|
||||||
|
assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(KeyName.class)));
|
||||||
|
|
||||||
|
keyName = getContent(ki.getContent(), KeyName.class);
|
||||||
|
assertThat(keyName.getName(), equalTo("BzYc4GwL8HVrAhNyNdp-lTah2DvU9jU03kby9Ynohr4"));
|
||||||
|
|
||||||
|
x509data = getContent(ki.getContent(), X509Data.class);
|
||||||
|
assertThat(x509data, notNullValue());
|
||||||
|
x509certificate = getContent(x509data.getContent(), X509Certificate.class);
|
||||||
|
assertThat(x509certificate, notNullValue());
|
||||||
|
assertThat(x509certificate.getSigAlgName(), equalTo("SHA256withRSA"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.saml.config.parsers;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.adapters.saml.config.IDP;
|
||||||
|
import org.keycloak.adapters.saml.config.Key;
|
||||||
|
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
|
||||||
|
import org.keycloak.adapters.saml.config.SP;
|
||||||
|
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KeycloakSamlAdapterXMLParserTest {
|
||||||
|
|
||||||
|
private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_7.xsd";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExpectedException expectedException = ExpectedException.none();
|
||||||
|
|
||||||
|
private void testValidationValid(String fileName) throws Exception {
|
||||||
|
InputStream schema = getClass().getResourceAsStream(CURRENT_XSD_LOCATION);
|
||||||
|
InputStream is = getClass().getResourceAsStream(fileName);
|
||||||
|
assertNotNull(is);
|
||||||
|
assertNotNull(schema);
|
||||||
|
StaxParserUtil.validate(is, schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationSimpleFile() throws Exception {
|
||||||
|
testValidationValid("keycloak-saml.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationMultipleKeys() throws Exception {
|
||||||
|
testValidationValid("keycloak-saml-multiple-signing-keys.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationWithHttpClient() throws Exception {
|
||||||
|
testValidationValid("keycloak-saml-wth-http-client-settings.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationKeyInvalid() throws Exception {
|
||||||
|
InputStream schemaIs = KeycloakSamlAdapterXMLParser.class.getResourceAsStream(CURRENT_XSD_LOCATION);
|
||||||
|
InputStream is = getClass().getResourceAsStream("keycloak-saml-invalid.xml");
|
||||||
|
assertNotNull(is);
|
||||||
|
assertNotNull(schemaIs);
|
||||||
|
|
||||||
|
expectedException.expect(ParsingException.class);
|
||||||
|
StaxParserUtil.validate(is, schemaIs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testXmlParser() throws Exception {
|
||||||
|
InputStream is = getClass().getResourceAsStream("keycloak-saml.xml");
|
||||||
|
assertNotNull(is);
|
||||||
|
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
|
||||||
|
|
||||||
|
KeycloakSamlAdapter config = (KeycloakSamlAdapter)parser.parse(is);
|
||||||
|
assertNotNull(config);
|
||||||
|
assertEquals(1, config.getSps().size());
|
||||||
|
SP sp = config.getSps().get(0);
|
||||||
|
assertEquals("sp", sp.getEntityID());
|
||||||
|
assertEquals("ssl", sp.getSslPolicy());
|
||||||
|
assertEquals("format", sp.getNameIDPolicyFormat());
|
||||||
|
assertTrue(sp.isForceAuthentication());
|
||||||
|
assertTrue(sp.isIsPassive());
|
||||||
|
assertEquals(2, sp.getKeys().size());
|
||||||
|
Key signing = sp.getKeys().get(0);
|
||||||
|
assertTrue(signing.isSigning());
|
||||||
|
Key.KeyStoreConfig keystore = signing.getKeystore();
|
||||||
|
assertNotNull(keystore);
|
||||||
|
assertEquals("file", keystore.getFile());
|
||||||
|
assertEquals("cp", keystore.getResource());
|
||||||
|
assertEquals("pw", keystore.getPassword());
|
||||||
|
assertEquals("private alias", keystore.getPrivateKeyAlias());
|
||||||
|
assertEquals("private pw", keystore.getPrivateKeyPassword());
|
||||||
|
assertEquals("cert alias", keystore.getCertificateAlias());
|
||||||
|
Key encryption = sp.getKeys().get(1);
|
||||||
|
assertTrue(encryption.isEncryption());
|
||||||
|
assertEquals("private pem", encryption.getPrivateKeyPem());
|
||||||
|
assertEquals("public pem", encryption.getPublicKeyPem());
|
||||||
|
assertEquals("policy", sp.getPrincipalNameMapping().getPolicy());
|
||||||
|
assertEquals("attribute", sp.getPrincipalNameMapping().getAttributeName());
|
||||||
|
assertTrue(sp.getRoleAttributes().size() == 1);
|
||||||
|
assertTrue(sp.getRoleAttributes().contains("member"));
|
||||||
|
|
||||||
|
IDP idp = sp.getIdp();
|
||||||
|
assertEquals("idp", idp.getEntityID());
|
||||||
|
assertEquals("RSA", idp.getSignatureAlgorithm());
|
||||||
|
assertEquals("canon", idp.getSignatureCanonicalizationMethod());
|
||||||
|
assertTrue(idp.getSingleSignOnService().isSignRequest());
|
||||||
|
assertTrue(idp.getSingleSignOnService().isValidateResponseSignature());
|
||||||
|
assertEquals("post", idp.getSingleSignOnService().getRequestBinding());
|
||||||
|
assertEquals("url", idp.getSingleSignOnService().getBindingUrl());
|
||||||
|
|
||||||
|
assertTrue(idp.getSingleLogoutService().isSignRequest());
|
||||||
|
assertTrue(idp.getSingleLogoutService().isSignResponse());
|
||||||
|
assertTrue(idp.getSingleLogoutService().isValidateRequestSignature());
|
||||||
|
assertTrue(idp.getSingleLogoutService().isValidateResponseSignature());
|
||||||
|
assertEquals("redirect", idp.getSingleLogoutService().getRequestBinding());
|
||||||
|
assertEquals("post", idp.getSingleLogoutService().getResponseBinding());
|
||||||
|
assertEquals("posturl", idp.getSingleLogoutService().getPostBindingUrl());
|
||||||
|
assertEquals("redirecturl", idp.getSingleLogoutService().getRedirectBindingUrl());
|
||||||
|
|
||||||
|
assertTrue(idp.getKeys().size() == 1);
|
||||||
|
assertTrue(idp.getKeys().get(0).isSigning());
|
||||||
|
assertEquals("cert pem", idp.getKeys().get(0).getCertificatePem());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testXmlParserMultipleSigningKeys() throws Exception {
|
||||||
|
InputStream is = getClass().getResourceAsStream("keycloak-saml-multiple-signing-keys.xml");
|
||||||
|
assertNotNull(is);
|
||||||
|
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
|
||||||
|
|
||||||
|
KeycloakSamlAdapter config = (KeycloakSamlAdapter) parser.parse(is);
|
||||||
|
assertNotNull(config);
|
||||||
|
assertEquals(1, config.getSps().size());
|
||||||
|
SP sp = config.getSps().get(0);
|
||||||
|
IDP idp = sp.getIdp();
|
||||||
|
|
||||||
|
assertTrue(idp.getKeys().size() == 4);
|
||||||
|
for (int i = 0; i < 4; i ++) {
|
||||||
|
Key key = idp.getKeys().get(i);
|
||||||
|
assertTrue(key.isSigning());
|
||||||
|
assertEquals("cert pem " + i, idp.getKeys().get(i).getCertificatePem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testXmlParserHttpClientSettings() throws Exception {
|
||||||
|
InputStream is = getClass().getResourceAsStream("keycloak-saml-wth-http-client-settings.xml");
|
||||||
|
assertNotNull(is);
|
||||||
|
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
|
||||||
|
|
||||||
|
KeycloakSamlAdapter config = (KeycloakSamlAdapter) parser.parse(is);
|
||||||
|
assertNotNull(config);
|
||||||
|
assertEquals(1, config.getSps().size());
|
||||||
|
SP sp = config.getSps().get(0);
|
||||||
|
IDP idp = sp.getIdp();
|
||||||
|
|
||||||
|
assertThat(idp.getHttpClientConfig(), notNullValue());
|
||||||
|
assertThat(idp.getHttpClientConfig().getClientKeystore(), is("ks"));
|
||||||
|
assertThat(idp.getHttpClientConfig().getClientKeystorePassword(), is("ks-pwd"));
|
||||||
|
assertThat(idp.getHttpClientConfig().getProxyUrl(), is("pu"));
|
||||||
|
assertThat(idp.getHttpClientConfig().getTruststore(), is("ts"));
|
||||||
|
assertThat(idp.getHttpClientConfig().getTruststorePassword(), is("tsp"));
|
||||||
|
assertThat(idp.getHttpClientConfig().getConnectionPoolSize(), is(42));
|
||||||
|
assertThat(idp.getHttpClientConfig().isAllowAnyHostname(), is(true));
|
||||||
|
assertThat(idp.getHttpClientConfig().isDisableTrustManager(), is(true));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,133 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.test.adapters.saml;
|
|
||||||
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.keycloak.adapters.saml.config.IDP;
|
|
||||||
import org.keycloak.adapters.saml.config.Key;
|
|
||||||
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
|
|
||||||
import org.keycloak.adapters.saml.config.SP;
|
|
||||||
import org.keycloak.adapters.saml.config.parsers.KeycloakSamlAdapterXMLParser;
|
|
||||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
|
||||||
|
|
||||||
import javax.xml.XMLConstants;
|
|
||||||
import javax.xml.transform.stream.StreamSource;
|
|
||||||
import javax.xml.validation.Schema;
|
|
||||||
import javax.xml.validation.SchemaFactory;
|
|
||||||
import javax.xml.validation.Validator;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
|
||||||
* @version $Revision: 1 $
|
|
||||||
*/
|
|
||||||
public class XmlParserTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testValidation() throws Exception {
|
|
||||||
{
|
|
||||||
InputStream schema = KeycloakSamlAdapterXMLParser.class.getResourceAsStream("/schema/keycloak_saml_adapter_1_6.xsd");
|
|
||||||
InputStream is = getClass().getResourceAsStream("/keycloak-saml.xml");
|
|
||||||
Assert.assertNotNull(is);
|
|
||||||
Assert.assertNotNull(schema);
|
|
||||||
StaxParserUtil.validate(is, schema);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
InputStream sch = KeycloakSamlAdapterXMLParser.class.getResourceAsStream("/schema/keycloak_saml_adapter_1_6.xsd");
|
|
||||||
InputStream doc = getClass().getResourceAsStream("/keycloak-saml2.xml");
|
|
||||||
Assert.assertNotNull(doc);
|
|
||||||
Assert.assertNotNull(sch);
|
|
||||||
try {
|
|
||||||
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
|
|
||||||
Schema schema = factory.newSchema(new StreamSource(sch));
|
|
||||||
Validator validator = schema.newValidator();
|
|
||||||
StreamSource source = new StreamSource(doc);
|
|
||||||
source.setSystemId("/keycloak-saml2.xml");
|
|
||||||
validator.validate(source);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testXmlParser() throws Exception {
|
|
||||||
InputStream is = getClass().getResourceAsStream("/keycloak-saml.xml");
|
|
||||||
Assert.assertNotNull(is);
|
|
||||||
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
|
|
||||||
KeycloakSamlAdapter config = (KeycloakSamlAdapter)parser.parse(is);
|
|
||||||
Assert.assertNotNull(config);
|
|
||||||
Assert.assertEquals(1, config.getSps().size());
|
|
||||||
SP sp = config.getSps().get(0);
|
|
||||||
Assert.assertEquals("sp", sp.getEntityID());
|
|
||||||
Assert.assertEquals("ssl", sp.getSslPolicy());
|
|
||||||
Assert.assertEquals("format", sp.getNameIDPolicyFormat());
|
|
||||||
Assert.assertTrue(sp.isForceAuthentication());
|
|
||||||
Assert.assertTrue(sp.isIsPassive());
|
|
||||||
Assert.assertEquals(2, sp.getKeys().size());
|
|
||||||
Key signing = sp.getKeys().get(0);
|
|
||||||
Assert.assertTrue(signing.isSigning());
|
|
||||||
Key.KeyStoreConfig keystore = signing.getKeystore();
|
|
||||||
Assert.assertNotNull(keystore);
|
|
||||||
Assert.assertEquals("file", keystore.getFile());
|
|
||||||
Assert.assertEquals("cp", keystore.getResource());
|
|
||||||
Assert.assertEquals("pw", keystore.getPassword());
|
|
||||||
Assert.assertEquals("private alias", keystore.getPrivateKeyAlias());
|
|
||||||
Assert.assertEquals("private pw", keystore.getPrivateKeyPassword());
|
|
||||||
Assert.assertEquals("cert alias", keystore.getCertificateAlias());
|
|
||||||
Key encryption = sp.getKeys().get(1);
|
|
||||||
Assert.assertTrue(encryption.isEncryption());
|
|
||||||
Assert.assertEquals("private pem", encryption.getPrivateKeyPem());
|
|
||||||
Assert.assertEquals("public pem", encryption.getPublicKeyPem());
|
|
||||||
Assert.assertEquals("policy", sp.getPrincipalNameMapping().getPolicy());
|
|
||||||
Assert.assertEquals("attribute", sp.getPrincipalNameMapping().getAttributeName());
|
|
||||||
Assert.assertTrue(sp.getRoleAttributes().size() == 1);
|
|
||||||
Assert.assertTrue(sp.getRoleAttributes().contains("member"));
|
|
||||||
|
|
||||||
IDP idp = sp.getIdp();
|
|
||||||
Assert.assertEquals("idp", idp.getEntityID());
|
|
||||||
Assert.assertEquals("RSA", idp.getSignatureAlgorithm());
|
|
||||||
Assert.assertEquals("canon", idp.getSignatureCanonicalizationMethod());
|
|
||||||
Assert.assertTrue(idp.getSingleSignOnService().isSignRequest());
|
|
||||||
Assert.assertTrue(idp.getSingleSignOnService().isValidateResponseSignature());
|
|
||||||
Assert.assertEquals("post", idp.getSingleSignOnService().getRequestBinding());
|
|
||||||
Assert.assertEquals("url", idp.getSingleSignOnService().getBindingUrl());
|
|
||||||
|
|
||||||
Assert.assertTrue(idp.getSingleLogoutService().isSignRequest());
|
|
||||||
Assert.assertTrue(idp.getSingleLogoutService().isSignResponse());
|
|
||||||
Assert.assertTrue(idp.getSingleLogoutService().isValidateRequestSignature());
|
|
||||||
Assert.assertTrue(idp.getSingleLogoutService().isValidateResponseSignature());
|
|
||||||
Assert.assertEquals("redirect", idp.getSingleLogoutService().getRequestBinding());
|
|
||||||
Assert.assertEquals("post", idp.getSingleLogoutService().getResponseBinding());
|
|
||||||
Assert.assertEquals("posturl", idp.getSingleLogoutService().getPostBindingUrl());
|
|
||||||
Assert.assertEquals("redirecturl", idp.getSingleLogoutService().getRedirectBindingUrl());
|
|
||||||
|
|
||||||
Assert.assertTrue(idp.getKeys().size() == 1);
|
|
||||||
Assert.assertTrue(idp.getKeys().get(0).isSigning());
|
|
||||||
Assert.assertEquals("cert pem", idp.getKeys().get(0).getCertificatePem());
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
~ 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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak">
|
||||||
|
<EntityDescriptor entityID="http://localhost:8081/auth/realms/master">
|
||||||
|
<IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
<KeyDescriptor use="signing">
|
||||||
|
<dsig:KeyInfo>
|
||||||
|
<dsig:KeyName>rJkJlvowmv1Id74GznieaAC5jU5QQp_ILzuG-GsweTI</dsig:KeyName>
|
||||||
|
<dsig:X509Data>
|
||||||
|
<dsig:X509Certificate>
|
||||||
|
MIICmzCCAYMCBgFX/9ccIDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYxMDI2MDcxMjUwWhcNMjYxMDI2MDgxNDMwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPjDrM890OoFWLIU5xNT+v8B8EkpOGY1y/9Yi/yQd95uG/p5LaywiPsw+lPy4tSn1pH/2SxNDST2zynKPDd1lYDev43m0sC2FfD2H73q3udQRqSOxW1e8FrTrGDIHxb82UNrCPlu+fH+xYSkigrkOvLvPigTwSIcu8vgs0lk9FqJ81ty3Wj2e9lS7JJGAJ3pC7rp39VLdJSKbfyj/v2RYBeG5Pscncl8cjUOHUq5u19hThjkU2jOBzgIK2JS0bNmzSfH1eBTZMoCQBI1UJ1IbA8tqjQwpOXc+JkPBRU8T/JUQoQlSR6DTcPFvDgH2oGZYFHFfUontZqtz8jrIt2pxBAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK5VgQp1x1FKgabFI6W/iGuy9ZCRoAixOOEGGkDps6dOEFgTQKTy5D/FZts9KuNxhhiD+NvS0d5BKYa5ITPLVPnGucgYkZhz+/+GhxmbjeQr0eJPaY7ZgLfH3tPA6tfdIkA0iE1En1sKEwt6R6DZjh9jtP9laoUoddTvYaFLJpZ2u1Ik94q6ZqX0fS/RKchaBHjhg6MtqCcHt07CBKHh8XNmKPXVSJC/p0MjyXv+qLaNNqyaAvAw6P6DX1hNjzrdkuaaHGXhu6kkezZUVlDWAm9cd1ppqalSK6ggy7yMW1NWTd/NYOPsFU2TS8DDPzRo14s1Qvw4v+TY6yT0NURJPQA=
|
||||||
|
</dsig:X509Certificate>
|
||||||
|
</dsig:X509Data>
|
||||||
|
</dsig:KeyInfo>
|
||||||
|
</KeyDescriptor>
|
||||||
|
<KeyDescriptor use="signing">
|
||||||
|
<dsig:KeyInfo>
|
||||||
|
<dsig:KeyName>BzYc4GwL8HVrAhNyNdp-lTah2DvU9jU03kby9Ynohr4</dsig:KeyName>
|
||||||
|
<dsig:X509Data>
|
||||||
|
<dsig:X509Certificate>
|
||||||
|
MIICmzCCAYMCBgFX/9eK7TANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYxMDI2MDcxMzE4WhcNMjYxMDI2MDgxNDU4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCDLT+40/BWzWPSVmpaSaZRs5lBMQ9VP9TCoXkby4PHqxIWRecTPM8fcNkPNPE/tiR2tUIpMXPDzgXNFA/EMoB3V1OEVXPecjKtiZczdR6pi75CBx7PJ2fSXg6xpjhZmHu0k7x591GZdP8Iiu2E6b9QA2p5VXgNgfuP07XzgabnSvIrLG60Imus3u6C2qA/QEuY7EYQWrFooriYLW6B8s3xU8R1a92SLMT8JsfMWXi+1CzAhIbVvdwUwkhVDDhAU6pUek88QQgxodd3FAMksoijCGFN1yrCkovlFhKb3j9AC6Icd9eeJuwYddN/nMeMGEDOeCcAGBACiaUisjUvZDw1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHAHbBI0CRfdw5ZHxHAjgSQvSj41c/4cfwln4Q7X3I5lMBbW3tcgond6Ku9eU46FzG5VpgXIgvEf4u0O9jUnxLlO50+t2SHwQ1RwHdBWQngVSZCRzscq3KrSzx1hx88qLyqcPrr3QtR92fYipDjENxttT/qJtDMrXlwLZEITlHDoneX319USYB9C4zlrCIsQ5XxQTTyCx886Pz15DSVSRxVp61HGk6ROsX/DG5/xwInlzgMZ0r3JWnAjtAaXqUrcwH9FXxco+xkiqKW79bGhWGQI9sXXvQSSNAaENMIUhxtd9uOi1l5e0EkKHE2fHlYyfdUDnFJWwSMXd/NM+hVI4Lw=
|
||||||
|
</dsig:X509Certificate>
|
||||||
|
</dsig:X509Data>
|
||||||
|
</dsig:KeyInfo>
|
||||||
|
</KeyDescriptor>
|
||||||
|
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||||
|
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||||
|
<NameIDFormat>
|
||||||
|
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
||||||
|
</NameIDFormat>
|
||||||
|
<NameIDFormat>
|
||||||
|
urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
||||||
|
</NameIDFormat>
|
||||||
|
<NameIDFormat>
|
||||||
|
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||||
|
</NameIDFormat>
|
||||||
|
<NameIDFormat>
|
||||||
|
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
||||||
|
</NameIDFormat>
|
||||||
|
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||||
|
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||||
|
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||||
|
</IDPSSODescriptor>
|
||||||
|
</EntityDescriptor>
|
||||||
|
</EntitiesDescriptor>
|
|
@ -0,0 +1,81 @@
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter">
|
||||||
|
<SP entityID="sp"
|
||||||
|
sslPolicy="ssl"
|
||||||
|
nameIDPolicyFormat="format"
|
||||||
|
forceAuthentication="true"
|
||||||
|
isPassive="true">
|
||||||
|
<Keys>
|
||||||
|
<Key signing="true" >
|
||||||
|
<KeyStore file="file" resource="cp" password="pw">
|
||||||
|
<PrivateKey alias="private alias" password="private pw"/>
|
||||||
|
<Certificate alias="cert alias"/>
|
||||||
|
</KeyStore>
|
||||||
|
</Key>
|
||||||
|
<Key encryption="true">
|
||||||
|
<PrivateKeyPem>
|
||||||
|
private pem
|
||||||
|
</PrivateKeyPem>
|
||||||
|
<PublicKeyPem>
|
||||||
|
public pem
|
||||||
|
</PublicKeyPem>
|
||||||
|
</Key>
|
||||||
|
</Keys>
|
||||||
|
<PrincipalNameMapping policy="policy" attribute="attribute"/>
|
||||||
|
<RoleIdentifiers>
|
||||||
|
<Attribute name="member"/>
|
||||||
|
</RoleIdentifiers>
|
||||||
|
<IDP entityID="idp"
|
||||||
|
signatureAlgorithm="RSA"
|
||||||
|
signatureCanonicalizationMethod="canon"
|
||||||
|
signaturesRequired="true"
|
||||||
|
>
|
||||||
|
<SingleSignOnService signRequest="true"
|
||||||
|
validateResponseSignature="true"
|
||||||
|
requestBinding="post"
|
||||||
|
bindingUrl="url"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SingleLogoutService
|
||||||
|
validateRequestSignature="true"
|
||||||
|
validateResponseSignature="true"
|
||||||
|
signRequest="true"
|
||||||
|
signResponse="true"
|
||||||
|
requestBinding="redirect"
|
||||||
|
responseBinding="post"
|
||||||
|
postBindingUrl="posturl"
|
||||||
|
redirectBindingUrl="redirecturl"
|
||||||
|
/>
|
||||||
|
<Keys>
|
||||||
|
<Key signing="true">
|
||||||
|
<CertificatePem>cert pem 0</CertificatePem>
|
||||||
|
</Key>
|
||||||
|
<Key signing="true">
|
||||||
|
<CertificatePem>cert pem 1</CertificatePem>
|
||||||
|
</Key>
|
||||||
|
<Key signing="true">
|
||||||
|
<CertificatePem>cert pem 2</CertificatePem>
|
||||||
|
</Key>
|
||||||
|
<Key signing="true">
|
||||||
|
<CertificatePem>cert pem 3</CertificatePem>
|
||||||
|
</Key>
|
||||||
|
</Keys>
|
||||||
|
</IDP>
|
||||||
|
</SP>
|
||||||
|
</keycloak-saml-adapter>
|
|
@ -0,0 +1,81 @@
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter">
|
||||||
|
<SP entityID="sp"
|
||||||
|
sslPolicy="ssl"
|
||||||
|
nameIDPolicyFormat="format"
|
||||||
|
forceAuthentication="true"
|
||||||
|
isPassive="true">
|
||||||
|
<Keys>
|
||||||
|
<Key signing="true" >
|
||||||
|
<KeyStore file="file" resource="cp" password="pw">
|
||||||
|
<PrivateKey alias="private alias" password="private pw"/>
|
||||||
|
<Certificate alias="cert alias"/>
|
||||||
|
</KeyStore>
|
||||||
|
</Key>
|
||||||
|
<Key encryption="true">
|
||||||
|
<PrivateKeyPem>
|
||||||
|
private pem
|
||||||
|
</PrivateKeyPem>
|
||||||
|
<PublicKeyPem>
|
||||||
|
public pem
|
||||||
|
</PublicKeyPem>
|
||||||
|
</Key>
|
||||||
|
</Keys>
|
||||||
|
<PrincipalNameMapping policy="policy" attribute="attribute"/>
|
||||||
|
<RoleIdentifiers>
|
||||||
|
<Attribute name="member"/>
|
||||||
|
</RoleIdentifiers>
|
||||||
|
<IDP entityID="idp"
|
||||||
|
signatureAlgorithm="RSA"
|
||||||
|
signatureCanonicalizationMethod="canon"
|
||||||
|
signaturesRequired="true"
|
||||||
|
>
|
||||||
|
<SingleSignOnService signRequest="true"
|
||||||
|
validateResponseSignature="true"
|
||||||
|
requestBinding="post"
|
||||||
|
bindingUrl="url"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SingleLogoutService
|
||||||
|
validateRequestSignature="true"
|
||||||
|
validateResponseSignature="true"
|
||||||
|
signRequest="true"
|
||||||
|
signResponse="true"
|
||||||
|
requestBinding="redirect"
|
||||||
|
responseBinding="post"
|
||||||
|
postBindingUrl="posturl"
|
||||||
|
redirectBindingUrl="redirecturl"
|
||||||
|
/>
|
||||||
|
<Keys>
|
||||||
|
<Key signing="true">
|
||||||
|
<CertificatePem>
|
||||||
|
cert pem
|
||||||
|
</CertificatePem>
|
||||||
|
</Key>
|
||||||
|
</Keys>
|
||||||
|
<HttpClient allowAnyHostname="true"
|
||||||
|
clientKeystore="ks" clientKeystorePassword="ks-pwd"
|
||||||
|
connectionPoolSize="42"
|
||||||
|
disableTrustManager="true"
|
||||||
|
proxyUrl="pu"
|
||||||
|
truststore="ts" truststorePassword="tsp"
|
||||||
|
/>
|
||||||
|
</IDP>
|
||||||
|
</SP>
|
||||||
|
</keycloak-saml-adapter>
|
|
@ -98,7 +98,7 @@ public final class StringPropertyReplacer
|
||||||
public static String replaceProperties(final String string, final Properties props)
|
public static String replaceProperties(final String string, final Properties props)
|
||||||
{
|
{
|
||||||
final char[] chars = string.toCharArray();
|
final char[] chars = string.toCharArray();
|
||||||
StringBuffer buffer = new StringBuffer();
|
StringBuilder buffer = new StringBuilder();
|
||||||
boolean properties = false;
|
boolean properties = false;
|
||||||
int state = NORMAL;
|
int state = NORMAL;
|
||||||
int start = 0;
|
int start = 0;
|
||||||
|
|
|
@ -26,26 +26,51 @@ public class Time {
|
||||||
|
|
||||||
private static int offset;
|
private static int offset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current time in seconds adjusted by adding {@link #offset) seconds.
|
||||||
|
* @return see description
|
||||||
|
*/
|
||||||
public static int currentTime() {
|
public static int currentTime() {
|
||||||
return ((int) (System.currentTimeMillis() / 1000)) + offset;
|
return ((int) (System.currentTimeMillis() / 1000)) + offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current time in milliseconds adjusted by adding {@link #offset) seconds.
|
||||||
|
* @return see description
|
||||||
|
*/
|
||||||
public static long currentTimeMillis() {
|
public static long currentTimeMillis() {
|
||||||
return System.currentTimeMillis() + (offset * 1000);
|
return System.currentTimeMillis() + (offset * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link Date} object, its value set to time
|
||||||
|
* @param time Time in milliseconds since the epoch
|
||||||
|
* @return see description
|
||||||
|
*/
|
||||||
public static Date toDate(int time) {
|
public static Date toDate(int time) {
|
||||||
return new Date(((long) time ) * 1000);
|
return new Date(((long) time ) * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns time in milliseconds for a time in seconds. No adjustment is made to the parameter.
|
||||||
|
* @param time Time in seconds since the epoch
|
||||||
|
* @return Time in milliseconds
|
||||||
|
*/
|
||||||
public static long toMillis(int time) {
|
public static long toMillis(int time) {
|
||||||
return ((long) time) * 1000;
|
return ((long) time) * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Time offset in seconds that will be added to {@link #currentTime()} and {@link #currentTimeMillis()}.
|
||||||
|
*/
|
||||||
public static int getOffset() {
|
public static int getOffset() {
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets time offset in seconds that will be added to {@link #currentTime()} and {@link #currentTimeMillis()}.
|
||||||
|
* @param offset Offset (in seconds)
|
||||||
|
*/
|
||||||
public static void setOffset(int offset) {
|
public static void setOffset(int offset) {
|
||||||
Time.offset = offset;
|
Time.offset = offset;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
"proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests",
|
"proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests",
|
||||||
"policy-enforcer"
|
"policy-enforcer"
|
||||||
})
|
})
|
||||||
public class AdapterConfig extends BaseAdapterConfig {
|
public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClientConfig {
|
||||||
|
|
||||||
@JsonProperty("allow-any-hostname")
|
@JsonProperty("allow-any-hostname")
|
||||||
protected boolean allowAnyHostname;
|
protected boolean allowAnyHostname;
|
||||||
|
@ -82,6 +82,7 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
@JsonProperty("proxy-url")
|
@JsonProperty("proxy-url")
|
||||||
protected String proxyUrl;
|
protected String proxyUrl;
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isAllowAnyHostname() {
|
public boolean isAllowAnyHostname() {
|
||||||
return allowAnyHostname;
|
return allowAnyHostname;
|
||||||
}
|
}
|
||||||
|
@ -90,6 +91,7 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
this.allowAnyHostname = allowAnyHostname;
|
this.allowAnyHostname = allowAnyHostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isDisableTrustManager() {
|
public boolean isDisableTrustManager() {
|
||||||
return disableTrustManager;
|
return disableTrustManager;
|
||||||
}
|
}
|
||||||
|
@ -98,6 +100,7 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
this.disableTrustManager = disableTrustManager;
|
this.disableTrustManager = disableTrustManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getTruststore() {
|
public String getTruststore() {
|
||||||
return truststore;
|
return truststore;
|
||||||
}
|
}
|
||||||
|
@ -106,6 +109,7 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
this.truststore = truststore;
|
this.truststore = truststore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getTruststorePassword() {
|
public String getTruststorePassword() {
|
||||||
return truststorePassword;
|
return truststorePassword;
|
||||||
}
|
}
|
||||||
|
@ -114,6 +118,7 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
this.truststorePassword = truststorePassword;
|
this.truststorePassword = truststorePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getClientKeystore() {
|
public String getClientKeystore() {
|
||||||
return clientKeystore;
|
return clientKeystore;
|
||||||
}
|
}
|
||||||
|
@ -122,6 +127,7 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
this.clientKeystore = clientKeystore;
|
this.clientKeystore = clientKeystore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getClientKeystorePassword() {
|
public String getClientKeystorePassword() {
|
||||||
return clientKeystorePassword;
|
return clientKeystorePassword;
|
||||||
}
|
}
|
||||||
|
@ -138,6 +144,7 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
this.clientKeyPassword = clientKeyPassword;
|
this.clientKeyPassword = clientKeyPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getConnectionPoolSize() {
|
public int getConnectionPoolSize() {
|
||||||
return connectionPoolSize;
|
return connectionPoolSize;
|
||||||
}
|
}
|
||||||
|
@ -202,6 +209,7 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
this.policyEnforcerConfig = policyEnforcerConfig;
|
this.policyEnforcerConfig = policyEnforcerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getProxyUrl() {
|
public String getProxyUrl() {
|
||||||
return proxyUrl;
|
return proxyUrl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.representations.adapters.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options relevant for configuring http client that can be used by adapter.
|
||||||
|
*
|
||||||
|
* NOTE: keep in sync with adapters/saml/core/src/main/java/org/keycloak/adapters/AdapterHttpClientConfig.java until unified.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public interface AdapterHttpClientConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns truststore filename.
|
||||||
|
*/
|
||||||
|
public String getTruststore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns truststore password.
|
||||||
|
*/
|
||||||
|
public String getTruststorePassword();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns keystore with client keys.
|
||||||
|
*/
|
||||||
|
public String getClientKeystore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns keystore password.
|
||||||
|
*/
|
||||||
|
public String getClientKeystorePassword();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns boolean flag whether any hostname verification is done on the server's
|
||||||
|
* certificate, {@code true} means that verification is not done.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isAllowAnyHostname();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns boolean flag whether any trust management and hostname verification is done.
|
||||||
|
* <p>
|
||||||
|
* <i>NOTE</i> Disabling trust manager is a security hole, so only set this option
|
||||||
|
* if you cannot or do not want to verify the identity of the
|
||||||
|
* host you are communicating with.
|
||||||
|
*/
|
||||||
|
public boolean isDisableTrustManager();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns size of connection pool.
|
||||||
|
*/
|
||||||
|
public int getConnectionPoolSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns URL of HTTP proxy.
|
||||||
|
*/
|
||||||
|
public String getProxyUrl();
|
||||||
|
|
||||||
|
}
|
|
@ -30,6 +30,7 @@
|
||||||
<module name="org.keycloak.keycloak-saml-core-public"/>
|
<module name="org.keycloak.keycloak-saml-core-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-core"/>
|
<module name="org.keycloak.keycloak-saml-core"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
|
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-core"/>
|
<module name="org.keycloak.keycloak-saml-core"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
|
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-adapter-core"/>
|
<module name="org.keycloak.keycloak-saml-adapter-core"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -39,5 +39,6 @@
|
||||||
<module name="org.jboss.logging"/>
|
<module name="org.jboss.logging"/>
|
||||||
<module name="org.jboss.vfs"/>
|
<module name="org.jboss.vfs"/>
|
||||||
<module name="org.jboss.metadata"/>
|
<module name="org.jboss.metadata"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
</imports>
|
</imports>
|
||||||
</module>
|
</module>
|
||||||
<module name="javax.api"/>
|
<module name="javax.api"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
</imports>
|
</imports>
|
||||||
</module>
|
</module>
|
||||||
<module name="javax.api"/>
|
<module name="javax.api"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<module name="org.picketbox"/>
|
<module name="org.picketbox"/>
|
||||||
<module name="org.keycloak.keycloak-adapter-spi"/>
|
<module name="org.keycloak.keycloak-adapter-spi"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
<module name="org.keycloak.keycloak-saml-core-public"/>
|
<module name="org.keycloak.keycloak-saml-core-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-core"/>
|
<module name="org.keycloak.keycloak-saml-core"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
<module name="org.keycloak.keycloak-saml-core-public"/>
|
<module name="org.keycloak.keycloak-saml-core-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-core"/>
|
<module name="org.keycloak.keycloak-saml-core"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
</imports>
|
</imports>
|
||||||
</module>
|
</module>
|
||||||
<module name="javax.api"/>
|
<module name="javax.api"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
</imports>
|
</imports>
|
||||||
</module>
|
</module>
|
||||||
<module name="javax.api"/>
|
<module name="javax.api"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
|
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-adapter-core"/>
|
<module name="org.keycloak.keycloak-saml-adapter-core"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
|
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-adapter-core"/>
|
<module name="org.keycloak.keycloak-saml-adapter-core"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -39,5 +39,6 @@
|
||||||
<module name="org.jboss.vfs"/>
|
<module name="org.jboss.vfs"/>
|
||||||
<module name="org.jboss.as.web-common"/>
|
<module name="org.jboss.as.web-common"/>
|
||||||
<module name="org.jboss.metadata"/>
|
<module name="org.jboss.metadata"/>
|
||||||
|
<module name="org.apache.httpcomponents"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</module>
|
</module>
|
||||||
|
|
0
saml-core/nbproject/project.properties
Normal file
0
saml-core/nbproject/project.properties
Normal file
|
@ -53,6 +53,18 @@
|
||||||
<groupId>org.apache.santuario</groupId>
|
<groupId>org.apache.santuario</groupId>
|
||||||
<artifactId>xmlsec</artifactId>
|
<artifactId>xmlsec</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.12</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hamcrest</groupId>
|
||||||
|
<artifactId>hamcrest-core</artifactId>
|
||||||
|
<version>1.3</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* 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.rotation;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link KeyLocator} that represents a list of multiple {@link KeyLocator}s. Key is searched
|
||||||
|
* from the first to the last {@link KeyLocator} in the order given by the list. If there are
|
||||||
|
* multiple {@link KeyLocator}s providing key with the same key ID, the first matching key is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class CompositeKeyLocator implements KeyLocator, Iterable<Key> {
|
||||||
|
|
||||||
|
private final List<KeyLocator> keyLocators = new LinkedList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Key getKey(String kid) throws KeyManagementException {
|
||||||
|
for (KeyLocator keyLocator : keyLocators) {
|
||||||
|
Key k = keyLocator.getKey(kid);
|
||||||
|
if (k != null) {
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshKeyCache() {
|
||||||
|
for (KeyLocator keyLocator : keyLocators) {
|
||||||
|
keyLocator.refreshKeyCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a given {@link KeyLocator} as the first {@link KeyLocator}.
|
||||||
|
*/
|
||||||
|
public void addFirst(KeyLocator keyLocator) {
|
||||||
|
this.keyLocators.add(0, keyLocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a given {@link KeyLocator} as the last {@link KeyLocator}.
|
||||||
|
*/
|
||||||
|
public void add(KeyLocator keyLocator) {
|
||||||
|
this.keyLocators.add(keyLocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the list of registered {@link KeyLocator}s
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
this.keyLocators.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (this.keyLocators.size() == 1) {
|
||||||
|
return this.keyLocators.get(0).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder("Key locator chain: [");
|
||||||
|
for (Iterator<KeyLocator> it = keyLocators.iterator(); it.hasNext();) {
|
||||||
|
KeyLocator keyLocator = it.next();
|
||||||
|
sb.append(keyLocator.toString());
|
||||||
|
if (it.hasNext()) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.append("]").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Key> iterator() {
|
||||||
|
final Iterator<Iterable<Key>> iterablesIterator = getKeyLocatorIterators().iterator();
|
||||||
|
|
||||||
|
return new JointKeyIterator(iterablesIterator).iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Iterable<Iterable<Key>> getKeyLocatorIterators() {
|
||||||
|
List<Iterable<Key>> res = new LinkedList<>();
|
||||||
|
for (KeyLocator kl : this.keyLocators) {
|
||||||
|
if (kl instanceof Iterable) {
|
||||||
|
res.add(((Iterable<Key>) kl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableCollection(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class JointKeyIterator implements Iterable<Key> {
|
||||||
|
|
||||||
|
// based on http://stackoverflow.com/a/34126154/6930869
|
||||||
|
private final Iterator<Iterable<Key>> iterablesIterator;
|
||||||
|
|
||||||
|
public JointKeyIterator(Iterator<Iterable<Key>> iterablesIterator) {
|
||||||
|
this.iterablesIterator = iterablesIterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Key> iterator() {
|
||||||
|
if (! iterablesIterator.hasNext()) {
|
||||||
|
return Collections.<Key>emptyIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Iterator<Key>() {
|
||||||
|
private Iterator<Key> currentIterator = nextIterator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return currentIterator.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Key next() {
|
||||||
|
final Key next = currentIterator.next();
|
||||||
|
findNext();
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterator<Key> nextIterator() {
|
||||||
|
return iterablesIterator.next().iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterator<Key> findNext() {
|
||||||
|
while (! currentIterator.hasNext()) {
|
||||||
|
if (! iterablesIterator.hasNext()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentIterator = nextIterator();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}.findNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* 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.rotation;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key locator that always returns a specified key.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
|
||||||
|
*/
|
||||||
|
public class HardcodedKeyLocator implements KeyLocator, Iterable<Key> {
|
||||||
|
|
||||||
|
private final Collection<? extends Key> keys;
|
||||||
|
|
||||||
|
public HardcodedKeyLocator(Key key) {
|
||||||
|
this.keys = Collections.singleton(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HardcodedKeyLocator(Collection<? extends Key> keys) {
|
||||||
|
if (keys == null) {
|
||||||
|
throw new NullPointerException("keys");
|
||||||
|
}
|
||||||
|
this.keys = new LinkedList<>(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Key getKey(String kid) {
|
||||||
|
if (this.keys.size() == 1) {
|
||||||
|
return this.keys.iterator().next();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshKeyCache() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "hardcoded keys, count: " + this.keys.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Key> iterator() {
|
||||||
|
return Collections.unmodifiableCollection(keys).iterator();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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.rotation;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines a method for obtaining a security key by ID.
|
||||||
|
* <p>
|
||||||
|
* If the {@code KeyLocator} implementor wants to make all its keys available for iteration,
|
||||||
|
* it should implement {@link Iterable}<{@code T extends }{@link Key}> interface.
|
||||||
|
* The base {@code KeyLocator} does not extend this interface to enable {@code KeyLocators}
|
||||||
|
* that do not support listing their keys.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
|
||||||
|
*/
|
||||||
|
public interface KeyLocator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a key with a particular ID.
|
||||||
|
* @param kid Key ID
|
||||||
|
* @param configuration Configuration
|
||||||
|
* @return key, which should be used for verify signature on given "input"
|
||||||
|
* @throws KeyManagementException
|
||||||
|
*/
|
||||||
|
Key getKey(String kid) throws KeyManagementException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this key locator caches keys in any way, forces this cache cleanup
|
||||||
|
* and refreshing the keys.
|
||||||
|
*/
|
||||||
|
void refreshKeyCache();
|
||||||
|
|
||||||
|
}
|
|
@ -38,11 +38,14 @@ import javax.crypto.spec.SecretKeySpec;
|
||||||
import javax.xml.crypto.dsig.CanonicalizationMethod;
|
import javax.xml.crypto.dsig.CanonicalizationMethod;
|
||||||
import javax.xml.namespace.QName;
|
import javax.xml.namespace.QName;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
import static org.keycloak.common.util.HtmlUtils.escapeAttribute;
|
import static org.keycloak.common.util.HtmlUtils.escapeAttribute;
|
||||||
|
@ -55,6 +58,7 @@ import static org.keycloak.saml.common.util.StringUtil.isNotNull;
|
||||||
public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
||||||
protected static final Logger logger = Logger.getLogger(BaseSAML2BindingBuilder.class);
|
protected static final Logger logger = Logger.getLogger(BaseSAML2BindingBuilder.class);
|
||||||
|
|
||||||
|
protected String signingKeyId;
|
||||||
protected KeyPair signingKeyPair;
|
protected KeyPair signingKeyPair;
|
||||||
protected X509Certificate signingCertificate;
|
protected X509Certificate signingCertificate;
|
||||||
protected boolean sign;
|
protected boolean sign;
|
||||||
|
@ -82,23 +86,27 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
||||||
return (T)this;
|
return (T)this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T signWith(KeyPair keyPair) {
|
public T signWith(String signingKeyId, KeyPair keyPair) {
|
||||||
|
this.signingKeyId = signingKeyId;
|
||||||
this.signingKeyPair = keyPair;
|
this.signingKeyPair = keyPair;
|
||||||
return (T)this;
|
return (T)this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T signWith(PrivateKey privateKey, PublicKey publicKey) {
|
public T signWith(String signingKeyId, PrivateKey privateKey, PublicKey publicKey) {
|
||||||
|
this.signingKeyId = signingKeyId;
|
||||||
this.signingKeyPair = new KeyPair(publicKey, privateKey);
|
this.signingKeyPair = new KeyPair(publicKey, privateKey);
|
||||||
return (T)this;
|
return (T)this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T signWith(KeyPair keyPair, X509Certificate cert) {
|
public T signWith(String signingKeyId, KeyPair keyPair, X509Certificate cert) {
|
||||||
|
this.signingKeyId = signingKeyId;
|
||||||
this.signingKeyPair = keyPair;
|
this.signingKeyPair = keyPair;
|
||||||
this.signingCertificate = cert;
|
this.signingCertificate = cert;
|
||||||
return (T)this;
|
return (T)this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T signWith(PrivateKey privateKey, PublicKey publicKey, X509Certificate cert) {
|
public T signWith(String signingKeyId, PrivateKey privateKey, PublicKey publicKey, X509Certificate cert) {
|
||||||
|
this.signingKeyId = signingKeyId;
|
||||||
this.signingKeyPair = new KeyPair(publicKey, privateKey);
|
this.signingKeyPair = new KeyPair(publicKey, privateKey);
|
||||||
this.signingCertificate = cert;
|
this.signingCertificate = cert;
|
||||||
return (T)this;
|
return (T)this;
|
||||||
|
@ -263,7 +271,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
||||||
samlSignature.setX509Certificate(signingCertificate);
|
samlSignature.setX509Certificate(signingCertificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
samlSignature.signSAMLDocument(samlDocument, signingKeyPair, canonicalizationMethodType);
|
samlSignature.signSAMLDocument(samlDocument, signingKeyId, signingKeyPair, canonicalizationMethodType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void signAssertion(Document samlDocument) throws ProcessingException {
|
public void signAssertion(Document samlDocument) throws ProcessingException {
|
||||||
|
@ -333,7 +341,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
||||||
|
|
||||||
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
|
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
|
||||||
String documentAsString = DocumentUtil.getDocumentAsString(document);
|
String documentAsString = DocumentUtil.getDocumentAsString(document);
|
||||||
logger.debugv("saml docment: {0}", documentAsString);
|
logger.debugv("saml document: {0}", documentAsString);
|
||||||
byte[] responseBytes = documentAsString.getBytes("UTF-8");
|
byte[] responseBytes = documentAsString.getBytes("UTF-8");
|
||||||
|
|
||||||
return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
|
return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
|
||||||
|
@ -358,7 +366,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
||||||
signature.initSign(signingKeyPair.getPrivate());
|
signature.initSign(signingKeyPair.getPrivate());
|
||||||
signature.update(rawQuery.getBytes("UTF-8"));
|
signature.update(rawQuery.getBytes("UTF-8"));
|
||||||
sig = signature.sign();
|
sig = signature.sign();
|
||||||
} catch (Exception e) {
|
} catch (InvalidKeyException | UnsupportedEncodingException | SignatureException e) {
|
||||||
throw new ProcessingException(e);
|
throw new ProcessingException(e);
|
||||||
}
|
}
|
||||||
String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
|
String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
|
||||||
|
|
|
@ -25,15 +25,19 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author pedroigor
|
* @author pedroigor
|
||||||
*/
|
*/
|
||||||
public class SAML2AuthnRequestBuilder {
|
public class SAML2AuthnRequestBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2AuthnRequestBuilder> {
|
||||||
|
|
||||||
private final AuthnRequestType authnRequestType;
|
private final AuthnRequestType authnRequestType;
|
||||||
protected String destination;
|
protected String destination;
|
||||||
protected String issuer;
|
protected String issuer;
|
||||||
|
protected final List<NodeGenerator> extensions = new LinkedList<>();
|
||||||
|
|
||||||
public SAML2AuthnRequestBuilder destination(String destination) {
|
public SAML2AuthnRequestBuilder destination(String destination) {
|
||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
|
@ -45,6 +49,12 @@ public class SAML2AuthnRequestBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SAML2AuthnRequestBuilder addExtension(NodeGenerator extension) {
|
||||||
|
this.extensions.add(extension);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public SAML2AuthnRequestBuilder() {
|
public SAML2AuthnRequestBuilder() {
|
||||||
try {
|
try {
|
||||||
this.authnRequestType = new AuthnRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
|
this.authnRequestType = new AuthnRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
|
||||||
|
@ -90,6 +100,14 @@ public class SAML2AuthnRequestBuilder {
|
||||||
|
|
||||||
authnRequestType.setDestination(URI.create(this.destination));
|
authnRequestType.setDestination(URI.create(this.destination));
|
||||||
|
|
||||||
|
if (! this.extensions.isEmpty()) {
|
||||||
|
ExtensionsType extensionsType = new ExtensionsType();
|
||||||
|
for (NodeGenerator extension : this.extensions) {
|
||||||
|
extensionsType.addExtension(extension);
|
||||||
|
}
|
||||||
|
authnRequestType.setExtensions(extensionsType);
|
||||||
|
}
|
||||||
|
|
||||||
return new SAML2Request().convert(authnRequestType);
|
return new SAML2Request().convert(authnRequestType);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Could not convert " + authnRequestType + " to a document.", e);
|
throw new RuntimeException("Could not convert " + authnRequestType + " to a document.", e);
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
|
|
||||||
package org.keycloak.saml;
|
package org.keycloak.saml;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
@ -32,11 +35,12 @@ import org.w3c.dom.Document;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class SAML2ErrorResponseBuilder {
|
public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2ErrorResponseBuilder> {
|
||||||
|
|
||||||
protected String status;
|
protected String status;
|
||||||
protected String destination;
|
protected String destination;
|
||||||
protected String issuer;
|
protected String issuer;
|
||||||
|
protected final List<NodeGenerator> extensions = new LinkedList<>();
|
||||||
|
|
||||||
public SAML2ErrorResponseBuilder status(String status) {
|
public SAML2ErrorResponseBuilder status(String status) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
|
@ -53,6 +57,11 @@ public class SAML2ErrorResponseBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SAML2ErrorResponseBuilder addExtension(NodeGenerator extension) {
|
||||||
|
this.extensions.add(extension);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Document buildDocument() throws ProcessingException {
|
public Document buildDocument() throws ProcessingException {
|
||||||
|
|
||||||
|
@ -66,6 +75,14 @@ public class SAML2ErrorResponseBuilder {
|
||||||
statusResponse.setIssuer(issuer);
|
statusResponse.setIssuer(issuer);
|
||||||
statusResponse.setDestination(destination);
|
statusResponse.setDestination(destination);
|
||||||
|
|
||||||
|
if (! this.extensions.isEmpty()) {
|
||||||
|
ExtensionsType extensionsType = new ExtensionsType();
|
||||||
|
for (NodeGenerator extension : this.extensions) {
|
||||||
|
extensionsType.addExtension(extension);
|
||||||
|
}
|
||||||
|
statusResponse.setExtensions(extensionsType);
|
||||||
|
}
|
||||||
|
|
||||||
SAML2Response saml2Response = new SAML2Response();
|
SAML2Response saml2Response = new SAML2Response();
|
||||||
return saml2Response.convert(statusResponse);
|
return saml2Response.convert(statusResponse);
|
||||||
} catch (ConfigurationException e) {
|
} catch (ConfigurationException e) {
|
||||||
|
|
|
@ -39,6 +39,9 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
|
||||||
import static org.keycloak.saml.common.util.StringUtil.isNotNull;
|
import static org.keycloak.saml.common.util.StringUtil.isNotNull;
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ import static org.keycloak.saml.common.util.StringUtil.isNotNull;
|
||||||
*
|
*
|
||||||
* @author bburke@redhat.com
|
* @author bburke@redhat.com
|
||||||
*/
|
*/
|
||||||
public class SAML2LoginResponseBuilder {
|
public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2LoginResponseBuilder> {
|
||||||
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
|
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
|
||||||
|
|
||||||
protected String destination;
|
protected String destination;
|
||||||
|
@ -64,6 +67,7 @@ public class SAML2LoginResponseBuilder {
|
||||||
protected String authMethod;
|
protected String authMethod;
|
||||||
protected String requestIssuer;
|
protected String requestIssuer;
|
||||||
protected String sessionIndex;
|
protected String sessionIndex;
|
||||||
|
protected final List<NodeGenerator> extensions = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
public SAML2LoginResponseBuilder sessionIndex(String sessionIndex) {
|
public SAML2LoginResponseBuilder sessionIndex(String sessionIndex) {
|
||||||
|
@ -136,6 +140,12 @@ public class SAML2LoginResponseBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SAML2LoginResponseBuilder addExtension(NodeGenerator extension) {
|
||||||
|
this.extensions.add(extension);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Document buildDocument(ResponseType responseType) throws ConfigurationException, ProcessingException {
|
public Document buildDocument(ResponseType responseType) throws ConfigurationException, ProcessingException {
|
||||||
Document samlResponseDocument = null;
|
Document samlResponseDocument = null;
|
||||||
|
|
||||||
|
@ -207,6 +217,14 @@ public class SAML2LoginResponseBuilder {
|
||||||
assertion.addStatement(authnStatement);
|
assertion.addStatement(authnStatement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! this.extensions.isEmpty()) {
|
||||||
|
ExtensionsType extensionsType = new ExtensionsType();
|
||||||
|
for (NodeGenerator extension : this.extensions) {
|
||||||
|
extensionsType.addExtension(extension);
|
||||||
|
}
|
||||||
|
responseType.setExtensions(extensionsType);
|
||||||
|
}
|
||||||
|
|
||||||
return responseType;
|
return responseType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,18 +27,22 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class SAML2LogoutRequestBuilder {
|
public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2LogoutRequestBuilder> {
|
||||||
protected String userPrincipal;
|
protected String userPrincipal;
|
||||||
protected String userPrincipalFormat;
|
protected String userPrincipalFormat;
|
||||||
protected String sessionIndex;
|
protected String sessionIndex;
|
||||||
protected long assertionExpiration;
|
protected long assertionExpiration;
|
||||||
protected String destination;
|
protected String destination;
|
||||||
protected String issuer;
|
protected String issuer;
|
||||||
|
protected final List<NodeGenerator> extensions = new LinkedList<>();
|
||||||
|
|
||||||
public SAML2LogoutRequestBuilder destination(String destination) {
|
public SAML2LogoutRequestBuilder destination(String destination) {
|
||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
|
@ -50,6 +54,12 @@ public class SAML2LogoutRequestBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SAML2LogoutRequestBuilder addExtension(NodeGenerator extension) {
|
||||||
|
this.extensions.add(extension);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Length of time in seconds the assertion is valid for
|
* Length of time in seconds the assertion is valid for
|
||||||
* See SAML core specification 2.5.1.2 NotOnOrAfter
|
* See SAML core specification 2.5.1.2 NotOnOrAfter
|
||||||
|
@ -99,6 +109,15 @@ public class SAML2LogoutRequestBuilder {
|
||||||
|
|
||||||
if (assertionExpiration > 0) lort.setNotOnOrAfter(XMLTimeUtil.add(lort.getIssueInstant(), assertionExpiration * 1000));
|
if (assertionExpiration > 0) lort.setNotOnOrAfter(XMLTimeUtil.add(lort.getIssueInstant(), assertionExpiration * 1000));
|
||||||
lort.setDestination(URI.create(destination));
|
lort.setDestination(URI.create(destination));
|
||||||
|
|
||||||
|
if (! this.extensions.isEmpty()) {
|
||||||
|
ExtensionsType extensionsType = new ExtensionsType();
|
||||||
|
for (NodeGenerator extension : this.extensions) {
|
||||||
|
extensionsType.addExtension(extension);
|
||||||
|
}
|
||||||
|
lort.setExtensions(extensionsType);
|
||||||
|
}
|
||||||
|
|
||||||
return lort;
|
return lort;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,16 +31,20 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class SAML2LogoutResponseBuilder {
|
public class SAML2LogoutResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2LogoutResponseBuilder> {
|
||||||
|
|
||||||
protected String logoutRequestID;
|
protected String logoutRequestID;
|
||||||
protected String destination;
|
protected String destination;
|
||||||
protected String issuer;
|
protected String issuer;
|
||||||
|
protected final List<NodeGenerator> extensions = new LinkedList<>();
|
||||||
|
|
||||||
public SAML2LogoutResponseBuilder logoutRequestID(String logoutRequestID) {
|
public SAML2LogoutResponseBuilder logoutRequestID(String logoutRequestID) {
|
||||||
this.logoutRequestID = logoutRequestID;
|
this.logoutRequestID = logoutRequestID;
|
||||||
|
@ -57,6 +61,11 @@ public class SAML2LogoutResponseBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SAML2LogoutResponseBuilder addExtension(NodeGenerator extension) {
|
||||||
|
this.extensions.add(extension);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Document buildDocument() throws ProcessingException {
|
public Document buildDocument() throws ProcessingException {
|
||||||
Document samlResponse = null;
|
Document samlResponse = null;
|
||||||
|
@ -77,6 +86,14 @@ public class SAML2LogoutResponseBuilder {
|
||||||
statusResponse.setIssuer(issuer);
|
statusResponse.setIssuer(issuer);
|
||||||
statusResponse.setDestination(destination);
|
statusResponse.setDestination(destination);
|
||||||
|
|
||||||
|
if (! this.extensions.isEmpty()) {
|
||||||
|
ExtensionsType extensionsType = new ExtensionsType();
|
||||||
|
for (NodeGenerator extension : this.extensions) {
|
||||||
|
extensionsType.addExtension(extension);
|
||||||
|
}
|
||||||
|
statusResponse.setExtensions(extensionsType);
|
||||||
|
}
|
||||||
|
|
||||||
SAML2Response saml2Response = new SAML2Response();
|
SAML2Response saml2Response = new SAML2Response();
|
||||||
samlResponse = saml2Response.convert(statusResponse);
|
samlResponse = saml2Response.convert(statusResponse);
|
||||||
} catch (ConfigurationException e) {
|
} catch (ConfigurationException e) {
|
||||||
|
|
|
@ -22,21 +22,14 @@ package org.keycloak.saml;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class SPMetadataDescriptor {
|
public class SPMetadataDescriptor {
|
||||||
public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String certificatePem) {
|
|
||||||
|
public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String signingCerts) {
|
||||||
String descriptor =
|
String descriptor =
|
||||||
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
|
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
|
||||||
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\"\n" +
|
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\"\n" +
|
||||||
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n";
|
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n";
|
||||||
if (wantAuthnRequestsSigned) {
|
if (wantAuthnRequestsSigned && signingCerts != null) {
|
||||||
descriptor +=
|
descriptor += signingCerts;
|
||||||
" <KeyDescriptor use=\"signing\">\n" +
|
|
||||||
" <dsig:KeyInfo xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
|
|
||||||
" <dsig:X509Data>\n" +
|
|
||||||
" <dsig:X509Certificate>\n" + certificatePem + "\n" +
|
|
||||||
" </dsig:X509Certificate>\n" +
|
|
||||||
" </dsig:X509Data>\n" +
|
|
||||||
" </dsig:KeyInfo>\n" +
|
|
||||||
" </KeyDescriptor>\n";
|
|
||||||
}
|
}
|
||||||
descriptor +=
|
descriptor +=
|
||||||
" <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" +
|
" <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" +
|
||||||
|
@ -44,10 +37,34 @@ public class SPMetadataDescriptor {
|
||||||
" </NameIDFormat>\n" +
|
" </NameIDFormat>\n" +
|
||||||
" <AssertionConsumerService\n" +
|
" <AssertionConsumerService\n" +
|
||||||
" Binding=\"" + binding + "\" Location=\"" + assertionEndpoint + "\"\n" +
|
" Binding=\"" + binding + "\" Location=\"" + assertionEndpoint + "\"\n" +
|
||||||
" index=\"1\" isDefault=\"true\" />\n";
|
" index=\"1\" isDefault=\"true\" />\n" +
|
||||||
descriptor +=
|
|
||||||
" </SPSSODescriptor>\n" +
|
" </SPSSODescriptor>\n" +
|
||||||
"</EntityDescriptor>\n";
|
"</EntityDescriptor>\n";
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String xmlKeyInfo(String indentation, String keyId, String pemEncodedCertificate, String purpose, boolean declareDSigNamespace) {
|
||||||
|
if (pemEncodedCertificate == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder target = new StringBuilder()
|
||||||
|
.append(indentation).append("<KeyDescriptor use=\"").append(purpose).append("\">\n")
|
||||||
|
.append(indentation).append(" <dsig:KeyInfo").append(declareDSigNamespace ? " xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" : ">\n");
|
||||||
|
|
||||||
|
if (keyId != null) {
|
||||||
|
target.append(indentation).append(" <dsig:KeyName>").append(keyId).append("</dsig:KeyName>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
target
|
||||||
|
.append(indentation).append(" <dsig:X509Data>\n")
|
||||||
|
.append(indentation).append(" <dsig:X509Certificate>").append(pemEncodedCertificate).append("</dsig:X509Certificate>\n")
|
||||||
|
.append(indentation).append(" </dsig:X509Data>\n")
|
||||||
|
.append(indentation).append(" </dsig:KeyInfo>\n")
|
||||||
|
.append(indentation).append("</KeyDescriptor>\n")
|
||||||
|
;
|
||||||
|
|
||||||
|
return target.toString();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* 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.saml;
|
||||||
|
|
||||||
|
import javax.xml.stream.XMLStreamWriter;
|
||||||
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementations of this interface are builders that can register <samlp:Extensions>
|
||||||
|
* content providers.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public interface SamlProtocolExtensionsAwareBuilder<T> {
|
||||||
|
|
||||||
|
public interface NodeGenerator {
|
||||||
|
/**
|
||||||
|
* Generate contents of the <samlp:Extensions> tag. When this method is invoked,
|
||||||
|
* the writer has already emitted the <samlp:Extensions> start tag.
|
||||||
|
*
|
||||||
|
* @param writer Writer to use for producing XML output
|
||||||
|
* @throws ProcessingException If any exception fails
|
||||||
|
*/
|
||||||
|
void write(XMLStreamWriter writer) throws ProcessingException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a given node subtree as a SAML protocol extension into the SAML protocol message.
|
||||||
|
*
|
||||||
|
* @param extension
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
T addExtension(NodeGenerator extension);
|
||||||
|
}
|
|
@ -450,6 +450,11 @@ public class DefaultPicketLinkLogger implements PicketLinkLogger {
|
||||||
return new RuntimeException(ErrorCodes.EXPECTED_TAG + tag + ">. Found <" + foundElementTag + ">");
|
return new RuntimeException(ErrorCodes.EXPECTED_TAG + tag + ">. Found <" + foundElementTag + ">");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RuntimeException parserExpectedNamespace(String ns, String foundElementNs) {
|
||||||
|
return new RuntimeException(ErrorCodes.EXPECTED_NAMESPACE + ns + ">. Found <" + foundElementNs + ">");
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*(non-Javadoc)
|
*(non-Javadoc)
|
||||||
*
|
*
|
||||||
|
@ -2378,4 +2383,10 @@ public class DefaultPicketLinkLogger implements PicketLinkLogger {
|
||||||
return new ProcessingException("Wrong audience [" + serviceURL + "].");
|
return new ProcessingException("Wrong audience [" + serviceURL + "].");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProcessingException samlExtensionUnknownChild(Class<?> clazz) {
|
||||||
|
return new ProcessingException("Unknown child type specified for extension: "
|
||||||
|
+ (clazz == null ? "<null>" : clazz.getSimpleName())
|
||||||
|
+ ".");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,8 @@ public interface ErrorCodes {
|
||||||
|
|
||||||
String EXPECTED_TAG = "PL00066: Parser : Expected start tag:";
|
String EXPECTED_TAG = "PL00066: Parser : Expected start tag:";
|
||||||
|
|
||||||
|
String EXPECTED_NAMESPACE = "PL00107: Parser : Expected start element namespace:";
|
||||||
|
|
||||||
String EXPECTED_TEXT_VALUE = "PL00071: Parser: Expected text value:";
|
String EXPECTED_TEXT_VALUE = "PL00071: Parser: Expected text value:";
|
||||||
|
|
||||||
String EXPECTED_END_TAG = "PL00066: Parser : Expected end tag:";
|
String EXPECTED_END_TAG = "PL00066: Parser : Expected end tag:";
|
||||||
|
|
|
@ -296,6 +296,14 @@ public interface PicketLinkLogger {
|
||||||
*/
|
*/
|
||||||
RuntimeException parserExpectedTag(String tag, String foundElementTag);
|
RuntimeException parserExpectedTag(String tag, String foundElementTag);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ns
|
||||||
|
* @param foundElementNs
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
RuntimeException parserExpectedNamespace(String ns, String foundElementNs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param elementName
|
* @param elementName
|
||||||
*
|
*
|
||||||
|
@ -1219,4 +1227,6 @@ public interface PicketLinkLogger {
|
||||||
RuntimeException parserFeatureNotSupported(String feature);
|
RuntimeException parserFeatureNotSupported(String feature);
|
||||||
|
|
||||||
ProcessingException samlAssertionWrongAudience(String serviceURL);
|
ProcessingException samlAssertionWrongAudience(String serviceURL);
|
||||||
|
|
||||||
|
ProcessingException samlExtensionUnknownChild(Class<?> clazz);
|
||||||
}
|
}
|
|
@ -35,8 +35,8 @@ import javax.xml.crypto.dsig.XMLSignatureException;
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that deals with SAML2 Signature
|
* Class that deals with SAML2 Signature
|
||||||
|
@ -121,7 +121,7 @@ public class SAML2Signature {
|
||||||
* @throws MarshalException
|
* @throws MarshalException
|
||||||
* @throws GeneralSecurityException
|
* @throws GeneralSecurityException
|
||||||
*/
|
*/
|
||||||
public Document sign(Document doc, String referenceID, KeyPair keyPair, String canonicalizationMethodType) throws ParserConfigurationException,
|
public Document sign(Document doc, String referenceID, String keyId, KeyPair keyPair, String canonicalizationMethodType) throws ParserConfigurationException,
|
||||||
GeneralSecurityException, MarshalException, XMLSignatureException {
|
GeneralSecurityException, MarshalException, XMLSignatureException {
|
||||||
String referenceURI = "#" + referenceID;
|
String referenceURI = "#" + referenceID;
|
||||||
|
|
||||||
|
@ -130,6 +130,7 @@ public class SAML2Signature {
|
||||||
if (sibling != null) {
|
if (sibling != null) {
|
||||||
SignatureUtilTransferObject dto = new SignatureUtilTransferObject();
|
SignatureUtilTransferObject dto = new SignatureUtilTransferObject();
|
||||||
dto.setDocumentToBeSigned(doc);
|
dto.setDocumentToBeSigned(doc);
|
||||||
|
dto.setKeyId(keyId);
|
||||||
dto.setKeyPair(keyPair);
|
dto.setKeyPair(keyPair);
|
||||||
dto.setDigestMethod(digestMethod);
|
dto.setDigestMethod(digestMethod);
|
||||||
dto.setSignatureMethod(signatureMethod);
|
dto.setSignatureMethod(signatureMethod);
|
||||||
|
@ -142,7 +143,7 @@ public class SAML2Signature {
|
||||||
|
|
||||||
return XMLSignatureUtil.sign(dto, canonicalizationMethodType);
|
return XMLSignatureUtil.sign(dto, canonicalizationMethodType);
|
||||||
}
|
}
|
||||||
return XMLSignatureUtil.sign(doc, keyPair, digestMethod, signatureMethod, referenceURI, canonicalizationMethodType);
|
return XMLSignatureUtil.sign(doc, keyId, keyPair, digestMethod, signatureMethod, referenceURI, canonicalizationMethodType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -153,12 +154,12 @@ public class SAML2Signature {
|
||||||
*
|
*
|
||||||
* @throws org.keycloak.saml.common.exceptions.ProcessingException
|
* @throws org.keycloak.saml.common.exceptions.ProcessingException
|
||||||
*/
|
*/
|
||||||
public void signSAMLDocument(Document samlDocument, KeyPair keypair, String canonicalizationMethodType) throws ProcessingException {
|
public void signSAMLDocument(Document samlDocument, String keyId, KeyPair keypair, String canonicalizationMethodType) throws ProcessingException {
|
||||||
// Get the ID from the root
|
// Get the ID from the root
|
||||||
String id = samlDocument.getDocumentElement().getAttribute(ID_ATTRIBUTE_NAME);
|
String id = samlDocument.getDocumentElement().getAttribute(ID_ATTRIBUTE_NAME);
|
||||||
try {
|
try {
|
||||||
sign(samlDocument, id, keypair, canonicalizationMethodType);
|
sign(samlDocument, id, keyId, keypair, canonicalizationMethodType);
|
||||||
} catch (Exception e) {
|
} catch (ParserConfigurationException | GeneralSecurityException | MarshalException | XMLSignatureException e) {
|
||||||
throw new ProcessingException(logger.signatureError(e));
|
throw new ProcessingException(logger.signatureError(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,20 +168,18 @@ public class SAML2Signature {
|
||||||
* Validate the SAML2 Document
|
* Validate the SAML2 Document
|
||||||
*
|
*
|
||||||
* @param signedDocument
|
* @param signedDocument
|
||||||
* @param publicKey
|
* @param keyLocator
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*
|
*
|
||||||
* @throws ProcessingException
|
* @throws ProcessingException
|
||||||
*/
|
*/
|
||||||
public boolean validate(Document signedDocument, PublicKey publicKey) throws ProcessingException {
|
public boolean validate(Document signedDocument, KeyLocator keyLocator) throws ProcessingException {
|
||||||
try {
|
try {
|
||||||
configureIdAttribute(signedDocument);
|
configureIdAttribute(signedDocument);
|
||||||
return XMLSignatureUtil.validate(signedDocument, publicKey);
|
return XMLSignatureUtil.validate(signedDocument, keyLocator);
|
||||||
} catch (MarshalException me) {
|
} catch (MarshalException | XMLSignatureException me) {
|
||||||
throw new ProcessingException(logger.signatureError(me));
|
throw new ProcessingException(logger.signatureError(me));
|
||||||
} catch (XMLSignatureException xse) {
|
|
||||||
throw new ProcessingException(logger.signatureError(xse));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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.saml.processing.api.util;
|
||||||
|
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyName;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.X509Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tools for {@link KeyInfo} object manipulation.
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class KeyInfoTools {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first object of the given class from the given Iterable.
|
||||||
|
* @param <T>
|
||||||
|
* @param objects
|
||||||
|
* @param clazz
|
||||||
|
* @return The object or {@code null} if not found.
|
||||||
|
*/
|
||||||
|
public static <T> T getContent(Iterable<Object> objects, Class<T> clazz) {
|
||||||
|
for (Object o : objects) {
|
||||||
|
if (clazz.isInstance(o)) {
|
||||||
|
return (T) o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static KeyName getKeyName(KeyInfo keyInfo) {
|
||||||
|
return getContent(keyInfo.getContent(), KeyName.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static X509Data getX509Data(KeyInfo keyInfo) {
|
||||||
|
return getContent(keyInfo.getContent(), X509Data.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static X509Certificate getX509Certificate(KeyInfo keyInfo) {
|
||||||
|
X509Data d = getX509Data(keyInfo);
|
||||||
|
return d == null ? null : getContent(d.getContent(), X509Certificate.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -58,6 +58,8 @@ public class SAMLArtifactResolveParser extends SAMLRequestAbstractParser impleme
|
||||||
continue;
|
continue;
|
||||||
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
||||||
continue;
|
continue;
|
||||||
|
} else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
|
||||||
|
continue;
|
||||||
} else
|
} else
|
||||||
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
|
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
|
||||||
+ startElement.getLocation());
|
+ startElement.getLocation());
|
||||||
|
|
|
@ -68,6 +68,9 @@ public class SAMLArtifactResponseParser extends SAMLStatusResponseTypeParser imp
|
||||||
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
||||||
Element sig = StaxParserUtil.getDOMElement(xmlEventReader);
|
Element sig = StaxParserUtil.getDOMElement(xmlEventReader);
|
||||||
response.setSignature(sig);
|
response.setSignature(sig);
|
||||||
|
} else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
|
||||||
|
SAMLExtensionsParser extensionsParser = new SAMLExtensionsParser();
|
||||||
|
response.setExtensions(extensionsParser.parse(xmlEventReader));
|
||||||
} else if (JBossSAMLConstants.AUTHN_REQUEST.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.AUTHN_REQUEST.get().equals(elementName)) {
|
||||||
SAMLAuthNRequestParser authnParser = new SAMLAuthNRequestParser();
|
SAMLAuthNRequestParser authnParser = new SAMLAuthNRequestParser();
|
||||||
AuthnRequestType authn = (AuthnRequestType) authnParser.parse(xmlEventReader);
|
AuthnRequestType authn = (AuthnRequestType) authnParser.parse(xmlEventReader);
|
||||||
|
|
|
@ -60,6 +60,8 @@ public class SAMLAttributeQueryParser extends SAMLRequestAbstractParser implemen
|
||||||
continue;
|
continue;
|
||||||
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
||||||
continue;
|
continue;
|
||||||
|
} else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
|
||||||
|
continue;
|
||||||
} else
|
} else
|
||||||
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
|
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
|
||||||
+ startElement.getLocation());
|
+ startElement.getLocation());
|
||||||
|
|
|
@ -76,6 +76,8 @@ public class SAMLAuthNRequestParser extends SAMLRequestAbstractParser implements
|
||||||
continue;
|
continue;
|
||||||
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
||||||
continue;
|
continue;
|
||||||
|
} else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
|
||||||
|
continue;
|
||||||
} else
|
} else
|
||||||
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
|
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
|
||||||
+ startElement.getLocation());
|
+ startElement.getLocation());
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* 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.saml.processing.core.parsers.saml;
|
||||||
|
|
||||||
|
import javax.xml.namespace.QName;
|
||||||
|
import javax.xml.stream.XMLEventReader;
|
||||||
|
import javax.xml.stream.events.EndElement;
|
||||||
|
import javax.xml.stream.events.StartElement;
|
||||||
|
import javax.xml.stream.events.XMLEvent;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
import org.keycloak.saml.common.PicketLinkLogger;
|
||||||
|
import org.keycloak.saml.common.PicketLinkLoggerFactory;
|
||||||
|
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.parsers.ParserNamespaceSupport;
|
||||||
|
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses <samlp:Extensions> SAML2 element into series of DOM nodes.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class SAMLExtensionsParser implements ParserNamespaceSupport {
|
||||||
|
|
||||||
|
private static final String EXTENSIONS = JBossSAMLConstants.EXTENSIONS.get();
|
||||||
|
|
||||||
|
private static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExtensionsType parse(XMLEventReader xmlEventReader) throws ParsingException {
|
||||||
|
// Get the startelement
|
||||||
|
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
|
StaxParserUtil.validate(startElement, EXTENSIONS);
|
||||||
|
|
||||||
|
ExtensionsType extensions = new ExtensionsType();
|
||||||
|
|
||||||
|
while (xmlEventReader.hasNext()) {
|
||||||
|
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
||||||
|
if (xmlEvent instanceof EndElement) {
|
||||||
|
EndElement endElement = (EndElement) xmlEvent;
|
||||||
|
if (StaxParserUtil.matches(endElement, EXTENSIONS)) {
|
||||||
|
endElement = StaxParserUtil.getNextEndElement(xmlEventReader);
|
||||||
|
break;
|
||||||
|
} else
|
||||||
|
throw logger.parserUnknownEndElement(StaxParserUtil.getEndElementName(endElement));
|
||||||
|
}
|
||||||
|
|
||||||
|
startElement = StaxParserUtil.peekNextStartElement(xmlEventReader);
|
||||||
|
if (startElement == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
extensions.addExtension(StaxParserUtil.getDOMElement(xmlEventReader));
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(QName qname) {
|
||||||
|
String nsURI = qname.getNamespaceURI();
|
||||||
|
String localPart = qname.getLocalPart();
|
||||||
|
|
||||||
|
return nsURI.equals(JBossSAMLURIConstants.PROTOCOL_NSURI.get())
|
||||||
|
&& localPart.equals(JBossSAMLConstants.EXTENSIONS.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -71,6 +71,9 @@ public class SAMLResponseParser extends SAMLStatusResponseTypeParser implements
|
||||||
} else if (JBossSAMLConstants.ASSERTION.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.ASSERTION.get().equals(elementName)) {
|
||||||
SAMLAssertionParser assertionParser = new SAMLAssertionParser();
|
SAMLAssertionParser assertionParser = new SAMLAssertionParser();
|
||||||
response.addAssertion(new RTChoiceType((AssertionType) assertionParser.parse(xmlEventReader)));
|
response.addAssertion(new RTChoiceType((AssertionType) assertionParser.parse(xmlEventReader)));
|
||||||
|
} else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
|
||||||
|
SAMLExtensionsParser extensionsParser = new SAMLExtensionsParser();
|
||||||
|
response.setExtensions(extensionsParser.parse(xmlEventReader));
|
||||||
} else if (JBossSAMLConstants.STATUS.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.STATUS.get().equals(elementName)) {
|
||||||
response.setStatus(parseStatus(xmlEventReader));
|
response.setStatus(parseStatus(xmlEventReader));
|
||||||
} else if (JBossSAMLConstants.ENCRYPTED_ASSERTION.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.ENCRYPTED_ASSERTION.get().equals(elementName)) {
|
||||||
|
|
|
@ -74,6 +74,8 @@ public class SAMLSloRequestParser extends SAMLRequestAbstractParser implements P
|
||||||
continue;
|
continue;
|
||||||
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
||||||
continue;
|
continue;
|
||||||
|
} else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
|
||||||
|
continue;
|
||||||
} else
|
} else
|
||||||
throw logger.parserUnknownTag(elementName, startElement.getLocation());
|
throw logger.parserUnknownTag(elementName, startElement.getLocation());
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,9 @@ public class SAMLSloResponseParser extends SAMLStatusResponseTypeParser implemen
|
||||||
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
|
||||||
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
StaxParserUtil.bypassElementBlock(xmlEventReader, JBossSAMLConstants.SIGNATURE.get());
|
StaxParserUtil.bypassElementBlock(xmlEventReader, JBossSAMLConstants.SIGNATURE.get());
|
||||||
|
} else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
|
||||||
|
SAMLExtensionsParser extensionsParser = new SAMLExtensionsParser();
|
||||||
|
response.setExtensions(extensionsParser.parse(xmlEventReader));
|
||||||
} else if (JBossSAMLConstants.STATUS.get().equals(elementName)) {
|
} else if (JBossSAMLConstants.STATUS.get().equals(elementName)) {
|
||||||
response.setStatus(parseStatus(xmlEventReader));
|
response.setStatus(parseStatus(xmlEventReader));
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ import java.security.PublicKey;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to deal with assertions
|
* Utility to deal with assertions
|
||||||
|
@ -276,7 +277,7 @@ public class AssertionUtil {
|
||||||
Node n = doc.importNode(assertionElement, true);
|
Node n = doc.importNode(assertionElement, true);
|
||||||
doc.appendChild(n);
|
doc.appendChild(n);
|
||||||
|
|
||||||
return new SAML2Signature().validate(doc, publicKey);
|
return new SAML2Signature().validate(doc, new HardcodedKeyLocator(publicKey));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.signatureAssertionValidationError(e);
|
logger.signatureAssertionValidationError(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,8 +43,12 @@ import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder;
|
||||||
|
|
||||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
|
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
|
||||||
|
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base Class for the Stax writers for SAML
|
* Base Class for the Stax writers for SAML
|
||||||
|
@ -244,6 +248,28 @@ public class BaseWriter {
|
||||||
StaxUtil.flush(writer);
|
StaxUtil.flush(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void write(ExtensionsType extensions) throws ProcessingException {
|
||||||
|
if (extensions.getAny().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.EXTENSIONS.get(), PROTOCOL_NSURI.get());
|
||||||
|
|
||||||
|
for (Object o : extensions.getAny()) {
|
||||||
|
if (o instanceof Node) {
|
||||||
|
StaxUtil.writeDOMNode(writer, (Node) o);
|
||||||
|
} else if (o instanceof SamlProtocolExtensionsAwareBuilder.NodeGenerator) {
|
||||||
|
SamlProtocolExtensionsAwareBuilder.NodeGenerator ng = (SamlProtocolExtensionsAwareBuilder.NodeGenerator) o;
|
||||||
|
ng.write(writer);
|
||||||
|
} else {
|
||||||
|
throw logger.samlExtensionUnknownChild(o == null ? null : o.getClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StaxUtil.writeEndElement(writer);
|
||||||
|
StaxUtil.flush(writer);
|
||||||
|
}
|
||||||
|
|
||||||
private void write(SubjectConfirmationType subjectConfirmationType) throws ProcessingException {
|
private void write(SubjectConfirmationType subjectConfirmationType) throws ProcessingException {
|
||||||
StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.SUBJECT_CONFIRMATION.get(),
|
StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.SUBJECT_CONFIRMATION.get(),
|
||||||
ASSERTION_NSURI.get());
|
ASSERTION_NSURI.get());
|
||||||
|
|
|
@ -36,6 +36,7 @@ import javax.xml.namespace.QName;
|
||||||
import javax.xml.stream.XMLStreamWriter;
|
import javax.xml.stream.XMLStreamWriter;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
|
||||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
|
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
|
||||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
|
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
|
||||||
|
@ -122,6 +123,11 @@ public class SAMLRequestWriter extends BaseWriter {
|
||||||
StaxUtil.writeDOMElement(writer, sig);
|
StaxUtil.writeDOMElement(writer, sig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExtensionsType extensions = request.getExtensions();
|
||||||
|
if (extensions != null && ! extensions.getAny().isEmpty()) {
|
||||||
|
write(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
NameIDPolicyType nameIDPolicy = request.getNameIDPolicy();
|
NameIDPolicyType nameIDPolicy = request.getNameIDPolicy();
|
||||||
if (nameIDPolicy != null) {
|
if (nameIDPolicy != null) {
|
||||||
write(nameIDPolicy);
|
write(nameIDPolicy);
|
||||||
|
@ -171,6 +177,11 @@ public class SAMLRequestWriter extends BaseWriter {
|
||||||
StaxUtil.writeDOMElement(writer, signature);
|
StaxUtil.writeDOMElement(writer, signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExtensionsType extensions = logOutRequest.getExtensions();
|
||||||
|
if (extensions != null && ! extensions.getAny().isEmpty()) {
|
||||||
|
write(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
NameIDType nameID = logOutRequest.getNameID();
|
NameIDType nameID = logOutRequest.getNameID();
|
||||||
if (nameID != null) {
|
if (nameID != null) {
|
||||||
write(nameID, new QName(ASSERTION_NSURI.get(), JBossSAMLConstants.NAMEID.get(), ASSERTION_PREFIX));
|
write(nameID, new QName(ASSERTION_NSURI.get(), JBossSAMLConstants.NAMEID.get(), ASSERTION_PREFIX));
|
||||||
|
@ -278,6 +289,11 @@ public class SAMLRequestWriter extends BaseWriter {
|
||||||
if (sig != null) {
|
if (sig != null) {
|
||||||
StaxUtil.writeDOMElement(writer, sig);
|
StaxUtil.writeDOMElement(writer, sig);
|
||||||
}
|
}
|
||||||
|
ExtensionsType extensions = request.getExtensions();
|
||||||
|
if (extensions != null && ! extensions.getAny().isEmpty()) {
|
||||||
|
write(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
String artifact = request.getArtifact();
|
String artifact = request.getArtifact();
|
||||||
if (StringUtil.isNotNull(artifact)) {
|
if (StringUtil.isNotNull(artifact)) {
|
||||||
StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.ARTIFACT.get(), PROTOCOL_NSURI.get());
|
StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.ARTIFACT.get(), PROTOCOL_NSURI.get());
|
||||||
|
@ -315,6 +331,10 @@ public class SAMLRequestWriter extends BaseWriter {
|
||||||
if (sig != null) {
|
if (sig != null) {
|
||||||
StaxUtil.writeDOMElement(writer, sig);
|
StaxUtil.writeDOMElement(writer, sig);
|
||||||
}
|
}
|
||||||
|
ExtensionsType extensions = request.getExtensions();
|
||||||
|
if (extensions != null && ! extensions.getAny().isEmpty()) {
|
||||||
|
write(extensions);
|
||||||
|
}
|
||||||
SubjectType subject = request.getSubject();
|
SubjectType subject = request.getSubject();
|
||||||
if (subject != null) {
|
if (subject != null) {
|
||||||
write(subject);
|
write(subject);
|
||||||
|
|
|
@ -37,6 +37,7 @@ import javax.xml.namespace.QName;
|
||||||
import javax.xml.stream.XMLStreamWriter;
|
import javax.xml.stream.XMLStreamWriter;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write a SAML Response to stream
|
* Write a SAML Response to stream
|
||||||
|
@ -78,6 +79,10 @@ public class SAMLResponseWriter extends BaseWriter {
|
||||||
if (sig != null) {
|
if (sig != null) {
|
||||||
StaxUtil.writeDOMElement(writer, sig);
|
StaxUtil.writeDOMElement(writer, sig);
|
||||||
}
|
}
|
||||||
|
ExtensionsType extensions = response.getExtensions();
|
||||||
|
if (extensions != null && extensions.getAny() != null && ! extensions.getAny().isEmpty()) {
|
||||||
|
write(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
StatusType status = response.getStatus();
|
StatusType status = response.getStatus();
|
||||||
write(status);
|
write(status);
|
||||||
|
@ -119,6 +124,10 @@ public class SAMLResponseWriter extends BaseWriter {
|
||||||
if (sig != null) {
|
if (sig != null) {
|
||||||
StaxUtil.writeDOMElement(writer, sig);
|
StaxUtil.writeDOMElement(writer, sig);
|
||||||
}
|
}
|
||||||
|
ExtensionsType extensions = response.getExtensions();
|
||||||
|
if (extensions != null && extensions.getAny() != null && ! extensions.getAny().isEmpty()) {
|
||||||
|
write(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
StatusType status = response.getStatus();
|
StatusType status = response.getStatus();
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
|
@ -163,6 +172,15 @@ public class SAMLResponseWriter extends BaseWriter {
|
||||||
NameIDType issuer = response.getIssuer();
|
NameIDType issuer = response.getIssuer();
|
||||||
write(issuer, new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ISSUER.get(), ASSERTION_PREFIX));
|
write(issuer, new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ISSUER.get(), ASSERTION_PREFIX));
|
||||||
|
|
||||||
|
Element sig = response.getSignature();
|
||||||
|
if (sig != null) {
|
||||||
|
StaxUtil.writeDOMElement(writer, sig);
|
||||||
|
}
|
||||||
|
ExtensionsType extensions = response.getExtensions();
|
||||||
|
if (extensions != null && extensions.getAny() != null && ! extensions.getAny().isEmpty()) {
|
||||||
|
write(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
StatusType status = response.getStatus();
|
StatusType status = response.getStatus();
|
||||||
write(status);
|
write(status);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.saml.processing.core.util;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import javax.xml.stream.XMLStreamWriter;
|
||||||
|
import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder;
|
||||||
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
|
import org.keycloak.saml.common.util.StaxUtil;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class KeycloakKeySamlExtensionGenerator implements SamlProtocolExtensionsAwareBuilder.NodeGenerator {
|
||||||
|
|
||||||
|
public static final String NS_URI = "urn:keycloak:ext:key:1.0";
|
||||||
|
|
||||||
|
public static final String NS_PREFIX = "kckey";
|
||||||
|
|
||||||
|
public static final String KC_KEY_INFO_ELEMENT_NAME = "KeyInfo";
|
||||||
|
|
||||||
|
public static final String KEY_ID_ATTRIBUTE_NAME = "MessageSigningKeyId";
|
||||||
|
|
||||||
|
private final String keyId;
|
||||||
|
|
||||||
|
public KeycloakKeySamlExtensionGenerator(String keyId) {
|
||||||
|
this.keyId = keyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(XMLStreamWriter writer) throws ProcessingException {
|
||||||
|
StaxUtil.writeStartElement(writer, NS_PREFIX, KC_KEY_INFO_ELEMENT_NAME, NS_URI);
|
||||||
|
StaxUtil.writeNameSpace(writer, NS_PREFIX, NS_URI);
|
||||||
|
if (this.keyId != null) {
|
||||||
|
StaxUtil.writeAttribute(writer, KEY_ID_ATTRIBUTE_NAME, this.keyId);
|
||||||
|
}
|
||||||
|
StaxUtil.writeEndElement(writer);
|
||||||
|
StaxUtil.flush(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the given element is indeed a Keycloak extension {@code KeyInfo} element and
|
||||||
|
* returns a content of {@code MessageSigningKeyId} attribute in the given element.
|
||||||
|
* @param element Element to obtain the key info from.
|
||||||
|
* @return {@code null} if the element is unknown or there is {@code MessageSigningKeyId} attribute unset,
|
||||||
|
* value of the {@code MessageSigningKeyId} attribute otherwise.
|
||||||
|
*/
|
||||||
|
public static String getMessageSigningKeyIdFromElement(Element element) {
|
||||||
|
if (Objects.equals(element.getNamespaceURI(), NS_URI) &&
|
||||||
|
Objects.equals(element.getLocalName(), KC_KEY_INFO_ELEMENT_NAME) &&
|
||||||
|
element.hasAttribute(KEY_ID_ATTRIBUTE_NAME)) {
|
||||||
|
return element.getAttribute(KEY_ID_ATTRIBUTE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -32,6 +32,9 @@ public class SignatureUtilTransferObject {
|
||||||
private X509Certificate x509Certificate;
|
private X509Certificate x509Certificate;
|
||||||
|
|
||||||
private Document documentToBeSigned;
|
private Document documentToBeSigned;
|
||||||
|
|
||||||
|
private String keyId;
|
||||||
|
|
||||||
private KeyPair keyPair;
|
private KeyPair keyPair;
|
||||||
|
|
||||||
private Node nextSibling;
|
private Node nextSibling;
|
||||||
|
@ -111,4 +114,12 @@ public class SignatureUtilTransferObject {
|
||||||
public void setX509Certificate(X509Certificate x509Certificate) {
|
public void setX509Certificate(X509Certificate x509Certificate) {
|
||||||
this.x509Certificate = x509Certificate;
|
this.x509Certificate = x509Certificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getKeyId() {
|
||||||
|
return keyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyId(String keyId) {
|
||||||
|
this.keyId = keyId;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -54,8 +54,6 @@ import javax.xml.crypto.dsig.dom.DOMSignContext;
|
||||||
import javax.xml.crypto.dsig.dom.DOMValidateContext;
|
import javax.xml.crypto.dsig.dom.DOMValidateContext;
|
||||||
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||||
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
|
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
|
||||||
import javax.xml.crypto.dsig.keyinfo.KeyValue;
|
|
||||||
import javax.xml.crypto.dsig.keyinfo.X509Data;
|
|
||||||
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
|
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
|
||||||
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
|
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
|
||||||
import javax.xml.namespace.QName;
|
import javax.xml.namespace.QName;
|
||||||
|
@ -69,6 +67,7 @@ import java.io.OutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.KeyException;
|
import java.security.KeyException;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.NoSuchProviderException;
|
import java.security.NoSuchProviderException;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
@ -79,7 +78,16 @@ import java.security.interfaces.DSAPublicKey;
|
||||||
import java.security.interfaces.RSAPublicKey;
|
import java.security.interfaces.RSAPublicKey;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import javax.xml.crypto.AlgorithmMethod;
|
||||||
|
import javax.xml.crypto.KeySelector;
|
||||||
|
import javax.xml.crypto.KeySelectorException;
|
||||||
|
import javax.xml.crypto.KeySelectorResult;
|
||||||
|
import javax.xml.crypto.XMLCryptoContext;
|
||||||
|
import javax.xml.crypto.dsig.keyinfo.KeyName;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
import org.keycloak.saml.processing.api.util.KeyInfoTools;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for XML Signature <b>Note:</b> You can change the canonicalization method type by using the system property
|
* Utility for XML Signature <b>Note:</b> You can change the canonicalization method type by using the system property
|
||||||
|
@ -105,15 +113,66 @@ public class XMLSignatureUtil {
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
private static String canonicalizationMethodType = CanonicalizationMethod.EXCLUSIVE;
|
private static final XMLSignatureFactory fac = getXMLSignatureFactory();
|
||||||
|
|
||||||
private static XMLSignatureFactory fac = getXMLSignatureFactory();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* By default, we include the keyinfo in the signature
|
* By default, we include the keyinfo in the signature
|
||||||
*/
|
*/
|
||||||
private static boolean includeKeyInfoInSignature = true;
|
private static boolean includeKeyInfoInSignature = true;
|
||||||
|
|
||||||
|
private static class KeySelectorUtilizingKeyNameHint extends KeySelector {
|
||||||
|
|
||||||
|
private final KeyLocator locator;
|
||||||
|
|
||||||
|
private boolean keyLocated = false;
|
||||||
|
|
||||||
|
private String keyName = null;
|
||||||
|
|
||||||
|
public KeySelectorUtilizingKeyNameHint(KeyLocator locator) {
|
||||||
|
this.locator = locator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException {
|
||||||
|
try {
|
||||||
|
KeyName keyNameEl = KeyInfoTools.getKeyName(keyInfo);
|
||||||
|
this.keyName = keyNameEl == null ? null : keyNameEl.getName();
|
||||||
|
final Key key = locator.getKey(keyName);
|
||||||
|
this.keyLocated = key != null;
|
||||||
|
return new KeySelectorResult() {
|
||||||
|
@Override public Key getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (KeyManagementException ex) {
|
||||||
|
throw new KeySelectorException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean wasKeyLocated() {
|
||||||
|
return this.keyLocated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class KeySelectorPresetKey extends KeySelector {
|
||||||
|
|
||||||
|
private final Key key;
|
||||||
|
|
||||||
|
public KeySelectorPresetKey(Key key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) {
|
||||||
|
return new KeySelectorResult() {
|
||||||
|
@Override public Key getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static XMLSignatureFactory getXMLSignatureFactory() {
|
private static XMLSignatureFactory getXMLSignatureFactory() {
|
||||||
XMLSignatureFactory xsf = null;
|
XMLSignatureFactory xsf = null;
|
||||||
|
|
||||||
|
@ -157,7 +216,7 @@ public class XMLSignatureUtil {
|
||||||
* @throws MarshalException
|
* @throws MarshalException
|
||||||
* @throws GeneralSecurityException
|
* @throws GeneralSecurityException
|
||||||
*/
|
*/
|
||||||
public static Document sign(Document doc, Node nodeToBeSigned, KeyPair keyPair, String digestMethod,
|
public static Document sign(Document doc, Node nodeToBeSigned, String keyId, KeyPair keyPair, String digestMethod,
|
||||||
String signatureMethod, String referenceURI, X509Certificate x509Certificate,
|
String signatureMethod, String referenceURI, X509Certificate x509Certificate,
|
||||||
String canonicalizationMethodType) throws ParserConfigurationException, GeneralSecurityException,
|
String canonicalizationMethodType) throws ParserConfigurationException, GeneralSecurityException,
|
||||||
MarshalException, XMLSignatureException {
|
MarshalException, XMLSignatureException {
|
||||||
|
@ -179,7 +238,7 @@ public class XMLSignatureUtil {
|
||||||
if (!referenceURI.isEmpty()) {
|
if (!referenceURI.isEmpty()) {
|
||||||
propagateIDAttributeSetup(nodeToBeSigned, newDoc.getDocumentElement());
|
propagateIDAttributeSetup(nodeToBeSigned, newDoc.getDocumentElement());
|
||||||
}
|
}
|
||||||
newDoc = sign(newDoc, keyPair, digestMethod, signatureMethod, referenceURI, x509Certificate, canonicalizationMethodType);
|
newDoc = sign(newDoc, keyId, keyPair, digestMethod, signatureMethod, referenceURI, x509Certificate, canonicalizationMethodType);
|
||||||
|
|
||||||
// if the signed element is a SAMLv2.0 assertion we need to move the signature element to the position
|
// if the signed element is a SAMLv2.0 assertion we need to move the signature element to the position
|
||||||
// specified in the schema (before the assertion subject element).
|
// specified in the schema (before the assertion subject element).
|
||||||
|
@ -220,10 +279,10 @@ public class XMLSignatureUtil {
|
||||||
* @throws MarshalException
|
* @throws MarshalException
|
||||||
* @throws XMLSignatureException
|
* @throws XMLSignatureException
|
||||||
*/
|
*/
|
||||||
public static void sign(Element elementToSign, Node nextSibling, KeyPair keyPair, String digestMethod,
|
public static void sign(Element elementToSign, Node nextSibling, String keyId, KeyPair keyPair, String digestMethod,
|
||||||
String signatureMethod, String referenceURI, String canonicalizationMethodType)
|
String signatureMethod, String referenceURI, String canonicalizationMethodType)
|
||||||
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
||||||
sign(elementToSign, nextSibling, keyPair, digestMethod, signatureMethod, referenceURI, null, canonicalizationMethodType);
|
sign(elementToSign, nextSibling, keyId, keyPair, digestMethod, signatureMethod, referenceURI, null, canonicalizationMethodType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -242,7 +301,7 @@ public class XMLSignatureUtil {
|
||||||
* @throws XMLSignatureException
|
* @throws XMLSignatureException
|
||||||
* @since 2.5.0
|
* @since 2.5.0
|
||||||
*/
|
*/
|
||||||
public static void sign(Element elementToSign, Node nextSibling, KeyPair keyPair, String digestMethod,
|
public static void sign(Element elementToSign, Node nextSibling, String keyId, KeyPair keyPair, String digestMethod,
|
||||||
String signatureMethod, String referenceURI, X509Certificate x509Certificate, String canonicalizationMethodType)
|
String signatureMethod, String referenceURI, X509Certificate x509Certificate, String canonicalizationMethodType)
|
||||||
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
||||||
PrivateKey signingKey = keyPair.getPrivate();
|
PrivateKey signingKey = keyPair.getPrivate();
|
||||||
|
@ -250,7 +309,7 @@ public class XMLSignatureUtil {
|
||||||
|
|
||||||
DOMSignContext dsc = new DOMSignContext(signingKey, elementToSign, nextSibling);
|
DOMSignContext dsc = new DOMSignContext(signingKey, elementToSign, nextSibling);
|
||||||
|
|
||||||
signImpl(dsc, digestMethod, signatureMethod, referenceURI, publicKey, x509Certificate, canonicalizationMethodType);
|
signImpl(dsc, digestMethod, signatureMethod, referenceURI, keyId, publicKey, x509Certificate, canonicalizationMethodType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -284,9 +343,9 @@ public class XMLSignatureUtil {
|
||||||
* @throws XMLSignatureException
|
* @throws XMLSignatureException
|
||||||
* @throws MarshalException
|
* @throws MarshalException
|
||||||
*/
|
*/
|
||||||
public static Document sign(Document doc, KeyPair keyPair, String digestMethod, String signatureMethod, String referenceURI, String canonicalizationMethodType)
|
public static Document sign(Document doc, String keyId, KeyPair keyPair, String digestMethod, String signatureMethod, String referenceURI, String canonicalizationMethodType)
|
||||||
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
||||||
return sign(doc, keyPair, digestMethod, signatureMethod, referenceURI, null, canonicalizationMethodType);
|
return sign(doc, keyId, keyPair, digestMethod, signatureMethod, referenceURI, null, canonicalizationMethodType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -304,7 +363,7 @@ public class XMLSignatureUtil {
|
||||||
* @throws MarshalException
|
* @throws MarshalException
|
||||||
* @since 2.5.0
|
* @since 2.5.0
|
||||||
*/
|
*/
|
||||||
public static Document sign(Document doc, KeyPair keyPair, String digestMethod, String signatureMethod, String referenceURI,
|
public static Document sign(Document doc, String keyId, KeyPair keyPair, String digestMethod, String signatureMethod, String referenceURI,
|
||||||
X509Certificate x509Certificate, String canonicalizationMethodType)
|
X509Certificate x509Certificate, String canonicalizationMethodType)
|
||||||
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
||||||
logger.trace("Document to be signed=" + DocumentUtil.asString(doc));
|
logger.trace("Document to be signed=" + DocumentUtil.asString(doc));
|
||||||
|
@ -313,7 +372,7 @@ public class XMLSignatureUtil {
|
||||||
|
|
||||||
DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement());
|
DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement());
|
||||||
|
|
||||||
signImpl(dsc, digestMethod, signatureMethod, referenceURI, publicKey, x509Certificate, canonicalizationMethodType);
|
signImpl(dsc, digestMethod, signatureMethod, referenceURI, keyId, publicKey, x509Certificate, canonicalizationMethodType);
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
@ -331,6 +390,7 @@ public class XMLSignatureUtil {
|
||||||
public static Document sign(SignatureUtilTransferObject dto, String canonicalizationMethodType) throws GeneralSecurityException, MarshalException,
|
public static Document sign(SignatureUtilTransferObject dto, String canonicalizationMethodType) throws GeneralSecurityException, MarshalException,
|
||||||
XMLSignatureException {
|
XMLSignatureException {
|
||||||
Document doc = dto.getDocumentToBeSigned();
|
Document doc = dto.getDocumentToBeSigned();
|
||||||
|
String keyId = dto.getKeyId();
|
||||||
KeyPair keyPair = dto.getKeyPair();
|
KeyPair keyPair = dto.getKeyPair();
|
||||||
Node nextSibling = dto.getNextSibling();
|
Node nextSibling = dto.getNextSibling();
|
||||||
String digestMethod = dto.getDigestMethod();
|
String digestMethod = dto.getDigestMethod();
|
||||||
|
@ -344,13 +404,14 @@ public class XMLSignatureUtil {
|
||||||
|
|
||||||
DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement(), nextSibling);
|
DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement(), nextSibling);
|
||||||
|
|
||||||
signImpl(dsc, digestMethod, signatureMethod, referenceURI, publicKey, dto.getX509Certificate(), canonicalizationMethodType);
|
signImpl(dsc, digestMethod, signatureMethod, referenceURI, keyId, publicKey, dto.getX509Certificate(), canonicalizationMethodType);
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a signed document with the given public key
|
* Validate a signed document with the given public key. All elements that contain a Signature are checked,
|
||||||
|
* this way both assertions and the containing document are verified when signed.
|
||||||
*
|
*
|
||||||
* @param signedDoc
|
* @param signedDoc
|
||||||
* @param publicKey
|
* @param publicKey
|
||||||
|
@ -361,7 +422,7 @@ public class XMLSignatureUtil {
|
||||||
* @throws XMLSignatureException
|
* @throws XMLSignatureException
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static boolean validate(Document signedDoc, Key publicKey) throws MarshalException, XMLSignatureException {
|
public static boolean validate(Document signedDoc, final KeyLocator locator) throws MarshalException, XMLSignatureException {
|
||||||
if (signedDoc == null)
|
if (signedDoc == null)
|
||||||
throw logger.nullArgumentError("Signed Document");
|
throw logger.nullArgumentError("Signed Document");
|
||||||
|
|
||||||
|
@ -374,7 +435,7 @@ public class XMLSignatureUtil {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicKey == null)
|
if (locator == null)
|
||||||
throw logger.nullValueError("Public Key");
|
throw logger.nullValueError("Public Key");
|
||||||
|
|
||||||
int signedAssertions = 0;
|
int signedAssertions = 0;
|
||||||
|
@ -390,24 +451,7 @@ public class XMLSignatureUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DOMValidateContext valContext = new DOMValidateContext(publicKey, nl.item(i));
|
if (! validateSingleNode(signatureNode, locator)) return false;
|
||||||
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
|
|
||||||
|
|
||||||
boolean coreValidity = signature.validate(valContext);
|
|
||||||
|
|
||||||
if (!coreValidity) {
|
|
||||||
if (logger.isTraceEnabled()) {
|
|
||||||
boolean sv = signature.getSignatureValue().validate(valContext);
|
|
||||||
logger.trace("Signature validation status: " + sv);
|
|
||||||
|
|
||||||
List<Reference> references = signature.getSignedInfo().getReferences();
|
|
||||||
for (Reference ref : references) {
|
|
||||||
logger.trace("[Ref id=" + ref.getId() + ":uri=" + ref.getURI() + "]validity status:" + ref.validate(valContext));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeList assertions = signedDoc.getElementsByTagNameNS(assertionNameSpaceUri, JBossSAMLConstants.ASSERTION.get());
|
NodeList assertions = signedDoc.getElementsByTagNameNS(assertionNameSpaceUri, JBossSAMLConstants.ASSERTION.get());
|
||||||
|
@ -423,6 +467,62 @@ public class XMLSignatureUtil {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean validateSingleNode(Node signatureNode, final KeyLocator locator) throws MarshalException, XMLSignatureException {
|
||||||
|
KeySelectorUtilizingKeyNameHint sel = new KeySelectorUtilizingKeyNameHint(locator);
|
||||||
|
try {
|
||||||
|
if (validateUsingKeySelector(signatureNode, sel)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (sel.wasKeyLocated()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (XMLSignatureException ex) { // pass through MarshalException
|
||||||
|
logger.debug("Verification failed for key " + sel.keyName + ": " + ex);
|
||||||
|
logger.trace(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("Could not validate signature using ds:KeyInfo/ds:KeyName hint.");
|
||||||
|
|
||||||
|
if (locator instanceof Iterable) {
|
||||||
|
Iterable<Key> availableKeys = (Iterable<Key>) locator;
|
||||||
|
|
||||||
|
logger.trace("Trying hard to validate XML signature using all available keys.");
|
||||||
|
|
||||||
|
for (Key key : availableKeys) {
|
||||||
|
try {
|
||||||
|
if (validateUsingKeySelector(signatureNode, new KeySelectorPresetKey(key))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (XMLSignatureException ex) { // pass through MarshalException
|
||||||
|
logger.debug("Verification failed: " + ex);
|
||||||
|
logger.trace(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean validateUsingKeySelector(Node signatureNode, KeySelector validationKeySelector) throws XMLSignatureException, MarshalException {
|
||||||
|
DOMValidateContext valContext = new DOMValidateContext(validationKeySelector, signatureNode);
|
||||||
|
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
|
||||||
|
boolean coreValidity = signature.validate(valContext);
|
||||||
|
|
||||||
|
if (! coreValidity) {
|
||||||
|
if (logger.isTraceEnabled()) {
|
||||||
|
boolean sv = signature.getSignatureValue().validate(valContext);
|
||||||
|
logger.trace("Signature validation status: " + sv);
|
||||||
|
|
||||||
|
List<Reference> references = signature.getSignedInfo().getReferences();
|
||||||
|
for (Reference ref : references) {
|
||||||
|
logger.trace("[Ref id=" + ref.getId() + ":uri=" + ref.getURI() + "]validity status:" + ref.validate(valContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return coreValidity;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marshall a SignatureType to output stream
|
* Marshall a SignatureType to output stream
|
||||||
*
|
*
|
||||||
|
@ -594,7 +694,7 @@ public class XMLSignatureUtil {
|
||||||
throw logger.unsupportedType(key.toString());
|
throw logger.unsupportedType(key.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void signImpl(DOMSignContext dsc, String digestMethod, String signatureMethod, String referenceURI, PublicKey publicKey,
|
private static void signImpl(DOMSignContext dsc, String digestMethod, String signatureMethod, String referenceURI, String keyId, PublicKey publicKey,
|
||||||
X509Certificate x509Certificate, String canonicalizationMethodType)
|
X509Certificate x509Certificate, String canonicalizationMethodType)
|
||||||
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
throws GeneralSecurityException, MarshalException, XMLSignatureException {
|
||||||
dsc.setDefaultNamespacePrefix("dsig");
|
dsc.setDefaultNamespacePrefix("dsig");
|
||||||
|
@ -603,7 +703,7 @@ public class XMLSignatureUtil {
|
||||||
Transform transform1 = fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null);
|
Transform transform1 = fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null);
|
||||||
Transform transform2 = fac.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#", (TransformParameterSpec) null);
|
Transform transform2 = fac.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#", (TransformParameterSpec) null);
|
||||||
|
|
||||||
List<Transform> transformList = new ArrayList<Transform>();
|
List<Transform> transformList = new ArrayList<>();
|
||||||
transformList.add(transform1);
|
transformList.add(transform1);
|
||||||
transformList.add(transform2);
|
transformList.add(transform2);
|
||||||
|
|
||||||
|
@ -616,37 +716,34 @@ public class XMLSignatureUtil {
|
||||||
SignatureMethod signatureMethodObj = fac.newSignatureMethod(signatureMethod, null);
|
SignatureMethod signatureMethodObj = fac.newSignatureMethod(signatureMethod, null);
|
||||||
SignedInfo si = fac.newSignedInfo(canonicalizationMethod, signatureMethodObj, referenceList);
|
SignedInfo si = fac.newSignedInfo(canonicalizationMethod, signatureMethodObj, referenceList);
|
||||||
|
|
||||||
KeyInfo ki = null;
|
KeyInfo ki;
|
||||||
if (includeKeyInfoInSignature) {
|
if (includeKeyInfoInSignature) {
|
||||||
ki = createKeyInfo(publicKey, x509Certificate);
|
ki = createKeyInfo(keyId, publicKey, x509Certificate);
|
||||||
|
} else {
|
||||||
|
ki = createKeyInfo(keyId, null, null);
|
||||||
}
|
}
|
||||||
XMLSignature signature = fac.newXMLSignature(si, ki);
|
XMLSignature signature = fac.newXMLSignature(si, ki);
|
||||||
|
|
||||||
signature.sign(dsc);
|
signature.sign(dsc);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static KeyInfo createKeyInfo(PublicKey publicKey, X509Certificate x509Certificate) throws KeyException {
|
private static KeyInfo createKeyInfo(String keyId, PublicKey publicKey, X509Certificate x509Certificate) throws KeyException {
|
||||||
KeyInfoFactory keyInfoFactory = fac.getKeyInfoFactory();
|
KeyInfoFactory keyInfoFactory = fac.getKeyInfoFactory();
|
||||||
KeyInfo keyInfo = null;
|
|
||||||
KeyValue keyValue = null;
|
List<Object> items = new LinkedList<>();
|
||||||
//Just with public key
|
|
||||||
if (publicKey != null) {
|
if (keyId != null) {
|
||||||
keyValue = keyInfoFactory.newKeyValue(publicKey);
|
items.add(keyInfoFactory.newKeyName(keyId));
|
||||||
keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(keyValue));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (x509Certificate != null) {
|
if (x509Certificate != null) {
|
||||||
List x509list = new ArrayList();
|
items.add(keyInfoFactory.newX509Data(Collections.singletonList(x509Certificate)));
|
||||||
|
}
|
||||||
|
|
||||||
x509list.add(x509Certificate);
|
if (publicKey != null) {
|
||||||
X509Data x509Data = keyInfoFactory.newX509Data(x509list);
|
items.add(keyInfoFactory.newKeyValue(publicKey));
|
||||||
List items = new ArrayList();
|
}
|
||||||
|
|
||||||
items.add(x509Data);
|
return keyInfoFactory.newKeyInfo(items);
|
||||||
if (keyValue != null) {
|
|
||||||
items.add(keyValue);
|
|
||||||
}
|
|
||||||
keyInfo = keyInfoFactory.newKeyInfo(items);
|
|
||||||
}
|
|
||||||
return keyInfo;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* 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.saml.processing.core.parsers.saml;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test class for SAML parser.
|
||||||
|
*
|
||||||
|
* TODO: Add further tests.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class SAMLParserTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaml20EncryptedAssertionsSignedReceivedWithRedirectBinding() throws Exception {
|
||||||
|
InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-encrypted-signed-redirect-response.xml");
|
||||||
|
SAMLParser parser = new SAMLParser();
|
||||||
|
|
||||||
|
Object parsedObject = parser.parse(st);
|
||||||
|
assertThat(parsedObject, instanceOf(ResponseType.class));
|
||||||
|
|
||||||
|
ResponseType resp = (ResponseType) parsedObject;
|
||||||
|
assertThat(resp.getSignature(), nullValue());
|
||||||
|
assertThat(resp.getConsent(), nullValue());
|
||||||
|
assertThat(resp.getIssuer(), not(nullValue()));
|
||||||
|
assertThat(resp.getIssuer().getValue(), is("http://localhost:8081/auth/realms/saml-demo"));
|
||||||
|
|
||||||
|
assertThat(resp.getExtensions(), not(nullValue()));
|
||||||
|
assertThat(resp.getExtensions().getAny().size(), is(1));
|
||||||
|
assertThat(resp.getExtensions().getAny().get(0), instanceOf(Element.class));
|
||||||
|
Element el = (Element) resp.getExtensions().getAny().get(0);
|
||||||
|
assertThat(el.getLocalName(), is("KeyInfo"));
|
||||||
|
assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:key:1.0"));
|
||||||
|
assertThat(el.hasAttribute("MessageSigningKeyId"), is(true));
|
||||||
|
assertThat(el.getAttribute("MessageSigningKeyId"), is("FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"));
|
||||||
|
|
||||||
|
assertThat(resp.getAssertions(), not(nullValue()));
|
||||||
|
assertThat(resp.getAssertions().size(), is(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaml20EncryptedAssertionsSignedTwoExtensionsReceivedWithRedirectBinding() throws Exception {
|
||||||
|
Element el;
|
||||||
|
|
||||||
|
InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-encrypted-signed-redirect-response-two-extensions.xml");
|
||||||
|
SAMLParser parser = new SAMLParser();
|
||||||
|
|
||||||
|
Object parsedObject = parser.parse(st);
|
||||||
|
assertThat(parsedObject, instanceOf(ResponseType.class));
|
||||||
|
|
||||||
|
ResponseType resp = (ResponseType) parsedObject;
|
||||||
|
assertThat(resp.getSignature(), nullValue());
|
||||||
|
assertThat(resp.getConsent(), nullValue());
|
||||||
|
assertThat(resp.getIssuer(), not(nullValue()));
|
||||||
|
assertThat(resp.getIssuer().getValue(), is("http://localhost:8081/auth/realms/saml-demo"));
|
||||||
|
|
||||||
|
assertThat(resp.getExtensions(), not(nullValue()));
|
||||||
|
assertThat(resp.getExtensions().getAny().size(), is(2));
|
||||||
|
assertThat(resp.getExtensions().getAny().get(0), instanceOf(Element.class));
|
||||||
|
el = (Element) resp.getExtensions().getAny().get(0);
|
||||||
|
assertThat(el.getLocalName(), is("KeyInfo"));
|
||||||
|
assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:key:1.0"));
|
||||||
|
assertThat(el.hasAttribute("MessageSigningKeyId"), is(true));
|
||||||
|
assertThat(el.getAttribute("MessageSigningKeyId"), is("FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"));
|
||||||
|
assertThat(resp.getExtensions().getAny().get(1), instanceOf(Element.class));
|
||||||
|
el = (Element) resp.getExtensions().getAny().get(1);
|
||||||
|
assertThat(el.getLocalName(), is("ever"));
|
||||||
|
assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:what:1.0"));
|
||||||
|
assertThat(el.hasAttribute("what"), is(true));
|
||||||
|
assertThat(el.getAttribute("what"), is("ever"));
|
||||||
|
|
||||||
|
assertThat(resp.getAssertions(), not(nullValue()));
|
||||||
|
assertThat(resp.getAssertions().size(), is(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaml20PostLogoutRequest() throws Exception {
|
||||||
|
InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-signed-logout-request.xml");
|
||||||
|
SAMLParser parser = new SAMLParser();
|
||||||
|
|
||||||
|
Object parsedObject = parser.parse(st);
|
||||||
|
assertThat(parsedObject, instanceOf(LogoutRequestType.class));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8080/sales-post-enc/saml" ID="ID_0b43d444-d1a8-44a5-8caf-38e176489e1f" InResponseTo="ID_223d3591-22fb-4b3c-9e38-4719293b2d94" IssueInstant="2016-11-01T13:52:43.054Z" Version="2.0">
|
||||||
|
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8081/auth/realms/saml-demo</saml:Issuer>
|
||||||
|
<samlp:Extensions>
|
||||||
|
<kckey:KeyInfo xmlns:kckey="urn:keycloak:ext:key:1.0" MessageSigningKeyId="FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"/>
|
||||||
|
<what:ever xmlns:what="urn:keycloak:ext:what:1.0" what="ever"/>
|
||||||
|
</samlp:Extensions>
|
||||||
|
<samlp:Status>
|
||||||
|
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||||
|
</samlp:Status>
|
||||||
|
<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||||
|
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">
|
||||||
|
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
|
||||||
|
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
|
||||||
|
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
|
||||||
|
<xenc:CipherData>
|
||||||
|
<xenc:CipherValue>
|
||||||
|
OkvZTx/ifYLef74rY0F9I8lbJaatgSEguo+zwh5JrYWcO09Ib2gtz5+z+67Is2+wk/OzKp154r8qAI5vY9AYvuXCslKL/wbcZ1UILL78F0T/iiUW3VpWy8Wvz5nezBFPRqot8WiFQykByjlBg1Z8XOts+uIdyqBBi/WjYeJGMaQ=
|
||||||
|
</xenc:CipherValue>
|
||||||
|
</xenc:CipherData>
|
||||||
|
</xenc:EncryptedKey>
|
||||||
|
</ds:KeyInfo>
|
||||||
|
<xenc:CipherData>
|
||||||
|
<xenc:CipherValue>
|
||||||
|
RW2eu9nP2Ez9hfRlug9xC+kFfVF3HZpEb4kIFH33gmVbzrQjPk0l67uXkwRjC82FZZ482QnHCBIqNFlAryds/zTa6wdRvFmhQnIM6WxoAl8TM+e9h8MoKkalMc8J/Qfp+WQ7/XdmCg2pp9VvUZTK+g0+G4aGuL+S5+ssZq4rl9k7LrSYyp6vj+djgvISZiz5hPYJCN/WY/gWXfVuLHSpu4CmZt8D2APtT3ax1WmGcuzStAfTW8q3MFIDNV59hkpFmDb+gvyLNbZ95cDYxofiPXaC5cOTftnSBp68Ay1eienqdttEDo4fyakszdvq128KwXkH9azCg6sqLxli6B8l2xdq41MeuJO54VqmOhhLxwKy42NtnJvK/NkNwttH4yMwDPpPbC4vOKCXxT2r2F7jjvJQNB2VFv+oiUAWSSc3fGQcc2uNlx9YQVuzTmjqc7fXAWCGgYoogC8AeNWni204bnBoVpFrEo3gzuOe2fFsddJIclglmTH1hWf31FXUHDO2nl/lT4puQVTo+I+d6jpiV+qdp823NDntRxljRlUJO2AzSTXuIIGtF5q5KWyEi9Nj93BCWa1Llcddkn3ZEZMvDwR4MacwUj8G8hwoH73VvT3jAiakjSpNEIqYCzofeejdfN/gEuuAUfe8uNbTu+gBS+iP3QJe3Pc0Fs/lKJzd3frPNj7xb83wpOf865EQQoOozhnRIKKcMReSjakr/Px5NNooeiJcWEreDagQO2TbwTnHg1kCNG3BAXV/2lV3XBU4afZBoUfxAzYWFOl6xFCAPzhQCPL1SFJp1VRADY/1MU2Kaje5AZoJ4jjph8+yspxBvjic1vC1uYRGW8LWRind9w4eVhCm0LfPiFRCpP+jKPQOJzcNH580/nIMFXPHHnLKv/It7Qex1unDv/QjkuCFFHR6SWJm4WBrwDek+MyOIvgT6o878Cu0Ps472QpoYBQ+7l2WoylWdG1lHZV1UiHPj7PLHPNAL4rbbN3U88fS6N9OJHegQTfcX0i/1KPk4IN/5Z+/15dHI658BINjRvI/6O1QqaTVZkqM8ORcoGpn6BjAiz5rRhjWpOCwlmT+VzOAp3IqACURS1X+txjWE2mfVjlHLJsvyGRDLv1dUR3IeStDAEfsjR/ruRgn5XTFpYaccB/u//DJonJr5A+KFiLbYl+sbbSVAoQCAiAdxKdUpKPx7C473UJ2nYQGby5H5xwboa0Uj0SnJLYWdQ0jvVvzWpWFVWATc4UqnaxdoUDAmewrM6cSSIAmQBB34orCunFbriK9Z4efZ7gB9erQ1fpi3z/IjQBoTEpOUUIPW/qMAApIDPVM6UV9PumW7RL9zKEP5PuWJoGGnKbWGP/b9G4vMFiWMaSNHBYYMI6OLH4WJ3E+4QBGh2vjjfQ0gobhaLgIerIwCQFYEdl9KddAjaflUEFXal9fIQ8Bz9L3rDhQE5AGBZL6ULZmJe3GnkN6Cc+UWAGyD5zv2rsCG2lvR5ox4UE2mFi6nBJbC5Vj5m9Sz1l0QpRwUkH2kD2QQ5iV6nNmQOcU/mz7ulxluf8+FBJJimYVqK8UkJ6+W6j8Eft9Q8fTpEuEVLxqTWGgOAEUBf87RWDU+iF3A+AxFGsJLc5RC+5BKNTEDlV2qDCjHT7b5wqBKJ3FHulOih9EenlZiI51m6kg5yyxnMdbhasvSh6Az8Mp/4lFo/wSA/mXxNhBrEEmRhFiIE5yYUEYIj5F8fH+93tIuWQqyhXIwCntEOdSSmoei9EYFzj8deXcEzVf8y/N6HQErZcJjyg34caOsfRcJYoxEiCm4icA/btWhdjUNT02B20qnxGFndO4CRUQlyDqTbyVD8LRLK9/95L9+5v9zojLle8xQe30dsxKn7r9TTJH8QQai5iam9lU1ik50lwTKpZb18k4rNdO5cnnYoHzCXeCg38YZxyFt9G7um/MxlID5Qd5Ywq6thDzL7WxvanKeRhCuJ2MTVV0EoJxZKIj9Yv0Ars9mZHkoHoP0ikcW8d5ciDj1Onnbj+XDcYI3FZj0Y2vToZvYi/7eLWi8EnSjaIQrr/AHnrmZK1w3Uicd691U6r3Y0UdnzQEl4Ub/l1uhSaGAg2oEdDxkOdZ3Frvf/C4nTEBmunPlNvnJjVFssdeVVXKLBOZ5eRiJjasHUKnTeJVwolvd/dBI+ypfw1+5ae/0upxd9/gV1lbwX9N2yOwqbxz24cKXZWvOFBAGc3+gQFu8RrF6NAeQ96PlkuRsiNOKPPtJT3JNrLGvVKY8g==
|
||||||
|
</xenc:CipherValue>
|
||||||
|
</xenc:CipherData>
|
||||||
|
</xenc:EncryptedData>
|
||||||
|
</saml:EncryptedAssertion>
|
||||||
|
</samlp:Response>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8080/sales-post-enc/saml" ID="ID_0b43d444-d1a8-44a5-8caf-38e176489e1f" InResponseTo="ID_223d3591-22fb-4b3c-9e38-4719293b2d94" IssueInstant="2016-11-01T13:52:43.054Z" Version="2.0">
|
||||||
|
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8081/auth/realms/saml-demo</saml:Issuer>
|
||||||
|
<samlp:Extensions>
|
||||||
|
<kckey:KeyInfo xmlns:kckey="urn:keycloak:ext:key:1.0" MessageSigningKeyId="FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"/>
|
||||||
|
</samlp:Extensions>
|
||||||
|
<samlp:Status>
|
||||||
|
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||||
|
</samlp:Status>
|
||||||
|
<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||||
|
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">
|
||||||
|
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
|
||||||
|
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
|
||||||
|
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
|
||||||
|
<xenc:CipherData>
|
||||||
|
<xenc:CipherValue>
|
||||||
|
OkvZTx/ifYLef74rY0F9I8lbJaatgSEguo+zwh5JrYWcO09Ib2gtz5+z+67Is2+wk/OzKp154r8qAI5vY9AYvuXCslKL/wbcZ1UILL78F0T/iiUW3VpWy8Wvz5nezBFPRqot8WiFQykByjlBg1Z8XOts+uIdyqBBi/WjYeJGMaQ=
|
||||||
|
</xenc:CipherValue>
|
||||||
|
</xenc:CipherData>
|
||||||
|
</xenc:EncryptedKey>
|
||||||
|
</ds:KeyInfo>
|
||||||
|
<xenc:CipherData>
|
||||||
|
<xenc:CipherValue>
|
||||||
|
RW2eu9nP2Ez9hfRlug9xC+kFfVF3HZpEb4kIFH33gmVbzrQjPk0l67uXkwRjC82FZZ482QnHCBIqNFlAryds/zTa6wdRvFmhQnIM6WxoAl8TM+e9h8MoKkalMc8J/Qfp+WQ7/XdmCg2pp9VvUZTK+g0+G4aGuL+S5+ssZq4rl9k7LrSYyp6vj+djgvISZiz5hPYJCN/WY/gWXfVuLHSpu4CmZt8D2APtT3ax1WmGcuzStAfTW8q3MFIDNV59hkpFmDb+gvyLNbZ95cDYxofiPXaC5cOTftnSBp68Ay1eienqdttEDo4fyakszdvq128KwXkH9azCg6sqLxli6B8l2xdq41MeuJO54VqmOhhLxwKy42NtnJvK/NkNwttH4yMwDPpPbC4vOKCXxT2r2F7jjvJQNB2VFv+oiUAWSSc3fGQcc2uNlx9YQVuzTmjqc7fXAWCGgYoogC8AeNWni204bnBoVpFrEo3gzuOe2fFsddJIclglmTH1hWf31FXUHDO2nl/lT4puQVTo+I+d6jpiV+qdp823NDntRxljRlUJO2AzSTXuIIGtF5q5KWyEi9Nj93BCWa1Llcddkn3ZEZMvDwR4MacwUj8G8hwoH73VvT3jAiakjSpNEIqYCzofeejdfN/gEuuAUfe8uNbTu+gBS+iP3QJe3Pc0Fs/lKJzd3frPNj7xb83wpOf865EQQoOozhnRIKKcMReSjakr/Px5NNooeiJcWEreDagQO2TbwTnHg1kCNG3BAXV/2lV3XBU4afZBoUfxAzYWFOl6xFCAPzhQCPL1SFJp1VRADY/1MU2Kaje5AZoJ4jjph8+yspxBvjic1vC1uYRGW8LWRind9w4eVhCm0LfPiFRCpP+jKPQOJzcNH580/nIMFXPHHnLKv/It7Qex1unDv/QjkuCFFHR6SWJm4WBrwDek+MyOIvgT6o878Cu0Ps472QpoYBQ+7l2WoylWdG1lHZV1UiHPj7PLHPNAL4rbbN3U88fS6N9OJHegQTfcX0i/1KPk4IN/5Z+/15dHI658BINjRvI/6O1QqaTVZkqM8ORcoGpn6BjAiz5rRhjWpOCwlmT+VzOAp3IqACURS1X+txjWE2mfVjlHLJsvyGRDLv1dUR3IeStDAEfsjR/ruRgn5XTFpYaccB/u//DJonJr5A+KFiLbYl+sbbSVAoQCAiAdxKdUpKPx7C473UJ2nYQGby5H5xwboa0Uj0SnJLYWdQ0jvVvzWpWFVWATc4UqnaxdoUDAmewrM6cSSIAmQBB34orCunFbriK9Z4efZ7gB9erQ1fpi3z/IjQBoTEpOUUIPW/qMAApIDPVM6UV9PumW7RL9zKEP5PuWJoGGnKbWGP/b9G4vMFiWMaSNHBYYMI6OLH4WJ3E+4QBGh2vjjfQ0gobhaLgIerIwCQFYEdl9KddAjaflUEFXal9fIQ8Bz9L3rDhQE5AGBZL6ULZmJe3GnkN6Cc+UWAGyD5zv2rsCG2lvR5ox4UE2mFi6nBJbC5Vj5m9Sz1l0QpRwUkH2kD2QQ5iV6nNmQOcU/mz7ulxluf8+FBJJimYVqK8UkJ6+W6j8Eft9Q8fTpEuEVLxqTWGgOAEUBf87RWDU+iF3A+AxFGsJLc5RC+5BKNTEDlV2qDCjHT7b5wqBKJ3FHulOih9EenlZiI51m6kg5yyxnMdbhasvSh6Az8Mp/4lFo/wSA/mXxNhBrEEmRhFiIE5yYUEYIj5F8fH+93tIuWQqyhXIwCntEOdSSmoei9EYFzj8deXcEzVf8y/N6HQErZcJjyg34caOsfRcJYoxEiCm4icA/btWhdjUNT02B20qnxGFndO4CRUQlyDqTbyVD8LRLK9/95L9+5v9zojLle8xQe30dsxKn7r9TTJH8QQai5iam9lU1ik50lwTKpZb18k4rNdO5cnnYoHzCXeCg38YZxyFt9G7um/MxlID5Qd5Ywq6thDzL7WxvanKeRhCuJ2MTVV0EoJxZKIj9Yv0Ars9mZHkoHoP0ikcW8d5ciDj1Onnbj+XDcYI3FZj0Y2vToZvYi/7eLWi8EnSjaIQrr/AHnrmZK1w3Uicd691U6r3Y0UdnzQEl4Ub/l1uhSaGAg2oEdDxkOdZ3Frvf/C4nTEBmunPlNvnJjVFssdeVVXKLBOZ5eRiJjasHUKnTeJVwolvd/dBI+ypfw1+5ae/0upxd9/gV1lbwX9N2yOwqbxz24cKXZWvOFBAGc3+gQFu8RrF6NAeQ96PlkuRsiNOKPPtJT3JNrLGvVKY8g==
|
||||||
|
</xenc:CipherValue>
|
||||||
|
</xenc:CipherData>
|
||||||
|
</xenc:EncryptedData>
|
||||||
|
</saml:EncryptedAssertion>
|
||||||
|
</samlp:Response>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8081/auth/realms/saml-demo/protocol/saml" ID="ID_4790c6a3-4b9f-4c0a-a368-5c0e498544e4" IssueInstant="2016-11-01T14:36:43.194Z" Version="2.0">
|
||||||
|
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8080/sales-post-enc/</saml:Issuer>
|
||||||
|
<dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<dsig:SignedInfo>
|
||||||
|
<dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||||
|
<dsig:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
|
||||||
|
<dsig:Reference URI="#ID_4790c6a3-4b9f-4c0a-a368-5c0e498544e4">
|
||||||
|
<dsig:Transforms>
|
||||||
|
<dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
|
||||||
|
<dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||||
|
</dsig:Transforms>
|
||||||
|
<dsig:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
||||||
|
<dsig:DigestValue>zeWNo5eav5tFOOCEJ1YU9eINkPnBSfixzAr8AOC4R4c=</dsig:DigestValue>
|
||||||
|
</dsig:Reference>
|
||||||
|
</dsig:SignedInfo>
|
||||||
|
<dsig:SignatureValue>
|
||||||
|
pyOiS1LsV/XR08zhcN6IqSYuKTDln4otmCvZxCc07ORP1C9jragu8V8rEE09qt/zBcdw7Arb8eLNNC6oCnrnMxuvzRInVTwt7T5K3t0UlzRWOb3HMElhcWFEgDzh6uKw5Cr45A01XNpojtJWCML/qU2Enyyy80FBlCJNcbzyLxE=
|
||||||
|
</dsig:SignatureValue>
|
||||||
|
<dsig:KeyInfo>
|
||||||
|
<dsig:KeyValue>
|
||||||
|
<dsig:RSAKeyValue>
|
||||||
|
<dsig:Modulus>
|
||||||
|
2+5MCT5BnVN+IYnKZcH6ev1pjXGi4feE0nOycq/VJ3aeaZMi4G9AxOxCBPupErOC7Kgm/Bw5AdJyw+Q12wSRXfJ9FhqCrLXpb7YOhbVSTJ8De5O8mW35DxAlh/cxe9FXjqPb286wKTUZ3LfGYR+X235UQeCTAPS/Ufi21EXaEik=
|
||||||
|
</dsig:Modulus>
|
||||||
|
<dsig:Exponent>AQAB</dsig:Exponent>
|
||||||
|
</dsig:RSAKeyValue>
|
||||||
|
</dsig:KeyValue>
|
||||||
|
</dsig:KeyInfo>
|
||||||
|
</dsig:Signature>
|
||||||
|
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">bburke</saml:NameID>
|
||||||
|
<samlp:SessionIndex>a3b2df1c-1095-487b-8b56-f62818c449e3</samlp:SessionIndex>
|
||||||
|
</samlp:LogoutRequest>
|
|
@ -73,9 +73,13 @@ import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.PublicKey;
|
import java.security.Key;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -174,14 +178,20 @@ public class SAMLEndpoint {
|
||||||
protected abstract void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException;
|
protected abstract void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException;
|
||||||
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
|
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
|
||||||
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
|
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
|
||||||
protected PublicKey getIDPKey() {
|
|
||||||
X509Certificate certificate = null;
|
protected KeyLocator getIDPKeyLocator() {
|
||||||
|
List<Key> keys = new LinkedList<>();
|
||||||
|
|
||||||
|
for (String signingCertificate : config.getSigningCertificates()) {
|
||||||
try {
|
try {
|
||||||
certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(config.getSigningCertificate().replaceAll("\\s", ""));
|
X509Certificate cert = XMLSignatureUtil.getX509CertificateFromKeyInfoString(signingCertificate.replaceAll("\\s", ""));
|
||||||
|
keys.add(cert.getPublicKey());
|
||||||
} catch (ProcessingException e) {
|
} catch (ProcessingException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
return certificate.getPublicKey();
|
}
|
||||||
|
|
||||||
|
return new HardcodedKeyLocator(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
||||||
|
@ -265,14 +275,18 @@ public class SAMLEndpoint {
|
||||||
builder.issuer(issuerURL);
|
builder.issuer(issuerURL);
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
||||||
.relayState(relayState);
|
.relayState(relayState);
|
||||||
|
boolean postBinding = config.isPostBindingResponse();
|
||||||
if (config.isWantAuthnRequestsSigned()) {
|
if (config.isWantAuthnRequestsSigned()) {
|
||||||
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
||||||
binding.signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
|
binding.signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
|
||||||
.signatureAlgorithm(provider.getSignatureAlgorithm())
|
.signatureAlgorithm(provider.getSignatureAlgorithm())
|
||||||
.signDocument();
|
.signDocument();
|
||||||
|
if (! postBinding && config.isAddExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
|
||||||
|
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (config.isPostBindingResponse()) {
|
if (postBinding) {
|
||||||
return binding.postBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
|
return binding.postBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
|
||||||
} else {
|
} else {
|
||||||
return binding.redirectBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
|
return binding.redirectBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
|
||||||
|
@ -418,7 +432,7 @@ public class SAMLEndpoint {
|
||||||
protected class PostBinding extends Binding {
|
protected class PostBinding extends Binding {
|
||||||
@Override
|
@Override
|
||||||
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
|
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||||
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey());
|
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKeyLocator());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -440,8 +454,8 @@ public class SAMLEndpoint {
|
||||||
protected class RedirectBinding extends Binding {
|
protected class RedirectBinding extends Binding {
|
||||||
@Override
|
@Override
|
||||||
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
|
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||||
PublicKey publicKey = getIDPKey();
|
KeyLocator locator = getIDPKeyLocator();
|
||||||
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, key);
|
SamlProtocolUtils.verifyRedirectSignature(documentHolder, locator, uriInfo, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,11 @@ import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PrivateKey;
|
import java.util.Set;
|
||||||
import java.security.PublicKey;
|
import java.util.TreeSet;
|
||||||
|
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||||
|
import org.keycloak.keys.KeyMetadata;
|
||||||
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Pedro Igor
|
* @author Pedro Igor
|
||||||
|
@ -97,18 +100,22 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
||||||
.relayState(request.getState());
|
.relayState(request.getState());
|
||||||
|
boolean postBinding = getConfig().isPostBindingAuthnRequest();
|
||||||
|
|
||||||
if (getConfig().isWantAuthnRequestsSigned()) {
|
if (getConfig().isWantAuthnRequestsSigned()) {
|
||||||
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
||||||
|
|
||||||
KeyPair keypair = new KeyPair(keys.getPublicKey(), keys.getPrivateKey());
|
KeyPair keypair = new KeyPair(keys.getPublicKey(), keys.getPrivateKey());
|
||||||
|
|
||||||
binding.signWith(keypair);
|
binding.signWith(keys.getKid(), keypair);
|
||||||
binding.signatureAlgorithm(getSignatureAlgorithm());
|
binding.signatureAlgorithm(getSignatureAlgorithm());
|
||||||
binding.signDocument();
|
binding.signDocument();
|
||||||
|
if (! postBinding && getConfig().isAddExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
|
||||||
|
authnRequestBuilder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getConfig().isPostBindingAuthnRequest()) {
|
if (postBinding) {
|
||||||
return binding.postBinding(authnRequestBuilder.toDocument()).request(destinationUrl);
|
return binding.postBinding(authnRequestBuilder.toDocument()).request(destinationUrl);
|
||||||
} else {
|
} else {
|
||||||
return binding.redirectBinding(authnRequestBuilder.toDocument()).request(destinationUrl);
|
return binding.redirectBinding(authnRequestBuilder.toDocument()).request(destinationUrl);
|
||||||
|
@ -198,7 +205,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
.relayState(userSession.getId());
|
.relayState(userSession.getId());
|
||||||
if (getConfig().isWantAuthnRequestsSigned()) {
|
if (getConfig().isWantAuthnRequestsSigned()) {
|
||||||
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
||||||
binding.signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
|
binding.signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
|
||||||
.signatureAlgorithm(getSignatureAlgorithm())
|
.signatureAlgorithm(getSignatureAlgorithm())
|
||||||
.signDocument();
|
.signDocument();
|
||||||
}
|
}
|
||||||
|
@ -225,11 +232,27 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned();
|
boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned();
|
||||||
String entityId = getEntityId(uriInfo, realm);
|
String entityId = getEntityId(uriInfo, realm);
|
||||||
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
|
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
|
||||||
String certificatePem = PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate());
|
|
||||||
String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, entityId, nameIDPolicyFormat, certificatePem);
|
StringBuilder keysString = new StringBuilder();
|
||||||
|
Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
|
||||||
|
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
|
||||||
|
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
|
||||||
|
keys.addAll(session.keys().getKeys(realm, false));
|
||||||
|
for (KeyMetadata key : keys) {
|
||||||
|
addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
|
||||||
|
}
|
||||||
|
String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, entityId, nameIDPolicyFormat, keysString.toString());
|
||||||
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
|
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
|
||||||
|
if (key == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.append(SPMetadataDescriptor.xmlKeyInfo(" ", key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, true));
|
||||||
|
}
|
||||||
|
|
||||||
public SignatureAlgorithm getSignatureAlgorithm() {
|
public SignatureAlgorithm getSignatureAlgorithm() {
|
||||||
String alg = getConfig().getSignatureAlgorithm();
|
String alg = getConfig().getSignatureAlgorithm();
|
||||||
if (alg != null) {
|
if (alg != null) {
|
||||||
|
|
|
@ -62,14 +62,45 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
||||||
getConfig().put("forceAuthn", String.valueOf(forceAuthn));
|
getConfig().put("forceAuthn", String.valueOf(forceAuthn));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link #getSigningCertificates()}}
|
||||||
|
* @param signingCertificate
|
||||||
|
*/
|
||||||
public String getSigningCertificate() {
|
public String getSigningCertificate() {
|
||||||
return getConfig().get("signingCertificate");
|
return getConfig().get(SIGNING_CERTIFICATE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link #addSigningCertificate(String)}}
|
||||||
|
* @param signingCertificate
|
||||||
|
*/
|
||||||
public void setSigningCertificate(String signingCertificate) {
|
public void setSigningCertificate(String signingCertificate) {
|
||||||
getConfig().put("signingCertificate", signingCertificate);
|
getConfig().put(SIGNING_CERTIFICATE_KEY, signingCertificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addSigningCertificate(String signingCertificate) {
|
||||||
|
String crt = getConfig().get(SIGNING_CERTIFICATE_KEY);
|
||||||
|
if (crt == null || crt.isEmpty()) {
|
||||||
|
getConfig().put(SIGNING_CERTIFICATE_KEY, signingCertificate);
|
||||||
|
} else {
|
||||||
|
// Note that "," is not coding character per PEM format specification:
|
||||||
|
// see https://tools.ietf.org/html/rfc1421, section 4.3.2.4 Step 4: Printable Encoding
|
||||||
|
getConfig().put(SIGNING_CERTIFICATE_KEY, crt + "," + signingCertificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getSigningCertificates() {
|
||||||
|
String crt = getConfig().get(SIGNING_CERTIFICATE_KEY);
|
||||||
|
if (crt == null || crt.isEmpty()) {
|
||||||
|
return new String[] { };
|
||||||
|
}
|
||||||
|
// Note that "," is not coding character per PEM format specification:
|
||||||
|
// see https://tools.ietf.org/html/rfc1421, section 4.3.2.4 Step 4: Printable Encoding
|
||||||
|
return crt.split(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
|
||||||
|
|
||||||
public String getNameIDPolicyFormat() {
|
public String getNameIDPolicyFormat() {
|
||||||
return getConfig().get("nameIDPolicyFormat");
|
return getConfig().get("nameIDPolicyFormat");
|
||||||
}
|
}
|
||||||
|
@ -86,6 +117,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
||||||
getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
|
getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAddExtensionsElementWithKeyInfo() {
|
||||||
|
return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddExtensionsElementWithKeyInfo(boolean addExtensionsElementWithKeyInfo) {
|
||||||
|
getConfig().put("addExtensionsElementWithKeyInfo", String.valueOf(addExtensionsElementWithKeyInfo));
|
||||||
|
}
|
||||||
|
|
||||||
public String getSignatureAlgorithm() {
|
public String getSignatureAlgorithm() {
|
||||||
return getConfig().get("signatureAlgorithm");
|
return getConfig().get("signatureAlgorithm");
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||||
samlIdentityProviderConfig.setSingleLogoutServiceUrl(singleLogoutServiceUrl);
|
samlIdentityProviderConfig.setSingleLogoutServiceUrl(singleLogoutServiceUrl);
|
||||||
samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl);
|
samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl);
|
||||||
samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned());
|
samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned());
|
||||||
|
samlIdentityProviderConfig.setAddExtensionsElementWithKeyInfo(false);
|
||||||
samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned());
|
samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned());
|
||||||
samlIdentityProviderConfig.setPostBindingResponse(postBinding);
|
samlIdentityProviderConfig.setPostBindingResponse(postBinding);
|
||||||
samlIdentityProviderConfig.setPostBindingAuthnRequest(postBinding);
|
samlIdentityProviderConfig.setPostBindingAuthnRequest(postBinding);
|
||||||
|
@ -121,7 +122,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||||
Element x509KeyInfo = DocumentUtil.getChildElement(keyInfo, new QName("dsig", "X509Certificate"));
|
Element x509KeyInfo = DocumentUtil.getChildElement(keyInfo, new QName("dsig", "X509Certificate"));
|
||||||
|
|
||||||
if (KeyTypes.SIGNING.equals(keyDescriptorType.getUse())) {
|
if (KeyTypes.SIGNING.equals(keyDescriptorType.getUse())) {
|
||||||
samlIdentityProviderConfig.setSigningCertificate(x509KeyInfo.getTextContent());
|
samlIdentityProviderConfig.addSigningCertificate(x509KeyInfo.getTextContent());
|
||||||
} else if (KeyTypes.ENCRYPTION.equals(keyDescriptorType.getUse())) {
|
} else if (KeyTypes.ENCRYPTION.equals(keyDescriptorType.getUse())) {
|
||||||
samlIdentityProviderConfig.setEncryptionPublicKey(x509KeyInfo.getTextContent());
|
samlIdentityProviderConfig.setEncryptionPublicKey(x509KeyInfo.getTextContent());
|
||||||
} else if (keyDescriptorType.getUse() == null) {
|
} else if (keyDescriptorType.getUse() == null) {
|
||||||
|
@ -131,8 +132,8 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defaultCertificate != null) {
|
if (defaultCertificate != null) {
|
||||||
if (samlIdentityProviderConfig.getSigningCertificate() == null) {
|
if (samlIdentityProviderConfig.getSigningCertificates().length == 0) {
|
||||||
samlIdentityProviderConfig.setSigningCertificate(defaultCertificate);
|
samlIdentityProviderConfig.addSigningCertificate(defaultCertificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samlIdentityProviderConfig.getEncryptionPublicKey() == null) {
|
if (samlIdentityProviderConfig.getEncryptionPublicKey() == null) {
|
||||||
|
|
|
@ -101,6 +101,7 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
|
||||||
app.setFullScopeAllowed(true);
|
app.setFullScopeAllowed(true);
|
||||||
app.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
app.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||||
attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true
|
attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true
|
||||||
|
attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT, SamlProtocol.ATTRIBUTE_FALSE_VALUE); // default to false
|
||||||
attributes.put(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString());
|
attributes.put(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString());
|
||||||
attributes.put(SamlConfigAttributes.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
|
attributes.put(SamlConfigAttributes.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
|
||||||
SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity);
|
SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity);
|
||||||
|
|
|
@ -23,6 +23,8 @@ import org.keycloak.saml.SignatureAlgorithm;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Configuration of a SAML-enabled client.
|
||||||
|
*
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
|
@ -116,7 +118,14 @@ public class SamlClient extends ClientConfigResolver {
|
||||||
|
|
||||||
public void setRequiresRealmSignature(boolean val) {
|
public void setRequiresRealmSignature(boolean val) {
|
||||||
client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(val));
|
client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addExtensionsElementWithKeyInfo() {
|
||||||
|
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddExtensionsElementWithKeyInfo(boolean val) {
|
||||||
|
client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT, Boolean.toString(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean forcePostBinding() {
|
public boolean forcePostBinding() {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.saml;
|
package org.keycloak.protocol.saml;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import org.keycloak.models.ClientTemplateModel;
|
import org.keycloak.models.ClientTemplateModel;
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
|
|
||||||
|
@ -89,7 +90,14 @@ public class SamlClientTemplate {
|
||||||
|
|
||||||
public void setRequiresRealmSignature(boolean val) {
|
public void setRequiresRealmSignature(boolean val) {
|
||||||
clientTemplate.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(val));
|
clientTemplate.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addExtensionsElementWithKeyInfo() {
|
||||||
|
return Objects.equals("true", clientTemplate.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddExtensionsElementWithKeyInfo(boolean val) {
|
||||||
|
clientTemplate.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT, Boolean.toString(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean forcePostBinding() {
|
public boolean forcePostBinding() {
|
||||||
|
|
|
@ -31,6 +31,7 @@ public interface SamlConfigAttributes {
|
||||||
String SAML_AUTHNSTATEMENT = "saml.authnstatement";
|
String SAML_AUTHNSTATEMENT = "saml.authnstatement";
|
||||||
String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format";
|
String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format";
|
||||||
String SAML_SERVER_SIGNATURE = "saml.server.signature";
|
String SAML_SERVER_SIGNATURE = "saml.server.signature";
|
||||||
|
String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
|
||||||
String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
|
String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
|
||||||
String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
|
String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
|
||||||
String SAML_ENCRYPT = "saml.encrypt";
|
String SAML_ENCRYPT = "saml.encrypt";
|
||||||
|
|
|
@ -74,8 +74,10 @@ import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -98,6 +100,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
public static final String SAML_REDIRECT_BINDING = "get";
|
public static final String SAML_REDIRECT_BINDING = "get";
|
||||||
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
|
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
|
||||||
public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
|
public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
|
||||||
|
public static final String SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "saml.logout.addExtensionsElementWithKeyInfo";
|
||||||
public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID";
|
public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID";
|
||||||
public static final String SAML_LOGOUT_RELAY_STATE = "SAML_LOGOUT_RELAY_STATE";
|
public static final String SAML_LOGOUT_RELAY_STATE = "SAML_LOGOUT_RELAY_STATE";
|
||||||
public static final String SAML_LOGOUT_CANONICALIZATION = "SAML_LOGOUT_CANONICALIZATION";
|
public static final String SAML_LOGOUT_CANONICALIZATION = "SAML_LOGOUT_CANONICALIZATION";
|
||||||
|
@ -373,7 +376,15 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
Document samlDocument = null;
|
Document samlDocument = null;
|
||||||
|
KeyManager keyManager = session.keys();
|
||||||
|
KeyManager.ActiveKey keys = keyManager.getActiveKey(realm);
|
||||||
|
boolean postBinding = isPostBinding(clientSession);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ((! postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
||||||
|
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
|
||||||
|
}
|
||||||
|
|
||||||
ResponseType samlModel = builder.buildModel();
|
ResponseType samlModel = builder.buildModel();
|
||||||
final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
|
final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
|
||||||
populateRoles(roleListMapper, session, userSession, clientSession, attributeStatement);
|
populateRoles(roleListMapper, session, userSession, clientSession, attributeStatement);
|
||||||
|
@ -394,22 +405,19 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder();
|
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder();
|
||||||
bindingBuilder.relayState(relayState);
|
bindingBuilder.relayState(relayState);
|
||||||
|
|
||||||
KeyManager keyManager = session.keys();
|
|
||||||
KeyManager.ActiveKey keys = keyManager.getActiveKey(realm);
|
|
||||||
|
|
||||||
if (samlClient.requiresRealmSignature()) {
|
if (samlClient.requiresRealmSignature()) {
|
||||||
String canonicalization = samlClient.getCanonicalizationMethod();
|
String canonicalization = samlClient.getCanonicalizationMethod();
|
||||||
if (canonicalization != null) {
|
if (canonicalization != null) {
|
||||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
bindingBuilder.canonicalizationMethod(canonicalization);
|
||||||
}
|
}
|
||||||
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
||||||
}
|
}
|
||||||
if (samlClient.requiresAssertionSignature()) {
|
if (samlClient.requiresAssertionSignature()) {
|
||||||
String canonicalization = samlClient.getCanonicalizationMethod();
|
String canonicalization = samlClient.getCanonicalizationMethod();
|
||||||
if (canonicalization != null) {
|
if (canonicalization != null) {
|
||||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
bindingBuilder.canonicalizationMethod(canonicalization);
|
||||||
}
|
}
|
||||||
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signAssertions();
|
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signAssertions();
|
||||||
}
|
}
|
||||||
if (samlClient.requiresEncryption()) {
|
if (samlClient.requiresEncryption()) {
|
||||||
PublicKey publicKey = null;
|
PublicKey publicKey = null;
|
||||||
|
@ -496,12 +504,17 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
if (isLogoutPostBindingForClient(clientSession)) {
|
if (isLogoutPostBindingForClient(clientSession)) {
|
||||||
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
|
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
|
||||||
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
|
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
|
||||||
|
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
|
||||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
||||||
return binding.postBinding(logoutBuilder.buildDocument()).request(bindingUri);
|
return binding.postBinding(logoutBuilder.buildDocument()).request(bindingUri);
|
||||||
} else {
|
} else {
|
||||||
logger.debug("frontchannel redirect binding");
|
logger.debug("frontchannel redirect binding");
|
||||||
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
|
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
|
||||||
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
|
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
|
||||||
|
if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
||||||
|
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
||||||
|
logoutBuilder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
|
||||||
|
}
|
||||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
||||||
return binding.redirectBinding(logoutBuilder.buildDocument()).request(bindingUri);
|
return binding.redirectBinding(logoutBuilder.buildDocument()).request(bindingUri);
|
||||||
}
|
}
|
||||||
|
@ -534,6 +547,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
|
||||||
binding.relayState(logoutRelayState);
|
binding.relayState(logoutRelayState);
|
||||||
String signingAlgorithm = userSession.getNote(SAML_LOGOUT_SIGNATURE_ALGORITHM);
|
String signingAlgorithm = userSession.getNote(SAML_LOGOUT_SIGNATURE_ALGORITHM);
|
||||||
|
boolean postBinding = isLogoutPostBindingForInitiator(userSession);
|
||||||
if (signingAlgorithm != null) {
|
if (signingAlgorithm != null) {
|
||||||
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(signingAlgorithm);
|
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(signingAlgorithm);
|
||||||
String canonicalization = userSession.getNote(SAML_LOGOUT_CANONICALIZATION);
|
String canonicalization = userSession.getNote(SAML_LOGOUT_CANONICALIZATION);
|
||||||
|
@ -541,7 +555,11 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
binding.canonicalizationMethod(canonicalization);
|
binding.canonicalizationMethod(canonicalization);
|
||||||
}
|
}
|
||||||
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
||||||
binding.signatureAlgorithm(algorithm).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
||||||
|
boolean addExtension = (! postBinding) && Objects.equals("true", userSession.getNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
|
||||||
|
if (addExtension) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
|
||||||
|
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -577,6 +595,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
String logoutRequestString = null;
|
String logoutRequestString = null;
|
||||||
try {
|
try {
|
||||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
||||||
|
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
|
||||||
logoutRequestString = binding.postBinding(logoutBuilder.buildDocument()).encoded();
|
logoutRequestString = binding.postBinding(logoutBuilder.buildDocument()).encoded();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("failed to send saml logout", e);
|
logger.warn("failed to send saml logout", e);
|
||||||
|
@ -639,7 +658,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
|
||||||
if (samlClient.requiresRealmSignature()) {
|
if (samlClient.requiresRealmSignature()) {
|
||||||
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
||||||
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
||||||
}
|
}
|
||||||
return binding;
|
return binding;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.saml;
|
package org.keycloak.protocol.saml;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.PemUtils;
|
import org.keycloak.common.util.PemUtils;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -33,6 +34,15 @@ import javax.ws.rs.core.UriInfo;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
|
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -40,20 +50,36 @@ import java.security.cert.Certificate;
|
||||||
*/
|
*/
|
||||||
public class SamlProtocolUtils {
|
public class SamlProtocolUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a signature of the given SAML document using settings for the given client.
|
||||||
|
* Throws an exception if the client signature is expected to be present as per the client
|
||||||
|
* settings and it is invalid, otherwise returns back to the caller.
|
||||||
|
*
|
||||||
|
* @param client
|
||||||
|
* @param document
|
||||||
|
* @throws VerificationException
|
||||||
|
*/
|
||||||
public static void verifyDocumentSignature(ClientModel client, Document document) throws VerificationException {
|
public static void verifyDocumentSignature(ClientModel client, Document document) throws VerificationException {
|
||||||
SamlClient samlClient = new SamlClient(client);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
if (!samlClient.requiresClientSignature()) {
|
if (!samlClient.requiresClientSignature()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PublicKey publicKey = getSignatureValidationKey(client);
|
PublicKey publicKey = getSignatureValidationKey(client);
|
||||||
verifyDocumentSignature(document, publicKey);
|
verifyDocumentSignature(document, new HardcodedKeyLocator(publicKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void verifyDocumentSignature(Document document, PublicKey publicKey) throws VerificationException {
|
/**
|
||||||
|
* Verifies a signature of the given SAML document using keys obtained from the given key locator.
|
||||||
|
* Throws an exception if the client signature is invalid, otherwise returns back to the caller.
|
||||||
|
*
|
||||||
|
* @param document
|
||||||
|
* @param keyLocator
|
||||||
|
* @throws VerificationException
|
||||||
|
*/
|
||||||
|
public static void verifyDocumentSignature(Document document, KeyLocator keyLocator) throws VerificationException {
|
||||||
SAML2Signature saml2Signature = new SAML2Signature();
|
SAML2Signature saml2Signature = new SAML2Signature();
|
||||||
try {
|
try {
|
||||||
if (!saml2Signature.validate(document, publicKey)) {
|
if (!saml2Signature.validate(document, keyLocator)) {
|
||||||
throw new VerificationException("Invalid signature on document");
|
throw new VerificationException("Invalid signature on document");
|
||||||
}
|
}
|
||||||
} catch (ProcessingException e) {
|
} catch (ProcessingException e) {
|
||||||
|
@ -61,10 +87,22 @@ public class SamlProtocolUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns public part of SAML signing key from the client settings.
|
||||||
|
* @param client
|
||||||
|
* @return Public key for signature validation.
|
||||||
|
* @throws VerificationException
|
||||||
|
*/
|
||||||
public static PublicKey getSignatureValidationKey(ClientModel client) throws VerificationException {
|
public static PublicKey getSignatureValidationKey(ClientModel client) throws VerificationException {
|
||||||
return getPublicKey(new SamlClient(client).getClientSigningCertificate());
|
return getPublicKey(new SamlClient(client).getClientSigningCertificate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns public part of SAML encryption key from the client settings.
|
||||||
|
* @param client
|
||||||
|
* @return Public key for encryption.
|
||||||
|
* @throws VerificationException
|
||||||
|
*/
|
||||||
public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
|
public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
|
||||||
return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
|
return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
|
||||||
}
|
}
|
||||||
|
@ -85,7 +123,7 @@ public class SamlProtocolUtils {
|
||||||
return cert.getPublicKey();
|
return cert.getPublicKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void verifyRedirectSignature(PublicKey publicKey, UriInfo uriInformation, String paramKey) throws VerificationException {
|
public static void verifyRedirectSignature(SAMLDocumentHolder documentHolder, KeyLocator locator, UriInfo uriInformation, String paramKey) throws VerificationException {
|
||||||
MultivaluedMap<String, String> encodedParams = uriInformation.getQueryParameters(false);
|
MultivaluedMap<String, String> encodedParams = uriInformation.getQueryParameters(false);
|
||||||
String request = encodedParams.getFirst(paramKey);
|
String request = encodedParams.getFirst(paramKey);
|
||||||
String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
||||||
|
@ -96,10 +134,11 @@ public class SamlProtocolUtils {
|
||||||
if (algorithm == null) throw new VerificationException("SigAlg was null");
|
if (algorithm == null) throw new VerificationException("SigAlg was null");
|
||||||
if (signature == null) throw new VerificationException("Signature was null");
|
if (signature == null) throw new VerificationException("Signature was null");
|
||||||
|
|
||||||
|
String keyId = getMessageSigningKeyId(documentHolder.getSamlObject());
|
||||||
|
|
||||||
// Shibboleth doesn't sign the document for redirect binding.
|
// Shibboleth doesn't sign the document for redirect binding.
|
||||||
// todo maybe a flag?
|
// todo maybe a flag?
|
||||||
|
|
||||||
|
|
||||||
UriBuilder builder = UriBuilder.fromPath("/")
|
UriBuilder builder = UriBuilder.fromPath("/")
|
||||||
.queryParam(paramKey, request);
|
.queryParam(paramKey, request);
|
||||||
if (encodedParams.containsKey(GeneralConstants.RELAY_STATE)) {
|
if (encodedParams.containsKey(GeneralConstants.RELAY_STATE)) {
|
||||||
|
@ -113,8 +152,13 @@ public class SamlProtocolUtils {
|
||||||
|
|
||||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
|
||||||
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
|
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
|
||||||
validator.initVerify(publicKey);
|
Key key = locator.getKey(keyId);
|
||||||
|
if (key instanceof PublicKey) {
|
||||||
|
validator.initVerify((PublicKey) key);
|
||||||
validator.update(rawQuery.getBytes("UTF-8"));
|
validator.update(rawQuery.getBytes("UTF-8"));
|
||||||
|
} else {
|
||||||
|
throw new VerificationException("Invalid key locator for signature verification");
|
||||||
|
}
|
||||||
if (!validator.verify(decodedSignature)) {
|
if (!validator.verify(decodedSignature)) {
|
||||||
throw new VerificationException("Invalid query param signature");
|
throw new VerificationException("Invalid query param signature");
|
||||||
}
|
}
|
||||||
|
@ -123,5 +167,32 @@ public class SamlProtocolUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String getMessageSigningKeyId(SAML2Object doc) {
|
||||||
|
final ExtensionsType extensions;
|
||||||
|
if (doc instanceof RequestAbstractType) {
|
||||||
|
extensions = ((RequestAbstractType) doc).getExtensions();
|
||||||
|
} else if (doc instanceof StatusResponseType) {
|
||||||
|
extensions = ((StatusResponseType) doc).getExtensions();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensions == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object ext : extensions.getAny()) {
|
||||||
|
if (! (ext instanceof Element)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String res = KeycloakKeySamlExtensionGenerator.getMessageSigningKeyIdFromElement((Element) ext);
|
||||||
|
|
||||||
|
if (res != null) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,11 @@ public class SamlRepresentationAttributes {
|
||||||
public String getSamlServerSignature() {
|
public String getSamlServerSignature() {
|
||||||
if (getAttributes() == null) return null;
|
if (getAttributes() == null) return null;
|
||||||
return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE);
|
return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddExtensionsElementWithKeyInfo() {
|
||||||
|
if (getAttributes() == null) return null;
|
||||||
|
return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getForcePostBinding() {
|
public String getForcePostBinding() {
|
||||||
|
|
|
@ -74,6 +74,17 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import org.keycloak.common.util.StringPropertyReplacer;
|
||||||
|
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||||
|
import org.keycloak.keys.KeyMetadata;
|
||||||
|
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||||
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
import org.keycloak.saml.SPMetadataDescriptor;
|
||||||
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resource class for the oauth/openid connect token service
|
* Resource class for the oauth/openid connect token service
|
||||||
|
@ -336,6 +347,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
String logoutBinding = getBindingType();
|
String logoutBinding = getBindingType();
|
||||||
if ("true".equals(samlClient.forcePostBinding()))
|
if ("true".equals(samlClient.forcePostBinding()))
|
||||||
logoutBinding = SamlProtocol.SAML_POST_BINDING;
|
logoutBinding = SamlProtocol.SAML_POST_BINDING;
|
||||||
|
boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding);
|
||||||
|
|
||||||
String bindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding);
|
String bindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding);
|
||||||
UserSessionModel userSession = authResult.getSession();
|
UserSessionModel userSession = authResult.getSession();
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
|
||||||
|
@ -347,6 +360,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
|
||||||
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, Boolean.toString((! postBinding) && samlClient.addExtensionsElementWithKeyInfo()));
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod());
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod());
|
||||||
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
|
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
|
||||||
// remove client from logout requests
|
// remove client from logout requests
|
||||||
|
@ -397,14 +411,17 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
builder.destination(logoutBindingUri);
|
builder.destination(logoutBindingUri);
|
||||||
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
|
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState);
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState);
|
||||||
|
boolean postBinding = SamlProtocol.SAML_POST_BINDING.equals(logoutBinding);
|
||||||
if (samlClient.requiresRealmSignature()) {
|
if (samlClient.requiresRealmSignature()) {
|
||||||
SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm();
|
SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm();
|
||||||
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
|
||||||
binding.signatureAlgorithm(algorithm).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
||||||
|
if (! postBinding && samlClient.addExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
|
||||||
|
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (SamlProtocol.SAML_POST_BINDING.equals(logoutBinding)) {
|
if (postBinding) {
|
||||||
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||||
} else {
|
} else {
|
||||||
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
|
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||||
|
@ -466,7 +483,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
|
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
|
||||||
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
|
KeyLocator clientKeyLocator = new HardcodedKeyLocator(publicKey);
|
||||||
|
SamlProtocolUtils.verifyRedirectSignature(documentHolder, clientKeyLocator, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -541,12 +559,30 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
public static String getIDPMetadataDescriptor(UriInfo uriInfo, KeycloakSession session, RealmModel realm) throws IOException {
|
public static String getIDPMetadataDescriptor(UriInfo uriInfo, KeycloakSession session, RealmModel realm) throws IOException {
|
||||||
InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml");
|
InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml");
|
||||||
String template = StreamUtil.readString(is);
|
String template = StreamUtil.readString(is);
|
||||||
template = template.replace("${idp.entityID}", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
|
Properties props = new Properties();
|
||||||
template = template.replace("${idp.sso.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
|
props.put("idp.entityID", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
|
||||||
template = template.replace("${idp.sso.HTTP-Redirect}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
|
props.put("idp.sso.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
|
||||||
template = template.replace("${idp.sls.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
|
props.put("idp.sso.HTTP-Redirect", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
|
||||||
template = template.replace("${idp.signing.certificate}", PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate()));
|
props.put("idp.sls.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
|
||||||
return template;
|
StringBuilder keysString = new StringBuilder();
|
||||||
|
Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
|
||||||
|
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
|
||||||
|
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
|
||||||
|
keys.addAll(session.keys().getKeys(realm, false));
|
||||||
|
for (KeyMetadata key : keys) {
|
||||||
|
addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
|
||||||
|
}
|
||||||
|
props.put("idp.signing.certificates", keysString.toString());
|
||||||
|
return StringPropertyReplacer.replaceProperties(template, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
|
||||||
|
if (key == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.append(SPMetadataDescriptor.xmlKeyInfo(" ",
|
||||||
|
key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.keycloak.protocol.saml.installation;
|
package org.keycloak.protocol.saml.installation;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.common.util.PemUtils;
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -42,14 +41,14 @@ public class KeycloakSamlClientInstallation implements ClientInstallationProvide
|
||||||
@Override
|
@Override
|
||||||
public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) {
|
public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) {
|
||||||
SamlClient samlClient = new SamlClient(client);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
StringBuffer buffer = new StringBuffer();
|
StringBuilder buffer = new StringBuilder();
|
||||||
buffer.append("<keycloak-saml-adapter>\n");
|
buffer.append("<keycloak-saml-adapter>\n");
|
||||||
baseXml(session, realm, client, baseUri, samlClient, buffer);
|
baseXml(session, realm, client, baseUri, samlClient, buffer);
|
||||||
buffer.append("</keycloak-saml-adapter>\n");
|
buffer.append("</keycloak-saml-adapter>\n");
|
||||||
return Response.ok(buffer.toString(), MediaType.TEXT_PLAIN_TYPE).build();
|
return Response.ok(buffer.toString(), MediaType.TEXT_PLAIN_TYPE).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void baseXml(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri, SamlClient samlClient, StringBuffer buffer) {
|
public static void baseXml(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri, SamlClient samlClient, StringBuilder buffer) {
|
||||||
buffer.append(" <SP entityID=\"").append(client.getClientId()).append("\"\n");
|
buffer.append(" <SP entityID=\"").append(client.getClientId()).append("\"\n");
|
||||||
buffer.append(" sslPolicy=\"").append(realm.getSslRequired().name()).append("\"\n");
|
buffer.append(" sslPolicy=\"").append(realm.getSslRequired().name()).append("\"\n");
|
||||||
buffer.append(" logoutPage=\"SPECIFY YOUR LOGOUT PAGE!\">\n");
|
buffer.append(" logoutPage=\"SPECIFY YOUR LOGOUT PAGE!\">\n");
|
||||||
|
@ -113,15 +112,6 @@ public class KeycloakSamlClientInstallation implements ClientInstallationProvide
|
||||||
buffer.append(" postBindingUrl=\"").append(bindingUrl).append("\"\n");
|
buffer.append(" postBindingUrl=\"").append(bindingUrl).append("\"\n");
|
||||||
buffer.append(" redirectBindingUrl=\"").append(bindingUrl).append("\"");
|
buffer.append(" redirectBindingUrl=\"").append(bindingUrl).append("\"");
|
||||||
buffer.append("/>\n");
|
buffer.append("/>\n");
|
||||||
if (samlClient.requiresRealmSignature()) {
|
|
||||||
buffer.append(" <Keys>\n");
|
|
||||||
buffer.append(" <Key signing=\"true\">\n");
|
|
||||||
buffer.append(" <CertificatePem>\n");
|
|
||||||
buffer.append(" ").append(PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate())).append("\n");
|
|
||||||
buffer.append(" </CertificatePem>\n");
|
|
||||||
buffer.append(" </Key>\n");
|
|
||||||
buffer.append(" </Keys>\n");
|
|
||||||
}
|
|
||||||
buffer.append(" </IDP>\n");
|
buffer.append(" </IDP>\n");
|
||||||
buffer.append(" </SP>\n");
|
buffer.append(" </SP>\n");
|
||||||
}
|
}
|
||||||
|
@ -138,7 +128,7 @@ public class KeycloakSamlClientInstallation implements ClientInstallationProvide
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getHelpText() {
|
public String getHelpText() {
|
||||||
return "Keycloak SAML adapter configuration file. Put this in WEB-INF directory if your WAR.";
|
return "Keycloak SAML adapter configuration file. Put this in WEB-INF directory of your WAR.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -39,7 +39,7 @@ public class KeycloakSamlSubsystemInstallation implements ClientInstallationProv
|
||||||
@Override
|
@Override
|
||||||
public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) {
|
public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) {
|
||||||
SamlClient samlClient = new SamlClient(client);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
StringBuffer buffer = new StringBuffer();
|
StringBuilder buffer = new StringBuilder();
|
||||||
buffer.append("<secure-deployment name=\"YOUR-WAR.war\">\n");
|
buffer.append("<secure-deployment name=\"YOUR-WAR.war\">\n");
|
||||||
KeycloakSamlClientInstallation.baseXml(session, realm, client, baseUri, samlClient, buffer);
|
KeycloakSamlClientInstallation.baseXml(session, realm, client, baseUri, samlClient, buffer);
|
||||||
buffer.append("</secure-deployment>\n");
|
buffer.append("</secure-deployment>\n");
|
||||||
|
|
|
@ -32,6 +32,11 @@ import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||||
|
import org.keycloak.keys.KeyMetadata;
|
||||||
|
import org.keycloak.saml.SPMetadataDescriptor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -41,49 +46,61 @@ public class SamlIDPDescriptorClientInstallation implements ClientInstallationPr
|
||||||
public static String getIDPDescriptorForClient(KeycloakSession session, RealmModel realm, ClientModel client, URI serverBaseUri) {
|
public static String getIDPDescriptorForClient(KeycloakSession session, RealmModel realm, ClientModel client, URI serverBaseUri) {
|
||||||
SamlClient samlClient = new SamlClient(client);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
String idpEntityId = RealmsResource.realmBaseUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName()).toString();
|
String idpEntityId = RealmsResource.realmBaseUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName()).toString();
|
||||||
String idp = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
StringBuilder sb = new StringBuilder();
|
||||||
"<EntityDescriptor entityID=\"" + idpEntityId + "\"\n" +
|
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||||
" xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n" +
|
+ "<EntityDescriptor entityID=\"").append(idpEntityId).append("\"\n"
|
||||||
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
|
+ " xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n"
|
||||||
" <IDPSSODescriptor WantAuthnRequestsSigned=\"" + Boolean.toString(samlClient.requiresClientSignature()) + "\"\n" +
|
+ " xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\"\n"
|
||||||
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n";
|
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"
|
||||||
|
+ " <IDPSSODescriptor WantAuthnRequestsSigned=\"")
|
||||||
|
.append(samlClient.requiresClientSignature())
|
||||||
|
.append("\"\n"
|
||||||
|
+ " protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n");
|
||||||
if (samlClient.forceNameIDFormat() && samlClient.getNameIDFormat() != null) {
|
if (samlClient.forceNameIDFormat() && samlClient.getNameIDFormat() != null) {
|
||||||
idp += " <NameIDFormat>" + samlClient.getNameIDFormat() + "</NameIDFormat>\n";
|
sb.append(" <NameIDFormat>").append(samlClient.getNameIDFormat()).append("</NameIDFormat>\n");
|
||||||
} else {
|
} else {
|
||||||
idp += " <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>\n" +
|
sb.append(" <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>\n"
|
||||||
" <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n" +
|
+ " <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n"
|
||||||
" <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>\n" +
|
+ " <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>\n"
|
||||||
" <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>\n";
|
+ " <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>\n");
|
||||||
}
|
}
|
||||||
String bindUrl = RealmsResource.protocolUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString();
|
String bindUrl = RealmsResource.protocolUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString();
|
||||||
idp += "\n" +
|
sb.append("\n"
|
||||||
" <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n" +
|
+ " <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n"
|
||||||
" Location=\"" + bindUrl + "\" />\n";
|
+ " Location=\"").append(bindUrl).append("\" />\n");
|
||||||
if (! samlClient.forcePostBinding()) {
|
if (! samlClient.forcePostBinding()) {
|
||||||
idp += " <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"\n" +
|
sb.append(" <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"\n"
|
||||||
" Location=\"" + bindUrl + "\" />\n";
|
+ " Location=\"").append(bindUrl).append("\" />\n");
|
||||||
|
|
||||||
}
|
}
|
||||||
idp += " <SingleLogoutService\n" +
|
sb.append(" <SingleLogoutService\n"
|
||||||
" Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n" +
|
+ " Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n"
|
||||||
" Location=\"" + bindUrl + "\" />\n";
|
+ " Location=\"").append(bindUrl).append("\" />\n");
|
||||||
if (! samlClient.forcePostBinding()) {
|
if (! samlClient.forcePostBinding()) {
|
||||||
idp += " <SingleLogoutService\n" +
|
sb.append(" <SingleLogoutService\n"
|
||||||
" Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"\n" +
|
+ " Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"\n"
|
||||||
" Location=\"" + bindUrl + "\" />\n";
|
+ " Location=\"").append(bindUrl).append("\" />\n");
|
||||||
}
|
}
|
||||||
idp += " <KeyDescriptor use=\"signing\">\n" +
|
|
||||||
" <dsig:KeyInfo xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
|
Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
|
||||||
" <dsig:X509Data>\n" +
|
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
|
||||||
" <dsig:X509Certificate>\n" +
|
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
|
||||||
" " + PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate()) + "\n" +
|
keys.addAll(session.keys().getKeys(realm, false));
|
||||||
" </dsig:X509Certificate>\n" +
|
for (KeyMetadata key : keys) {
|
||||||
" </dsig:X509Data>\n" +
|
addKeyInfo(sb, key, KeyTypes.SIGNING.value());
|
||||||
" </dsig:KeyInfo>\n" +
|
}
|
||||||
" </KeyDescriptor>\n" +
|
|
||||||
" </IDPSSODescriptor>\n" +
|
sb.append(" </IDPSSODescriptor>\n"
|
||||||
"</EntityDescriptor>\n";
|
+ "</EntityDescriptor>\n");
|
||||||
return idp;
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
|
||||||
|
if (key == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.append(SPMetadataDescriptor.xmlKeyInfo(" ", key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -45,7 +46,8 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro
|
||||||
if (logoutUrl == null) logoutUrl = client.getManagementUrl();
|
if (logoutUrl == null) logoutUrl = client.getManagementUrl();
|
||||||
String nameIdFormat = samlClient.getNameIDFormat();
|
String nameIdFormat = samlClient.getNameIDFormat();
|
||||||
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
|
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
|
||||||
return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl, samlClient.requiresClientSignature(), client.getClientId(), nameIdFormat, samlClient.getClientSigningCertificate());
|
String spCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientSigningCertificate(), KeyTypes.SIGNING.value(), true);
|
||||||
|
return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl, samlClient.requiresClientSignature(), client.getClientId(), nameIdFormat, spCertificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue