Oidc additional client auth types (#58708) (#62289)

The OpenID Connect specification defines a number of ways for a
client (RP) to authenticate itself to the OP when accessing the
Token Endpoint. We currently only support `client_secret_basic`.

This change introduces support for 2 additional authentication
methods, namely `client_secret_post` (where the client credentials
are passed in the body of the POST request to the OP) and
`client_secret_jwt` where the client constructs a JWT and signs
it using the the client secret as a key.

Support for the above, and especially `client_secret_jwt` in our
integration tests meant that the OP we use ( Connect2id server )
should be able to validate the JWT that we send it from the RP.
Since we run the OP in docker and it listens on an ephemeral port
we would have no way of knowing the port so that we can configure
the ES running via the testcluster to know the "correct" Token
Endpoint, and even if we did, this would not be the Token Endpoint
URL that the OP would think it listens on. To alleviate this, we
run an ES single node cluster in docker, alongside the OP so that
we can configured it with the correct hostname and port within
the docker network.

Co-authored-by: Ioannis Kakavas <ioannis@elastic.co>
This commit is contained in:
Yang Wang 2020-09-16 14:29:09 +10:00 committed by GitHub
parent 24a24d050a
commit a11dfbe031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 506 additions and 180 deletions

View File

@ -32,9 +32,15 @@ public class OpenIdConnectRealmSettings {
private OpenIdConnectRealmSettings() {
}
private static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS = Collections.unmodifiableList(
Arrays.asList("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"));
private static final List<String> RESPONSE_TYPES = Arrays.asList("code", "id_token", "id_token token");
public static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS =
org.elasticsearch.common.collect.List.of(
"HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512");
private static final List<String> RESPONSE_TYPES = org.elasticsearch.common.collect.List.of(
"code", "id_token", "id_token token");
public static final List<String> CLIENT_AUTH_METHODS = org.elasticsearch.common.collect.List.of(
"client_secret_basic", "client_secret_post", "client_secret_jwt");
public static final List<String> SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS = org.elasticsearch.common.collect.List.of(
"HS256", "HS384", "HS512");
public static final String TYPE = "oidc";
public static final Setting.AffixSetting<String> RP_CLIENT_ID
@ -78,7 +84,22 @@ public class OpenIdConnectRealmSettings {
public static final Setting.AffixSetting<List<String>> 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<String> 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<String> 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<String> OP_AUTHORIZATION_ENDPOINT
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.authorization_endpoint",
key -> Setting.simpleString(key, v -> {
@ -194,8 +215,9 @@ public class OpenIdConnectRealmSettings {
public static Set<Setting.AffixSetting<?>> getSettings() {
final Set<Setting.AffixSetting<?>> 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));

View File

@ -22,6 +22,8 @@ import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
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.FileWatcher;
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 @@ public class OpenIdConnectAuthenticator {
try {
final AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, rpConfig.getRedirectUri());
final HttpPost httpPost = new HttpPost(opConfig.getTokenEndpoint());
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
final List<NameValuePair> params = new ArrayList<>();
for (Map.Entry<String, List<String>> 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.name()),
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8.name()));
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<String, List<String>> 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.name()),
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8.name()));
httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null));
SpecialPermission.check();
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
httpClient.execute(httpPost, new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse result) {
@ -496,7 +516,7 @@ public class OpenIdConnectAuthenticator {
});
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));
}

View File

@ -12,6 +12,7 @@ import com.nimbusds.jwt.JWTClaimsSet;
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;
@ -73,6 +74,8 @@ import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectReal
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;
@ -266,9 +269,11 @@ public class OpenIdConnectRealm extends Realm implements Releasable {
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) {

View File

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authc.oidc;
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 class RelyingPartyConfiguration {
public URI getPostLogoutRedirectUri() {
return postLogoutRedirectUri;
}
public ClientAuthenticationMethod getClientAuthenticationMethod() {
return clientAuthenticationMethod;
}
public JWSAlgorithm getClientAuthenticationJwtAlgorithm() {
return clientAuthenticationJwtAlgorithm;
}
}

View File

@ -27,6 +27,7 @@ import com.nimbusds.jwt.SignedJWT;
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;
@ -892,8 +893,11 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
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"),
@ -902,6 +906,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
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"));
}
@ -913,6 +919,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
new ResponseType("id_token"),
new Scope("openid"),
JWSAlgorithm.parse(alg),
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
JWSAlgorithm.HS384,
new URI("https://rp.elastic.co/successfull_logout"));
}

View File

