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 1 commit into
base: 2.x
Choose a base branch
from

Conversation

Copy link
Contributor

@Skyllarr Skyllarr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PrarthonaPaul Looks good! I've just had a quick look at it and added some minor comments, I'll look at it more and its affiliated wildfly PR later this week

<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>3.1.0.Final</version>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PrarthonaPaul Just a minor, please put a version as a property to the properties tag in the parent pom

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Skyllarr
I am not sure how to do this. Do you have an example of this being done for another dependency?
Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PrarthonaPaul In the parent pom.xml we have properties tag where you can find versions of libraries, see here. Place your keycloak-services dependency there. Then, as with other libraries in that parent pom.xml, place your dependency to the dependencyManagement tag the same way as other deps are. Then you can just use this dependency in child modules without having to specify a version. So in http/oidc/pom.xml you will then be able to place it without a version tag

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!
I have updated the pom files to reflect this.

@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PrarthonaPaul Just a minor, can this method be protected instead of public?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I have changed it to protected.

@@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PrarthonaPaul Just a minor, should the "client-keystore" stay here as possible configuration property because of backwards compatibility? Users could have been using it before?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh! Thanks for noticing it! I have fixed it! Also changed the class to add a new variable called clientKeyStoreFile that's different from clientKeystore along with a getter and setter for that.

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.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PrarthonaPaul Just a minor, please put this exception into the ElytronMessages class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added! Thanks

}

private static KeyStore createEmptyKeyStore() throws IOException, GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance("JKS");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a minor, let's not forget to change this from JKS to PKCS12

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!
Thanks for going though the code!

Comment on lines 224 to 240
} else {
// send request as usual
redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri)
.addParameter(STATE, state)
.addParameters(forwardedQueryParams);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently if request or request_uri is not supported, then the authentication is sent as usual.
However, we can change it so that if it is not supported, then we throw a runtime error.
Another thing we can do, is keep it as is, but add a log message saying "Request/request_uri is not supported by the OpenID provider. Request is sent using the Oauth2 format"

Copy link
Contributor

@Skyllarr Skyllarr Oct 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PrarthonaPaul Thinking.. this should be discussed in the proposal, I see you asked me this there as well. If the authentication is sent as oauth despite a different configuration it has to be logged as a minimum.

I think we should log this and then document that the request and the request_uri methods are used only if available with the OpenID provider, if not available a warning message will be logged. I will write this on the proposal as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have fixed the elytron code and the docs to reflect it.
Will change it in the proposal as well. Thanks!

<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>3.1.0.Final</version>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Skyllarr
I am not sure how to do this. Do you have an example of this being done for another dependency?
Thanks!

@@ -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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I have changed it to protected.

@@ -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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh! Thanks for noticing it! I have fixed it! Also changed the class to add a new variable called clientKeyStoreFile that's different from clientKeystore along with a getter and setter for that.

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.");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added! Thanks

}

private static KeyStore createEmptyKeyStore() throws IOException, GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance("JKS");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!
Thanks for going though the code!

