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
KEYCLOAK-7560 Refactor Token Sign and Verify by Token Signature SPI #5260
Conversation
Thanks. I've only had a quick glance so far, but it looks good. Both me and @mposolda will review this in detail soon. A couple of comments for now:
|
@stianst, thanks a lot for your comments. I would like to wait for others' comments. But my proposals incorporating your comments are the followings. What do you think about them?
Aside from it, to accommodate the token signature algorithm setting in Realm, two ideas hit on me.
This feature seems to be common. Therefore, the latter might be preferable. However, if do that, need a lot of work I'm not familiar with. Ex. prepare liquibase changelog for realm schema change, add codes to RealmModel implementing classes (infinispan, jpa). |
algorithm specific codes removed from JWSBuilder. |
mainly add KeyManager's algorithm specific methods like getActiveRsaKey with algorithm neutral methods. I stop committing further changes until receiving reviews and comments. |
|
KeyManager - I like what you did so far. I think there's one last piece of the puzzle here and that is allowing custom keys. To explain that a bit better what I'd like is imagine if you wanted ECDSA and we didn't want it in the code base (that's not the case we want ECDSA, but just in an imaginary world where we didn't). You should still be able to develop it as a custom extension and deploy it to Keycloak. That means you need to be able to plugin the key provider (which is possible today) and the signature provider which you're working on now. What it also enables is also that you can add a new algorithm to core Keycloak without having to wire it in everywhere. All that should be needed is to list it in META-INF/services and all the wiring should be automatic. |
Thank you for your comments. |
What would be great is if you could start developing ECDSA as an extension. That way we can really see how pluggable this is. Then once this is merged it would be trivial to move the ECDSA extension to the Keycloak code base. |
I've already implemented ECDSA key and its provider, but it has not yet been pluggable. Therefore, I would try to make it pluggable and commit it on this PR. |
The goal for this commit is to support new signature algorithm and corresponding new key if necessary without modifying the current code base. To do that, I've come up with the following design concepts and realized them on this commit. Could you please review them and give me some feedback?
Alternatively, make TokenSignatureProvider and KeyProvider in the plugin extension return the key type (AlgorithmType) as String each of which requires or provides respectively.
Alternatively, make TokenSignatureProvider in the plugin extension provide the same features.
E.g., SignatureKeyProvider interface for signing and verifying KeyProvider. The user of a key recognize it as its role, not its type.
Instead of use bare TokenSignatureProvider, use API provided by this TokenSignature which internally get an appropriate TokenSignatureProvider and KeyProvider, and conduct signing and verifying, and returns its result.
Alternatively, make KeyProvider itself in the plugin extension provide its key representaion.
Alternatively, provide signing capable JWSSignatureProvider instance externally and JWSBuilder use it. This commit is for only Access, Refresh, ID Token treated in TokenManager. Not treat other signature related points such as /certs endpoint to provide verification keys' information, codes calling RSAProvider directly, codes using RSATokenVerifier, Client Adapter and so on. |
I've been digging into this a bit today and realised there's quite a lot of cleanup needed in the KeyProvider, TokenSignatureProvider and other existing code. At this point I can't really give you a long list of things to do so will see if I can get time to sort this out myself. I will try to make time next week to look at this if not it's not going to be until I return from PTO a few weeks after that. |
@tnorimat done a first round and refactored key providers. There are no generic methods to obtain keys given the algorithm type. There's also additional metadata available for keys including use (enc/sign) and algorithms it supports. |
After finishing your key provider refactoring, could I continue token signature provider support by incorporating it? |
I've revised codes to incorporate refactored key provider. |
incorporate refactored key provider
support authorization server side ECDSA key provider and token signature provider
Add ECDSA key provider (NIST P-256, P-384, P-521) and token signature provider (ES256, ES384, ES512) in authorization server side only. Those are in keycloak's existing projects (keycloak-services and so on) but it seems to be possible to make them put into a separated new individual project (e.g. keycloak-ecdsa). Not yet support TokenVerifier (used in UserInfo endpoint, TokenIntrospection endpoint, client adapter and test cases) for ECDSA so that added test cases only verify tokens' ECDSA signature. Not yet support ECDSA in client adapter so that also not yet support JWK Set documents at jwks_uri (/certs) which currently only supports RSA key. In my opinion, ECDSA support for client adapters and test suites seems not to be pluggable such as authorization server side (keycloak). |
Thanks for the updates. I'm working on finishing something else and I'm away next week on holiday. After that I will give this some priority. For clients I think at least for now we need to live with the providers being hardcoded and not pluggable. We would need to introduce some sort of SPI framework for clients if we wanted to have it pluggable, which would be nice, but to much work in this context. |
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 @tnorimat for this PR, it looks rather nice. Few comments inline
|
||
private static KeyWrapper KEY; | ||
|
||
private static long EXPIRES; |
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.
should be volatile
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.
It seems that KEY and EXPIRES are guarded against multiple accesses concurrently by synchronized (FailsafeEcdsaKeyProvider.class) block in the constructor and also make them visible
out of this block from other threads. Moreover, KEY and EXPIRES are not accessed other than the constructor.
Does it already suffice to guard KEY and EXPIRE static members against simultaneous multiple accesses and make them visible from other threads ?
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.
Yes, you're right.
@@ -334,6 +349,12 @@ public JWSHeader getHeader() throws VerificationException { | |||
} | |||
|
|||
public void verifySignature() throws VerificationException { | |||
// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI | |||
if (this.signatureProvider != null && this.verify() != 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.
verify()
calls verifySignature()
, and then now verifySignature()
calls verify()
=> stack overflow
Should be this.verifyKey != null
instead?
Even in that case, I'd say it should suffice that this.signatureProvider != null
since e.g. in case of hardware token, one might have no access to the key.
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.
Yes, I also think that only this.signatureProvider != null is sufficient to check.
I've commented above that this TokenVerifier's refactoring had not yet completely been implemented and I had not yet written arquillian integration test cases for it so I had not recognized this stack overflow potential. Thank you very much.
|
||
@SuppressWarnings("rawtypes") | ||
private TokenSignatureProvider getTokenSignatureProvider(String sigAlgName) { | ||
List<ComponentModel> components = new LinkedList<>(realm.getComponents(realm.getId(), TokenSignatureProvider.class.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.
getTokenSignatureProvider
is called rather often and would benefit from caching c.getProviderId()
object per sigAlgName
.
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.
To make new provider instance by its corresponding provider factory, need to use its corresponding ComponentModel instance. Therefore, whole ComponentModel c needs to be cached instead of c.getProviderId().
Or how about caching the provider instance per sigAlgName?
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.
Good idea yet it would also need a mechanism for invalidating the provider instances upon change of the component configuration. Let's leave this out for this PR for now and file a JIRA (once this PR would be merged ) with this optimization later.
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.
Yes. I agree with you. So I will not modify here and make a JIRA issue for its optimization.
@hmlnarik, thanks for your review. I'll incorporate them and push new commit afterward. |
KEY = createKeyWrapper(); | ||
EXPIRES = Time.currentTime() + 60 * 10; | ||
|
||
if (EXPIRES > 0) { |
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.
This will be always true. Should be moved before the previous 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.
Yes. I will move
logger.warnv("Keys expired, re-generated kid={0}", KEY.getKid());
just below
EXPIRES = Time.currentTime() + 60 * 10;
The existing FailsafeRsaKeyProvider.java also have the same codes. What do you think about it ? Should I do the same to FailsafeRsaKeyProvider.java ?
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 keep this commit focused on ECDSA, I've created https://issues.jboss.org/browse/KEYCLOAK-8027 for this nit. Feel free to either fix it separately or leave it for later.
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 see. I will only modify FailsafeEcdsaKeyProvider.
I've created a new branch to be able to collaborate on this work: https://github.com/keycloak/keycloak/tree/token-signature-spi I've opened a new PR from this branch here #5478 and will close this PR. @tnorimat let's collaborate in #5478 to get this work completed. First thing I've done is to refactor token providers to not use component model. Component model is useful when you want to have multiple configurable instances of a provider in a realm (like user federation allows creating multiple instances of the LDAP provider). That does not apply in this case. Further, it's a lot simpler to use the providers if they are not using the component model and there's no need for config in the realm. |
I see. I would like to continue collaborating this issue on the new PR. |
This PR is for refactoring token signing and verifying codes by introducing token signature SPI.
This first commit is specialized for the SPI/Provider/Factory design and implementation.
I would be happy if it is reviewed. If approved, I would like to do next refactoring current codes for signing and verifying tokens in RSASSA-PKCS1-v1_5.
After that, I would like to support newly ECDSA.
[supported signature algorithm]
RS256(as default), RS384, RS512
[on which we can select signature algorithm]
Realm, Client(supersede realm setting)
The corresponding JIRA ticket si the following.
https://issues.jboss.org/browse/KEYCLOAK-7560