diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java index 55a7a4f4aaff7..f40613349e056 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java @@ -32,9 +32,11 @@ public class OpenIdConnectRealmSettings { private OpenIdConnectRealmSettings() { } - private static final List SUPPORTED_SIGNATURE_ALGORITHMS = - List.of("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"); + public static final List SUPPORTED_SIGNATURE_ALGORITHMS = + List.of("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"); private static final List RESPONSE_TYPES = List.of("code", "id_token", "id_token token"); + public static final List CLIENT_AUTH_METHODS = List.of("client_secret_basic", "client_secret_post", "client_secret_jwt"); + public static final List SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS = List.of("HS256", "HS384", "HS512"); public static final String TYPE = "oidc"; public static final Setting.AffixSetting RP_CLIENT_ID @@ -78,7 +80,22 @@ private OpenIdConnectRealmSettings() { public static final Setting.AffixSetting> RP_REQUESTED_SCOPES = Setting.affixKeySetting( RealmSettings.realmSettingPrefix(TYPE), "rp.requested_scopes", key -> Setting.listSetting(key, Collections.singletonList("openid"), Function.identity(), Setting.Property.NodeScope)); - + public static final Setting.AffixSetting RP_CLIENT_AUTH_METHOD + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.client_auth_method", + key -> new Setting<>(key, "client_secret_basic", Function.identity(), v -> { + if (CLIENT_AUTH_METHODS.contains(v) == false) { + throw new IllegalArgumentException( + "Invalid value [" + v + "] for [" + key + "]. Allowed values are " + CLIENT_AUTH_METHODS + "}]"); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.client_auth_jwt_signature_algorithm", + key -> new Setting<>(key, "HS384", Function.identity(), v -> { + if (SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS.contains(v) == false) { + throw new IllegalArgumentException( + "Invalid value [" + v + "] for [" + key + "]. Allowed values are " + SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS + "}]"); + } + }, Setting.Property.NodeScope)); public static final Setting.AffixSetting OP_AUTHORIZATION_ENDPOINT = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.authorization_endpoint", key -> Setting.simpleString(key, v -> { @@ -194,8 +211,9 @@ public Iterator> settings() { public static Set> getSettings() { final Set> set = Sets.newHashSet( RP_CLIENT_ID, RP_REDIRECT_URI, RP_RESPONSE_TYPE, RP_REQUESTED_SCOPES, RP_CLIENT_SECRET, RP_SIGNATURE_ALGORITHM, - RP_POST_LOGOUT_REDIRECT_URI, OP_AUTHORIZATION_ENDPOINT, OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT, - OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH, POPULATE_USER_METADATA, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT, + RP_POST_LOGOUT_REDIRECT_URI, RP_CLIENT_AUTH_METHOD, RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM, OP_AUTHORIZATION_ENDPOINT, + OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT, OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH, + POPULATE_USER_METADATA, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT, HTTP_SOCKET_TIMEOUT, HTTP_MAX_CONNECTIONS, HTTP_MAX_ENDPOINT_CONNECTIONS, HTTP_PROXY_HOST, HTTP_PROXY_PORT, HTTP_PROXY_SCHEME, ALLOWED_CLOCK_SKEW); set.addAll(DelegatedAuthorizationSettings.getSettings(TYPE)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java index 1d052b2608e85..7f791d928c6e2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java @@ -22,6 +22,8 @@ import com.nimbusds.oauth2.sdk.ErrorObject; import com.nimbusds.oauth2.sdk.ResponseType; import com.nimbusds.oauth2.sdk.TokenErrorResponse; +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; +import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT; import com.nimbusds.oauth2.sdk.auth.Secret; import com.nimbusds.oauth2.sdk.id.State; import com.nimbusds.oauth2.sdk.token.AccessToken; @@ -85,6 +87,7 @@ import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLConfiguration; import org.elasticsearch.xpack.core.ssl.SSLService; @@ -463,19 +466,36 @@ private void exchangeCodeForToken(AuthorizationCode code, ActionListener params = new ArrayList<>(); for (Map.Entry> entry : codeGrant.toParameters().entrySet()) { // All parameters of AuthorizationCodeGrant are singleton lists params.add(new BasicNameValuePair(entry.getKey(), entry.getValue().get(0))); } + if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { + UsernamePasswordCredentials creds = + new UsernamePasswordCredentials(URLEncoder.encode(rpConfig.getClientId().getValue(), StandardCharsets.UTF_8), + URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8)); + httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null)); + } else if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) { + params.add(new BasicNameValuePair("client_id", rpConfig.getClientId().getValue())); + params.add(new BasicNameValuePair("client_secret", rpConfig.getClientSecret().toString())); + } else if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { + ClientSecretJWT clientSecretJWT = new ClientSecretJWT(rpConfig.getClientId(), opConfig.getTokenEndpoint(), + rpConfig.getClientAuthenticationJwtAlgorithm(), new Secret(rpConfig.getClientSecret().toString())); + for (Map.Entry> entry : clientSecretJWT.toParameters().entrySet()) { + // Both client_assertion and client_assertion_type are singleton lists + params.add(new BasicNameValuePair(entry.getKey(), entry.getValue().get(0))); + } + } else { + tokensListener.onFailure(new ElasticsearchSecurityException("Failed to exchange code for Id Token using Token Endpoint." + + "Expected client authentication method to be one of " + OpenIdConnectRealmSettings.CLIENT_AUTH_METHODS + + " but was [" + rpConfig.getClientAuthenticationMethod() + "]")); + } httpPost.setEntity(new UrlEncodedFormEntity(params)); - httpPost.setHeader("Content-type", "application/x-www-form-urlencoded"); - UsernamePasswordCredentials creds = - new UsernamePasswordCredentials(URLEncoder.encode(rpConfig.getClientId().getValue(), StandardCharsets.UTF_8), - URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8)); - httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null)); SpecialPermission.check(); AccessController.doPrivileged((PrivilegedAction) () -> { + httpClient.execute(httpPost, new FutureCallback() { @Override public void completed(HttpResponse result) { @@ -496,7 +516,7 @@ public void cancelled() { }); return null; }); - } catch (AuthenticationException | UnsupportedEncodingException e) { + } catch (AuthenticationException | UnsupportedEncodingException | JOSEException e) { tokensListener.onFailure( new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint.", e)); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java index af347f5dfb801..2fbdf57ca05fe 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java @@ -12,6 +12,7 @@ import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.ResponseType; import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; import com.nimbusds.oauth2.sdk.id.ClientID; import com.nimbusds.oauth2.sdk.id.Issuer; import com.nimbusds.oauth2.sdk.id.State; @@ -71,6 +72,8 @@ import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_USERINFO_ENDPOINT; import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.POPULATE_USER_METADATA; import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.PRINCIPAL_CLAIM; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_AUTH_METHOD; import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_ID; import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_SECRET; import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_POST_LOGOUT_REDIRECT_URI; @@ -264,9 +267,11 @@ private RelyingPartyConfiguration buildRelyingPartyConfiguration(RealmConfig con requestedScope.add("openid"); } final JWSAlgorithm signatureAlgorithm = JWSAlgorithm.parse(require(config, RP_SIGNATURE_ALGORITHM)); - + final ClientAuthenticationMethod clientAuthenticationMethod = + ClientAuthenticationMethod.parse(require(config, RP_CLIENT_AUTH_METHOD)); + final JWSAlgorithm clientAuthJwtAlgorithm = JWSAlgorithm.parse(require(config, RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM)); return new RelyingPartyConfiguration(clientId, clientSecret, redirectUri, responseType, requestedScope, - signatureAlgorithm, postLogoutRedirectUri); + signatureAlgorithm, clientAuthenticationMethod, clientAuthJwtAlgorithm, postLogoutRedirectUri); } private OpenIdConnectProviderConfiguration buildOpenIdConnectProviderConfiguration(RealmConfig config) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java index ed67974c0b0d2..deae6fcfff011 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java @@ -8,6 +8,7 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.oauth2.sdk.ResponseType; import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; import com.nimbusds.oauth2.sdk.id.ClientID; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.settings.SecureString; @@ -26,15 +27,22 @@ public class RelyingPartyConfiguration { private final Scope requestedScope; private final JWSAlgorithm signatureAlgorithm; private final URI postLogoutRedirectUri; + private final ClientAuthenticationMethod clientAuthenticationMethod; + private final JWSAlgorithm clientAuthenticationJwtAlgorithm; public RelyingPartyConfiguration(ClientID clientId, SecureString clientSecret, URI redirectUri, ResponseType responseType, - Scope requestedScope, JWSAlgorithm algorithm, @Nullable URI postLogoutRedirectUri) { + Scope requestedScope, JWSAlgorithm algorithm, ClientAuthenticationMethod clientAuthenticationMethod, + JWSAlgorithm clientAuthenticationJwtAlgorithm, @Nullable URI postLogoutRedirectUri) { this.clientId = Objects.requireNonNull(clientId, "clientId must be provided"); this.clientSecret = Objects.requireNonNull(clientSecret, "clientSecret must be provided"); this.redirectUri = Objects.requireNonNull(redirectUri, "redirectUri must be provided"); this.responseType = Objects.requireNonNull(responseType, "responseType must be provided"); this.requestedScope = Objects.requireNonNull(requestedScope, "responseType must be provided"); this.signatureAlgorithm = Objects.requireNonNull(algorithm, "algorithm must be provided"); + this.clientAuthenticationMethod = Objects.requireNonNull(clientAuthenticationMethod, + "clientAuthenticationMethod must be provided"); + this.clientAuthenticationJwtAlgorithm = Objects.requireNonNull(clientAuthenticationJwtAlgorithm, + "clientAuthenticationJwtAlgorithm must be provided"); this.postLogoutRedirectUri = postLogoutRedirectUri; } @@ -65,4 +73,12 @@ public JWSAlgorithm getSignatureAlgorithm() { public URI getPostLogoutRedirectUri() { return postLogoutRedirectUri; } + + public ClientAuthenticationMethod getClientAuthenticationMethod() { + return clientAuthenticationMethod; + } + + public JWSAlgorithm getClientAuthenticationJwtAlgorithm() { + return clientAuthenticationJwtAlgorithm; + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java index cbfd633d74c15..c43d59de0d61f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java @@ -27,6 +27,7 @@ import com.nimbusds.jwt.proc.BadJWTException; import com.nimbusds.oauth2.sdk.ResponseType; import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; import com.nimbusds.oauth2.sdk.auth.Secret; import com.nimbusds.oauth2.sdk.id.ClientID; import com.nimbusds.oauth2.sdk.id.Issuer; @@ -886,8 +887,11 @@ private RelyingPartyConfiguration getDefaultRpConfig() throws URISyntaxException new ResponseType("id_token", "token"), new Scope("openid"), JWSAlgorithm.RS384, + ClientAuthenticationMethod.CLIENT_SECRET_BASIC, + JWSAlgorithm.HS384, new URI("https://rp.elastic.co/successfull_logout")); } + private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxException { return new RelyingPartyConfiguration( new ClientID("rp-my"), @@ -896,6 +900,8 @@ private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxExcept new ResponseType("id_token", "token"), new Scope("openid"), JWSAlgorithm.parse(alg), + ClientAuthenticationMethod.CLIENT_SECRET_BASIC, + JWSAlgorithm.HS384, new URI("https://rp.elastic.co/successfull_logout")); } @@ -907,6 +913,8 @@ private RelyingPartyConfiguration getRpConfigNoAccessToken(String alg) throws UR new ResponseType("id_token"), new Scope("openid"), JWSAlgorithm.parse(alg), + ClientAuthenticationMethod.CLIENT_SECRET_BASIC, + JWSAlgorithm.HS384, new URI("https://rp.elastic.co/successfull_logout")); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmSettingsTests.java index 9827be75a2c88..0b8c236844675 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmSettingsTests.java @@ -34,6 +34,43 @@ public void setupEnv() { threadContext = new ThreadContext(globalSettings); } + public void testAllSettings() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_USERINFO_ENDPOINT), "https://op.example.com/userinfo") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.POPULATE_USER_METADATA), randomBoolean()) + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.GROUPS_CLAIM.getClaim()), "group1") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.DN_CLAIM.getClaim()), "uid=sub,ou=people,dc=example,dc=com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getClaim()), "name") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.MAIL_CLAIM.getClaim()), "e@mail.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES), "openid") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_SIGNATURE_ALGORITHM), + randomFrom(OpenIdConnectRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS)) + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_POST_LOGOUT_REDIRECT_URI), "https://my.rp.com/logout") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_AUTH_METHOD), + randomFrom(OpenIdConnectRealmSettings.CLIENT_AUTH_METHODS)) + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM), + randomFrom(OpenIdConnectRealmSettings.SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS)) + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.HTTP_CONNECT_TIMEOUT), "5s") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.HTTP_CONNECTION_READ_TIMEOUT), "5s") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.HTTP_SOCKET_TIMEOUT), "5s") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.HTTP_MAX_CONNECTIONS), "5") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.HTTP_MAX_ENDPOINT_CONNECTIONS), "5") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.HTTP_PROXY_HOST), "host") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.HTTP_PROXY_PORT), "8080") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.HTTP_PROXY_SCHEME), "http") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.ALLOWED_CLOCK_SKEW), "10s"); + settingsBuilder.setSecureSettings(getSecureSettings()); + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + } + public void testIncorrectResponseTypeThrowsError() { final Settings.Builder settingsBuilder = Settings.builder() .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") @@ -116,10 +153,8 @@ public void testMissingTokenEndpointIsAllowedInImplicitFlow() { settingsBuilder.setSecureSettings(getSecureSettings()); final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); assertNotNull(realm); - } - public void testInvalidTokenEndpointThrowsError() { final Settings.Builder settingsBuilder = Settings.builder() .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") @@ -327,6 +362,45 @@ public void testInvalidProxyHostThrowsError() { )); } + public void testInvalidClientAuthenticationMethodThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_AUTH_METHOD), "none") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + settingsBuilder.setSecureSettings(getSecureSettings()); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_AUTH_METHOD))); + } + + public void testInvalidClientAuthenticationJwtAlgorithmThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_AUTH_METHOD), "client_secret_jwt") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM), "AB234"); + settingsBuilder.setSecureSettings(getSecureSettings()); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM))); + } + private MockSecureSettings getSecureSettings() { MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_SECRET), diff --git a/x-pack/qa/oidc-op-tests/build.gradle b/x-pack/qa/oidc-op-tests/build.gradle index 8e95504ec35a7..dec75d293c592 100644 --- a/x-pack/qa/oidc-op-tests/build.gradle +++ b/x-pack/qa/oidc-op-tests/build.gradle @@ -11,82 +11,6 @@ dependencies { testImplementation project(path: xpackModule('security'), configuration: 'testArtifacts') } testFixtures.useFixture ":x-pack:test:idp-fixture", "oidc-provider" - -String ephemeralOpPort -String ephemeralProxyPort -tasks.register("setupPorts") { - // Don't attempt to get ephemeral ports when Docker is not available - onlyIf { idpFixtureProject.postProcessFixture.state.skipped == false } - dependsOn idpFixtureProject.postProcessFixture - doLast { - ephemeralOpPort = idpFixtureProject.postProcessFixture.ext."test.fixtures.oidc-provider.tcp.8080" - ephemeralProxyPort = idpFixtureProject.postProcessFixture.ext."test.fixtures.http-proxy.tcp.8888" - } -} - -integTest { - dependsOn "setupPorts" -} - -testClusters.integTest { - testDistribution = 'DEFAULT' - 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.issuer', 'http://localhost:8080' - setting 'xpack.security.authc.realms.oidc.c2id.op.authorization_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/c2id-login" } - setting 'xpack.security.authc.realms.oidc.c2id.op.token_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/c2id/token" } - setting 'xpack.security.authc.realms.oidc.c2id.op.userinfo_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/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', 'https://my.elasticsearch.org/rp' - keystore '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.issuer', 'http://localhost:8080' - setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.authorization_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/c2id-login" } - setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.token_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/c2id/token" } - setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.userinfo_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/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' - keystore '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' - // OpenID Connect Realm 3 configured to use a proxy - setting 'xpack.security.authc.realms.oidc.c2id-proxy.order', '4' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.op.issuer', 'http://localhost:8080' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.op.authorization_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/c2id-login" } - setting 'xpack.security.authc.realms.oidc.c2id-proxy.op.token_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/c2id/token" } - setting 'xpack.security.authc.realms.oidc.c2id-proxy.op.userinfo_endpoint', { "http://127.0.0.1:${ephemeralOpPort}/c2id/userinfo" } - setting 'xpack.security.authc.realms.oidc.c2id-proxy.op.jwkset_path', 'op-jwks.json' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.rp.redirect_uri', 'https://my.fantastic.rp/cb' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.rp.client_id', 'https://my.elasticsearch.org/rp' - keystore 'xpack.security.authc.realms.oidc.c2id-proxy.rp.client_secret', 'b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.rp.response_type', 'code' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.claims.principal', 'sub' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.claims.name', 'name' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.claims.mail', 'email' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.claims.groups', 'groups' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.http.proxy.host', '127.0.0.1' - setting 'xpack.security.authc.realms.oidc.c2id-proxy.http.proxy.port', {"${ephemeralProxyPort}"} - setting 'xpack.ml.enabled', 'false' - extraConfigFile 'op-jwks.json', idpFixtureProject.file("oidc/op-jwks.json") - - user username: "test_admin", password: "x-pack-test-password" -} +testFixtures.useFixture ":x-pack:test:idp-fixture", "elasticsearch-node" thirdPartyAudit.enabled = false diff --git a/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java b/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java index 6e1dd7c7fdce9..b2fd3907dbd00 100644 --- a/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java +++ b/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java @@ -9,6 +9,7 @@ import net.minidev.json.parser.JSONParser; import org.apache.http.Header; import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.config.RequestConfig; @@ -29,10 +30,12 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; +import org.elasticsearch.client.RestClient; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -47,8 +50,11 @@ import org.junit.Before; import org.junit.BeforeClass; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; +import java.net.URL; +import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -65,16 +71,34 @@ public class OpenIdConnectAuthIT extends ESRestTestCase { private static final String REALM_NAME = "c2id"; private static final String REALM_NAME_IMPLICIT = "c2id-implicit"; private static final String REALM_NAME_PROXY = "c2id-proxy"; + private static final String REALM_NAME_CLIENT_POST_AUTH = "c2id-post"; + private static final String REALM_NAME_CLIENT_JWT_AUTH = "c2id-jwt"; private static final String FACILITATOR_PASSWORD = "f@cilit@t0r"; - private static final String REGISTRATION_URL = "http://127.0.0.1:" + getEphemeralPortFromProperty("8080") + "/c2id/clients"; - private static final String LOGIN_API = "http://127.0.0.1:" + getEphemeralPortFromProperty("8080") + "/c2id-login/api/"; + private static final String REGISTRATION_URL = "http://127.0.0.1:" + getEphemeralTcpPortFromProperty("oidc-provider", "8080") + + "/c2id/clients"; + private static final String LOGIN_API = "http://127.0.0.1:" + getEphemeralTcpPortFromProperty("oidc-provider", "8080") + + "/c2id-login/api/"; + private static final String CLIENT_SECRET = "b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2"; + // SHA256 of this is defined in x-pack/test/idp-fixture/oidc/override.properties + private static final String OP_API_BEARER_TOKEN = "811fa888f3e0fdc9e01d4201bfeee46a"; + private static final String ES_PORT = getEphemeralTcpPortFromProperty("elasticsearch-node", "9200"); + private static Path HTTP_TRUSTSTORE; @Before - public void setupUserAndRoles() throws IOException { + public void setupUserAndRoles() throws Exception { setFacilitatorUser(); setRoleMappings(); } + @BeforeClass + public static void readTrustStore() throws Exception { + final URL resource = OpenIdConnectAuthIT.class.getResource("/tls/testnode.jks"); + if (resource == null) { + throw new FileNotFoundException("Cannot find classpath resource /tls/testnode.jks"); + } + HTTP_TRUSTSTORE = PathUtils.get(resource.toURI()); + } + /** * C2id server only supports dynamic registration, so we can't pre-seed it's config with our client data. Execute only once */ @@ -85,40 +109,82 @@ public static void registerClients() throws Exception { "\"grant_types\": [\"authorization_code\"]," + "\"response_types\": [\"code\"]," + "\"preferred_client_id\":\"https://my.elasticsearch.org/rp\"," + - "\"preferred_client_secret\":\"b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2\"," + - "\"redirect_uris\": [\"https://my.fantastic.rp/cb\"]" + + "\"preferred_client_secret\":\"" + CLIENT_SECRET + "\"," + + "\"redirect_uris\": [\"https://my.fantastic.rp/cb\"]," + + "\"token_endpoint_auth_method\":\"client_secret_basic\"" + "}"; String implicitClient = "{" + "\"grant_types\": [\"implicit\"]," + "\"response_types\": [\"token id_token\"]," + "\"preferred_client_id\":\"elasticsearch-rp\"," + - "\"preferred_client_secret\":\"b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2\"," + + "\"preferred_client_secret\":\"" + CLIENT_SECRET + "\"," + "\"redirect_uris\": [\"https://my.fantastic.rp/cb\"]" + "}"; + String postClient = "{" + + "\"grant_types\": [\"authorization_code\"]," + + "\"response_types\": [\"code\"]," + + "\"preferred_client_id\":\"elasticsearch-post\"," + + "\"preferred_client_secret\":\"" + CLIENT_SECRET + "\"," + + "\"redirect_uris\": [\"https://my.fantastic.rp/cb\"]," + + "\"token_endpoint_auth_method\":\"client_secret_post\"" + + "}"; + String jwtClient = "{" + + "\"grant_types\": [\"authorization_code\"]," + + "\"response_types\": [\"code\"]," + + "\"preferred_client_id\":\"elasticsearch-post-jwt\"," + + "\"preferred_client_secret\":\"" + CLIENT_SECRET + "\"," + + "\"redirect_uris\": [\"https://my.fantastic.rp/cb\"]," + + "\"token_endpoint_auth_method\":\"client_secret_jwt\"" + + "}"; HttpPost httpPost = new HttpPost(REGISTRATION_URL); final BasicHttpContext context = new BasicHttpContext(); httpPost.setEntity(new StringEntity(codeClient, ContentType.APPLICATION_JSON)); httpPost.setHeader("Accept", "application/json"); httpPost.setHeader("Content-type", "application/json"); - httpPost.setHeader("Authorization", "Bearer 811fa888f3e0fdc9e01d4201bfeee46a"); - CloseableHttpResponse response = SocketAccess.doPrivileged(() -> httpClient.execute(httpPost, context)); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - httpPost.setEntity(new StringEntity(implicitClient, ContentType.APPLICATION_JSON)); + httpPost.setHeader("Authorization", "Bearer " + OP_API_BEARER_TOKEN); + HttpPost httpPost2 = new HttpPost(REGISTRATION_URL); httpPost2.setEntity(new StringEntity(implicitClient, ContentType.APPLICATION_JSON)); httpPost2.setHeader("Accept", "application/json"); httpPost2.setHeader("Content-type", "application/json"); - httpPost2.setHeader("Authorization", "Bearer 811fa888f3e0fdc9e01d4201bfeee46a"); - CloseableHttpResponse response2 = SocketAccess.doPrivileged(() -> httpClient.execute(httpPost2, context)); - assertThat(response2.getStatusLine().getStatusCode(), equalTo(200)); + httpPost2.setHeader("Authorization", "Bearer " + OP_API_BEARER_TOKEN); + + HttpPost httpPost3 = new HttpPost(REGISTRATION_URL); + httpPost3.setEntity(new StringEntity(postClient, ContentType.APPLICATION_JSON)); + httpPost3.setHeader("Accept", "application/json"); + httpPost3.setHeader("Content-type", "application/json"); + httpPost3.setHeader("Authorization", "Bearer " + OP_API_BEARER_TOKEN); + + HttpPost httpPost4 = new HttpPost(REGISTRATION_URL); + httpPost4.setEntity(new StringEntity(jwtClient, ContentType.APPLICATION_JSON)); + httpPost4.setHeader("Accept", "application/json"); + httpPost4.setHeader("Content-type", "application/json"); + httpPost4.setHeader("Authorization", "Bearer " + OP_API_BEARER_TOKEN); + + SocketAccess.doPrivileged(() -> { + try (CloseableHttpResponse response = httpClient.execute(httpPost, context)) { + assertThat(response.getStatusLine().getStatusCode(), equalTo(201)); + } + try (CloseableHttpResponse response2 = httpClient.execute(httpPost2, context)) { + assertThat(response2.getStatusLine().getStatusCode(), equalTo(201)); + } + try (CloseableHttpResponse response3 = httpClient.execute(httpPost3, context)) { + assertThat(response3.getStatusLine().getStatusCode(), equalTo(201)); + } + try (CloseableHttpResponse response4 = httpClient.execute(httpPost4, context)) { + assertThat(response4.getStatusLine().getStatusCode(), equalTo(201)); + } + }); } } @Override protected Settings restAdminSettings() { - String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); + String token = basicAuthHeaderValue("x_pack_rest_user", new SecureString("x-pack-test-password".toCharArray())); return Settings.builder() .put(ThreadContext.PREFIX + ".Authorization", token) + .put(TRUSTSTORE_PATH, HTTP_TRUSTSTORE) + .put(TRUSTSTORE_PASSWORD, "testnode") .build(); } @@ -193,19 +259,21 @@ private String authenticateAtOP(URI opAuthUri) throws Exception { } } - private static String getEphemeralPortFromProperty(String port) { - String key = "test.fixtures.oidc-provider.tcp." + port; + private static String getEphemeralTcpPortFromProperty(String service, String port) { + String key = "test.fixtures." + service + ".tcp." + port; final String value = System.getProperty(key); assertNotNull("Expected the actual value for port " + port + " to be in system property " + key, value); return value; } - private Map callAuthenticateApiUsingAccessToken(String accessToken) throws IOException { + private Map callAuthenticateApiUsingAccessToken(String accessToken) throws Exception { Request request = new Request("GET", "/_security/_authenticate"); RequestOptions.Builder options = request.getOptions().toBuilder(); options.addHeader("Authorization", "Bearer " + accessToken); request.setOptions(options); - return entityAsMap(client().performRequest(request)); + try (RestClient restClient = getClient()) { + return entityAsMap(restClient.performRequest(request)); + } } private T execute(CloseableHttpClient client, HttpEntityEnclosingRequestBase request, @@ -250,10 +318,25 @@ public void testAuthenticateWithCodeFlow() throws Exception { verifyElasticsearchAccessTokenForCodeFlow(tokens.v1()); } + public void testAuthenticateWithCodeFlowAndClientPost() throws Exception { + final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME_CLIENT_POST_AUTH); + final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri()); + Tuple tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(), + prepareAuthResponse.getNonce(), REALM_NAME_CLIENT_POST_AUTH); + verifyElasticsearchAccessTokenForCodeFlow(tokens.v1()); + } + + public void testAuthenticateWithCodeFlowAndClientJwtPost() throws Exception { + final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME_CLIENT_JWT_AUTH); + final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri()); + Tuple tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(), + prepareAuthResponse.getNonce(), REALM_NAME_CLIENT_JWT_AUTH); + verifyElasticsearchAccessTokenForCodeFlow(tokens.v1()); + } + public void testAuthenticateWithImplicitFlow() throws Exception { final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME_IMPLICIT); final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri()); - Tuple tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(), prepareAuthResponse.getNonce(), REALM_NAME_IMPLICIT); verifyElasticsearchAccessTokenForImplicitFlow(tokens.v1()); @@ -280,7 +363,7 @@ public void testAuthenticateWithCodeFlowFailsForWrongRealm() throws Exception { assertThat(401, equalTo(e.getResponse().getStatusLine().getStatusCode())); } - private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throws IOException { + private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throws Exception { final Map map = callAuthenticateApiUsingAccessToken(accessToken); logger.info("Authentication with token Response: " + map); assertThat(map.get("username"), equalTo("alice")); @@ -289,10 +372,10 @@ private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throw assertThat(map.get("metadata"), instanceOf(Map.class)); final Map metadata = (Map) map.get("metadata"); assertThat(metadata.get("oidc(sub)"), equalTo("alice")); - assertThat(metadata.get("oidc(iss)"), equalTo("http://localhost:8080")); + assertThat(metadata.get("oidc(iss)"), equalTo("http://oidc-provider:8080/c2id")); } - private void verifyElasticsearchAccessTokenForImplicitFlow(String accessToken) throws IOException { + private void verifyElasticsearchAccessTokenForImplicitFlow(String accessToken) throws Exception { final Map map = callAuthenticateApiUsingAccessToken(accessToken); logger.info("Authentication with token Response: " + map); assertThat(map.get("username"), equalTo("alice")); @@ -301,22 +384,23 @@ private void verifyElasticsearchAccessTokenForImplicitFlow(String accessToken) t assertThat(map.get("metadata"), instanceOf(Map.class)); final Map metadata = (Map) map.get("metadata"); assertThat(metadata.get("oidc(sub)"), equalTo("alice")); - assertThat(metadata.get("oidc(iss)"), equalTo("http://localhost:8080")); + assertThat(metadata.get("oidc(iss)"), equalTo("http://oidc-provider:8080/c2id")); } - private PrepareAuthResponse getRedirectedFromFacilitator(String realmName) throws Exception { final Map body = Collections.singletonMap("realm", realmName); - Request request = buildRequest("POST", "/_security/oidc/prepare", body, facilitatorAuth()); - final Response prepare = client().performRequest(request); - assertOK(prepare); - final Map responseBody = parseResponseAsMap(prepare.getEntity()); - logger.info("Created OpenIDConnect authentication request {}", responseBody); - final String state = (String) responseBody.get("state"); - final String nonce = (String) responseBody.get("nonce"); - final String authUri = (String) responseBody.get("redirect"); - final String realm = (String) responseBody.get("realm"); - return new PrepareAuthResponse(new URI(authUri), state, nonce, realm); + Request request = buildRequest("POST", "/_security/oidc/prepare?error_trace=true", body, facilitatorAuth()); + try (RestClient restClient = getClient()) { + final Response prepare = restClient.performRequest(request); + assertOK(prepare); + final Map responseBody = parseResponseAsMap(prepare.getEntity()); + logger.info("Created OpenIDConnect authentication request {}", responseBody); + final String state = (String) responseBody.get("state"); + final String nonce = (String) responseBody.get("nonce"); + final String authUri = (String) responseBody.get("redirect"); + final String realm = (String) responseBody.get("realm"); + return new PrepareAuthResponse(new URI(authUri), state, nonce, realm); + } } private Tuple completeAuthentication(String redirectUri, String state, String nonce, @Nullable String realm) @@ -325,17 +409,19 @@ private Tuple completeAuthentication(String redirectUri, String body.put("redirect_uri", redirectUri); body.put("state", state); body.put("nonce", nonce); - if (realm != null){ + if (realm != null) { body.put("realm", realm); } Request request = buildRequest("POST", "/_security/oidc/authenticate", body, facilitatorAuth()); - final Response authenticate = client().performRequest(request); - assertOK(authenticate); - final Map responseBody = parseResponseAsMap(authenticate.getEntity()); - logger.info(" OpenIDConnect authentication response {}", responseBody); - assertNotNull(responseBody.get("access_token")); - assertNotNull(responseBody.get("refresh_token")); - return Tuple.tuple(responseBody.get("access_token").toString(), responseBody.get("refresh_token").toString()); + try (RestClient restClient = getClient()) { + final Response authenticate = restClient.performRequest(request); + assertOK(authenticate); + final Map responseBody = parseResponseAsMap(authenticate.getEntity()); + logger.info(" OpenIDConnect authentication response {}", responseBody); + assertNotNull(responseBody.get("access_token")); + assertNotNull(responseBody.get("refresh_token")); + return Tuple.tuple(responseBody.get("access_token").toString(), responseBody.get("refresh_token").toString()); + } } private Request buildRequest(String method, String endpoint, Map body, Header... headers) throws IOException { @@ -371,47 +457,56 @@ private void assertHttpOk(StatusLine status) { * We create a user named `facilitator` with the appropriate privileges ( `manage_oidc` ). A facilitator web app * would need to create one also, in order to access the OIDC related APIs on behalf of the user. */ - private void setFacilitatorUser() throws IOException { - Request createRoleRequest = new Request("PUT", "/_security/role/facilitator"); - createRoleRequest.setJsonEntity("{ \"cluster\" : [\"manage_oidc\", \"manage_token\"] }"); - adminClient().performRequest(createRoleRequest); - Request createUserRequest = new Request("PUT", "/_security/user/facilitator"); - createUserRequest.setJsonEntity("{ \"password\" : \"" + FACILITATOR_PASSWORD + "\", \"roles\" : [\"facilitator\"] }"); - adminClient().performRequest(createUserRequest); + private void setFacilitatorUser() throws Exception { + try (RestClient restClient = getClient()) { + Request createRoleRequest = new Request("PUT", "/_security/role/facilitator"); + createRoleRequest.setJsonEntity("{ \"cluster\" : [\"manage_oidc\", \"manage_token\"] }"); + restClient.performRequest(createRoleRequest); + Request createUserRequest = new Request("PUT", "/_security/user/facilitator"); + createUserRequest.setJsonEntity("{ \"password\" : \"" + FACILITATOR_PASSWORD + "\", \"roles\" : [\"facilitator\"] }"); + restClient.performRequest(createUserRequest); + } } - private void setRoleMappings() throws IOException { - Request createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_kibana"); - createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"kibana_admin\"]," + - "\"enabled\": true," + - "\"rules\": {" + - " \"any\" : [" + - " {\"field\": { \"realm.name\": \"" + REALM_NAME + "\"} }," + - " {\"field\": { \"realm.name\": \"" + REALM_NAME_PROXY + "\"} }" + - " ]" + - "}" + - "}"); - adminClient().performRequest(createRoleMappingRequest); - - createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_limited"); - createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"limited_user\"]," + - "\"enabled\": true," + - "\"rules\": {" + - "\"field\": { \"realm.name\": \"" + REALM_NAME_IMPLICIT + "\"}" + - "}" + - "}"); - adminClient().performRequest(createRoleMappingRequest); - - createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_auditor"); - createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"auditor\"]," + - "\"enabled\": true," + - "\"rules\": {" + - "\"field\": { \"groups\": \"audit\"}" + - "}" + - "}"); - adminClient().performRequest(createRoleMappingRequest); + private void setRoleMappings() throws Exception { + try (RestClient restClient = getClient()) { + Request createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_kibana"); + createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"kibana_admin\"]," + + "\"enabled\": true," + + "\"rules\": {" + + " \"any\" : [" + + " {\"field\": { \"realm.name\": \"" + REALM_NAME + "\"} }," + + " {\"field\": { \"realm.name\": \"" + REALM_NAME_PROXY + "\"} }," + + " {\"field\": { \"realm.name\": \"" + REALM_NAME_CLIENT_POST_AUTH + "\"} }," + + " {\"field\": { \"realm.name\": \"" + REALM_NAME_CLIENT_JWT_AUTH + "\"} }" + + " ]" + + "}" + + "}"); + restClient.performRequest(createRoleMappingRequest); + + createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_limited"); + createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"limited_user\"]," + + "\"enabled\": true," + + "\"rules\": {" + + "\"field\": { \"realm.name\": \"" + REALM_NAME_IMPLICIT + "\"}" + + "}" + + "}"); + restClient.performRequest(createRoleMappingRequest); + + createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_auditor"); + createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"auditor\"]," + + "\"enabled\": true," + + "\"rules\": {" + + "\"field\": { \"groups\": \"audit\"}" + + "}" + + "}"); + restClient.performRequest(createRoleMappingRequest); + } } + private RestClient getClient() throws Exception { + return buildClient(restAdminSettings(), new HttpHost[]{new HttpHost("localhost", Integer.parseInt(ES_PORT), "https")}); + } /** * Simple POJO encapsulating a response to calling /_security/oidc/prepare diff --git a/x-pack/qa/oidc-op-tests/src/test/resources/tls/testnode.jks b/x-pack/qa/oidc-op-tests/src/test/resources/tls/testnode.jks new file mode 100644 index 0000000000000..7c0c4f1aae7fb Binary files /dev/null and b/x-pack/qa/oidc-op-tests/src/test/resources/tls/testnode.jks differ diff --git a/x-pack/test/idp-fixture/build.gradle b/x-pack/test/idp-fixture/build.gradle index c55123e08d0f1..cb9e49d7d693e 100644 --- a/x-pack/test/idp-fixture/build.gradle +++ b/x-pack/test/idp-fixture/build.gradle @@ -1,4 +1,37 @@ -apply plugin: 'elasticsearch.build' +import org.elasticsearch.gradle.VersionProperties +import org.elasticsearch.gradle.Architecture + apply plugin: 'elasticsearch.test.fixtures' +apply plugin: 'elasticsearch.internal-distribution-download' + +task copyKeystore(type: Sync) { + from project(':x-pack:plugin:core') + .file('src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks') + into "${buildDir}/certs" + doLast { + file("${buildDir}/certs").setReadable(true, false) + file("${buildDir}/certs/testnode.jks").setReadable(true, false) + } +} + +elasticsearch_distributions { + docker { + type = 'docker' + architecture = Architecture.current() + flavor = System.getProperty('tests.distribution', 'default') + version = VersionProperties.getElasticsearch() + failIfUnavailable = false // This ensures we skip this testing if Docker is unavailable + } +} +preProcessFixture { + dependsOn copyKeystore, elasticsearch_distributions.docker + doLast { + File file = file("${buildDir}/logs/node1") + file.mkdirs() + file.setWritable(true, false) + } +} -test.enabled = false \ No newline at end of file +tasks.named('composeUp').configure { + dependsOn "preProcessFixture" +} diff --git a/x-pack/test/idp-fixture/docker-compose.yml b/x-pack/test/idp-fixture/docker-compose.yml index 281badacf3a88..5c6bc0762217e 100644 --- a/x-pack/test/idp-fixture/docker-compose.yml +++ b/x-pack/test/idp-fixture/docker-compose.yml @@ -1,8 +1,121 @@ -version: '3.1' +version: '3.7' services: + elasticsearch-node: + image: elasticsearch:test + environment: + - node.name=elasticsearch-node + - cluster.initial_master_nodes=elasticsearch-node + - cluster.name=elasticsearch-node + - bootstrap.memory_lock=true + - network.publish_host=127.0.0.1 + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - path.repo=/tmp/es-repo + - node.attr.testattr=test + - cluster.routing.allocation.disk.watermark.low=1b + - cluster.routing.allocation.disk.watermark.high=1b + - cluster.routing.allocation.disk.watermark.flood_stage=1b + - node.store.allow_mmap=false + - xpack.license.self_generated.type=trial + - xpack.security.enabled=true + - xpack.security.http.ssl.enabled=true + - xpack.security.http.ssl.keystore.path=testnode.jks + - xpack.security.authc.token.enabled=true + - xpack.security.authc.realms.file.file.order=0 + - xpack.security.authc.realms.native.native.order=1 + - xpack.security.authc.realms.oidc.c2id.order=2 + - xpack.security.authc.realms.oidc.c2id.op.issuer=http://oidc-provider:8080/c2id + - xpack.security.authc.realms.oidc.c2id.op.authorization_endpoint=http://oidc-provider:8080/c2id-login + - xpack.security.authc.realms.oidc.c2id.op.token_endpoint=http://oidc-provider:8080/c2id/token + - xpack.security.authc.realms.oidc.c2id.op.userinfo_endpoint=http://oidc-provider:8080/c2id/userinfo + - xpack.security.authc.realms.oidc.c2id.op.jwkset_path=op-jwks.json + - xpack.security.authc.realms.oidc.c2id.rp.redirect_uri=https://my.fantastic.rp/cb + - xpack.security.authc.realms.oidc.c2id.rp.client_id=https://my.elasticsearch.org/rp + - xpack.security.authc.realms.oidc.c2id.rp.response_type=code + - xpack.security.authc.realms.oidc.c2id.claims.principal=sub + - xpack.security.authc.realms.oidc.c2id.claims.name=name + - xpack.security.authc.realms.oidc.c2id.claims.mail=email + - xpack.security.authc.realms.oidc.c2id.claims.groups=groups + - xpack.security.authc.realms.oidc.c2id-implicit.order=3 + - xpack.security.authc.realms.oidc.c2id-implicit.op.issuer=http://oidc-provider:8080/c2id + - xpack.security.authc.realms.oidc.c2id-implicit.op.authorization_endpoint=http://oidc-provider:8080/c2id-login + - xpack.security.authc.realms.oidc.c2id-implicit.op.token_endpoint=http://oidc-provider:8080/c2id/token + - xpack.security.authc.realms.oidc.c2id-implicit.op.userinfo_endpoint=http://oidc-provider:8080/c2id/userinfo + - xpack.security.authc.realms.oidc.c2id-implicit.op.jwkset_path=op-jwks.json + - xpack.security.authc.realms.oidc.c2id-implicit.rp.redirect_uri=https://my.fantastic.rp/cb + - xpack.security.authc.realms.oidc.c2id-implicit.rp.client_id=elasticsearch-rp + - xpack.security.authc.realms.oidc.c2id-implicit.rp.response_type=id_token token + - xpack.security.authc.realms.oidc.c2id-implicit.claims.principal=sub + - xpack.security.authc.realms.oidc.c2id-implicit.claims.name=name + - xpack.security.authc.realms.oidc.c2id-implicit.claims.mail=email + - xpack.security.authc.realms.oidc.c2id-implicit.claims.groups=groups + - xpack.security.authc.realms.oidc.c2id-proxy.order=4 + - xpack.security.authc.realms.oidc.c2id-proxy.op.issuer=http://oidc-provider:8080/c2id + - xpack.security.authc.realms.oidc.c2id-proxy.op.authorization_endpoint=http://oidc-provider:8080/c2id-login + - xpack.security.authc.realms.oidc.c2id-proxy.op.token_endpoint=http://oidc-provider:8080/c2id/token + - xpack.security.authc.realms.oidc.c2id-proxy.op.userinfo_endpoint=http://oidc-provider:8080/c2id/userinfo + - xpack.security.authc.realms.oidc.c2id-proxy.op.jwkset_path=op-jwks.json + - xpack.security.authc.realms.oidc.c2id-proxy.rp.redirect_uri=https://my.fantastic.rp/cb + - xpack.security.authc.realms.oidc.c2id-proxy.rp.client_id=https://my.elasticsearch.org/rp + - xpack.security.authc.realms.oidc.c2id-proxy.rp.response_type=code + - xpack.security.authc.realms.oidc.c2id-proxy.claims.principal=sub + - xpack.security.authc.realms.oidc.c2id-proxy.claims.name=name + - xpack.security.authc.realms.oidc.c2id-proxy.claims.mail=email + - xpack.security.authc.realms.oidc.c2id-proxy.claims.groups=groups + - xpack.security.authc.realms.oidc.c2id-proxy.http.proxy.host=http-proxy + - xpack.security.authc.realms.oidc.c2id-proxy.http.proxy.port=8888 + - xpack.security.authc.realms.oidc.c2id-post.order=5 + - xpack.security.authc.realms.oidc.c2id-post.op.issuer=http://oidc-provider:8080/c2id + - xpack.security.authc.realms.oidc.c2id-post.op.authorization_endpoint=http://oidc-provider:8080/c2id-login + - xpack.security.authc.realms.oidc.c2id-post.op.token_endpoint=http://oidc-provider:8080/c2id/token + - xpack.security.authc.realms.oidc.c2id-post.op.userinfo_endpoint=http://oidc-provider:8080/c2id/userinfo + - xpack.security.authc.realms.oidc.c2id-post.op.jwkset_path=op-jwks.json + - xpack.security.authc.realms.oidc.c2id-post.rp.redirect_uri=https://my.fantastic.rp/cb + - xpack.security.authc.realms.oidc.c2id-post.rp.client_id=elasticsearch-post + - xpack.security.authc.realms.oidc.c2id-post.rp.client_auth_method=client_secret_post + - xpack.security.authc.realms.oidc.c2id-post.rp.response_type=code + - xpack.security.authc.realms.oidc.c2id-post.claims.principal=sub + - xpack.security.authc.realms.oidc.c2id-post.claims.name=name + - xpack.security.authc.realms.oidc.c2id-post.claims.mail=email + - xpack.security.authc.realms.oidc.c2id-post.claims.groups=groups + - xpack.security.authc.realms.oidc.c2id-jwt.order=6 + - xpack.security.authc.realms.oidc.c2id-jwt.op.issuer=http://oidc-provider:8080/c2id + - xpack.security.authc.realms.oidc.c2id-jwt.op.authorization_endpoint=http://oidc-provider:8080/c2id-login + - xpack.security.authc.realms.oidc.c2id-jwt.op.token_endpoint=http://oidc-provider:8080/c2id/token + - xpack.security.authc.realms.oidc.c2id-jwt.op.userinfo_endpoint=http://oidc-provider:8080/c2id/userinfo + - xpack.security.authc.realms.oidc.c2id-jwt.op.jwkset_path=op-jwks.json + - xpack.security.authc.realms.oidc.c2id-jwt.rp.redirect_uri=https://my.fantastic.rp/cb + - xpack.security.authc.realms.oidc.c2id-jwt.rp.client_id=elasticsearch-post-jwt + - xpack.security.authc.realms.oidc.c2id-jwt.rp.client_auth_method=client_secret_jwt + - xpack.security.authc.realms.oidc.c2id-jwt.rp.response_type=code + - xpack.security.authc.realms.oidc.c2id-jwt.claims.principal=sub + - xpack.security.authc.realms.oidc.c2id-jwt.claims.name=name + - xpack.security.authc.realms.oidc.c2id-jwt.claims.mail=email + - xpack.security.authc.realms.oidc.c2id-jwt.claims.groups=groups + volumes: + - ./build/logs/node1:/usr/share/elasticsearch/logs + - ./build/certs/testnode.jks:/usr/share/elasticsearch/config/testnode.jks + - ./docker-test-entrypoint.sh:/docker-test-entrypoint.sh + - ./oidc/op-jwks.json:/usr/share/elasticsearch/config/op-jwks.json + ports: + - "9200" + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + entrypoint: /docker-test-entrypoint.sh + healthcheck: + start_period: 15s + test: ["CMD", "curl", "-f", "-u", "x_pack_rest_user:x-pack-test-password", "-k", "https://localhost:9200"] + interval: 10s + timeout: 2s + retries: 5 + openldap: command: --copy-service --loglevel debug - image: "osixia/openldap:1.2.3" + image: "osixia/openldap:1.4.0" ports: - "389" - "636" @@ -41,11 +154,13 @@ services: - ./idp/shib-jetty-base/start.d/ssl.ini:/opt/shib-jetty-base/start.d/ssl.ini oidc-provider: - image: "c2id/c2id-server:7.8" + image: "c2id/c2id-server:9.5" depends_on: - http-proxy ports: - "8080" + expose: + - "8080" volumes: - ./oidc/override.properties:/etc/c2id/override.properties @@ -55,3 +170,5 @@ services: - ./oidc/nginx.conf:/etc/nginx/nginx.conf ports: - "8888" + expose: + - "8888" diff --git a/x-pack/test/idp-fixture/docker-test-entrypoint.sh b/x-pack/test/idp-fixture/docker-test-entrypoint.sh new file mode 100755 index 0000000000000..b0309360e89c5 --- /dev/null +++ b/x-pack/test/idp-fixture/docker-test-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash +cd /usr/share/elasticsearch/bin/ +./elasticsearch-users useradd x_pack_rest_user -p x-pack-test-password -r superuser || true +echo "testnode" >/tmp/password +echo "b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2" >/tmp/client_secret +cat /tmp/password | ./elasticsearch-keystore add -x -f -v 'xpack.security.http.ssl.keystore.secure_password' +cat /tmp/client_secret | ./elasticsearch-keystore add -x -f -v 'xpack.security.authc.realms.oidc.c2id.rp.client_secret' +cat /tmp/client_secret | ./elasticsearch-keystore add -x -f -v 'xpack.security.authc.realms.oidc.c2id-implicit.rp.client_secret' +cat /tmp/client_secret | ./elasticsearch-keystore add -x -f -v 'xpack.security.authc.realms.oidc.c2id-proxy.rp.client_secret' +cat /tmp/client_secret | ./elasticsearch-keystore add -x -f -v 'xpack.security.authc.realms.oidc.c2id-post.rp.client_secret' +cat /tmp/client_secret | ./elasticsearch-keystore add -x -f -v 'xpack.security.authc.realms.oidc.c2id-jwt.rp.client_secret' +/usr/local/bin/docker-entrypoint.sh | tee >/usr/share/elasticsearch/logs/console.log diff --git a/x-pack/test/idp-fixture/oidc/override.properties b/x-pack/test/idp-fixture/oidc/override.properties index 888bde9acb48e..fe4ba4a6e894f 100644 --- a/x-pack/test/idp-fixture/oidc/override.properties +++ b/x-pack/test/idp-fixture/oidc/override.properties @@ -1,4 +1,4 @@ -op.issuer=http://localhost:8080 -op.authz.endpoint=http://localhost:8080/c2id-login/ +op.issuer=http://oidc-provider:8080/c2id +op.authz.endpoint=http://oidc-provider:8080/c2id-login/ op.reg.apiAccessTokenSHA256=d1c4fa70d9ee708d13cfa01daa0e060a05a2075a53c5cc1ad79e460e96ab5363 -jose.jwkSer=RnVsbCBrZXk6CnsKICAia2V5cyI6IFsKICAgIHsKICAgICAgInAiOiAiLXhhN2d2aW5tY3N3QXU3Vm1mV2loZ2o3U3gzUzhmd2dFSTdMZEVveW5FU1RzcElaeUY5aHc0NVhQZmI5VHlpbzZsOHZTS0F5RmU4T2lOalpkNE1Ra0ttYlJzTmxxR1Y5VlBoWF84UG1JSm5mcGVhb3E5YnZfU0k1blZHUl9zYUUzZE9sTEE2VWpaS0lsRVBNb0ZuRlZCMUFaUU9qQlhRRzZPTDg2eDZ2NHMwIiwKICAgICAgImt0eSI6ICJSU0EiLAogICAgICAicSI6ICJ2Q3pDQUlpdHV0MGx1V0djQloyLUFabURLc1RxNkkxcUp0RmlEYkIyZFBNQVlBNldOWTdaWEZoVWxsSjJrT2ZELWdlYjlkYkN2ODBxNEwyajVZSjZoOTBUc1NRWWVHRlljN1lZMGdCMU5VR3l5cXctb29QN0EtYlJmMGI3b3I4ajZJb0hzQTZKa2JranN6c3otbkJ2U2RmUURlZkRNSVc3Ni1ZWjN0c2hsY2MiLAogICAgICAiZCI6ICJtbFBOcm1zVVM5UmJtX1I5SElyeHdmeFYzZnJ2QzlaQktFZzRzc1ZZaThfY09lSjV2U1hyQV9laEtwa2g4QVhYaUdWUGpQbVlyd29xQzFVUksxUkZmLVg0dG10emV2OUVHaU12Z0JCaEF5RkdTSUd0VUNla2x4Q2dhb3BpMXdZSU1Bd0M0STZwMUtaZURxTVNCWVZGeHA5ZWlJZ2pwb05JbV9lR3hXUUs5VHNnYmk5T3lyc1VqaE9KLVczN2JVMEJWUU56UXpxODhCcGxmNzM3VmV1dy1FeDZaMk1iWXR3SWdfZ0JVb0JEZ0NrZkhoOVE4MElYcEZRV0x1RzgwenFrdkVwTHZ0RWxLbDRvQ3BHVnBjcmFUOFNsOGpYc3FDT1k0dnVRT19LRVUzS2VPNUNJbHd4eEhJYXZjQTE5cHFpSWJ5cm1LbThxS0ZEWHluUFJMSGFNZ1EiLAogICAgICAiZSI6ICJBUUFCIiwKICAgICAgImtpZCI6ICJyc2EzODRfMjA0OCIsCiAgICAgICJxaSI6ICJzMldTamVrVDl3S2JPbk9neGNoaDJPY3VubzE2Y20wS281Z3hoUWJTdVMyMldfUjJBR2ZVdkRieGF0cTRLakQ3THo3X1k2TjdTUkwzUVpudVhoZ1djeXgyNGhrUGppQUZLNmlkYVZKQzJqQmgycEZTUDVTNXZxZ0lsME12eWY4NjlwdkN4S0NzaGRKMGdlRWhveE93VkRPYXJqdTl2Zm9IQV90LWJoRlZrUnciLAogICAgICAiZHAiOiAiQlJhQTFqYVRydG9mTHZBSUJBYW1OSEVhSm51RU9zTVJJMFRCZXFuR1BNUm0tY2RjSG1OUVo5WUtqb2JpdXlmbnhGZ0piVDlSeElBRG0ySkpoZEp5RTN4Y1dTSzhmSjBSM1Jick1aT1dwako0QmJTVzFtU1VtRnlKTGxib3puRFhZR2RaZ1hzS0o1UkFrRUNQZFBCY3YwZVlkbk9NYWhfZndfaFZoNjRuZ2tFIiwKICAgICAgImFsZyI6ICJSU0EzODQiLAogICAgICAiZHEiOiAiUFJoVERKVlR3cDNXaDZfWFZrTjIwMUlpTWhxcElrUDN1UTYyUlRlTDNrQ2ZXSkNqMkZPLTRxcVRIQk0tQjZJWUVPLXpoVWZyQnhiMzJ1djNjS2JDWGFZN3BJSFJxQlFEQWQ2WGhHYzlwc0xqNThXd3VGY2RncERJYUFpRjNyc3NUMjJ4UFVvYkJFTVdBalV3bFJrNEtNTjItMnpLQk5FR3lIcDIzOUpKdnpVIiwKICAgICAgIm4iOiAidUpDWDVDbEZpM0JnTXBvOWhRSVZ2SDh0Vi1jLTVFdG5OeUZxVm91R3NlNWwyUG92MWJGb0tsRllsU25YTzNWUE9KRWR3azNDdl9VT0UtQzlqZERYRHpvS3Z4RURaTVM1TDZWMFpIVEJoNndIOV9iN3JHSlBxLV9RdlNkejczSzZxbHpGaUtQamRvdTF6VlFYTmZfblBZbnRnQkdNRUtBc1pRNGp0cWJCdE5lV0h0MF9UM001cEktTV9KNGVlRWpCTW95TkZuU2ExTEZDVmZRNl9YVnpjelp1TlRGMlh6UmdRWkFmcmJGRXZ6eXR1TzVMZTNTTXFrUUFJeDhFQmkwYXVlRUNqNEQ4cDNVNXFVRG92NEF2VnRJbUZlbFJvb1pBMHJtVW1KRHJ4WExrVkhuVUpzaUF6ZW9TLTNBSnV1bHJkMGpuNjJ5VjZHV2dFWklZMVNlZVd3IgogICAgfQogIF0KfQo \ No newline at end of file +jose.jwkSer=RnVsbCBrZXk6CnsKICAia2V5cyI6IFsKICAgIHsKICAgICAgInAiOiAiLXhhN2d2aW5tY3N3QXU3Vm1mV2loZ2o3U3gzUzhmd2dFSTdMZEVveW5FU1RzcElaeUY5aHc0NVhQZmI5VHlpbzZsOHZTS0F5RmU4T2lOalpkNE1Ra0ttYlJzTmxxR1Y5VlBoWF84UG1JSm5mcGVhb3E5YnZfU0k1blZHUl9zYUUzZE9sTEE2VWpaS0lsRVBNb0ZuRlZCMUFaUU9qQlhRRzZPTDg2eDZ2NHMwIiwKICAgICAgImt0eSI6ICJSU0EiLAogICAgICAicSI6ICJ2Q3pDQUlpdHV0MGx1V0djQloyLUFabURLc1RxNkkxcUp0RmlEYkIyZFBNQVlBNldOWTdaWEZoVWxsSjJrT2ZELWdlYjlkYkN2ODBxNEwyajVZSjZoOTBUc1NRWWVHRlljN1lZMGdCMU5VR3l5cXctb29QN0EtYlJmMGI3b3I4ajZJb0hzQTZKa2JranN6c3otbkJ2U2RmUURlZkRNSVc3Ni1ZWjN0c2hsY2MiLAogICAgICAiZCI6ICJtbFBOcm1zVVM5UmJtX1I5SElyeHdmeFYzZnJ2QzlaQktFZzRzc1ZZaThfY09lSjV2U1hyQV9laEtwa2g4QVhYaUdWUGpQbVlyd29xQzFVUksxUkZmLVg0dG10emV2OUVHaU12Z0JCaEF5RkdTSUd0VUNla2x4Q2dhb3BpMXdZSU1Bd0M0STZwMUtaZURxTVNCWVZGeHA5ZWlJZ2pwb05JbV9lR3hXUUs5VHNnYmk5T3lyc1VqaE9KLVczN2JVMEJWUU56UXpxODhCcGxmNzM3VmV1dy1FeDZaMk1iWXR3SWdfZ0JVb0JEZ0NrZkhoOVE4MElYcEZRV0x1RzgwenFrdkVwTHZ0RWxLbDRvQ3BHVnBjcmFUOFNsOGpYc3FDT1k0dnVRT19LRVUzS2VPNUNJbHd4eEhJYXZjQTE5cHFpSWJ5cm1LbThxS0ZEWHluUFJMSGFNZ1EiLAogICAgICAiZSI6ICJBUUFCIiwKICAgICAgImtpZCI6ICJyc2EzODRfMjA0OCIsCiAgICAgICJxaSI6ICJzMldTamVrVDl3S2JPbk9neGNoaDJPY3VubzE2Y20wS281Z3hoUWJTdVMyMldfUjJBR2ZVdkRieGF0cTRLakQ3THo3X1k2TjdTUkwzUVpudVhoZ1djeXgyNGhrUGppQUZLNmlkYVZKQzJqQmgycEZTUDVTNXZxZ0lsME12eWY4NjlwdkN4S0NzaGRKMGdlRWhveE93VkRPYXJqdTl2Zm9IQV90LWJoRlZrUnciLAogICAgICAiZHAiOiAiQlJhQTFqYVRydG9mTHZBSUJBYW1OSEVhSm51RU9zTVJJMFRCZXFuR1BNUm0tY2RjSG1OUVo5WUtqb2JpdXlmbnhGZ0piVDlSeElBRG0ySkpoZEp5RTN4Y1dTSzhmSjBSM1Jick1aT1dwako0QmJTVzFtU1VtRnlKTGxib3puRFhZR2RaZ1hzS0o1UkFrRUNQZFBCY3YwZVlkbk9NYWhfZndfaFZoNjRuZ2tFIiwKICAgICAgImFsZyI6ICJSU0EzODQiLAogICAgICAiZHEiOiAiUFJoVERKVlR3cDNXaDZfWFZrTjIwMUlpTWhxcElrUDN1UTYyUlRlTDNrQ2ZXSkNqMkZPLTRxcVRIQk0tQjZJWUVPLXpoVWZyQnhiMzJ1djNjS2JDWGFZN3BJSFJxQlFEQWQ2WGhHYzlwc0xqNThXd3VGY2RncERJYUFpRjNyc3NUMjJ4UFVvYkJFTVdBalV3bFJrNEtNTjItMnpLQk5FR3lIcDIzOUpKdnpVIiwKICAgICAgIm4iOiAidUpDWDVDbEZpM0JnTXBvOWhRSVZ2SDh0Vi1jLTVFdG5OeUZxVm91R3NlNWwyUG92MWJGb0tsRllsU25YTzNWUE9KRWR3azNDdl9VT0UtQzlqZERYRHpvS3Z4RURaTVM1TDZWMFpIVEJoNndIOV9iN3JHSlBxLV9RdlNkejczSzZxbHpGaUtQamRvdTF6VlFYTmZfblBZbnRnQkdNRUtBc1pRNGp0cWJCdE5lV0h0MF9UM001cEktTV9KNGVlRWpCTW95TkZuU2ExTEZDVmZRNl9YVnpjelp1TlRGMlh6UmdRWkFmcmJGRXZ6eXR1TzVMZTNTTXFrUUFJeDhFQmkwYXVlRUNqNEQ4cDNVNXFVRG92NEF2VnRJbUZlbFJvb1pBMHJtVW1KRHJ4WExrVkhuVUpzaUF6ZW9TLTNBSnV1bHJkMGpuNjJ5VjZHV2dFWklZMVNlZVd3IgogICAgfQogIF0KfQo