Comment on lines 531 to 536
} else if (deployment.getRequestSignatureAlgorithm().contains(HS_256)
|| deployment.getRequestSignatureAlgorithm().contains(HS_384)
|| deployment.getRequestSignatureAlgorithm().contains(HS_512)) { //signed with symmetric key
jsonWebSignature.setAlgorithmHeaderValue(deployment.getRequestSignatureAlgorithm());
Key key = new HmacKey(deployment.getResourceCredentials().get("secret").toString().getBytes(StandardCharsets.UTF_8)); //the client secret is a shared secret between the server and the client
jsonWebSignature.setDoKeyValidation(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, when the user wants to sign using symmetric keys, we are signing using the client secret. However, I am not sure what to do when they use JWT as client credentials instead of client secret.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PrarthonaPaul Can they use JWT as a client credential? Can you please describe the background for the situation you are asking about?

in the proposal you mentioned:

In order to sign the jwt using an algorithm other than none, the user must specify the KeyPair used.

So I thought the keypair can be used in all situations for signing but you might be asking about something else

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry.
According to this doc: https://www.keycloak.org/docs/latest/securing_apps/#_client_authentication_adapter
we can either set up the client credentials to be just the client secret, which is a just a hash/SHA (like 19666a4f-32dd-4049-b082-684c74115f28).
But you can also configure it using a JWT. But it is not related to the request RFE.
This is what the options are for specifying the client credentials:
image

Now the reason I am using the client secret to sign a JWT for my RFE is because as we found out HS keys are symmetric. And from what I have seen instead of creating a certificate, we create a secret that is shared between the client. For Keycloak, the client secret is something that is shared between the client and the server.

But what I am wondering is what happens when the user configures the client credentials as a JWT or X509 certificate?

@@ -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) {
protected static KeyPair loadKeyPairFromKeyStore(String keyStoreFile, String storePassword, String keyPassword, String keyAlias, String keyStoreType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a generic helper method that will also be used in OidcRequestAuthenticator, it would be better to move this method to either an existing utility class or introduce a new utility class and include this method there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.
Does this class: https://github.com/wildfly-security/wildfly-elytron/blob/2.x/http/oidc/src/main/java/org/wildfly/security/http/oidc/ClientCredentialsProviderUtils.java look like a good one to move this function to?
If not, then we can create another utility class called JWTClientCredentialsProviderUtils.java or something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the method will also be used for signing the authentication request in addition to being used for client credentials handling, I think we could create another utility class but try to give it a more generic name (e.g., JWTSigningUtils or something along those lines).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay! Sounds good! Thanks :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done!

@@ -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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something that's used for tests? It shouldn't be added to the constants class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved relevant constants to KeycloakConfiguration class.

@@ -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 PKCS12_KEYSTORE_TYPE = "PKCS12";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, this looks like something that's only needed for tests, it shouldn't be in the Oidc constants class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to keycloakConfiguration class

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/#";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check all the new constants introduced in this class, it looks like these were meant for tests purposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

@@ -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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this class already contains some constants with similar names and also makes use of the AlgorithmIdentifiers class.

These newly added constants appear to be specific to the Keycloak OpenID provider.

Are these really needed?

Can we get the allowed values from discovery?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!
I am using AlgorithmIdentifiers class and throwing an exception if the algorithm is not supported.

public static final String PS_512 = "PS512";

/* Accepted Request Object Encrypting Algorithms for KeyCloak*/
public static final String RSA_OAEP = "RSA-OAEP";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

public static final String RSA1_5 = "RSA1_5";

/* Accepted Request Object Encryption Methods for KeyCloak*/
public static final String A256GCM = "A256GCM";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, can we get these accepted values through discovery instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like most of these, I use for testing, and some of them I don't use at all.
So, I can remove/move those to a test Class, like KeycloakConfiguration. For some of the others, I am using them in the actual implementation. So, I can replace them using AlgorithmIdentifiers.
I can get them using Discovery and I think I do get them. But I don't think I use them yet.

@@ -223,6 +249,15 @@ protected void resolveUrls() {
tokenUrl = config.getTokenEndpoint();
logoutUrl = config.getLogoutEndpoint();
jwksUrl = config.getJwksUri();
authorizationEndpoint = config.getAuthorizationEndpoint();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which endpoint is this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see something that corresponds to this added in OidcProviderMetadata, is this necessary? Or is it just the pushedAuthorizationRequestEndpoint that's needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I do see authorization_endpoint already referenced in the existing OidcProviderMetadata code. But just to understand better, is the authorizationEndpoint variable that's being introduced here used anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has this endpoint: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth/device
I don't think I use it tho. So I can get rid of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

requestObjectEncryptionEncValuesSupported = config.getRequestObjectEncryptionEncValuesSupported();
requestObjectEncryptionAlgValuesSupported = config.getRequestObjectEncryptionAlgValuesSupported();
requestUriParameterSupported = config.getRequestUriParameterSupported();
realmKeySet = createrealmKeySet();
Copy link
Contributor

@fjuma fjuma Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to create the realm key set here. It would be better to do this when we know we actually need to make use of it (similar to JWKPublicKeyLocator where we use the jwks url when we know we need to).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to OidcPublicKeyExtractor

@@ -237,6 +272,26 @@ protected void resolveUrls() {
}
}

private JsonWebKeySet createrealmKeySet() throws Exception{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"realm" is terminology specific to Keycloak so I don't think we should use it in the method name here.

This method is meant to obtain the encryption keys from the OpenID provider, right? We should rename this method to reflect that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the function from this class and moved it to a utility class. New function is called extractPublicKeySet

@@ -237,6 +272,26 @@ protected void resolveUrls() {
}
}

private JsonWebKeySet createrealmKeySet() throws Exception{
HttpGet request = new HttpGet(jwksUrl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make use of the Oidc.sendJsonHttpRequest method here to simplify the logic below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried doing this and kept getting a class mismatch for the JsonSerialization.readValue() function.
I tried this for the JsonWebKeySet, String, and StringBuilder classes.
When I was initially writing this function, I was also trying to use JsonSerialization.readValue() and did not succeed. Which is why I had to do this workaround.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be an example in JWKPublicKeyLocator where we call sendJsonHttpRequest and store the result in a JsonWebKeySet. Did you take a look there to see how that differs from what you were trying to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what the difference is.
The JWKPublicKeyLocator class uses org.wildfly.security.jose.jwk.JsonWebKeySet and I am using the Jose4j one, which is why the parsing failed before.
I have switched it to the wildfly.security one now and it works.
Thank you!

return authenticationRequestFormat;
}

public void setAuthenticationRequestFormat(String requestObjectType ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/requestObjectType/authenticationRequestFormat ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

return requestSignatureAlgorithm;
}

public void setRequestSignatureAlgorithm(String algorithm) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/algorithm/requestSignatureAlgorithm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

return requestEncryptAlgorithm;
}

public void setRequestEncryptAlgorithm(String algorithm) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/algorithm/requestEncryptionAlgorithm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

return requestEncryptEncValue;
}

public void setRequestEncryptEncValue (String enc) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra white space before bracket

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

this.requestSignatureAlgorithm = algorithm;
}

public String getRequestEncryptAlgorithm() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be consistent with the naming pattern for the signing case, there we used getRequestObjectSigningAlgValuesSupported so we should follow a similar pattern here and below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

this.requestEncryptEncValue = enc;
}

