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

OIDC realm authentication flows #37787

Merged

Conversation

jkakavas
Copy link
Member

@jkakavas jkakavas commented Jan 23, 2019

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: Tests Most 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

@jkakavas jkakavas added >feature WIP v7.0.0 :Security/Authentication Logging in, Usernames/passwords, Realms (Native/LDAP/AD/SAML/PKI/etc) labels Jan 23, 2019
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-security

AccessToken accessToken;
if (rpConfig.getResponseType().impliesCodeFlow()) {
final AuthorizationCode code = response.getAuthorizationCode();
Tuple<AccessToken, JWT> tokens = exchangeCodeForToken(code);
Copy link
Contributor

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.

@jkakavas jkakavas force-pushed the oidc-realm-authentication-flows branch from 8d50fb1 to 422319d Compare January 25, 2019 16:12
@jkakavas jkakavas changed the title [WIP] OIDC realm authentication flows OIDC realm authentication flows Jan 25, 2019
@jkakavas jkakavas removed the WIP label Jan 25, 2019
Copy link
Member

@jaymode jaymode left a 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.

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));
Copy link
Member

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:

  1. fork to the generic threadpool and execute this blocking call there
  2. 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.

Copy link
Member Author

@jkakavas jkakavas Jan 29, 2019

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?

Copy link
Member

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,
Copy link
Member

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

Copy link
Contributor

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).

Copy link
Member

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.

Copy link
Member Author

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 )

Copy link
Member Author

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

- Make the calls to TokenEndpoint and UserInfoEndpoint asynchrously
- Move IdTokenValidator to an instance variable
@jkakavas
Copy link
Member Author

jkakavas commented Jan 29, 2019

@elasticmachine test this please

@jkakavas
Copy link
Member Author

jkakavas commented Feb 1, 2019

@jaymode I'd appreciate a round of 👀

Copy link
Member

@jaymode jaymode left a 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) {
Copy link
Member

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;
Copy link
Member

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();
}

Copy link
Member Author

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}.
	 */

@jkakavas
Copy link
Member Author

jkakavas commented Feb 4, 2019

@elasticmachine run elasticsearch-ci/1

@jkakavas
Copy link
Member Author

jkakavas commented Feb 4, 2019

@elasticmachine run elasticsearch-ci/2

@jkakavas
Copy link
Member Author

jkakavas commented Feb 4, 2019

@albertzaharovits I'd appreciate a final round as I have a couple of small PRs I want to open that depend on this one 🙏

Copy link
Contributor

@albertzaharovits albertzaharovits left a 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;
}
Copy link
Contributor

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

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

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

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.

Copy link
Member Author

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

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);
}));
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 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.

Copy link
Member Author

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

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 .

Copy link
Member Author

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.",

@jkakavas
Copy link
Member Author

jkakavas commented Feb 5, 2019

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

@jkakavas
Copy link
Member Author

jkakavas commented Feb 5, 2019

I keep hitting #36816, I'll try and get this muted before re-invoking the CI

@jkakavas
Copy link
Member Author

jkakavas commented Feb 5, 2019

@elasticmachine please run elasticsearch-ci/packaging-sample

@jkakavas
Copy link
Member Author

jkakavas commented Feb 5, 2019

16:07:29 Tests with failures:
16:07:29 
16:07:29   - org.elasticsearch.xpack.monitoring.collector.indices.IndexRecoveryMonitoringDocTests.testToUTC
REPRODUCE WITH: ./gradlew :x-pack:plugin:monitoring:unitTest -Dtests.seed=B110FE213E2190D6 -Dtests.class=org.elasticsearch.xpack.monitoring.collector.indices.IndexRecoveryMonitoringDocTests -Dtests.method="testToUTC" -Dtests.security.manager=true -Dtests.locale=sr-CS -Dtests.timezone=Africa/Lubumbashi -Dcompiler.java=11 -Druntime.java=8

which doesn't reproduce locally, so I'll roll the dice and ask @elasticmachine to run elasticsearch-ci/2 once more

@jkakavas
Copy link
Member Author

jkakavas commented Feb 5, 2019

@elasticmachine run elasticsearch-ci/2 ?

@jkakavas jkakavas merged commit e5859fd into elastic:feature-oidc-realm Feb 5, 2019
jkakavas added a commit that referenced this pull request Apr 4, 2019
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
jkakavas added a commit to jkakavas/elasticsearch that referenced this pull request Apr 14, 2019
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
gurkankaymak pushed a commit to gurkankaymak/elasticsearch that referenced this pull request May 27, 2019
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>feature :Security/Authentication Logging in, Usernames/passwords, Realms (Native/LDAP/AD/SAML/PKI/etc) v7.0.0-beta1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants