From cd4478d3d4f08baef32998153fa9b187c07424ac 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 | 16 ++ .../http/oidc/JWKPublicKeySetExtractor.java | 44 ++++ .../oidc/JWTClientCredentialsProvider.java | 47 +--- .../security/http/oidc/JWTSigning.java | 46 ++++ .../security/http/oidc/JWTSigningUtils.java | 81 +++++++ .../org/wildfly/security/http/oidc/Oidc.java | 23 ++ .../http/oidc/OidcClientConfiguration.java | 126 ++++++++++- .../oidc/OidcClientConfigurationBuilder.java | 29 ++- .../security/http/oidc/OidcClientContext.java | 101 +++++++++ .../http/oidc/OidcJsonConfiguration.java | 111 +++++++-- .../http/oidc/OidcProviderMetadata.java | 33 +++ .../http/oidc/OidcPublicKeyExtractor.java | 36 +++ .../http/oidc/OidcRequestAuthenticator.java | 211 +++++++++++++++++- .../http/oidc/KeycloakConfiguration.java | 50 ++++- .../wildfly/security/http/oidc/OidcTest.java | 178 ++++++++++++++- pom.xml | 7 + 17 files changed, 1074 insertions(+), 81 deletions(-) create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/JWKPublicKeySetExtractor.java create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigning.java create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigningUtils.java create mode 100644 http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcPublicKeyExtractor.java diff --git a/http/oidc/pom.xml b/http/oidc/pom.xml index b01609b09f0..ecc11a62668 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 c4ba08c8fb2..5726e4e7665 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 @@ -19,6 +19,7 @@ package org.wildfly.security.http.oidc; import static org.jboss.logging.Logger.Level.ERROR; +import static org.jboss.logging.Logger.Level.INFO; import static org.jboss.logging.Logger.Level.WARN; import static org.jboss.logging.annotations.Message.NONE; @@ -233,5 +234,20 @@ interface ElytronMessages extends BasicLogger { @Message(id = 23056, value = "No message entity") IOException noMessageEntity(); + @Message(id = 23057, value = "Invalid keystore configuration for signing Request Objects.") + IOException invalidKeyStoreConfiguration(); + + @Message(id = 23058, value = "The signature algorithm specified is not supported by the OpenID Provider.") + IOException invalidRequestObjectSignatureAlgorithm(); + + @Message(id = 23059, value = "The encryption algorithm specified is not supported by the OpenID Provider.") + IOException invalidRequestObjectEncryptionAlgorithm(); + + @Message(id = 23060, value = "The content encryption algorithm specified is not supported by the OpenID Provider.") + IOException invalidRequestObjectContentEncryptionAlgorithm(); + + @LogMessage(level = INFO) + @Message(id = 23061, value = "The OpenID provider does not support request parameters. Sending the request using OAuth2 format.") + void requestParameterNotSupported(); } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWKPublicKeySetExtractor.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWKPublicKeySetExtractor.java new file mode 100644 index 00000000000..4069b73b7bb --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWKPublicKeySetExtractor.java @@ -0,0 +1,44 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2020 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.Oidc.JSON_CONTENT_TYPE; + +import java.io.IOException; +import org.apache.http.client.methods.HttpGet; +import org.jose4j.lang.JoseException; +import org.wildfly.security.jose.jwk.JsonWebKeySet; +/** + * A public key locator that dynamically obtains the public key from an OpenID + * provider by sending a request to the provider's {@code jwks_uri} when needed. + * + * @author Prarthona Paul + * */ +public class JWKPublicKeySetExtractor implements OidcPublicKeyExtractor { + public JWKPublicKeySetExtractor() { + } + @Override + public JsonWebKeySet extractPublicKeySet(OidcClientConfiguration config) throws IOException, JoseException { + HttpGet request = new HttpGet(config.getJwksUrl()); + request.addHeader(ACCEPT, JSON_CONTENT_TYPE); + return Oidc.sendJsonHttpRequest(config, request, JsonWebKeySet.class); + } + +} 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..5ea6383e7a5 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 @@ -22,15 +22,9 @@ 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; @@ -115,7 +109,7 @@ public void init(OidcClientConfiguration oidcClientConfiguration, Object credent clientKeyAlias = oidcClientConfiguration.getResourceName(); } - KeyPair keyPair = loadKeyPairFromKeyStore(clientKeyStoreFile, clientKeyStorePassword, clientKeyPassword, clientKeyAlias, clientKeyStoreType); + KeyPair keyPair = new JWTSigningUtils().loadKeyPairFromKeyStore(clientKeyStoreFile, clientKeyStorePassword, clientKeyPassword, clientKeyAlias, clientKeyStoreType); setupKeyPair(keyPair); this.tokenTimeout = asInt(cfg, "token-timeout", 10); } @@ -155,43 +149,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/JWTSigning.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigning.java new file mode 100644 index 00000000000..0b9207934c2 --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigning.java @@ -0,0 +1,46 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 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.InputStream; +import java.security.KeyPair; + +/** + * An interface to obtain the KeyPair from a keystore file. + * + * @author Prarthona Paul + */ + +public interface JWTSigning { + /** + * @param keyStoreFile the path to the keystore file + * @param storePassword the password for the keystore file + * @param keyPassword the password for the key we would like ot extract from the keystore + * @param keyAlias the alias for the key that uniquely identifies it + * @param keyStoreType the type of keystore we are trying to access + * @return the private-public keypair extracted from the keystore + */ + KeyPair loadKeyPairFromKeyStore(String keyStoreFile, String storePassword, String keyPassword, String keyAlias, String keyStoreType); + + /** + * @param keystoreFile the path the keystore file we are trying to access + * @return the contents of the file as an inputStream + */ + InputStream findFile(String keystoreFile); +} 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..4725e0d5e80 --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/JWTSigningUtils.java @@ -0,0 +1,81 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2020 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; + +/** + * An interface to obtain the KeyPair from a keystore file. + * + * @author Prarthona Paul + */ + +public class JWTSigningUtils implements JWTSigning{ + public JWTSigningUtils() {} + + @Override + public 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); + } + } + + @Override + public 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/Oidc.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java index 8d0170fa75a..ee1030b9042 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 @@ -73,6 +73,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"; @@ -199,6 +201,27 @@ public enum TokenStore { COOKIE } + public enum AuthenticationFormat { + REQUEST_TYPE_OAUTH2("oauth2"), + REQUEST_TYPE_REQUEST("request"), + REQUEST_TYPE_REQUEST_URI("request_uri"); + + private final String value; + + AuthenticationFormat(String value) { + this.value = value; + } + + /** + * Get the string value for this referral mode. + * + * @return the string value for this referral mode + */ + 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..5a1324137b9 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 requestObjectContentEncryptionMethodsSupported; + protected List requestObjectEncryptionAlgValuesSupported; + protected boolean requestParameterSupported; + protected boolean requestUriParameterSupported; protected String resource; protected String clientId; @@ -126,6 +132,16 @@ public enum RelativeUrlsUsed { protected boolean verifyTokenAudience = false; protected String tokenSignatureAlgorithm = DEFAULT_TOKEN_SIGNATURE_ALGORITHM; + protected String authenticationRequestFormat; + protected String requestSignatureAlgorithm; + protected String requestEncryptAlgorithm; + protected String requestContentEncryptionMethod; + protected String pushedAuthorizationRequestEndpoint; + protected String requestObjectSigningKeystoreFile; + protected String requestObjectSigningKeyStorePass; + protected String requestObjectSigningKeyPass; + protected String requestObjectSigningKeyAlias; + protected String requestObjectSigningKeystoreType; public OidcClientConfiguration() { } @@ -223,6 +239,13 @@ protected void resolveUrls() { tokenUrl = config.getTokenEndpoint(); logoutUrl = config.getLogoutEndpoint(); jwksUrl = config.getJwksUri(); + requestParameterSupported = config.getRequestParameterSupported(); + requestObjectSigningAlgValuesSupported = config.getRequestObjectSigningAlgValuesSupported(); + requestObjectContentEncryptionMethodsSupported = config.getRequestObjectContentEncryptionMethodsSupported(); + requestObjectEncryptionAlgValuesSupported = config.getRequestObjectEncryptionAlgValuesSupported(); + requestUriParameterSupported = config.getRequestUriParameterSupported(); + pushedAuthorizationRequestEndpoint = config.getPushedAuthorizationRequestEndpoint(); + if (authServerBaseUrl != null) { // keycloak-specific properties accountUrl = getUrl(issuerUrl, ACCOUNT_PATH); @@ -246,7 +269,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 +352,26 @@ public String getIssuerUrl() { return issuerUrl; } + public List getRequestObjectSigningAlgValuesSupported() { + return requestObjectSigningAlgValuesSupported; + } + + public List getRequestObjectEncryptionAlgValuesSupported() { + return requestObjectEncryptionAlgValuesSupported; + } + + public List getRequestObjectContentEncryptionMethodsSupported() { + return requestObjectContentEncryptionMethodsSupported; + } + + public boolean getRequestParameterSupported() { + return requestParameterSupported; + } + + public boolean getRequestUriParameterSupported() { + return requestUriParameterSupported; + } + public void setResource(String resource) { this.resource = resource; } @@ -648,4 +691,83 @@ public String getTokenSignatureAlgorithm() { return tokenSignatureAlgorithm; } + public String getAuthenticationRequestFormat() { + return authenticationRequestFormat; + } + + public void setAuthenticationRequestFormat(String authenticationRequestFormat ) { + this.authenticationRequestFormat = authenticationRequestFormat; + } + + public String getRequestSignatureAlgorithm() { + return requestSignatureAlgorithm; + } + + public void setRequestSignatureAlgorithm(String requestSignatureAlgorithm) { + this.requestSignatureAlgorithm = requestSignatureAlgorithm; + } + + public String getRequestEncryptAlgorithm() { + return requestEncryptAlgorithm; + } + + public void setRequestEncryptAlgorithm(String requestEncryptAlgorithm) { + this.requestEncryptAlgorithm = requestEncryptAlgorithm; + } + + public String getRequestContentEncryptionMethod() { + return requestContentEncryptionMethod; + } + + public void setRequestContentEncryptionMethod(String requestContentEncryptionMethod) { + this.requestContentEncryptionMethod = requestContentEncryptionMethod; + } + + public String getRequestObjectSigningKeystoreFile() { + return requestObjectSigningKeystoreFile; + } + + public void setRequestObjectSigningKeystoreFile(String keyStoreFile) { + this.requestObjectSigningKeystoreFile = keyStoreFile; + } + + public String getRequestObjectSigningKeyStorePassword() { + return requestObjectSigningKeyStorePass; + } + + public void setRequestObjectSigningKeyStorePassword(String pass) { + this.requestObjectSigningKeyStorePass = pass; + } + + public String getRequestObjectSigningKeyPassword() { + return requestObjectSigningKeyPass; + } + + public void setRequestObjectSigningKeyPassword(String pass) { + this.requestObjectSigningKeyPass = pass; + } + + public String getRequestObjectSigningKeystoreType() { + return requestObjectSigningKeystoreType; + } + + public void setRequestObjectSigningKeystoreType(String type) { + this.requestObjectSigningKeystoreType = type; + } + + public String getRequestObjectSigningKeyAlias() { + return requestObjectSigningKeyAlias; + } + + public void setRequestObjectSigningKeyAlias(String alias) { + this.requestObjectSigningKeyAlias = alias; + } + + public String getPushedAuthorizationRequestEndpoint() { + return pushedAuthorizationRequestEndpoint; + } + + public void setPushedAuthorizationRequestEndpoint(String url) { + this.pushedAuthorizationRequestEndpoint = url; + } } 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 99f9b185a5d..ec886a17a8d 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 @@ -19,6 +19,8 @@ package org.wildfly.security.http.oidc; import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.jose4j.jws.AlgorithmIdentifiers.NONE; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationFormat.REQUEST_TYPE_OAUTH2; import static org.wildfly.security.http.oidc.Oidc.SSLRequired; import static org.wildfly.security.http.oidc.Oidc.TokenStore; @@ -100,6 +102,32 @@ protected OidcClientConfiguration internalBuild(final OidcJsonConfiguration oidc if (oidcJsonConfiguration.getTokenCookiePath() != null) { oidcClientConfiguration.setOidcStateCookiePath(oidcJsonConfiguration.getTokenCookiePath()); } + if (oidcJsonConfiguration.getAuthenticationRequestFormat() != null) { + oidcClientConfiguration.setAuthenticationRequestFormat(oidcJsonConfiguration.getAuthenticationRequestFormat()); + } else { + oidcClientConfiguration.setAuthenticationRequestFormat(REQUEST_TYPE_OAUTH2.getValue()); + } + if (oidcJsonConfiguration.getRequestSignatureAlgorithm() != null) { + oidcClientConfiguration.setRequestSignatureAlgorithm(oidcJsonConfiguration.getRequestSignatureAlgorithm()); + } else { + oidcClientConfiguration.setRequestSignatureAlgorithm(NONE); + } + if (oidcJsonConfiguration.getRequestEncryptAlgorithm() != null && oidcJsonConfiguration.getRequestContentEncryptionMethod() != null) { //both are required to encrypt the request object + oidcClientConfiguration.setRequestEncryptAlgorithm(oidcJsonConfiguration.getRequestEncryptAlgorithm()); + oidcClientConfiguration.setRequestContentEncryptionMethod(oidcJsonConfiguration.getRequestContentEncryptionMethod()); + } + 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()); @@ -190,7 +218,6 @@ 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..62424e7b5c3 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 getRequestSignatureAlgorithm() { + return delegate.getRequestSignatureAlgorithm(); + } + + @Override + public void setRequestSignatureAlgorithm(String requestSignature) { + delegate.setRequestSignatureAlgorithm(requestSignature); + } + + @Override + public String getRequestEncryptAlgorithm() { + return delegate.getRequestEncryptAlgorithm(); + } + + @Override + public void setRequestEncryptAlgorithm(String algorithm) { + delegate.setRequestEncryptAlgorithm(algorithm); + } + + @Override + public String getRequestContentEncryptionMethod() { + return delegate.requestContentEncryptionMethod; + } + + @Override + public void setRequestContentEncryptionMethod (String enc) { + delegate.requestContentEncryptionMethod = enc; + } + + @Override + public String getRequestObjectSigningKeystoreFile() { + return delegate.requestObjectSigningKeystoreFile; + } + + @Override + public void setRequestObjectSigningKeystoreFile(String keyStoreFile) { + delegate.requestObjectSigningKeystoreFile = keyStoreFile; + } + + @Override + public String getRequestObjectSigningKeyStorePassword() { + return delegate.requestObjectSigningKeyStorePass; + } + + @Override + public void setRequestObjectSigningKeyStorePassword(String pass) { + delegate.requestObjectSigningKeyStorePass = pass; + } + + @Override + public String getRequestObjectSigningKeyPassword() { + return delegate.requestObjectSigningKeyPass; + } + + @Override + public void setRequestObjectSigningKeyPassword(String pass) { + delegate.requestObjectSigningKeyPass = pass; + } + + @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 5e65d60fe06..5fd8ffb2be6 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,16 @@ "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" + "client-keystore", "client-keystore-password", "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", + "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", + "authentication-request-format", "request-object-signing-algorithm", "request-object-encryption-algorithm", + "request-object-content-encryption-algorithm" }) public class OidcJsonConfiguration { @@ -62,8 +63,16 @@ public class OidcJsonConfiguration { protected String clientKeystore; @JsonProperty("client-keystore-password") 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") @@ -140,6 +149,18 @@ public class OidcJsonConfiguration { @JsonProperty("token-signature-algorithm") protected String tokenSignatureAlgorithm = DEFAULT_TOKEN_SIGNATURE_ALGORITHM; + @JsonProperty("authentication-request-format") + protected String authenticationRequestFormat; + + @JsonProperty("request-object-signing-algorithm") + protected String requestSignatureAlgorithm; + + @JsonProperty("request-object-encryption-algorithm") + protected String requestEncryptAlgorithm; + + @JsonProperty("request-object-content-encryption-algorithm") + protected String requestContentEncryptionMethod; + /** * The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}. */ @@ -178,6 +199,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; } @@ -186,6 +214,22 @@ public void setClientKeystore(String clientKeystore) { this.clientKeystore = clientKeystore; } + public String getRequestObjectSigningKeystoreType() { + return requestObjectSigningKeystoreType; + } + + public void setRequestObjectSigningKeystoreType(String type) { + this.requestObjectSigningKeystoreType = type; + } + + public String getRequestObjectSigningKeyAlias() { + return requestObjectSigningKeyAlias; + } + + public void setRequestObjectSigningKeyAlias(String alias) { + this.requestObjectSigningKeyAlias = alias; + } + public String getClientKeystorePassword() { return clientKeystorePassword; } @@ -194,12 +238,20 @@ public void setClientKeystorePassword(String clientKeystorePassword) { this.clientKeystorePassword = clientKeystorePassword; } - 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() { @@ -511,5 +563,36 @@ public void setTokenSignatureAlgorithm(String tokenSignatureAlgorithm) { this.tokenSignatureAlgorithm = tokenSignatureAlgorithm; } + public String getAuthenticationRequestFormat() { + return authenticationRequestFormat; + } + + public void setAuthenticationRequestFormat(String authenticationRequestFormat) { + this.authenticationRequestFormat = authenticationRequestFormat; + } + + public String getRequestSignatureAlgorithm() { + return requestSignatureAlgorithm; + } + + public void setRequestSignatureAlgorithm(String requestSignatureAlgorithm) { + this.requestSignatureAlgorithm = requestSignatureAlgorithm; + } + + public String getRequestEncryptAlgorithm() { + return requestEncryptAlgorithm; + } + + public void setRequestEncryptAlgorithm(String requestSignatureAlgorithm) { + this.requestEncryptAlgorithm = requestSignatureAlgorithm; + } + + public String getRequestContentEncryptionMethod() { + return requestContentEncryptionMethod; + } + + public void setRequestContentEncryptionMethod (String requestContentEncryptionMethod) { + this.requestContentEncryptionMethod = requestContentEncryptionMethod; + } } 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..6da737f5b14 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 requestObjectContentEncryptionMethodsSupported; + + @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 getRequestObjectContentEncryptionMethodsSupported() { + return requestObjectContentEncryptionMethodsSupported; + } + + public void setRequestObjectContentEncryptionMethodsSupported(List requestObjectContentEncryptionMethodsSupported) { + this.requestObjectContentEncryptionMethodsSupported = requestObjectContentEncryptionMethodsSupported; + } + + 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/OidcPublicKeyExtractor.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcPublicKeyExtractor.java new file mode 100644 index 00000000000..75b5294341d --- /dev/null +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcPublicKeyExtractor.java @@ -0,0 +1,36 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 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 org.wildfly.security.jose.jwk.JsonWebKeySet; + +/** + * An interface to send a GET request to the JWKSUrl of the OpenID + * provider and obtain the public key. + * + * @author Prarthona Paul + */ +public interface OidcPublicKeyExtractor { + /** + * @param config the OpenID Connect client configuration + * @return a Json Web Key Set with the public keys for the OpenID provider + */ + JsonWebKeySet extractPublicKeySet(OidcClientConfiguration config) throws Exception; + +} 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 6b51d980d97..716d610253c 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,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.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256; +import static org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA384; +import static org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA512; import static org.wildfly.security.http.oidc.Oidc.CLIENT_ID; import static org.wildfly.security.http.oidc.Oidc.CODE; import static org.wildfly.security.http.oidc.Oidc.DOMAIN_HINT; @@ -28,31 +32,57 @@ import static org.wildfly.security.http.oidc.Oidc.MAX_AGE; import static org.wildfly.security.http.oidc.Oidc.OIDC_SCOPE; import static org.wildfly.security.http.oidc.Oidc.PROMPT; +import static org.wildfly.security.http.oidc.Oidc.PROTOCOL_CLASSPATH; 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.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.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.IOException; +import java.io.Reader; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyPair; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; -import org.apache.http.HttpStatus; +import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; +import org.apache.http.HttpStatus; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jose4j.jwa.AlgorithmConstraints; +import org.jose4j.jwe.JsonWebEncryption; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.keys.HmacKey; +import org.jose4j.lang.JoseException; import org.wildfly.security.http.HttpConstants; +import org.wildfly.security.jose.jwk.JWK; +import org.wildfly.security.jose.jwk.JWKParser; + /** * @author Bill Burke @@ -180,18 +210,68 @@ protected String getRedirectUri(String state) { if (deployment.getAuthUrl() == null) { 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 + createRequestWithRequestParameter(REQUEST, redirectUriBuilder, redirectUri, state, forwardedQueryParams); + } else { + // send request as usual + createOAuthRequest(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + log.requestParameterNotSupported(); + } + break; + case REQUEST_URI: + if (deployment.getRequestUriParameterSupported()) { + createRequestWithRequestParameter(REQUEST_URI, redirectUriBuilder, redirectUri, state, forwardedQueryParams); + } 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); + } catch (IOException | JoseException | InvalidJwtException e) { + throw new RuntimeException(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, InvalidJwtException { + 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 = getRequestUri(request); + redirectUriBuilder.addParameter(REQUEST_URI, request_uri) + .addParameter(REDIRECT_URI, redirectUri); + break; + } + return redirectUriBuilder; + } + protected int getSSLRedirectPort() { return sslRedirectPort; } @@ -416,4 +496,119 @@ private static boolean hasScope(String scopeParam, String targetScope) { } return false; } + private String convertToRequestParameter(URIBuilder redirectUriBuilder, String redirectUri, String state, List forwardedQueryParams) throws IOException, JoseException { + redirectUriBuilder.addParameter(SCOPE, OIDC_SCOPE); + + JwtClaims jwtClaims = new JwtClaims(); + jwtClaims.setIssuer(deployment.getIssuerUrl()); + jwtClaims.setAudience(deployment.getProviderUrl()); + + 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); + + // Encrypting optional + if (deployment.getRequestEncryptAlgorithm() != null && !deployment.getRequestEncryptAlgorithm().isEmpty() && + deployment.getRequestContentEncryptionMethod() != null && !deployment.getRequestContentEncryptionMethod().isEmpty()) { + return encryptRequest(signedRequest).getCompactSerialization(); + } else { + return signedRequest.getCompactSerialization(); + } + } + + public KeyPair getkeyPair() throws IOException { + if (deployment.getRequestObjectSigningKeystoreFile().contains(PROTOCOL_CLASSPATH)) { + deployment.setRequestObjectSigningKeystoreFile(deployment.getRequestObjectSigningKeystoreFile().replace(PROTOCOL_CLASSPATH, Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource("")).getPath())); + } + if (!deployment.getRequestSignatureAlgorithm().equals(NONE) && deployment.getRequestObjectSigningKeystoreFile() == null){ + throw log.invalidKeyStoreConfiguration(); + } else { + return new JWTSigningUtils().loadKeyPairFromKeyStore(deployment.getRequestObjectSigningKeystoreFile(), + deployment.getRequestObjectSigningKeyStorePassword(), deployment.getRequestObjectSigningKeyPassword(), + deployment.getRequestObjectSigningKeyAlias(), deployment.getRequestObjectSigningKeystoreType()); + } + } + + public JsonWebSignature signRequest(JwtClaims jwtClaims) throws IOException, JoseException { + JsonWebSignature jsonWebSignature = new JsonWebSignature(); + jsonWebSignature.setPayload(jwtClaims.toJson()); + + if (!deployment.getRequestObjectSigningAlgValuesSupported().contains(deployment.getRequestSignatureAlgorithm())) { + throw log.invalidRequestObjectSignatureAlgorithm(); + } else { + if (deployment.getRequestSignatureAlgorithm().contains(NONE)) { //unsigned + jsonWebSignature.setAlgorithmConstraints(AlgorithmConstraints.NO_CONSTRAINTS); + jsonWebSignature.setAlgorithmHeaderValue(NONE); + } else if (deployment.getRequestSignatureAlgorithm().contains(HMAC_SHA256) + || deployment.getRequestSignatureAlgorithm().contains(HMAC_SHA384) + || deployment.getRequestSignatureAlgorithm().contains(HMAC_SHA512)) { //signed with symmetric key + jsonWebSignature.setAlgorithmHeaderValue(deployment.getRequestSignatureAlgorithm()); + Key key = new HmacKey(deployment.getResourceCredentials().get("secret").toString().getBytes(StandardCharsets.UTF_8)); //the client secret is a shared secret between the server and the client + jsonWebSignature.setDoKeyValidation(false); //skips validation so that size of the secret does not matter + jsonWebSignature.setKey(key); + } else { //signed with asymmetric key + KeyPair keyPair = getkeyPair(); + jsonWebSignature.setKey(keyPair.getPrivate()); + jsonWebSignature.setAlgorithmHeaderValue(deployment.getRequestSignatureAlgorithm()); + } + jsonWebSignature.sign(); + return jsonWebSignature; + } + } + + private JsonWebEncryption encryptRequest(JsonWebSignature signedRequest) throws IOException, JoseException { + if (!deployment.getRequestObjectEncryptionAlgValuesSupported().contains(deployment.getRequestEncryptAlgorithm())) { + throw log.invalidRequestObjectEncryptionAlgorithm(); + } else if (!deployment.getRequestObjectContentEncryptionMethodsSupported().contains(deployment.getRequestContentEncryptionMethod())) { + throw log.invalidRequestObjectContentEncryptionAlgorithm(); + } else { + JsonWebEncryption jsonEncryption = new JsonWebEncryption(); + jsonEncryption.setPayload(signedRequest.getCompactSerialization()); + jsonEncryption.setAlgorithmConstraints(new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, deployment.getRequestEncryptAlgorithm(), deployment.getRequestContentEncryptionMethod())); + jsonEncryption.setAlgorithmHeaderValue(deployment.getRequestEncryptAlgorithm()); + jsonEncryption.setEncryptionMethodHeaderParameter(deployment.getRequestContentEncryptionMethod()); + JWK[] jwkList = new JWKPublicKeySetExtractor().extractPublicKeySet(deployment).getKeys(); + for (JWK jwk : jwkList) { + if (jwk.getPublicKeyUse().equals("enc")) { //JWTs are to be encrypted with public keys shared by the OpenID provider and decrypted by the private key they hold + jsonEncryption.setKey(new JWKParser(jwk).toPublicKey()); + break; + } + } + return jsonEncryption; + } + } + + private String getRequestUri(String request) throws IOException, InvalidJwtException { + HttpPost parRequest = new HttpPost(deployment.getPushedAuthorizationRequestEndpoint()); + List formParams = new ArrayList(); + formParams.add(new BasicNameValuePair(REQUEST, request)); + ClientCredentialsProviderUtils.setClientCredentials(deployment, parRequest, formParams); + parRequest.addHeader("Content-type", "application/x-www-form-urlencoded"); + + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8); + parRequest.setEntity(form); + HttpResponse response = deployment.getClient().execute(parRequest); + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_CREATED) { + EntityUtils.consumeQuietly(response.getEntity()); + throw log.unexpectedResponseCodeFromOidcProvider(response.getStatusLine().getStatusCode()); + } + InputStream inputStream = response.getEntity().getContent(); + StringBuilder textBuilder = new StringBuilder(); + try (Reader reader = new BufferedReader(new InputStreamReader + (inputStream, StandardCharsets.UTF_8))) { + int c = 0; + while ((c = reader.read()) != -1) { + textBuilder.append((char) c); + } + } + JwtClaims jwt = JwtClaims.parse(textBuilder.toString()); + return jwt.getClaimValueAsString(REQUEST_URI); + } } 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 5dfa052ed28..7c8c246f280 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 @@ -18,11 +18,15 @@ package org.wildfly.security.http.oidc; +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 org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -30,9 +34,11 @@ 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 javax.security.auth.x500.X500Principal; + /** * Keycloak configuration for testing. * @@ -47,6 +53,24 @@ public class KeycloakConfiguration { private static final String BOB = "bob"; 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; /** * Configure RealmRepresentation as follows: @@ -60,14 +84,14 @@ public class KeycloakConfiguration { * */ public static RealmRepresentation getRealmRepresentation(final String realmName, String clientId, String clientSecret, - String clientHostName, int clientPort, String clientApp) { + String clientHostName, int clientPort, String clientApp) throws Exception { return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp); } 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); } @@ -102,14 +126,14 @@ public static String getAccessToken(String authServerUrl, String realmName, Stri } private static RealmRepresentation createRealm(String name, String clientId, String clientSecret, - String clientHostName, int clientPort, String clientApp) { + String clientHostName, int clientPort, String clientApp) throws Exception { return createRealm(name, clientId, clientSecret, clientHostName, clientPort, clientApp, false, null, null); } private static RealmRepresentation createRealm(String name, String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, boolean directAccessGrantEnabled, String bearerOnlyClientId, - String corsClientId) { + String corsClientId) throws Exception { RealmRepresentation realm = new RealmRepresentation(); realm.setRealm(name); @@ -140,15 +164,16 @@ private static RealmRepresentation createRealm(String name, String clientId, Str realm.getUsers().add(createUser(ALICE, ALICE_PASSWORD, Arrays.asList(USER_ROLE, ADMIN_ROLE))); realm.getUsers().add(createUser(BOB, BOB_PASSWORD, Arrays.asList(USER_ROLE))); + return realm; } - private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, boolean directAccessGrantEnabled) { + private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp, boolean directAccessGrantEnabled) throws Exception { return createWebAppClient(clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, null); } private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, - String clientApp, boolean directAccessGrantEnabled, String allowedOrigin) { + String clientApp, boolean directAccessGrantEnabled, String allowedOrigin) throws Exception { ClientRepresentation client = new ClientRepresentation(); client.setClientId(clientId); client.setPublicClient(false); @@ -157,9 +182,20 @@ private static ClientRepresentation createWebAppClient(String clientId, String c client.setRedirectUris(Arrays.asList("http://" + clientHostName + ":" + clientPort + "/" + clientApp)); 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(); + 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; } 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 9ce5a55c934..6ef4ccf450d 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,10 +18,29 @@ 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.HMAC_SHA512; +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.assertTrue; import static org.junit.Assume.assumeTrue; 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.AuthenticationFormat.REQUEST_TYPE_OAUTH2; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationFormat.REQUEST_TYPE_REQUEST; +import static org.wildfly.security.http.oidc.Oidc.AuthenticationFormat.REQUEST_TYPE_REQUEST_URI; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -30,6 +49,8 @@ import java.util.HashMap; import java.util.Map; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.QueueDispatcher; import org.apache.http.HttpStatus; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -40,8 +61,6 @@ import com.gargoylesoftware.htmlunit.html.HtmlPage; import io.restassured.RestAssured; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.QueueDispatcher; /** * Tests for the OpenID Connect authentication mechanism. @@ -163,8 +182,102 @@ public void testTokenSignatureAlgorithm() throws Exception { } // 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 testOpenIDWithOauth2Request() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_OAUTH2.getValue(), "", "", ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testOpenIDWithPlaintextRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_REQUEST.getValue(), NONE, "", ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testOpenIDWithPlaintextEncryptedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_REQUEST.getValue(), NONE, RSA_OAEP, A128CBC_HS256), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testOpenIDWithRsaSignedAndEncryptedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_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 testOpenIDWithPsSignedAndRsaEncryptedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_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 testOpenIDWithInvalidSignAlgorithm() throws Exception { + //RSNULL is a valid signature algorithm, but not one of the ones supported by keycloak + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_REQUEST.getValue(), "RSNULL", 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, true); + } + + @Test + public void testOpenIDWithRsaSignedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_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 testOpenIDWithPsSignedRequest() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_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 testOpenIDWithInvalidRequestEncryptionAlgorithm() throws Exception { + // None is not a valid algorithm for encrypting jwt's and RSA-OAEP is not a valid algorithm for signing + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_REQUEST.getValue(), RSA1_5, NONE, NONE, 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, true); + } + + @Test + public void testOpenIDWithPlaintextRequestUri() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_REQUEST_URI.getValue(), NONE, "", ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testOpenIDWithHmacRequestUri() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_REQUEST.getValue(), HMAC_SHA256, "", ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testOpenIDWithHmacEncryptedRequestUri() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_REQUEST.getValue(), HMAC_SHA512, RSA_OAEP, A128CBC_HS256), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testOpenIDWithSignedAndEncryptedRequestUri() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_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); + } + private void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String expectedLocation, String clientPageText) throws Exception { + performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, expectedLocation, clientPageText, null, false); + } + + private void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, + int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, String expectedScope, boolean checkInvalidScopeError) throws Exception { + performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, expectedLocation, clientPageText, expectedScope, checkInvalidScopeError, false); + } + + private void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, + int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, boolean checkInvalidRequestAlgorithm) throws Exception { + performAuthentication(oidcConfig, username, password, loginToKeycloak, expectedDispatcherStatusCode, expectedLocation, clientPageText, null, false, checkInvalidRequestAlgorithm); + } + + private void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, + int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, String expectedScope, boolean checkInvalidScopeError, boolean checkInvalidRequestAlgorithm) throws Exception { try { Map props = new HashMap<>(); OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig); @@ -176,10 +289,29 @@ private void performAuthentication(InputStream oidcConfig, String username, Stri URI requestUri = new URI(getClientUrl()); TestingHttpServerRequest request = new TestingHttpServerRequest(null, requestUri); - mechanism.evaluateRequest(request); + try { + mechanism.evaluateRequest(request); + } catch (Exception e) { + if (checkInvalidRequestAlgorithm) { + assertTrue(e.getMessage().contains("java.io.IOException: ELY23058")); + return; //Expected to get an exception and ignore the rest + } else { + throw e; + } + } TestingHttpServerResponse response = request.getResponse(); assertEquals(loginToKeycloak ? HttpStatus.SC_MOVED_TEMPORARILY : HttpStatus.SC_FORBIDDEN, response.getStatusCode()); assertEquals(Status.NO_AUTH, request.getResult()); + if (expectedScope != null) { + assertTrue(response.getFirstResponseHeaderValue("Location").contains("scope=" + expectedScope)); + } + if (oidcClientConfiguration.getAuthenticationRequestFormat().contains(REQUEST_TYPE_REQUEST_URI.getValue())) { + assertTrue(response.getFirstResponseHeaderValue("Location").contains("scope=" + OIDC_SCOPE)); + assertTrue(response.getFirstResponseHeaderValue("Location").contains("request_uri=")); + } else if (oidcClientConfiguration.getAuthenticationRequestFormat().contains(REQUEST_TYPE_REQUEST.getValue())) { + assertTrue(response.getFirstResponseHeaderValue("Location").contains("scope=" + OIDC_SCOPE)); + assertTrue(response.getFirstResponseHeaderValue("Location").contains("request=")); + } if (loginToKeycloak) { client.setDispatcher(createAppResponse(mechanism, expectedDispatcherStatusCode, expectedLocation, clientPageText)); @@ -291,4 +423,44 @@ private InputStream getOidcConfigurationInputStreamWithTokenSignatureAlgorithm() "}"; return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); } + + private InputStream getOidcConfigurationInputStreamWithRequestParameter(String requestParameter, String signAlgorithm, String encryptAlgorithm, String encMethod){ + String oidcConfig = "{\n" + + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "/" + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"authentication-request-format\" : \"" + requestParameter + "\",\n" + + " \"request-object-signing-algorithm\" : \"" + signAlgorithm + "\",\n" + + " \"request-object-encryption-algorithm\" : \"" + encryptAlgorithm + "\",\n" + + " \"request-object-content-encryption-algorithm\" : \"" + encMethod + "\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationInputStreamWithRequestParameter(String requestParameter, String signAlgorithm, String encryptAlgorithm, String encMethod, String keyStorePath, String alias, String keyStoreType){ + String oidcConfig = "{\n" + + " \"client-id\" : \"" + CLIENT_ID + "\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "/" + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"authentication-request-format\" : \"" + requestParameter + "\",\n" + + " \"request-object-signing-algorithm\" : \"" + signAlgorithm + "\",\n" + + " \"request-object-encryption-algorithm\" : \"" + encryptAlgorithm + "\",\n" + + " \"request-object-content-encryption-algorithm\" : \"" + 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" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } } + diff --git a/pom.xml b/pom.xml index dd4eebaa9d2..feda2be1d08 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,7 @@ 4.3.3 2.40.0 2.3.0 + 3.1.0.Final INFO @@ -1146,6 +1147,12 @@ ${version.org.bouncycastle} test + + org.keycloak + keycloak-services + ${version.org.keycloak.keycloak-services} + test +