elastic / elasticsearch

Free and Open Source, Distributed, RESTful Search Engine
https://www.elastic.co/products/elasticsearch
Other
1.27k stars 24.86k forks source link

bug on OpenID connect: java.lang.ClassCastException when the users claims do contain JSON #55658

Open Augustin-FL opened 4 years ago

Augustin-FL commented 4 years ago

Elasticsearch version (bin/elasticsearch --version): 7.6.1

Plugins installed: []

JVM version (java -version): 1.8.0_242

OS version (uname -a if on a Unix-like system): RHEL 7.7 (exact version 3.10.0-1062.12.1.el7.x86_64)

Description of the problem including expected versus actual behavior: When using OpenID connect method, some claims can contain JSON objects in JSON lists. This is especially desirable in real world applications, when having rights to multiple ressources (eg, multiple elastic instances). While it isn't strictly part of the OpenID standard, many companies are using this format to transmit users permissions.

For example, a claim can have the following format : "user_authorization":[{"resource":"myElasticInstance","permissions":[{"name":"SUPERUSER","constraints":[]}]}],

ElasticSearch does not handle the JSON object {"ressource": ...}, and thus throw an error.

Current behavior: A java.lang.ClassCastException is trigered

Expected behavior: The "permission" field is mapped to the roles of the user (via role mapping or automatically)

The logs (see below) should provide a relevant explanation of the issue

Steps to reproduce:

  1. Install an OpenAM instance (or any OpenID server). Store the permissions in the JSON format.
  2. Configure a basic OpenID authentication in elasticsearch.yml :
    logger.org.elasticsearch.xpack.security.authc.oidc: "TRACE"
    xpack.security.authc.realms.oidc.oidc1:
      rp.client_id: "11111111-1111-1111-1111-111111111111"
      rp.response_type: code
      rp.requested_scopes: ["openid","profile","myElasticInstance.KibanaAccess"]
      rp.redirect_uri: "https://kibanaURL/api/security/oidc/callback"
      op.issuer: "https://opURL/oauth2"
      op.authorization_endpoint: "https://opURL/oauth2/authorize"
      ssl.certificate_authorities: ["sslkeys/ca-bundle.crt"]
      op.token_endpoint: "https://opURL/oauth2/access_token"
      op.jwkset_path: "https://opURL/oauth2/jwk_uri"
      op.userinfo_endpoint: "https://opURL/oauth2/userinfo"
      op.endsession_endpoint: "https://opURL/oauth2/logout"
      rp.post_logout_redirect_uri: "https://kibanaURL/logged_out"
      claims.principal: "sub"
      claims.groups: "user_authorization"
      claims.name: "name"
      claims.mail: "mail"
    1. Try to get authentiated on kibana.
    2. View your logs and see a java.lang.ClassCastException

The problem seems to come from this part of the code. Not sure what's the best way to fix that through...

Provide logs (if relevant):

