-
Notifications
You must be signed in to change notification settings - Fork 6k
NimbusJwtEncoder should simplify constructing with javax.security Keys #17033
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
base: main
Are you sure you want to change the base?
Conversation
6e3841d
to
abddfb3
Compare
Working on test cases and code refactoring |
78ef577
to
6370999
Compare
Helper Methods for Key Generation // 1. Using SecretKey with defaults
SecretKey defaultSecretKey = createSecretKey("HmacSHA256", 256); // Key for default HS256
NimbusJwtEncoder encoderWithSecretDefaults = NimbusJwtEncoder.withSecretKey(defaultSecretKey)
// .macAlgorithm(MacAlgorithm.HS256) // Default is HS256
// .keyId(null) // Default keyId is null
// .jwkSelector(...) // Default selector throws if multiple keys match
.build();
// 2. Using SecretKey with specific algorithm and keyId
SecretKey hs512SecretKey = createSecretKey("HmacSHA512", 512);
String secretKeyId = "my-hmac-key-01";
NimbusJwtEncoder encoderWithSecretCustom = NimbusJwtEncoder.withSecretKey(hs512SecretKey)
.macAlgorithm(MacAlgorithm.HS512) // Specify HS512
.keyId(secretKeyId) // Specify a key ID
.build();
// 3. Using RSA KeyPair with defaults
KeyPair rsaKeyPair = createRsaKeyPair(2048);
NimbusJwtEncoder encoderWithRsaDefaults = NimbusJwtEncoder.withKeyPair(rsaKeyPair)
// .signatureAlgorithm(SignatureAlgorithm.RS256) // Default for RSA is RS256
// .keyId(UUID generated) // Default keyId is a random UUID
// .jwkSelector(...) // Default selector picks first if multiple
.build();
// 4. Using RSA KeyPair with specific algorithm (PSS) and keyId
String rsaKeyId = "my-rsa-key-01";
NimbusJwtEncoder encoderWithRsaCustom = NimbusJwtEncoder.withKeyPair(rsaKeyPair)
.signatureAlgorithm(SignatureAlgorithm.PS256) // Specify PS256
.keyId(rsaKeyId) // Specify a key ID
.build();
// 5. Using EC KeyPair with defaults
KeyPair ecP256KeyPair = createEcKeyPair(Curve.P_256); // Key for default ES256
NimbusJwtEncoder encoderWithEcDefaults = NimbusJwtEncoder.withKeyPair(ecP256KeyPair)
// .signatureAlgorithm(SignatureAlgorithm.ES256) // Default for EC is ES256
// .keyId(UUID generated) // Default keyId is a random UUID
// .jwkSelector(...) // Default selector picks first if multiple
.build();
// 6. Using EC KeyPair with specific algorithm and keyId
KeyPair ecP521KeyPair = createEcKeyPair(Curve.P_521); // Key for ES512
String ecKeyId = "my-ec-key-01";
NimbusJwtEncoder encoderWithEcCustom = NimbusJwtEncoder.withKeyPair(ecP521KeyPair)
.signatureAlgorithm(SignatureAlgorithm.ES512) // Specify ES512
.keyId(ecKeyId) // Specify a key ID
.build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, @surajbh123! This PR will make using NimbusJwtEncoder
much simpler.
I've left my feedback inline. In addition to that, please ensure that your commit only has the signoff in the description and not in the title. Please also ensure that the description has the issue that this PR resolves:
Add NimbusJwtEncoder Builders
Closes gh-16267
Signed-off-by ...
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
*/ | ||
public KeyPairJwtEncoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { | ||
Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null"); | ||
this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's please validate the signature algorithm at this point instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can,t do now as as now we have two separate builder for RSA and EC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please push this down to the subclass, have the subclass provide default and valid algorithms by calling the super constructor, or do not use the abstract class so that you can validate here. From a security perspective, it's valuable to validate as early as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean is that the RSA-specific checking should go in the RSA-specific class. And the EC-specific checking should go in the EC-specific class.
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
Just one more thing, @surajbh123, when you submit, it will save you time in the review process if you run the following before submitting:
|
6370999
to
567e729
Compare
Working on reviews comments |
bb6d143
to
d6154cb
Compare
Closes spring-projectsgh-16267 Signed-off-by: Suraj Bhadrike <[email protected]>
d6154cb
to
52ef97d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the quick turnaround, @surajbh123! This is coming together nicely. I've added some additional inline feedback.
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Outdated
Show resolved
Hide resolved
* with a {@link SecretKey}. | ||
*/ | ||
public NimbusJwtEncoder build() { | ||
this.jwsAlgorithm = (this.jwsAlgorithm != null) ? this.jwsAlgorithm : JWSAlgorithm.HS256; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you are giving this a default value and there isn't a way for the application to set it back to null
, I believe you can remove this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the following case, it may be null.
SecretKey defaultSecretKey = createSecretKey("HmacSHA256", 256); // Key for default HS256
NimbusJwtEncoder encoderWithSecretDefaults = NimbusJwtEncoder.withSecretKey(defaultSecretKey)
// .macAlgorithm(MacAlgorithm.HS256) // Default is HS256
// .keyId(null) // Default keyId is null
// .jwkSelector(...) // Default selector throws if multiple keys match
.build();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please take a look at my suggestions for the constructor so that this value need not be checked in this way.
By setting reasonable defaults in the constructor, readying the JWK
is as simple as:
OctetSequenceKey jwk = this.builder.build()
*/ | ||
public KeyPairJwtEncoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { | ||
Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null"); | ||
this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please push this down to the subclass, have the subclass provide default and valid algorithms by calling the super constructor, or do not use the abstract class so that you can validate here. From a security perspective, it's valuable to validate as early as possible.
* | ||
* @since 7.0 | ||
*/ | ||
public abstract static class KeyPairJwtEncoderBuilder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be best to not have an abstract class since that can be quickly complex for builders. As it stands, the amount of reuse is quite small.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @jzheaux
Running ./gradlew :spring-security-oauth2-jose:check is raising a warning that says:
Class KeyPairJwtEncoderBuilder should be declared as final. |
So I think we should keep the class abstract
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I wasn't clear here. What I'm saying is that the abstract class itself doesn't provide enough reuse value to justify the maintenance. I think that we should just have SecretKeyJwtEncoderBuilder
, RsaKeyJwtEncoderBuilder
, and EcKeyJwtEncoderBuilder
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
a53bfb3
to
2d1e2c6
Compare
2d1e2c6
to
24300cb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, @surajbh123, thanks for the updates and for your feedback.
Given some of the confusion I caused with some of my feedback, I wanted to take some more time with the PR before replying. You'll see that there are many more comments now with more specifics about what and why; hopefully that's clearer this time around.
To help with processing the comments, I also added a commit to my fork for you to review which applies my comments. I feel these changes make the builder classes easier to use and more maintainable by our team. You are welcome to review this commit to get a holistic view of the changes I'm proposing:
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Show resolved
Hide resolved
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java
Show resolved
Hide resolved
* with a {@link SecretKey}. | ||
*/ | ||
public NimbusJwtEncoder build() { | ||
this.jwsAlgorithm = (this.jwsAlgorithm != null) ? this.jwsAlgorithm : JWSAlgorithm.HS256; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please take a look at my suggestions for the constructor so that this value need not be checked in this way.
By setting reasonable defaults in the constructor, readying the JWK
is as simple as:
OctetSequenceKey jwk = this.builder.build()
if (headers == null) { | ||
headers = DEFAULT_JWS_HEADER; | ||
} | ||
headers = (headers != null) ? headers : (this.jwsHeader != null) ? this.jwsHeader : DEFAULT_JWS_HEADER; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of checking for this.jwsHeader
being non-null, please ensure that it is not null as part of the construction process. Then you can change this to:
headers = (headers != null) ? headers : this.jwsHeader;
@@ -83,6 +95,8 @@ public final class NimbusJwtEncoder implements JwtEncoder { | |||
|
|||
private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory(); | |||
|
|||
private JwsHeader jwsHeader; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename this to defaultJwsHeader
for clarity.
|
||
@Test | ||
void keyPairBuilderWithRsaDefaultAlgorithm() throws Exception { | ||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of re-constructing the generator, please move this to a member variable. Also, I feel this will be simpler if you use RSAKeyGenerator
from Nimbus instead:
private final RSAKeyGenerator rsaGenerator = new RSAKeyGenerator(2048);
Then in this test you can do:
RSAKey jwk = this.rsaGenerator.generate();
and then use that instance to construct the builder. The other unit tests would benefit from this same polish.
|
||
@Test | ||
void keyPairBuilderWithEcDefaultAlgorithm() throws Exception { | ||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of re-constructing the generator, please move this to a member variable. Also, I feel this will be simpler if you use ECKeyGenerator
from Nimbus instead:
private final ECKeyGenerator ecGnerator = new ECKeyGenerator(Curve.P_256);
Then in this test you can do:
ECKey jwk = this.ecGnerator.generate();
and then use that instance to construct the builder.
KeyPair keyPair = keyPairGenerator.generateKeyPair(); | ||
NimbusJwtEncoder encoder = NimbusJwtEncoder.withKeyPair(keyPair) | ||
.keyId(UUID.randomUUID().toString()) | ||
.signatureAlgorithm(SignatureAlgorithm.ES256) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since EC defaults to ES256
, I think this should be a different algorithm, given the test name.
* @since 7.0 | ||
*/ | ||
public static SecretKeyJwtEncoderBuilder withSecretKey(SecretKey secretKey) { | ||
Assert.notNull(secretKey, "secretKey cannot be null"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move the assertion into the builder constructor so that the subclass can be self-sufficient. This is helpful in the event that we turn the subclass into a top-level class.
* @since 7.0 | ||
*/ | ||
public static KeyPairJwtEncoderBuilder withKeyPair(KeyPair keyPair) { | ||
Assert.isTrue(keyPair != null && keyPair.getPrivate() != null && keyPair.getPublic() != null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move any null checking to the subclass constructors so they are self-sufficient. This is helpful in the event that we turn the subclasses into top-level classes.
No description provided.