From 3ce624a15e0fec44eaf2b4091e1e5caaaa7ccf8d 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 | 11 ++ .../oidc/JWTClientCredentialsProvider.java | 2 +- .../org/wildfly/security/http/oidc/Oidc.java | 40 ++++ .../http/oidc/OidcClientConfiguration.java | 170 ++++++++++++++++- .../oidc/OidcClientConfigurationBuilder.java | 27 ++- .../security/http/oidc/OidcClientContext.java | 92 ++++++++++ .../http/oidc/OidcJsonConfiguration.java | 70 ++++++- .../http/oidc/OidcProviderMetadata.java | 33 ++++ .../http/oidc/OidcRequestAuthenticator.java | 173 +++++++++++++++++- .../http/oidc/KeycloakConfiguration.java | 93 +++++++++- .../wildfly/security/http/oidc/OidcTest.java | 164 ++++++++++++++++- 11 files changed, 851 insertions(+), 24 deletions(-) diff --git a/http/oidc/pom.xml b/http/oidc/pom.xml index be137f0e480..e9ba2bca0ac 100644 --- a/http/oidc/pom.xml +++ b/http/oidc/pom.xml @@ -128,6 +128,12 @@ keycloak-admin-client test + + org.keycloak + keycloak-services + 3.1.0.Final + test + org.jboss.logmanager jboss-logmanager @@ -173,6 +179,11 @@ jmockit test + + org.wildfly.security + wildfly-elytron-credential-source-impl + test + 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..0810110b4be 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 @@ -156,7 +156,7 @@ protected JwtClaims createRequestToken(String clientId, String tokenUrl) { return jwtClaims; } - private static KeyPair loadKeyPairFromKeyStore(String keyStoreFile, String storePassword, String keyPassword, String keyAlias, String keyStoreType) { + public static KeyPair loadKeyPairFromKeyStore(String keyStoreFile, String storePassword, String keyPassword, String keyAlias, String keyStoreType) { InputStream stream = findFile(keyStoreFile); try { KeyStore keyStore = KeyStore.getInstance(keyStoreType); 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..259aaa5d1bb 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 @@ -52,10 +52,14 @@ public class Oidc { public static final String TEXT_CONTENT_TYPE = "text/*"; public static final String DISCOVERY_PATH = ".well-known/openid-configuration"; public static final String KEYCLOAK_REALMS_PATH = "realms/"; + public static final String KEYSTORE_PASS = "password"; + public static final String JKS_KEYSTORE_TYPE = "JKS"; public static final String JSON_CONFIG_CONTEXT_PARAM = "org.wildfly.security.http.oidc.json.config"; static final String ACCOUNT_PATH = "account"; public static final String CLIENTS_MANAGEMENT_REGISTER_NODE_PATH = "clients-managements/register-node"; public static final String CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH = "clients-managements/unregister-node"; + public static final String ADMIN_CONSOLE_PATH = "admin/master/console/#"; + public static final String REALM_SETTING_KEYS_PATH = "realm-settings/keys"; public static final String SLASH = "/"; public static final String OIDC_CLIENT_CONTEXT_KEY = OidcClientContext.class.getName(); public static final String CLIENT_ID = "client_id"; @@ -73,12 +77,17 @@ 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"; public static final String OIDC_SCOPE = "openid"; public static final String REDIRECT_URI = "redirect_uri"; public static final String REFRESH_TOKEN = "refresh_token"; + public static final String REQUEST_TYPE_OAUTH2 = "oauth2"; + public static final String REQUEST_TYPE_REQUEST = "request"; + public static final String REQUEST_TYPE_REQUEST_URI = "request_uri"; public static final String RESPONSE_TYPE = "response_type"; public static final String SESSION_STATE = "session_state"; public static final String SOAP_ACTION = "SOAPAction"; @@ -116,6 +125,35 @@ public class Oidc { public static final String X_REQUESTED_WITH = "X-Requested-With"; public static final String XML_HTTP_REQUEST = "XMLHttpRequest"; + /* Accepted Request Object Signing Algorithms for KeyCloak*/ + public static final String NONE = "none"; + public static final String RS_256 = "RS256"; + public static final String HS_256 = "HS256"; + public static final String HS_384 = "HS384"; + public static final String HS_512 = "HS512"; + public static final String ES_256 = "ES256"; + public static final String ES_384 = "ES384"; + public static final String ES_512 = "ES512"; + public static final String ES_256K = "ES256K"; + public static final String RS_384 = "RS384"; + public static final String RS_512 = "RS512"; + public static final String PS_256 = "PS256"; + public static final String PS_384 = "PS384"; + public static final String PS_512 = "PS512"; + + /* 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 A256GCM = "A256GCM"; + public static final String A192GCM = "A192GCM"; + public static final String A128GCM = "A128GCM"; + public static final String A128CBC_HS256 = "A128CBC-HS256"; + public static final String A192CBC_HS384 = "A192CBC-HS384"; + public static final String A256CBC_HS512 = "A256CBC-HS512"; + /** * Bearer token pattern. * The Bearer token authorization header is of the form "Bearer", followed by optional whitespace, followed by @@ -276,6 +314,8 @@ public static String getJavaAlgorithm(String algorithm) { return ES384; case AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512: return ES512; + case AlgorithmIdentifiers.NONE: + return NONE; default: throw log.unknownAlgorithm(algorithm); } 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 db872b30a89..34641e1045a 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,16 @@ 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.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -41,7 +48,7 @@ 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; +import org.jose4j.jwk.JsonWebKeySet; /** * The OpenID Connect (OIDC) configuration for a client application. This class is based on @@ -80,7 +87,13 @@ public enum RelativeUrlsUsed { protected String unregisterNodeUrl; protected String jwksUrl; protected String issuerUrl; + protected String authorizationEndpoint; 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 +139,19 @@ 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 requestEncryptEncValue; + protected String pushedAuthorizationRequestEndpoint; + protected String clientKeyStoreFile; + protected String clientKeyStorePass; + protected String clientKeyPass; + protected String clientKeyAlias; + protected String clientKeystoreType; + + protected String realmKey; + protected JsonWebKeySet realmKeySet = new JsonWebKeySet(); public OidcClientConfiguration() { } @@ -223,6 +249,15 @@ protected void resolveUrls() { tokenUrl = config.getTokenEndpoint(); logoutUrl = config.getLogoutEndpoint(); jwksUrl = config.getJwksUri(); + authorizationEndpoint = config.getAuthorizationEndpoint(); + requestParameterSupported = config.getRequestParameterSupported(); + requestObjectSigningAlgValuesSupported = config.getRequestObjectSigningAlgValuesSupported(); + requestObjectEncryptionEncValuesSupported = config.getRequestObjectEncryptionEncValuesSupported(); + requestObjectEncryptionAlgValuesSupported = config.getRequestObjectEncryptionAlgValuesSupported(); + requestUriParameterSupported = config.getRequestUriParameterSupported(); + realmKeySet = createrealmKeySet(); + pushedAuthorizationRequestEndpoint = config.getPushedAuthorizationRequestEndpoint(); + if (authServerBaseUrl != null) { // keycloak-specific properties accountUrl = getUrl(issuerUrl, ACCOUNT_PATH); @@ -237,6 +272,26 @@ protected void resolveUrls() { } } + private JsonWebKeySet createrealmKeySet() throws Exception{ + HttpGet request = new HttpGet(jwksUrl); + request.addHeader(ACCEPT, JSON_CONTENT_TYPE); + HttpResponse response = getClient().execute(request); + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + EntityUtils.consumeQuietly(response.getEntity()); + throw new Exception(response.getStatusLine().getReasonPhrase()); + } + 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); + } + } + return new JsonWebKeySet(textBuilder.toString()); + } + protected OidcProviderMetadata getOidcProviderMetadata(String discoveryUrl) throws Exception { HttpGet request = new HttpGet(discoveryUrl); request.addHeader(ACCEPT, JSON_CONTENT_TYPE); @@ -246,7 +301,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 +384,22 @@ public String getIssuerUrl() { return issuerUrl; } + public List getRequestObjectSigningAlgValuesSupported() { + return requestObjectSigningAlgValuesSupported; + } + + public boolean getRequestParameterSupported() { + return requestParameterSupported; + } + + public boolean getRequestUriParameterSupported() { + return requestUriParameterSupported; + } + + public String getAuthorizationEndpoint() { + return authorizationEndpoint; + } + public void setResource(String resource) { this.resource = resource; } @@ -651,4 +722,99 @@ public String getTokenSignatureAlgorithm() { return tokenSignatureAlgorithm; } + public String getAuthenticationRequestFormat() { + return authenticationRequestFormat; + } + + public void setAuthenticationRequestFormat(String requestObjectType ) { + this.authenticationRequestFormat = requestObjectType; + } + + public String getRequestSignatureAlgorithm() { + return requestSignatureAlgorithm; + } + + public void setRequestSignatureAlgorithm(String algorithm) { + this.requestSignatureAlgorithm = algorithm; + } + + public String getRequestEncryptAlgorithm() { + return requestEncryptAlgorithm; + } + + public void setRequestEncryptAlgorithm(String algorithm) { + this.requestEncryptAlgorithm = algorithm; + } + + public String getRequestEncryptEncValue() { + return requestEncryptEncValue; + } + + public void setRequestEncryptEncValue (String enc) { + this.requestEncryptEncValue = enc; + } + + public String getRealmKey () { + return realmKey; + } + + public void setRealmKey(String key) { + this.realmKey = key; + } + + public String getClientKeyStore () { + return clientKeyStoreFile; + } + + public void setClientKeyStore(String keyStoreFile) { + this.clientKeyStoreFile = keyStoreFile; + } + + public String getClientKeyStorePassword () { + return clientKeyStorePass; + } + + public void setClientKeyStorePassword(String pass) { + this.clientKeyStorePass = pass; + } + + public String getClientKeyPassword () { + return clientKeyPass; + } + + public void setClientKeyPassword(String pass) { + this.clientKeyPass = pass; + } + + public String getClientKeystoreType() { + return clientKeystoreType; + } + + public void setClientKeystoreType(String type) { + this.clientKeystoreType = type; + } + + public String getClientKeyAlias() { + return clientKeyAlias; + } + + public void setClientKeyAlias(String alias) { + this.clientKeyAlias = alias; + } + + public JsonWebKeySet getrealmKeySet() { + return realmKeySet; + } + + public void setrealmKeySet(JsonWebKeySet keySet) { + this.realmKeySet = keySet; + } + + 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..0dee6a550c4 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.wildfly.security.http.oidc.Oidc.NONE; +import static org.wildfly.security.http.oidc.Oidc.REQUEST_TYPE_OAUTH2; import static org.wildfly.security.http.oidc.Oidc.SSLRequired; import static org.wildfly.security.http.oidc.Oidc.TokenStore; @@ -100,6 +102,30 @@ 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); + } + if (oidcJsonConfiguration.getRequestSignatureAlgorithm() != null) { + oidcClientConfiguration.setRequestSignatureAlgorithm(oidcJsonConfiguration.getRequestSignatureAlgorithm()); + } else { + oidcClientConfiguration.setRequestSignatureAlgorithm(NONE); + } + if (oidcJsonConfiguration.getRequestEncryptAlgorithm() != null && oidcJsonConfiguration.getRequestEncryptEncValue() != null) { //both are required to encrypt the request object + oidcClientConfiguration.setRequestEncryptAlgorithm(oidcJsonConfiguration.getRequestEncryptAlgorithm()); + oidcClientConfiguration.setRequestEncryptEncValue(oidcJsonConfiguration.getRequestEncryptEncValue()); + } + if (oidcJsonConfiguration.getClientKeystore() != null && oidcJsonConfiguration.getClientKeystorePassword() != null && + oidcJsonConfiguration.getClientKeyPassword() != null && oidcJsonConfiguration.getClientKeyAlias() != null) { + oidcClientConfiguration.setClientKeyStore(oidcJsonConfiguration.getClientKeystore()); + oidcClientConfiguration.setClientKeyStorePassword(oidcJsonConfiguration.getClientKeystorePassword()); + oidcClientConfiguration.setClientKeyPassword(oidcJsonConfiguration.getClientKeyPassword()); + oidcClientConfiguration.setClientKeyAlias(oidcJsonConfiguration.getClientKeyAlias()); + if (oidcJsonConfiguration.getClientKeystoreType() != null) { + oidcClientConfiguration.setClientKeystoreType(oidcJsonConfiguration.getClientKeystoreType()); + } + } if (oidcJsonConfiguration.getPrincipalAttribute() != null) oidcClientConfiguration.setPrincipalAttribute(oidcJsonConfiguration.getPrincipalAttribute()); oidcClientConfiguration.setResourceCredentials(oidcJsonConfiguration.getCredentials()); @@ -190,7 +216,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..021346fa155 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 @@ -28,6 +28,7 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.utils.URIBuilder; +import org.jose4j.jwk.JsonWebKeySet; /** * @@ -525,6 +526,97 @@ 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 getRequestEncryptEncValue() { + return delegate.requestEncryptEncValue; + } + + @Override + public void setRequestEncryptEncValue (String enc) { + delegate.requestEncryptEncValue = enc; + } + + @Override + public String getRealmKey () { + return delegate.realmKey; + } + + @Override + public void setRealmKey(String key) { + delegate.realmKey = key; + } + + @Override + public JsonWebKeySet getrealmKeySet() { + return delegate.realmKeySet; + } + + @Override + public String getClientKeystoreType() { + return delegate.clientKeystoreType; + } + + @Override + public void setClientKeystoreType(String type) { + delegate.clientKeystoreType = type; + } + + @Override + public String getClientKeyAlias() { + return delegate.clientKeyAlias; + } + + @Override + public void setClientKeyAlias(String alias) { + delegate.clientKeyAlias = alias; + } + + @Override + public void setrealmKeySet(JsonWebKeySet keySet) { + delegate.realmKeySet = keySet; + } + + @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..aa3eeff265d 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 @@ -41,12 +41,13 @@ "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", + "client-keystore-file", "client-keystore-password", "client-key-password", "client-key-alias", "client-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" + "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 { @@ -58,12 +59,16 @@ public class OidcJsonConfiguration { protected String truststore; @JsonProperty("truststore-password") protected String truststorePassword; - @JsonProperty("client-keystore") + @JsonProperty("client-keystore-file") protected String clientKeystore; @JsonProperty("client-keystore-password") protected String clientKeystorePassword; @JsonProperty("client-key-password") protected String clientKeyPassword; + @JsonProperty("client-key-alias") + protected String clientKeyAlias; + @JsonProperty("client-keystore-type") + protected String clientKeystoreType; @JsonProperty("connection-pool-size") protected int connectionPoolSize = 20; @JsonProperty("always-refresh-token") @@ -140,6 +145,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 requestEncryptEncValue; + /** * The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}. */ @@ -186,6 +203,22 @@ public void setClientKeystore(String clientKeystore) { this.clientKeystore = clientKeystore; } + public String getClientKeystoreType() { + return clientKeystoreType; + } + + public void setClientKeystoreType(String type) { + this.clientKeystoreType = type; + } + + public String getClientKeyAlias() { + return clientKeyAlias; + } + + public void setClientKeyAlias(String alias) { + this.clientKeyAlias = alias; + } + public String getClientKeystorePassword() { return clientKeystorePassword; } @@ -511,5 +544,36 @@ public void setTokenSignatureAlgorithm(String tokenSignatureAlgorithm) { this.tokenSignatureAlgorithm = tokenSignatureAlgorithm; } + public String getAuthenticationRequestFormat() { + return authenticationRequestFormat; + } + + public void setAuthenticationRequestFormat(String requestType) { + this.authenticationRequestFormat = requestType; + } + + public String getRequestSignatureAlgorithm() { + return requestSignatureAlgorithm; + } + + public void setRequestSignatureAlgorithm(String algorithm) { + this.requestSignatureAlgorithm = algorithm; + } + + public String getRequestEncryptAlgorithm() { + return requestEncryptAlgorithm; + } + + public void setRequestEncryptAlgorithm(String algorithm) { + this.requestEncryptAlgorithm = algorithm; + } + + public String getRequestEncryptEncValue() { + return requestEncryptEncValue; + } + + public void setRequestEncryptEncValue (String enc) { + this.requestEncryptEncValue = enc; + } } 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..3f775251cb0 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 6b51d980d97..7ca0ab9a4fa 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,9 @@ package org.wildfly.security.http.oidc; +import static org.jose4j.jwa.AlgorithmConstraints.ConstraintType.PERMIT; import static org.wildfly.security.http.oidc.ElytronMessages.log; +import static org.wildfly.security.http.oidc.JWTClientCredentialsProvider.loadKeyPairFromKeyStore; 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; @@ -26,10 +28,20 @@ import static org.wildfly.security.http.oidc.Oidc.KC_IDP_HINT; import static org.wildfly.security.http.oidc.Oidc.LOGIN_HINT; import static org.wildfly.security.http.oidc.Oidc.MAX_AGE; +import static org.wildfly.security.http.oidc.Oidc.NONE; 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_TYPE_OAUTH2; +import static org.wildfly.security.http.oidc.Oidc.REQUEST_TYPE_REQUEST; +import static org.wildfly.security.http.oidc.Oidc.REQUEST_TYPE_REQUEST_URI; +import static org.wildfly.security.http.oidc.Oidc.REQUEST_URI; +import static org.wildfly.security.http.oidc.Oidc.RSA_OAEP; +import static org.wildfly.security.http.oidc.Oidc.RSA_OAEP_256; +import static org.wildfly.security.http.oidc.Oidc.RSA1_5; 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; @@ -39,19 +51,36 @@ 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.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.jwk.JsonWebKey; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.lang.JoseException; import org.wildfly.security.http.HttpConstants; /** @@ -180,15 +209,45 @@ 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()); + if (deployment.getAuthenticationRequestFormat().contains(REQUEST_TYPE_OAUTH2)) { + redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri) + .addParameter(STATE, state) + .addParameters(forwardedQueryParams); + } else if (deployment.getAuthenticationRequestFormat().contains(REQUEST_TYPE_REQUEST_URI)) { + if (deployment.getRequestUriParameterSupported()) { + String request = convertToRequestParameter(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + String request_uri = getRequestUri(request); + redirectUriBuilder.addParameter(REQUEST_URI, request_uri); + redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri); + } else { + // send request as usual + redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri) + .addParameter(STATE, state) + .addParameters(forwardedQueryParams); + } + } else if (deployment.getAuthenticationRequestFormat().contains(REQUEST_TYPE_REQUEST)) { + if (deployment.getRequestParameterSupported()) { + // add request objects into request parameter + String request = convertToRequestParameter(redirectUriBuilder, redirectUri, state, forwardedQueryParams); + redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri); + redirectUriBuilder.addParameter(REQUEST, request); + } else { + // send request as usual + redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri) + .addParameter(STATE, state) + .addParameters(forwardedQueryParams); + } + } return redirectUriBuilder.build().toString(); } catch (URISyntaxException e) { throw log.unableToCreateRedirectResponse(e); + } catch (Exception e) { + throw new RuntimeException(e); } } @@ -416,4 +475,104 @@ private static boolean hasScope(String scopeParam, String targetScope) { } return false; } + private String convertToRequestParameter(URIBuilder redirectUriBuilder, String redirectUri, String state, List forwardedQueryParams) throws Exception { + redirectUriBuilder.addParameter(SCOPE, OIDC_SCOPE); + forwardedQueryParams.add(new BasicNameValuePair(STATE, state)); + forwardedQueryParams.add(new BasicNameValuePair(REDIRECT_URI, redirectUri)); + forwardedQueryParams.add(new BasicNameValuePair(RESPONSE_TYPE, CODE)); + forwardedQueryParams.add(new BasicNameValuePair(CLIENT_ID, deployment.getResourceName())); + + JwtClaims jwtClaims = new JwtClaims(); + jwtClaims.setIssuer(deployment.getIssuerUrl()); + jwtClaims.setAudience(deployment.getProviderUrl()); + for ( NameValuePair parameter: forwardedQueryParams) { + jwtClaims.setClaim(parameter.getName(), parameter.getValue()); + } + + // sign JWT first before encrypting + JsonWebSignature signedRequest = signRequest(jwtClaims); + + // Encrypting optional + if (deployment.getRequestEncryptAlgorithm() != null && !deployment.getRequestEncryptAlgorithm().isEmpty() && + deployment.getRequestEncryptEncValue() != null && !deployment.getRequestEncryptEncValue().isEmpty()) { + return encryptRequest(signedRequest).getCompactSerialization(); + } else { + return signedRequest.getCompactSerialization(); + } + } + + public KeyPair getkeyPair() { + if (deployment.getClientKeyStore().contains(PROTOCOL_CLASSPATH)) { + deployment.setClientKeyStore(deployment.getClientKeyStore().replace(PROTOCOL_CLASSPATH, Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource("")).getPath())); + } + if (!deployment.getRequestSignatureAlgorithm().equals(NONE) && deployment.getClientKeyStore() == null){ + throw new RuntimeException("Invalid keystore configuration for signing Request Objects."); + } else { + return loadKeyPairFromKeyStore(deployment.getClientKeyStore(), + deployment.getClientKeyStorePassword(), deployment.getClientKeyPassword(), + deployment.getClientKeyAlias(), deployment.getClientKeystoreType()); + } + } + + public JsonWebSignature signRequest(JwtClaims jwtClaims) throws Exception { + JsonWebSignature jsonWebSignature = new JsonWebSignature(); + jsonWebSignature.setPayload(jwtClaims.toJson()); + + if (deployment.getRequestSignatureAlgorithm().contains(NONE)) { + jsonWebSignature.setAlgorithmConstraints(AlgorithmConstraints.NO_CONSTRAINTS); + jsonWebSignature.setAlgorithmHeaderValue(NONE); + } else { + KeyPair keyPair = getkeyPair(); + jsonWebSignature.setKey(keyPair.getPrivate()); + jsonWebSignature.setAlgorithmConstraints(new AlgorithmConstraints(PERMIT, deployment.getRequestSignatureAlgorithm())); + jsonWebSignature.setAlgorithmHeaderValue(deployment.getRequestSignatureAlgorithm()); + } + jsonWebSignature.sign(); + return jsonWebSignature; + } + + private JsonWebEncryption encryptRequest(JsonWebSignature signedRequest) throws JoseException { + JsonWebEncryption jsonEncryption = new JsonWebEncryption(); + jsonEncryption.setPayload(signedRequest.getCompactSerialization()); + jsonEncryption.setAlgorithmConstraints(new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, deployment.getRequestEncryptAlgorithm(), deployment.getRequestEncryptEncValue())); + jsonEncryption.setAlgorithmHeaderValue(deployment.getRequestEncryptAlgorithm()); + jsonEncryption.setEncryptionMethodHeaderParameter(deployment.getRequestEncryptEncValue()); + List jwkList = deployment.getrealmKeySet().getJsonWebKeys(); + for (JsonWebKey jwk : jwkList) { + if (deployment.getRequestEncryptAlgorithm().contains(RSA_OAEP) || deployment.getRequestEncryptAlgorithm().contains(RSA_OAEP_256) || deployment.getRequestEncryptAlgorithm().contains(RSA1_5)) { + if (jwk.getAlgorithm().equals(RSA_OAEP)) { //JWT's for keycloak are to be encrypted with realm public keys + jsonEncryption.setKey(jwk.getKey()); + break; + } + } + } + return jsonEncryption; + } + + private String getRequestUri(String request) throws Exception { + 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 new Exception(response.getStatusLine().getReasonPhrase()); + } + 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..4a097f0bd86 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,21 +18,46 @@ package org.wildfly.security.http.oidc; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; +import java.util.Date; import java.util.List; +import java.util.Objects; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.jose4j.lang.JoseException; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; - import io.restassured.RestAssured; +import static org.wildfly.security.http.oidc.Oidc.KEYSTORE_PASS; + /** * Keycloak configuration for testing. * @@ -47,6 +72,11 @@ 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 String KEYSTORE_CLASSPATH; /** * Configure RealmRepresentation as follows: @@ -60,14 +90,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 JoseException, GeneralSecurityException, IOException, OperatorCreationException { 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 JoseException, GeneralSecurityException, IOException, OperatorCreationException { return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp, directAccessGrantEnabled, bearerOnlyClientId, corsClientId); } @@ -102,14 +132,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 JoseException, GeneralSecurityException, IOException, OperatorCreationException { 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 GeneralSecurityException, IOException, OperatorCreationException { RealmRepresentation realm = new RealmRepresentation(); realm.setRealm(name); @@ -140,15 +170,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 GeneralSecurityException, IOException, OperatorCreationException { 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 GeneralSecurityException, IOException, OperatorCreationException { ClientRepresentation client = new ClientRepresentation(); client.setClientId(clientId); client.setPublicClient(false); @@ -157,12 +188,60 @@ 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(); + String rsaCert = generateKeyStoreFileAndGetCertificate("Rsa", 2048, KEYSTORE_CLASSPATH + RSA_KEYSTORE_FILE_NAME, "SHA256WITHRSA"); + client.getAttributes().put("jwt.credential.certificate", rsaCert); return client; } + static String generateKeyStoreFileAndGetCertificate(String algorithm, int keySize, String keystorePath, String certSignAlg) throws GeneralSecurityException, IOException, OperatorCreationException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm); + keyGen.initialize(keySize); + KeyPair keys = keyGen.generateKeyPair(); + X509Certificate cert = createCertificate(keys, certSignAlg); + KeyStore keystore = createKeyStore(cert, keys.getPrivate()); + try (FileOutputStream fileOutputStream = new FileOutputStream(keystorePath)) { + keystore.store(fileOutputStream, KEYSTORE_PASS.toCharArray()); + } + + KeyStoreConfig keyStoreConfig = new KeyStoreConfig(); + keyStoreConfig.setKeyAlias(KEYSTORE_ALIAS); + keyStoreConfig.setStorePassword(KEYSTORE_PASS); + keyStoreConfig.setKeyPassword(KEYSTORE_PASS); + keyStoreConfig.setRealmCertificate(true); + return Base64.getEncoder().encodeToString(cert.getEncoded()); + } + + private static KeyStore createKeyStore(X509Certificate certificate, PrivateKey privateKey) throws IOException, GeneralSecurityException { + KeyStore keyStore = createEmptyKeyStore(); + keyStore.setCertificateEntry("jwtKeystore", certificate); + keyStore.setKeyEntry("jwtKeystore", privateKey, "password".toCharArray(), new Certificate[]{certificate}); + return keyStore; + } + + private static KeyStore createEmptyKeyStore() throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null,null); + return keyStore; + } + + private static X509Certificate createCertificate(KeyPair keyPair, String certSignAlg) throws GeneralSecurityException, OperatorCreationException { + X500Name subject = new X500Name("cn=localhost"); + Date startDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000); //1 day before now + Date endDate = new Date(System.currentTimeMillis() + (long) 2 * 365 * 24 * 60 * 60 * 1000); //2 years from now + X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(subject, BigInteger.valueOf(System.currentTimeMillis()), startDate, endDate, subject, keyPair.getPublic()); + final ContentSigner contentSigner = new JcaContentSignerBuilder(certSignAlg).build(keyPair.getPrivate()); + + return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()) + .getCertificate(certGen.build(contentSigner)); + } + private static ClientRepresentation createBearerOnlyClient(String clientId) { ClientRepresentation client = new ClientRepresentation(); client.setClientId(clientId); 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 84132472d1c..c951acff3f7 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 @@ -21,7 +21,24 @@ 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.KeycloakConfiguration.KEYSTORE_CLASSPATH; 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.REQUEST_TYPE_OAUTH2; +import static org.wildfly.security.http.oidc.Oidc.REQUEST_TYPE_REQUEST; +import static org.wildfly.security.http.oidc.Oidc.REQUEST_TYPE_REQUEST_URI; +import static org.wildfly.security.http.oidc.Oidc.NONE; +import static org.wildfly.security.http.oidc.Oidc.RSA1_5; +import static org.wildfly.security.http.oidc.Oidc.RSA_OAEP; +import static org.wildfly.security.http.oidc.Oidc.RSA_OAEP_256; +import static org.wildfly.security.http.oidc.Oidc.A128CBC_HS256; +import static org.wildfly.security.http.oidc.Oidc.A192CBC_HS384; +import static org.wildfly.security.http.oidc.Oidc.A256CBC_HS512; +import static org.wildfly.security.http.oidc.Oidc.RS_256; +import static org.wildfly.security.http.oidc.Oidc.RS_512; +import static org.wildfly.security.http.oidc.Oidc.PS_256; +import static org.wildfly.security.http.oidc.Oidc.KEYSTORE_PASS; +import static org.wildfly.security.http.oidc.Oidc.JKS_KEYSTORE_TYPE; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -30,6 +47,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 +59,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. @@ -162,8 +179,90 @@ public void testTokenSignatureAlgorithm() throws Exception { true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); } + @Test + public void testOpenIDWithOauth2Request() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithRequestParameter(REQUEST_TYPE_OAUTH2, "", "", ""), 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, 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, 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, RS_512, RSA_OAEP, A192CBC_HS384, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, JKS_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, PS_256, RSA_OAEP_256, A256CBC_HS512, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, JKS_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, "RSNULL", RSA1_5, A256CBC_HS512, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, JKS_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, RS_256, "", "", KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, JKS_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, PS_256, "", "", KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, JKS_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, RSA_OAEP, NONE, NONE, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, JKS_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, NONE, "", ""), 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, RS_256, RSA_OAEP_256, A256CBC_HS512, KEYSTORE_CLASSPATH + KeycloakConfiguration.RSA_KEYSTORE_FILE_NAME, KeycloakConfiguration.KEYSTORE_ALIAS, JKS_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); @@ -175,10 +274,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("org.jose4j.lang.InvalidAlgorithmException")); + 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)) { + assertTrue(response.getFirstResponseHeaderValue("Location").contains("scope=" + OIDC_SCOPE)); + assertTrue(response.getFirstResponseHeaderValue("Location").contains("request_uri=")); + } else if (oidcClientConfiguration.getAuthenticationRequestFormat().contains(REQUEST_TYPE_REQUEST)) { + assertTrue(response.getFirstResponseHeaderValue("Location").contains("scope=" + OIDC_SCOPE)); + assertTrue(response.getFirstResponseHeaderValue("Location").contains("request=")); + } if (loginToKeycloak) { client.setDispatcher(createAppResponse(mechanism, expectedDispatcherStatusCode, expectedLocation, clientPageText)); @@ -290,4 +408,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" + + " \"client-keystore-file\" : \"" + keyStorePath + "\",\n" + + " \"client-keystore-type\" : \"" + keyStoreType + "\",\n" + + " \"client-keystore-password\" : \"" + KEYSTORE_PASS + "\",\n" + + " \"client-key-password\" : \"" + KEYSTORE_PASS + "\",\n" + + " \"client-key-alias\" : \"" + alias + "\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } } +