diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 998ffcc834c08..1a679fcd5ef9d 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -189,7 +189,7 @@ 5.8.0 4.10.1 2.0.2.Final - 21.0.2 + 22.0.0 1.15.0 3.35.0 2.20.0 @@ -6393,7 +6393,7 @@ org.keycloak - keycloak-admin-client-jakarta + keycloak-admin-client ${keycloak.version} @@ -6412,6 +6412,11 @@ keycloak-authz-client ${keycloak.version} + + org.keycloak + keycloak-policy-enforcer + ${keycloak.version} + io.quarkus quarkus-vertx-graphql diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 351d2a1e63575..3e534a1e6a12c 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -103,7 +103,7 @@ - 21.0.2 + 22.0.0 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy diff --git a/extensions/keycloak-admin-client-reactive/runtime/pom.xml b/extensions/keycloak-admin-client-reactive/runtime/pom.xml index b80c470083f5b..695466e2212a8 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/pom.xml +++ b/extensions/keycloak-admin-client-reactive/runtime/pom.xml @@ -34,7 +34,7 @@ org.keycloak - keycloak-admin-client-jakarta + keycloak-admin-client org.jboss.resteasy @@ -76,7 +76,7 @@ - org.keycloak:keycloak-admin-client-jakarta + org.keycloak:keycloak-admin-client org/keycloak/admin/client/JacksonProvider.class diff --git a/extensions/keycloak-admin-client/runtime/pom.xml b/extensions/keycloak-admin-client/runtime/pom.xml index 6e02535f0a76a..8570274cc8c28 100644 --- a/extensions/keycloak-admin-client/runtime/pom.xml +++ b/extensions/keycloak-admin-client/runtime/pom.xml @@ -46,7 +46,7 @@ org.keycloak - keycloak-admin-client-jakarta + keycloak-admin-client org.jboss.resteasy diff --git a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java index c3ed5e0d022d6..d450e440b913f 100644 --- a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java +++ b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java @@ -1,21 +1,19 @@ package io.quarkus.keycloak.pep.deployment; -import org.keycloak.adapters.authentication.ClientCredentialsProvider; -import org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider; -import org.keycloak.adapters.authentication.JWTClientCredentialsProvider; -import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider; -import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory; import org.keycloak.adapters.authorization.cip.ClaimsInformationPointProviderFactory; import org.keycloak.adapters.authorization.cip.HttpClaimInformationPointProviderFactory; +import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory; import org.keycloak.authorization.client.representation.ServerConfiguration; import org.keycloak.authorization.client.representation.TokenIntrospectionResponse; -import org.keycloak.common.crypto.CryptoProvider; -import org.keycloak.crypto.def.DefaultCryptoProvider; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jws.JWSHeader; import org.keycloak.json.StringListMapDeserializer; import org.keycloak.json.StringOrArrayDeserializer; +import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; +import org.keycloak.protocol.oidc.client.authentication.ClientIdAndSecretCredentialsProvider; +import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider; +import org.keycloak.protocol.oidc.client.authentication.JWTClientSecretCredentialsProvider; import org.keycloak.protocol.oidc.representations.MTLSEndpointAliases; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.representations.AccessToken; @@ -80,8 +78,6 @@ public void registerServiceProviders(BuildProducer ser serviceProvider.produce(new ServiceProviderBuildItem(ClaimInformationPointProviderFactory.class.getName(), HttpClaimInformationPointProviderFactory.class.getName(), ClaimsInformationPointProviderFactory.class.getName())); - serviceProvider.produce(new ServiceProviderBuildItem(CryptoProvider.class.getName(), - DefaultCryptoProvider.class.getName())); } @BuildStep diff --git a/extensions/keycloak-authorization/runtime/pom.xml b/extensions/keycloak-authorization/runtime/pom.xml index 18a2eba3c6b31..a09782733e4d0 100644 --- a/extensions/keycloak-authorization/runtime/pom.xml +++ b/extensions/keycloak-authorization/runtime/pom.xml @@ -18,10 +18,6 @@ io.quarkus quarkus-oidc - - org.keycloak - keycloak-adapter-core - org.keycloak keycloak-core @@ -32,6 +28,11 @@ + + org.keycloak + keycloak-policy-enforcer + ${keycloak.version} + org.eclipse.angus angus-activation diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/HttpClientBuilder.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/HttpClientBuilder.java new file mode 100644 index 0000000000000..17ac4bac215d6 --- /dev/null +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/HttpClientBuilder.java @@ -0,0 +1,488 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2021 Red Hat, Inc., and individual 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 io.quarkus.keycloak.pep.runtime; + +import java.io.IOException; +import java.net.URI; +import java.security.KeyStore; +import java.security.Principal; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +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 org.apache.http.HttpHost; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.client.CookieStore; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.AuthSchemes; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.ConnectionConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.AllowAllHostnameVerifier; +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +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.cookie.CookieSpecProvider; +import org.apache.http.impl.auth.SPNegoSchemeFactory; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CookieSpecRegistries; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.impl.cookie.DefaultCookieSpecProvider; +import org.apache.http.ssl.SSLContexts; +import org.keycloak.common.util.EnvUtil; +import org.keycloak.common.util.KeystoreUtil; +import org.keycloak.representations.adapters.config.AdapterHttpClientConfig; + +/** + * Creates a {@link HttpClient} based on an {@link org.keycloak.representations.adapters.config.AdapterConfig}. + * + * This is the same code from the Keycloak {@code org.keycloak.adapters.HttpClientBuilder} class but without + * using Keycloak Adapter API/SPI. + */ +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 Bill Burke + * @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; + private SPNegoSchemeFactory spNegoSchemeFactory; + private boolean useSpNego; + + /** + * 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. NOTE 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(boolean disable) { + this.disableCookieCache = disable; + 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 HttpClientBuilder spNegoSchemeFactory(SPNegoSchemeFactory spnegoSchemeFactory) { + this.spNegoSchemeFactory = spnegoSchemeFactory; + return this; + } + + public HttpClientBuilder useSPNego(boolean useSpnego) { + this.useSpNego = useSpnego; + return this; + } + + 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 { + ConnectionSocketFactory sslsf; + SSLContext theContext = sslContext; + if (disableTrustManager) { + theContext = SSLContext.getInstance("SSL"); + theContext.init(null, new TrustManager[] { new PassthroughTrustManager() }, + new SecureRandom()); + verifier = new AllowAllHostnameVerifier(); + sslsf = new SSLConnectionSocketFactory(theContext, verifier); + } else if (theContext != null) { + sslsf = new SSLConnectionSocketFactory(theContext, verifier); + } else if (clientKeyStore != null || truststore != null) { + sslsf = new SSLConnectionSocketFactory(SSLContexts.custom() + .setProtocol(SSLConnectionSocketFactory.TLS) + .setSecureRandom(null) + .loadKeyMaterial(clientKeyStore, + clientPrivateKeyPassword != null ? clientPrivateKeyPassword.toCharArray() : null) + .loadTrustMaterial(truststore, null) + .build(), verifier); + } else { + final SSLContext tlsContext = SSLContext.getInstance(SSLSocketFactory.TLS); + tlsContext.init(null, null, null); + sslsf = new SSLConnectionSocketFactory(tlsContext, verifier); + } + + RegistryBuilder sf = RegistryBuilder.create(); + + sf.register("http", PlainConnectionSocketFactory.getSocketFactory()); + sf.register("https", sslsf); + + HttpClientConnectionManager cm; + + if (connectionPoolSize > 0) { + PoolingHttpClientConnectionManager tcm = new PoolingHttpClientConnectionManager(sf.build(), null, null, null, + connectionTTL, connectionTTLUnit); + tcm.setMaxTotal(connectionPoolSize); + if (maxPooledPerRoute == 0) + maxPooledPerRoute = connectionPoolSize; + tcm.setDefaultMaxPerRoute(maxPooledPerRoute); + cm = tcm; + + } else { + cm = new BasicHttpClientConnectionManager(sf.build()); + } + + SocketConfig.Builder socketConfig = SocketConfig.copy(SocketConfig.DEFAULT); + ConnectionConfig.Builder connConfig = ConnectionConfig.copy(ConnectionConfig.DEFAULT); + RequestConfig.Builder requestConfig = RequestConfig.copy(RequestConfig.DEFAULT); + + if (proxyHost != null) { + requestConfig.setProxy(new HttpHost(proxyHost)); + } + + if (socketTimeout > -1) { + requestConfig.setSocketTimeout((int) socketTimeoutUnits.toMillis(socketTimeout)); + + } + if (establishConnectionTimeout > -1) { + requestConfig.setConnectTimeout((int) establishConnectionTimeoutUnits.toMillis(establishConnectionTimeout)); + } + + Registry cookieSpecs = CookieSpecRegistries.createDefaultBuilder() + .register(CookieSpecs.DEFAULT, new DefaultCookieSpecProvider()).build(); + + if (useSpNego) { + requestConfig.setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.SPNEGO)); + } + + org.apache.http.impl.client.HttpClientBuilder clientBuilder = org.apache.http.impl.client.HttpClientBuilder.create() + .setDefaultSocketConfig(socketConfig.build()) + .setDefaultConnectionConfig(connConfig.build()) + .setDefaultRequestConfig(requestConfig.build()) + .setDefaultCookieSpecRegistry(cookieSpecs) + .setConnectionManager(cm); + + if (spNegoSchemeFactory != null) { + RegistryBuilder authSchemes = RegistryBuilder.create(); + + authSchemes.register(AuthSchemes.SPNEGO, spNegoSchemeFactory); + + clientBuilder.setDefaultAuthSchemeRegistry(authSchemes.build()); + } + + if (useSpNego) { + Credentials fake = new Credentials() { + + @Override + public String getPassword() { + return null; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + }; + + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, fake); + clientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + + if (disableCookieCache) { + clientBuilder.setDefaultCookieStore(new CookieStore() { + @Override + public void addCookie(Cookie cookie) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public List 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 clientBuilder.build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public HttpClient build(AdapterHttpClientConfig adapterConfig) { + disableCookieCache(true); // 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); + } + } + + HttpClientBuilder.HostnameVerificationPolicy policy = HttpClientBuilder.HostnameVerificationPolicy.WILDCARD; + if (adapterConfig.isAllowAnyHostname()) + policy = HttpClientBuilder.HostnameVerificationPolicy.ANY; + connectionPoolSize(adapterConfig.getConnectionPoolSize()); + hostnameVerification(policy); + if (adapterConfig.isDisableTrustManager()) { + disableTrustManager(); + } else { + trustStore(truststore); + } + + configureProxyForAuthServerIfProvided(adapterConfig); + + if (socketTimeout == -1 && adapterConfig.getSocketTimeout() > 0) { + socketTimeout(adapterConfig.getSocketTimeout(), TimeUnit.MILLISECONDS); + } + + if (establishConnectionTimeout == -1 && adapterConfig.getConnectionTimeout() > 0) { + establishConnectionTimeout(adapterConfig.getConnectionTimeout(), TimeUnit.MILLISECONDS); + } + + if (connectionTTL == -1 && adapterConfig.getConnectionTTL() > 0) { + connectionTTL(adapterConfig.getConnectionTTL(), TimeUnit.MILLISECONDS); + } + + return build(); + } + + /** + * Configures a the proxy to use for auth-server requests if provided. + *

+ * 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. + *

+ * + * @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()); + } +} diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java index 20db1a13e572b..abd5fa3db6d43 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java @@ -12,7 +12,7 @@ import jakarta.inject.Singleton; import org.keycloak.AuthorizationContext; -import org.keycloak.adapters.authorization.KeycloakAdapterPolicyEnforcer; +import org.keycloak.adapters.authorization.PolicyEnforcer; import org.keycloak.authorization.client.AuthzClient; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; @@ -59,9 +59,8 @@ public CheckResult apply(RoutingContext routingContext, SecurityIdentity identit VertxHttpFacade httpFacade = new VertxHttpFacade(routingContext, credential.getToken(), resolver.getReadTimeout()); - KeycloakAdapterPolicyEnforcer adapterPolicyEnforcer = new KeycloakAdapterPolicyEnforcer( - resolver.getPolicyEnforcer(identity.getAttribute(TENANT_ID_ATTRIBUTE))); - AuthorizationContext result = adapterPolicyEnforcer.authorize(httpFacade); + PolicyEnforcer policyEnforcer = resolver.getPolicyEnforcer(identity.getAttribute(TENANT_ID_ATTRIBUTE)); + AuthorizationContext result = policyEnforcer.enforce(httpFacade, httpFacade); if (result.isGranted()) { SecurityIdentity newIdentity = enhanceSecurityIdentity(identity, result); @@ -75,7 +74,7 @@ public CheckResult apply(RoutingContext routingContext, SecurityIdentity identit @RequestScoped public AuthzClient getAuthzClient() { SecurityIdentity identity = (SecurityIdentity) Arc.container().instance(SecurityIdentity.class).get(); - return resolver.getPolicyEnforcer(identity.getAttribute(TENANT_ID_ATTRIBUTE)).getClient(); + return resolver.getPolicyEnforcer(identity.getAttribute(TENANT_ID_ATTRIBUTE)).getAuthzClient(); } private SecurityIdentity enhanceSecurityIdentity(SecurityIdentity current, diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java index cb59aa64574b7..7165eb7e39c79 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java @@ -7,7 +7,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.authorization.PolicyEnforcer; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; @@ -103,7 +102,15 @@ private static PolicyEnforcer createPolicyEnforcer(OidcTenantConfig oidcConfig, adapterConfig.setPolicyEnforcerConfig(enforcerConfig); - return new PolicyEnforcer(KeycloakDeploymentBuilder.build(adapterConfig), adapterConfig); + return PolicyEnforcer.builder() + .authServerUrl(adapterConfig.getAuthServerUrl()) + .realm(adapterConfig.getRealm()) + .clientId(adapterConfig.getResource()) + .credentials(adapterConfig.getCredentials()) + .bearerOnly(adapterConfig.isBearerOnly()) + .enforcerConfig(enforcerConfig) + .httpClient(new HttpClientBuilder().build(adapterConfig)) + .build(); } private static Map getCredentials(OidcTenantConfig oidcConfig) { diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java index e0a3a6a28c115..21e8bd8b8b0cf 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java @@ -1,69 +1,115 @@ package io.quarkus.keycloak.pep.runtime; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.InputStream; -import java.io.OutputStream; import java.net.URI; import java.util.List; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.security.cert.X509Certificate; - -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.spi.AuthenticationError; -import org.keycloak.adapters.spi.LogoutError; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.jose.jws.JWSInputException; -import org.keycloak.representations.AccessToken; +import org.keycloak.adapters.authorization.TokenPrincipal; +import org.keycloak.adapters.authorization.spi.HttpRequest; +import org.keycloak.adapters.authorization.spi.HttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.quarkus.vertx.http.runtime.VertxInputStream; -import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.http.impl.CookieImpl; import io.vertx.ext.web.RoutingContext; -public class VertxHttpFacade implements OIDCHttpFacade { +public class VertxHttpFacade implements HttpRequest, HttpResponse { - private final Response response; private final RoutingContext routingContext; - private final Request request; - private final String token; private final long readTimeout; + private final HttpRequest request; + private final HttpResponse response; + private final TokenPrincipal tokenPrincipal; public VertxHttpFacade(RoutingContext routingContext, String token, long readTimeout) { this.routingContext = routingContext; - this.token = token; this.readTimeout = readTimeout; this.request = createRequest(routingContext); this.response = createResponse(routingContext); + tokenPrincipal = new TokenPrincipal() { + @Override + public String getRawToken() { + return token; + } + }; + } + + @Override + public String getRelativePath() { + return request.getRelativePath(); + } + + @Override + public String getMethod() { + return request.getMethod(); + } + + @Override + public String getURI() { + return request.getURI(); + } + + @Override + public List getHeaders(String name) { + return request.getHeaders(name); + } + + @Override + public String getFirstParam(String name) { + return request.getFirstParam(name); } @Override - public Request getRequest() { - return request; + public String getCookieValue(String name) { + return request.getCookieValue(name); } @Override - public Response getResponse() { - return response; + public String getRemoteAddr() { + return request.getRemoteAddr(); } @Override - public X509Certificate[] getCertificateChain() { - try { - return routingContext.request().peerCertificateChain(); - } catch (SSLPeerUnverifiedException e) { - throw new RuntimeException("Failed to fetch certificates from request", e); - } + public boolean isSecure() { + return request.isSecure(); } - private Request createRequest(RoutingContext routingContext) { + @Override + public String getHeader(String name) { + return request.getHeader(name); + } + + @Override + public InputStream getInputStream(boolean buffered) { + return request.getInputStream(buffered); + } + + @Override + public TokenPrincipal getPrincipal() { + return request.getPrincipal(); + } + + @Override + public void sendError(int statusCode) { + response.sendError(statusCode); + } + + @Override + public void sendError(int statusCode, String reason) { + response.sendError(statusCode, reason); + } + + @Override + public void setHeader(String name, String value) { + response.setHeader(name, value); + } + + private HttpRequest createRequest(RoutingContext routingContext) { HttpServerRequest request = routingContext.request(); - return new Request() { + return new HttpRequest() { @Override public String getMethod() { return request.method().name(); @@ -90,19 +136,14 @@ public String getFirstParam(String param) { } @Override - public String getQueryParamValue(String param) { - return request.getParam(param); - } - - @Override - public Cookie getCookie(String cookieName) { - io.vertx.core.http.Cookie c = request.getCookie(cookieName); + public String getCookieValue(String name) { + Cookie cookie = request.getCookie(name); - if (c == null) { + if (cookie == null) { return null; } - return new Cookie(c.getName(), c.getValue(), 1, c.getDomain(), c.getPath()); + return cookie.getValue(); } @Override @@ -115,11 +156,6 @@ public List getHeaders(String name) { return request.headers().getAll(name); } - @Override - public InputStream getInputStream() { - return getInputStream(false); - } - @Override public InputStream getInputStream(boolean buffered) { try { @@ -136,69 +172,27 @@ public InputStream getInputStream(boolean buffered) { } @Override - public String getRemoteAddr() { - return request.remoteAddress().host(); + public TokenPrincipal getPrincipal() { + return tokenPrincipal; } @Override - public void setError(AuthenticationError error) { - // no-op - } - - @Override - public void setError(LogoutError error) { - // no-op + public String getRemoteAddr() { + return request.remoteAddress().host(); } }; } - private Response createResponse(RoutingContext routingContext) { + private HttpResponse createResponse(RoutingContext routingContext) { HttpServerResponse response = routingContext.response(); - return new Response() { - @Override - public void setStatus(int status) { - response.setStatusCode(status); - } - - @Override - public void addHeader(String name, String value) { - response.headers().add(name, value); - } + return new HttpResponse() { @Override public void setHeader(String name, String value) { response.headers().set(name, value); } - @Override - public void resetCookie(String name, String path) { - response.removeCookie(name, true); - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, - boolean httpOnly) { - CookieImpl cookie = new CookieImpl(name, value); - - cookie.setPath(path); - cookie.setDomain(domain); - cookie.setMaxAge(maxAge); - cookie.setSecure(secure); - cookie.setHttpOnly(httpOnly); - - response.addCookie(cookie); - } - - @Override - public OutputStream getOutputStream() { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - - response.headersEndHandler(event -> response.write(Buffer.buffer().appendBytes(os.toByteArray()))); - - return os; - } - @Override public void sendError(int code) { response.setStatusCode(code); @@ -210,20 +204,6 @@ public void sendError(int code, String message) { response.setStatusCode(code); response.setStatusMessage(message); } - - @Override - public void end() { - response.end(); - } }; } - - @Override - public KeycloakSecurityContext getSecurityContext() { - try { - return new KeycloakSecurityContext(token, new JWSInput(token).readJsonContent(AccessToken.class), null, null); - } catch (JWSInputException e) { - throw new RuntimeException("Failed to create access token", e); - } - } } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 02b6e63752e16..f61dad35278e3 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -35,7 +35,7 @@ public class DevServicesConfig { * string. * Set 'quarkus.keycloak.devservices.keycloak-x-image' to override this check. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:21.0.2") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:22.0.0") public String imageName; /**