@ -33,6 +33,43 @@ public class OpenIdConnectRealmSettingsTests extends ESTestCase {
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")
@ -115,10 +152,8 @@ public class OpenIdConnectRealmSettingsTests extends ESTestCase {
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")
@ -326,6 +361,45 @@ public class OpenIdConnectRealmSettingsTests extends ESTestCase {
));
}
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),

View File

@ -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

View File

@ -9,6 +9,7 @@ import net.minidev.json.JSONObject;
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.Request;
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.hamcrest.Matchers;
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 class OpenIdConnectAuthIT extends ESRestTestCase {
"\"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 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
}
}
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<String, Object> callAuthenticateApiUsingAccessToken(String accessToken) throws IOException {
private Map<String, Object> 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> T execute(CloseableHttpClient client, HttpEntityEnclosingRequestBase request,
@ -250,10 +318,25 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
verifyElasticsearchAccessTokenForCodeFlow(tokens.v1());
}
public void testAuthenticateWithCodeFlowAndClientPost() throws Exception {
final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME_CLIENT_POST_AUTH);
final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri());
Tuple<String, String> 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<String, String> 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<String, String> tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(),
prepareAuthResponse.getNonce(), REALM_NAME_IMPLICIT);
verifyElasticsearchAccessTokenForImplicitFlow(tokens.v1());
@ -280,7 +363,7 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
assertThat(401, equalTo(e.getResponse().getStatusLine().getStatusCode()));
}
private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throws IOException {
private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throws Exception {
final Map<String, Object> map = callAuthenticateApiUsingAccessToken(accessToken);
logger.info("Authentication with token Response: " + map);
assertThat(map.get("username"), equalTo("alice"));
@ -289,10 +372,10 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
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<String, Object> map = callAuthenticateApiUsingAccessToken(accessToken);
logger.info("Authentication with token Response: " + map);
assertThat(map.get("username"), equalTo("alice"));
@ -301,22 +384,23 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
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<String, String> body = Collections.singletonMap("realm", realmName);
Request request = buildRequest("POST", "/_security/oidc/prepare", body, facilitatorAuth());
final Response prepare = client().performRequest(request);
assertOK(prepare);
final Map<String, Object> 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<String, Object> 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<String, String> completeAuthentication(String redirectUri, String state, String nonce, @Nullable String realm)
@ -325,17 +409,19 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
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<String, Object> 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<String, Object> 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<String, ?> body, Header... headers) throws IOException {
@ -371,47 +457,56 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
* 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);
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 + "\"}" +
"}" +
"}");
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 + "\"}" +
"}" +
"}");
restClient.performRequest(createRoleMappingRequest);
createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_auditor");
createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"auditor\"]," +
"\"enabled\": true," +
"\"rules\": {" +
"\"field\": { \"groups\": \"audit\"}" +
"}" +
"}");
adminClient().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

View File

@ -1,4 +1,37 @@
apply plugin: 'elasticsearch.build'
apply plugin: 'elasticsearch.test.fixtures'
import org.elasticsearch.gradle.VersionProperties
import org.elasticsearch.gradle.Architecture
test.enabled = false
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)
}
}
tasks.named('composeUp').configure {
dependsOn "preProcessFixture"
}

View File

@ -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"

View File

@ -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

View File