2020-04-23T11:26:32,779][TRACE][o.e.x.s.a.o.OpenIdConnectAuthenticator] [elastic-node-0] Successfully retrieved user information: [
{
"sub":"myname@mycompany.com",
"mail":"myname@mycompany.com",
"auth_level":"L2",
"origin_network":"LAN",
"rc_local_sigle":"MyWonderful\/And\/Amazing\/Department",
"first_name":"MyFirstName",
"preferred_language":"EN",
"last_name":"myLastName", 
"user_authorization":[{"resource":"myElasticInstance","permissions":[{"name":"SUPERUSER","constraints":[]}]}],
"name":"MyFirstName MyLastName",
"family_name":"MyLastName"
}
]
[2020-04-23T11:26:32,779][WARN][o.e.x.s.a.AuthenticationService] [elastic-node-0] An error occurred while attempting to authenticate [<OIDC Token>] against realm [oidc1]
java.lang.ClassCastException: class net.minidev.json.JSONObject cannot be cast to class java.lang.String (net.minidev.json.JSONObject is in unnamed module of loader java.net.FactoryURLClassLoader @7e4d2287; java.lang.String is in module java.base of loader 'bootstrap')
        at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:176) ~[?:?]
        at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1621) ~[?:?]
        at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) ~[?:?]
        at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) ~[?:?]
        at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) ~[?:?]
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[?:?]
        at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) ~[?:?]
        at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm$ClaimParser.lambda$forSetting$2(OpenIdConnectRealm.java:480) ~[?:?]
        at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm$ClaimParser.getClaimValues(OpenIdConnectRealm.java:403) ~[?:?]
        at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm.buildUserFromClaims(OpenIdConnectRealm.java:228) ~[?:?]
        at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm.lambda$authenticate$0(OpenIdConnectRealm.java:168) ~[?:?]
        at org.elasticsearch.action.ActionListener$1.onResponse(ActionListener.java:63) [elasticsearch-7.6.1.jar:7.6.1]
        at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator.handleUserinfoResponse(OpenIdConnectAuthenticator.java:411) [x-pack-security-7.6.1.jar:7.6.1]
        at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator.access$700(OpenIdConnectAuthenticator.java:122) [x-pack-security-7.6.1.jar:7.6.1]
        at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator$1.completed(OpenIdConnectAuthenticator.java:364) [x-pack-security-7.6.1.jar:7.6.1]
        at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator$1.completed(OpenIdConnectAuthenticator.java:361) [x-pack-security-7.6.1.jar:7.6.1]
        at org.apache.http.concurrent.BasicFuture.completed(BasicFuture.java:122) [httpcore-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.client.DefaultClientExchangeHandlerImpl.responseCompleted(DefaultClientExchangeHandlerImpl.java:181) [httpasyncclient-4.1.4.jar:4.1.4]
        at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.processResponse(HttpAsyncRequestExecutor.java:448) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.inputReady(HttpAsyncRequestExecutor.java:338) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.DefaultNHttpClientConnection.consumeInput(DefaultNHttpClientConnection.java:265) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:81) [httpasyncclient-4.1.4.jar:4.1.4]
        at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:39) [httpasyncclient-4.1.4.jar:4.1.4]
        at org.apache.http.impl.nio.reactor.AbstractIODispatch.inputReady(AbstractIODispatch.java:121) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.reactor.BaseIOReactor.readable(BaseIOReactor.java:162) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvent(AbstractIOReactor.java:337) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvents(AbstractIOReactor.java:315) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:276) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:104) [httpcore-nio-4.4.12.jar:4.4.12]
        at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:591) [httpcore-nio-4.4.12.jar:4.4.12]
        at java.lang.Thread.run(Thread.java:830) [?:?]
elasticmachine commented 4 years ago

Pinging @elastic/es-security (:Security/Authentication)

jkakavas commented 4 years ago

Thanks for the feedback @Augustin-FL, we appreciate the real world use case and insights.

The way we deal with the values we map from claims.groups means that there is an implicit assumption that this is a list of strings that will become the list of groups that the elasticsearch user has (and will be used for role mapping etc. )

We could potentially look into handling JSON Objects for this but I believe it should still be up to the administrator to configure the claims mapping accordingly as there is no specific way to deduce what should be consumed from a JSON object.

i.e. if you configure

      claims.groups: "user_authorization"

and user_authorization is

"user_authorization":[{"resource":"myElasticInstance","permissions":[{"name":"SUPERUSER","constraints":[]}]}],

what exactly in that object defines the groups that the elasticsearch user should get? Since this is not part of any standard, it should fall to the administrator to correctly configure. I'm thinking that the parsing of the JSON Object should not be guessed or deduced, but explicitly configured. We could allow that the claims.groups can get a value that is a pointer to a nested JSON field that itself needs to be a string or array ( as is the current constraint ) . That would allow your OP to use JSON objects and you could configure your realm with something like

      claims.groups: "user_authorization.resource:myElasticInstance.permissions.name"

I'm not certain how well this could be implemented in a generic way that is usable/configurable and could fit a generic use case, but we could take a look at some point.

Given that this is the first time such a requirement comes up, I don't think we can prioritize any work towards it now, but we can keep this issue as reference! In the meantime, could you also open an issue with ForgeRock and see if it is possible to configure this in the OP side so as to release the name of the permissions for the resource (RP) that made the authorization request as a separate claim ?