public String getRealmKey () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't use "realm" here. We should rename this to better explain what this returns.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't using these functions, so I removed anything related to realmKey that I added.

this.clientKeyAlias = alias;
}

public JsonWebKeySet getrealmKeySet() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that method names should use camel case, i.e., style wise, getrealm should be getRealm.

However, we should rename this to not mention realm as described in comments above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to utility class and renamed.

redirectUriBuilder.addParameter(REDIRECT_URI, redirectUri)
.addParameter(STATE, state)
.addParameters(forwardedQueryParams);
log.info("The OpenID provider does not support the request parameter. Sending the request using OAuth2 format.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should make use of ElytronMessages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

@@ -0,0 +1,44 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2020 Red Hat, Inc., and individual contributors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2024?

@PrarthonaPaul PrarthonaPaul force-pushed the ELY-2584 branch 3 times, most recently from df298ec to 6ad38e0 Compare April 4, 2024 17:01
@PrarthonaPaul
Copy link
Contributor Author

Thanks, @fjuma and @darranl for your review.
I have addressed your comments on this PR.

Copy link
Contributor

@fjuma fjuma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks very much for the updates @PrarthonaPaul! I've added some comments.

*
* @author <a href="mailto:prpaul@redhat.com">Prarthona Paul</a>
* */
class JWKPublicKeySetExtractor implements PublicKeyLocator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering if we could rename this so the difference between this one and the existing JWKPublicKeyLocator class is more obvious. Maybe JWKEncPublicKeyLocator? WDYT?

We should also update the javadoc accordingly to indicate that this locator dynamically obtains the public key to use for encryption.

private Map<String, PublicKey> currentKeys = new ConcurrentHashMap<>();

private volatile int lastRequestTime = 0;
public JWKPublicKeySetExtractor() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be removed (since it's just the default no-arg constructor).

}

private void sendRequest(String kid, OidcClientConfiguration config) {
HttpGet request = new HttpGet(config.getJwksUrl());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a trace log message at the beginning like the one here:

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the kid that will end up being passed in is "enc" but am wondering if that's something that we can rely on. Shouldn't we be filtering for encryption keys using something like JsonWebKeySetUtil.getKeysForUse(jwks, JWK.Use.ENC)? The kid can be any value right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this function is only called within the elytron Oidc code, and it is not dependent on any user input, could there be cases where kid would not be JWK.Use.ENC?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized I misread this a bit. The kid stands for key identifier and this is something that would be specified by the OpenID provider in their JWKS, we don't have control over this. So when I saw the kid being set to enc in the other file, I assumed that we were going to search for a key identifier with value enc. But here, I see that the enc value is actually being used to check the getPublicKeyUse() value which is what the method I suggested to use makes use of. Because this public key locator class is going to be used specifically for locating the encryption key, it doesn't need to be a parameter that's passed in. Take a look at the existing locator implementation to see how we make use of the JsonWebKeySetUtil.getKeysForUse method there, we can do something similar here. The main difference is just the JWK.Use value that is used. Here we'll use JWK.Use.ENC.

* @author <a href="mailto:prpaul@redhat.com">Prarthona Paul</a>
*/

public class JWTSigningUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public?

return authenticationRequestFormat;
}

public void setAuthenticationRequestFormat(String authenticationRequestFormat ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor, extra space before the closing bracket.

this.requestObjectSignatureAlgorithm = requestObjectSignatureAlgorithm;
}

public String getRequestObjectEncryptAlgorithm() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Encrypt/Encryption here and below?

try {
response = deployment.getClient().execute(parRequest);
} catch (Exception e) {
throw log.failedToConnectToOidcProvider(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this log message more specific? e.g., it would be good to indicate there was an issue with sending the request to the OpenID provider's pushed authorization request endpoint.

return requestObjectSigningKeyStorePass;
}

public void setRequestObjectSigningKeyStorePassword(String pass) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/pass/requestObjectSigningKeyStorePass

return requestObjectSigningKeyPass;
}

public void setRequestObjectSigningKeyPassword(String pass) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/pass/requestObjectSigningKeyPassword

s/requestObjectSigningKeyPass/requestObjectSigningKeyPassword

return requestObjectSigningKeyStoreType;
}