@ -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
jose.jwkSer=RnVsbCBrZXk6CnsKICAia2V5cyI6IFsKICAgIHsKICAgICAgInAiOiAiLXhhN2d2aW5tY3N3QXU3Vm1mV2loZ2o3U3gzUzhmd2dFSTdMZEVveW5FU1RzcElaeUY5aHc0NVhQZmI5VHlpbzZsOHZTS0F5RmU4T2lOalpkNE1Ra0ttYlJzTmxxR1Y5VlBoWF84UG1JSm5mcGVhb3E5YnZfU0k1blZHUl9zYUUzZE9sTEE2VWpaS0lsRVBNb0ZuRlZCMUFaUU9qQlhRRzZPTDg2eDZ2NHMwIiwKICAgICAgImt0eSI6ICJSU0EiLAogICAgICAicSI6ICJ2Q3pDQUlpdHV0MGx1V0djQloyLUFabURLc1RxNkkxcUp0RmlEYkIyZFBNQVlBNldOWTdaWEZoVWxsSjJrT2ZELWdlYjlkYkN2ODBxNEwyajVZSjZoOTBUc1NRWWVHRlljN1lZMGdCMU5VR3l5cXctb29QN0EtYlJmMGI3b3I4ajZJb0hzQTZKa2JranN6c3otbkJ2U2RmUURlZkRNSVc3Ni1ZWjN0c2hsY2MiLAogICAgICAiZCI6ICJtbFBOcm1zVVM5UmJtX1I5SElyeHdmeFYzZnJ2QzlaQktFZzRzc1ZZaThfY09lSjV2U1hyQV9laEtwa2g4QVhYaUdWUGpQbVlyd29xQzFVUksxUkZmLVg0dG10emV2OUVHaU12Z0JCaEF5RkdTSUd0VUNla2x4Q2dhb3BpMXdZSU1Bd0M0STZwMUtaZURxTVNCWVZGeHA5ZWlJZ2pwb05JbV9lR3hXUUs5VHNnYmk5T3lyc1VqaE9KLVczN2JVMEJWUU56UXpxODhCcGxmNzM3VmV1dy1FeDZaMk1iWXR3SWdfZ0JVb0JEZ0NrZkhoOVE4MElYcEZRV0x1RzgwenFrdkVwTHZ0RWxLbDRvQ3BHVnBjcmFUOFNsOGpYc3FDT1k0dnVRT19LRVUzS2VPNUNJbHd4eEhJYXZjQTE5cHFpSWJ5cm1LbThxS0ZEWHluUFJMSGFNZ1EiLAogICAgICAiZSI6ICJBUUFCIiwKICAgICAgImtpZCI6ICJyc2EzODRfMjA0OCIsCiAgICAgICJxaSI6ICJzMldTamVrVDl3S2JPbk9neGNoaDJPY3VubzE2Y20wS281Z3hoUWJTdVMyMldfUjJBR2ZVdkRieGF0cTRLakQ3THo3X1k2TjdTUkwzUVpudVhoZ1djeXgyNGhrUGppQUZLNmlkYVZKQzJqQmgycEZTUDVTNXZxZ0lsME12eWY4NjlwdkN4S0NzaGRKMGdlRWhveE93VkRPYXJqdTl2Zm9IQV90LWJoRlZrUnciLAogICAgICAiZHAiOiAiQlJhQTFqYVRydG9mTHZBSUJBYW1OSEVhSm51RU9zTVJJMFRCZXFuR1BNUm0tY2RjSG1OUVo5WUtqb2JpdXlmbnhGZ0piVDlSeElBRG0ySkpoZEp5RTN4Y1dTSzhmSjBSM1Jick1aT1dwako0QmJTVzFtU1VtRnlKTGxib3puRFhZR2RaZ1hzS0o1UkFrRUNQZFBCY3YwZVlkbk9NYWhfZndfaFZoNjRuZ2tFIiwKICAgICAgImFsZyI6ICJSU0EzODQiLAogICAgICAiZHEiOiAiUFJoVERKVlR3cDNXaDZfWFZrTjIwMUlpTWhxcElrUDN1UTYyUlRlTDNrQ2ZXSkNqMkZPLTRxcVRIQk0tQjZJWUVPLXpoVWZyQnhiMzJ1djNjS2JDWGFZN3BJSFJxQlFEQWQ2WGhHYzlwc0xqNThXd3VGY2RncERJYUFpRjNyc3NUMjJ4UFVvYkJFTVdBalV3bFJrNEtNTjItMnpLQk5FR3lIcDIzOUpKdnpVIiwKICAgICAgIm4iOiAidUpDWDVDbEZpM0JnTXBvOWhRSVZ2SDh0Vi1jLTVFdG5OeUZxVm91R3NlNWwyUG92MWJGb0tsRllsU25YTzNWUE9KRWR3azNDdl9VT0UtQzlqZERYRHpvS3Z4RURaTVM1TDZWMFpIVEJoNndIOV9iN3JHSlBxLV9RdlNkejczSzZxbHpGaUtQamRvdTF6VlFYTmZfblBZbnRnQkdNRUtBc1pRNGp0cWJCdE5lV0h0MF9UM001cEktTV9KNGVlRWpCTW95TkZuU2ExTEZDVmZRNl9YVnpjelp1TlRGMlh6UmdRWkFmcmJGRXZ6eXR1TzVMZTNTTXFrUUFJeDhFQmkwYXVlRUNqNEQ4cDNVNXFVRG92NEF2VnRJbUZlbFJvb1pBMHJtVW1KRHJ4WExrVkhuVUpzaUF6ZW9TLTNBSnV1bHJkMGpuNjJ5VjZHV2dFWklZMVNlZVd3IgogICAgfQogIF0KfQo