Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ELY-2584] Add the ability to specify that the OIDC Authentication Request should include request and request_uri parameters #1984

Open
wants to merge 2 commits into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions http/oidc/pom.xml
Expand Up @@ -128,6 +128,11 @@
<artifactId>keycloak-admin-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.logmanager</groupId>
<artifactId>jboss-logmanager</artifactId>
Expand Down Expand Up @@ -173,6 +178,17 @@
<artifactId>jmockit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-credential-source-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-tests-common</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down
Expand Up @@ -234,5 +234,29 @@ interface ElytronMessages extends BasicLogger {
@Message(id = 23056, value = "No message entity")
IOException noMessageEntity();

@Message(id = 23057, value = "Invalid keystore configuration for signing Request Objects.")
RuntimeException invalidKeyStoreConfiguration();

@Message(id = 23058, value = "The request object signature algorithm specified is not supported by the OpenID Provider.")
RuntimeException invalidRequestObjectSignatureAlgorithm();

@Message(id = 23059, value = "The request object encryption algorithm specified is not supported by the OpenID Provider.")
RuntimeException invalidRequestObjectEncryptionAlgorithm();

@Message(id = 23060, value = "The request object content encryption algorithm specified is not supported by the OpenID Provider.")
RuntimeException invalidRequestObjectContentEncryptionAlgorithm();

@LogMessage(level = WARN)
@Message(id = 23061, value = "The OpenID provider does not support request parameters. Sending the request using OAuth2 format.")
void requestParameterNotSupported();

@Message(id = 23062, value = "Both request object encryption algorithm and request object content encryption algorithm must be configured to encrypt the request object.")
IllegalArgumentException invalidRequestEncryptionAlgorithmConfiguration();

@Message(id = 23063, value = "Failed to create the authentication request with request or request_uri parameter.")
RuntimeException unableToCreateRequestWithRequestParameter();

@Message (id = 23064, value = "Failed to connect with the OpenID provider.")
RuntimeException failedToConnectToOidcProvider(@Cause Exception cause);
}

@@ -0,0 +1,106 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2024 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.wildfly.security.http.oidc;

import static org.apache.http.HttpHeaders.ACCEPT;
import static org.wildfly.security.http.oidc.ElytronMessages.log;
import static org.wildfly.security.http.oidc.Oidc.JSON_CONTENT_TYPE;

import java.security.PublicKey;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.http.client.methods.HttpGet;
import org.wildfly.security.jose.jwk.JWK;
import org.wildfly.security.jose.jwk.JWKParser;
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 <a href="mailto:prpaul@redhat.com">Prarthona Paul</a>
* */
class JWKPublicKeySetExtractor implements PublicKeyLocator {
private Map<String, PublicKey> currentKeys = new ConcurrentHashMap<>();

private volatile int lastRequestTime = 0;
public JWKPublicKeySetExtractor() {
}

@Override
public PublicKey getPublicKey(String kid, OidcClientConfiguration config) {
int minTimeBetweenRequests = config.getMinTimeBetweenJwksRequests();
int publicKeyCacheTtl = config.getPublicKeyCacheTtl();
int currentTime = getCurrentTime();

PublicKey publicKey = lookupCachedKey(publicKeyCacheTtl, currentTime, kid);
if (publicKey != null) {
return publicKey;
}

synchronized (this) {
currentTime = getCurrentTime();
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
sendRequest(kid, config);
lastRequestTime = currentTime;
} else {
log.debug("Won't send request to jwks url. Last request time was " + lastRequestTime);
}
return lookupCachedKey(publicKeyCacheTtl, currentTime, kid);
}

}

@Override
public void reset(OidcClientConfiguration config) {
synchronized (this) {
sendRequest("enc", config);
lastRequestTime = getCurrentTime();
}
}

private PublicKey lookupCachedKey(int publicKeyCacheTtl, int currentTime, String kid) {
if (lastRequestTime + publicKeyCacheTtl > currentTime && kid != null) {
return currentKeys.get(kid);
} else {
return null;
}
}

private static int getCurrentTime() {
return (int) (System.currentTimeMillis() / 1000);
}

private void sendRequest(String kid, OidcClientConfiguration config) {
HttpGet request = new HttpGet(config.getJwksUrl());
request.addHeader(ACCEPT, JSON_CONTENT_TYPE);
try {
JWK[] jwkList = Oidc.sendJsonHttpRequest(config, request, JsonWebKeySet.class).getKeys();
for (JWK jwk : jwkList) {
if (jwk.getPublicKeyUse().equals(kid)) { //JWTs are to be encrypted with public keys shared by the OpenID provider and decrypted by the private key they hold
currentKeys.clear();
currentKeys.put(kid, new JWKParser(jwk).toPublicKey());
}
}
} catch (OidcException e) {
log.error("Error when sending request to retrieve public keys", e);
}
}
}
Expand Up @@ -19,18 +19,13 @@
package org.wildfly.security.http.oidc;