public void setRequestObjectSigningKeyStoreType(String type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/type/requestObjectSigningKeyStoreType

return requestObjectSigningKeyAlias;
}

public void setRequestObjectSigningKeyAlias(String alias) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/alias/requestObjectSigningKeyAlias

return pushedAuthorizationRequestEndpoint;
}

public void setPushedAuthorizationRequestEndpoint(String url) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/url/pushedAuthorizationRequestEndpoint

@@ -126,6 +132,17 @@ public enum RelativeUrlsUsed {
protected boolean verifyTokenAudience = false;

protected String tokenSignatureAlgorithm = DEFAULT_TOKEN_SIGNATURE_ALGORITHM;
protected String authenticationRequestFormat;
protected String requestObjectSignatureAlgorithm;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good to be consistent with the JSON config option that will be used to configure this. It looks like we use signing there as opposed to signature, right? If so, we should update this.

…quest should include request and request_uri parameters
this.requestObjectSignatureAlgorithm = requestObjectSignatureAlgorithm;
}

public String getRequestEncryptAlgorithm() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be getRequestObjectEncryptionAlgorithm?

}

@Override
public String getRequestObjectSignatureAlgorithm() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signing?

}

@Override
public void setRequestObjectSignatureAlgorithm(String requestSignature) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signing?

