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

Add OIDC authentication Integration Tests #40262

Merged
merged 17 commits into from
Mar 30, 2019
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x-pack/plugin/security/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ dependencies {
compile "com.nimbusds:oauth2-oidc-sdk:6.5"
compile "com.nimbusds:nimbus-jose-jwt:4.41.2"
compile "com.nimbusds:lang-tag:1.4.4"
compile "com.sun.mail:javax.mail:1.6.2"
compile "com.sun.mail:jakarta.mail:1.6.3"
compile "net.jcip:jcip-annotations:1.0"
compile "net.minidev:json-smart:2.3"
compile "net.minidev:accessors-smart:1.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
787e007e377223bba85a33599d3da416c135f99b
637 changes: 637 additions & 0 deletions x-pack/plugin/security/licenses/jakarta.mail-LICENSE.txt

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions x-pack/plugin/security/licenses/jakarta.mail-NOTICE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Notices for Eclipse Project for JavaMail

This content is produced and maintained by the Eclipse Project for JavaMail
project.

* Project home: https://projects.eclipse.org/projects/ee4j.javamail

## Trademarks

Eclipse Project for JavaMail is a trademark of the Eclipse Foundation.

## Copyright

All content is the property of the respective authors or their employers. For
more information regarding authorship of content, please consult the listed
source code repository logs.

## Declared Project Licenses

This program and the accompanying materials are made available under the terms
of the Eclipse Public License v. 2.0 which is available at
http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made
available under the following Secondary Licenses when the conditions for such
availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU
General Public License, version 2 with the GNU Classpath Exception which is
available at https://www.gnu.org/software/classpath/license.html.

SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0

## Source Code

The project maintains the following source code repositories:

* https://github.com/eclipse-ee4j/javamail

## Third-party Content

This project leverages the following third party content.

None

## Cryptography

Content may contain encryption software. The country in which you are currently
may have restrictions on the import, possession, and use, and/or re-export to
another country, of encryption software. BEFORE using any encryption software,
please check the country's laws, regulations and policies concerning the import,
possession, or use, and re-export of encryption software, to see if this is
permitted.

1 change: 0 additions & 1 deletion x-pack/plugin/security/licenses/javax.mail-1.6.2.jar.sha1

This file was deleted.

759 changes: 0 additions & 759 deletions x-pack/plugin/security/licenses/javax.mail-LICENSE.txt

This file was deleted.

Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request,
}
@SuppressWarnings("unchecked") final Map<String, Object> tokenMetadata = (Map<String, Object>) result.getMetadata()
.get(OpenIdConnectRealm.CONTEXT_TOKEN_DATA);
tokenService.createUserToken(authentication, originatingAuthentication,
tokenService.createOAuth2Tokens(authentication, originatingAuthentication, tokenMetadata, true,
ActionListener.wrap(tuple -> {
final String tokenString = tokenService.getUserTokenString(tuple.v1());
final String tokenString = tokenService.getAccessTokenAsString(tuple.v1());
final TimeValue expiresIn = tokenService.getExpirationDelay();
listener.onResponse(new OpenIdConnectAuthenticateResponse(authentication.getUser().principal(), tokenString,
tuple.v2(), expiresIn));
}, listener::onFailure), tokenMetadata, true);
}, listener::onFailure));
}, e -> {
logger.debug(() -> new ParameterizedMessage("OpenIDConnectToken [{}] could not be authenticated", token), e);
listener.onFailure(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import org.elasticsearch.xpack.security.authc.TokenService;
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm;

import java.io.IOException;
import java.text.ParseException;
import java.util.Map;

Expand All @@ -54,29 +53,25 @@ public TransportOpenIdConnectLogoutAction(TransportService transportService, Act
@Override
protected void doExecute(Task task, OpenIdConnectLogoutRequest request, ActionListener<OpenIdConnectLogoutResponse> listener) {
invalidateRefreshToken(request.getRefreshToken(), ActionListener.wrap(ignore -> {
try {
final String token = request.getToken();
tokenService.getAuthenticationAndMetaData(token, ActionListener.wrap(
tuple -> {
final Authentication authentication = tuple.v1();
final Map<String, Object> tokenMetadata = tuple.v2();
validateAuthenticationAndMetadata(authentication, tokenMetadata);
tokenService.invalidateAccessToken(token, ActionListener.wrap(
result -> {
if (logger.isTraceEnabled()) {
logger.trace("OpenID Connect Logout for user [{}] and token [{}...{}]",
authentication.getUser().principal(),
token.substring(0, 8),
token.substring(token.length() - 8));
}
OpenIdConnectLogoutResponse response = buildResponse(authentication, tokenMetadata);
listener.onResponse(response);
}, listener::onFailure)
);
}, listener::onFailure));
} catch (IOException e) {
listener.onFailure(e);
}
final String token = request.getToken();
tokenService.getAuthenticationAndMetaData(token, ActionListener.wrap(
tuple -> {
final Authentication authentication = tuple.v1();
final Map<String, Object> tokenMetadata = tuple.v2();
validateAuthenticationAndMetadata(authentication, tokenMetadata);
tokenService.invalidateAccessToken(token, ActionListener.wrap(
result -> {
if (logger.isTraceEnabled()) {
logger.trace("OpenID Connect Logout for user [{}] and token [{}...{}]",
authentication.getUser().principal(),
token.substring(0, 8),
token.substring(token.length() - 8));
}
OpenIdConnectLogoutResponse response = buildResponse(authentication, tokenMetadata);
listener.onResponse(response);
}, listener::onFailure)
);
}, listener::onFailure));
}, listener::onFailure));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ public OpenIdConnectAuthenticator(RealmConfig realmConfig, OpenIdConnectProvider
this.rpConfig = rpConfig;
this.sslService = sslService;
this.httpClient = createHttpClient();
this.idTokenValidator.set(createIdTokenValidator());
this.watcherService = watcherService;
this.idTokenValidator.set(createIdTokenValidator());
}

// For testing
Expand Down Expand Up @@ -278,19 +278,22 @@ private void validateAccessToken(AccessToken accessToken, JWT idToken) {
if (rpConfig.getResponseType().equals(ResponseType.parse("id_token token")) ||
rpConfig.getResponseType().equals(ResponseType.parse("code"))) {
assert (accessToken != null) : "Access Token cannot be null for Response Type " + rpConfig.getResponseType().toString();
final boolean optional = rpConfig.getResponseType().equals(ResponseType.parse("code"));
final boolean isValidationOptional = rpConfig.getResponseType().equals(ResponseType.parse("code"));
// only "Bearer" is defined in the specification but check just in case
if (accessToken.getType().toString().equals("Bearer") == false) {
throw new ElasticsearchSecurityException("Invalid access token type [{}], while [Bearer] was expected",
accessToken.getType());
}
String atHashValue = idToken.getJWTClaimsSet().getStringClaim("at_hash");
if (null == atHashValue && optional == false) {
throw new ElasticsearchSecurityException("Failed to verify access token. at_hash claim is missing from the ID Token");
if (Strings.hasText(atHashValue) == false) {
if (isValidationOptional == false) {
throw new ElasticsearchSecurityException("Failed to verify access token. ID Token doesn't contain at_hash claim ");
}
} else {
AccessTokenHash atHash = new AccessTokenHash(atHashValue);
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(idToken.getHeader().getAlgorithm().getName());
AccessTokenValidator.validate(accessToken, jwsAlgorithm, atHash);
}
AccessTokenHash atHash = new AccessTokenHash(atHashValue);
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(idToken.getHeader().getAlgorithm().getName());
AccessTokenValidator.validate(accessToken, jwsAlgorithm, atHash);
} else if (rpConfig.getResponseType().equals(ResponseType.parse("id_token")) && accessToken != null) {
// This should NOT happen and indicates a misconfigured OP. Warn the user but do not fail
LOGGER.warn("Access Token incorrectly returned from the OpenId Connect Provider while using \"id_token\" response type.");
Expand Down Expand Up @@ -324,7 +327,6 @@ private void validateResponseType(AuthenticationSuccessResponse response) {
if (rpConfig.getResponseType().equals(response.impliedResponseType()) == false) {
throw new ElasticsearchSecurityException("Unexpected response type [{}], while [{}] is configured",
response.impliedResponseType(), rpConfig.getResponseType());

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,10 @@ public void testLogoutInvalidatesTokens() throws Exception {
tokenMetadata.put("oidc_realm", REALM_NAME);

final PlainActionFuture<Tuple<UserToken, String>> future = new PlainActionFuture<>();
tokenService.createUserToken(authentication, authentication, future, tokenMetadata, true);
tokenService.createOAuth2Tokens(authentication, authentication, tokenMetadata, true, future);
final UserToken userToken = future.actionGet().v1();
mockGetTokenFromId(userToken, false, client);
final String tokenString = tokenService.getUserTokenString(userToken);
final String tokenString = tokenService.getAccessTokenAsString(userToken);

final OpenIdConnectLogoutRequest request = new OpenIdConnectLogoutRequest();
request.setToken(tokenString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ public void testImplicitFlowFailsWithExpiredToken() throws Exception {
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
.jwtID(randomAlphaOfLength(8))
.audience(rpConfig.getClientId().getValue())
// Expired 61 seconds ago with an allowed clock skew of 60 seconds
.expirationTime(Date.from(now().minusSeconds(61)))
// Expired 65 seconds ago with an allowed clock skew of 60 seconds
.expirationTime(Date.from(now().minusSeconds(65)))
.issuer(opConfig.getIssuer().getValue())
.issueTime(Date.from(now().minusSeconds(200)))
.notBeforeTime(Date.from(now().minusSeconds(200)))
Expand Down Expand Up @@ -333,11 +333,11 @@ public void testImplicitFlowFailsNotYetIssuedToken() throws Exception {
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
.jwtID(randomAlphaOfLength(8))
.audience(rpConfig.getClientId().getValue())
// Expired 61 seconds ago with an allowed clock skew of 60 seconds
.expirationTime(Date.from(now().plusSeconds(3600)))
.issuer(opConfig.getIssuer().getValue())
.issueTime(Date.from(now().plusSeconds(61)))
.notBeforeTime(Date.from(now().minusSeconds(61)))
// Issued 80 seconds in the future with max allowed clock skew of 60
.issueTime(Date.from(now().plusSeconds(80)))
.notBeforeTime(Date.from(now().minusSeconds(80)))
.claim("nonce", nonce)
.subject(subject);
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
Expand Down
83 changes: 83 additions & 0 deletions x-pack/qa/oidc-op-tests/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
Project idpFixtureProject = xpackProject("test:idp-fixture")

apply plugin: 'elasticsearch.standalone-rest-test'
apply plugin: 'elasticsearch.rest-test'
apply plugin: 'elasticsearch.test.fixtures'

dependencies {
// "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here
testCompile project(path: xpackModule('core'), configuration: 'default')
testCompile project(path: xpackModule('core'), configuration: 'testArtifacts')
testCompile project(path: xpackModule('security'), configuration: 'testArtifacts')
}
testFixtures.useFixture ":x-pack:test:idp-fixture"

String ephemeralPort;
task setupPorts {
dependsOn idpFixtureProject.postProcessFixture
doLast {
ephemeralPort = idpFixtureProject.postProcessFixture.ext."test.fixtures.oidc-provider.tcp.8080"
Copy link
Contributor

Choose a reason for hiding this comment

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

This won't work on hosts without docker -Dtests.fixture.enabled=false can be used to simulate it.
I think it also needs a fix from #40297

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 raised #40585 because I wasn't sure how to do this properly and will be integrating whatever solution we come up with here to. Did you mean something like https://github.com/elastic/elasticsearch/pull/40297/files#diff-4fbfb78d054fdb66e0b02f321c8d63c5R79?

Copy link
Member Author

@jkakavas jkakavas Mar 28, 2019

Choose a reason for hiding this comment

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

This won't work on hosts without docker

@atorok these kind of projects only contain integration tests that depend on systems running on Docker so could we have a generic way of "Don't even consider this project if Docker is not available" instead of simply disabling the integTest task as I did in #40585 ?

}
// Don't attempt to get ephemeral ports when Docker is not available
setupPorts.onlyIf { idpFixtureProject.postProcessFixture.enabled }
}
integTestCluster {
dependsOn setupPorts
setting 'xpack.license.self_generated.type', 'trial'
setting 'xpack.security.enabled', 'true'
setting 'xpack.security.http.ssl.enabled', 'false'
setting 'xpack.security.authc.token.enabled', 'true'
setting 'xpack.security.authc.realms.file.file.order', '0'
setting 'xpack.security.authc.realms.native.native.order', '1'
// OpenID Connect Realm 1 configured for authorization grant flow
setting 'xpack.security.authc.realms.oidc.c2id.order', '2'
setting 'xpack.security.authc.realms.oidc.c2id.op.name', 'c2id-op'
setting 'xpack.security.authc.realms.oidc.c2id.op.issuer', 'http://localhost:8080'
setting 'xpack.security.authc.realms.oidc.c2id.op.authorization_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id-login"
setting 'xpack.security.authc.realms.oidc.c2id.op.token_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/token"
setting 'xpack.security.authc.realms.oidc.c2id.op.userinfo_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/userinfo"
setting 'xpack.security.authc.realms.oidc.c2id.op.jwkset_path', 'op-jwks.json'
setting 'xpack.security.authc.realms.oidc.c2id.rp.redirect_uri', 'https://my.fantastic.rp/cb'
setting 'xpack.security.authc.realms.oidc.c2id.rp.client_id', 'elasticsearch-rp'
keystoreSetting 'xpack.security.authc.realms.oidc.c2id.rp.client_secret', 'b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2'
setting 'xpack.security.authc.realms.oidc.c2id.rp.response_type', 'code'
setting 'xpack.security.authc.realms.oidc.c2id.claims.principal', 'sub'
setting 'xpack.security.authc.realms.oidc.c2id.claims.name', 'name'
setting 'xpack.security.authc.realms.oidc.c2id.claims.mail', 'email'
setting 'xpack.security.authc.realms.oidc.c2id.claims.groups', 'groups'
// OpenID Connect Realm 2 configured for implicit flow
setting 'xpack.security.authc.realms.oidc.c2id-implicit.order', '3'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.name', 'c2id-implicit'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.issuer', 'http://localhost:8080'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.authorization_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id-login"
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.token_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/token"
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.userinfo_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/userinfo"
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.jwkset_path', 'op-jwks.json'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.redirect_uri', 'https://my.fantastic.rp/cb'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.client_id', 'elasticsearch-rp'
keystoreSetting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.client_secret', 'b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.response_type', 'id_token token'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.principal', 'sub'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.name', 'name'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.mail', 'email'
setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.groups', 'groups'
setting 'xpack.ml.enabled', 'false'

extraConfigFile 'op-jwks.json', idpFixtureProject.file("oidc/op-jwks.json")

setupCommand 'setupTestAdmin',
'bin/elasticsearch-users', 'useradd', "test_admin", '-p', 'x-pack-test-password', '-r', "superuser"

waitCondition = { node, ant ->
File tmpFile = new File(node.cwd, 'wait.success')
ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",
dest: tmpFile.toString(),
username: 'test_admin',
password: 'x-pack-test-password',
ignoreerrors: true,
retries: 10)
return tmpFile.exists()
}
}

thirdPartyAudit.enabled = false
Loading