This commit adds an OpenID Connect authentication realm to elasticsearch. Elasticsearch (with the assistance of kibana or another web component) acts as an OpenID Connect Relying Party and supports the Authorization Code Grant and Implicit flows as described in http://ela.st/oidc-spec. It adds support for consuming and verifying signed ID Tokens, both RP initiated and 3rd party initiated Single Sign on and RP initiated signle logout. It also adds an OpenID Connect Provider in the idp-fixture to be used for the associated integration tests. This is a backport of #40674
This commit is contained in:
parent
2980a6c70f
commit
fe9442b05b
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.Action;
|
||||
import org.elasticsearch.common.io.stream.Writeable;
|
||||
|
||||
/**
|
||||
* Action for initiating an authentication process using OpenID Connect
|
||||
*/
|
||||
public final class OpenIdConnectAuthenticateAction extends Action<OpenIdConnectAuthenticateResponse> {
|
||||
|
||||
public static final OpenIdConnectAuthenticateAction INSTANCE = new OpenIdConnectAuthenticateAction();
|
||||
public static final String NAME = "cluster:admin/xpack/security/oidc/authenticate";
|
||||
|
||||
private OpenIdConnectAuthenticateAction() {
|
||||
super(NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenIdConnectAuthenticateResponse newResponse() {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writeable.Reader<OpenIdConnectAuthenticateResponse> getResponseReader() {
|
||||
return OpenIdConnectAuthenticateResponse::new;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.elasticsearch.action.ValidateActions.addValidationError;
|
||||
|
||||
/**
|
||||
* Represents a request for authentication using OpenID Connect
|
||||
*/
|
||||
public class OpenIdConnectAuthenticateRequest extends ActionRequest {
|
||||
|
||||
/**
|
||||
* The URI where the OP redirected the browser after the authentication attempt. This is passed as is from the
|
||||
* facilitator entity (i.e. Kibana)
|
||||
*/
|
||||
private String redirectUri;
|
||||
|
||||
/**
|
||||
* The state value that we generated or the facilitator provided for this specific flow and that should be stored at the user's session
|
||||
* with the facilitator
|
||||
*/
|
||||
private String state;
|
||||
|
||||
/**
|
||||
* The nonce value that we generated or the facilitator provided for this specific flow and that should be stored at the user's session
|
||||
* with the facilitator
|
||||
*/
|
||||
private String nonce;
|
||||
|
||||
public OpenIdConnectAuthenticateRequest() {
|
||||
|
||||
}
|
||||
|
||||
public OpenIdConnectAuthenticateRequest(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
redirectUri = in.readString();
|
||||
state = in.readString();
|
||||
nonce = in.readString();
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
public void setRedirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public void setNonce(String nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionRequestValidationException validate() {
|
||||
ActionRequestValidationException validationException = null;
|
||||
if (Strings.isNullOrEmpty(state)) {
|
||||
validationException = addValidationError("state parameter is missing", validationException);
|
||||
}
|
||||
if (Strings.isNullOrEmpty(nonce)) {
|
||||
validationException = addValidationError("nonce parameter is missing", validationException);
|
||||
}
|
||||
if (Strings.isNullOrEmpty(redirectUri)) {
|
||||
validationException = addValidationError("redirect_uri parameter is missing", validationException);
|
||||
}
|
||||
return validationException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
super.writeTo(out);
|
||||
out.writeString(redirectUri);
|
||||
out.writeString(state);
|
||||
out.writeString(nonce);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "{redirectUri=" + redirectUri + ", state=" + state + ", nonce=" + nonce + "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionRequestBuilder;
|
||||
import org.elasticsearch.client.ElasticsearchClient;
|
||||
|
||||
/**
|
||||
* Request builder for populating a {@link OpenIdConnectAuthenticateRequest}
|
||||
*/
|
||||
public class OpenIdConnectAuthenticateRequestBuilder
|
||||
extends ActionRequestBuilder<OpenIdConnectAuthenticateRequest, OpenIdConnectAuthenticateResponse> {
|
||||
|
||||
public OpenIdConnectAuthenticateRequestBuilder(ElasticsearchClient client) {
|
||||
super(client, OpenIdConnectAuthenticateAction.INSTANCE, new OpenIdConnectAuthenticateRequest());
|
||||
}
|
||||
|
||||
public OpenIdConnectAuthenticateRequestBuilder redirectUri(String redirectUri) {
|
||||
request.setRedirectUri(redirectUri);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OpenIdConnectAuthenticateRequestBuilder state(String state) {
|
||||
request.setState(state);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OpenIdConnectAuthenticateRequestBuilder nonce(String nonce) {
|
||||
request.setNonce(nonce);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class OpenIdConnectAuthenticateResponse extends ActionResponse {
|
||||
private String principal;
|
||||
private String accessTokenString;
|
||||
private String refreshTokenString;
|
||||
private TimeValue expiresIn;
|
||||
|
||||
public OpenIdConnectAuthenticateResponse(String principal, String accessTokenString, String refreshTokenString, TimeValue expiresIn) {
|
||||
this.principal = principal;
|
||||
this.accessTokenString = accessTokenString;
|
||||
this.refreshTokenString = refreshTokenString;
|
||||
this.expiresIn = expiresIn;
|
||||
}
|
||||
|
||||
public OpenIdConnectAuthenticateResponse(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
principal = in.readString();
|
||||
accessTokenString = in.readString();
|
||||
refreshTokenString = in.readString();
|
||||
expiresIn = in.readTimeValue();
|
||||
}
|
||||
|
||||
public String getPrincipal() {
|
||||
return principal;
|
||||
}
|
||||
|
||||
public String getAccessTokenString() {
|
||||
return accessTokenString;
|
||||
}
|
||||
|
||||
public String getRefreshTokenString() {
|
||||
return refreshTokenString;
|
||||
}
|
||||
|
||||
public TimeValue getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
super.writeTo(out);
|
||||
out.writeString(principal);
|
||||
out.writeString(accessTokenString);
|
||||
out.writeString(refreshTokenString);
|
||||
out.writeTimeValue(expiresIn);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.Action;
|
||||
import org.elasticsearch.common.io.stream.Writeable;
|
||||
|
||||
public class OpenIdConnectLogoutAction extends Action<OpenIdConnectLogoutResponse> {
|
||||
|
||||
public static final OpenIdConnectLogoutAction INSTANCE = new OpenIdConnectLogoutAction();
|
||||
public static final String NAME = "cluster:admin/xpack/security/oidc/logout";
|
||||
|
||||
private OpenIdConnectLogoutAction() {
|
||||
super(NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenIdConnectLogoutResponse newResponse() {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writeable.Reader<OpenIdConnectLogoutResponse> getResponseReader() {
|
||||
return OpenIdConnectLogoutResponse::new;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.elasticsearch.action.ValidateActions.addValidationError;
|
||||
|
||||
public final class OpenIdConnectLogoutRequest extends ActionRequest {
|
||||
|
||||
private String token;
|
||||
@Nullable
|
||||
private String refreshToken;
|
||||
|
||||
public OpenIdConnectLogoutRequest() {
|
||||
|
||||
}
|
||||
|
||||
public OpenIdConnectLogoutRequest(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
token = in.readString();
|
||||
refreshToken = in.readOptionalString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionRequestValidationException validate() {
|
||||
ActionRequestValidationException validationException = null;
|
||||
if (Strings.isNullOrEmpty(token)) {
|
||||
validationException = addValidationError("token is missing", validationException);
|
||||
}
|
||||
return validationException;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public void setRefreshToken(String refreshToken) {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
super.writeTo(out);
|
||||
out.writeString(token);
|
||||
out.writeOptionalString(refreshToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class OpenIdConnectLogoutResponse extends ActionResponse {
|
||||
|
||||
private String endSessionUrl;
|
||||
|
||||
public OpenIdConnectLogoutResponse(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
this.endSessionUrl = in.readString();
|
||||
}
|
||||
|
||||
public OpenIdConnectLogoutResponse(String endSessionUrl) {
|
||||
this.endSessionUrl = endSessionUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
super.writeTo(out);
|
||||
out.writeString(endSessionUrl);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "{endSessionUrl=" + endSessionUrl + "}";
|
||||
}
|
||||
|
||||
public String getEndSessionUrl() {
|
||||
return endSessionUrl;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.Action;
|
||||
import org.elasticsearch.common.io.stream.Writeable;
|
||||
|
||||
public class OpenIdConnectPrepareAuthenticationAction extends Action<OpenIdConnectPrepareAuthenticationResponse> {
|
||||
|
||||
public static final OpenIdConnectPrepareAuthenticationAction INSTANCE = new OpenIdConnectPrepareAuthenticationAction();
|
||||
public static final String NAME = "cluster:admin/xpack/security/oidc/prepare";
|
||||
|
||||
private OpenIdConnectPrepareAuthenticationAction() {
|
||||
super(NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenIdConnectPrepareAuthenticationResponse newResponse() {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writeable.Reader<OpenIdConnectPrepareAuthenticationResponse> getResponseReader() {
|
||||
return OpenIdConnectPrepareAuthenticationResponse::new;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.elasticsearch.action.ValidateActions.addValidationError;
|
||||
|
||||
/**
|
||||
* Represents a request to prepare an OAuth 2.0 authorization request
|
||||
*/
|
||||
public class OpenIdConnectPrepareAuthenticationRequest extends ActionRequest {
|
||||
|
||||
/**
|
||||
* The name of the OpenID Connect realm in the configuration that should be used for authentication
|
||||
*/
|
||||
private String realmName;
|
||||
/**
|
||||
* In case of a
|
||||
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin">3rd party initiated authentication</a>, the
|
||||
* issuer that the User Agent needs to be redirected to for authentication
|
||||
*/
|
||||
private String issuer;
|
||||
private String loginHint;
|
||||
private String state;
|
||||
private String nonce;
|
||||
|
||||
public String getRealmName() {
|
||||
return realmName;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public String getLoginHint() {
|
||||
return loginHint;
|
||||
}
|
||||
|
||||
public void setRealmName(String realmName) {
|
||||
this.realmName = realmName;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public void setNonce(String nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public void setLoginHint(String loginHint) {
|
||||
this.loginHint = loginHint;
|
||||
}
|
||||
|
||||
public OpenIdConnectPrepareAuthenticationRequest() {
|
||||
}
|
||||
|
||||
public OpenIdConnectPrepareAuthenticationRequest(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
realmName = in.readOptionalString();
|
||||
issuer = in.readOptionalString();
|
||||
loginHint = in.readOptionalString();
|
||||
state = in.readOptionalString();
|
||||
nonce = in.readOptionalString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionRequestValidationException validate() {
|
||||
ActionRequestValidationException validationException = null;
|
||||
if (Strings.hasText(realmName) == false && Strings.hasText(issuer) == false) {
|
||||
validationException = addValidationError("one of [realm, issuer] must be provided", null);
|
||||
}
|
||||
if (Strings.hasText(realmName) && Strings.hasText(issuer)) {
|
||||
validationException = addValidationError("only one of [realm, issuer] can be provided in the same request", null);
|
||||
}
|
||||
return validationException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
super.writeTo(out);
|
||||
out.writeOptionalString(realmName);
|
||||
out.writeOptionalString(issuer);
|
||||
out.writeOptionalString(loginHint);
|
||||
out.writeOptionalString(state);
|
||||
out.writeOptionalString(nonce);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "{realmName=" + realmName + ", issuer=" + issuer + ", login_hint=" +
|
||||
loginHint + ", state=" + state + ", nonce=" + nonce + "}";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionRequestBuilder;
|
||||
import org.elasticsearch.client.ElasticsearchClient;
|
||||
|
||||
/**
|
||||
* Request builder for populating a {@link OpenIdConnectPrepareAuthenticationRequest}
|
||||
*/
|
||||
public class OpenIdConnectPrepareAuthenticationRequestBuilder
|
||||
extends ActionRequestBuilder<OpenIdConnectPrepareAuthenticationRequest, OpenIdConnectPrepareAuthenticationResponse> {
|
||||
|
||||
public OpenIdConnectPrepareAuthenticationRequestBuilder(ElasticsearchClient client) {
|
||||
super(client, OpenIdConnectPrepareAuthenticationAction.INSTANCE, new OpenIdConnectPrepareAuthenticationRequest());
|
||||
}
|
||||
|
||||
public OpenIdConnectPrepareAuthenticationRequestBuilder realmName(String name) {
|
||||
request.setRealmName(name);
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.common.xcontent.ToXContentObject;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A response object that contains the OpenID Connect Authentication Request as a URL and the state and nonce values that were
|
||||
* generated for this request.
|
||||
*/
|
||||
public class OpenIdConnectPrepareAuthenticationResponse extends ActionResponse implements ToXContentObject {
|
||||
|
||||
private String authenticationRequestUrl;
|
||||
/*
|
||||
* The oAuth2 state parameter used for CSRF protection.
|
||||
*/
|
||||
private String state;
|
||||
/*
|
||||
* String value used to associate a Client session with an ID Token, and to mitigate replay attacks.
|
||||
*/
|
||||
private String nonce;
|
||||
|
||||
public OpenIdConnectPrepareAuthenticationResponse(String authorizationEndpointUrl, String state, String nonce) {
|
||||
this.authenticationRequestUrl = authorizationEndpointUrl;
|
||||
this.state = state;
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public OpenIdConnectPrepareAuthenticationResponse(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
authenticationRequestUrl = in.readString();
|
||||
state = in.readString();
|
||||
nonce = in.readString();
|
||||
}
|
||||
|
||||
public String getAuthenticationRequestUrl() {
|
||||
return authenticationRequestUrl;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) throws IOException {
|
||||
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
super.writeTo(out);
|
||||
out.writeString(authenticationRequestUrl);
|
||||
out.writeString(state);
|
||||
out.writeString(nonce);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "{authenticationRequestUrl=" + authenticationRequestUrl + ", state=" + state + ", nonce=" + nonce + "}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
builder.startObject();
|
||||
builder.field("redirect", authenticationRequestUrl);
|
||||
builder.field("state", state);
|
||||
builder.field("nonce", nonce);
|
||||
builder.endObject();
|
||||
return builder;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
|
|||
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings;
|
||||
|
||||
|
@ -34,6 +35,7 @@ public final class InternalRealmsSettings {
|
|||
set.addAll(PkiRealmSettings.getSettings());
|
||||
set.addAll(SamlRealmSettings.getSettings());
|
||||
set.addAll(KerberosRealmSettings.getSettings());
|
||||
set.addAll(OpenIdConnectRealmSettings.getSettings());
|
||||
return Collections.unmodifiableSet(set);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
package org.elasticsearch.xpack.core.security.authc;
|
||||
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
import org.elasticsearch.common.settings.SecureSetting;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Setting;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
|
||||
|
@ -55,6 +57,17 @@ public class RealmSettings {
|
|||
return Setting.affixKeySetting(realmSettingPrefix(realmType), suffix, key -> Setting.simpleString(key, properties));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link SecureSetting#secureString secure string} {@link Setting} object of a realm of
|
||||
* with the provided type and setting suffix.
|
||||
*
|
||||
* @param realmType The type of the realm, used within the setting prefix
|
||||
* @param suffix The suffix of the setting (everything following the realm name in the affix setting)
|
||||
*/
|
||||
public static Setting.AffixSetting<SecureString> secureString(String realmType, String suffix) {
|
||||
return Setting.affixKeySetting(realmSettingPrefix(realmType), suffix, key -> SecureSetting.secureString(key, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Function} that acts as a factory an {@link org.elasticsearch.common.settings.Setting.AffixSetting}.
|
||||
* The {@code Function} takes the <em>realm-type</em> as an argument.
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.authc.oidc;
|
||||
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Setting;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.common.util.set.Sets;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;
|
||||
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
|
||||
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 String TYPE = "oidc";
|
||||
|
||||
public static final Setting.AffixSetting<String> RP_CLIENT_ID
|
||||
= RealmSettings.simpleString(TYPE, "rp.client_id", Setting.Property.NodeScope);
|
||||
public static final Setting.AffixSetting<SecureString> RP_CLIENT_SECRET
|
||||
= RealmSettings.secureString(TYPE, "rp.client_secret");
|
||||
public static final Setting.AffixSetting<String> RP_REDIRECT_URI
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.redirect_uri",
|
||||
key -> Setting.simpleString(key, v -> {
|
||||
try {
|
||||
new URI(v);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
|
||||
}
|
||||
}, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<String> RP_POST_LOGOUT_REDIRECT_URI
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.post_logout_redirect_uri",
|
||||
key -> Setting.simpleString(key, v -> {
|
||||
try {
|
||||
new URI(v);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
|
||||
}
|
||||
}, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<String> RP_RESPONSE_TYPE
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.response_type",
|
||||
key -> Setting.simpleString(key, v -> {
|
||||
if (RESPONSE_TYPES.contains(v) == false) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid value [" + v + "] for [" + key + "]. Allowed values are " + RESPONSE_TYPES + "");
|
||||
}
|
||||
}, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<String> RP_SIGNATURE_ALGORITHM
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.signature_algorithm",
|
||||
key -> new Setting<>(key, "RS256", Function.identity(), v -> {
|
||||
if (SUPPORTED_SIGNATURE_ALGORITHMS.contains(v) == false) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid value [" + v + "] for [" + key + "]. Allowed values are " + SUPPORTED_SIGNATURE_ALGORITHMS + "}]");
|
||||
}
|
||||
}, Setting.Property.NodeScope));
|
||||
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> OP_NAME
|
||||
= RealmSettings.simpleString(TYPE, "op.name", 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 -> {
|
||||
try {
|
||||
new URI(v);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
|
||||
}
|
||||
}, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<String> OP_TOKEN_ENDPOINT
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.token_endpoint",
|
||||
key -> Setting.simpleString(key, v -> {
|
||||
try {
|
||||
new URI(v);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
|
||||
}
|
||||
}, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<String> OP_USERINFO_ENDPOINT
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.userinfo_endpoint",
|
||||
key -> Setting.simpleString(key, v -> {
|
||||
try {
|
||||
new URI(v);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
|
||||
}
|
||||
}, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<String> OP_ENDSESSION_ENDPOINT
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.endsession_endpoint",
|
||||
key -> Setting.simpleString(key, v -> {
|
||||
try {
|
||||
new URI(v);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
|
||||
}
|
||||
}, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<String> OP_ISSUER
|
||||
= RealmSettings.simpleString(TYPE, "op.issuer", Setting.Property.NodeScope);
|
||||
public static final Setting.AffixSetting<String> OP_JWKSET_PATH
|
||||
= RealmSettings.simpleString(TYPE, "op.jwkset_path", Setting.Property.NodeScope);
|
||||
|
||||
public static final Setting.AffixSetting<TimeValue> ALLOWED_CLOCK_SKEW
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "allowed_clock_skew",
|
||||
key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(60), Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<Boolean> POPULATE_USER_METADATA = Setting.affixKeySetting(
|
||||
RealmSettings.realmSettingPrefix(TYPE), "populate_user_metadata",
|
||||
key -> Setting.boolSetting(key, true, Setting.Property.NodeScope));
|
||||
private static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueSeconds(5);
|
||||
public static final Setting.AffixSetting<TimeValue> HTTP_CONNECT_TIMEOUT
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.connect_timeout",
|
||||
key -> Setting.timeSetting(key, DEFAULT_TIMEOUT, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<TimeValue> HTTP_CONNECTION_READ_TIMEOUT
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.connection_read_timeout",
|
||||
key -> Setting.timeSetting(key, DEFAULT_TIMEOUT, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<TimeValue> HTTP_SOCKET_TIMEOUT
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.socket_timeout",
|
||||
key -> Setting.timeSetting(key, DEFAULT_TIMEOUT, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<Integer> HTTP_MAX_CONNECTIONS
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.max_connections",
|
||||
key -> Setting.intSetting(key, 200, Setting.Property.NodeScope));
|
||||
public static final Setting.AffixSetting<Integer> HTTP_MAX_ENDPOINT_CONNECTIONS
|
||||
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.max_endpoint_connections",
|
||||
key -> Setting.intSetting(key, 200, Setting.Property.NodeScope));
|
||||
|
||||
public static final ClaimSetting PRINCIPAL_CLAIM = new ClaimSetting("principal");
|
||||
public static final ClaimSetting GROUPS_CLAIM = new ClaimSetting("groups");
|
||||
public static final ClaimSetting NAME_CLAIM = new ClaimSetting("name");
|
||||
public static final ClaimSetting DN_CLAIM = new ClaimSetting("dn");
|
||||
public static final ClaimSetting MAIL_CLAIM = new ClaimSetting("mail");
|
||||
|
||||
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_NAME, OP_AUTHORIZATION_ENDPOINT, OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT,
|
||||
OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT, HTTP_SOCKET_TIMEOUT,
|
||||
HTTP_MAX_CONNECTIONS, HTTP_MAX_ENDPOINT_CONNECTIONS, ALLOWED_CLOCK_SKEW);
|
||||
set.addAll(DelegatedAuthorizationSettings.getSettings(TYPE));
|
||||
set.addAll(RealmSettings.getStandardSettings(TYPE));
|
||||
set.addAll(SSLConfigurationSettings.getRealmSettings(TYPE));
|
||||
set.addAll(PRINCIPAL_CLAIM.settings());
|
||||
set.addAll(GROUPS_CLAIM.settings());
|
||||
set.addAll(DN_CLAIM.settings());
|
||||
set.addAll(NAME_CLAIM.settings());
|
||||
set.addAll(MAIL_CLAIM.settings());
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* The OIDC realm offers a number of settings that rely on claim values that are populated by the OP in the ID Token or the User Info
|
||||
* response.
|
||||
* Each claim has 2 settings:
|
||||
* <ul>
|
||||
* <li>The name of the OpenID Connect claim to use</li>
|
||||
* <li>An optional java pattern (regex) to apply to that claim value in order to extract the substring that should be used.</li>
|
||||
* </ul>
|
||||
* For example, the Elasticsearch User Principal could be configured to come from the OpenID Connect standard claim "email",
|
||||
* and extract only the local-part of the user's email address (i.e. the name before the '@').
|
||||
* This class encapsulates those 2 settings.
|
||||
*/
|
||||
public static final class ClaimSetting {
|
||||
public static final String CLAIMS_PREFIX = "claims.";
|
||||
public static final String CLAIM_PATTERNS_PREFIX = "claim_patterns.";
|
||||
|
||||
private final Setting.AffixSetting<String> claim;
|
||||
private final Setting.AffixSetting<String> pattern;
|
||||
|
||||
public ClaimSetting(String name) {
|
||||
claim = RealmSettings.simpleString(TYPE, CLAIMS_PREFIX + name, Setting.Property.NodeScope);
|
||||
pattern = RealmSettings.simpleString(TYPE, CLAIM_PATTERNS_PREFIX + name, Setting.Property.NodeScope);
|
||||
}
|
||||
|
||||
public Collection<Setting.AffixSetting<?>> settings() {
|
||||
return Arrays.asList(getClaim(), getPattern());
|
||||
}
|
||||
|
||||
public String name(RealmConfig config) {
|
||||
return getClaim().getConcreteSettingForNamespace(config.name()).getKey();
|
||||
}
|
||||
|
||||
public Setting.AffixSetting<String> getClaim() {
|
||||
return claim;
|
||||
}
|
||||
|
||||
public Setting.AffixSetting<String> getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ public final class ClusterPrivilege extends Privilege {
|
|||
private static final Automaton MANAGE_SECURITY_AUTOMATON = patterns("cluster:admin/xpack/security/*");
|
||||
private static final Automaton MANAGE_SAML_AUTOMATON = patterns("cluster:admin/xpack/security/saml/*",
|
||||
InvalidateTokenAction.NAME, RefreshTokenAction.NAME);
|
||||
private static final Automaton MANAGE_OIDC_AUTOMATON = patterns("cluster:admin/xpack/security/oidc/*");
|
||||
private static final Automaton MANAGE_TOKEN_AUTOMATON = patterns("cluster:admin/xpack/security/token/*");
|
||||
private static final Automaton MONITOR_AUTOMATON = patterns("cluster:monitor/*");
|
||||
private static final Automaton MONITOR_ML_AUTOMATON = patterns("cluster:monitor/xpack/ml/*");
|
||||
|
@ -82,6 +83,7 @@ public final class ClusterPrivilege extends Privilege {
|
|||
public static final ClusterPrivilege TRANSPORT_CLIENT = new ClusterPrivilege("transport_client", TRANSPORT_CLIENT_AUTOMATON);
|
||||
public static final ClusterPrivilege MANAGE_SECURITY = new ClusterPrivilege("manage_security", MANAGE_SECURITY_AUTOMATON);
|
||||
public static final ClusterPrivilege MANAGE_SAML = new ClusterPrivilege("manage_saml", MANAGE_SAML_AUTOMATON);
|
||||
public static final ClusterPrivilege MANAGE_OIDC = new ClusterPrivilege("manage_oidc", MANAGE_OIDC_AUTOMATON);
|
||||
public static final ClusterPrivilege MANAGE_PIPELINE = new ClusterPrivilege("manage_pipeline", "cluster:admin/ingest/pipeline/*");
|
||||
public static final ClusterPrivilege MANAGE_CCR = new ClusterPrivilege("manage_ccr", MANAGE_CCR_AUTOMATON);
|
||||
public static final ClusterPrivilege READ_CCR = new ClusterPrivilege("read_ccr", READ_CCR_AUTOMATON);
|
||||
|
@ -109,6 +111,7 @@ public final class ClusterPrivilege extends Privilege {
|
|||
.put("transport_client", TRANSPORT_CLIENT)
|
||||
.put("manage_security", MANAGE_SECURITY)
|
||||
.put("manage_saml", MANAGE_SAML)
|
||||
.put("manage_oidc", MANAGE_OIDC)
|
||||
.put("manage_pipeline", MANAGE_PIPELINE)
|
||||
.put("manage_rollup", MANAGE_ROLLUP)
|
||||
.put("manage_ccr", MANAGE_CCR)
|
||||
|
|
|
@ -56,6 +56,16 @@ dependencies {
|
|||
compile "org.apache.httpcomponents:httpclient-cache:${versions.httpclient}"
|
||||
compile 'com.google.guava:guava:19.0'
|
||||
|
||||
// Dependencies for oidc
|
||||
compile "com.nimbusds:oauth2-oidc-sdk:6.5"
|
||||
compile "com.nimbusds:nimbus-jose-jwt:4.41.2"
|
||||
compile "com.nimbusds:lang-tag:1.4.4"
|
||||
compile "com.sun.mail:jakarta.mail:1.6.3"
|
||||
compile "net.jcip:jcip-annotations:1.0"
|
||||
compile "net.minidev:json-smart:2.3"
|
||||
compile "net.minidev:accessors-smart:1.2"
|
||||
compile "org.ow2.asm:asm:7.1"
|
||||
|
||||
testCompile 'org.elasticsearch:securemock:1.2'
|
||||
testCompile "org.elasticsearch:mocksocket:${versions.mocksocket}"
|
||||
//testCompile "org.yaml:snakeyaml:${versions.snakeyaml}"
|
||||
|
@ -162,7 +172,7 @@ forbiddenPatterns {
|
|||
}
|
||||
|
||||
forbiddenApisMain {
|
||||
signaturesFiles += files('forbidden/ldap-signatures.txt', 'forbidden/xml-signatures.txt')
|
||||
signaturesFiles += files('forbidden/ldap-signatures.txt', 'forbidden/xml-signatures.txt', 'forbidden/oidc-signatures.txt')
|
||||
}
|
||||
|
||||
// classes are missing, e.g. com.ibm.icu.lang.UCharacter
|
||||
|
@ -259,7 +269,9 @@ thirdPartyAudit {
|
|||
'net.sf.ehcache.Ehcache',
|
||||
'net.sf.ehcache.Element',
|
||||
// [missing classes] SLF4j includes an optional class that depends on an extension class (!)
|
||||
'org.slf4j.ext.EventData'
|
||||
'org.slf4j.ext.EventData',
|
||||
// Optional dependency of oauth2-oidc-sdk that we don't need since we do not support AES-SIV for JWE
|
||||
'org.cryptomator.siv.SivMode'
|
||||
)
|
||||
|
||||
ignoreViolations (
|
||||
|
@ -280,7 +292,13 @@ if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) {
|
|||
'javax.xml.bind.JAXBElement',
|
||||
'javax.xml.bind.JAXBException',
|
||||
'javax.xml.bind.Unmarshaller',
|
||||
'javax.xml.bind.UnmarshallerHandler'
|
||||
'javax.xml.bind.UnmarshallerHandler',
|
||||
'javax.activation.ActivationDataFlavor',
|
||||
'javax.activation.DataContentHandler',
|
||||
'javax.activation.DataHandler',
|
||||
'javax.activation.DataSource',
|
||||
'javax.activation.FileDataSource',
|
||||
'javax.activation.FileTypeMap'
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
@defaultMessage Blocking methods should not be used for HTTP requests. Use CloseableHttpAsyncClient instead
|
||||
com.nimbusds.oauth2.sdk.http.HTTPRequest#send(javax.net.ssl.HostnameVerifier, javax.net.ssl.SSLSocketFactory)
|
||||
com.nimbusds.oauth2.sdk.http.HTTPRequest#send()
|
|
@ -0,0 +1 @@
|
|||
c592b500269bfde36096641b01238a8350f8aa31
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1 @@
|
|||
fa29aa438674ff19d5e1386d2c3527a0267f291e
|
|
@ -0,0 +1,26 @@
|
|||
Copyright (c) 2012 France Télécom
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holders nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
787e007e377223bba85a33599d3da416c135f99b
|
|
@ -0,0 +1,637 @@
|
|||
# Eclipse Public License - v 2.0
|
||||
|
||||
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
|
||||
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
|
||||
OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
|
||||
|
||||
1. DEFINITIONS
|
||||
|
||||
"Contribution" means:
|
||||
|
||||
a) in the case of the initial Contributor, the initial content
|
||||
Distributed under this Agreement, and
|
||||
|
||||
b) in the case of each subsequent Contributor:
|
||||
i) changes to the Program, and
|
||||
ii) additions to the Program;
|
||||
where such changes and/or additions to the Program originate from
|
||||
and are Distributed by that particular Contributor. A Contribution
|
||||
"originates" from a Contributor if it was added to the Program by
|
||||
such Contributor itself or anyone acting on such Contributor's behalf.
|
||||
Contributions do not include changes or additions to the Program that
|
||||
are not Modified Works.
|
||||
|
||||
"Contributor" means any person or entity that Distributes the Program.
|
||||
|
||||
"Licensed Patents" mean patent claims licensable by a Contributor which
|
||||
are necessarily infringed by the use or sale of its Contribution alone
|
||||
or when combined with the Program.
|
||||
|
||||
"Program" means the Contributions Distributed in accordance with this
|
||||
Agreement.
|
||||
|
||||
"Recipient" means anyone who receives the Program under this Agreement
|
||||
or any Secondary License (as applicable), including Contributors.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source Code or other
|
||||
form, that is based on (or derived from) the Program and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship.
|
||||
|
||||
"Modified Works" shall mean any work in Source Code or other form that
|
||||
results from an addition to, deletion from, or modification of the
|
||||
contents of the Program, including, for purposes of clarity any new file
|
||||
in Source Code form that contains any contents of the Program. Modified
|
||||
Works shall not include works that contain only declarations,
|
||||
interfaces, types, classes, structures, or files of the Program solely
|
||||
in each case in order to link to, bind by name, or subclass the Program
|
||||
or Modified Works thereof.
|
||||
|
||||
"Distribute" means the acts of a) distributing or b) making available
|
||||
in any manner that enables the transfer of a copy.
|
||||
|
||||
"Source Code" means the form of a Program preferred for making
|
||||
modifications, including but not limited to software source code,
|
||||
documentation source, and configuration files.
|
||||
|
||||
"Secondary License" means either the GNU General Public License,
|
||||
Version 2.0, or any later versions of that license, including any
|
||||
exceptions or additional permissions as identified by the initial
|
||||
Contributor.
|
||||
|
||||
2. GRANT OF RIGHTS
|
||||
|
||||
a) Subject to the terms of this Agreement, each Contributor hereby
|
||||
grants Recipient a non-exclusive, worldwide, royalty-free copyright
|
||||
license to reproduce, prepare Derivative Works of, publicly display,
|
||||
publicly perform, Distribute and sublicense the Contribution of such
|
||||
Contributor, if any, and such Derivative Works.
|
||||
|
||||
b) Subject to the terms of this Agreement, each Contributor hereby
|
||||
grants Recipient a non-exclusive, worldwide, royalty-free patent
|
||||
license under Licensed Patents to make, use, sell, offer to sell,
|
||||
import and otherwise transfer the Contribution of such Contributor,
|
||||
if any, in Source Code or other form. This patent license shall
|
||||
apply to the combination of the Contribution and the Program if, at
|
||||
the time the Contribution is added by the Contributor, such addition
|
||||
of the Contribution causes such combination to be covered by the
|
||||
Licensed Patents. The patent license shall not apply to any other
|
||||
combinations which include the Contribution. No hardware per se is
|
||||
licensed hereunder.
|
||||
|
||||
c) Recipient understands that although each Contributor grants the
|
||||
licenses to its Contributions set forth herein, no assurances are
|
||||
provided by any Contributor that the Program does not infringe the
|
||||
patent or other intellectual property rights of any other entity.
|
||||
Each Contributor disclaims any liability to Recipient for claims
|
||||
brought by any other entity based on infringement of intellectual
|
||||
property rights or otherwise. As a condition to exercising the
|
||||
rights and licenses granted hereunder, each Recipient hereby
|
||||
assumes sole responsibility to secure any other intellectual
|
||||
property rights needed, if any. For example, if a third party
|
||||
patent license is required to allow Recipient to Distribute the
|
||||
Program, it is Recipient's responsibility to acquire that license
|
||||
before distributing the Program.
|
||||
|
||||
d) Each Contributor represents that to its knowledge it has
|
||||
sufficient copyright rights in its Contribution, if any, to grant
|
||||
the copyright license set forth in this Agreement.
|
||||
|
||||
e) Notwithstanding the terms of any Secondary License, no
|
||||
Contributor makes additional grants to any Recipient (other than
|
||||
those set forth in this Agreement) as a result of such Recipient's
|
||||
receipt of the Program under the terms of a Secondary License
|
||||
(if permitted under the terms of Section 3).
|
||||
|
||||
3. REQUIREMENTS
|
||||
|
||||
3.1 If a Contributor Distributes the Program in any form, then:
|
||||
|
||||
a) the Program must also be made available as Source Code, in
|
||||
accordance with section 3.2, and the Contributor must accompany
|
||||
the Program with a statement that the Source Code for the Program
|
||||
is available under this Agreement, and informs Recipients how to
|
||||
obtain it in a reasonable manner on or through a medium customarily
|
||||
used for software exchange; and
|
||||
|
||||
b) the Contributor may Distribute the Program under a license
|
||||
different than this Agreement, provided that such license:
|
||||
i) effectively disclaims on behalf of all other Contributors all
|
||||
warranties and conditions, express and implied, including
|
||||
warranties or conditions of title and non-infringement, and
|
||||
implied warranties or conditions of merchantability and fitness
|
||||
for a particular purpose;
|
||||
|
||||
ii) effectively excludes on behalf of all other Contributors all
|
||||
liability for damages, including direct, indirect, special,
|
||||
incidental and consequential damages, such as lost profits;
|
||||
|
||||
iii) does not attempt to limit or alter the recipients' rights
|
||||
in the Source Code under section 3.2; and
|
||||
|
||||
iv) requires any subsequent distribution of the Program by any
|
||||
party to be under a license that satisfies the requirements
|
||||
of this section 3.
|
||||
|
||||
3.2 When the Program is Distributed as Source Code:
|
||||
|
||||
a) it must be made available under this Agreement, or if the
|
||||
Program (i) is combined with other material in a separate file or
|
||||
files made available under a Secondary License, and (ii) the initial
|
||||
Contributor attached to the Source Code the notice described in
|
||||
Exhibit A of this Agreement, then the Program may be made available
|
||||
under the terms of such Secondary Licenses, and
|
||||
|
||||
b) a copy of this Agreement must be included with each copy of
|
||||
the Program.
|
||||
|
||||
3.3 Contributors may not remove or alter any copyright, patent,
|
||||
trademark, attribution notices, disclaimers of warranty, or limitations
|
||||
of liability ("notices") contained within the Program from any copy of
|
||||
the Program which they Distribute, provided that Contributors may add
|
||||
their own appropriate notices.
|
||||
|
||||
4. COMMERCIAL DISTRIBUTION
|
||||
|
||||
Commercial distributors of software may accept certain responsibilities
|
||||
with respect to end users, business partners and the like. While this
|
||||
license is intended to facilitate the commercial use of the Program,
|
||||
the Contributor who includes the Program in a commercial product
|
||||
offering should do so in a manner which does not create potential
|
||||
liability for other Contributors. Therefore, if a Contributor includes
|
||||
the Program in a commercial product offering, such Contributor
|
||||
("Commercial Contributor") hereby agrees to defend and indemnify every
|
||||
other Contributor ("Indemnified Contributor") against any losses,
|
||||
damages and costs (collectively "Losses") arising from claims, lawsuits
|
||||
and other legal actions brought by a third party against the Indemnified
|
||||
Contributor to the extent caused by the acts or omissions of such
|
||||
Commercial Contributor in connection with its distribution of the Program
|
||||
in a commercial product offering. The obligations in this section do not
|
||||
apply to any claims or Losses relating to any actual or alleged
|
||||
intellectual property infringement. In order to qualify, an Indemnified
|
||||
Contributor must: a) promptly notify the Commercial Contributor in
|
||||
writing of such claim, and b) allow the Commercial Contributor to control,
|
||||
and cooperate with the Commercial Contributor in, the defense and any
|
||||
related settlement negotiations. The Indemnified Contributor may
|
||||
participate in any such claim at its own expense.
|
||||
|
||||
For example, a Contributor might include the Program in a commercial
|
||||
product offering, Product X. That Contributor is then a Commercial
|
||||
Contributor. If that Commercial Contributor then makes performance
|
||||
claims, or offers warranties related to Product X, those performance
|
||||
claims and warranties are such Commercial Contributor's responsibility
|
||||
alone. Under this section, the Commercial Contributor would have to
|
||||
defend claims against the other Contributors related to those performance
|
||||
claims and warranties, and if a court requires any other Contributor to
|
||||
pay any damages as a result, the Commercial Contributor must pay
|
||||
those damages.
|
||||
|
||||
5. NO WARRANTY
|
||||
|
||||
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
|
||||
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
|
||||
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
|
||||
TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
|
||||
PURPOSE. Each Recipient is solely responsible for determining the
|
||||
appropriateness of using and distributing the Program and assumes all
|
||||
risks associated with its exercise of rights under this Agreement,
|
||||
including but not limited to the risks and costs of program errors,
|
||||
compliance with applicable laws, damage to or loss of data, programs
|
||||
or equipment, and unavailability or interruption of operations.
|
||||
|
||||
6. DISCLAIMER OF LIABILITY
|
||||
|
||||
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
|
||||
SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
|
||||
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
|
||||
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
7. GENERAL
|
||||
|
||||
If any provision of this Agreement is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this Agreement, and without further
|
||||
action by the parties hereto, such provision shall be reformed to the
|
||||
minimum extent necessary to make such provision valid and enforceable.
|
||||
|
||||
If Recipient institutes patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that the
|
||||
Program itself (excluding combinations of the Program with other software
|
||||
or hardware) infringes such Recipient's patent(s), then such Recipient's
|
||||
rights granted under Section 2(b) shall terminate as of the date such
|
||||
litigation is filed.
|
||||
|
||||
All Recipient's rights under this Agreement shall terminate if it
|
||||
fails to comply with any of the material terms or conditions of this
|
||||
Agreement and does not cure such failure in a reasonable period of
|
||||
time after becoming aware of such noncompliance. If all Recipient's
|
||||
rights under this Agreement terminate, Recipient agrees to cease use
|
||||
and distribution of the Program as soon as reasonably practicable.
|
||||
However, Recipient's obligations under this Agreement and any licenses
|
||||
granted by Recipient relating to the Program shall continue and survive.
|
||||
|
||||
Everyone is permitted to copy and distribute copies of this Agreement,
|
||||
but in order to avoid inconsistency the Agreement is copyrighted and
|
||||
may only be modified in the following manner. The Agreement Steward
|
||||
reserves the right to publish new versions (including revisions) of
|
||||
this Agreement from time to time. No one other than the Agreement
|
||||
Steward has the right to modify this Agreement. The Eclipse Foundation
|
||||
is the initial Agreement Steward. The Eclipse Foundation may assign the
|
||||
responsibility to serve as the Agreement Steward to a suitable separate
|
||||
entity. Each new version of the Agreement will be given a distinguishing
|
||||
version number. The Program (including Contributions) may always be
|
||||
Distributed subject to the version of the Agreement under which it was
|
||||
received. In addition, after a new version of the Agreement is published,
|
||||
Contributor may elect to Distribute the Program (including its
|
||||
Contributions) under the new version.
|
||||
|
||||
Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
|
||||
receives no rights or licenses to the intellectual property of any
|
||||
Contributor under this Agreement, whether expressly, by implication,
|
||||
estoppel or otherwise. All rights in the Program not expressly granted
|
||||
under this Agreement are reserved. Nothing in this Agreement is intended
|
||||
to be enforceable by any entity that is not a Contributor or Recipient.
|
||||
No third-party beneficiary rights are created under this Agreement.
|
||||
|
||||
Exhibit A - Form of Secondary Licenses Notice
|
||||
|
||||
"This Source Code may also be made available under the following
|
||||
Secondary Licenses when the conditions for such availability set forth
|
||||
in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
|
||||
version(s), and exceptions or additional permissions here}."
|
||||
|
||||
Simply including a copy of this Agreement, including this Exhibit A
|
||||
is not sufficient to license the Source Code under Secondary Licenses.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to
|
||||
look for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
---
|
||||
|
||||
## The GNU General Public License (GPL) Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor
|
||||
Boston, MA 02110-1335
|
||||
USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to
|
||||
share and change it. By contrast, the GNU General Public License is
|
||||
intended to guarantee your freedom to share and change free software--to
|
||||
make sure the software is free for all its users. This General Public
|
||||
License applies to most of the Free Software Foundation's software and
|
||||
to any other program whose authors commit to using it. (Some other Free
|
||||
Software Foundation software is covered by the GNU Library General
|
||||
Public License instead.) You can apply it to your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price.
|
||||
Our General Public Licenses are designed to make sure that you have the
|
||||
freedom to distribute copies of free software (and charge for this
|
||||
service if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid anyone
|
||||
to deny you these rights or to ask you to surrender the rights. These
|
||||
restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that you have.
|
||||
You must make sure that they, too, receive or can get the source code.
|
||||
And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software patents.
|
||||
We wish to avoid the danger that redistributors of a free program will
|
||||
individually obtain patent licenses, in effect making the program
|
||||
proprietary. To prevent this, we have made it clear that any patent must
|
||||
be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains a
|
||||
notice placed by the copyright holder saying it may be distributed under
|
||||
the terms of this General Public License. The "Program", below, refers
|
||||
to any such program or work, and a "work based on the Program" means
|
||||
either the Program or any derivative work under copyright law: that is
|
||||
to say, a work containing the Program or a portion of it, either
|
||||
verbatim or with modifications and/or translated into another language.
|
||||
(Hereinafter, translation is included without limitation in the term
|
||||
"modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of running
|
||||
the Program is not restricted, and the output from the Program is
|
||||
covered only if its contents constitute a work based on the Program
|
||||
(independent of having been made by running the Program). Whether that
|
||||
is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's source
|
||||
code as you receive it, in any medium, provided that you conspicuously
|
||||
and appropriately publish on each copy an appropriate copyright notice
|
||||
and disclaimer of warranty; keep intact all the notices that refer to
|
||||
this License and to the absence of any warranty; and give any other
|
||||
recipients of the Program a copy of this License along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion of
|
||||
it, thus forming a work based on the Program, and copy and distribute
|
||||
such modifications or work under the terms of Section 1 above, provided
|
||||
that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any part
|
||||
thereof, to be licensed as a whole at no charge to all third parties
|
||||
under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a notice
|
||||
that there is no warranty (or else, saying that you provide a
|
||||
warranty) and that users may redistribute the program under these
|
||||
conditions, and telling the user how to view a copy of this License.
|
||||
(Exception: if the Program itself is interactive but does not
|
||||
normally print such an announcement, your work based on the Program
|
||||
is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program, and
|
||||
can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based on
|
||||
the Program, the distribution of the whole must be on the terms of this
|
||||
License, whose permissions for other licensees extend to the entire
|
||||
whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of a
|
||||
storage or distribution medium does not bring the other work under the
|
||||
scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections 1
|
||||
and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your cost
|
||||
of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer to
|
||||
distribute corresponding source code. (This alternative is allowed
|
||||
only for noncommercial distribution and only if you received the
|
||||
program in object code or executable form with such an offer, in
|
||||
accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source code
|
||||
means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to control
|
||||
compilation and installation of the executable. However, as a special
|
||||
exception, the source code distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies the
|
||||
executable.
|
||||
|
||||
If distribution of executable or object code is made by offering access
|
||||
to copy from a designated place, then offering equivalent access to copy
|
||||
the source code from the same place counts as distribution of the source
|
||||
code, even though third parties are not compelled to copy the source
|
||||
along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt otherwise
|
||||
to copy, modify, sublicense or distribute the Program is void, and will
|
||||
automatically terminate your rights under this License. However, parties
|
||||
who have received copies, or rights, from you under this License will
|
||||
not have their licenses terminated so long as such parties remain in
|
||||
full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and all
|
||||
its terms and conditions for copying, distributing or modifying the
|
||||
Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further restrictions
|
||||
on the recipients' exercise of the rights granted herein. You are not
|
||||
responsible for enforcing compliance by third parties to this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot distribute
|
||||
so as to satisfy simultaneously your obligations under this License and
|
||||
any other pertinent obligations, then as a consequence you may not
|
||||
distribute the Program at all. For example, if a patent license would
|
||||
not permit royalty-free redistribution of the Program by all those who
|
||||
receive copies directly or indirectly through you, then the only way you
|
||||
could satisfy both it and this License would be to refrain entirely from
|
||||
distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is implemented
|
||||
by public license practices. Many people have made generous
|
||||
contributions to the wide range of software distributed through that
|
||||
system in reliance on consistent application of that system; it is up to
|
||||
the author/donor to decide if he or she is willing to distribute
|
||||
software through any other system and a licensee cannot impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to be
|
||||
a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License may
|
||||
add an explicit geographical distribution limitation excluding those
|
||||
countries, so that distribution is permitted only in or among countries
|
||||
not thus excluded. In such case, this License incorporates the
|
||||
limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new
|
||||
versions of the General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Program does not specify a version
|
||||
number of this License, you may choose any version ever published by the
|
||||
Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the
|
||||
author to ask for permission. For software which is copyrighted by the
|
||||
Free Software Foundation, write to the Free Software Foundation; we
|
||||
sometimes make exceptions for this. Our decision will be guided by the
|
||||
two goals of preserving the free status of all derivatives of our free
|
||||
software and of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
||||
EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
|
||||
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH
|
||||
YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
|
||||
NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
|
||||
DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
|
||||
DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
|
||||
(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
|
||||
INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF
|
||||
THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR
|
||||
OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to
|
||||
attach them to the start of each source file to most effectively convey
|
||||
the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
One line to give the program's name and a brief idea of what it does.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type
|
||||
`show w'. This is free software, and you are welcome to redistribute
|
||||
it under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the
|
||||
appropriate parts of the General Public License. Of course, the commands
|
||||
you use may be called something other than `show w' and `show c'; they
|
||||
could even be mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
program `Gnomovision' (which makes passes at compilers) written by
|
||||
James Hacker.
|
||||
|
||||
signature of Ty Coon, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications
|
||||
with the library. If this is what you want to do, use the GNU Library
|
||||
General Public License instead of this License.
|
||||
|
||||
---
|
||||
|
||||
## CLASSPATH EXCEPTION
|
||||
|
||||
Linking this library statically or dynamically with other modules is
|
||||
making a combined work based on this library. Thus, the terms and
|
||||
conditions of the GNU General Public License version 2 cover the whole
|
||||
combination.
|
||||
|
||||
As a special exception, the copyright holders of this library give you
|
||||
permission to link this library with independent modules to produce an
|
||||
executable, regardless of the license terms of these independent
|
||||
modules, and to copy and distribute the resulting executable under
|
||||
terms of your choice, provided that you also meet, for each linked
|
||||
independent module, the terms and conditions of the license of that
|
||||
module. An independent module is a module which is not derived from or
|
||||
based on this library. If you modify this library, you may extend this
|
||||
exception to your version of the library, but you are not obligated to
|
||||
do so. If you do not wish to do so, delete this exception statement
|
||||
from your version.
|
|
@ -0,0 +1,50 @@
|
|||
# Notices for Eclipse Project for JavaMail
|
||||
|
||||
This content is produced and maintained by the Eclipse Project for JavaMail
|
||||
project.
|
||||
|
||||
* Project home: https://projects.eclipse.org/projects/ee4j.javamail
|
||||
|
||||
## Trademarks
|
||||
|
||||
Eclipse Project for JavaMail is a trademark of the Eclipse Foundation.
|
||||
|
||||
## Copyright
|
||||
|
||||
All content is the property of the respective authors or their employers. For
|
||||
more information regarding authorship of content, please consult the listed
|
||||
source code repository logs.
|
||||
|
||||
## Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License v. 2.0 which is available at
|
||||
http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made
|
||||
available under the following Secondary Licenses when the conditions for such
|
||||
availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU
|
||||
General Public License, version 2 with the GNU Classpath Exception which is
|
||||
available at https://www.gnu.org/software/classpath/license.html.
|
||||
|
||||
SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
||||
|
||||
## Source Code
|
||||
|
||||
The project maintains the following source code repositories:
|
||||
|
||||
* https://github.com/eclipse-ee4j/javamail
|
||||
|
||||
## Third-party Content
|
||||
|
||||
This project leverages the following third party content.
|
||||
|
||||
None
|
||||
|
||||
## Cryptography
|
||||
|
||||
Content may contain encryption software. The country in which you are currently
|
||||
may have restrictions on the import, possession, and use, and/or re-export to
|
||||
another country, of encryption software. BEFORE using any encryption software,
|
||||
please check the country's laws, regulations and policies concerning the import,
|
||||
possession, or use, and re-export of encryption software, to see if this is
|
||||
permitted.
|
||||
|
|
@ -0,0 +1 @@
|
|||
afba4942caaeaf46aab0b976afd57cc7c181467e
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1 @@
|
|||
007396407491352ce4fa30de92efb158adb76b5b
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1 @@
|
|||
1db9a709239ae473a69b5424c7e78d0b7108229d
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,14 @@
|
|||
Nimbus Language Tags
|
||||
|
||||
Copyright 2012-2016, Connect2id Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
this file except in compliance with the License. You may obtain a copy of the
|
||||
License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed
|
||||
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations under the License.
|
|
@ -0,0 +1 @@
|
|||
3981d32ddfa2919a7af46eb5e484f8dc064da665
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,14 @@
|
|||
Nimbus JOSE + JWT
|
||||
|
||||
Copyright 2012 - 2018, Connect2id Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
this file except in compliance with the License. You may obtain a copy of the
|
||||
License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed
|
||||
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations under the License.
|
|
@ -0,0 +1 @@
|
|||
422759fc195f65345e8da3265c69dea3c6cf56a5
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,14 @@
|
|||
Nimbus OAuth 2.0 SDK with OpenID Connect extensions
|
||||
|
||||
Copyright 2012-2018, Connect2id Ltd and contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
this file except in compliance with the License. You may obtain a copy of the
|
||||
License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed
|
||||
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations under the License.
|
|
@ -80,6 +80,9 @@ import org.elasticsearch.xpack.core.security.SecuritySettings;
|
|||
import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction;
|
||||
import org.elasticsearch.xpack.core.security.action.GetApiKeyAction;
|
||||
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction;
|
||||
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction;
|
||||
import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction;
|
||||
import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction;
|
||||
|
@ -135,6 +138,9 @@ import org.elasticsearch.xpack.security.action.TransportCreateApiKeyAction;
|
|||
import org.elasticsearch.xpack.security.action.TransportGetApiKeyAction;
|
||||
import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction;
|
||||
import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter;
|
||||
import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction;
|
||||
import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectLogoutAction;
|
||||
import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectPrepareAuthenticationAction;
|
||||
import org.elasticsearch.xpack.security.action.privilege.TransportDeletePrivilegesAction;
|
||||
import org.elasticsearch.xpack.security.action.privilege.TransportGetPrivilegesAction;
|
||||
import org.elasticsearch.xpack.security.action.privilege.TransportPutPrivilegesAction;
|
||||
|
@ -194,6 +200,7 @@ import org.elasticsearch.xpack.security.rest.action.RestGetApiKeyAction;
|
|||
import org.elasticsearch.xpack.security.rest.action.RestInvalidateApiKeyAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectLogoutAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.privilege.RestDeletePrivilegesAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.privilege.RestGetPrivilegesAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.privilege.RestPutPrivilegesAction;
|
||||
|
@ -205,6 +212,8 @@ import org.elasticsearch.xpack.security.rest.action.role.RestPutRoleAction;
|
|||
import org.elasticsearch.xpack.security.rest.action.rolemapping.RestDeleteRoleMappingAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.rolemapping.RestGetRoleMappingsAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.rolemapping.RestPutRoleMappingAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectAuthenticateAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectPrepareAuthenticationAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlAuthenticateAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlInvalidateSessionAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction;
|
||||
|
@ -746,6 +755,10 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
|
|||
new ActionHandler<>(SamlAuthenticateAction.INSTANCE, TransportSamlAuthenticateAction.class),
|
||||
new ActionHandler<>(SamlLogoutAction.INSTANCE, TransportSamlLogoutAction.class),
|
||||
new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class),
|
||||
new ActionHandler<>(OpenIdConnectPrepareAuthenticationAction.INSTANCE,
|
||||
TransportOpenIdConnectPrepareAuthenticationAction.class),
|
||||
new ActionHandler<>(OpenIdConnectAuthenticateAction.INSTANCE, TransportOpenIdConnectAuthenticateAction.class),
|
||||
new ActionHandler<>(OpenIdConnectLogoutAction.INSTANCE, TransportOpenIdConnectLogoutAction.class),
|
||||
new ActionHandler<>(GetPrivilegesAction.INSTANCE, TransportGetPrivilegesAction.class),
|
||||
new ActionHandler<>(PutPrivilegesAction.INSTANCE, TransportPutPrivilegesAction.class),
|
||||
new ActionHandler<>(DeletePrivilegesAction.INSTANCE, TransportDeletePrivilegesAction.class),
|
||||
|
@ -798,6 +811,9 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
|
|||
new RestSamlAuthenticateAction(settings, restController, getLicenseState()),
|
||||
new RestSamlLogoutAction(settings, restController, getLicenseState()),
|
||||
new RestSamlInvalidateSessionAction(settings, restController, getLicenseState()),
|
||||
new RestOpenIdConnectPrepareAuthenticationAction(settings, restController, getLicenseState()),
|
||||
new RestOpenIdConnectAuthenticateAction(settings, restController, getLicenseState()),
|
||||
new RestOpenIdConnectLogoutAction(settings, restController, getLicenseState()),
|
||||
new RestGetPrivilegesAction(settings, restController, getLicenseState()),
|
||||
new RestPutPrivilegesAction(settings, restController, getLicenseState()),
|
||||
new RestDeletePrivilegesAction(settings, restController, getLicenseState()),
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.action.oidc;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import com.nimbusds.openid.connect.sdk.Nonce;
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.ActionFilters;
|
||||
import org.elasticsearch.action.support.HandledTransportAction;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.io.stream.Writeable;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.tasks.Task;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateResponse;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
|
||||
import org.elasticsearch.xpack.security.authc.AuthenticationService;
|
||||
import org.elasticsearch.xpack.security.authc.TokenService;
|
||||
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm;
|
||||
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectToken;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class TransportOpenIdConnectAuthenticateAction
|
||||
extends HandledTransportAction<OpenIdConnectAuthenticateRequest, OpenIdConnectAuthenticateResponse> {
|
||||
|
||||
private final ThreadPool threadPool;
|
||||
private final AuthenticationService authenticationService;
|
||||
private final TokenService tokenService;
|
||||
|
||||
@Inject
|
||||
public TransportOpenIdConnectAuthenticateAction(ThreadPool threadPool, TransportService transportService,
|
||||
ActionFilters actionFilters, AuthenticationService authenticationService,
|
||||
TokenService tokenService) {
|
||||
super(OpenIdConnectAuthenticateAction.NAME, transportService, actionFilters,
|
||||
(Writeable.Reader<OpenIdConnectAuthenticateRequest>) OpenIdConnectAuthenticateRequest::new);
|
||||
this.threadPool = threadPool;
|
||||
this.authenticationService = authenticationService;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request,
|
||||
ActionListener<OpenIdConnectAuthenticateResponse> listener) {
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(request.getRedirectUri(), new State(request.getState()),
|
||||
new Nonce(request.getNonce()));
|
||||
final ThreadContext threadContext = threadPool.getThreadContext();
|
||||
Authentication originatingAuthentication = Authentication.getAuthentication(threadContext);
|
||||
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
|
||||
authenticationService.authenticate(OpenIdConnectAuthenticateAction.NAME, request, token, ActionListener.wrap(
|
||||
authentication -> {
|
||||
AuthenticationResult result = threadContext.getTransient(AuthenticationResult.THREAD_CONTEXT_KEY);
|
||||
if (result == null) {
|
||||
listener.onFailure(new IllegalStateException("Cannot find AuthenticationResult on thread context"));
|
||||
return;
|
||||
}
|
||||
@SuppressWarnings("unchecked") final Map<String, Object> tokenMetadata = (Map<String, Object>) result.getMetadata()
|
||||
.get(OpenIdConnectRealm.CONTEXT_TOKEN_DATA);
|
||||
tokenService.createOAuth2Tokens(authentication, originatingAuthentication, tokenMetadata, true,
|
||||
ActionListener.wrap(tuple -> {
|
||||
final String tokenString = tokenService.getAccessTokenAsString(tuple.v1());
|
||||
final TimeValue expiresIn = tokenService.getExpirationDelay();
|
||||
listener.onResponse(new OpenIdConnectAuthenticateResponse(authentication.getUser().principal(), tokenString,
|
||||
tuple.v2(), expiresIn));
|
||||
}, listener::onFailure));
|
||||
}, e -> {
|
||||
logger.debug(() -> new ParameterizedMessage("OpenIDConnectToken [{}] could not be authenticated", token), e);
|
||||
listener.onFailure(e);
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.action.oidc;
|
||||
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import com.nimbusds.jwt.JWTParser;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.ActionFilters;
|
||||
import org.elasticsearch.action.support.HandledTransportAction;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.io.stream.Writeable;
|
||||
import org.elasticsearch.tasks.Task;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.core.security.authc.Realm;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationResult;
|
||||
import org.elasticsearch.xpack.core.security.user.User;
|
||||
import org.elasticsearch.xpack.security.authc.Realms;
|
||||
import org.elasticsearch.xpack.security.authc.TokenService;
|
||||
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Transport action responsible for generating an OpenID connect logout request to be sent to an OpenID Connect Provider
|
||||
*/
|
||||
public class TransportOpenIdConnectLogoutAction extends HandledTransportAction<OpenIdConnectLogoutRequest, OpenIdConnectLogoutResponse> {
|
||||
|
||||
private final Realms realms;
|
||||
private final TokenService tokenService;
|
||||
private static final Logger logger = LogManager.getLogger(TransportOpenIdConnectLogoutAction.class);
|
||||
|
||||
@Inject
|
||||
public TransportOpenIdConnectLogoutAction(TransportService transportService, ActionFilters actionFilters, Realms realms,
|
||||
TokenService tokenService) {
|
||||
super(OpenIdConnectLogoutAction.NAME, transportService, actionFilters,
|
||||
(Writeable.Reader<OpenIdConnectLogoutRequest>) OpenIdConnectLogoutRequest::new);
|
||||
this.realms = realms;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doExecute(Task task, OpenIdConnectLogoutRequest request, ActionListener<OpenIdConnectLogoutResponse> listener) {
|
||||
invalidateRefreshToken(request.getRefreshToken(), ActionListener.wrap(ignore -> {
|
||||
final String token = request.getToken();
|
||||
tokenService.getAuthenticationAndMetaData(token, ActionListener.wrap(
|
||||
tuple -> {
|
||||
final Authentication authentication = tuple.v1();
|
||||
final Map<String, Object> tokenMetadata = tuple.v2();
|
||||
validateAuthenticationAndMetadata(authentication, tokenMetadata);
|
||||
tokenService.invalidateAccessToken(token, ActionListener.wrap(
|
||||
result -> {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("OpenID Connect Logout for user [{}] and token [{}...{}]",
|
||||
authentication.getUser().principal(),
|
||||
token.substring(0, 8),
|
||||
token.substring(token.length() - 8));
|
||||
}
|
||||
OpenIdConnectLogoutResponse response = buildResponse(authentication, tokenMetadata);
|
||||
listener.onResponse(response);
|
||||
}, listener::onFailure)
|
||||
);
|
||||
}, listener::onFailure));
|
||||
}, listener::onFailure));
|
||||
}
|
||||
|
||||
private OpenIdConnectLogoutResponse buildResponse(Authentication authentication, Map<String, Object> tokenMetadata) {
|
||||
final String idTokenHint = (String) getFromMetadata(tokenMetadata, "id_token_hint");
|
||||
final Realm realm = this.realms.realm(authentication.getAuthenticatedBy().getName());
|
||||
final JWT idToken;
|
||||
try {
|
||||
idToken = JWTParser.parse(idTokenHint);
|
||||
} catch (ParseException e) {
|
||||
throw new ElasticsearchSecurityException("Token Metadata did not contain a valid IdToken", e);
|
||||
}
|
||||
return ((OpenIdConnectRealm) realm).buildLogoutResponse(idToken);
|
||||
}
|
||||
|
||||
private void validateAuthenticationAndMetadata(Authentication authentication, Map<String, Object> tokenMetadata) {
|
||||
if (tokenMetadata == null) {
|
||||
throw new ElasticsearchSecurityException("Authentication did not contain metadata");
|
||||
}
|
||||
if (authentication == null) {
|
||||
throw new ElasticsearchSecurityException("No active authentication");
|
||||
}
|
||||
final User user = authentication.getUser();
|
||||
if (user == null) {
|
||||
throw new ElasticsearchSecurityException("No active user");
|
||||
}
|
||||
|
||||
final Authentication.RealmRef ref = authentication.getAuthenticatedBy();
|
||||
if (ref == null || Strings.isNullOrEmpty(ref.getName())) {
|
||||
throw new ElasticsearchSecurityException("Authentication {} has no authenticating realm",
|
||||
authentication);
|
||||
}
|
||||
final Realm realm = this.realms.realm(authentication.getAuthenticatedBy().getName());
|
||||
if (realm == null) {
|
||||
throw new ElasticsearchSecurityException("Authenticating realm {} does not exist", ref.getName());
|
||||
}
|
||||
if (realm instanceof OpenIdConnectRealm == false) {
|
||||
throw new IllegalArgumentException("Access token is not valid for an OpenID Connect realm");
|
||||
}
|
||||
}
|
||||
|
||||
private Object getFromMetadata(Map<String, Object> metadata, String key) {
|
||||
if (metadata.containsKey(key) == false) {
|
||||
throw new ElasticsearchSecurityException("Authentication token does not have OpenID Connect metadata [{}]", key);
|
||||
}
|
||||
Object value = metadata.get(key);
|
||||
if (null != value && value instanceof String == false) {
|
||||
throw new ElasticsearchSecurityException("In authentication token, OpenID Connect metadata [{}] is [{}] rather than " +
|
||||
"String", key, value.getClass());
|
||||
}
|
||||
return value;
|
||||
|
||||
}
|
||||
private void invalidateRefreshToken(String refreshToken, ActionListener<TokensInvalidationResult> listener) {
|
||||
if (refreshToken == null) {
|
||||
listener.onResponse(null);
|
||||
} else {
|
||||
tokenService.invalidateRefreshToken(refreshToken, listener);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.ActionFilters;
|
||||
import org.elasticsearch.action.support.HandledTransportAction;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.io.stream.Writeable;
|
||||
import org.elasticsearch.tasks.Task;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse;
|
||||
import org.elasticsearch.xpack.core.security.authc.Realm;
|
||||
import org.elasticsearch.xpack.security.authc.Realms;
|
||||
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TransportOpenIdConnectPrepareAuthenticationAction extends HandledTransportAction<OpenIdConnectPrepareAuthenticationRequest,
|
||||
OpenIdConnectPrepareAuthenticationResponse> {
|
||||
|
||||
private final Realms realms;
|
||||
|
||||
@Inject
|
||||
public TransportOpenIdConnectPrepareAuthenticationAction(TransportService transportService,
|
||||
ActionFilters actionFilters, Realms realms) {
|
||||
super(OpenIdConnectPrepareAuthenticationAction.NAME, transportService, actionFilters,
|
||||
(Writeable.Reader<OpenIdConnectPrepareAuthenticationRequest>) OpenIdConnectPrepareAuthenticationRequest::new);
|
||||
this.realms = realms;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doExecute(Task task, OpenIdConnectPrepareAuthenticationRequest request,
|
||||
ActionListener<OpenIdConnectPrepareAuthenticationResponse> listener) {
|
||||
Realm realm = null;
|
||||
if (Strings.hasText(request.getIssuer())) {
|
||||
List<OpenIdConnectRealm> matchingRealms = this.realms.stream()
|
||||
.filter(r -> r instanceof OpenIdConnectRealm && ((OpenIdConnectRealm) r).isIssuerValid(request.getIssuer()))
|
||||
.map(r -> (OpenIdConnectRealm) r)
|
||||
.collect(Collectors.toList());
|
||||
if (matchingRealms.isEmpty()) {
|
||||
listener.onFailure(
|
||||
new ElasticsearchSecurityException("Cannot find OpenID Connect realm with issuer [{}]", request.getIssuer()));
|
||||
} else if (matchingRealms.size() > 1) {
|
||||
listener.onFailure(
|
||||
new ElasticsearchSecurityException("Found multiple OpenID Connect realm with issuer [{}]", request.getIssuer()));
|
||||
} else {
|
||||
realm = matchingRealms.get(0);
|
||||
}
|
||||
} else if (Strings.hasText(request.getRealmName())) {
|
||||
realm = this.realms.realm(request.getRealmName());
|
||||
}
|
||||
|
||||
if (realm instanceof OpenIdConnectRealm) {
|
||||
prepareAuthenticationResponse((OpenIdConnectRealm) realm, request.getState(), request.getNonce(), request.getLoginHint(),
|
||||
listener);
|
||||
} else {
|
||||
listener.onFailure(
|
||||
new ElasticsearchSecurityException("Cannot find OpenID Connect realm with name [{}]", request.getRealmName()));
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareAuthenticationResponse(OpenIdConnectRealm realm, String state, String nonce, String loginHint,
|
||||
ActionListener<OpenIdConnectPrepareAuthenticationResponse> listener) {
|
||||
try {
|
||||
final OpenIdConnectPrepareAuthenticationResponse authenticationResponse =
|
||||
realm.buildAuthenticationRequestUri(state, nonce, loginHint);
|
||||
listener.onResponse(authenticationResponse);
|
||||
} catch (ElasticsearchException e) {
|
||||
listener.onFailure(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
|
|||
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings;
|
||||
import org.elasticsearch.xpack.core.ssl.SSLService;
|
||||
|
@ -27,6 +28,7 @@ import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
|
|||
import org.elasticsearch.xpack.security.authc.file.FileRealm;
|
||||
import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealm;
|
||||
import org.elasticsearch.xpack.security.authc.ldap.LdapRealm;
|
||||
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm;
|
||||
import org.elasticsearch.xpack.security.authc.pki.PkiRealm;
|
||||
import org.elasticsearch.xpack.security.authc.saml.SamlRealm;
|
||||
import org.elasticsearch.xpack.security.authc.support.RoleMappingFileBootstrapCheck;
|
||||
|
@ -45,6 +47,7 @@ import java.util.stream.Collectors;
|
|||
/**
|
||||
* Provides a single entry point into dealing with all standard XPack security {@link Realm realms}.
|
||||
* This class does not handle extensions.
|
||||
*
|
||||
* @see Realms for the component that manages configured realms (including custom extension realms)
|
||||
*/
|
||||
public final class InternalRealms {
|
||||
|
@ -53,15 +56,16 @@ public final class InternalRealms {
|
|||
* The list of all <em>internal</em> realm types, excluding {@link ReservedRealm#TYPE}.
|
||||
*/
|
||||
private static final Set<String> XPACK_TYPES = Collections
|
||||
.unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE,
|
||||
LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE, SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE));
|
||||
.unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE,
|
||||
LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE, SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE,
|
||||
OpenIdConnectRealmSettings.TYPE));
|
||||
|
||||
/**
|
||||
* The list of all standard realm types, which are those provided by x-pack and do not have extensive
|
||||
* interaction with third party sources
|
||||
*/
|
||||
private static final Set<String> STANDARD_TYPES = Collections.unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE,
|
||||
FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE));
|
||||
FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE));
|
||||
|
||||
/**
|
||||
* Determines whether <code>type</code> is an internal realm-type that is provided by x-pack,
|
||||
|
@ -90,6 +94,7 @@ public final class InternalRealms {
|
|||
/**
|
||||
* Creates {@link Realm.Factory factories} for each <em>internal</em> realm type.
|
||||
* This excludes the {@link ReservedRealm}, as it cannot be created dynamically.
|
||||
*
|
||||
* @return A map from <em>realm-type</em> to <code>Factory</code>
|
||||
*/
|
||||
public static Map<String, Realm.Factory> getFactories(ThreadPool threadPool, ResourceWatcherService resourceWatcherService,
|
||||
|
@ -105,12 +110,14 @@ public final class InternalRealms {
|
|||
return nativeRealm;
|
||||
});
|
||||
map.put(LdapRealmSettings.AD_TYPE, config -> new LdapRealm(config, sslService,
|
||||
resourceWatcherService, nativeRoleMappingStore, threadPool));
|
||||
resourceWatcherService, nativeRoleMappingStore, threadPool));
|
||||
map.put(LdapRealmSettings.LDAP_TYPE, config -> new LdapRealm(config,
|
||||
sslService, resourceWatcherService, nativeRoleMappingStore, threadPool));
|
||||
sslService, resourceWatcherService, nativeRoleMappingStore, threadPool));
|
||||
map.put(PkiRealmSettings.TYPE, config -> new PkiRealm(config, resourceWatcherService, nativeRoleMappingStore));
|
||||
map.put(SamlRealmSettings.TYPE, config -> SamlRealm.create(config, sslService, resourceWatcherService, nativeRoleMappingStore));
|
||||
map.put(KerberosRealmSettings.TYPE, config -> new KerberosRealm(config, nativeRoleMappingStore, threadPool));
|
||||
map.put(OpenIdConnectRealmSettings.TYPE, config -> new OpenIdConnectRealm(config, sslService, nativeRoleMappingStore,
|
||||
resourceWatcherService));
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,722 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKSelector;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.BadJOSEException;
|
||||
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import com.nimbusds.jose.util.IOUtils;
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
||||
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.Secret;
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import com.nimbusds.oauth2.sdk.token.AccessToken;
|
||||
import com.nimbusds.oauth2.sdk.token.BearerTokenError;
|
||||
import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
|
||||
import com.nimbusds.openid.connect.sdk.Nonce;
|
||||
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
|
||||
import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash;
|
||||
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
|
||||
import com.nimbusds.openid.connect.sdk.validators.AccessTokenValidator;
|
||||
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
|
||||
import net.minidev.json.JSONObject;
|
||||
import org.apache.commons.codec.Charsets;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.auth.AuthenticationException;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.concurrent.FutureCallback;
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.impl.auth.BasicScheme;
|
||||
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
|
||||
import org.apache.http.impl.nio.client.HttpAsyncClients;
|
||||
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
|
||||
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.nio.conn.NoopIOSessionStrategy;
|
||||
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
|
||||
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
|
||||
import org.apache.http.nio.reactor.ConnectingIOReactor;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.SpecialPermission;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.CheckedRunnable;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.SuppressForbidden;
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
import org.elasticsearch.common.util.concurrent.EsExecutors;
|
||||
import org.elasticsearch.common.util.concurrent.ListenableFuture;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
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.ssl.SSLConfiguration;
|
||||
import org.elasticsearch.xpack.core.ssl.SSLService;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.security.PrivilegedActionException;
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.ALLOWED_CLOCK_SKEW;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_CONNECT_TIMEOUT;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_CONNECTION_READ_TIMEOUT;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_MAX_CONNECTIONS;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_MAX_ENDPOINT_CONNECTIONS;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_SOCKET_TIMEOUT;
|
||||
|
||||
/**
|
||||
* Handles an OpenID Connect Authentication response as received by the facilitator. In the case of an implicit flow, validates
|
||||
* the ID Token and extracts the elasticsearch user properties from it. In the case of an authorization code flow, it first
|
||||
* exchanges the code in the authentication response for an ID Token at the token endpoint of the OpenID Connect Provider.
|
||||
*/
|
||||
public class OpenIdConnectAuthenticator {
|
||||
|
||||
private final RealmConfig realmConfig;
|
||||
private final OpenIdConnectProviderConfiguration opConfig;
|
||||
private final RelyingPartyConfiguration rpConfig;
|
||||
private final SSLService sslService;
|
||||
private AtomicReference<IDTokenValidator> idTokenValidator = new AtomicReference<>();
|
||||
private final CloseableHttpAsyncClient httpClient;
|
||||
private final ResourceWatcherService watcherService;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OpenIdConnectAuthenticator.class);
|
||||
|
||||
public OpenIdConnectAuthenticator(RealmConfig realmConfig, OpenIdConnectProviderConfiguration opConfig,
|
||||
RelyingPartyConfiguration rpConfig, SSLService sslService, ResourceWatcherService watcherService) {
|
||||
this.realmConfig = realmConfig;
|
||||
this.opConfig = opConfig;
|
||||
this.rpConfig = rpConfig;
|
||||
this.sslService = sslService;
|
||||
this.httpClient = createHttpClient();
|
||||
this.watcherService = watcherService;
|
||||
this.idTokenValidator.set(createIdTokenValidator());
|
||||
}
|
||||
|
||||
// For testing
|
||||
OpenIdConnectAuthenticator(RealmConfig realmConfig, OpenIdConnectProviderConfiguration opConfig, RelyingPartyConfiguration rpConfig,
|
||||
SSLService sslService, IDTokenValidator idTokenValidator, ResourceWatcherService watcherService) {
|
||||
this.realmConfig = realmConfig;
|
||||
this.opConfig = opConfig;
|
||||
this.rpConfig = rpConfig;
|
||||
this.sslService = sslService;
|
||||
this.httpClient = createHttpClient();
|
||||
this.idTokenValidator.set(idTokenValidator);
|
||||
this.watcherService = watcherService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an OpenID Connect Response to an Authentication Request that comes in the form of a URL with the necessary parameters,
|
||||
* that is contained in the provided Token. If the response is valid, it calls the provided listener with a set of OpenID Connect
|
||||
* claims that identify the authenticated user. If the UserInfo endpoint is specified in the configuration, we attempt to make a
|
||||
* UserInfo request and add the returned claims to the Id Token claims.
|
||||
*
|
||||
* @param token The OpenIdConnectToken to consume
|
||||
* @param listener The listener to notify with the resolved {@link JWTClaimsSet}
|
||||
*/
|
||||
public void authenticate(OpenIdConnectToken token, final ActionListener<JWTClaimsSet> listener) {
|
||||
try {
|
||||
AuthenticationResponse authenticationResponse = AuthenticationResponseParser.parse(new URI(token.getRedirectUrl()));
|
||||
final Nonce expectedNonce = token.getNonce();
|
||||
State expectedState = token.getState();
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("OpenID Connect Provider redirected user to [{}]. Expected Nonce is [{}] and expected State is [{}]",
|
||||
token.getRedirectUrl(), expectedNonce, expectedState);
|
||||
}
|
||||
if (authenticationResponse instanceof AuthenticationErrorResponse) {
|
||||
ErrorObject error = ((AuthenticationErrorResponse) authenticationResponse).getErrorObject();
|
||||
listener.onFailure(new ElasticsearchSecurityException("OpenID Connect Provider response indicates authentication failure" +
|
||||
"Code=[{}], Description=[{}]", error.getCode(), error.getDescription()));
|
||||
return;
|
||||
}
|
||||
final AuthenticationSuccessResponse response = authenticationResponse.toSuccessResponse();
|
||||
validateState(expectedState, response.getState());
|
||||
validateResponseType(response);
|
||||
if (rpConfig.getResponseType().impliesCodeFlow()) {
|
||||
final AuthorizationCode code = response.getAuthorizationCode();
|
||||
exchangeCodeForToken(code, ActionListener.wrap(tokens -> {
|
||||
final AccessToken accessToken = tokens.v1();
|
||||
final JWT idToken = tokens.v2();
|
||||
validateAccessToken(accessToken, idToken);
|
||||
getUserClaims(accessToken, idToken, expectedNonce, true, listener);
|
||||
}, listener::onFailure));
|
||||
} else {
|
||||
final JWT idToken = response.getIDToken();
|
||||
final AccessToken accessToken = response.getAccessToken();
|
||||
validateAccessToken(accessToken, idToken);
|
||||
getUserClaims(accessToken, idToken, expectedNonce, true, listener);
|
||||
}
|
||||
} catch (ElasticsearchSecurityException e) {
|
||||
// Don't wrap in a new ElasticsearchSecurityException
|
||||
listener.onFailure(e);
|
||||
} catch (Exception e) {
|
||||
listener.onFailure(new ElasticsearchSecurityException("Failed to consume the OpenID connect response. ", e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all the user claims we can get for the authenticated user. This happens in two steps:
|
||||
* <ul>
|
||||
* <li>First we attempt to validate the Id Token we have received and get any claims it contains</li>
|
||||
* <li>If we have received an Access Token and the UserInfo endpoint is configured, we also attempt to get the user info response
|
||||
* from there and parse the returned claims,
|
||||
* see {@link OpenIdConnectAuthenticator#getAndCombineUserInfoClaims(AccessToken, JWTClaimsSet, ActionListener)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param accessToken The {@link AccessToken} that the OP has issued for this user
|
||||
* @param idToken The {@link JWT} Id Token that the OP has issued for this user
|
||||
* @param expectedNonce The nonce value we sent in the authentication request and should be contained in the Id Token
|
||||
* @param claimsListener The listener to notify with the resolved {@link JWTClaimsSet}
|
||||
*/
|
||||
private void getUserClaims(@Nullable AccessToken accessToken, JWT idToken, Nonce expectedNonce, boolean shouldRetry,
|
||||
ActionListener<JWTClaimsSet> claimsListener) {
|
||||
try {
|
||||
JWTClaimsSet verifiedIdTokenClaims = idTokenValidator.get().validate(idToken, expectedNonce).toJWTClaimsSet();
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Received and validated the Id Token for the user: [{}]", verifiedIdTokenClaims);
|
||||
}
|
||||
// Add the Id Token string as a synthetic claim
|
||||
final JSONObject verifiedIdTokenClaimsObject = verifiedIdTokenClaims.toJSONObject();
|
||||
final JWTClaimsSet idTokenClaim = new JWTClaimsSet.Builder().claim("id_token_hint", idToken.serialize()).build();
|
||||
verifiedIdTokenClaimsObject.merge(idTokenClaim.toJSONObject());
|
||||
final JWTClaimsSet enrichedVerifiedIdTokenClaims = JWTClaimsSet.parse(verifiedIdTokenClaimsObject);
|
||||
if (accessToken != null && opConfig.getUserinfoEndpoint() != null) {
|
||||
getAndCombineUserInfoClaims(accessToken, enrichedVerifiedIdTokenClaims, claimsListener);
|
||||
} else {
|
||||
if (accessToken == null && opConfig.getUserinfoEndpoint() != null) {
|
||||
LOGGER.debug("UserInfo endpoint is configured but the OP didn't return an access token so we can't query it");
|
||||
} else if (accessToken != null && opConfig.getUserinfoEndpoint() == null) {
|
||||
LOGGER.debug("OP returned an access token but the UserInfo endpoint is not configured.");
|
||||
}
|
||||
claimsListener.onResponse(enrichedVerifiedIdTokenClaims);
|
||||
}
|
||||
} catch (BadJOSEException e) {
|
||||
// We only try to update the cached JWK set once if a remote source is used and
|
||||
// RSA or ECDSA is used for signatures
|
||||
if (shouldRetry
|
||||
&& JWSAlgorithm.Family.HMAC_SHA.contains(rpConfig.getSignatureAlgorithm()) == false
|
||||
&& opConfig.getJwkSetPath().startsWith("https://")) {
|
||||
((ReloadableJWKSource) ((JWSVerificationKeySelector) idTokenValidator.get().getJWSKeySelector()).getJWKSource())
|
||||
.triggerReload(ActionListener.wrap(v -> {
|
||||
getUserClaims(accessToken, idToken, expectedNonce, false, claimsListener);
|
||||
}, ex -> {
|
||||
LOGGER.trace("Attempted and failed to refresh JWK cache upon token validation failure", e);
|
||||
claimsListener.onFailure(ex);
|
||||
}));
|
||||
} else {
|
||||
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
|
||||
}
|
||||
} catch (com.nimbusds.oauth2.sdk.ParseException | ParseException | JOSEException e) {
|
||||
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an access token according to the
|
||||
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#ImplicitTokenValidation">specification</a>.
|
||||
* <p>
|
||||
* When using the authorization code flow the OP might not provide the at_hash parameter in the
|
||||
* Id Token as allowed in the specification. In such a case we can't validate the access token
|
||||
* but this is considered safe as it was received in a back channel communication that was protected
|
||||
* by TLS. Also when using the implicit flow with the response type set to "id_token", no Access
|
||||
* Token will be returned from the OP
|
||||
*
|
||||
* @param accessToken The Access Token to validate. Can be null when the configured response type is "id_token"
|
||||
* @param idToken The Id Token that was received in the same response
|
||||
*/
|
||||
private void validateAccessToken(AccessToken accessToken, JWT idToken) {
|
||||
try {
|
||||
if (rpConfig.getResponseType().equals(ResponseType.parse("id_token token")) ||
|
||||
rpConfig.getResponseType().equals(ResponseType.parse("code"))) {
|
||||
assert (accessToken != null) : "Access Token cannot be null for Response Type " + rpConfig.getResponseType().toString();
|
||||
final boolean isValidationOptional = rpConfig.getResponseType().equals(ResponseType.parse("code"));
|
||||
// only "Bearer" is defined in the specification but check just in case
|
||||
if (accessToken.getType().toString().equals("Bearer") == false) {
|
||||
throw new ElasticsearchSecurityException("Invalid access token type [{}], while [Bearer] was expected",
|
||||
accessToken.getType());
|
||||
}
|
||||
String atHashValue = idToken.getJWTClaimsSet().getStringClaim("at_hash");
|
||||
if (Strings.hasText(atHashValue) == false) {
|
||||
if (isValidationOptional == false) {
|
||||
throw new ElasticsearchSecurityException("Failed to verify access token. ID Token doesn't contain at_hash claim ");
|
||||
}
|
||||
} else {
|
||||
AccessTokenHash atHash = new AccessTokenHash(atHashValue);
|
||||
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(idToken.getHeader().getAlgorithm().getName());
|
||||
AccessTokenValidator.validate(accessToken, jwsAlgorithm, atHash);
|
||||
}
|
||||
} else if (rpConfig.getResponseType().equals(ResponseType.parse("id_token")) && accessToken != null) {
|
||||
// This should NOT happen and indicates a misconfigured OP. Warn the user but do not fail
|
||||
LOGGER.warn("Access Token incorrectly returned from the OpenId Connect Provider while using \"id_token\" response type.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ElasticsearchSecurityException("Failed to verify access token.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses a JWKSet from a file
|
||||
*
|
||||
* @param jwkSetPath The path to the file that contains the JWKs as a string.
|
||||
* @return the parsed {@link JWKSet}
|
||||
* @throws ParseException if the file cannot be parsed
|
||||
* @throws IOException if the file cannot be read
|
||||
*/
|
||||
@SuppressForbidden(reason = "uses toFile")
|
||||
private JWKSet readJwkSetFromFile(String jwkSetPath) throws IOException, ParseException {
|
||||
final Path path = realmConfig.env().configFile().resolve(jwkSetPath);
|
||||
return JWKSet.load(path.toFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the response we received corresponds to the response type we requested
|
||||
*
|
||||
* @param response The {@link AuthenticationSuccessResponse} we received
|
||||
* @throws ElasticsearchSecurityException if the response is not the expected one for the configured response type
|
||||
*/
|
||||
private void validateResponseType(AuthenticationSuccessResponse response) {
|
||||
if (rpConfig.getResponseType().equals(response.impliedResponseType()) == false) {
|
||||
throw new ElasticsearchSecurityException("Unexpected response type [{}], while [{}] is configured",
|
||||
response.impliedResponseType(), rpConfig.getResponseType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the state parameter the response contained corresponds to the one that we generated in the
|
||||
* beginning of this authentication attempt and was stored with the user's session at the facilitator
|
||||
*
|
||||
* @param expectedState The state that was originally generated
|
||||
* @param state The state that was contained in the response
|
||||
*/
|
||||
private void validateState(State expectedState, State state) {
|
||||
if (null == state) {
|
||||
throw new ElasticsearchSecurityException("Failed to validate the response, the response did not contain a state parameter");
|
||||
} else if (null == expectedState) {
|
||||
throw new ElasticsearchSecurityException("Failed to validate the response, the user's session did not contain a state " +
|
||||
"parameter");
|
||||
} else if (state.equals(expectedState) == false) {
|
||||
throw new ElasticsearchSecurityException("Invalid state parameter [{}], while [{}] was expected", state, expectedState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to make a request to the UserInfo Endpoint of the OpenID Connect provider
|
||||
*/
|
||||
private void getAndCombineUserInfoClaims(AccessToken accessToken, JWTClaimsSet verifiedIdTokenClaims,
|
||||
ActionListener<JWTClaimsSet> claimsListener) {
|
||||
try {
|
||||
final HttpGet httpGet = new HttpGet(opConfig.getUserinfoEndpoint());
|
||||
httpGet.setHeader("Authorization", "Bearer " + accessToken.getValue());
|
||||
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
|
||||
httpClient.execute(httpGet, new FutureCallback<HttpResponse>() {
|
||||
@Override
|
||||
public void completed(HttpResponse result) {
|
||||
handleUserinfoResponse(result, verifiedIdTokenClaims, claimsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Exception ex) {
|
||||
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to get claims from the Userinfo Endpoint.",
|
||||
ex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelled() {
|
||||
claimsListener.onFailure(
|
||||
new ElasticsearchSecurityException("Failed to get claims from the Userinfo Endpoint. Request was cancelled"));
|
||||
}
|
||||
});
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to get user information from the UserInfo endpoint.", e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the UserInfo Response from the OpenID Connect Provider. If successful, merge the returned claims with the claims
|
||||
* of the Id Token and call the provided listener.
|
||||
*/
|
||||
private void handleUserinfoResponse(HttpResponse httpResponse, JWTClaimsSet verifiedIdTokenClaims,
|
||||
ActionListener<JWTClaimsSet> claimsListener) {
|
||||
try {
|
||||
final HttpEntity entity = httpResponse.getEntity();
|
||||
final Header encodingHeader = entity.getContentEncoding();
|
||||
final Charset encoding = encodingHeader == null ? StandardCharsets.UTF_8 : Charsets.toCharset(encodingHeader.getValue());
|
||||
final Header contentHeader = entity.getContentType();
|
||||
final String contentAsString = EntityUtils.toString(entity, encoding);
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Received UserInfo Response from OP with status [{}] and content [{}] ",
|
||||
httpResponse.getStatusLine().getStatusCode(), contentAsString);
|
||||
}
|
||||
if (httpResponse.getStatusLine().getStatusCode() == 200) {
|
||||
if (ContentType.parse(contentHeader.getValue()).getMimeType().equals("application/json")) {
|
||||
final JWTClaimsSet userInfoClaims = JWTClaimsSet.parse(contentAsString);
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Successfully retrieved user information: [{}]", userInfoClaims.toJSONObject().toJSONString());
|
||||
}
|
||||
final JSONObject combinedClaims = verifiedIdTokenClaims.toJSONObject();
|
||||
combinedClaims.merge(userInfoClaims.toJSONObject());
|
||||
claimsListener.onResponse(JWTClaimsSet.parse(combinedClaims));
|
||||
} else if (ContentType.parse(contentHeader.getValue()).getMimeType().equals("application/jwt")) {
|
||||
//TODO Handle validating possibly signed responses
|
||||
claimsListener.onFailure(new IllegalStateException("Unable to parse Userinfo Response. Signed/encryopted JWTs are" +
|
||||
"not currently supported"));
|
||||
} else {
|
||||
claimsListener.onFailure(new IllegalStateException("Unable to parse Userinfo Response. Content type was expected to " +
|
||||
"be [application/json] or [appliation/jwt] but was [" + contentHeader.getValue() + "]"));
|
||||
}
|
||||
} else {
|
||||
final Header wwwAuthenticateHeader = httpResponse.getFirstHeader("WWW-Authenticate");
|
||||
if (Strings.hasText(wwwAuthenticateHeader.getValue())) {
|
||||
BearerTokenError error = BearerTokenError.parse(wwwAuthenticateHeader.getValue());
|
||||
claimsListener.onFailure(
|
||||
new ElasticsearchSecurityException("Failed to get user information from the UserInfo endpoint. Code=[{}], " +
|
||||
"Description=[{}]", error.getCode(), error.getDescription()));
|
||||
} else {
|
||||
claimsListener.onFailure(
|
||||
new ElasticsearchSecurityException("Failed to get user information from the UserInfo endpoint. Code=[{}], " +
|
||||
"Description=[{}]", httpResponse.getStatusLine().getStatusCode(),
|
||||
httpResponse.getStatusLine().getReasonPhrase()));
|
||||
}
|
||||
}
|
||||
} catch (IOException | com.nimbusds.oauth2.sdk.ParseException | ParseException e) {
|
||||
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to get user information from the UserInfo endpoint.",
|
||||
e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to make a request to the Token Endpoint of the OpenID Connect provider in order to exchange an
|
||||
* authorization code for an Id Token (and potentially an Access Token)
|
||||
*/
|
||||
private void exchangeCodeForToken(AuthorizationCode code, ActionListener<Tuple<AccessToken, JWT>> tokensListener) {
|
||||
try {
|
||||
final AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, rpConfig.getRedirectUri());
|
||||
final HttpPost httpPost = new HttpPost(opConfig.getTokenEndpoint());
|
||||
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)));
|
||||
}
|
||||
httpPost.setEntity(new UrlEncodedFormEntity(params));
|
||||
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
UsernamePasswordCredentials creds = new UsernamePasswordCredentials(rpConfig.getClientId().getValue(),
|
||||
rpConfig.getClientSecret().toString());
|
||||
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) {
|
||||
handleTokenResponse(result, tokensListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Exception ex) {
|
||||
tokensListener.onFailure(
|
||||
new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint.", ex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelled() {
|
||||
final String message = "Failed to exchange code for Id Token using the Token Endpoint. Request was cancelled";
|
||||
tokensListener.onFailure(new ElasticsearchSecurityException(message));
|
||||
}
|
||||
});
|
||||
return null;
|
||||
});
|
||||
} catch (AuthenticationException | UnsupportedEncodingException e) {
|
||||
tokensListener.onFailure(
|
||||
new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint.", e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Token Response from the OpenID Connect Provider. If successful, extract the (yet not validated) Id Token
|
||||
* and access token and call the provided listener.
|
||||
*/
|
||||
private void handleTokenResponse(HttpResponse httpResponse, ActionListener<Tuple<AccessToken, JWT>> tokensListener) {
|
||||
try {
|
||||
final HttpEntity entity = httpResponse.getEntity();
|
||||
final Header encodingHeader = entity.getContentEncoding();
|
||||
final Header contentHeader = entity.getContentType();
|
||||
if (ContentType.parse(contentHeader.getValue()).getMimeType().equals("application/json") == false) {
|
||||
tokensListener.onFailure(new IllegalStateException("Unable to parse Token Response. Content type was expected to be " +
|
||||
"[application/json] but was [" + contentHeader.getValue() + "]"));
|
||||
return;
|
||||
}
|
||||
final Charset encoding = encodingHeader == null ? StandardCharsets.UTF_8 : Charsets.toCharset(encodingHeader.getValue());
|
||||
final String json = EntityUtils.toString(entity, encoding);
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Received Token Response from OP with status [{}] and content [{}] ",
|
||||
httpResponse.getStatusLine().getStatusCode(), json);
|
||||
}
|
||||
final OIDCTokenResponse oidcTokenResponse = OIDCTokenResponse.parse(JSONObjectUtils.parse(json));
|
||||
if (oidcTokenResponse.indicatesSuccess() == false) {
|
||||
TokenErrorResponse errorResponse = oidcTokenResponse.toErrorResponse();
|
||||
tokensListener.onFailure(
|
||||
new ElasticsearchSecurityException("Failed to exchange code for Id Token. Code=[{}], Description=[{}]",
|
||||
errorResponse.getErrorObject().getCode(), errorResponse.getErrorObject().getDescription()));
|
||||
} else {
|
||||
OIDCTokenResponse successResponse = oidcTokenResponse.toSuccessResponse();
|
||||
final OIDCTokens oidcTokens = successResponse.getOIDCTokens();
|
||||
final AccessToken accessToken = oidcTokens.getAccessToken();
|
||||
final JWT idToken = oidcTokens.getIDToken();
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Successfully exchanged code for ID Token: [{}] and Access Token [{}]",
|
||||
idToken, accessToken);
|
||||
}
|
||||
if (idToken == null) {
|
||||
tokensListener.onFailure(new ElasticsearchSecurityException("Token Response did not contain an ID Token or parsing of" +
|
||||
" the JWT failed."));
|
||||
return;
|
||||
}
|
||||
tokensListener.onResponse(new Tuple<>(accessToken, idToken));
|
||||
}
|
||||
} catch (IOException | com.nimbusds.oauth2.sdk.ParseException e) {
|
||||
tokensListener.onFailure(
|
||||
new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint. " +
|
||||
"Unable to parse Token Response", e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CloseableHttpAsyncClient} that uses a {@link PoolingNHttpClientConnectionManager}
|
||||
*/
|
||||
private CloseableHttpAsyncClient createHttpClient() {
|
||||
try {
|
||||
SpecialPermission.check();
|
||||
return AccessController.doPrivileged(
|
||||
(PrivilegedExceptionAction<CloseableHttpAsyncClient>) () -> {
|
||||
ConnectingIOReactor ioReactor = new DefaultConnectingIOReactor();
|
||||
final String sslKey = RealmSettings.realmSslPrefix(realmConfig.identifier());
|
||||
final SSLConfiguration sslConfiguration = sslService.getSSLConfiguration(sslKey);
|
||||
final SSLContext clientContext = sslService.sslContext(sslConfiguration);
|
||||
boolean isHostnameVerificationEnabled = sslConfiguration.verificationMode().isHostnameVerificationEnabled();
|
||||
final HostnameVerifier verifier = isHostnameVerificationEnabled ?
|
||||
new DefaultHostnameVerifier() : NoopHostnameVerifier.INSTANCE;
|
||||
Registry<SchemeIOSessionStrategy> registry = RegistryBuilder.<SchemeIOSessionStrategy>create()
|
||||
.register("http", NoopIOSessionStrategy.INSTANCE)
|
||||
.register("https", new SSLIOSessionStrategy(clientContext, verifier))
|
||||
.build();
|
||||
PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager(ioReactor, registry);
|
||||
connectionManager.setDefaultMaxPerRoute(realmConfig.getSetting(HTTP_MAX_ENDPOINT_CONNECTIONS));
|
||||
connectionManager.setMaxTotal(realmConfig.getSetting(HTTP_MAX_CONNECTIONS));
|
||||
final RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(Math.toIntExact(realmConfig.getSetting(HTTP_CONNECT_TIMEOUT).getMillis()))
|
||||
.setConnectionRequestTimeout(Math.toIntExact(realmConfig.getSetting(HTTP_CONNECTION_READ_TIMEOUT).getSeconds()))
|
||||
.setSocketTimeout(Math.toIntExact(realmConfig.getSetting(HTTP_SOCKET_TIMEOUT).getMillis())).build();
|
||||
CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.custom()
|
||||
.setConnectionManager(connectionManager)
|
||||
.setDefaultRequestConfig(requestConfig)
|
||||
.build();
|
||||
httpAsyncClient.start();
|
||||
return httpAsyncClient;
|
||||
});
|
||||
} catch (PrivilegedActionException e) {
|
||||
throw new IllegalStateException("Unable to create a HttpAsyncClient instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates an {@link IDTokenValidator} based on the current Relying Party configuration
|
||||
*/
|
||||
IDTokenValidator createIdTokenValidator() {
|
||||
try {
|
||||
final JWSAlgorithm requestedAlgorithm = rpConfig.getSignatureAlgorithm();
|
||||
final int allowedClockSkew = Math.toIntExact(realmConfig.getSetting(ALLOWED_CLOCK_SKEW).getMillis());
|
||||
final IDTokenValidator idTokenValidator;
|
||||
if (JWSAlgorithm.Family.HMAC_SHA.contains(requestedAlgorithm)) {
|
||||
final Secret clientSecret = new Secret(rpConfig.getClientSecret().toString());
|
||||
idTokenValidator =
|
||||
new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), requestedAlgorithm, clientSecret);
|
||||
} else {
|
||||
String jwkSetPath = opConfig.getJwkSetPath();
|
||||
if (jwkSetPath.startsWith("https://")) {
|
||||
final JWSVerificationKeySelector keySelector = new JWSVerificationKeySelector(requestedAlgorithm,
|
||||
new ReloadableJWKSource(new URL(jwkSetPath)));
|
||||
idTokenValidator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), keySelector, null);
|
||||
} else {
|
||||
setMetadataFileWatcher(jwkSetPath);
|
||||
final JWKSet jwkSet = readJwkSetFromFile(jwkSetPath);
|
||||
idTokenValidator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), requestedAlgorithm, jwkSet);
|
||||
}
|
||||
}
|
||||
idTokenValidator.setMaxClockSkew(allowedClockSkew);
|
||||
return idTokenValidator;
|
||||
} catch (IOException | ParseException e) {
|
||||
throw new IllegalStateException("Unable to create a IDTokenValidator instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMetadataFileWatcher(String jwkSetPath) throws IOException {
|
||||
final Path path = realmConfig.env().configFile().resolve(jwkSetPath);
|
||||
FileWatcher watcher = new FileWatcher(path);
|
||||
watcher.addListener(new FileListener(LOGGER, () -> this.idTokenValidator.set(createIdTokenValidator())));
|
||||
watcherService.add(watcher, ResourceWatcherService.Frequency.MEDIUM);
|
||||
}
|
||||
|
||||
protected void close() {
|
||||
try {
|
||||
this.httpClient.close();
|
||||
} catch (IOException e) {
|
||||
LOGGER.debug("Unable to close the HttpAsyncClient", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileListener implements FileChangesListener {
|
||||
|
||||
private final Logger logger;
|
||||
private final CheckedRunnable<Exception> onChange;
|
||||
|
||||
private FileListener(Logger logger, CheckedRunnable<Exception> onChange) {
|
||||
this.logger = logger;
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileCreated(Path file) {
|
||||
onFileChanged(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileDeleted(Path file) {
|
||||
onFileChanged(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileChanged(Path file) {
|
||||
try {
|
||||
onChange.run();
|
||||
} catch (Exception e) {
|
||||
logger.warn(new ParameterizedMessage("An error occurred while reloading file {}", file), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote JSON Web Key source specified by a JWKSet URL. The retrieved JWK set is cached to
|
||||
* avoid unnecessary http requests. A single attempt to update the cached set is made
|
||||
* (with {@ling ReloadableJWKSource#triggerReload}) when the {@link IDTokenValidator} fails
|
||||
* to validate an ID Token (because of an unknown key) as this might mean that the OpenID
|
||||
* Connect Provider has rotated the signing keys.
|
||||
*/
|
||||
class ReloadableJWKSource<C extends SecurityContext> implements JWKSource<C> {
|
||||
|
||||
private volatile JWKSet cachedJwkSet = new JWKSet();
|
||||
private final AtomicReference<ListenableFuture<Void>> reloadFutureRef = new AtomicReference<>();
|
||||
private final URL jwkSetPath;
|
||||
|
||||
private ReloadableJWKSource(URL jwkSetPath) {
|
||||
this.jwkSetPath = jwkSetPath;
|
||||
triggerReload(ActionListener.wrap(success -> LOGGER.trace("Successfully loaded and cached remote JWKSet on startup"),
|
||||
failure -> LOGGER.trace("Failed to load and cache remote JWKSet on startup", failure)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JWK> get(JWKSelector jwkSelector, C context) {
|
||||
return jwkSelector.select(cachedJwkSet);
|
||||
}
|
||||
|
||||
void triggerReload(ActionListener<Void> toNotify) {
|
||||
ListenableFuture<Void> future = reloadFutureRef.get();
|
||||
while (future == null) {
|
||||
future = new ListenableFuture<>();
|
||||
if (reloadFutureRef.compareAndSet(null, future)) {
|
||||
reloadAsync(future);
|
||||
} else {
|
||||
future = reloadFutureRef.get();
|
||||
}
|
||||
}
|
||||
future.addListener(toNotify, EsExecutors.newDirectExecutorService(), null);
|
||||
}
|
||||
|
||||
void reloadAsync(final ListenableFuture<Void> future) {
|
||||
try {
|
||||
final HttpGet httpGet = new HttpGet(jwkSetPath.toURI());
|
||||
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
|
||||
httpClient.execute(httpGet, new FutureCallback<HttpResponse>() {
|
||||
@Override
|
||||
public void completed(HttpResponse result) {
|
||||
try {
|
||||
cachedJwkSet = JWKSet.parse(IOUtils.readInputStreamToString(result.getEntity().getContent(),
|
||||
StandardCharsets.UTF_8));
|
||||
reloadFutureRef.set(null);
|
||||
LOGGER.trace("Successfully refreshed and cached remote JWKSet");
|
||||
} catch (IOException | ParseException e) {
|
||||
failed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Exception ex) {
|
||||
future.onFailure(new ElasticsearchSecurityException("Failed to retrieve remote JWK set.", ex));
|
||||
reloadFutureRef.set(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelled() {
|
||||
future.onFailure(
|
||||
new ElasticsearchSecurityException("Failed to retrieve remote JWK set. Request was cancelled."));
|
||||
reloadFutureRef.set(null);
|
||||
}
|
||||
});
|
||||
return null;
|
||||
});
|
||||
} catch (URISyntaxException e) {
|
||||
future.onFailure(e);
|
||||
reloadFutureRef.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.id.Issuer;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A Class that contains all the OpenID Connect Provider configuration
|
||||
*/
|
||||
public class OpenIdConnectProviderConfiguration {
|
||||
private final String providerName;
|
||||
private final URI authorizationEndpoint;
|
||||
private final URI tokenEndpoint;
|
||||
private final URI userinfoEndpoint;
|
||||
private final URI endsessionEndpoint;
|
||||
private final Issuer issuer;
|
||||
private final String jwkSetPath;
|
||||
|
||||
public OpenIdConnectProviderConfiguration(String providerName, Issuer issuer, String jwkSetPath, URI authorizationEndpoint,
|
||||
URI tokenEndpoint, @Nullable URI userinfoEndpoint, @Nullable URI endsessionEndpoint) {
|
||||
this.providerName = Objects.requireNonNull(providerName, "OP Name must be provided");
|
||||
this.authorizationEndpoint = Objects.requireNonNull(authorizationEndpoint, "Authorization Endpoint must be provided");
|
||||
this.tokenEndpoint = Objects.requireNonNull(tokenEndpoint, "Token Endpoint must be provided");
|
||||
this.userinfoEndpoint = userinfoEndpoint;
|
||||
this.endsessionEndpoint = endsessionEndpoint;
|
||||
this.issuer = Objects.requireNonNull(issuer, "OP Issuer must be provided");
|
||||
this.jwkSetPath = Objects.requireNonNull(jwkSetPath, "jwkSetUrl must be provided");
|
||||
}
|
||||
|
||||
public String getProviderName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
public URI getAuthorizationEndpoint() {
|
||||
return authorizationEndpoint;
|
||||
}
|
||||
|
||||
public URI getTokenEndpoint() {
|
||||
return tokenEndpoint;
|
||||
}
|
||||
|
||||
public URI getUserinfoEndpoint() {
|
||||
return userinfoEndpoint;
|
||||
}
|
||||
|
||||
public URI getEndsessionEndpoint() {
|
||||
return endsessionEndpoint;
|
||||
}
|
||||
|
||||
public Issuer getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public String getJwkSetPath() {
|
||||
return jwkSetPath;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,473 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jwt.JWT;
|
||||
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.id.ClientID;
|
||||
import com.nimbusds.oauth2.sdk.id.Issuer;
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
|
||||
import com.nimbusds.openid.connect.sdk.LogoutRequest;
|
||||
import com.nimbusds.openid.connect.sdk.Nonce;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.lease.Releasable;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Setting;
|
||||
import org.elasticsearch.common.settings.SettingsException;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.elasticsearch.xpack.core.XPackSettings;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
|
||||
import org.elasticsearch.xpack.core.security.authc.Realm;
|
||||
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.security.user.User;
|
||||
import org.elasticsearch.xpack.core.ssl.SSLService;
|
||||
import org.elasticsearch.xpack.security.authc.TokenService;
|
||||
import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport;
|
||||
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.DN_CLAIM;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.GROUPS_CLAIM;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.MAIL_CLAIM;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.NAME_CLAIM;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_ENDSESSION_ENDPOINT;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_ISSUER;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_JWKSET_PATH;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_NAME;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT;
|
||||
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_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;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_REDIRECT_URI;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_RESPONSE_TYPE;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES;
|
||||
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_SIGNATURE_ALGORITHM;
|
||||
|
||||
public class OpenIdConnectRealm extends Realm implements Releasable {
|
||||
|
||||
public static final String CONTEXT_TOKEN_DATA = "_oidc_tokendata";
|
||||
private final OpenIdConnectProviderConfiguration opConfiguration;
|
||||
private final RelyingPartyConfiguration rpConfiguration;
|
||||
private final OpenIdConnectAuthenticator openIdConnectAuthenticator;
|
||||
private final ClaimParser principalAttribute;
|
||||
private final ClaimParser groupsAttribute;
|
||||
private final ClaimParser dnAttribute;
|
||||
private final ClaimParser nameAttribute;
|
||||
private final ClaimParser mailAttribute;
|
||||
private final Boolean populateUserMetadata;
|
||||
private final UserRoleMapper roleMapper;
|
||||
|
||||
private DelegatedAuthorizationSupport delegatedRealms;
|
||||
|
||||
public OpenIdConnectRealm(RealmConfig config, SSLService sslService, UserRoleMapper roleMapper,
|
||||
ResourceWatcherService watcherService) {
|
||||
super(config);
|
||||
this.roleMapper = roleMapper;
|
||||
this.rpConfiguration = buildRelyingPartyConfiguration(config);
|
||||
this.opConfiguration = buildOpenIdConnectProviderConfiguration(config);
|
||||
this.principalAttribute = ClaimParser.forSetting(logger, PRINCIPAL_CLAIM, config, true);
|
||||
this.groupsAttribute = ClaimParser.forSetting(logger, GROUPS_CLAIM, config, false);
|
||||
this.dnAttribute = ClaimParser.forSetting(logger, DN_CLAIM, config, false);
|
||||
this.nameAttribute = ClaimParser.forSetting(logger, NAME_CLAIM, config, false);
|
||||
this.mailAttribute = ClaimParser.forSetting(logger, MAIL_CLAIM, config, false);
|
||||
this.populateUserMetadata = config.getSetting(POPULATE_USER_METADATA);
|
||||
if (TokenService.isTokenServiceEnabled(config.settings()) == false) {
|
||||
throw new IllegalStateException("OpenID Connect Realm requires that the token service be enabled ("
|
||||
+ XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey() + ")");
|
||||
}
|
||||
this.openIdConnectAuthenticator =
|
||||
new OpenIdConnectAuthenticator(config, opConfiguration, rpConfiguration, sslService, watcherService);
|
||||
}
|
||||
|
||||
// For testing
|
||||
OpenIdConnectRealm(RealmConfig config, OpenIdConnectAuthenticator authenticator, UserRoleMapper roleMapper) {
|
||||
super(config);
|
||||
this.roleMapper = roleMapper;
|
||||
this.rpConfiguration = buildRelyingPartyConfiguration(config);
|
||||
this.opConfiguration = buildOpenIdConnectProviderConfiguration(config);
|
||||
this.openIdConnectAuthenticator = authenticator;
|
||||
this.principalAttribute = ClaimParser.forSetting(logger, PRINCIPAL_CLAIM, config, true);
|
||||
this.groupsAttribute = ClaimParser.forSetting(logger, GROUPS_CLAIM, config, false);
|
||||
this.dnAttribute = ClaimParser.forSetting(logger, DN_CLAIM, config, false);
|
||||
this.nameAttribute = ClaimParser.forSetting(logger, NAME_CLAIM, config, false);
|
||||
this.mailAttribute = ClaimParser.forSetting(logger, MAIL_CLAIM, config, false);
|
||||
this.populateUserMetadata = config.getSetting(POPULATE_USER_METADATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Iterable<Realm> realms, XPackLicenseState licenseState) {
|
||||
if (delegatedRealms != null) {
|
||||
throw new IllegalStateException("Realm has already been initialized");
|
||||
}
|
||||
delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(AuthenticationToken token) {
|
||||
return token instanceof OpenIdConnectToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationToken token(ThreadContext context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationToken token, ActionListener<AuthenticationResult> listener) {
|
||||
if (token instanceof OpenIdConnectToken) {
|
||||
OpenIdConnectToken oidcToken = (OpenIdConnectToken) token;
|
||||
openIdConnectAuthenticator.authenticate(oidcToken, ActionListener.wrap(
|
||||
jwtClaimsSet -> {
|
||||
buildUserFromClaims(jwtClaimsSet, listener);
|
||||
},
|
||||
e -> {
|
||||
logger.debug("Failed to consume the OpenIdConnectToken ", e);
|
||||
if (e instanceof ElasticsearchSecurityException) {
|
||||
listener.onResponse(AuthenticationResult.unsuccessful("Failed to authenticate user with OpenID Connect", e));
|
||||
} else {
|
||||
listener.onFailure(e);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
listener.onResponse(AuthenticationResult.notHandled());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lookupUser(String username, ActionListener<User> listener) {
|
||||
listener.onResponse(null);
|
||||
}
|
||||
|
||||
|
||||
private void buildUserFromClaims(JWTClaimsSet claims, ActionListener<AuthenticationResult> authResultListener) {
|
||||
final String principal = principalAttribute.getClaimValue(claims);
|
||||
if (Strings.isNullOrEmpty(principal)) {
|
||||
authResultListener.onResponse(AuthenticationResult.unsuccessful(
|
||||
principalAttribute + "not found in " + claims.toJSONObject(), null));
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, Object> tokenMetadata = new HashMap<>();
|
||||
tokenMetadata.put("id_token_hint", claims.getClaim("id_token_hint"));
|
||||
ActionListener<AuthenticationResult> wrappedAuthResultListener = ActionListener.wrap(auth -> {
|
||||
if (auth.isAuthenticated()) {
|
||||
// Add the ID Token as metadata on the authentication, so that it can be used for logout requests
|
||||
Map<String, Object> metadata = new HashMap<>(auth.getMetadata());
|
||||
metadata.put(CONTEXT_TOKEN_DATA, tokenMetadata);
|
||||
auth = AuthenticationResult.success(auth.getUser(), metadata);
|
||||
}
|
||||
authResultListener.onResponse(auth);
|
||||
}, authResultListener::onFailure);
|
||||
|
||||
if (delegatedRealms.hasDelegation()) {
|
||||
delegatedRealms.resolve(principal, wrappedAuthResultListener);
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, Object> userMetadata = new HashMap<>();
|
||||
if (populateUserMetadata) {
|
||||
Map<String, Object> claimsMap = claims.getClaims();
|
||||
/*
|
||||
* We whitelist the Types that we want to parse as metadata from the Claims, explicitly filtering out {@link Date}s
|
||||
*/
|
||||
Set<Map.Entry> allowedEntries = claimsMap.entrySet().stream().filter(entry -> {
|
||||
Object v = entry.getValue();
|
||||
return (v instanceof String || v instanceof Boolean || v instanceof Number || v instanceof Collections);
|
||||
}).collect(Collectors.toSet());
|
||||
for (Map.Entry entry : allowedEntries) {
|
||||
userMetadata.put("oidc(" + entry.getKey() + ")", entry.getValue());
|
||||
}
|
||||
}
|
||||
final List<String> groups = groupsAttribute.getClaimValues(claims);
|
||||
final String dn = dnAttribute.getClaimValue(claims);
|
||||
final String mail = mailAttribute.getClaimValue(claims);
|
||||
final String name = nameAttribute.getClaimValue(claims);
|
||||
UserRoleMapper.UserData userData = new UserRoleMapper.UserData(principal, dn, groups, userMetadata, config);
|
||||
roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
|
||||
final User user = new User(principal, roles.toArray(Strings.EMPTY_ARRAY), name, mail, userMetadata, true);
|
||||
wrappedAuthResultListener.onResponse(AuthenticationResult.success(user));
|
||||
}, wrappedAuthResultListener::onFailure));
|
||||
|
||||
}
|
||||
|
||||
private RelyingPartyConfiguration buildRelyingPartyConfiguration(RealmConfig config) {
|
||||
final String redirectUriString = require(config, RP_REDIRECT_URI);
|
||||
final URI redirectUri;
|
||||
try {
|
||||
redirectUri = new URI(redirectUriString);
|
||||
} catch (URISyntaxException e) {
|
||||
// This should never happen as it's already validated in the settings
|
||||
throw new SettingsException("Invalid URI:" + RP_REDIRECT_URI.getKey(), e);
|
||||
}
|
||||
final String postLogoutRedirectUriString = config.getSetting(RP_POST_LOGOUT_REDIRECT_URI);
|
||||
final URI postLogoutRedirectUri;
|
||||
try {
|
||||
postLogoutRedirectUri = new URI(postLogoutRedirectUriString);
|
||||
} catch (URISyntaxException e) {
|
||||
// This should never happen as it's already validated in the settings
|
||||
throw new SettingsException("Invalid URI:" + RP_POST_LOGOUT_REDIRECT_URI.getKey(), e);
|
||||
}
|
||||
final ClientID clientId = new ClientID(require(config, RP_CLIENT_ID));
|
||||
final SecureString clientSecret = config.getSetting(RP_CLIENT_SECRET);
|
||||
final ResponseType responseType;
|
||||
try {
|
||||
// This should never happen as it's already validated in the settings
|
||||
responseType = ResponseType.parse(require(config, RP_RESPONSE_TYPE));
|
||||
} catch (ParseException e) {
|
||||
throw new SettingsException("Invalid value for " + RP_RESPONSE_TYPE.getKey(), e);
|
||||
}
|
||||
|
||||
final Scope requestedScope = new Scope(config.getSetting(RP_REQUESTED_SCOPES).toArray(Strings.EMPTY_ARRAY));
|
||||
if (requestedScope.contains("openid") == false) {
|
||||
requestedScope.add("openid");
|
||||
}
|
||||
final JWSAlgorithm signatureAlgorithm = JWSAlgorithm.parse(require(config, RP_SIGNATURE_ALGORITHM));
|
||||
|
||||
return new RelyingPartyConfiguration(clientId, clientSecret, redirectUri, responseType, requestedScope,
|
||||
signatureAlgorithm, postLogoutRedirectUri);
|
||||
}
|
||||
|
||||
private OpenIdConnectProviderConfiguration buildOpenIdConnectProviderConfiguration(RealmConfig config) {
|
||||
String providerName = require(config, OP_NAME);
|
||||
Issuer issuer = new Issuer(require(config, OP_ISSUER));
|
||||
|
||||
String jwkSetUrl = require(config, OP_JWKSET_PATH);
|
||||
|
||||
URI authorizationEndpoint;
|
||||
try {
|
||||
authorizationEndpoint = new URI(require(config, OP_AUTHORIZATION_ENDPOINT));
|
||||
} catch (URISyntaxException e) {
|
||||
// This should never happen as it's already validated in the settings
|
||||
throw new SettingsException("Invalid URI: " + OP_AUTHORIZATION_ENDPOINT.getKey(), e);
|
||||
}
|
||||
URI tokenEndpoint;
|
||||
try {
|
||||
tokenEndpoint = new URI(require(config, OP_TOKEN_ENDPOINT));
|
||||
} catch (URISyntaxException e) {
|
||||
// This should never happen as it's already validated in the settings
|
||||
throw new SettingsException("Invalid URL: " + OP_TOKEN_ENDPOINT.getKey(), e);
|
||||
}
|
||||
URI userinfoEndpoint;
|
||||
try {
|
||||
userinfoEndpoint = (config.getSetting(OP_USERINFO_ENDPOINT, () -> null) == null) ? null :
|
||||
new URI(config.getSetting(OP_USERINFO_ENDPOINT, () -> null));
|
||||
} catch (URISyntaxException e) {
|
||||
// This should never happen as it's already validated in the settings
|
||||
throw new SettingsException("Invalid URI: " + OP_USERINFO_ENDPOINT.getKey(), e);
|
||||
}
|
||||
URI endsessionEndpoint;
|
||||
try {
|
||||
endsessionEndpoint = (config.getSetting(OP_ENDSESSION_ENDPOINT, () -> null) == null) ? null :
|
||||
new URI(config.getSetting(OP_ENDSESSION_ENDPOINT, () -> null));
|
||||
} catch (URISyntaxException e) {
|
||||
// This should never happen as it's already validated in the settings
|
||||
throw new SettingsException("Invalid URI: " + OP_ENDSESSION_ENDPOINT.getKey(), e);
|
||||
}
|
||||
|
||||
return new OpenIdConnectProviderConfiguration(providerName, issuer, jwkSetUrl, authorizationEndpoint, tokenEndpoint,
|
||||
userinfoEndpoint, endsessionEndpoint);
|
||||
}
|
||||
|
||||
private static String require(RealmConfig config, Setting.AffixSetting<String> setting) {
|
||||
final String value = config.getSetting(setting);
|
||||
if (value.isEmpty()) {
|
||||
throw new SettingsException("The configuration setting [" + RealmSettings.getFullSettingKey(config, setting)
|
||||
+ "] is required");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the URI for an OIDC Authentication Request from the realm configuration using URI Query String Serialization and
|
||||
* possibly generates a state parameter and a nonce. It then returns the URI, state and nonce encapsulated in a
|
||||
* {@link OpenIdConnectPrepareAuthenticationResponse}. A facilitator can provide a state and a nonce parameter in two cases:
|
||||
* <ul>
|
||||
* <li>In case of Kibana, it allows for a better UX by ensuring that all requests to an OpenID Connect Provider within
|
||||
* the same browser context (even across tabs) will use the same state and nonce values.</li>
|
||||
* <li>In case of custom facilitators, the implementer might require/support generating the state parameter in order
|
||||
* to tie this to an anti-XSRF token.</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* @param existingState An existing state that can be reused or null if we need to generate one
|
||||
* @param existingNonce An existing nonce that can be reused or null if we need to generate one
|
||||
* @param loginHint A String with a login hint to add to the authentication request in case of a 3rd party initiated login
|
||||
*
|
||||
* @return an {@link OpenIdConnectPrepareAuthenticationResponse}
|
||||
*/
|
||||
public OpenIdConnectPrepareAuthenticationResponse buildAuthenticationRequestUri(@Nullable String existingState,
|
||||
@Nullable String existingNonce,
|
||||
@Nullable String loginHint) {
|
||||
final State state = existingState != null ? new State(existingState) : new State();
|
||||
final Nonce nonce = existingNonce != null ? new Nonce(existingNonce) : new Nonce();
|
||||
final AuthenticationRequest.Builder builder = new AuthenticationRequest.Builder(rpConfiguration.getResponseType(),
|
||||
rpConfiguration.getRequestedScope(),
|
||||
rpConfiguration.getClientId(),
|
||||
rpConfiguration.getRedirectUri())
|
||||
.endpointURI(opConfiguration.getAuthorizationEndpoint())
|
||||
.state(state)
|
||||
.nonce(nonce);
|
||||
if (Strings.hasText(loginHint)) {
|
||||
builder.loginHint(loginHint);
|
||||
}
|
||||
return new OpenIdConnectPrepareAuthenticationResponse(builder.build().toURI().toString(),
|
||||
state.getValue(), nonce.getValue());
|
||||
}
|
||||
|
||||
public boolean isIssuerValid(String issuer) {
|
||||
return this.opConfiguration.getIssuer().getValue().equals(issuer);
|
||||
}
|
||||
|
||||
public OpenIdConnectLogoutResponse buildLogoutResponse(JWT idTokenHint) {
|
||||
if (opConfiguration.getEndsessionEndpoint() != null) {
|
||||
final State state = new State();
|
||||
final LogoutRequest logoutRequest = new LogoutRequest(opConfiguration.getEndsessionEndpoint(), idTokenHint,
|
||||
rpConfiguration.getPostLogoutRedirectUri(), state);
|
||||
return new OpenIdConnectLogoutResponse(logoutRequest.toURI().toString());
|
||||
} else {
|
||||
return new OpenIdConnectLogoutResponse((String) null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
openIdConnectAuthenticator.close();
|
||||
}
|
||||
|
||||
static final class ClaimParser {
|
||||
private final String name;
|
||||
private final Function<JWTClaimsSet, List<String>> parser;
|
||||
|
||||
ClaimParser(String name, Function<JWTClaimsSet, List<String>> parser) {
|
||||
this.name = name;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
List<String> getClaimValues(JWTClaimsSet claims) {
|
||||
return parser.apply(claims);
|
||||
}
|
||||
|
||||
String getClaimValue(JWTClaimsSet claims) {
|
||||
List<String> claimValues = parser.apply(claims);
|
||||
if (claimValues == null || claimValues.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return claimValues.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
static ClaimParser forSetting(Logger logger, OpenIdConnectRealmSettings.ClaimSetting setting, RealmConfig realmConfig,
|
||||
boolean required) {
|
||||
|
||||
if (realmConfig.hasSetting(setting.getClaim())) {
|
||||
String claimName = realmConfig.getSetting(setting.getClaim());
|
||||
if (realmConfig.hasSetting(setting.getPattern())) {
|
||||
Pattern regex = Pattern.compile(realmConfig.getSetting(setting.getPattern()));
|
||||
return new ClaimParser(
|
||||
"OpenID Connect Claim [" + claimName + "] with pattern [" + regex.pattern() + "] for ["
|
||||
+ setting.name(realmConfig) + "]",
|
||||
claims -> {
|
||||
Object claimValueObject = claims.getClaim(claimName);
|
||||
List<String> values;
|
||||
if (claimValueObject == null) {
|
||||
values = Collections.emptyList();
|
||||
} else if (claimValueObject instanceof String) {
|
||||
values = Collections.singletonList((String) claimValueObject);
|
||||
} else if (claimValueObject instanceof List) {
|
||||
values = (List<String>) claimValueObject;
|
||||
} else {
|
||||
throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim())
|
||||
+ " expects a claim with String or a String Array value but found a "
|
||||
+ claimValueObject.getClass().getName());
|
||||
}
|
||||
return values.stream().map(s -> {
|
||||
final Matcher matcher = regex.matcher(s);
|
||||
if (matcher.find() == false) {
|
||||
logger.debug("OpenID Connect Claim [{}] is [{}], which does not match [{}]",
|
||||
claimName, s, regex.pattern());
|
||||
return null;
|
||||
}
|
||||
final String value = matcher.group(1);
|
||||
if (Strings.isNullOrEmpty(value)) {
|
||||
logger.debug("OpenID Connect Claim [{}] is [{}], which does match [{}] but group(1) is empty",
|
||||
claimName, s, regex.pattern());
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
});
|
||||
} else {
|
||||
return new ClaimParser(
|
||||
"OpenID Connect Claim [" + claimName + "] for [" + setting.name(realmConfig) + "]",
|
||||
claims -> {
|
||||
Object claimValueObject = claims.getClaim(claimName);
|
||||
if (claimValueObject == null) {
|
||||
return Collections.emptyList();
|
||||
} else if (claimValueObject instanceof String) {
|
||||
return Collections.singletonList((String) claimValueObject);
|
||||
} else if (claimValueObject instanceof List == false) {
|
||||
throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim())
|
||||
+ " expects a claim with String or a String Array value but found a "
|
||||
+ claimValueObject.getClass().getName());
|
||||
}
|
||||
return (List<String>) claimValueObject;
|
||||
});
|
||||
}
|
||||
} else if (required) {
|
||||
throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim())
|
||||
+ "] is required");
|
||||
} else if (realmConfig.hasSetting(setting.getPattern())) {
|
||||
throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getPattern())
|
||||
+ "] cannot be set unless [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim())
|
||||
+ "] is also set");
|
||||
} else {
|
||||
return new ClaimParser("No OpenID Connect Claim for [" + setting.name(realmConfig) + "]",
|
||||
attributes -> Collections.emptyList());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import com.nimbusds.openid.connect.sdk.Nonce;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
|
||||
|
||||
/**
|
||||
* A {@link AuthenticationToken} to hold OpenID Connect related content.
|
||||
* Depending on the flow the token can contain only a code ( oAuth2 authorization code
|
||||
* grant flow ) or even an Identity Token ( oAuth2 implicit flow )
|
||||
*/
|
||||
public class OpenIdConnectToken implements AuthenticationToken {
|
||||
|
||||
private String redirectUrl;
|
||||
private State state;
|
||||
private Nonce nonce;
|
||||
|
||||
/**
|
||||
* @param redirectUrl The URI where the OP redirected the browser after the authentication event at the OP. This is passed as is from
|
||||
* the facilitator entity (i.e. Kibana), so it is URL Encoded. It contains either the code or the id_token itself
|
||||
* depending on the flow used
|
||||
* @param state The state value that we generated or the facilitator provided for this specific flow and should be stored at the
|
||||
* user's session with the facilitator.
|
||||
* @param nonce The nonce value that we generated or the facilitator provided for this specific flow and should be stored at the
|
||||
* user's session with the facilitator.
|
||||
*/
|
||||
public OpenIdConnectToken(String redirectUrl, State state, Nonce nonce) {
|
||||
this.redirectUrl = redirectUrl;
|
||||
this.state = state;
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String principal() {
|
||||
return "<OIDC Token>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object credentials() {
|
||||
return redirectUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCredentials() {
|
||||
this.redirectUrl = null;
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public Nonce getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public String getRedirectUrl() {
|
||||
return redirectUrl;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "{ redirectUrl=" + redirectUrl + ", state=" + state + ", nonce=" + nonce + "}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
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.id.ClientID;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A Class that contains all the OpenID Connect Relying Party configuration
|
||||
*/
|
||||
public class RelyingPartyConfiguration {
|
||||
private final ClientID clientId;
|
||||
private final SecureString clientSecret;
|
||||
private final URI redirectUri;
|
||||
private final ResponseType responseType;
|
||||
private final Scope requestedScope;
|
||||
private final JWSAlgorithm signatureAlgorithm;
|
||||
private final URI postLogoutRedirectUri;
|
||||
|
||||
public RelyingPartyConfiguration(ClientID clientId, SecureString clientSecret, URI redirectUri, ResponseType responseType,
|
||||
Scope requestedScope, JWSAlgorithm algorithm, @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.postLogoutRedirectUri = postLogoutRedirectUri;
|
||||
}
|
||||
|
||||
public ClientID getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public SecureString getClientSecret() {
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
public URI getRedirectUri() {
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
public ResponseType getResponseType() {
|
||||
return responseType;
|
||||
}
|
||||
|
||||
public Scope getRequestedScope() {
|
||||
return requestedScope;
|
||||
}
|
||||
|
||||
public JWSAlgorithm getSignatureAlgorithm() {
|
||||
return signatureAlgorithm;
|
||||
}
|
||||
|
||||
public URI getPostLogoutRedirectUri() {
|
||||
return postLogoutRedirectUri;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.rest.action.oidc;
|
||||
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.license.LicenseUtils;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
|
||||
import org.elasticsearch.xpack.security.authc.Realms;
|
||||
import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
|
||||
|
||||
public abstract class OpenIdConnectBaseRestHandler extends SecurityBaseRestHandler {
|
||||
|
||||
private static final String OIDC_REALM_TYPE = OpenIdConnectRealmSettings.TYPE;
|
||||
|
||||
/**
|
||||
* @param settings the node's settings
|
||||
* @param licenseState the license state that will be used to determine if security is licensed
|
||||
*/
|
||||
protected OpenIdConnectBaseRestHandler(Settings settings, XPackLicenseState licenseState) {
|
||||
super(settings, licenseState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Exception checkFeatureAvailable(RestRequest request) {
|
||||
Exception failedFeature = super.checkFeatureAvailable(request);
|
||||
if (failedFeature != null) {
|
||||
return failedFeature;
|
||||
} else if (Realms.isRealmTypeAvailable(licenseState.allowedRealmType(), OIDC_REALM_TYPE)) {
|
||||
return null;
|
||||
} else {
|
||||
logger.info("The '{}' realm is not available under the current license", OIDC_REALM_TYPE);
|
||||
return LicenseUtils.newComplianceException(OIDC_REALM_TYPE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.rest.action.oidc;
|
||||
|
||||
import org.elasticsearch.client.node.NodeClient;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.xcontent.ObjectParser;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
import org.elasticsearch.rest.BytesRestResponse;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.RestResponse;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.rest.action.RestBuilderListener;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.POST;
|
||||
|
||||
/**
|
||||
* Rest handler that authenticates the user based on the information provided as parameters of the redirect_uri
|
||||
*/
|
||||
public class RestOpenIdConnectAuthenticateAction extends OpenIdConnectBaseRestHandler {
|
||||
|
||||
static final ObjectParser<OpenIdConnectAuthenticateRequest, Void> PARSER = new ObjectParser<>("oidc_authn",
|
||||
OpenIdConnectAuthenticateRequest::new);
|
||||
|
||||
static {
|
||||
PARSER.declareString(OpenIdConnectAuthenticateRequest::setRedirectUri, new ParseField("redirect_uri"));
|
||||
PARSER.declareString(OpenIdConnectAuthenticateRequest::setState, new ParseField("state"));
|
||||
PARSER.declareString(OpenIdConnectAuthenticateRequest::setNonce, new ParseField("nonce"));
|
||||
}
|
||||
|
||||
public RestOpenIdConnectAuthenticateAction(Settings settings, RestController controller, XPackLicenseState licenseState) {
|
||||
super(settings, licenseState);
|
||||
controller.registerHandler(POST, "/_security/oidc/authenticate", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
try (XContentParser parser = request.contentParser()) {
|
||||
final OpenIdConnectAuthenticateRequest authenticateRequest = PARSER.parse(parser, null);
|
||||
logger.trace("OIDC Authenticate: " + authenticateRequest);
|
||||
return channel -> client.execute(OpenIdConnectAuthenticateAction.INSTANCE, authenticateRequest,
|
||||
new RestBuilderListener<OpenIdConnectAuthenticateResponse>(channel) {
|
||||
@Override
|
||||
public RestResponse buildResponse(OpenIdConnectAuthenticateResponse response, XContentBuilder builder)
|
||||
throws Exception {
|
||||
builder.startObject()
|
||||
.field("username", response.getPrincipal())
|
||||
.field("access_token", response.getAccessTokenString())
|
||||
.field("refresh_token", response.getRefreshTokenString())
|
||||
.field("expires_in", response.getExpiresIn().seconds())
|
||||
.endObject();
|
||||
return new BytesRestResponse(RestStatus.OK, builder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "security_oidc_authenticate_action";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.rest.action.oidc;
|
||||
|
||||
import org.elasticsearch.client.node.NodeClient;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.xcontent.ObjectParser;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
import org.elasticsearch.rest.BytesRestResponse;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.RestResponse;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.rest.action.RestBuilderListener;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.POST;
|
||||
|
||||
/**
|
||||
* Rest handler that invalidates a security token for the given OpenID Connect realm and if the configuration of
|
||||
* the realm supports it, generates a redirect to the `end_session_endpoint` of the OpenID Connect Provider.
|
||||
*/
|
||||
public class RestOpenIdConnectLogoutAction extends OpenIdConnectBaseRestHandler {
|
||||
|
||||
static final ObjectParser<OpenIdConnectLogoutRequest, Void> PARSER = new ObjectParser<>("oidc_logout",
|
||||
OpenIdConnectLogoutRequest::new);
|
||||
|
||||
static {
|
||||
PARSER.declareString(OpenIdConnectLogoutRequest::setToken, new ParseField("token"));
|
||||
PARSER.declareString(OpenIdConnectLogoutRequest::setRefreshToken, new ParseField("refresh_token"));
|
||||
}
|
||||
|
||||
public RestOpenIdConnectLogoutAction(Settings settings, RestController controller, XPackLicenseState licenseState) {
|
||||
super(settings, licenseState);
|
||||
controller.registerHandler(POST, "/_security/oidc/logout", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
try (XContentParser parser = request.contentParser()) {
|
||||
final OpenIdConnectLogoutRequest logoutRequest = PARSER.parse(parser, null);
|
||||
return channel -> client.execute(OpenIdConnectLogoutAction.INSTANCE, logoutRequest,
|
||||
new RestBuilderListener<OpenIdConnectLogoutResponse>(channel) {
|
||||
@Override
|
||||
public RestResponse buildResponse(OpenIdConnectLogoutResponse response, XContentBuilder builder) throws Exception {
|
||||
builder.startObject();
|
||||
builder.field("redirect", response.getEndSessionUrl());
|
||||
builder.endObject();
|
||||
return new BytesRestResponse(RestStatus.OK, builder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "security_oidc_logout_action";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.rest.action.oidc;
|
||||
|
||||
import org.elasticsearch.client.node.NodeClient;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.xcontent.ObjectParser;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
import org.elasticsearch.rest.BytesRestResponse;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.RestResponse;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.rest.action.RestBuilderListener;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.POST;
|
||||
|
||||
/**
|
||||
* Generates an oAuth 2.0 authentication request as a URL string and returns it to the REST client.
|
||||
*/
|
||||
public class RestOpenIdConnectPrepareAuthenticationAction extends OpenIdConnectBaseRestHandler {
|
||||
|
||||
static final ObjectParser<OpenIdConnectPrepareAuthenticationRequest, Void> PARSER = new ObjectParser<>("oidc_prepare_authentication",
|
||||
OpenIdConnectPrepareAuthenticationRequest::new);
|
||||
|
||||
static {
|
||||
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setRealmName, new ParseField("realm"));
|
||||
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setIssuer, new ParseField("iss"));
|
||||
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setLoginHint, new ParseField("login_hint"));
|
||||
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setState, new ParseField("state"));
|
||||
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setNonce, new ParseField("nonce"));
|
||||
}
|
||||
|
||||
public RestOpenIdConnectPrepareAuthenticationAction(Settings settings, RestController controller, XPackLicenseState licenseState) {
|
||||
super(settings, licenseState);
|
||||
controller.registerHandler(POST, "/_security/oidc/prepare", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
try (XContentParser parser = request.contentParser()) {
|
||||
final OpenIdConnectPrepareAuthenticationRequest prepareAuthenticationRequest = PARSER.parse(parser, null);
|
||||
logger.trace("OIDC Prepare Authentication: " + prepareAuthenticationRequest);
|
||||
return channel -> client.execute(OpenIdConnectPrepareAuthenticationAction.INSTANCE, prepareAuthenticationRequest,
|
||||
new RestBuilderListener<OpenIdConnectPrepareAuthenticationResponse>(channel) {
|
||||
@Override
|
||||
public RestResponse buildResponse(OpenIdConnectPrepareAuthenticationResponse response, XContentBuilder builder)
|
||||
throws Exception {
|
||||
logger.trace("OIDC Prepare Authentication Response: " + response);
|
||||
return new BytesRestResponse(RestStatus.OK, response.toXContent(builder, request));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "security_oidc_prepare_authentication_action";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.action.oidc;
|
||||
|
||||
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class OpenIdConnectAuthenticateRequestTests extends ESTestCase {
|
||||
|
||||
public void testSerialization() throws IOException {
|
||||
final OpenIdConnectAuthenticateRequest request = new OpenIdConnectAuthenticateRequest();
|
||||
final String nonce = randomAlphaOfLengthBetween(8, 12);
|
||||
final String state = randomAlphaOfLengthBetween(8, 12);
|
||||
final String redirectUri = "https://rp.com/cb?code=thisisacode&state=" + state;
|
||||
request.setRedirectUri(redirectUri);
|
||||
request.setState(state);
|
||||
request.setNonce(nonce);
|
||||
final BytesStreamOutput out = new BytesStreamOutput();
|
||||
request.writeTo(out);
|
||||
|
||||
final OpenIdConnectAuthenticateRequest unserialized = new OpenIdConnectAuthenticateRequest(out.bytes().streamInput());
|
||||
assertThat(unserialized.getRedirectUri(), equalTo(redirectUri));
|
||||
assertThat(unserialized.getState(), equalTo(state));
|
||||
assertThat(unserialized.getNonce(), equalTo(nonce));
|
||||
}
|
||||
|
||||
public void testValidation() {
|
||||
final OpenIdConnectAuthenticateRequest request = new OpenIdConnectAuthenticateRequest();
|
||||
final ActionRequestValidationException validation = request.validate();
|
||||
assertNotNull(validation);
|
||||
assertThat(validation.validationErrors().size(), equalTo(3));
|
||||
assertThat(validation.validationErrors().get(0), containsString("state parameter is missing"));
|
||||
assertThat(validation.validationErrors().get(1), containsString("nonce parameter is missing"));
|
||||
assertThat(validation.validationErrors().get(2), containsString("redirect_uri parameter is missing"));
|
||||
|
||||
final OpenIdConnectAuthenticateRequest request2 = new OpenIdConnectAuthenticateRequest();
|
||||
request2.setRedirectUri("https://rp.company.com/cb?code=abc");
|
||||
request2.setState(randomAlphaOfLengthBetween(8, 12));
|
||||
final ActionRequestValidationException validation2 = request2.validate();
|
||||
assertNotNull(validation2);
|
||||
assertThat(validation2.validationErrors().size(), equalTo(1));
|
||||
assertThat(validation2.validationErrors().get(0), containsString("nonce parameter is missing"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.action.oidc;
|
||||
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class OpenIdConnectPrepareAuthenticationRequestTests extends ESTestCase {
|
||||
|
||||
public void testSerialization() throws IOException {
|
||||
final OpenIdConnectPrepareAuthenticationRequest request = new OpenIdConnectPrepareAuthenticationRequest();
|
||||
request.setRealmName("oidc-realm1");
|
||||
final BytesStreamOutput out = new BytesStreamOutput();
|
||||
request.writeTo(out);
|
||||
|
||||
final OpenIdConnectPrepareAuthenticationRequest deserialized =
|
||||
new OpenIdConnectPrepareAuthenticationRequest(out.bytes().streamInput());
|
||||
assertThat(deserialized.getRealmName(), equalTo("oidc-realm1"));
|
||||
|
||||
final OpenIdConnectPrepareAuthenticationRequest request2 = new OpenIdConnectPrepareAuthenticationRequest();
|
||||
request2.setIssuer("https://op.company.org/");
|
||||
final BytesStreamOutput out2 = new BytesStreamOutput();
|
||||
request2.writeTo(out2);
|
||||
|
||||
final OpenIdConnectPrepareAuthenticationRequest deserialized2 =
|
||||
new OpenIdConnectPrepareAuthenticationRequest(out2.bytes().streamInput());
|
||||
assertThat(deserialized2.getIssuer(), equalTo("https://op.company.org/"));
|
||||
}
|
||||
|
||||
public void testSerializationWithStateAndNonce() throws IOException {
|
||||
final OpenIdConnectPrepareAuthenticationRequest request = new OpenIdConnectPrepareAuthenticationRequest();
|
||||
final String nonce = randomAlphaOfLengthBetween(8, 12);
|
||||
final String state = randomAlphaOfLengthBetween(8, 12);
|
||||
request.setRealmName("oidc-realm1");
|
||||
request.setNonce(nonce);
|
||||
request.setState(state);
|
||||
final BytesStreamOutput out = new BytesStreamOutput();
|
||||
request.writeTo(out);
|
||||
|
||||
final OpenIdConnectPrepareAuthenticationRequest deserialized =
|
||||
new OpenIdConnectPrepareAuthenticationRequest(out.bytes().streamInput());
|
||||
assertThat(deserialized.getRealmName(), equalTo("oidc-realm1"));
|
||||
assertThat(deserialized.getState(), equalTo(state));
|
||||
assertThat(deserialized.getNonce(), equalTo(nonce));
|
||||
}
|
||||
|
||||
public void testValidation() {
|
||||
final OpenIdConnectPrepareAuthenticationRequest request = new OpenIdConnectPrepareAuthenticationRequest();
|
||||
final ActionRequestValidationException validation = request.validate();
|
||||
assertNotNull(validation);
|
||||
assertThat(validation.validationErrors().size(), equalTo(1));
|
||||
assertThat(validation.validationErrors().get(0), containsString("one of [realm, issuer] must be provided"));
|
||||
|
||||
final OpenIdConnectPrepareAuthenticationRequest request2 = new OpenIdConnectPrepareAuthenticationRequest();
|
||||
request2.setRealmName("oidc-realm1");
|
||||
request2.setIssuer("https://op.company.org/");
|
||||
final ActionRequestValidationException validation2 = request2.validate();
|
||||
assertNotNull(validation2);
|
||||
assertThat(validation2.validationErrors().size(), equalTo(1));
|
||||
assertThat(validation2.validationErrors().get(0),
|
||||
containsString("only one of [realm, issuer] can be provided in the same request"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.action.oidc;
|
||||
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.bulk.BulkAction;
|
||||
import org.elasticsearch.action.bulk.BulkItemResponse;
|
||||
import org.elasticsearch.action.bulk.BulkRequest;
|
||||
import org.elasticsearch.action.bulk.BulkRequestBuilder;
|
||||
import org.elasticsearch.action.bulk.BulkResponse;
|
||||
import org.elasticsearch.action.get.GetAction;
|
||||
import org.elasticsearch.action.get.GetRequestBuilder;
|
||||
import org.elasticsearch.action.index.IndexAction;
|
||||
import org.elasticsearch.action.index.IndexRequest;
|
||||
import org.elasticsearch.action.index.IndexRequestBuilder;
|
||||
import org.elasticsearch.action.index.IndexResponse;
|
||||
import org.elasticsearch.action.support.ActionFilters;
|
||||
import org.elasticsearch.action.support.PlainActionFuture;
|
||||
import org.elasticsearch.action.update.UpdateAction;
|
||||
import org.elasticsearch.action.update.UpdateRequest;
|
||||
import org.elasticsearch.action.update.UpdateRequestBuilder;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.cluster.service.ClusterService;
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.env.TestEnvironment;
|
||||
import org.elasticsearch.tasks.Task;
|
||||
import org.elasticsearch.test.ClusterServiceUtils;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.transport.Transport;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.elasticsearch.xpack.core.XPackSettings;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.user.User;
|
||||
import org.elasticsearch.xpack.core.ssl.SSLService;
|
||||
import org.elasticsearch.xpack.security.authc.Realms;
|
||||
import org.elasticsearch.xpack.security.authc.TokenService;
|
||||
import org.elasticsearch.xpack.security.authc.UserToken;
|
||||
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm;
|
||||
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectTestCase;
|
||||
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
|
||||
import org.elasticsearch.xpack.security.support.SecurityIndexManager;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.elasticsearch.xpack.security.authc.TokenServiceTests.mockGetTokenFromId;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TransportOpenIdConnectLogoutActionTests extends OpenIdConnectTestCase {
|
||||
|
||||
private OpenIdConnectRealm oidcRealm;
|
||||
private TokenService tokenService;
|
||||
private List<IndexRequest> indexRequests;
|
||||
private List<BulkRequest> bulkRequests;
|
||||
private Client client;
|
||||
private TransportOpenIdConnectLogoutAction action;
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
final Settings settings = getBasicRealmSettings()
|
||||
.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true)
|
||||
.put("path.home", createTempDir())
|
||||
.build();
|
||||
final Settings sslSettings = Settings.builder()
|
||||
.put("xpack.security.authc.realms.oidc.oidc-realm.ssl.verification_mode", "certificate")
|
||||
.put("path.home", createTempDir())
|
||||
.build();
|
||||
final ThreadContext threadContext = new ThreadContext(settings);
|
||||
final ThreadPool threadPool = mock(ThreadPool.class);
|
||||
when(threadPool.getThreadContext()).thenReturn(threadContext);
|
||||
new Authentication(new User("kibana"), new Authentication.RealmRef("realm", "type", "node"), null).writeToContext(threadContext);
|
||||
indexRequests = new ArrayList<>();
|
||||
bulkRequests = new ArrayList<>();
|
||||
client = mock(Client.class);
|
||||
when(client.threadPool()).thenReturn(threadPool);
|
||||
when(client.settings()).thenReturn(settings);
|
||||
doAnswer(invocationOnMock -> {
|
||||
GetRequestBuilder builder = new GetRequestBuilder(client, GetAction.INSTANCE);
|
||||
builder.setIndex((String) invocationOnMock.getArguments()[0])
|
||||
.setType((String) invocationOnMock.getArguments()[1])
|
||||
.setId((String) invocationOnMock.getArguments()[2]);
|
||||
return builder;
|
||||
}).when(client).prepareGet(anyString(), anyString(), anyString());
|
||||
doAnswer(invocationOnMock -> {
|
||||
IndexRequestBuilder builder = new IndexRequestBuilder(client, IndexAction.INSTANCE);
|
||||
builder.setIndex((String) invocationOnMock.getArguments()[0])
|
||||
.setType((String) invocationOnMock.getArguments()[1])
|
||||
.setId((String) invocationOnMock.getArguments()[2]);
|
||||
return builder;
|
||||
}).when(client).prepareIndex(anyString(), anyString(), anyString());
|
||||
doAnswer(invocationOnMock -> {
|
||||
UpdateRequestBuilder builder = new UpdateRequestBuilder(client, UpdateAction.INSTANCE);
|
||||
builder.setIndex((String) invocationOnMock.getArguments()[0])
|
||||
.setType((String) invocationOnMock.getArguments()[1])
|
||||
.setId((String) invocationOnMock.getArguments()[2]);
|
||||
return builder;
|
||||
}).when(client).prepareUpdate(anyString(), anyString(), anyString());
|
||||
doAnswer(invocationOnMock -> {
|
||||
BulkRequestBuilder builder = new BulkRequestBuilder(client, BulkAction.INSTANCE);
|
||||
return builder;
|
||||
}).when(client).prepareBulk();
|
||||
doAnswer(invocationOnMock -> {
|
||||
IndexRequest indexRequest = (IndexRequest) invocationOnMock.getArguments()[0];
|
||||
ActionListener<IndexResponse> listener = (ActionListener<IndexResponse>) invocationOnMock.getArguments()[1];
|
||||
indexRequests.add(indexRequest);
|
||||
final IndexResponse response = new IndexResponse(
|
||||
indexRequest.shardId(), indexRequest.type(), indexRequest.id(), 1, 1, 1, true);
|
||||
listener.onResponse(response);
|
||||
return Void.TYPE;
|
||||
}).when(client).index(any(IndexRequest.class), any(ActionListener.class));
|
||||
doAnswer(invocationOnMock -> {
|
||||
IndexRequest indexRequest = (IndexRequest) invocationOnMock.getArguments()[1];
|
||||
ActionListener<IndexResponse> listener = (ActionListener<IndexResponse>) invocationOnMock.getArguments()[2];
|
||||
indexRequests.add(indexRequest);
|
||||
final IndexResponse response = new IndexResponse(
|
||||
indexRequest.shardId(), indexRequest.type(), indexRequest.id(), 1, 1, 1, true);
|
||||
listener.onResponse(response);
|
||||
return Void.TYPE;
|
||||
}).when(client).execute(eq(IndexAction.INSTANCE), any(IndexRequest.class), any(ActionListener.class));
|
||||
doAnswer(invocationOnMock -> {
|
||||
BulkRequest bulkRequest = (BulkRequest) invocationOnMock.getArguments()[0];
|
||||
ActionListener<BulkResponse> listener = (ActionListener<BulkResponse>) invocationOnMock.getArguments()[1];
|
||||
bulkRequests.add(bulkRequest);
|
||||
final BulkResponse response = new BulkResponse(new BulkItemResponse[0], 1);
|
||||
listener.onResponse(response);
|
||||
return Void.TYPE;
|
||||
}).when(client).bulk(any(BulkRequest.class), any(ActionListener.class));
|
||||
|
||||
final SecurityIndexManager securityIndex = mock(SecurityIndexManager.class);
|
||||
doAnswer(inv -> {
|
||||
((Runnable) inv.getArguments()[1]).run();
|
||||
return null;
|
||||
}).when(securityIndex).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class));
|
||||
doAnswer(inv -> {
|
||||
((Runnable) inv.getArguments()[1]).run();
|
||||
return null;
|
||||
}).when(securityIndex).checkIndexVersionThenExecute(any(Consumer.class), any(Runnable.class));
|
||||
when(securityIndex.isAvailable()).thenReturn(true);
|
||||
|
||||
final ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool);
|
||||
tokenService = new TokenService(settings, Clock.systemUTC(), client, securityIndex, clusterService);
|
||||
|
||||
final TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null,
|
||||
TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet());
|
||||
final Realms realms = mock(Realms.class);
|
||||
action = new TransportOpenIdConnectLogoutAction(transportService, mock(ActionFilters.class), realms, tokenService);
|
||||
|
||||
final Environment env = TestEnvironment.newEnvironment(settings);
|
||||
|
||||
final RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier("oidc", REALM_NAME);
|
||||
|
||||
final RealmConfig realmConfig = new RealmConfig(realmIdentifier, settings, env, threadContext);
|
||||
oidcRealm = new OpenIdConnectRealm(realmConfig, new SSLService(sslSettings, env), mock(UserRoleMapper.class),
|
||||
mock(ResourceWatcherService.class));
|
||||
when(realms.realm(realmConfig.name())).thenReturn(oidcRealm);
|
||||
}
|
||||
|
||||
public void testLogoutInvalidatesTokens() throws Exception {
|
||||
final String subject = randomAlphaOfLength(8);
|
||||
final JWT signedIdToken = generateIdToken(subject, randomAlphaOfLength(8), randomAlphaOfLength(8));
|
||||
final User user = new User("oidc-user", new String[]{"superuser"}, null, null, null, true);
|
||||
final Authentication.RealmRef realmRef = new Authentication.RealmRef(oidcRealm.name(), OpenIdConnectRealmSettings.TYPE, "node01");
|
||||
final Authentication authentication = new Authentication(user, realmRef, null);
|
||||
|
||||
final Map<String, Object> tokenMetadata = new HashMap<>();
|
||||
tokenMetadata.put("id_token_hint", signedIdToken.serialize());
|
||||
tokenMetadata.put("oidc_realm", REALM_NAME);
|
||||
|
||||
final PlainActionFuture<Tuple<UserToken, String>> future = new PlainActionFuture<>();
|
||||
tokenService.createOAuth2Tokens(authentication, authentication, tokenMetadata, true, future);
|
||||
final UserToken userToken = future.actionGet().v1();
|
||||
mockGetTokenFromId(userToken, false, client);
|
||||
final String tokenString = tokenService.getAccessTokenAsString(userToken);
|
||||
|
||||
final OpenIdConnectLogoutRequest request = new OpenIdConnectLogoutRequest();
|
||||
request.setToken(tokenString);
|
||||
|
||||
final PlainActionFuture<OpenIdConnectLogoutResponse> listener = new PlainActionFuture<>();
|
||||
action.doExecute(mock(Task.class), request, listener);
|
||||
final OpenIdConnectLogoutResponse response = listener.get();
|
||||
assertNotNull(response);
|
||||
assertThat(response.getEndSessionUrl(), notNullValue());
|
||||
// One index request to create the token
|
||||
assertThat(indexRequests.size(), equalTo(1));
|
||||
final IndexRequest indexRequest = indexRequests.get(0);
|
||||
assertThat(indexRequest, notNullValue());
|
||||
assertThat(indexRequest.id(), startsWith("token"));
|
||||
// One bulk request (containing one update request) to invalidate the token
|
||||
assertThat(bulkRequests.size(), equalTo(1));
|
||||
final BulkRequest bulkRequest = bulkRequests.get(0);
|
||||
assertThat(bulkRequest.requests().size(), equalTo(1));
|
||||
assertThat(bulkRequest.requests().get(0), instanceOf(UpdateRequest.class));
|
||||
assertThat(bulkRequest.requests().get(0).id(), startsWith("token_"));
|
||||
assertThat(bulkRequest.requests().get(0).toString(), containsString("\"access_token\":{\"invalidated\":true"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
oidcRealm.close();
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
|
|||
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings;
|
||||
import org.elasticsearch.xpack.core.ssl.SSLService;
|
||||
|
@ -61,7 +62,7 @@ public class InternalRealmsTests extends ESTestCase {
|
|||
String type = randomFrom(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE,
|
||||
PkiRealmSettings.TYPE);
|
||||
assertThat(InternalRealms.isStandardRealm(type), is(true));
|
||||
type = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE);
|
||||
type = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, OpenIdConnectRealmSettings.TYPE);
|
||||
assertThat(InternalRealms.isStandardRealm(type), is(false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.elasticsearch.xpack.core.security.authc.Realm;
|
|||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
|
||||
import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealmTestCase;
|
||||
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectTestCase;
|
||||
import org.elasticsearch.xpack.security.authc.saml.SamlRealmTestHelper;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.AfterClass;
|
||||
|
@ -48,6 +49,9 @@ public class SecurityRealmSettingsTests extends SecurityIntegTestCase {
|
|||
final Path kerbKeyTab = createTempFile("es", "keytab");
|
||||
KerberosRealmTestCase.writeKeyTab(kerbKeyTab, null);
|
||||
|
||||
final Path jwkSet = createTempFile("jwkset", "json");
|
||||
OpenIdConnectTestCase.writeJwkSetToFile(jwkSet);
|
||||
|
||||
settings = Settings.builder()
|
||||
.put(super.nodeSettings(nodeOrdinal).filter(s -> s.startsWith("xpack.security.authc.realms.") == false))
|
||||
.put("xpack.security.authc.token.enabled", true)
|
||||
|
@ -67,6 +71,16 @@ public class SecurityRealmSettingsTests extends SecurityIntegTestCase {
|
|||
.put("xpack.security.authc.realms.saml.saml1.attributes.principal", "uid")
|
||||
.put("xpack.security.authc.realms.kerberos.kerb1.order", 7)
|
||||
.put("xpack.security.authc.realms.kerberos.kerb1.keytab.path", kerbKeyTab.toAbsolutePath())
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.order", 8)
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.op.name", "myprovider")
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.op.issuer", "https://the.issuer.com:8090")
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.op.jwkset_path", jwkSet.toAbsolutePath())
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.op.authorization_endpoint", "https://the.issuer.com:8090/login")
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.op.token_endpoint", "https://the.issuer.com:8090/token")
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.rp.redirect_uri", "https://localhost/cb")
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.rp.client_id", "my_client")
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.rp.response_type", "code")
|
||||
.put("xpack.security.authc.realms.oidc.oidc1.claims.principal", "sub")
|
||||
.build();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
@ -84,7 +98,7 @@ public class SecurityRealmSettingsTests extends SecurityIntegTestCase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Some realms (currently only SAML, but maybe more in the future) hold on to resources that may need to be explicitly closed.
|
||||
* Some realms (SAML and OIDC at the moment) hold on to resources that may need to be explicitly closed.
|
||||
*/
|
||||
@AfterClass
|
||||
public static void closeRealms() throws IOException {
|
||||
|
|
|
@ -0,0 +1,808 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.crypto.ECDSASigner;
|
||||
import com.nimbusds.jose.crypto.MACSigner;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.jwk.ECKey;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.OctetSequenceKey;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.proc.BadJOSEException;
|
||||
import com.nimbusds.jose.proc.BadJWSException;
|
||||
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.PlainJWT;
|
||||
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.Secret;
|
||||
import com.nimbusds.oauth2.sdk.id.ClientID;
|
||||
import com.nimbusds.oauth2.sdk.id.Issuer;
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import com.nimbusds.oauth2.sdk.token.AccessToken;
|
||||
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
|
||||
import com.nimbusds.openid.connect.sdk.Nonce;
|
||||
import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash;
|
||||
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
|
||||
import com.nimbusds.openid.connect.sdk.validators.InvalidHashException;
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.PlainActionFuture;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.env.TestEnvironment;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.ssl.SSLService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.time.Instant.now;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||
|
||||
private OpenIdConnectAuthenticator authenticator;
|
||||
private Settings globalSettings;
|
||||
private Environment env;
|
||||
private ThreadContext threadContext;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
globalSettings = Settings.builder().put("path.home", createTempDir())
|
||||
.put("xpack.security.authc.realms.oidc.oidc-realm.ssl.verification_mode", "certificate").build();
|
||||
env = TestEnvironment.newEnvironment(globalSettings);
|
||||
threadContext = new ThreadContext(globalSettings);
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
authenticator.close();
|
||||
}
|
||||
|
||||
private OpenIdConnectAuthenticator buildAuthenticator() throws URISyntaxException {
|
||||
final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext);
|
||||
return new OpenIdConnectAuthenticator(config, getOpConfig(), getDefaultRpConfig(), new SSLService(globalSettings, env), null);
|
||||
}
|
||||
|
||||
private OpenIdConnectAuthenticator buildAuthenticator(OpenIdConnectProviderConfiguration opConfig, RelyingPartyConfiguration rpConfig,
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource) {
|
||||
final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext);
|
||||
final JWSVerificationKeySelector keySelector = new JWSVerificationKeySelector(rpConfig.getSignatureAlgorithm(), jwkSource);
|
||||
final IDTokenValidator validator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), keySelector, null);
|
||||
return new OpenIdConnectAuthenticator(config, opConfig, rpConfig, new SSLService(globalSettings, env), validator,
|
||||
null);
|
||||
}
|
||||
|
||||
private OpenIdConnectAuthenticator buildAuthenticator(OpenIdConnectProviderConfiguration opConfig,
|
||||
RelyingPartyConfiguration rpConfig) {
|
||||
final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext);
|
||||
final IDTokenValidator validator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(),
|
||||
rpConfig.getSignatureAlgorithm(), new Secret(rpConfig.getClientSecret().toString()));
|
||||
return new OpenIdConnectAuthenticator(config, opConfig, rpConfig, new SSLService(globalSettings, env), validator,
|
||||
null);
|
||||
}
|
||||
|
||||
public void testEmptyRedirectUrlIsRejected() throws Exception {
|
||||
authenticator = buildAuthenticator();
|
||||
OpenIdConnectToken token = new OpenIdConnectToken(null, new State(), new Nonce());
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to consume the OpenID connect response"));
|
||||
}
|
||||
|
||||
public void testInvalidStateIsRejected() throws URISyntaxException {
|
||||
authenticator = buildAuthenticator();
|
||||
final String code = randomAlphaOfLengthBetween(8, 12);
|
||||
final String state = randomAlphaOfLengthBetween(8, 12);
|
||||
final String invalidState = state.concat(randomAlphaOfLength(2));
|
||||
final String redirectUrl = "https://rp.elastic.co/cb?code=" + code + "&state=" + state;
|
||||
OpenIdConnectToken token = new OpenIdConnectToken(redirectUrl, new State(invalidState), new Nonce());
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Invalid state parameter"));
|
||||
}
|
||||
|
||||
public void testInvalidNonceIsRejected() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final Nonce invalidNonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(invalidNonce, key, jwk.getAlgorithm().getName(), keyId, subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT nonce"));
|
||||
}
|
||||
|
||||
public void testAuthenticateImplicitFlowWithRsa() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType("RS");
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
JWTClaimsSet claimsSet = future.actionGet();
|
||||
assertThat(claimsSet.getSubject(), equalTo(subject));
|
||||
}
|
||||
|
||||
public void testAuthenticateImplicitFlowWithEcdsa() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType("RS");
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
JWTClaimsSet claimsSet = future.actionGet();
|
||||
assertThat(claimsSet.getSubject(), equalTo(subject));
|
||||
}
|
||||
|
||||
public void testAuthenticateImplicitFlowWithHmac() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType("HS");
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), null, subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
JWTClaimsSet claimsSet = future.actionGet();
|
||||
assertThat(claimsSet.getSubject(), equalTo(subject));
|
||||
}
|
||||
|
||||
public void testClockSkewIsHonored() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
|
||||
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.audience(rpConfig.getClientId().getValue())
|
||||
// Expired 55 seconds ago with an allowed clock skew of 60 seconds
|
||||
.expirationTime(Date.from(now().minusSeconds(55)))
|
||||
.issuer(opConfig.getIssuer().getValue())
|
||||
.issueTime(Date.from(now().minusSeconds(200)))
|
||||
.notBeforeTime(Date.from(now().minusSeconds(200)))
|
||||
.claim("nonce", nonce)
|
||||
.subject(subject);
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId, subject,
|
||||
true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
JWTClaimsSet claimsSet = future.actionGet();
|
||||
assertThat(claimsSet.getSubject(), equalTo(subject));
|
||||
}
|
||||
|
||||
public void testImplicitFlowFailsWithExpiredToken() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
|
||||
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.audience(rpConfig.getClientId().getValue())
|
||||
// Expired 65 seconds ago with an allowed clock skew of 60 seconds
|
||||
.expirationTime(Date.from(now().minusSeconds(65)))
|
||||
.issuer(opConfig.getIssuer().getValue())
|
||||
.issueTime(Date.from(now().minusSeconds(200)))
|
||||
.notBeforeTime(Date.from(now().minusSeconds(200)))
|
||||
.claim("nonce", nonce)
|
||||
.subject(subject);
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
|
||||
subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Expired JWT"));
|
||||
}
|
||||
|
||||
public void testImplicitFlowFailsNotYetIssuedToken() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
|
||||
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.audience(rpConfig.getClientId().getValue())
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issuer(opConfig.getIssuer().getValue())
|
||||
// Issued 80 seconds in the future with max allowed clock skew of 60
|
||||
.issueTime(Date.from(now().plusSeconds(80)))
|
||||
.notBeforeTime(Date.from(now().minusSeconds(80)))
|
||||
.claim("nonce", nonce)
|
||||
.subject(subject);
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
|
||||
subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("JWT issue time ahead of current time"));
|
||||
}
|
||||
|
||||
public void testImplicitFlowFailsInvalidIssuer() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
|
||||
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.audience(rpConfig.getClientId().getValue())
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issuer("https://another.op.org")
|
||||
.issueTime(Date.from(now().minusSeconds(200)))
|
||||
.notBeforeTime(Date.from(now().minusSeconds(200)))
|
||||
.claim("nonce", nonce)
|
||||
.subject(subject);
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
|
||||
subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT issuer"));
|
||||
}
|
||||
|
||||
public void testImplicitFlowFailsInvalidAudience() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
|
||||
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.audience("some-other-RP")
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issuer(opConfig.getIssuer().getValue())
|
||||
.issueTime(Date.from(now().minusSeconds(200)))
|
||||
.notBeforeTime(Date.from(now().minusSeconds(80)))
|
||||
.claim("nonce", nonce)
|
||||
.subject(subject);
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
|
||||
subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT audience"));
|
||||
}
|
||||
|
||||
public void testAuthenticateImplicitFlowFailsWithForgedRsaIdToken() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType("RS");
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, true);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
||||
}
|
||||
|
||||
public void testAuthenticateImplicitFlowFailsWithForgedEcsdsaIdToken() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType("ES");
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, true);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
||||
}
|
||||
|
||||
public void testAuthenticateImplicitFlowFailsWithForgedHmacIdToken() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType("HS");
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), null, subject, true, true);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
||||
}
|
||||
|
||||
public void testAuthenticateImplicitFlowFailsWithForgedAccessToken() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), keyId, subject, true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), new BearerAccessToken("someforgedAccessToken"), state,
|
||||
rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to verify access token"));
|
||||
assertThat(e.getCause(), instanceOf(InvalidHashException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Access token hash (at_hash) mismatch"));
|
||||
}
|
||||
|
||||
public void testImplicitFlowFailsWithNoneAlgorithm() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
final Key key = keyMaterial.v1();
|
||||
RelyingPartyConfiguration rpConfig = getRpConfigNoAccessToken(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), keyId, subject, false, false);
|
||||
JWT idToken = tokens.v2();
|
||||
// Change the algorithm of the signed JWT to NONE
|
||||
String[] serializedParts = idToken.serialize().split("\\.");
|
||||
String legitimateHeader = new String(Base64.getUrlDecoder().decode(serializedParts[0]), StandardCharsets.UTF_8);
|
||||
String forgedHeader = legitimateHeader.replace(jwk.getAlgorithm().getName(), "NONE");
|
||||
String encodedForgedHeader =
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(forgedHeader.getBytes(StandardCharsets.UTF_8));
|
||||
String fordedTokenString = encodedForgedHeader + "." + serializedParts[1] + "." + serializedParts[2];
|
||||
idToken = SignedJWT.parse(fordedTokenString);
|
||||
final String responseUrl = buildAuthResponse(idToken, tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJOSEException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The premise of this attack is that an RP that expects a JWT signed with an asymmetric algorithm (RSA, ECDSA)
|
||||
* receives a JWT signed with an HMAC. Trusting the received JWT's alg claim more than it's own configuration,
|
||||
* it attempts to validate the HMAC with the provider's {RSA,EC} public key as a secret key
|
||||
*/
|
||||
public void testImplicitFlowFailsWithAlgorithmMixupAttack() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
SecretKeySpec hmacKey = new SecretKeySpec("thisismysupersupersupersupersupersuperlongsecret".getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSha384");
|
||||
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, hmacKey, "HS384", null, subject,
|
||||
true, false);
|
||||
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJOSEException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found"));
|
||||
}
|
||||
|
||||
public void testImplicitFlowFailsWithUnsignedJwt() throws Exception {
|
||||
final Tuple<Key, JWKSet> keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS"));
|
||||
final JWK jwk = keyMaterial.v2().getKeys().get(0);
|
||||
RelyingPartyConfiguration rpConfig = getRpConfigNoAccessToken(jwk.getAlgorithm().getName());
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
if (jwk.getAlgorithm().getName().startsWith("HS")) {
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig);
|
||||
} else {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk);
|
||||
authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource);
|
||||
}
|
||||
final State state = new State();
|
||||
final Nonce nonce = new Nonce();
|
||||
final String subject = "janedoe";
|
||||
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.audience(rpConfig.getClientId().getValue())
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issuer(opConfig.getIssuer().getValue())
|
||||
.issueTime(Date.from(now().minusSeconds(200)))
|
||||
.notBeforeTime(Date.from(now().minusSeconds(200)))
|
||||
.claim("nonce", nonce)
|
||||
.subject(subject);
|
||||
|
||||
final String responseUrl = buildAuthResponse(new PlainJWT(idTokenBuilder.build()), null, state,
|
||||
rpConfig.getRedirectUri());
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
|
||||
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
|
||||
authenticator.authenticate(token, future);
|
||||
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
|
||||
future::actionGet);
|
||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||
assertThat(e.getCause().getMessage(), containsString("Signed ID token expected"));
|
||||
}
|
||||
|
||||
private OpenIdConnectProviderConfiguration getOpConfig() throws URISyntaxException {
|
||||
return new OpenIdConnectProviderConfiguration("op_name",
|
||||
new Issuer("https://op.example.com"),
|
||||
"https://op.example.org/jwks.json",
|
||||
new URI("https://op.example.org/login"),
|
||||
new URI("https://op.example.org/token"),
|
||||
null,
|
||||
new URI("https://op.example.org/logout"));
|
||||
}
|
||||
|
||||
private RelyingPartyConfiguration getDefaultRpConfig() throws URISyntaxException {
|
||||
return new RelyingPartyConfiguration(
|
||||
new ClientID("rp-my"),
|
||||
new SecureString("thisismysupersupersupersupersupersuperlongsecret".toCharArray()),
|
||||
new URI("https://rp.elastic.co/cb"),
|
||||
new ResponseType("id_token", "token"),
|
||||
new Scope("openid"),
|
||||
JWSAlgorithm.RS384,
|
||||
new URI("https://rp.elastic.co/successfull_logout"));
|
||||
}
|
||||
private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxException {
|
||||
return new RelyingPartyConfiguration(
|
||||
new ClientID("rp-my"),
|
||||
new SecureString("thisismysupersupersupersupersupersuperlongsecret".toCharArray()),
|
||||
new URI("https://rp.elastic.co/cb"),
|
||||
new ResponseType("id_token", "token"),
|
||||
new Scope("openid"),
|
||||
JWSAlgorithm.parse(alg),
|
||||
new URI("https://rp.elastic.co/successfull_logout"));
|
||||
}
|
||||
|
||||
private RelyingPartyConfiguration getRpConfigNoAccessToken(String alg) throws URISyntaxException {
|
||||
return new RelyingPartyConfiguration(
|
||||
new ClientID("rp-my"),
|
||||
new SecureString("thisismysupersupersupersupersupersuperlongsecret".toCharArray()),
|
||||
new URI("https://rp.elastic.co/cb"),
|
||||
new ResponseType("id_token"),
|
||||
new Scope("openid"),
|
||||
JWSAlgorithm.parse(alg),
|
||||
new URI("https://rp.elastic.co/successfull_logout"));
|
||||
}
|
||||
|
||||
private String buildAuthResponse(JWT idToken, @Nullable AccessToken accessToken, State state, URI redirectUri) {
|
||||
AuthenticationSuccessResponse response = new AuthenticationSuccessResponse(
|
||||
redirectUri,
|
||||
null,
|
||||
idToken,
|
||||
accessToken,
|
||||
state,
|
||||
null,
|
||||
null);
|
||||
return response.toURI().toString();
|
||||
}
|
||||
|
||||
private OpenIdConnectAuthenticator.ReloadableJWKSource mockSource(JWK jwk) {
|
||||
OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource =
|
||||
mock(OpenIdConnectAuthenticator.ReloadableJWKSource.class);
|
||||
when(jwkSource.get(any(), any())).thenReturn(Collections.singletonList(jwk));
|
||||
Mockito.doAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
ActionListener<Void> listener = (ActionListener<Void>) invocation.getArguments()[0];
|
||||
listener.onResponse(null);
|
||||
return null;
|
||||
}).when(jwkSource).triggerReload(any(ActionListener.class));
|
||||
return jwkSource;
|
||||
}
|
||||
|
||||
private Tuple<AccessToken, JWT> buildTokens(JWTClaimsSet idToken, Key key, String alg, String keyId,
|
||||
String subject, boolean withAccessToken, boolean forged) throws Exception {
|
||||
AccessToken accessToken = null;
|
||||
if (withAccessToken) {
|
||||
accessToken = new BearerAccessToken(Base64.getUrlEncoder().encodeToString(randomByteArrayOfLength(32)));
|
||||
AccessTokenHash expectedHash = AccessTokenHash.compute(accessToken, JWSAlgorithm.parse(alg));
|
||||
idToken = JWTClaimsSet.parse(idToken.toJSONObject().appendField("at_hash", expectedHash.getValue()));
|
||||
}
|
||||
SignedJWT jwt = new SignedJWT(
|
||||
new JWSHeader.Builder(JWSAlgorithm.parse(alg)).keyID(keyId).build(),
|
||||
idToken);
|
||||
|
||||
if (key instanceof RSAPrivateKey) {
|
||||
jwt.sign(new RSASSASigner((PrivateKey) key));
|
||||
} else if (key instanceof ECPrivateKey) {
|
||||
jwt.sign(new ECDSASigner((ECPrivateKey) key));
|
||||
} else if (key instanceof SecretKey) {
|
||||
jwt.sign(new MACSigner((SecretKey) key));
|
||||
}
|
||||
if (forged) {
|
||||
// Change the sub claim to "attacker"
|
||||
String[] serializedParts = jwt.serialize().split("\\.");
|
||||
String legitimatePayload = new String(Base64.getUrlDecoder().decode(serializedParts[1]), StandardCharsets.UTF_8);
|
||||
String forgedPayload = legitimatePayload.replace(subject, "attacker");
|
||||
String encodedForgedPayload =
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(forgedPayload.getBytes(StandardCharsets.UTF_8));
|
||||
String fordedTokenString = serializedParts[0] + "." + encodedForgedPayload + "." + serializedParts[2];
|
||||
jwt = SignedJWT.parse(fordedTokenString);
|
||||
}
|
||||
return new Tuple<>(accessToken, jwt);
|
||||
}
|
||||
|
||||
private Tuple<AccessToken, JWT> buildTokens(Nonce nonce, Key key, String alg, String keyId, String subject, boolean withAccessToken,
|
||||
boolean forged) throws Exception {
|
||||
RelyingPartyConfiguration rpConfig = getRpConfig(alg);
|
||||
OpenIdConnectProviderConfiguration opConfig = getOpConfig();
|
||||
JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder()
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.audience(rpConfig.getClientId().getValue())
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issuer(opConfig.getIssuer().getValue())
|
||||
.issueTime(Date.from(now().minusSeconds(4)))
|
||||
.notBeforeTime(Date.from(now().minusSeconds(4)))
|
||||
.claim("nonce", nonce)
|
||||
.subject(subject);
|
||||
|
||||
return buildTokens(idTokenBuilder.build(), key, alg, keyId, subject, withAccessToken, forged);
|
||||
}
|
||||
|
||||
private Tuple<Key, JWKSet> getRandomJwkForType(String type) throws Exception {
|
||||
JWK jwk;
|
||||
Key key;
|
||||
int hashSize;
|
||||
if (type.equals("RS")) {
|
||||
hashSize = randomFrom(256, 384, 512);
|
||||
int keySize = randomFrom(2048, 4096);
|
||||
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
|
||||
gen.initialize(keySize);
|
||||
KeyPair keyPair = gen.generateKeyPair();
|
||||
key = keyPair.getPrivate();
|
||||
jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
|
||||
.privateKey((RSAPrivateKey) keyPair.getPrivate())
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.keyID(UUID.randomUUID().toString())
|
||||
.algorithm(JWSAlgorithm.parse(type + hashSize))
|
||||
.build();
|
||||
|
||||
} else if (type.equals("HS")) {
|
||||
hashSize = randomFrom(256, 384);
|
||||
SecretKeySpec hmacKey = new SecretKeySpec("thisismysupersupersupersupersupersuperlongsecret".getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSha" + hashSize);
|
||||
//SecretKey hmacKey = KeyGenerator.getInstance("HmacSha" + hashSize).generateKey();
|
||||
key = hmacKey;
|
||||
jwk = new OctetSequenceKey.Builder(hmacKey)
|
||||
.keyID(UUID.randomUUID().toString())
|
||||
.algorithm(JWSAlgorithm.parse(type + hashSize))
|
||||
.build();
|
||||
|
||||
} else if (type.equals("ES")) {
|
||||
hashSize = randomFrom(256, 384, 512);
|
||||
ECKey.Curve curve = curveFromHashSize(hashSize);
|
||||
KeyPairGenerator gen = KeyPairGenerator.getInstance("EC");
|
||||
gen.initialize(curve.toECParameterSpec());
|
||||
KeyPair keyPair = gen.generateKeyPair();
|
||||
key = keyPair.getPrivate();
|
||||
jwk = new ECKey.Builder(curve, (ECPublicKey) keyPair.getPublic())
|
||||
.privateKey((ECPrivateKey) keyPair.getPrivate())
|
||||
.algorithm(JWSAlgorithm.parse(type + hashSize))
|
||||
.build();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid key type :" + type);
|
||||
}
|
||||
return new Tuple(key, new JWKSet(jwk));
|
||||
}
|
||||
|
||||
private ECKey.Curve curveFromHashSize(int size) {
|
||||
if (size == 256) {
|
||||
return ECKey.Curve.P_256;
|
||||
} else if (size == 384) {
|
||||
return ECKey.Curve.P_384;
|
||||
} else if (size == 512) {
|
||||
return ECKey.Curve.P_521;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid hash size:" + size);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.settings.SettingsException;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.env.TestEnvironment;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey;
|
||||
|
||||
public class OpenIdConnectRealmSettingsTests extends ESTestCase {
|
||||
|
||||
private static final String REALM_NAME = "oidc1-realm";
|
||||
private ThreadContext threadContext;
|
||||
|
||||
@Before
|
||||
public void setupEnv() {
|
||||
Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build();
|
||||
threadContext = new ThreadContext(globalSettings);
|
||||
}
|
||||
|
||||
public void testIncorrectResponseTypeThrowsError() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.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), "hybrid");
|
||||
IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(), Matchers.containsString(getFullSettingKey(REALM_NAME,
|
||||
OpenIdConnectRealmSettings.RP_RESPONSE_TYPE)));
|
||||
}
|
||||
|
||||
public void testMissingAuthorizationEndpointThrowsError() {
|
||||
final Settings.Builder settingsBuilder = Settings.builder()
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.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");
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT)));
|
||||
}
|
||||
|
||||
public void testInvalidAuthorizationEndpointThrowsError() {
|
||||
final Settings.Builder settingsBuilder = Settings.builder()
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "this is not a URI")
|
||||
.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.OP_NAME), "the op")
|
||||
.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");
|
||||
IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT)));
|
||||
}
|
||||
|
||||
public void testMissingTokenEndpointThrowsError() {
|
||||
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_NAME), "the op")
|
||||
.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");
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT)));
|
||||
}
|
||||
|
||||
public void testInvalidTokenEndpointThrowsError() {
|
||||
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), "This is not a uri")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.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");
|
||||
IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT)));
|
||||
}
|
||||
|
||||
public void testMissingJwksUrlThrowsError() {
|
||||
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_NAME), "the op")
|
||||
.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");
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH)));
|
||||
}
|
||||
|
||||
public void testMissingIssuerThrowsError() {
|
||||
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_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.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");
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER)));
|
||||
}
|
||||
|
||||
public void testMissingNameTypeThrowsError() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.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");
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME)));
|
||||
}
|
||||
|
||||
public void testMissingRedirectUriThrowsError() {
|
||||
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_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_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code");
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI)));
|
||||
}
|
||||
|
||||
public void testMissingClientIdThrowsError() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.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_RESPONSE_TYPE), "code");
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID)));
|
||||
}
|
||||
|
||||
public void testMissingPrincipalClaimThrowsError() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code")
|
||||
.putList(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES),
|
||||
Arrays.asList("openid", "scope1", "scope2"));
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim())));
|
||||
}
|
||||
|
||||
public void testPatternWithoutSettingThrowsError() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getPattern()), "^(.*)$")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code")
|
||||
.putList(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES),
|
||||
Arrays.asList("openid", "scope1", "scope2"));
|
||||
SettingsException exception = expectThrows(SettingsException.class, () -> {
|
||||
new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null);
|
||||
});
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getClaim())));
|
||||
assertThat(exception.getMessage(),
|
||||
Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getPattern())));
|
||||
}
|
||||
|
||||
private RealmConfig buildConfig(Settings realmSettings) {
|
||||
final Settings settings = Settings.builder()
|
||||
.put("path.home", createTempDir())
|
||||
.put(realmSettings).build();
|
||||
final Environment env = TestEnvironment.newEnvironment(settings);
|
||||
return new RealmConfig(new RealmConfig.RealmIdentifier("oidc", REALM_NAME), settings, env, threadContext);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import com.nimbusds.openid.connect.sdk.Nonce;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.PlainActionFuture;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.env.TestEnvironment;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse;
|
||||
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
|
||||
import org.elasticsearch.xpack.core.security.authc.Realm;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;
|
||||
import org.elasticsearch.xpack.core.security.user.User;
|
||||
import org.elasticsearch.xpack.security.authc.support.MockLookupRealm;
|
||||
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.time.Instant.now;
|
||||
import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey;
|
||||
import static org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm.CONTEXT_TOKEN_DATA;
|
||||
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
|
||||
|
||||
private Settings globalSettings;
|
||||
private Environment env;
|
||||
private ThreadContext threadContext;
|
||||
|
||||
@Before
|
||||
public void setupEnv() {
|
||||
globalSettings = Settings.builder().put("path.home", createTempDir()).build();
|
||||
env = TestEnvironment.newEnvironment(globalSettings);
|
||||
threadContext = new ThreadContext(globalSettings);
|
||||
}
|
||||
|
||||
public void testAuthentication() throws Exception {
|
||||
final UserRoleMapper roleMapper = mock(UserRoleMapper.class);
|
||||
AtomicReference<UserRoleMapper.UserData> userData = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
assert invocation.getArguments().length == 2;
|
||||
userData.set((UserRoleMapper.UserData) invocation.getArguments()[0]);
|
||||
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1];
|
||||
listener.onResponse(new HashSet<>(Arrays.asList("kibana_user", "role1")));
|
||||
return null;
|
||||
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class));
|
||||
|
||||
final boolean notPopulateMetadata = randomBoolean();
|
||||
|
||||
AuthenticationResult result = authenticateWithOidc(roleMapper, notPopulateMetadata, false);
|
||||
assertThat(result.getUser().roles(), arrayContainingInAnyOrder("kibana_user", "role1"));
|
||||
if (notPopulateMetadata == false) {
|
||||
assertThat(result.getUser().metadata().get("oidc(iss)"), equalTo("https://op.company.org"));
|
||||
assertThat(result.getUser().metadata().get("oidc(name)"), equalTo("Clinton Barton"));
|
||||
}
|
||||
}
|
||||
|
||||
public void testWithAuthorizingRealm() throws Exception {
|
||||
final UserRoleMapper roleMapper = mock(UserRoleMapper.class);
|
||||
doAnswer(invocation -> {
|
||||
assert invocation.getArguments().length == 2;
|
||||
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1];
|
||||
listener.onFailure(new RuntimeException("Role mapping should not be called"));
|
||||
return null;
|
||||
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class));
|
||||
|
||||
AuthenticationResult result = authenticateWithOidc(roleMapper, randomBoolean(), true);
|
||||
assertThat(result.getUser().roles(), arrayContainingInAnyOrder("lookup_user_role"));
|
||||
assertThat(result.getUser().fullName(), equalTo("Clinton Barton"));
|
||||
assertThat(result.getUser().metadata().entrySet(), Matchers.iterableWithSize(1));
|
||||
assertThat(result.getUser().metadata().get("is_lookup"), Matchers.equalTo(true));
|
||||
assertNotNull(result.getMetadata().get(CONTEXT_TOKEN_DATA));
|
||||
assertThat(result.getMetadata().get(CONTEXT_TOKEN_DATA), instanceOf(Map.class));
|
||||
Map<String, Object> tokenMetadata = (Map) result.getMetadata().get(CONTEXT_TOKEN_DATA);
|
||||
assertThat(tokenMetadata.get("id_token_hint"), equalTo("thisis.aserialized.jwt"));
|
||||
}
|
||||
|
||||
public void testClaimPatternParsing() throws Exception {
|
||||
final Settings.Builder builder = getBasicRealmSettings();
|
||||
builder.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getPattern()), "^OIDC-(.+)");
|
||||
final RealmConfig config = buildConfig(builder.build(), threadContext);
|
||||
final OpenIdConnectRealmSettings.ClaimSetting principalSetting = new OpenIdConnectRealmSettings.ClaimSetting("principal");
|
||||
final OpenIdConnectRealm.ClaimParser parser = OpenIdConnectRealm.ClaimParser.forSetting(logger, principalSetting, config, true);
|
||||
final JWTClaimsSet claims = new JWTClaimsSet.Builder()
|
||||
.subject("OIDC-cbarton")
|
||||
.audience("https://rp.elastic.co/cb")
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issueTime(Date.from(now().minusSeconds(5)))
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.issuer("https://op.company.org")
|
||||
.build();
|
||||
assertThat(parser.getClaimValue(claims), equalTo("cbarton"));
|
||||
}
|
||||
|
||||
public void testInvalidPrincipalClaimPatternParsing() {
|
||||
final OpenIdConnectAuthenticator authenticator = mock(OpenIdConnectAuthenticator.class);
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce());
|
||||
final Settings.Builder builder = getBasicRealmSettings();
|
||||
builder.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getPattern()), "^OIDC-(.+)");
|
||||
final RealmConfig config = buildConfig(builder.build(), threadContext);
|
||||
final OpenIdConnectRealm realm = new OpenIdConnectRealm(config, authenticator, null);
|
||||
final JWTClaimsSet claims = new JWTClaimsSet.Builder()
|
||||
.subject("cbarton@avengers.com")
|
||||
.audience("https://rp.elastic.co/cb")
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issueTime(Date.from(now().minusSeconds(5)))
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.issuer("https://op.company.org")
|
||||
.build();
|
||||
doAnswer((i) -> {
|
||||
ActionListener<JWTClaimsSet> listener = (ActionListener<JWTClaimsSet>) i.getArguments()[1];
|
||||
listener.onResponse(claims);
|
||||
return null;
|
||||
}).when(authenticator).authenticate(any(OpenIdConnectToken.class), any(ActionListener.class));
|
||||
|
||||
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
|
||||
realm.authenticate(token, future);
|
||||
final AuthenticationResult result = future.actionGet();
|
||||
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE));
|
||||
assertThat(result.getMessage(), containsString("claims.principal"));
|
||||
assertThat(result.getMessage(), containsString("sub"));
|
||||
assertThat(result.getMessage(), containsString("^OIDC-(.+)"));
|
||||
}
|
||||
|
||||
public void testBuildRelyingPartyConfigWithoutOpenIdScope() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code")
|
||||
.putList(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES),
|
||||
Arrays.asList("scope1", "scope2"));
|
||||
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null,
|
||||
null);
|
||||
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null);
|
||||
final String state = response.getState();
|
||||
final String nonce = response.getNonce();
|
||||
assertThat(response.getAuthenticationRequestUrl(),
|
||||
equalTo("https://op.example.com/login?scope=scope1+scope2+openid&response_type=code" +
|
||||
"&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my"));
|
||||
}
|
||||
|
||||
public void testBuildingAuthenticationRequest() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code")
|
||||
.putList(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES),
|
||||
Arrays.asList("openid", "scope1", "scope2"));
|
||||
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null,
|
||||
null);
|
||||
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null);
|
||||
final String state = response.getState();
|
||||
final String nonce = response.getNonce();
|
||||
assertThat(response.getAuthenticationRequestUrl(),
|
||||
equalTo("https://op.example.com/login?scope=openid+scope1+scope2&response_type=code" +
|
||||
"&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my"));
|
||||
}
|
||||
|
||||
public void testBuilidingAuthenticationRequestWithDefaultScope() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code");
|
||||
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null,
|
||||
null);
|
||||
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null);
|
||||
final String state = response.getState();
|
||||
final String nonce = response.getNonce();
|
||||
assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?scope=openid&response_type=code" +
|
||||
"&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my"));
|
||||
}
|
||||
|
||||
public void testBuildLogoutResponse() throws Exception {
|
||||
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(getBasicRealmSettings().build(), threadContext), null,
|
||||
null);
|
||||
// Random strings, as we will not validate the token here
|
||||
final JWT idToken = generateIdToken(randomAlphaOfLength(8), randomAlphaOfLength(8), randomAlphaOfLength(8));
|
||||
final OpenIdConnectLogoutResponse logoutResponse = realm.buildLogoutResponse(idToken);
|
||||
assertThat(logoutResponse.getEndSessionUrl(), containsString("https://op.example.org/logout?id_token_hint="));
|
||||
assertThat(logoutResponse.getEndSessionUrl(),
|
||||
containsString("&post_logout_redirect_uri=https%3A%2F%2Frp.elastic.co%2Fsucc_logout&state="));
|
||||
}
|
||||
|
||||
public void testBuildingAuthenticationRequestWithExistingStateAndNonce() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code");
|
||||
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null,
|
||||
null);
|
||||
final String state = new State().getValue();
|
||||
final String nonce = new Nonce().getValue();
|
||||
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(state, nonce, null);
|
||||
|
||||
assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?scope=openid&response_type=code" +
|
||||
"&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my"));
|
||||
}
|
||||
|
||||
public void testBuildingAuthenticationRequestWithLoginHint() {
|
||||
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_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code");
|
||||
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null,
|
||||
null);
|
||||
final String state = new State().getValue();
|
||||
final String nonce = new Nonce().getValue();
|
||||
final String thehint = randomAlphaOfLength(8);
|
||||
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(state, nonce, thehint);
|
||||
|
||||
assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?login_hint=" + thehint +
|
||||
"&scope=openid&response_type=code&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" +
|
||||
state + "&nonce=" + nonce + "&client_id=rp-my"));
|
||||
}
|
||||
|
||||
private AuthenticationResult authenticateWithOidc(UserRoleMapper roleMapper, boolean notPopulateMetadata, boolean useAuthorizingRealm)
|
||||
throws Exception {
|
||||
|
||||
final String principal = "324235435454";
|
||||
final MockLookupRealm lookupRealm = new MockLookupRealm(
|
||||
new RealmConfig(new RealmConfig.RealmIdentifier("mock", "mock_lookup"), globalSettings, env, threadContext));
|
||||
final OpenIdConnectAuthenticator authenticator = mock(OpenIdConnectAuthenticator.class);
|
||||
|
||||
final Settings.Builder builder = getBasicRealmSettings();
|
||||
if (notPopulateMetadata) {
|
||||
builder.put(getFullSettingKey(REALM_NAME, SamlRealmSettings.POPULATE_USER_METADATA),
|
||||
false);
|
||||
}
|
||||
if (useAuthorizingRealm) {
|
||||
builder.putList(getFullSettingKey(new RealmConfig.RealmIdentifier("oidc", REALM_NAME),
|
||||
DelegatedAuthorizationSettings.AUTHZ_REALMS), lookupRealm.name());
|
||||
lookupRealm.registerUser(new User(principal, new String[]{"lookup_user_role"}, "Clinton Barton", "cbarton@shield.gov",
|
||||
Collections.singletonMap("is_lookup", true), true));
|
||||
}
|
||||
final RealmConfig config = buildConfig(builder.build(), threadContext);
|
||||
final OpenIdConnectRealm realm = new OpenIdConnectRealm(config, authenticator, roleMapper);
|
||||
initializeRealms(realm, lookupRealm);
|
||||
final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce());
|
||||
final JWTClaimsSet claims = new JWTClaimsSet.Builder()
|
||||
.subject(principal)
|
||||
.audience("https://rp.elastic.co/cb")
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issueTime(Date.from(now().minusSeconds(5)))
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.issuer("https://op.company.org")
|
||||
.claim("groups", Arrays.asList("group1", "group2", "groups3"))
|
||||
.claim("mail", "cbarton@shield.gov")
|
||||
.claim("name", "Clinton Barton")
|
||||
.claim("id_token_hint", "thisis.aserialized.jwt")
|
||||
.build();
|
||||
|
||||
doAnswer((i) -> {
|
||||
ActionListener<JWTClaimsSet> listener = (ActionListener<JWTClaimsSet>) i.getArguments()[1];
|
||||
listener.onResponse(claims);
|
||||
return null;
|
||||
}).when(authenticator).authenticate(any(OpenIdConnectToken.class), any(ActionListener.class));
|
||||
|
||||
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
|
||||
realm.authenticate(token, future);
|
||||
final AuthenticationResult result = future.get();
|
||||
assertThat(result, notNullValue());
|
||||
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS));
|
||||
assertThat(result.getUser().principal(), equalTo(principal));
|
||||
assertThat(result.getUser().email(), equalTo("cbarton@shield.gov"));
|
||||
assertThat(result.getUser().fullName(), equalTo("Clinton Barton"));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void initializeRealms(Realm... realms) {
|
||||
XPackLicenseState licenseState = mock(XPackLicenseState.class);
|
||||
when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true);
|
||||
|
||||
final List<Realm> realmList = Arrays.asList(realms);
|
||||
for (Realm realm : realms) {
|
||||
realm.initialize(realmList, licenseState);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import com.nimbusds.openid.connect.sdk.Nonce;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.env.TestEnvironment;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
|
||||
import static java.time.Instant.now;
|
||||
import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey;
|
||||
|
||||
public abstract class OpenIdConnectTestCase extends ESTestCase {
|
||||
|
||||
protected static final String REALM_NAME = "oidc-realm";
|
||||
|
||||
protected static Settings.Builder getBasicRealmSettings() {
|
||||
return Settings.builder()
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.org/login")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.org/token")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ENDSESSION_ENDPOINT), "https://op.example.org/logout")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.org/jwks.json")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.elastic.co/cb")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_POST_LOGOUT_REDIRECT_URI), "https://rp.elastic.co/succ_logout")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), randomFrom("code", "id_token"))
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.GROUPS_CLAIM.getClaim()), "groups")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.MAIL_CLAIM.getClaim()), "mail")
|
||||
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getClaim()), "name");
|
||||
}
|
||||
|
||||
protected JWT generateIdToken(String subject, String audience, String issuer) throws Exception {
|
||||
int hashSize = randomFrom(256, 384, 512);
|
||||
int keySize = randomFrom(2048, 4096);
|
||||
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
|
||||
gen.initialize(keySize);
|
||||
KeyPair keyPair = gen.generateKeyPair();
|
||||
JWTClaimsSet idTokenClaims = new JWTClaimsSet.Builder()
|
||||
.jwtID(randomAlphaOfLength(8))
|
||||
.audience(audience)
|
||||
.expirationTime(Date.from(now().plusSeconds(3600)))
|
||||
.issuer(issuer)
|
||||
.issueTime(Date.from(now().minusSeconds(4)))
|
||||
.notBeforeTime(Date.from(now().minusSeconds(4)))
|
||||
.claim("nonce", new Nonce())
|
||||
.subject(subject)
|
||||
.build();
|
||||
|
||||
SignedJWT jwt = new SignedJWT(
|
||||
new JWSHeader.Builder(JWSAlgorithm.parse("RS" + hashSize)).build(),
|
||||
idTokenClaims);
|
||||
jwt.sign(new RSASSASigner(keyPair.getPrivate()));
|
||||
return jwt;
|
||||
}
|
||||
|
||||
protected RealmConfig buildConfig(Settings realmSettings, ThreadContext threadContext) {
|
||||
final Settings settings = Settings.builder()
|
||||
.put("path.home", createTempDir())
|
||||
.put(realmSettings).build();
|
||||
final Environment env = TestEnvironment.newEnvironment(settings);
|
||||
return new RealmConfig(new RealmConfig.RealmIdentifier("oidc", REALM_NAME), settings, env, threadContext);
|
||||
}
|
||||
|
||||
public static void writeJwkSetToFile(Path file) throws IOException {
|
||||
Files.write(file, Arrays.asList(
|
||||
"{\n" +
|
||||
" \"keys\": [\n" +
|
||||
" {\n" +
|
||||
" \"kty\": \"RSA\",\n" +
|
||||
" \"d\": \"lT2V49RNsu0eTroQDqFCiHY-CkPWdKfKAf66sJrWPNpSX8URa6pTCruFQMsb9ZSqQ8eIvqys9I9rq6Wpaxn1aGRahVzxp7nsBPZYw" +
|
||||
"SY09LRzhvAxJwWdwtF-ogrV5-p99W9mhEa0khot3myzzfWNnGzcf1IudqvkqE9zrlUJg-kvA3icbs6HgaZVAevb_mx-bgbtJdnUxyPGwXLyQ7g6hlntQ" +
|
||||
"R_vpzTnK7XFU6fvkrojh7UPJkanKAH0gf3qPrB-Y2gQML7RSlKo-ZfJNHa83G4NRLHKuWTI6dSKJlqmS9zWGmyC3dx5kGjgqD6YgwtWlip8q-U839zxt" +
|
||||
"z25yeslsQ\",\n" +
|
||||
" \"e\": \"AQAB\",\n" +
|
||||
" \"use\": \"sig\",\n" +
|
||||
" \"kid\": \"testkey\",\n" +
|
||||
" \"alg\": \"RS256\",\n" +
|
||||
" \"n\": \"lXBe4UngWJiUfbqbeOvwbH04kYLCpeH4k0o3ngScZDo6ydc_gBDEVwPLQpi8D930aIzr3XHP3RCj0hnpxUun7MNMhWxJZVOd1eg5u" +
|
||||
"uO-nPIhkqr9iGKV5srJk0Dvw0wBaGZuXMBheY2ViNaKTR9EEtjNwU2d2-I5U3YlrnFR6nj-Pn_hWaiCbb_pSFM4w9QpoLDmuwMRanHY_YK7Td2WMICSG" +
|
||||
"P3IRGmbecRZCqgkWVZk396EMoMLNxi8WcErYknyY9r-QeJMruRkr27kgx78L7KZ9uBmu9oKXRQl15ZDYe7Bnt9E5wSdOCV9R9h5VRVUur-_129XkDeAX" +
|
||||
"-6re63_Mw\"\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
"}"
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
Project idpFixtureProject = xpackProject("test:idp-fixture")
|
||||
|
||||
apply plugin: 'elasticsearch.standalone-rest-test'
|
||||
apply plugin: 'elasticsearch.rest-test'
|
||||
apply plugin: 'elasticsearch.test.fixtures'
|
||||
|
||||
dependencies {
|
||||
// "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here
|
||||
testCompile project(path: xpackModule('core'), configuration: 'default')
|
||||
testCompile project(path: xpackModule('core'), configuration: 'testArtifacts')
|
||||
testCompile project(path: xpackModule('security'), configuration: 'testArtifacts')
|
||||
}
|
||||
testFixtures.useFixture ":x-pack:test:idp-fixture"
|
||||
|
||||
String ephemeralPort;
|
||||
task setupPorts {
|
||||
// Don't attempt to get ephemeral ports when Docker is not available
|
||||
onlyIf { idpFixtureProject.postProcessFixture.enabled }
|
||||
dependsOn idpFixtureProject.postProcessFixture
|
||||
doLast {
|
||||
ephemeralPort = idpFixtureProject.postProcessFixture.ext."test.fixtures.oidc-provider.tcp.8080"
|
||||
}
|
||||
}
|
||||
|
||||
integTestCluster {
|
||||
dependsOn setupPorts
|
||||
setting 'xpack.license.self_generated.type', 'trial'
|
||||
setting 'xpack.security.enabled', 'true'
|
||||
setting 'xpack.security.http.ssl.enabled', 'false'
|
||||
setting 'xpack.security.authc.token.enabled', 'true'
|
||||
setting 'xpack.security.authc.realms.file.file.order', '0'
|
||||
setting 'xpack.security.authc.realms.native.native.order', '1'
|
||||
// OpenID Connect Realm 1 configured for authorization grant flow
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.order', '2'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.op.name', 'c2id-op'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.op.issuer', 'http://localhost:8080'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.op.authorization_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id-login"
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.op.token_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/token"
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.op.userinfo_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/userinfo"
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.op.jwkset_path', 'op-jwks.json'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.rp.redirect_uri', 'https://my.fantastic.rp/cb'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.rp.client_id', 'elasticsearch-rp'
|
||||
keystoreSetting 'xpack.security.authc.realms.oidc.c2id.rp.client_secret', 'b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.rp.response_type', 'code'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.claims.principal', 'sub'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.claims.name', 'name'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.claims.mail', 'email'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id.claims.groups', 'groups'
|
||||
// OpenID Connect Realm 2 configured for implicit flow
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.order', '3'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.name', 'c2id-implicit'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.issuer', 'http://localhost:8080'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.authorization_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id-login"
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.token_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/token"
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.userinfo_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/userinfo"
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.jwkset_path', 'op-jwks.json'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.redirect_uri', 'https://my.fantastic.rp/cb'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.client_id', 'elasticsearch-rp'
|
||||
keystoreSetting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.client_secret', 'b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.response_type', 'id_token token'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.principal', 'sub'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.name', 'name'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.mail', 'email'
|
||||
setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.groups', 'groups'
|
||||
setting 'xpack.ml.enabled', 'false'
|
||||
|
||||
extraConfigFile 'op-jwks.json', idpFixtureProject.file("oidc/op-jwks.json")
|
||||
|
||||
setupCommand 'setupTestAdmin',
|
||||
'bin/elasticsearch-users', 'useradd', "test_admin", '-p', 'x-pack-test-password', '-r', "superuser"
|
||||
|
||||
waitCondition = { node, ant ->
|
||||
File tmpFile = new File(node.cwd, 'wait.success')
|
||||
ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",
|
||||
dest: tmpFile.toString(),
|
||||
username: 'test_admin',
|
||||
password: 'x-pack-test-password',
|
||||
ignoreerrors: true,
|
||||
retries: 10)
|
||||
return tmpFile.exists()
|
||||
}
|
||||
}
|
||||
|
||||
thirdPartyAudit.enabled = false
|
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.authc.oidc;
|
||||
|
||||
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.HttpResponse;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpPut;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.apache.http.protocol.BasicHttpContext;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.elasticsearch.client.Request;
|
||||
import org.elasticsearch.client.RequestOptions;
|
||||
import org.elasticsearch.client.Response;
|
||||
import org.elasticsearch.common.CheckedFunction;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.test.rest.ESRestTestCase;
|
||||
import org.elasticsearch.xpack.core.common.socket.SocketAccess;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap;
|
||||
import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
|
||||
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 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/";
|
||||
|
||||
@Before
|
||||
public void setupUserAndRoles() throws IOException {
|
||||
setFacilitatorUser();
|
||||
setRoleMappings();
|
||||
}
|
||||
|
||||
/**
|
||||
* C2id server only supports dynamic registration, so we can't pre-seed it's config with our client data. Execute only once
|
||||
*/
|
||||
@BeforeClass
|
||||
public static void registerClient() throws Exception {
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
HttpPost httpPost = new HttpPost(REGISTRATION_URL);
|
||||
final BasicHttpContext context = new BasicHttpContext();
|
||||
String json = "{" +
|
||||
"\"grant_types\": [\"implicit\", \"authorization_code\"]," +
|
||||
"\"response_types\": [\"code\", \"token id_token\"]," +
|
||||
"\"preferred_client_id\":\"elasticsearch-rp\"," +
|
||||
"\"preferred_client_secret\":\"b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2\"," +
|
||||
"\"redirect_uris\": [\"https://my.fantastic.rp/cb\"]" +
|
||||
"}";
|
||||
httpPost.setEntity(new StringEntity(json, 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));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Settings restAdminSettings() {
|
||||
String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray()));
|
||||
return Settings.builder()
|
||||
.put(ThreadContext.PREFIX + ".Authorization", token)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String authenticateAtOP(URI opAuthUri) throws Exception {
|
||||
// C2ID doesn't have a non JS login page :/, so use their API directly
|
||||
// see https://connect2id.com/products/server/docs/guides/login-page
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
final BasicHttpContext context = new BasicHttpContext();
|
||||
// Initiate the authentication process
|
||||
HttpPost httpPost = new HttpPost(LOGIN_API + "initAuthRequest");
|
||||
String initJson = "{" +
|
||||
" \"qs\":\"" + opAuthUri.getRawQuery() + "\"" +
|
||||
"}";
|
||||
configureJsonRequest(httpPost, initJson);
|
||||
JSONObject initResponse = execute(httpClient, httpPost, context, response -> {
|
||||
assertHttpOk(response.getStatusLine());
|
||||
return parseJsonResponse(response);
|
||||
});
|
||||
assertThat(initResponse.getAsString("type"), equalTo("auth"));
|
||||
final String sid = initResponse.getAsString("sid");
|
||||
// Actually authenticate the user with ldapAuth
|
||||
HttpPost loginHttpPost = new HttpPost(LOGIN_API + "authenticateSubject?cacheBuster=" + randomAlphaOfLength(8));
|
||||
String loginJson = "{" +
|
||||
"\"username\":\"alice\"," +
|
||||
"\"password\":\"secret\"" +
|
||||
"}";
|
||||
configureJsonRequest(loginHttpPost, loginJson);
|
||||
JSONObject loginJsonResponse = execute(httpClient, loginHttpPost, context, response -> {
|
||||
assertHttpOk(response.getStatusLine());
|
||||
return parseJsonResponse(response);
|
||||
});
|
||||
// Get the consent screen
|
||||
HttpPut consentFetchHttpPut =
|
||||
new HttpPut(LOGIN_API + "updateAuthRequest" + "/" + sid + "?cacheBuster=" + randomAlphaOfLength(8));
|
||||
String consentFetchJson = "{" +
|
||||
"\"sub\": \"" + loginJsonResponse.getAsString("id") + "\"," +
|
||||
"\"acr\": \"http://loa.c2id.com/basic\"," +
|
||||
"\"amr\": [\"pwd\"]," +
|
||||
"\"data\": {" +
|
||||
"\"email\": \"" + loginJsonResponse.getAsString("email") + "\"," +
|
||||
"\"name\": \"" + loginJsonResponse.getAsString("name") + "\"" +
|
||||
"}" +
|
||||
"}";
|
||||
configureJsonRequest(consentFetchHttpPut, consentFetchJson);
|
||||
JSONObject consentFetchResponse = execute(httpClient, consentFetchHttpPut, context, response -> {
|
||||
assertHttpOk(response.getStatusLine());
|
||||
return parseJsonResponse(response);
|
||||
});
|
||||
if (consentFetchResponse.getAsString("type").equals("consent")) {
|
||||
// If needed, submit the consent
|
||||
HttpPut consentHttpPut =
|
||||
new HttpPut(LOGIN_API + "updateAuthRequest" + "/" + sid + "?cacheBuster=" + randomAlphaOfLength(8));
|
||||
String consentJson = "{" +
|
||||
"\"claims\":[\"name\", \"email\"]," +
|
||||
"\"scope\":[\"openid\"]" +
|
||||
"}";
|
||||
configureJsonRequest(consentHttpPut, consentJson);
|
||||
JSONObject jsonConsentResponse = execute(httpClient, consentHttpPut, context, response -> {
|
||||
assertHttpOk(response.getStatusLine());
|
||||
return parseJsonResponse(response);
|
||||
});
|
||||
assertThat(jsonConsentResponse.getAsString("type"), equalTo("response"));
|
||||
JSONObject parameters = (JSONObject) jsonConsentResponse.get("parameters");
|
||||
return parameters.getAsString("uri");
|
||||
} else if (consentFetchResponse.getAsString("type").equals("response")) {
|
||||
JSONObject parameters = (JSONObject) consentFetchResponse.get("parameters");
|
||||
return parameters.getAsString("uri");
|
||||
} else {
|
||||
fail("Received an invalid response from the OP");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getEphemeralPortFromProperty(String port) {
|
||||
String key = "test.fixtures.oidc-provider.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 {
|
||||
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));
|
||||
}
|
||||
|
||||
private <T> T execute(CloseableHttpClient client, HttpEntityEnclosingRequestBase request,
|
||||
HttpContext context, CheckedFunction<HttpResponse, T, Exception> body)
|
||||
throws Exception {
|
||||
final int timeout = (int) TimeValue.timeValueSeconds(90).millis();
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectionRequestTimeout(timeout)
|
||||
.setConnectTimeout(timeout)
|
||||
.setSocketTimeout(timeout)
|
||||
.build();
|
||||
request.setConfig(requestConfig);
|
||||
logger.info("Execute HTTP " + request.getMethod() + " " + request.getURI() +
|
||||
" with payload " + EntityUtils.toString(request.getEntity()));
|
||||
try (CloseableHttpResponse response = SocketAccess.doPrivileged(() -> client.execute(request, context))) {
|
||||
return body.apply(response);
|
||||
} catch (Exception e) {
|
||||
logger.warn(new ParameterizedMessage("HTTP Request [{}] failed", request.getURI()), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject parseJsonResponse(HttpResponse response) throws Exception {
|
||||
JSONParser parser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE);
|
||||
String entity = EntityUtils.toString(response.getEntity());
|
||||
logger.info("Response entity as string: " + entity);
|
||||
return (JSONObject) parser.parse(entity);
|
||||
}
|
||||
|
||||
private void configureJsonRequest(HttpEntityEnclosingRequestBase request, String jsonBody) {
|
||||
StringEntity entity = new StringEntity(jsonBody, ContentType.APPLICATION_JSON);
|
||||
request.setEntity(entity);
|
||||
request.setHeader("Accept", "application/json");
|
||||
request.setHeader("Content-type", "application/json");
|
||||
}
|
||||
|
||||
public void testAuthenticateWithCodeFlow() throws Exception {
|
||||
final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME);
|
||||
final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri());
|
||||
Tuple<String, String> tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(),
|
||||
prepareAuthResponse.getNonce());
|
||||
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());
|
||||
verifyElasticsearchAccessTokenForImplicitFlow(tokens.v1());
|
||||
}
|
||||
|
||||
private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throws IOException {
|
||||
final Map<String, Object> map = callAuthenticateApiUsingAccessToken(accessToken);
|
||||
logger.info("Authentication with token Response: " + map);
|
||||
assertThat(map.get("username"), equalTo("alice"));
|
||||
assertThat((List<?>) map.get("roles"), containsInAnyOrder("kibana_user", "auditor"));
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
private void verifyElasticsearchAccessTokenForImplicitFlow(String accessToken) throws IOException {
|
||||
final Map<String, Object> map = callAuthenticateApiUsingAccessToken(accessToken);
|
||||
logger.info("Authentication with token Response: " + map);
|
||||
assertThat(map.get("username"), equalTo("alice"));
|
||||
assertThat((List<?>) map.get("roles"), containsInAnyOrder("limited_user", "auditor"));
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
|
||||
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");
|
||||
return new PrepareAuthResponse(new URI(authUri), state, nonce);
|
||||
}
|
||||
|
||||
private Tuple<String, String> completeAuthentication(String redirectUri, String state, String nonce) throws Exception {
|
||||
final Map<String, String> body = new HashMap<>();
|
||||
body.put("redirect_uri", redirectUri);
|
||||
body.put("state", state);
|
||||
body.put("nonce", nonce);
|
||||
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 new Tuple(responseBody.get("access_token"), responseBody.get("refresh_token"));
|
||||
}
|
||||
|
||||
private Request buildRequest(String method, String endpoint, Map<String, ?> body, Header... headers) throws IOException {
|
||||
Request request = new Request(method, endpoint);
|
||||
XContentBuilder builder = XContentFactory.jsonBuilder().map(body);
|
||||
if (body != null) {
|
||||
request.setJsonEntity(BytesReference.bytes(builder).utf8ToString());
|
||||
}
|
||||
final RequestOptions.Builder options = request.getOptions().toBuilder();
|
||||
for (Header header : headers) {
|
||||
options.addHeader(header.getName(), header.getValue());
|
||||
}
|
||||
request.setOptions(options);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static BasicHeader facilitatorAuth() {
|
||||
final String auth =
|
||||
UsernamePasswordToken.basicAuthHeaderValue("facilitator", new SecureString(FACILITATOR_PASSWORD.toCharArray()));
|
||||
return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, auth);
|
||||
}
|
||||
|
||||
private Map<String, Object> parseResponseAsMap(HttpEntity entity) throws IOException {
|
||||
return convertToMap(XContentType.JSON.xContent(), entity.getContent(), false);
|
||||
}
|
||||
|
||||
|
||||
private void assertHttpOk(StatusLine status) {
|
||||
assertThat("Unexpected HTTP Response status: " + status, status.getStatusCode(), Matchers.equalTo(200));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 setRoleMappings() throws IOException {
|
||||
Request createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_kibana");
|
||||
createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"kibana_user\"]," +
|
||||
"\"enabled\": true," +
|
||||
"\"rules\": {" +
|
||||
"\"field\": { \"realm.name\": \"" + REALM_NAME + "\"}" +
|
||||
"}" +
|
||||
"}");
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Simple POJO encapsulating a response to calling /_security/oidc/prepare
|
||||
*/
|
||||
class PrepareAuthResponse {
|
||||
private URI authUri;
|
||||
private String state;
|
||||
private String nonce;
|
||||
|
||||
PrepareAuthResponse(URI authUri, String state, String nonce) {
|
||||
this.authUri = authUri;
|
||||
this.state = state;
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
URI getAuthUri() {
|
||||
return authUri;
|
||||
}
|
||||
|
||||
String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,3 +38,10 @@ services:
|
|||
- ./idp/shibboleth-idp/conf:/opt/shibboleth-idp/conf
|
||||
- ./idp/shibboleth-idp/credentials:/opt/shibboleth-idp/credentials
|
||||
- ./idp/shib-jetty-base/start.d/ssl.ini:/opt/shib-jetty-base/start.d/ssl.ini
|
||||
|
||||
oidc-provider:
|
||||
image: "c2id/c2id-server:7.8"
|
||||
ports:
|
||||
- "8080"
|
||||
volumes:
|
||||
- ./oidc/override.properties:/etc/c2id/override.properties
|
|
@ -0,0 +1 @@
|
|||
{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"CXup","n":"hrwD-lc-IwzwidCANmy4qsiZk11yp9kHykOuP0yOnwi36VomYTQVEzZXgh2sDJpGgAutdQudgwLoV8tVSsTG9SQHgJjH9Pd_9V4Ab6PANyZNG6DSeiq1QfiFlEP6Obt0JbRB3W7X2vkxOVaNoWrYskZodxU2V0ogeVL_LkcCGAyNu2jdx3j0DjJatNVk7ystNxb9RfHhJGgpiIkO5S3QiSIVhbBKaJHcZHPF1vq9g0JMGuUCI-OTSVg6XBkTLEGw1C_R73WD_oVEBfdXbXnLukoLHBS11p3OxU7f4rfxA_f_72_UwmWGJnsqS3iahbms3FkvqoL9x_Vj3GhuJSf97Q"},{"kty":"EC","use":"sig","crv":"P-256","kid":"yGvt","x":"pvgdqM3RCshljmuCF1D2Ez1w5ei5k7-bpimWLPNeEHI","y":"JSmUhbUTqiFclVLEdw6dz038F7Whw4URobjXbAReDuM"},{"kty":"EC","use":"sig","crv":"P-384","kid":"9nHY","x":"JPKhjhE0Bj579Mgj3Cn3ERGA8fKVYoGOaV9BPKhtnEobphf8w4GSeigMesL-038W","y":"UbJa1QRX7fo9LxSlh7FOH5ABT5lEtiQeQUcX9BW0bpJFlEVGqwec80tYLdOIl59M"},{"kty":"EC","use":"sig","crv":"P-521","kid":"tVzS","x":"AZgkRHlIyNQJlPIwTWdHqouw41k9dS3GJO04BDEnJnd_Dd1owlCn9SMXA-JuXINn4slwbG4wcECbctXb2cvdGtmn","y":"AdBC6N9lpupzfzcIY3JLIuc8y8MnzV-ItmzHQcC5lYWMTbuM9NU_FlvINeVo8g6i4YZms2xFB-B0VVdaoF9kUswC"}]}
|
|
@ -0,0 +1,4 @@
|
|||
op.issuer=http://localhost:8080
|
||||
op.authz.endpoint=http://localhost:8080/c2id-login/
|
||||
op.reg.apiAccessTokenSHA256=d1c4fa70d9ee708d13cfa01daa0e060a05a2075a53c5cc1ad79e460e96ab5363
|
||||
jose.jwkSer=RnVsbCBrZXk6CnsKICAia2V5cyI6IFsKICAgIHsKICAgICAgInAiOiAiLXhhN2d2aW5tY3N3QXU3Vm1mV2loZ2o3U3gzUzhmd2dFSTdMZEVveW5FU1RzcElaeUY5aHc0NVhQZmI5VHlpbzZsOHZTS0F5RmU4T2lOalpkNE1Ra0ttYlJzTmxxR1Y5VlBoWF84UG1JSm5mcGVhb3E5YnZfU0k1blZHUl9zYUUzZE9sTEE2VWpaS0lsRVBNb0ZuRlZCMUFaUU9qQlhRRzZPTDg2eDZ2NHMwIiwKICAgICAgImt0eSI6ICJSU0EiLAogICAgICAicSI6ICJ2Q3pDQUlpdHV0MGx1V0djQloyLUFabURLc1RxNkkxcUp0RmlEYkIyZFBNQVlBNldOWTdaWEZoVWxsSjJrT2ZELWdlYjlkYkN2ODBxNEwyajVZSjZoOTBUc1NRWWVHRlljN1lZMGdCMU5VR3l5cXctb29QN0EtYlJmMGI3b3I4ajZJb0hzQTZKa2JranN6c3otbkJ2U2RmUURlZkRNSVc3Ni1ZWjN0c2hsY2MiLAogICAgICAiZCI6ICJtbFBOcm1zVVM5UmJtX1I5SElyeHdmeFYzZnJ2QzlaQktFZzRzc1ZZaThfY09lSjV2U1hyQV9laEtwa2g4QVhYaUdWUGpQbVlyd29xQzFVUksxUkZmLVg0dG10emV2OUVHaU12Z0JCaEF5RkdTSUd0VUNla2x4Q2dhb3BpMXdZSU1Bd0M0STZwMUtaZURxTVNCWVZGeHA5ZWlJZ2pwb05JbV9lR3hXUUs5VHNnYmk5T3lyc1VqaE9KLVczN2JVMEJWUU56UXpxODhCcGxmNzM3VmV1dy1FeDZaMk1iWXR3SWdfZ0JVb0JEZ0NrZkhoOVE4MElYcEZRV0x1RzgwenFrdkVwTHZ0RWxLbDRvQ3BHVnBjcmFUOFNsOGpYc3FDT1k0dnVRT19LRVUzS2VPNUNJbHd4eEhJYXZjQTE5cHFpSWJ5cm1LbThxS0ZEWHluUFJMSGFNZ1EiLAogICAgICAiZSI6ICJBUUFCIiwKICAgICAgImtpZCI6ICJyc2EzODRfMjA0OCIsCiAgICAgICJxaSI6ICJzMldTamVrVDl3S2JPbk9neGNoaDJPY3VubzE2Y20wS281Z3hoUWJTdVMyMldfUjJBR2ZVdkRieGF0cTRLakQ3THo3X1k2TjdTUkwzUVpudVhoZ1djeXgyNGhrUGppQUZLNmlkYVZKQzJqQmgycEZTUDVTNXZxZ0lsME12eWY4NjlwdkN4S0NzaGRKMGdlRWhveE93VkRPYXJqdTl2Zm9IQV90LWJoRlZrUnciLAogICAgICAiZHAiOiAiQlJhQTFqYVRydG9mTHZBSUJBYW1OSEVhSm51RU9zTVJJMFRCZXFuR1BNUm0tY2RjSG1OUVo5WUtqb2JpdXlmbnhGZ0piVDlSeElBRG0ySkpoZEp5RTN4Y1dTSzhmSjBSM1Jick1aT1dwako0QmJTVzFtU1VtRnlKTGxib3puRFhZR2RaZ1hzS0o1UkFrRUNQZFBCY3YwZVlkbk9NYWhfZndfaFZoNjRuZ2tFIiwKICAgICAgImFsZyI6ICJSU0EzODQiLAogICAgICAiZHEiOiAiUFJoVERKVlR3cDNXaDZfWFZrTjIwMUlpTWhxcElrUDN1UTYyUlRlTDNrQ2ZXSkNqMkZPLTRxcVRIQk0tQjZJWUVPLXpoVWZyQnhiMzJ1djNjS2JDWGFZN3BJSFJxQlFEQWQ2WGhHYzlwc0xqNThXd3VGY2RncERJYUFpRjNyc3NUMjJ4UFVvYkJFTVdBalV3bFJrNEtNTjItMnpLQk5FR3lIcDIzOUpKdnpVIiwKICAgICAgIm4iOiAidUpDWDVDbEZpM0JnTXBvOWhRSVZ2SDh0Vi1jLTVFdG5OeUZxVm91R3NlNWwyUG92MWJGb0tsRllsU25YTzNWUE9KRWR3azNDdl9VT0UtQzlqZERYRHpvS3Z4RURaTVM1TDZWMFpIVEJoNndIOV9iN3JHSlBxLV9RdlNkejczSzZxbHpGaUtQamRvdTF6VlFYTmZfblBZbnRnQkdNRUtBc1pRNGp0cWJCdE5lV0h0MF9UM001cEktTV9KNGVlRWpCTW95TkZuU2ExTEZDVmZRNl9YVnpjelp1TlRGMlh6UmdRWkFmcmJGRXZ6eXR1TzVMZTNTTXFrUUFJeDhFQmkwYXVlRUNqNEQ4cDNVNXFVRG92NEF2VnRJbUZlbFJvb1pBMHJtVW1KRHJ4WExrVkhuVUpzaUF6ZW9TLTNBSnV1bHJkMGpuNjJ5VjZHV2dFWklZMVNlZVd3IgogICAgfQogIF0KfQo
|
Loading…
Reference in New Issue