Skip to content

Commit

Permalink
[ELY-2584] Add the ability to specify that the OIDC Authentication Re…
Browse files Browse the repository at this point in the history
…quest should include request and request_uri parameters
  • Loading branch information
PrarthonaPaul committed May 22, 2024
1 parent 0dba5eb commit a52d5a2
Show file tree
Hide file tree
Showing 15 changed files with 1,154 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,9 +18,7 @@

package org.wildfly.security.http.oidc;

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.Logger.Level.*;
import static org.jboss.logging.annotations.Message.NONE;

import java.io.IOException;
Expand Down Expand Up @@ -238,5 +236,29 @@ 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 specified is not supported by the OpenID Provider.")
IOException invalidRequestObjectContentEncryptionAlgorithm();

@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 invalidRequestEncryptionAlgorithmConfiguration();

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

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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 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 Map<String, PublicKey> currentKeys = new ConcurrentHashMap<>();

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, JWK.Use.ENC.toString());
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, JWK.Use.ENC.toString());
}

}

@Override
public void reset(OidcClientConfiguration config) {
synchronized (this) {
sendRequest(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(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 {
JWK[] jwkList = Oidc.sendJsonHttpRequest(config, request, JsonWebKeySet.class).getKeys();
for (JWK jwk : jwkList) {
if (jwk.getPublicKeyUse().toUpperCase().equals(JWK.Use.ENC.toString())) { //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(JWK.Use.ENC.toString(), new JWKParser(jwk).toPublicKey());
}
}
} 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;

/**
* An interface 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 = 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);
}
}
}
}
32 changes: 32 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,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 @@ -201,6 +212,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

0 comments on commit a52d5a2

Please sign in to comment.