-
Notifications
You must be signed in to change notification settings - Fork 25.2k
OIDC realm authentication flows #37787
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
OIDC realm authentication flows #37787
Conversation
- Use nimbus oidc sdk - JWE not handled - UserInfo requests not handled
Pinging @elastic/es-security |
AccessToken accessToken; | ||
if (rpConfig.getResponseType().impliesCodeFlow()) { | ||
final AuthorizationCode code = response.getAuthorizationCode(); | ||
Tuple<AccessToken, JWT> tokens = exchangeCodeForToken(code); |
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 believe this should be async.
…alm-authentication-flows
8d50fb1
to
422319d
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.
I think we'll need to put some more work in unfortunately to prevent blocking of network threads. Authentication will typically happen on a network thread and operations on a network thread should be non-blocking otherwise there is the potential to make a node unresponsive.
...c/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java
Outdated
Show resolved
Hide resolved
...c/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java
Outdated
Show resolved
Hide resolved
...ty/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java
Outdated
Show resolved
Hide resolved
...ty/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java
Outdated
Show resolved
Hide resolved
final ClientAuthentication clientAuth = new ClientSecretBasic(rpConfig.getClientId(), clientSecret); | ||
final AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, rpConfig.getRedirectUri()); | ||
final TokenRequest tokenRequest = new TokenRequest(opConfig.getTokenEndpoint(), clientAuth, codeGrant); | ||
final HTTPResponse httpResponse = getResponse(buildRequest(tokenRequest)); |
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.
as @albertzaharovits mentioned already, we need to make this asynchronous. We have a few options:
- fork to the generic threadpool and execute this blocking call there
- add HTTPRequest#send as a forbidden api, add http-asyncclient as a dependency and use it to execute asynchronously.
I'm in favor of option 2 even though it is more work. It feels more like the right thing to do.
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 went with option 2. Do you think it's necessary to add the HTTPRequest#send
as a forbidden API?
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.
Yeah I do think we should. The reason being that it might not be obvious to someone else that it should not be used
} else { | ||
String jwkSetPath = opConfig.getJwkSetPath(); | ||
if (jwkSetPath.startsWith("https://")) { | ||
validator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), requestedAlgorithm, |
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.
If we want to use this, we need to fork to a different threadpool as they expect a synchronous resolution or go async to retrieve the content ourselves and then use JWKSet.parse
on the returned body content
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 we would have to do some caching to not reach out for JWKSet
for each new authn. For that matter I think it's better to let the IDTokenValidator
handle its business on a separate thread and not build the IDTokenValidator
everytime (make it an instance variable).
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 point regarding making IDTokenValidator an instance variable. It is thread safe. I do wonder if we should have some sort of built in update mechanism for obtaining the jwkset rather than having the token validator reach out for every auth.
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 moved IDTokenValidator
an instance variable and since it's thread safe I think we'll be fine. WDYT ?
It won't reach out for the jwkset on each authn, as there is a simple built in caching mechanism. It will only reach out again, in case it receives a JWT that carries a kid
in its Header that the IDTokenValidator
has not seen before ( the use case is OP key rotation )
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 also added a FileWatcher
to make sure that we pick up changes to the JWKSet if it's provided in a local file
...ty/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java
Outdated
Show resolved
Hide resolved
...n/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java
Outdated
Show resolved
Hide resolved
...n/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java
Show resolved
Hide resolved
...c/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java
Outdated
Show resolved
Hide resolved
...ty/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java
Show resolved
Hide resolved
...ty/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java
Outdated
Show resolved
Hide resolved
...ty/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java
Outdated
Show resolved
Hide resolved
...c/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java
Outdated
Show resolved
Hide resolved
...n/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java
Outdated
Show resolved
Hide resolved
...ty/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java
Outdated
Show resolved
Hide resolved
...c/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java
Show resolved
Hide resolved
- Make the calls to TokenEndpoint and UserInfoEndpoint asynchrously - Move IdTokenValidator to an instance variable
@elasticmachine test this please |
…alm-authentication-flows
@jaymode I'd appreciate a round of 👀 |
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.
LGTM. Lets see if @albertzaharovits can also take a final pass
@@ -219,11 +225,26 @@ private void getUserClaims(@Nullable AccessToken accessToken, JWT idToken, Nonce | |||
claimsListener.onResponse(verifiedIdTokenClaims); | |||
} | |||
} catch (com.nimbusds.oauth2.sdk.ParseException | JOSEException | BadJOSEException e) { |
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 is easier to read if we catch BadJOSEException
separately:
try {
...
} catch (BadJOSEException e) {
if (shouldRetry && ... ) {
} else {
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
}
} catch (com.nimbusds.oauth2.sdk.ParseException | JOSEException e) {
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
}
private SSLContext clientContext; | ||
private HostnameVerifier verifier; | ||
private RealmConfig config; | ||
private volatile JWKSet cachedJwkSet = 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.
can we use new JWKSet()
instead of null? Otherwise there is a chance we get a NPE in the get
method. If you prefer null, then the get method should be something like:
JWKSet local = this.jwkSet;
if (local != null) {
return jwkSelector.select(local);
} else {
return Collections.emptyList();
}
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 will change it but I think null is fine too because JWKSelector#select
explicitly handles it
/**
* Selects the keys from the specified JWK set according to the
* matcher's criteria.
*
* @param jwkSet The JWK set. May be {@code null}.
*
* @return The selected keys, ordered by their position in the JWK set,
* empty list if none were matched or the JWK is {@code null}.
*/
@elasticmachine run elasticsearch-ci/1 |
@elasticmachine run elasticsearch-ci/2 |
@albertzaharovits I'd appreciate a final round as I have a couple of small PRs I want to open that depend on this one 🙏 |
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.
LGTM, This is solid work Ioannis!
I have left a few suggestions, but I don't think another review round is necessary.
+ claimValueObject.getClass().getName()); | ||
} else { | ||
values = (List<String>) claimValueObject; | ||
} |
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.
nit:
} else if (claimValueObject instanceof List) {
values = (List<String>) claimValueObject;
} else {
throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim())
+ " expects a claim with String or a String Array value but found a "
+ claimValueObject.getClass().getName());
}
this.rpConfiguration = buildRelyingPartyConfiguration(config); | ||
this.opConfiguration = buildOpenIdConnectProviderConfiguration(config); | ||
this.openIdConnectAuthenticator = | ||
new OpenIdConnectAuthenticator(config, opConfiguration, rpConfiguration, sslService, watcherService); |
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.
nit:
As a best practice, I try to put everything that throws in a constructor, first, and any Closeable
last (OpenIdConnectAuthenticator
).
private final CloseableHttpAsyncClient httpClient; | ||
private final ResourceWatcherService watcherService; | ||
|
||
protected final Logger logger = LogManager.getLogger(getClass()); |
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.
nit:
We prefer these static.
CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.custom() | ||
.setConnectionManager(connectionManager) | ||
.setSSLContext(clientContext) | ||
.setSSLHostnameVerifier(verifier) |
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 you please double check that hostnameVerifier
works together with connectionManger
. I see they overlap a little.
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 catch !!! Setting the connection manager explicitly overrides the SslContext :/
private void setMetadataFileWatcher(String jwkSetPath) throws IOException { | ||
final Path path = realmConfig.env().configFile().resolve(jwkSetPath); | ||
FileWatcher watcher = new FileWatcher(path); | ||
watcher.addListener(new FileListener(logger, () -> this.idTokenValidator = createIdTokenValidator())); |
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.
idTokenValidator
should be AtomicReference
.
}, ex -> { | ||
logger.trace("Attempted and failed to refresh JWK cache upon token validation failure", e); | ||
claimsListener.onFailure(ex); | ||
})); |
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 we please, please move this "should retry" check inside a method in idTokenValidator
.
And I would leave the messageSigned JWT rejected: Another algorithm expected, or no matching key(s) found
out, because I think in the case of the HTTP JWTKeySets the Exception type is enough of a check. If they change the message when we update the library we would blindly loose the ability to rotate keys on OP side.
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.
And I would leave the messageSigned JWT rejected: Another algorithm expected, or no matching key(s) found out, because I think in the case of the HTTP JWTKeySets the Exception type is enough of a check. If they change the message when we update the library we would blindly loose the ability to rotate keys on OP side.
Yeap, makes sense.
Can we please, please move this "should retry" check inside a method in idTokenValidator.
You mean extend IDTokenValidator
and add the method there or inside a method in idTokenValidator.
== inside a method in OpenIdConnectAuthenticator
?.
if (logger.isTraceEnabled()) { | ||
logger.trace("Received and validated the Id Token for the user: [{}]", verifiedIdTokenClaims); | ||
} | ||
if (accessToken != null && opConfig.getUserinfoEndpoint() != 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.
if accessToken != null
&& opConfig.getUserinfoEndpoint() == null
we do nothing. I think at least logging should be done. For example if the URL is wrong, getAndCombineUserInfoClaims
would throw ElasticsearchSecurityException
.
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.
accessToken != null && opConfig.getUserinfoEndpoint() == null
is a valid case where the OP returned an access token but we haven't configured a userInfoEndpoint in our settings because we are not interested in hitting it ( all necessary claims are in the ID token ). The same goes for accessToken == null && opConfig.getUserinfoEndpoint() != null
: We have configured an userinfo endpoint but the OP didn't return an access token (maybe we misconfigured our response type to id_token
instead of token id_token
, so we can't make a request to the user info endpoint. Logging the latter wouldn't hurt, you're right :)
For example if the URL is wrong, getAndCombineUserInfoClaims would throw ElasticsearchSecurityException .
If the URL is invalid, we would throw on building the realm config. If it is valid but wrong, we would catch that in getAndCombineUserInfoClaims and call onFailure(new ElasticsearchSecurityException("Failed to get claims from the Userinfo Endpoint.",
Thanks for the feedback Albert, that was really valuable. I 'll merge this to the feature branch on green CI and we can re-iterate any of the points in the merge of the feature branch to master |
I keep hitting #36816, I'll try and get this muted before re-invoking the CI |
@elasticmachine please run elasticsearch-ci/packaging-sample |
This reverts commit 6098583.
which doesn't reproduce locally, so I'll roll the dice and ask @elasticmachine to run elasticsearch-ci/2 once more |
@elasticmachine run elasticsearch-ci/2 ? |
This commit adds an OpenID Connect authentication realm to elasticsearch. Elasticsearch (with the assistance of kibana or another web component) acts as an OpenID Connect Relying Party and supports the Authorization Code Grant and Implicit flows as described in http://ela.st/oidc-spec. It adds support for consuming and verifying signed ID Tokens, both RP initiated and 3rd party initiated Single Sign on and RP initiated signle logout. It also adds an OpenID Connect Provider in the idp-fixture to be used for the associated integration tests. The code in this commit has been tracked in a feature branch and has been previously reviewed and approved in : #37009 #37787 #38474 #38475 #40262
This commit adds an OpenID Connect authentication realm to elasticsearch. Elasticsearch (with the assistance of kibana or another web component) acts as an OpenID Connect Relying Party and supports the Authorization Code Grant and Implicit flows as described in http://ela.st/oidc-spec. It adds support for consuming and verifying signed ID Tokens, both RP initiated and 3rd party initiated Single Sign on and RP initiated signle logout. It also adds an OpenID Connect Provider in the idp-fixture to be used for the associated integration tests. The code in this commit has been tracked in a feature branch and has been previously reviewed and approved in : elastic#37009 elastic#37787 elastic#38474 elastic#38475 elastic#40262
This commit adds an OpenID Connect authentication realm to elasticsearch. Elasticsearch (with the assistance of kibana or another web component) acts as an OpenID Connect Relying Party and supports the Authorization Code Grant and Implicit flows as described in http://ela.st/oidc-spec. It adds support for consuming and verifying signed ID Tokens, both RP initiated and 3rd party initiated Single Sign on and RP initiated signle logout. It also adds an OpenID Connect Provider in the idp-fixture to be used for the associated integration tests. The code in this commit has been tracked in a feature branch and has been previously reviewed and approved in : elastic#37009 elastic#37787 elastic#38474 elastic#38475 elastic#40262
Adds implementation for authentication in the OIDC realm using the authorization code grant and the implicit flows as described in https://openid.net/specs/openid-connect-core-1_0.html
TODO:
TestsMost unit tests are added here, IT will be on its own PR.Documentation will be handled in a different PR.
Better late than never, the flow diagrams for the implicit flow in https://drive.google.com/open?id=1FF9LltN7qBUdGO98zLfCB8R55WQc1spL might help with the review