import static org.wildfly.security.http.oidc.ElytronMessages.log;
import static org.wildfly.security.http.oidc.JWTSigningUtils.loadKeyPairFromKeyStore;
import static org.wildfly.security.http.oidc.Oidc.CLIENT_ASSERTION;
import static org.wildfly.security.http.oidc.Oidc.CLIENT_ASSERTION_TYPE;
import static org.wildfly.security.http.oidc.Oidc.CLIENT_ASSERTION_TYPE_JWT;
import static org.wildfly.security.http.oidc.Oidc.PROTOCOL_CLASSPATH;
import static org.wildfly.security.http.oidc.Oidc.asInt;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
Expand Down Expand Up @@ -155,43 +150,4 @@ protected JwtClaims createRequestToken(String clientId, String tokenUrl) {
jwtClaims.setExpirationTime(exp);
return jwtClaims;
}

private static KeyPair loadKeyPairFromKeyStore(String keyStoreFile, String storePassword, String keyPassword, String keyAlias, String keyStoreType) {
InputStream stream = findFile(keyStoreFile);
try {
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(stream, storePassword.toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
if (privateKey == null) {
log.unableToLoadKeyWithAlias(keyAlias);
}
PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();
return new KeyPair(publicKey, privateKey);
} catch (Exception e) {
throw log.unableToLoadPrivateKey(e);
}
}

private static InputStream findFile(String keystoreFile) {
if (keystoreFile.startsWith(PROTOCOL_CLASSPATH)) {
String classPathLocation = keystoreFile.replace(PROTOCOL_CLASSPATH, "");
// try current class classloader first
InputStream is = JWTClientCredentialsProvider.class.getClassLoader().getResourceAsStream(classPathLocation);
if (is == null) {
is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation);
}
if (is != null) {
return is;
} else {
throw log.unableToFindKeystoreFile(keystoreFile);
}
} else {
try {
// fallback to file
return new FileInputStream(keystoreFile);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}
@@ -0,0 +1,79 @@
/*
* 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 <a href="mailto:prpaul@redhat.com">Prarthona Paul</a>
*/

public class JWTSigningUtils {
private JWTSigningUtils() {}

public static KeyPair loadKeyPairFromKeyStore(String keyStoreFile, String storePassword, String keyPassword, String keyAlias, String keyStoreType) {
InputStream stream = findFile(keyStoreFile);
try {
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(stream, storePassword.toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
if (privateKey == null) {
throw log.unableToLoadKeyWithAlias(keyAlias);
}
PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();
return new KeyPair(publicKey, privateKey);
} catch (Exception e) {
throw log.unableToLoadPrivateKey(e);
}
}

public static InputStream findFile(String keystoreFile) {
if (keystoreFile.startsWith(PROTOCOL_CLASSPATH)) {
String classPathLocation = keystoreFile.replace(PROTOCOL_CLASSPATH, "");
// try current class classloader first
InputStream is = 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);
}
}
}
}
Expand Up @@ -45,6 +45,7 @@
public class Oidc {

public static final String ACCEPT = "Accept";
public static final String AUTHENTICATION_REQUEST_FORMAT = "authentication-request-format";
public static final String OIDC_NAME = "OIDC";
public static final String JSON_CONTENT_TYPE = "application/json";
public static final String HTML_CONTENT_TYPE = "text/html";
Expand Down Expand Up @@ -73,6 +74,16 @@ 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_OBJECT_CONTENT_ENCRYPTION_ALGORITHM = "request-object-content-encryption-algorithm";
public static final String REQUEST_OBJECT_ENCRYPTION_ALGORITHM = "request-object-encryption-algorithm";
public static final String REQUEST_OBJECT_SIGNING_ALGORITHM = "request-object-signing-algorithm";
public static final String REQUEST_OBJECT_SIGNING_KEYSTORE_FILE = "request-object-signing-keystore-file";
public static final String REQUEST_OBJECT_SIGNING_KEYSTORE_PASSWORD = "request-object-signing-keystore-password";
public static final String REQUEST_OBJECT_SIGNING_KEY_PASSWORD = "request-object-signing-key-password";
public static final String REQUEST_OBJECT_SIGNING_KEY_ALIAS = "request-object-signing-key-alias";
public static final String REQUEST_OBJECT_SIGNING_KEYSTORE_TYPE = "request-object-signing-keystore-type";
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";
Expand Down Expand Up @@ -199,6 +210,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 authentication format.
*
* @return the string value for this authentication format
*/
public String getValue() {
return value;
}
}

public enum ClientCredentialsProviderType {
SECRET("secret"),
JWT("jwt"),
Expand Down