Skip to content

Commit

Permalink
Merge pull request #1984 from PrarthonaPaul/ELY-2584
Browse files Browse the repository at this point in the history
[ELY-2584] Add the ability to specify that the OIDC Authentication Request should include request and request_uri parameters
  • Loading branch information
fjuma authored Jun 26, 2024
2 parents a1f68a3 + 8b0c237 commit 1c6246a
Show file tree
Hide file tree
Showing 17 changed files with 1,202 additions and 88 deletions.
16 changes: 16 additions & 0 deletions http/oidc/pom.xml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

package org.wildfly.security.http.oidc;

import static org.jboss.logging.annotations.Message.NONE;
import static org.jboss.logging.Logger.Level.DEBUG;
import static org.jboss.logging.Logger.Level.ERROR;
import static org.jboss.logging.Logger.Level.WARN;
import static org.jboss.logging.annotations.Message.NONE;

import java.io.IOException;

Expand Down Expand Up @@ -238,5 +238,45 @@ interface ElytronMessages extends BasicLogger {
@Message(id = 23057, value = "principal-attribute '%s' claim does not exist, falling back to 'sub'")
void principalAttributeClaimDoesNotExist(String principalAttributeClaim);

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

@Message(id = 23059, value = "The signature algorithm specified is not supported by the OpenID Provider.")
IOException invalidRequestObjectSignatureAlgorithm();

@Message(id = 23060, value = "The encryption algorithm specified is not supported by the OpenID Provider.")
IOException invalidRequestObjectEncryptionAlgorithm();

@Message(id = 23061, value = "The content encryption algorithm (enc value) specified is not supported by the OpenID Provider.")
IOException invalidRequestObjectEncryptionEncValue();

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

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

@Message(id = 23064, value = "Failed to create the authentication request using the request parameter.")
RuntimeException unableToCreateRequestWithRequestParameter(@Cause Exception cause);

@Message(id = 23065, value = "Failed to create the authentication request using the request_uri parameter.")
RuntimeException unableToCreateRequestUriWithRequestParameter(@Cause Exception cause);

@Message (id = 23066, value = "Failed to send a request to the OpenID provider's Pushed Authorization Request endpoint.")
RuntimeException failedToSendPushedAuthorizationRequest(@Cause Exception cause);

@Message(id = 23067, value = "Cannot retrieve the request_uri as the pushed authorization request endpoint is not available for the OpenID provider.")
RuntimeException pushedAuthorizationRequestEndpointNotAvailable();

@LogMessage(level = WARN)
@Message(id = 23068, value = "The request object will be unsigned. This should not be used in a production environment. To sign the request object, for use in a production environment, please specify the request object signing algorithm.")
void unsignedRequestObjectIsUsed();

@Message(id = 23069, value = "The client secret has not been configured. Unable to sign the request object using the client secret.")
RuntimeException clientSecretNotConfigured();

@Message(id = 23070, value = "Authentication request format must be one of the following: oauth2, request, request_uri.")
RuntimeException invalidAuthenticationRequestFormat();
}

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

package org.wildfly.security.http.oidc;

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

import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Map;
import java.util.List;

import org.apache.http.client.methods.HttpGet;
import org.wildfly.security.jose.jwk.JWK;
import org.wildfly.security.jose.jwk.JsonWebKeySet;
import org.wildfly.security.jose.jwk.JsonWebKeySetUtil;

/**
* A public key locator that dynamically obtains the public key used for encryption
* from an OpenID provider by sending a request to the provider's {@code jwks_uri}
* when needed.
*
* @author <a href="mailto:prpaul@redhat.com">Prarthona Paul</a>
* */
class JWKEncPublicKeyLocator implements PublicKeyLocator {
private List<PublicKey> currentKeys = new ArrayList<>();

private volatile int lastRequestTime = 0;

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

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

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

}

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

private PublicKey lookupCachedKey(int publicKeyCacheTtl, int currentTime) {
if (lastRequestTime + publicKeyCacheTtl > currentTime) {
return currentKeys.get(0); // returns the first cached public key
} else {
return null;
}
}

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

private void sendRequest(OidcClientConfiguration config) {
if (log.isTraceEnabled()) {
log.trace("Going to send request to retrieve new set of public keys to encrypt a JWT request for client " + config.getResourceName());
}

HttpGet request = new HttpGet(config.getJwksUrl());
request.addHeader(ACCEPT, JSON_CONTENT_TYPE);
try {
JsonWebKeySet jwks = Oidc.sendJsonHttpRequest(config, request, JsonWebKeySet.class);
Map<String, PublicKey> publicKeys = JsonWebKeySetUtil.getKeysForUse(jwks, JWK.Use.ENC);

if (log.isDebugEnabled()) {
log.debug("Public keys successfully retrieved for client " + config.getResourceName() + ". New kids: " + publicKeys.keySet());
}

// update current keys
currentKeys.clear();
currentKeys.addAll(publicKeys.values());
} catch (OidcException e) {
log.error("Error when sending request to retrieve public keys", e);
}
}
}
Original file line number Diff line number Diff line change
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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2024 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.wildfly.security.http.oidc;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;

import static org.wildfly.security.http.oidc.ElytronMessages.log;
import static org.wildfly.security.http.oidc.Oidc.PROTOCOL_CLASSPATH;

/**
* A utility class to obtain the KeyPair from a keystore file.
*
* @author <a href="mailto:prpaul@redhat.com">Prarthona Paul</a>
*/

class JWTSigningUtils {

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

public static InputStream findFile(String keystoreFile) {
if (keystoreFile.startsWith(PROTOCOL_CLASSPATH)) {
String classPathLocation = keystoreFile.replace(PROTOCOL_CLASSPATH, "");
// try current class classloader first
InputStream is = JWTSigningUtils.class.getClassLoader().getResourceAsStream(classPathLocation);
if (is == null) {
is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation);
}
if (is != null) {
return is;
} else {
throw log.unableToFindKeystoreFile(keystoreFile);
}
} else {
try {
// fallback to file
return new FileInputStream(keystoreFile);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}
24 changes: 24 additions & 0 deletions http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java
Original file line number Diff line number Diff line change
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 @@ -74,6 +75,8 @@ public class Oidc {
public static final String PARTIAL = "partial/";
public static final String PASSWORD = "password";
public static final String PROMPT = "prompt";
public static final String REQUEST = "request";
public static final String REQUEST_URI = "request_uri";
public static final String SCOPE = "scope";
public static final String UI_LOCALES = "ui_locales";
public static final String USERNAME = "username";
Expand Down Expand Up @@ -201,6 +204,27 @@ public enum TokenStore {
COOKIE
}

public enum AuthenticationRequestFormat {
OAUTH2("oauth2"),
REQUEST("request"),
REQUEST_URI("request_uri");

private final String value;

AuthenticationRequestFormat(String value) {
this.value = value;
}

/**
* Get the string value for this authentication format.
*
* @return the string value for this authentication format
*/
public String getValue() {
return value;
}
}

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

0 comments on commit 1c6246a

Please sign in to comment.