From 8b0c237d55e70dda5a27b2230f44c20eca400ef1 Mon Sep 17 00:00:00 2001 From: Prarthona Paul Date: Thu, 24 Aug 2023 09:12:53 -0400 Subject: [PATCH] [ELY-2584] Add the ability to specify that the OIDC Authentication Request should include request and request_uri parameters. --- http/oidc/pom.xml | 16 ++ .../security/http/oidc/ElytronMessages.java | 42 ++- .../http/oidc/JWKEncPublicKeyLocator.java | 113 ++++++++ .../oidc/JWTClientCredentialsProvider.java | 46 +--- .../security/http/oidc/JWTSigningUtils.java | 78 ++++++ .../org/wildfly/security/http/oidc/Oidc.java | 24 ++ .../http/oidc/OidcClientConfiguration.java | 135 +++++++++- .../oidc/OidcClientConfigurationBuilder.java | 41 ++- .../security/http/oidc/OidcClientContext.java | 101 +++++++ .../http/oidc/OidcJsonConfiguration.java | 100 ++++++- .../http/oidc/OidcProviderMetadata.java | 33 +++ .../http/oidc/OidcRequestAuthenticator.java | 177 ++++++++++++- .../security/http/oidc/ServerRequest.java | 35 ++- .../http/oidc/KeycloakConfiguration.java | 85 ++++-- .../security/http/oidc/OidcBaseTest.java | 9 +- .../wildfly/security/http/oidc/OidcTest.java | 248 +++++++++++++++++- pom.xml | 7 + 17 files changed, 1202 insertions(+), 88 deletions(-) create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/JWKEncPublicKeyLocator.java create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigningUtils.java diff --git a/http/oidc/pom.xml b/http/oidc/pom.xml index 64a7f7285d2..5f3a6504fda 100644 --- a/http/oidc/pom.xml +++ b/http/oidc/pom.xml @@ -128,6 +128,11 @@ keycloak-admin-client test + + org.keycloak + keycloak-services + test + org.jboss.logmanager jboss-logmanager @@ -173,6 +178,17 @@ jmockit test + + org.wildfly.security + wildfly-elytron-credential-source-impl + test + + + org.wildfly.security + wildfly-elytron-tests-common + test-jar + test + diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java index ac5e2861fc3..e836cc3b468 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ElytronMessages.java @@ -18,10 +18,10 @@ package org.wildfly.security.http.oidc; +import static org.jboss.logging.annotations.Message.NONE; import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.ERROR; import static org.jboss.logging.Logger.Level.WARN; -import static org.jboss.logging.annotations.Message.NONE; import java.io.IOException; @@ -238,5 +238,45 @@ interface ElytronMessages extends BasicLogger { @Message(id = 23057, value = "principal-attribute '%s' claim does not exist, falling back to 'sub'") void principalAttributeClaimDoesNotExist(String principalAttributeClaim); + @Message(id = 23058, value = "Invalid keystore configuration for signing Request Objects.") + IOException invalidKeyStoreConfiguration(); + + @Message(id = 23059, value = "The signature algorithm specified is not supported by the OpenID Provider.") + IOException invalidRequestObjectSignatureAlgorithm(); + + @Message(id = 23060, value = "The encryption algorithm specified is not supported by the OpenID Provider.") + IOException invalidRequestObjectEncryptionAlgorithm(); + + @Message(id = 23061, value = "The content encryption algorithm (enc value) specified is not supported by the OpenID Provider.") + IOException invalidRequestObjectEncryptionEncValue(); + + @LogMessage(level = WARN) + @Message(id = 23062, value = "The OpenID provider does not support request parameters. Sending the request using OAuth2 format.") + void requestParameterNotSupported(); + + @Message(id = 23063, value = "Both request object encryption algorithm and request object content encryption algorithm must be configured to encrypt the request object.") + IllegalArgumentException invalidRequestObjectEncryptionAlgorithmConfiguration(); + + @Message(id = 23064, value = "Failed to create the authentication request using the request parameter.") + RuntimeException unableToCreateRequestWithRequestParameter(@Cause Exception cause); + + @Message(id = 23065, value = "Failed to create the authentication request using the request_uri parameter.") + RuntimeException unableToCreateRequestUriWithRequestParameter(@Cause Exception cause); + + @Message (id = 23066, value = "Failed to send a request to the OpenID provider's Pushed Authorization Request endpoint.") + RuntimeException failedToSendPushedAuthorizationRequest(@Cause Exception cause); + + @Message(id = 23067, value = "Cannot retrieve the request_uri as the pushed authorization request endpoint is not available for the OpenID provider.") + RuntimeException pushedAuthorizationRequestEndpointNotAvailable(); + + @LogMessage(level = WARN) + @Message(id = 23068, value = "The request object will be unsigned. This should not be used in a production environment. To sign the request object, for use in a production environment, please specify the request object signing algorithm.") + void unsignedRequestObjectIsUsed(); + + @Message(id = 23069, value = "The client secret has not been configured. Unable to sign the request object using the client secret.") + RuntimeException clientSecretNotConfigured(); + + @Message(id = 23070, value = "Authentication request format must be one of the following: oauth2, request, request_uri.") + RuntimeException invalidAuthenticationRequestFormat(); } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWKEncPublicKeyLocator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWKEncPublicKeyLocator.java new file mode 100644 index 00000000000..819e5950671 --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWKEncPublicKeyLocator.java @@ -0,0 +1,113 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2024 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 org.wildfly.security.http.oidc; + +import static org.apache.http.HttpHeaders.ACCEPT; +import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.http.oidc.Oidc.JSON_CONTENT_TYPE; + +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Map; +import java.util.List; + +import org.apache.http.client.methods.HttpGet; +import org.wildfly.security.jose.jwk.JWK; +import org.wildfly.security.jose.jwk.JsonWebKeySet; +import org.wildfly.security.jose.jwk.JsonWebKeySetUtil; + +/** + * A public key locator that dynamically obtains the public key used for encryption + * from an OpenID provider by sending a request to the provider's {@code jwks_uri} + * when needed. + * + * @author Prarthona Paul + * */ +class JWKEncPublicKeyLocator implements PublicKeyLocator { + private List currentKeys = new ArrayList<>(); + + private volatile int lastRequestTime = 0; + + @Override + public PublicKey getPublicKey(String kid, OidcClientConfiguration config) { + int minTimeBetweenRequests = config.getMinTimeBetweenJwksRequests(); + int publicKeyCacheTtl = config.getPublicKeyCacheTtl(); + int currentTime = getCurrentTime(); + + PublicKey publicKey = lookupCachedKey(publicKeyCacheTtl, currentTime); + if (publicKey != null) { + return publicKey; + } + + synchronized (this) { + currentTime = getCurrentTime(); + if (currentTime > lastRequestTime + minTimeBetweenRequests) { + sendRequest(config); + lastRequestTime = currentTime; + } else { + log.debug("Won't send request to jwks url. Last request time was " + lastRequestTime); + } + return lookupCachedKey(publicKeyCacheTtl, currentTime); + } + + } + + @Override + public void reset(OidcClientConfiguration config) { + synchronized (this) { + sendRequest(config); + lastRequestTime = getCurrentTime(); + } + } + + private PublicKey lookupCachedKey(int publicKeyCacheTtl, int currentTime) { + if (lastRequestTime + publicKeyCacheTtl > currentTime) { + return currentKeys.get(0); // returns the first cached public key + } else { + return null; + } + } + + private static int getCurrentTime() { + return (int) (System.currentTimeMillis() / 1000); + } + + private void sendRequest(OidcClientConfiguration config) { + if (log.isTraceEnabled()) { + log.trace("Going to send request to retrieve new set of public keys to encrypt a JWT request for client " + config.getResourceName()); + } + + HttpGet request = new HttpGet(config.getJwksUrl()); + request.addHeader(ACCEPT, JSON_CONTENT_TYPE); + try { + JsonWebKeySet jwks = Oidc.sendJsonHttpRequest(config, request, JsonWebKeySet.class); + Map publicKeys = JsonWebKeySetUtil.getKeysForUse(jwks, JWK.Use.ENC); + + if (log.isDebugEnabled()) { + log.debug("Public keys successfully retrieved for client " + config.getResourceName() + ". New kids: " + publicKeys.keySet()); + } + + // update current keys + currentKeys.clear(); + currentKeys.addAll(publicKeys.values()); + } catch (OidcException e) { + log.error("Error when sending request to retrieve public keys", e); + } + } +} diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTClientCredentialsProvider.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTClientCredentialsProvider.java index 4da8d3a5384..13df213373b 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTClientCredentialsProvider.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTClientCredentialsProvider.java @@ -19,18 +19,13 @@ package org.wildfly.security.http.oidc; import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.http.oidc.JWTSigningUtils.loadKeyPairFromKeyStore; import static org.wildfly.security.http.oidc.Oidc.CLIENT_ASSERTION; import static org.wildfly.security.http.oidc.Oidc.CLIENT_ASSERTION_TYPE; import static org.wildfly.security.http.oidc.Oidc.CLIENT_ASSERTION_TYPE_JWT; -import static org.wildfly.security.http.oidc.Oidc.PROTOCOL_CLASSPATH; import static org.wildfly.security.http.oidc.Oidc.asInt; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; import java.security.KeyPair; -import java.security.KeyStore; -import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.util.Map; @@ -155,43 +150,4 @@ protected JwtClaims createRequestToken(String clientId, String tokenUrl) { jwtClaims.setExpirationTime(exp); return jwtClaims; } - - private static KeyPair loadKeyPairFromKeyStore(String keyStoreFile, String storePassword, String keyPassword, String keyAlias, String keyStoreType) { - InputStream stream = findFile(keyStoreFile); - try { - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - keyStore.load(stream, storePassword.toCharArray()); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray()); - if (privateKey == null) { - log.unableToLoadKeyWithAlias(keyAlias); - } - PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey(); - return new KeyPair(publicKey, privateKey); - } catch (Exception e) { - throw log.unableToLoadPrivateKey(e); - } - } - - private static InputStream findFile(String keystoreFile) { - if (keystoreFile.startsWith(PROTOCOL_CLASSPATH)) { - String classPathLocation = keystoreFile.replace(PROTOCOL_CLASSPATH, ""); - // try current class classloader first - InputStream is = JWTClientCredentialsProvider.class.getClassLoader().getResourceAsStream(classPathLocation); - if (is == null) { - is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation); - } - if (is != null) { - return is; - } else { - throw log.unableToFindKeystoreFile(keystoreFile); - } - } else { - try { - // fallback to file - return new FileInputStream(keystoreFile); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - } } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigningUtils.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigningUtils.java new file mode 100644 index 00000000000..03546d8a23f --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigningUtils.java @@ -0,0 +1,78 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2024 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 org.wildfly.security.http.oidc; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; + +import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.http.oidc.Oidc.PROTOCOL_CLASSPATH; + +/** + * A utility class to obtain the KeyPair from a keystore file. + * + * @author Prarthona Paul + */ + +class JWTSigningUtils { + + public static KeyPair loadKeyPairFromKeyStore(String keyStoreFile, String storePassword, String keyPassword, String keyAlias, String keyStoreType) { + InputStream stream = findFile(keyStoreFile); + try { + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(stream, storePassword.toCharArray()); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray()); + if (privateKey == null) { + throw log.unableToLoadKeyWithAlias(keyAlias); + } + PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey(); + return new KeyPair(publicKey, privateKey); + } catch (Exception e) { + throw log.unableToLoadPrivateKey(e); + } + } + + public static InputStream findFile(String keystoreFile) { + if (keystoreFile.startsWith(PROTOCOL_CLASSPATH)) { + String classPathLocation = keystoreFile.replace(PROTOCOL_CLASSPATH, ""); + // try current class classloader first + InputStream is = JWTSigningUtils.class.getClassLoader().getResourceAsStream(classPathLocation); + if (is == null) { + is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation); + } + if (is != null) { + return is; + } else { + throw log.unableToFindKeystoreFile(keystoreFile); + } + } else { + try { + // fallback to file + return new FileInputStream(keystoreFile); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java index f42313b7f58..575809f2f4b 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java @@ -45,6 +45,7 @@ public class Oidc { public static final String ACCEPT = "Accept"; + public static final String AUTHENTICATION_REQUEST_FORMAT = "authentication-request-format"; public static final String OIDC_NAME = "OIDC"; public static final String JSON_CONTENT_TYPE = "application/json"; public static final String HTML_CONTENT_TYPE = "text/html"; @@ -74,6 +75,8 @@ public class Oidc { public static final String PARTIAL = "partial/"; public static final String PASSWORD = "password"; public static final String PROMPT = "prompt"; + public static final String REQUEST = "request"; + public static final String REQUEST_URI = "request_uri"; public static final String SCOPE = "scope"; public static final String UI_LOCALES = "ui_locales"; public static final String USERNAME = "username"; @@ -201,6 +204,27 @@ public enum TokenStore { COOKIE } + public enum AuthenticationRequestFormat { + OAUTH2("oauth2"), + REQUEST("request"), + REQUEST_URI("request_uri"); + + private final String value; + + AuthenticationRequestFormat(String value) { + this.value = value; + } + + /** + * Get the string value for this authentication format. + * + * @return the string value for this authentication format + */ + public String getValue() { + return value; + } + } + public enum ClientCredentialsProviderType { SECRET("secret"), JWT("jwt"), diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java index 3e18fb4eb6d..ca56da28633 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java @@ -30,9 +30,11 @@ import static org.wildfly.security.http.oidc.Oidc.SLASH; import static org.wildfly.security.http.oidc.Oidc.SSLRequired; import static org.wildfly.security.http.oidc.Oidc.TokenStore; +import static org.wildfly.security.jose.util.JsonSerialization.readValue; import java.net.URI; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -41,7 +43,6 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.util.EntityUtils; -import org.wildfly.security.jose.util.JsonSerialization; /** * The OpenID Connect (OIDC) configuration for a client application. This class is based on @@ -81,6 +82,11 @@ public enum RelativeUrlsUsed { protected String jwksUrl; protected String issuerUrl; protected String principalAttribute = "sub"; + protected List requestObjectSigningAlgValuesSupported; + protected List requestObjectEncryptionEncValuesSupported; + protected List requestObjectEncryptionAlgValuesSupported; + protected boolean requestParameterSupported; + protected boolean requestUriParameterSupported; protected String resource; protected String clientId; @@ -126,6 +132,17 @@ public enum RelativeUrlsUsed { protected boolean verifyTokenAudience = false; protected String tokenSignatureAlgorithm = DEFAULT_TOKEN_SIGNATURE_ALGORITHM; + protected String authenticationRequestFormat; + protected String requestObjectSigningAlgorithm; + protected String requestObjectEncryptionAlgValue; + protected String requestObjectEncryptionEncValue; + protected String pushedAuthorizationRequestEndpoint; + protected String requestObjectSigningKeyStoreFile; + protected String requestObjectSigningKeyStorePassword; + protected String requestObjectSigningKeyPassword; + protected String requestObjectSigningKeyAlias; + protected String requestObjectSigningKeyStoreType; + protected JWKEncPublicKeyLocator encryptionPublicKeyLocator; public OidcClientConfiguration() { } @@ -223,6 +240,13 @@ protected void resolveUrls() { tokenUrl = config.getTokenEndpoint(); logoutUrl = config.getLogoutEndpoint(); jwksUrl = config.getJwksUri(); + requestParameterSupported = config.getRequestParameterSupported(); + requestObjectSigningAlgValuesSupported = config.getRequestObjectSigningAlgValuesSupported(); + requestObjectEncryptionEncValuesSupported = config.getRequestObjectEncryptionEncValuesSupported(); + requestObjectEncryptionAlgValuesSupported = config.getRequestObjectEncryptionAlgValuesSupported(); + requestUriParameterSupported = config.getRequestUriParameterSupported(); + pushedAuthorizationRequestEndpoint = config.getPushedAuthorizationRequestEndpoint(); + if (authServerBaseUrl != null) { // keycloak-specific properties accountUrl = getUrl(issuerUrl, ACCOUNT_PATH); @@ -246,7 +270,7 @@ protected OidcProviderMetadata getOidcProviderMetadata(String discoveryUrl) thro EntityUtils.consumeQuietly(response.getEntity()); throw new Exception(response.getStatusLine().getReasonPhrase()); } - return JsonSerialization.readValue(response.getEntity().getContent(), OidcProviderMetadata.class); + return readValue(response.getEntity().getContent(), OidcProviderMetadata.class); } finally { request.releaseConnection(); } @@ -329,6 +353,26 @@ public String getIssuerUrl() { return issuerUrl; } + public List getRequestObjectSigningAlgValuesSupported() { + return requestObjectSigningAlgValuesSupported; + } + + public List getRequestObjectEncryptionAlgValuesSupported() { + return requestObjectEncryptionAlgValuesSupported; + } + + public List getRequestObjectEncryptionEncValuesSupported() { + return requestObjectEncryptionEncValuesSupported; + } + + public boolean getRequestParameterSupported() { + return requestParameterSupported; + } + + public boolean getRequestUriParameterSupported() { + return requestUriParameterSupported; + } + public void setResource(String resource) { this.resource = resource; } @@ -648,4 +692,91 @@ public String getTokenSignatureAlgorithm() { return tokenSignatureAlgorithm; } + public String getAuthenticationRequestFormat() { + return authenticationRequestFormat; + } + + public void setAuthenticationRequestFormat(String authenticationRequestFormat) { + this.authenticationRequestFormat = authenticationRequestFormat; + } + + public String getRequestObjectSigningAlgorithm() { + return requestObjectSigningAlgorithm; + } + + public void setRequestObjectSigningAlgorithm(String requestObjectSigningAlgorithm) { + this.requestObjectSigningAlgorithm = requestObjectSigningAlgorithm; + } + + public String getRequestObjectEncryptionAlgValue() { + return requestObjectEncryptionAlgValue; + } + + public void setRequestObjectEncryptionAlgValue(String requestObjectEncryptionAlgValue) { + this.requestObjectEncryptionAlgValue = requestObjectEncryptionAlgValue; + } + + public String getRequestObjectEncryptionEncValue() { + return requestObjectEncryptionEncValue; + } + + public void setRequestObjectEncryptionEncValue(String requestObjectEncryptionEncValue) { + this.requestObjectEncryptionEncValue = requestObjectEncryptionEncValue; + } + + public String getRequestObjectSigningKeyStoreFile() { + return requestObjectSigningKeyStoreFile; + } + + public void setRequestObjectSigningKeyStoreFile(String keyStoreFile) { + this.requestObjectSigningKeyStoreFile = keyStoreFile; + } + + public String getRequestObjectSigningKeyStorePassword() { + return requestObjectSigningKeyStorePassword; + } + + public void setRequestObjectSigningKeyStorePassword(String requestObjectSigningKeyStorePassword) { + this.requestObjectSigningKeyStorePassword = requestObjectSigningKeyStorePassword; + } + + public String getRequestObjectSigningKeyPassword() { + return requestObjectSigningKeyPassword; + } + + public void setRequestObjectSigningKeyPassword(String requestObjectSigningKeyPassword) { + this.requestObjectSigningKeyPassword = requestObjectSigningKeyPassword; + } + + public String getRequestObjectSigningKeyStoreType() { + return requestObjectSigningKeyStoreType; + } + + public void setRequestObjectSigningKeyStoreType(String requestObjectSigningKeyStoreType) { + this.requestObjectSigningKeyStoreType = requestObjectSigningKeyStoreType; + } + + public String getRequestObjectSigningKeyAlias() { + return requestObjectSigningKeyAlias; + } + + public void setRequestObjectSigningKeyAlias(String requestObjectSigningKeyAlias) { + this.requestObjectSigningKeyAlias = requestObjectSigningKeyAlias; + } + + public String getPushedAuthorizationRequestEndpoint() { + return pushedAuthorizationRequestEndpoint; + } + + public void setPushedAuthorizationRequestEndpoint(String pushedAuthorizationRequestEndpoint) { + this.pushedAuthorizationRequestEndpoint = pushedAuthorizationRequestEndpoint; + } + + public void setEncryptionPublicKeyLocator(JWKEncPublicKeyLocator publicKeySetExtractor) { + this.encryptionPublicKeyLocator = publicKeySetExtractor; + } + + public JWKEncPublicKeyLocator getEncryptionPublicKeyLocator() { + return this.encryptionPublicKeyLocator; + } } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java index f2d757e493c..43bebace9f6 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java @@ -18,7 +18,11 @@ package org.wildfly.security.http.oidc; +import static org.jose4j.jws.AlgorithmIdentifiers.NONE; import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationRequestFormat.OAUTH2; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationRequestFormat.REQUEST; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationRequestFormat.REQUEST_URI; import static org.wildfly.security.http.oidc.Oidc.SSLRequired; import static org.wildfly.security.http.oidc.Oidc.TokenStore; @@ -103,6 +107,41 @@ protected OidcClientConfiguration internalBuild(final OidcJsonConfiguration oidc if (oidcJsonConfiguration.getScope() != null) { oidcClientConfiguration.setScope(oidcJsonConfiguration.getScope()); } + if (oidcJsonConfiguration.getAuthenticationRequestFormat() != null) { + if (!(oidcJsonConfiguration.getAuthenticationRequestFormat().equals(OAUTH2.getValue()) || + oidcJsonConfiguration.getAuthenticationRequestFormat().equals(REQUEST.getValue()) || + oidcJsonConfiguration.getAuthenticationRequestFormat().equals(REQUEST_URI.getValue()))) { + throw log.invalidAuthenticationRequestFormat(); + } + oidcClientConfiguration.setAuthenticationRequestFormat(oidcJsonConfiguration.getAuthenticationRequestFormat()); + } else { + oidcClientConfiguration.setAuthenticationRequestFormat(OAUTH2.getValue()); + } + if (oidcJsonConfiguration.getRequestObjectSigningAlgorithm() != null) { + oidcClientConfiguration.setRequestObjectSigningAlgorithm(oidcJsonConfiguration.getRequestObjectSigningAlgorithm()); + } else { + oidcClientConfiguration.setRequestObjectSigningAlgorithm(NONE); + } + if (oidcJsonConfiguration.getRequestObjectEncryptionAlgValue() != null && oidcJsonConfiguration.getRequestObjectEncryptionEncValue() != null) { //both are required to encrypt the request object + oidcClientConfiguration.setRequestObjectEncryptionAlgValue(oidcJsonConfiguration.getRequestObjectEncryptionAlgValue()); + oidcClientConfiguration.setRequestObjectEncryptionEncValue(oidcJsonConfiguration.getRequestObjectEncryptionEncValue()); + JWKEncPublicKeyLocator encryptionPublicKeyLocator = new JWKEncPublicKeyLocator(); + oidcClientConfiguration.setEncryptionPublicKeyLocator(encryptionPublicKeyLocator); + } else if (oidcJsonConfiguration.getRequestObjectEncryptionAlgValue() != null || oidcJsonConfiguration.getRequestObjectEncryptionEncValue() != null) { //if only one is specified, that is not correct + throw log.invalidRequestObjectEncryptionAlgorithmConfiguration(); + } + if (oidcJsonConfiguration.getRequestObjectSigningKeyStoreFile() != null + && oidcJsonConfiguration.getRequestObjectSigningKeyStorePassword() != null + && oidcJsonConfiguration.getRequestObjectSigningKeyPassword() != null + && oidcJsonConfiguration.getRequestObjectSigningKeyAlias() != null) { + oidcClientConfiguration.setRequestObjectSigningKeyStoreFile(oidcJsonConfiguration.getRequestObjectSigningKeyStoreFile()); + oidcClientConfiguration.setRequestObjectSigningKeyStorePassword(oidcJsonConfiguration.getRequestObjectSigningKeyStorePassword()); + oidcClientConfiguration.setRequestObjectSigningKeyPassword(oidcJsonConfiguration.getRequestObjectSigningKeyPassword()); + oidcClientConfiguration.setRequestObjectSigningKeyAlias(oidcJsonConfiguration.getRequestObjectSigningKeyAlias()); + if (oidcJsonConfiguration.getRequestObjectSigningKeyStoreType() != null) { + oidcClientConfiguration.setRequestObjectSigningKeyStoreType(oidcJsonConfiguration.getRequestObjectSigningKeyStoreType()); + } + } if (oidcJsonConfiguration.getPrincipalAttribute() != null) oidcClientConfiguration.setPrincipalAttribute(oidcJsonConfiguration.getPrincipalAttribute()); oidcClientConfiguration.setResourceCredentials(oidcJsonConfiguration.getCredentials()); @@ -193,8 +232,8 @@ public static OidcJsonConfiguration loadOidcJsonConfiguration(InputStream is) { return adapterConfig; } - public static OidcClientConfiguration build(OidcJsonConfiguration oidcJsonConfiguration) { return new OidcClientConfigurationBuilder().internalBuild(oidcJsonConfiguration); } + } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientContext.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientContext.java index 3c249bb846b..f5d930bd525 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientContext.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientContext.java @@ -525,6 +525,107 @@ public String getTokenSignatureAlgorithm() { public void setTokenSignatureAlgorithm(String tokenSignatureAlgorithm) { delegate.setTokenSignatureAlgorithm(tokenSignatureAlgorithm); } + + @Override + public String getAuthenticationRequestFormat() { + return delegate.getAuthenticationRequestFormat(); + } + + @Override + public void setAuthenticationRequestFormat(String authFormat) { + delegate.setAuthenticationRequestFormat(authFormat); + } + + @Override + public String getRequestObjectSigningAlgorithm() { + return delegate.getRequestObjectSigningAlgorithm(); + } + + @Override + public void setRequestObjectSigningAlgorithm(String requestSignature) { + delegate.setRequestObjectSigningAlgorithm(requestSignature); + } + + @Override + public String getRequestObjectEncryptionAlgValue() { + return delegate.getRequestObjectEncryptionAlgValue(); + } + + @Override + public void setRequestObjectEncryptionAlgValue(String requestObjectEncryptionAlgValue) { + delegate.setRequestObjectEncryptionAlgValue(requestObjectEncryptionAlgValue); + } + + @Override + public String getRequestObjectEncryptionEncValue() { + return delegate.requestObjectEncryptionEncValue; + } + + @Override + public void setRequestObjectEncryptionEncValue (String requestObjectEncryptionEncValue) { + delegate.requestObjectEncryptionEncValue = requestObjectEncryptionEncValue; + } + + @Override + public String getRequestObjectSigningKeyStoreFile() { + return delegate.requestObjectSigningKeyStoreFile; + } + + @Override + public void setRequestObjectSigningKeyStoreFile(String keyStoreFile) { + delegate.requestObjectSigningKeyStoreFile = keyStoreFile; + } + + @Override + public String getRequestObjectSigningKeyStorePassword() { + return delegate.requestObjectSigningKeyStorePassword; + } + + @Override + public void setRequestObjectSigningKeyStorePassword(String requestObjectSigningKeyStorePassword) { + delegate.requestObjectSigningKeyStorePassword = requestObjectSigningKeyStorePassword; + } + + @Override + public String getRequestObjectSigningKeyPassword() { + return delegate.requestObjectSigningKeyPassword; + } + + @Override + public void setRequestObjectSigningKeyPassword(String requestObjectSigningKeyPassword) { + delegate.requestObjectSigningKeyPassword = requestObjectSigningKeyPassword; + } + + @Override + public String getRequestObjectSigningKeyStoreType() { + return delegate.requestObjectSigningKeyStoreType; + } + + @Override + public void setRequestObjectSigningKeyStoreType(String type) { + delegate.requestObjectSigningKeyStoreType = type; + } + + @Override + public String getRequestObjectSigningKeyAlias() { + return delegate.requestObjectSigningKeyAlias; + } + + @Override + public void setRequestObjectSigningKeyAlias(String alias) { + delegate.requestObjectSigningKeyAlias = alias; + } + + @Override + public boolean getRequestParameterSupported() { + return delegate.requestParameterSupported; + } + + @Override + public boolean getRequestUriParameterSupported() { + return delegate.requestUriParameterSupported; + } + } protected String getAuthServerBaseUrl(OidcHttpFacade facade, String base) { diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcJsonConfiguration.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcJsonConfiguration.java index f835cc4fbc2..29d2d785e3c 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcJsonConfiguration.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcJsonConfiguration.java @@ -38,15 +38,18 @@ "resource", "public-client", "credentials", "use-resource-role-mappings", "use-realm-role-mappings", "enable-cors", "cors-max-age", "cors-allowed-methods", "cors-exposed-headers", - "expose-token", "bearer-only", "autodetect-bearer-only", - "connection-pool-size", + "expose-token", "bearer-only", "autodetect-bearer-only", "connection-pool-size", "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "client-keystore", "client-keystore-password", "client-key-password", "always-refresh-token", "register-node-at-startup", "register-node-period", "token-store", "adapter-state-cookie-path", "principal-attribute", "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests", "public-key-cache-ttl", - "ignore-oauth-query-parameter", "verify-token-audience", "token-signature-algorithm", "scope" + "ignore-oauth-query-parameter", "verify-token-audience", "token-signature-algorithm", "scope", + "authentication-request-format", "request-object-signing-algorithm", "request-object-encryption-alg-value", + "request-object-encryption-enc-value", "request-object-signing-keystore-file", + "request-object-signing-keystore-password","request-object-signing-key-password", "request-object-signing-key-alias", + "request-object-signing-keystore-type" }) public class OidcJsonConfiguration { @@ -64,6 +67,16 @@ public class OidcJsonConfiguration { protected String clientKeystorePassword; @JsonProperty("client-key-password") protected String clientKeyPassword; + @JsonProperty("request-object-signing-keystore-file") + protected String requestObjectSigningKeyStoreFile; + @JsonProperty("request-object-signing-keystore-password") + protected String requestObjectSigningKeyStorePassword; + @JsonProperty("request-object-signing-key-password") + protected String requestObjectSigningKeyPassword; + @JsonProperty("request-object-signing-key-alias") + protected String requestObjectSigningKeyAlias; + @JsonProperty("request-object-signing-keystore-type") + protected String requestObjectSigningKeyStoreType; @JsonProperty("connection-pool-size") protected int connectionPoolSize = 20; @JsonProperty("always-refresh-token") @@ -142,6 +155,17 @@ public class OidcJsonConfiguration { @JsonProperty("scope") protected String scope; + @JsonProperty("authentication-request-format") + protected String authenticationRequestFormat; + + @JsonProperty("request-object-signing-algorithm") + protected String requestObjectSigningAlgorithm; + + @JsonProperty("request-object-encryption-alg-value") + protected String requestObjectEncryptionAlgValue; + + @JsonProperty("request-object-encryption-enc-value") + protected String requestObjectEncryptionEncValue; /** * The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}. @@ -181,6 +205,13 @@ public void setTruststorePassword(String truststorePassword) { this.truststorePassword = truststorePassword; } + public String getRequestObjectSigningKeyStoreFile() { + return requestObjectSigningKeyStoreFile; + } + + public void setRequestObjectSigningKeyStoreFile(String requestObjectSigningKeyStoreFile) { + this.requestObjectSigningKeyStoreFile = requestObjectSigningKeyStoreFile; + } public String getClientKeystore() { return clientKeystore; } @@ -189,6 +220,22 @@ public void setClientKeystore(String clientKeystore) { this.clientKeystore = clientKeystore; } + public String getRequestObjectSigningKeyStoreType() { + return requestObjectSigningKeyStoreType; + } + + public void setRequestObjectSigningKeyStoreType(String requestObjectSigningKeyStoreType) { + this.requestObjectSigningKeyStoreType = requestObjectSigningKeyStoreType; + } + + public String getRequestObjectSigningKeyAlias() { + return requestObjectSigningKeyAlias; + } + + public void setRequestObjectSigningKeyAlias(String requestObjectSigningKeyAlias) { + this.requestObjectSigningKeyAlias = requestObjectSigningKeyAlias; + } + public String getClientKeystorePassword() { return clientKeystorePassword; } @@ -201,10 +248,26 @@ public String getClientKeyPassword() { return clientKeyPassword; } + public String getRequestObjectSigningKeyPassword() { + return requestObjectSigningKeyPassword; + } + + public String getRequestObjectSigningKeyStorePassword() { + return requestObjectSigningKeyStorePassword; + } + public void setClientKeyPassword(String clientKeyPassword) { this.clientKeyPassword = clientKeyPassword; } + public void setRequestObjectSigningKeyStorePassword(String requestObjectSigningKeyStorePassword) { + this.requestObjectSigningKeyStorePassword = requestObjectSigningKeyStorePassword; + } + + public void setRequestObjectSigningKeyPassword(String requestObjectSigningKeyPassword) { + this.requestObjectSigningKeyPassword = requestObjectSigningKeyPassword; + } + public int getConnectionPoolSize() { return connectionPoolSize; } @@ -521,5 +584,36 @@ public String getScope() { public void setScope(String scope) { this.scope = scope; } + public String getAuthenticationRequestFormat() { + return authenticationRequestFormat; + } + + public void setAuthenticationRequestFormat(String authenticationRequestFormat) { + this.authenticationRequestFormat = authenticationRequestFormat; + } + + public String getRequestObjectSigningAlgorithm() { + return requestObjectSigningAlgorithm; + } + + public void setRequestObjectSigningAlgorithm(String requestObjectSigningAlgorithm) { + this.requestObjectSigningAlgorithm = requestObjectSigningAlgorithm; + } + + public String getRequestObjectEncryptionAlgValue() { + return requestObjectEncryptionAlgValue; + } + + public void setRequestObjectEncryptionAlgValue(String requestObjectEncryptionAlgValue) { + this.requestObjectEncryptionAlgValue = requestObjectEncryptionAlgValue; + } + + public String getRequestObjectEncryptionEncValue() { + return requestObjectEncryptionEncValue; + } + + public void setRequestObjectEncryptionEncValue (String requestObjectEncryptionEncValue) { + this.requestObjectEncryptionEncValue = requestObjectEncryptionEncValue; + } } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcProviderMetadata.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcProviderMetadata.java index 9984de7c023..6c964dbfe1b 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcProviderMetadata.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcProviderMetadata.java @@ -114,6 +114,9 @@ public class OidcProviderMetadata { @JsonProperty("request_uri_parameter_supported") private Boolean requestUriParameterSupported; + @JsonProperty("pushed_authorization_request_endpoint") + private String pushedAuthorizationRequestEndpoint; + @JsonProperty("revocation_endpoint") private String revocationEndpoint; @@ -142,6 +145,12 @@ public class OidcProviderMetadata { @JsonProperty("tls_client_certificate_bound_access_tokens") private Boolean tlsClientCertificateBoundAccessTokens; + @JsonProperty("request_object_encryption_enc_values_supported") + private List requestObjectEncryptionEncValuesSupported; + + @JsonProperty("request_object_encryption_alg_values_supported") + private List requestObjectEncryptionAlgValuesSupported; + protected Map otherClaims = new HashMap(); public String getIssuer() { @@ -411,6 +420,30 @@ public Boolean getTlsClientCertificateBoundAccessTokens() { return tlsClientCertificateBoundAccessTokens; } + public List getRequestObjectEncryptionAlgValuesSupported() { + return requestObjectEncryptionAlgValuesSupported; + } + + public void setRequestObjectEncryptionAlgValuesSupported(List requestObjectEncryptionAlgValuesSupported) { + this.requestObjectEncryptionAlgValuesSupported = requestObjectEncryptionAlgValuesSupported; + } + + public List getRequestObjectEncryptionEncValuesSupported() { + return requestObjectEncryptionEncValuesSupported; + } + + public void setRequestObjectEncryptionEncValuesSupported(List requestObjectEncryptionEncValuesSupported) { + this.requestObjectEncryptionEncValuesSupported = requestObjectEncryptionEncValuesSupported; + } + + public String getPushedAuthorizationRequestEndpoint() { + return pushedAuthorizationRequestEndpoint; + } + + public void setPushedAuthorizationRequestEndpoint(String url) { + this.pushedAuthorizationRequestEndpoint = url; + } + @JsonAnyGetter public Map getOtherClaims() { return otherClaims; diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java index bf67e938598..5ef5c26122e 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java @@ -18,6 +18,10 @@ package org.wildfly.security.http.oidc; +import static org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256; +import static org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA384; +import static org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA512; +import static org.jose4j.jws.AlgorithmIdentifiers.NONE; import static org.wildfly.security.http.oidc.ElytronMessages.log; import static org.wildfly.security.http.oidc.Oidc.ALLOW_QUERY_PARAMS_PROPERTY_NAME; import static org.wildfly.security.http.oidc.Oidc.CLIENT_ID; @@ -32,13 +36,17 @@ import static org.wildfly.security.http.oidc.Oidc.PROMPT; import static org.wildfly.security.http.oidc.Oidc.REDIRECT_URI; import static org.wildfly.security.http.oidc.Oidc.RESPONSE_TYPE; +import static org.wildfly.security.http.oidc.Oidc.REQUEST; +import static org.wildfly.security.http.oidc.Oidc.REQUEST_URI; import static org.wildfly.security.http.oidc.Oidc.SCOPE; import static org.wildfly.security.http.oidc.Oidc.SESSION_STATE; import static org.wildfly.security.http.oidc.Oidc.STATE; import static org.wildfly.security.http.oidc.Oidc.UI_LOCALES; +import static org.wildfly.security.http.oidc.Oidc.ClientCredentialsProviderType.SECRET; + +import static org.wildfly.security.http.oidc.Oidc.logToken; import static org.wildfly.security.http.oidc.Oidc.generateId; import static org.wildfly.security.http.oidc.Oidc.getQueryParamValue; -import static org.wildfly.security.http.oidc.Oidc.logToken; import static org.wildfly.security.http.oidc.Oidc.stripQueryParam; import java.io.IOException; @@ -47,6 +55,10 @@ import java.net.URL; import java.security.AccessController; import java.security.PrivilegedAction; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyPair; +import java.security.PublicKey; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -54,10 +66,16 @@ import java.util.Map; import java.util.Set; -import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; +import org.apache.http.HttpStatus; import org.apache.http.client.utils.URIBuilder; import org.apache.http.message.BasicNameValuePair; +import org.jose4j.jwa.AlgorithmConstraints; +import org.jose4j.jwe.JsonWebEncryption; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.keys.HmacKey; +import org.jose4j.lang.JoseException; import org.wildfly.security.http.HttpConstants; /** @@ -201,18 +219,73 @@ protected String getRedirectUri(String state) { return null; } - URIBuilder redirectUriBuilder = new URIBuilder(deployment.getAuthUrl()) - .addParameter(RESPONSE_TYPE, CODE) - .addParameter(CLIENT_ID, deployment.getResourceName()) - .addParameter(REDIRECT_URI, rewrittenRedirectUri(url)) - .addParameter(STATE, state); - redirectUriBuilder.addParameters(forwardedQueryParams); + String redirectUri = rewrittenRedirectUri(url); + URIBuilder redirectUriBuilder = new URIBuilder(deployment.getAuthUrl()); + redirectUriBuilder.addParameter(RESPONSE_TYPE, CODE) + .addParameter(CLIENT_ID, deployment.getResourceName()); + + switch (deployment.getAuthenticationRequestFormat()) { + case REQUEST: + if (deployment.getRequestParameterSupported()) { + // add request objects into request parameter + try { + createRequestWithRequestParameter(REQUEST, redirectUriBuilder, redirectUri, state, forwardedQueryParams); + } catch (IOException | JoseException e) { + throw log.unableToCreateRequestWithRequestParameter(e); + } + } else { + // send request as usual + createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + log.requestParameterNotSupported(); + } + break; + case REQUEST_URI: + if (deployment.getRequestUriParameterSupported()) { + try { + createRequestWithRequestParameter(REQUEST_URI, redirectUriBuilder, redirectUri, state, forwardedQueryParams); + } catch (IOException | JoseException e) { + throw log.unableToCreateRequestUriWithRequestParameter(e); + } + } else { + // send request as usual + createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + log.requestParameterNotSupported(); + } + break; + default: + createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + break; + } return redirectUriBuilder.build().toString(); } catch (URISyntaxException e) { throw log.unableToCreateRedirectResponse(e); } } + protected URIBuilder createOAuthRequest(URIBuilder redirectUriBuilder, String redirectUri, String state, List forwardedQueryParams) { + redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri) + .addParameter(STATE, state) + .addParameters(forwardedQueryParams); + return redirectUriBuilder; + } + + protected URIBuilder createRequestWithRequestParameter(String requestFormat, URIBuilder redirectUriBuilder, String redirectUri, String state, List forwardedQueryParams) throws JoseException, IOException { + String request = convertToRequestParameter(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + + switch (requestFormat) { + case REQUEST: + redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri) + .addParameter(REQUEST, request); + break; + case REQUEST_URI: + String request_uri = ServerRequest.getRequestUri(request, deployment); + redirectUriBuilder.addParameter("request_uri", request_uri) + .addParameter(REDIRECT_URI, redirectUri); + break; + } + return redirectUriBuilder; + } + protected int getSSLRedirectPort() { return sslRedirectPort; } @@ -461,4 +534,92 @@ private void addScopes(String scopes, Set allScopes) { allScopes.addAll(Arrays.asList(scopes.split("\\s+"))); } } + + private String convertToRequestParameter(URIBuilder redirectUriBuilder, String redirectUri, String state, List forwardedQueryParams) throws JoseException, IOException { + redirectUriBuilder.addParameter(SCOPE, OIDC_SCOPE); + + JwtClaims jwtClaims = new JwtClaims(); + jwtClaims.setIssuer(deployment.getResourceName()); + jwtClaims.setAudience(deployment.getIssuerUrl()); + + for ( NameValuePair parameter: forwardedQueryParams) { + jwtClaims.setClaim(parameter.getName(), parameter.getValue()); + } + jwtClaims.setClaim(STATE, state); + jwtClaims.setClaim(REDIRECT_URI, redirectUri); + jwtClaims.setClaim(RESPONSE_TYPE, CODE); + jwtClaims.setClaim(CLIENT_ID, deployment.getResourceName()); + + // sign JWT first before encrypting + JsonWebSignature signedRequest = signRequest(jwtClaims, deployment); + + // Encrypting optional + if (deployment.getRequestObjectEncryptionAlgValue() != null && !deployment.getRequestObjectEncryptionAlgValue().isEmpty() && + deployment.getRequestObjectEncryptionEncValue() != null && !deployment.getRequestObjectEncryptionEncValue().isEmpty()) { + return encryptRequest(signedRequest).getCompactSerialization(); + } else { + return signedRequest.getCompactSerialization(); + } + } + + private static KeyPair getkeyPair(OidcClientConfiguration deployment) throws IOException { + if (!deployment.getRequestObjectSigningAlgorithm().equals(NONE) && deployment.getRequestObjectSigningKeyStoreFile() == null){ + throw log.invalidKeyStoreConfiguration(); + } else { + return JWTSigningUtils.loadKeyPairFromKeyStore(deployment.getRequestObjectSigningKeyStoreFile(), + deployment.getRequestObjectSigningKeyStorePassword(), deployment.getRequestObjectSigningKeyPassword(), + deployment.getRequestObjectSigningKeyAlias(), deployment.getRequestObjectSigningKeyStoreType()); + } + } + + private static JsonWebSignature signRequest(JwtClaims jwtClaims, OidcClientConfiguration deployment) throws IOException, JoseException { + JsonWebSignature jsonWebSignature = new JsonWebSignature(); + jsonWebSignature.setPayload(jwtClaims.toJson()); + + if (!deployment.getRequestObjectSigningAlgValuesSupported().contains(deployment.getRequestObjectSigningAlgorithm())) { + throw log.invalidRequestObjectSignatureAlgorithm(); + } else { + if (deployment.getRequestObjectSigningAlgorithm().equals(NONE)) { //unsigned + jsonWebSignature.setAlgorithmConstraints(AlgorithmConstraints.NO_CONSTRAINTS); + jsonWebSignature.setAlgorithmHeaderValue(NONE); + } else if (deployment.getRequestObjectSigningAlgorithm().equals(HMAC_SHA256) + || deployment.getRequestObjectSigningAlgorithm().equals(HMAC_SHA384) + || deployment.getRequestObjectSigningAlgorithm().equals(HMAC_SHA512)) { //signed with symmetric key + jsonWebSignature.setAlgorithmHeaderValue(deployment.getRequestObjectSigningAlgorithm()); + String secretKey = (String) deployment.getResourceCredentials().get(SECRET.getValue()); + if (secretKey == null) { + throw log.clientSecretNotConfigured(); + } else { + Key key = new HmacKey(secretKey.getBytes(StandardCharsets.UTF_8)); //the client secret is a shared secret between the server and the client + jsonWebSignature.setKey(key); + } + } else { //signed with asymmetric key + KeyPair keyPair = getkeyPair(deployment); + jsonWebSignature.setKey(keyPair.getPrivate()); + jsonWebSignature.setAlgorithmHeaderValue(deployment.getRequestObjectSigningAlgorithm()); + } + if (!deployment.getRequestObjectSigningAlgorithm().equals(NONE)) + jsonWebSignature.sign(); + else + log.unsignedRequestObjectIsUsed(); + return jsonWebSignature; + } + } + + private JsonWebEncryption encryptRequest(JsonWebSignature signedRequest) throws JoseException, IOException { + if (!deployment.getRequestObjectEncryptionAlgValuesSupported().contains(deployment.getRequestObjectEncryptionAlgValue())) { + throw log.invalidRequestObjectEncryptionAlgorithm(); + } else if (!deployment.getRequestObjectEncryptionEncValuesSupported().contains(deployment.getRequestObjectEncryptionEncValue())) { + throw log.invalidRequestObjectEncryptionEncValue(); + } else { + JsonWebEncryption jsonEncryption = new JsonWebEncryption(); + jsonEncryption.setPayload(signedRequest.getCompactSerialization()); + jsonEncryption.setAlgorithmConstraints(new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, deployment.getRequestObjectEncryptionAlgValue(), deployment.getRequestObjectEncryptionEncValue())); + jsonEncryption.setAlgorithmHeaderValue(deployment.getRequestObjectEncryptionAlgValue()); + jsonEncryption.setEncryptionMethodHeaderParameter(deployment.getRequestObjectEncryptionEncValue()); + PublicKey encPublicKey = deployment.getEncryptionPublicKeyLocator().getPublicKey(null, deployment); + jsonEncryption.setKey(encPublicKey); + return jsonEncryption; + } + } } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java index ad50d715c56..3a203541ee4 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java @@ -25,13 +25,14 @@ import static org.wildfly.security.http.oidc.Oidc.KEYCLOAK_CLIENT_CLUSTER_HOST; import static org.wildfly.security.http.oidc.Oidc.PASSWORD; import static org.wildfly.security.http.oidc.Oidc.REDIRECT_URI; +import static org.wildfly.security.http.oidc.Oidc.REQUEST; import static org.wildfly.security.http.oidc.Oidc.USERNAME; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -46,6 +47,8 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.consumer.InvalidJwtException; import org.wildfly.security.jose.util.JsonSerialization; /** @@ -274,4 +277,34 @@ public static AccessAndIDTokenResponse getBearerToken(OidcClientConfiguration oi } return tokenResponse; } + + public static String getRequestUri(String request, OidcClientConfiguration deployment) throws OidcException { + if (deployment.getPushedAuthorizationRequestEndpoint() == null) { + throw log.pushedAuthorizationRequestEndpointNotAvailable(); + } + HttpPost parRequest = new HttpPost(deployment.getPushedAuthorizationRequestEndpoint()); + List formParams = new ArrayList(); + formParams.add(new BasicNameValuePair(REQUEST, request)); + ClientCredentialsProviderUtils.setClientCredentials(deployment, parRequest, formParams); + + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8); + parRequest.setEntity(form); + + HttpResponse response; + try { + response = deployment.getClient().execute(parRequest); + } catch (Exception e) { + throw log.failedToSendPushedAuthorizationRequest(e); + } + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_CREATED) { + EntityUtils.consumeQuietly(response.getEntity()); + throw log.unexpectedResponseCodeFromOidcProvider(response.getStatusLine().getStatusCode()); + } + try (InputStream inputStream = response.getEntity().getContent()) { + JwtClaims jwt = JwtClaims.parse(readString(inputStream, StandardCharsets.UTF_8)); + return jwt.getClaimValueAsString("request_uri"); + } catch (IOException | InvalidJwtException e) { + throw log.failedToDecodeRequestUri(e); + } + } } diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java index 4bb5e2b33b0..8ebf4051bf2 100644 --- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java @@ -20,12 +20,23 @@ import static org.wildfly.security.http.oidc.OidcBaseTest.TENANT1_REALM; import static org.wildfly.security.http.oidc.OidcBaseTest.TENANT2_REALM; +import static org.wildfly.security.http.oidc.Oidc.OIDC_SCOPE; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Objects; +import javax.security.auth.x500.X500Principal; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -33,10 +44,9 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; - +import org.wildfly.security.ssl.test.util.CAGenerationTool; import io.restassured.RestAssured; -import static org.wildfly.security.http.oidc.Oidc.OIDC_SCOPE; /** * Keycloak configuration for testing. @@ -53,6 +63,24 @@ public class KeycloakConfiguration { private static final String BOB_PASSWORD = "bob123+"; public static final String ALLOWED_ORIGIN = "http://somehost"; public static final boolean EMAIL_VERIFIED = false; + public static final String RSA_KEYSTORE_FILE_NAME = "jwt.keystore"; + public static final String EC_KEYSTORE_FILE_NAME = "jwtEC.keystore"; + public static final String KEYSTORE_ALIAS = "jwtKeystore"; + public static final String KEYSTORE_PASS = "Elytron"; + public static final String PKCS12_KEYSTORE_TYPE = "PKCS12"; + public static String KEYSTORE_CLASSPATH; + + /* Accepted Request Object Encrypting Algorithms for KeyCloak*/ + public static final String RSA_OAEP = "RSA-OAEP"; + public static final String RSA_OAEP_256 = "RSA-OAEP-256"; + public static final String RSA1_5 = "RSA1_5"; + + /* Accepted Request Object Encryption Methods for KeyCloak*/ + public static final String A128CBC_HS256 = "A128CBC-HS256"; + public static final String A192CBC_HS384 = "A192CBC-HS384"; + public static final String A256CBC_HS512 = "A256CBC-HS512"; + public static CAGenerationTool caGenerationTool = null; + public X509Certificate caCertificate = null; // the users below are for multi-tenancy tests specifically public static final String TENANT1_USER = "tenant1_user"; @@ -76,20 +104,20 @@ public class KeycloakConfiguration { * */ public static RealmRepresentation getRealmRepresentation(final String realmName, String clientId, String clientSecret, - String clientHostName, int clientPort, String clientApp, boolean configureClientScopes) { + String clientHostName, int clientPort, String clientApp, boolean configureClientScopes) throws Exception { return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp, configureClientScopes); } public static RealmRepresentation getRealmRepresentation(final String realmName, String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, int accessTokenLifespan, - int ssoSessionMaxLifespan, boolean configureClientScopes, boolean multiTenancyApp) { + int ssoSessionMaxLifespan, boolean configureClientScopes, boolean multiTenancyApp) throws Exception { return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp, accessTokenLifespan, ssoSessionMaxLifespan, configureClientScopes, multiTenancyApp); } public static RealmRepresentation getRealmRepresentation(final String realmName, String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, boolean directAccessGrantEnabled, String bearerOnlyClientId, - String corsClientId) { + String corsClientId) throws Exception { return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, bearerOnlyClientId, corsClientId); } @@ -126,25 +154,25 @@ public static String getAccessToken(String authServerUrl, String realmName, Stri private static RealmRepresentation createRealm(final String realmName, String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, boolean directAccessGrantEnabled, String bearerOnlyClientId, - String corsClientId) { + String corsClientId) throws Exception { return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, bearerOnlyClientId, corsClientId, false); } private static RealmRepresentation createRealm(String name, String clientId, String clientSecret, - String clientHostName, int clientPort, String clientApp, boolean configureClientScopes) { + String clientHostName, int clientPort, String clientApp, boolean configureClientScopes) throws Exception { return createRealm(name, clientId, clientSecret, clientHostName, clientPort, clientApp, false, null, null, configureClientScopes); } private static RealmRepresentation createRealm(String name, String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, int accessTokenLifeSpan, int ssoSessionMaxLifespan, - boolean configureClientScopes, boolean multiTenancyApp) { + boolean configureClientScopes, boolean multiTenancyApp) throws Exception { return createRealm(name, clientId, clientSecret, clientHostName, clientPort, clientApp, false, null, null, accessTokenLifeSpan, ssoSessionMaxLifespan, configureClientScopes, multiTenancyApp); } private static RealmRepresentation createRealm(String name, String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, boolean directAccessGrantEnabled, String bearerOnlyClientId, - String corsClientId, boolean configureClientScopes) { + String corsClientId, boolean configureClientScopes) throws Exception { return createRealm(name, clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, bearerOnlyClientId, corsClientId, 3, 3, configureClientScopes, false); } @@ -152,7 +180,7 @@ private static RealmRepresentation createRealm(String name, String clientId, Str String clientHostName, int clientPort, String clientApp, boolean directAccessGrantEnabled, String bearerOnlyClientId, String corsClientId, int accessTokenLifespan, int ssoSessionMaxLifespan, - boolean configureClientScopes, boolean multiTenancyApp) { + boolean configureClientScopes, boolean multiTenancyApp) throws Exception { RealmRepresentation realm = new RealmRepresentation(); realm.setRealm(name); realm.setEnabled(true); @@ -201,17 +229,12 @@ private static RealmRepresentation createRealm(String name, String clientId, Str } private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, - boolean directAccessGrantEnabled, boolean multiTenancyApp) { + boolean directAccessGrantEnabled, boolean multiTenancyApp) throws Exception { return createWebAppClient(clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, null, multiTenancyApp); } private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, - String clientApp, boolean directAccessGrantEnabled, String allowedOrigin) { - return createWebAppClient(clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, allowedOrigin, false); - } - - private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, - String clientApp, boolean directAccessGrantEnabled, String allowedOrigin, boolean multiTenancyApp) { + String clientApp, boolean directAccessGrantEnabled, String allowedOrigin, boolean multiTenancyApp) throws Exception { ClientRepresentation client = new ClientRepresentation(); client.setClientId(clientId); client.setPublicClient(false); @@ -224,9 +247,29 @@ private static ClientRepresentation createWebAppClient(String clientId, String c } client.setEnabled(true); client.setDirectAccessGrantsEnabled(directAccessGrantEnabled); + if (allowedOrigin != null) { client.setWebOrigins(Collections.singletonList(allowedOrigin)); } + + OIDCAdvancedConfigWrapper oidcAdvancedConfigWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client); + oidcAdvancedConfigWrapper.setUseJwksUrl(false); + KEYSTORE_CLASSPATH = Objects.requireNonNull(KeycloakConfiguration.class.getClassLoader().getResource("")).getPath(); + File ksFile = new File(KEYSTORE_CLASSPATH + RSA_KEYSTORE_FILE_NAME); + if (ksFile.exists()) { + InputStream stream = findFile(KEYSTORE_CLASSPATH + RSA_KEYSTORE_FILE_NAME); + KeyStore keyStore = KeyStore.getInstance(PKCS12_KEYSTORE_TYPE); + keyStore.load(stream, KEYSTORE_PASS.toCharArray()); + client.getAttributes().put("jwt.credential.certificate", Base64.getEncoder().encodeToString(keyStore.getCertificate(KEYSTORE_ALIAS).getEncoded())); + } else { + caGenerationTool = CAGenerationTool.builder() + .setBaseDir(KEYSTORE_CLASSPATH) + .setRequestIdentities(CAGenerationTool.Identity.values()) // Create all identities. + .build(); + X500Principal principal = new X500Principal("OU=Elytron, O=Elytron, C=UK, ST=Elytron, CN=OcspResponder"); + X509Certificate rsaCert = caGenerationTool.createIdentity(KEYSTORE_ALIAS, principal, RSA_KEYSTORE_FILE_NAME, CAGenerationTool.Identity.CA); + client.getAttributes().put("jwt.credential.certificate", Base64.getEncoder().encodeToString(rsaCert.getEncoded())); + } return client; } @@ -257,4 +300,12 @@ private static UserRepresentation createUser(String username, String password, L return user; } + private static InputStream findFile(String keystoreFile) { + try { + return new FileInputStream(keystoreFile); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + } \ No newline at end of file diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java index de3115d96b0..b604af8a8f8 100644 --- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java @@ -81,7 +81,7 @@ public class OidcBaseTest extends AbstractBaseHttpTest { public static final String CLIENT_ID = "test-webapp"; - public static final String CLIENT_SECRET = "secret"; + public static final String CLIENT_SECRET = "longerclientsecretthatisstleast256bitslong"; public static KeycloakContainer KEYCLOAK_CONTAINER; public static final String TEST_REALM = "WildFly"; public static final String TEST_REALM_WITH_SCOPES = "WildFlyScopes"; @@ -100,6 +100,13 @@ public class OidcBaseTest extends AbstractBaseHttpTest { public static final String TENANT2_ENDPOINT = "tenant2"; protected HttpServerAuthenticationMechanismFactory oidcFactory; + public enum RequestObjectErrorType { + INVALID_ALGORITHM, + MISSING_CLIENT_SECRET, + INVALID_REQUEST_FORMAT, + MISSING_ENC_VALUE + } + @AfterClass public static void generalCleanup() throws Exception { if (KEYCLOAK_CONTAINER != null) { diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java index b7e1ce6ec6a..fdda1aac447 100644 --- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java @@ -18,6 +18,20 @@ package org.wildfly.security.http.oidc; +import static org.jose4j.jws.AlgorithmIdentifiers.NONE; +import static org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA256; +import static org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA512; +import static org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256; +import static org.jose4j.jws.AlgorithmIdentifiers.RSA_PSS_USING_SHA256; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.KEYSTORE_CLASSPATH; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.KEYSTORE_PASS; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.PKCS12_KEYSTORE_TYPE; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.RSA1_5; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.RSA_OAEP; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.RSA_OAEP_256; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.A128CBC_HS256; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.A192CBC_HS384; +import static org.wildfly.security.http.oidc.KeycloakConfiguration.A256CBC_HS512; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -32,6 +46,9 @@ import static org.wildfly.security.http.oidc.KeycloakConfiguration.TENANT2_USER; import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME; import static org.wildfly.security.http.oidc.Oidc.OIDC_SCOPE; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationRequestFormat.OAUTH2; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationRequestFormat.REQUEST; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationRequestFormat.REQUEST_URI; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -42,19 +59,18 @@ import javax.security.auth.callback.CallbackHandler; -import org.apache.http.HttpStatus; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.wildfly.security.http.HttpServerAuthenticationMechanism; - +import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.TextPage; import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.restassured.RestAssured; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.QueueDispatcher; +import org.apache.http.HttpStatus; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; /** * Tests for the OpenID Connect authentication mechanism. @@ -237,6 +253,100 @@ public void testOpenIDWithMultipleScopeValue() throws Exception { true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, expectedScope, false); } + // Note: The tests will fail if `localhost` is not listed first in `/etc/hosts` file for the loopback addresses (IPv4 and IPv6). + @Test + public void testSuccessfulOauth2Request() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(OAUTH2.getValue(), "", "", ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulPlaintextRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), NONE, "", ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulPlaintextEncryptedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), NONE, RSA_OAEP, A128CBC_HS256), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulRsaSignedAndEncryptedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), RSA_USING_SHA512, RSA_OAEP, A192CBC_HS384, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, PKCS12_KEYSTORE_TYPE), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulPsSignedAndRsaEncryptedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), RSA_PSS_USING_SHA256, RSA_OAEP_256, A256CBC_HS512, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, PKCS12_KEYSTORE_TYPE), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testInvalidSigningAlgorithm() throws Exception { + //ES256K is a valid signature algorithm, but not one of the ones supported by keycloak + testRequestObjectInvalidConfiguration(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), "ES256K", RSA_OAEP_256, A256CBC_HS512, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, PKCS12_KEYSTORE_TYPE), RequestObjectErrorType.INVALID_ALGORITHM); + } + + @Test + public void testSuccessfulRsaSignedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), RSA_USING_SHA256, "", "", KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, PKCS12_KEYSTORE_TYPE), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulPsSignedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), RSA_PSS_USING_SHA256, "", "", KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, PKCS12_KEYSTORE_TYPE), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + @Test + public void testInvalidRequestEncryptionAlgorithm() throws Exception { + // None is not a valid algorithm for encrypting jwt's and RSA-OAEP is not a valid algorithm for signing + testRequestObjectInvalidConfiguration(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), RSA1_5, NONE, NONE, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, PKCS12_KEYSTORE_TYPE), RequestObjectErrorType.INVALID_ALGORITHM); + } + + @Test + public void testSuccessfulPlaintextRequestUri() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_URI.getValue(), NONE, "", ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulHmacSignedRequestUri() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), HMAC_SHA256, "", ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulHmacSignedAndEncryptedRequestUri() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST.getValue(), HMAC_SHA256, RSA_OAEP, A128CBC_HS256), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulSignedAndEncryptedRequestUri() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_URI.getValue(), RSA_USING_SHA256, RSA_OAEP_256, A256CBC_HS512, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, PKCS12_KEYSTORE_TYPE), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSuccessfulHmacSignedRequestObjectWithoutSecret() throws Exception { + // this is supposed to fail since for symmetric algorithms we sign the request object with the client secret + testRequestObjectInvalidConfiguration(getOidcConfigurationInputStreamWithRequestObjectPublicClient(REQUEST.getValue(), HMAC_SHA256), RequestObjectErrorType.MISSING_CLIENT_SECRET); + } + + @Test + public void testIncorrectAuthenticationFormat() throws Exception { + testRequestObjectInvalidConfiguration(getOidcConfigurationInputStreamWithRequestObjectPublicClient("INVALID_REQUEST_PARAMETER", HMAC_SHA256), RequestObjectErrorType.INVALID_REQUEST_FORMAT); + } + + @Test + public void testRequestObjectConfigMissingENCValue() throws Exception { + testRequestObjectInvalidConfiguration(getOidcConfigurationInputStreamWithoutEncValue(REQUEST.getValue(), RSA_OAEP), RequestObjectErrorType.MISSING_ENC_VALUE); + } + /***************************************************************************************************************************************** * Tests for multi-tenancy. * @@ -496,6 +606,54 @@ private void performTenantRequest(String username, String password, String tenan } } + private void testRequestObjectInvalidConfiguration(InputStream oidcConfig, RequestObjectErrorType requestObjectErrorType) throws Exception { + try { + Map props = new HashMap<>(); + try { + OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig); + if (requestObjectErrorType == RequestObjectErrorType.MISSING_ENC_VALUE || requestObjectErrorType == RequestObjectErrorType.INVALID_REQUEST_FORMAT) { + Assert.fail("No error was thrown while attempting to build the client configuration."); + } + assertEquals(OidcClientConfiguration.RelativeUrlsUsed.NEVER, oidcClientConfiguration.getRelativeUrls()); + + OidcClientContext oidcClientContext = new OidcClientContext(oidcClientConfiguration); + oidcFactory = new OidcMechanismFactory(oidcClientContext); + HttpServerAuthenticationMechanism mechanism; + + if (oidcClientConfiguration.getAuthenticationRequestFormat().contains(REQUEST.getValue())) { + mechanism = oidcFactory.createAuthenticationMechanism(OIDC_NAME, props, getCallbackHandler(true, "+phone+profile+email")); + } else { + mechanism = oidcFactory.createAuthenticationMechanism(OIDC_NAME, props, getCallbackHandler()); + } + + URI requestUri = new URI(getClientUrl()); + TestingHttpServerRequest request = new TestingHttpServerRequest(null, requestUri); + try { + mechanism.evaluateRequest(request); + Assert.fail("No error was thrown while attempting to evaluate the request"); + } catch (Exception e) { + + if (requestObjectErrorType == RequestObjectErrorType.INVALID_ALGORITHM) { + assertTrue(e.getMessage().contains("Failed to create the authentication request")); + } else if (requestObjectErrorType == RequestObjectErrorType.MISSING_CLIENT_SECRET) { + assertTrue(e.getMessage().contains("The client secret has not been configured.")); + } else { + throw e; + } + } + } catch (Exception e) { + if (requestObjectErrorType == RequestObjectErrorType.INVALID_REQUEST_FORMAT) { + assertTrue(e.getMessage().contains("Authentication request format must be one of the following: oauth2, request, request_uri.")); + } else if (requestObjectErrorType == RequestObjectErrorType.MISSING_ENC_VALUE) { + assertTrue(e.getMessage().contains("Both request object encryption algorithm and request object content encryption algorithm must be configured to encrypt the request object.")); + } + } + } finally { + client.setDispatcher(new QueueDispatcher()); + } + } + + private InputStream getOidcConfigurationInputStream() { return getOidcConfigurationInputStream(CLIENT_SECRET); } @@ -582,7 +740,6 @@ private InputStream getOidcConfigurationInputStreamWithTokenSignatureAlgorithm() "}"; return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); } - private InputStream getOidcConfigurationInputStreamWithScope(String scopeValue){ String oidcConfig = "{\n" + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + @@ -590,6 +747,25 @@ private InputStream getOidcConfigurationInputStreamWithScope(String scopeValue){ " \"public-client\" : \"false\",\n" + " \"scope\" : \"" + scopeValue + "\",\n" + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + private InputStream getOidcConfigurationInputStreamWithRequestParameter(String requestParameter, String signingAlgorithm, String encryptionAlgorithm, String encMethod){ + String oidcConfig = "{\n" + + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM_WITH_SCOPES + "/" + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"authentication-request-format\" : \"" + requestParameter + "\",\n" + + " \"request-object-signing-algorithm\" : \"" + signingAlgorithm + "\",\n" + + " \"request-object-encryption-alg-value\" : \"" + encryptionAlgorithm + "\",\n" + + " \"request-object-encryption-enc-value\" : \"" + encMethod + "\",\n" + + " \"scope\" : \"profile email phone\",\n" + " \"credentials\" : {\n" + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + " }\n" + @@ -597,6 +773,59 @@ private InputStream getOidcConfigurationInputStreamWithScope(String scopeValue){ return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); } + private InputStream getOidcConfigurationInputStreamWithoutEncValue(String requestParameter, String encryptionAlgorithm){ + String oidcConfig = "{\n" + + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM_WITH_SCOPES + "/" + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"authentication-request-format\" : \"" + requestParameter + "\",\n" + + " \"request-object-encryption-alg-value\" : \"" + encryptionAlgorithm + "\",\n" + + " \"scope\" : \"profile email phone\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationInputStreamWithRequestParameter(String requestParameter, String signingAlgorithm, String encryptionAlgorithm, String encMethod, String keyStorePath, String alias, String keyStoreType){ + String oidcConfig = "{\n" + + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM_WITH_SCOPES + "/" + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"authentication-request-format\" : \"" + requestParameter + "\",\n" + + " \"request-object-signing-algorithm\" : \"" + signingAlgorithm + "\",\n" + + " \"request-object-encryption-alg-value\" : \"" + encryptionAlgorithm + "\",\n" + + " \"request-object-encryption-enc-value\" : \"" + encMethod + "\",\n" + + " \"request-object-signing-keystore-file\" : \"" + keyStorePath + "\",\n" + + " \"request-object-signing-keystore-type\" : \"" + keyStoreType + "\",\n" + + " \"request-object-signing-keystore-password\" : \"" + KEYSTORE_PASS + "\",\n" + + " \"request-object-signing-key-password\" : \"" + KEYSTORE_PASS + "\",\n" + + " \"request-object-signing-key-alias\" : \"" + alias + "\",\n" + + " \"scope\" : \"email phone profile\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationInputStreamWithRequestObjectPublicClient(String requestParameter, String signingAlgorithm){ + String oidcConfig = "{\n" + + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM_WITH_SCOPES + "/" + "\",\n" + + " \"public-client\" : \"true\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"authentication-request-format\" : \"" + requestParameter + "\",\n" + + " \"request-object-signing-algorithm\" : \"" + signingAlgorithm + "\",\n" + + " \"scope\" : \"email phone profile\"\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + private InputStream getOidcConfigurationInputStreamWithPrincipalAttribute(String principalAttributeValue) { String oidcConfig = "{\n" + " \"principal-attribute\" : \"" + principalAttributeValue + "\",\n" + @@ -642,3 +871,4 @@ private static final String getClientPageTestForTenant(String tenant) { return tenant.equals(TENANT1_ENDPOINT) ? TENANT1_ENDPOINT : TENANT2_ENDPOINT + ":" + CLIENT_PAGE_TEXT; } } + diff --git a/pom.xml b/pom.xml index 20543860b1e..e71dc5e2d33 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,7 @@ 4.3.3 2.40.0 2.3.0 + 3.1.0.Final INFO @@ -1152,6 +1153,12 @@ ${version.org.bouncycastle} test + + org.keycloak + keycloak-services + ${version.org.keycloak.keycloak-services} + test +