protected String authenticationRequestFormat;

@JsonProperty("request-object-signing-algorithm")
protected String requestObjectSignatureAlgorithm;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signing?

protected String requestObjectSignatureAlgorithm;

@JsonProperty("request-object-encryption-algorithm")
protected String requestObjectEncryptAlgorithm;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encryption?

protected String requestObjectEncryptAlgorithm;

@JsonProperty("request-object-content-encryption-algorithm")
protected String requestObjectContentEncryptionMethod;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/requestObjectContentEncryptionMethod/requestObjectContentEncryptionAlgorithm?

It's good for the variable name to be consistent with the configuration option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just looking closer in the OIDC spec and I don't see references to "request object content encryption methods". Am just wondering if from a usability perspective, it will be confusing to determine what values should be specified for request-object-encryption-algorithm vs. request-object-content-encryption-algorithm. Do you recall the rationale for these names?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was just reviewing the analysis doc and see that we picked these names to match the Keycloak options in their console. However, it doesn't look like the OIDC spec uses the same terminology.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could use something like request-object-encryption-enc-value and request-object-encryption-alg-value to more closely align with the spec? WDYT?

return requestObjectSigningKeyStoreType;
}

public void setRequestObjectSigningKeyStoreType(String type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/type/requestObjectSigningKeyStoreType

return requestObjectSigningKeyAlias;
}

public void setRequestObjectSigningKeyAlias(String alias) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

return requestObjectEncryptAlgorithm;
}

public void setRequestObjectEncryptAlgorithm(String requestObjectSignatureAlgorithm) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encryption?

this.requestObjectEncryptAlgorithm = requestObjectSignatureAlgorithm;
}

public String getRequestObjectContentEncryptionMethod() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be getRequestObjectContentEncryptionAlgorithm?

return requestObjectContentEncryptionMethod;
}

public void setRequestObjectContentEncryptionMethod (String requestObjectContentEncryptionMethod) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@@ -461,4 +544,124 @@ private void addScopes(String scopes, Set<String> allScopes) {
allScopes.addAll(Arrays.asList(scopes.split("\\s+")));
}
}

private String convertToRequestParameter(URIBuilder redirectUriBuilder, String redirectUri, String state, List<NameValuePair> forwardedQueryParams) throws JoseException, IOException {
redirectUriBuilder.addParameter(SCOPE, OIDC_SCOPE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if additional scopes have been configured?


JwtClaims jwtClaims = new JwtClaims();
jwtClaims.setIssuer(deployment.getIssuerUrl());
jwtClaims.setAudience(deployment.getProviderUrl());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the spec, "the aud value SHOULD be or include the OP's Issuer Identifier URL". So getIssuerUrl should be used here.

Copy link
Contributor

@fjuma fjuma May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the iss and aud claims should be included if the request object will be signed.

redirectUriBuilder.addParameter(SCOPE, OIDC_SCOPE);

JwtClaims jwtClaims = new JwtClaims();
jwtClaims.setIssuer(deployment.getIssuerUrl());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the spec, "the iss value SHOULD be the Client ID of the RP, unless it was signed by a different party than the RP". So here, we can use getResourceName.

JsonWebSignature signedRequest = signRequest(jwtClaims);

// Encrypting optional
if (deployment.getRequestObjectEncryptionAlgorithm() != null && !deployment.getRequestObjectEncryptionAlgorithm().isEmpty() &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be the empty string? Is that a valid value?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants