Elastic Cloud Integration with FusionAuth

Hi Everyone,

I am trying to using FusionAuth (https://fusionauth.io/) as IdP (OpenID or SAML) to my elastic stack running on Elastic Cloud.

Here is my elasticsearch.yaml configuration:

xpack: 
  security: 
    authc: 
      realms: 
        oidc: 
          oidc1:
            claims.principal: sub
            op.authorization_endpoint: "https://xxx/oauth2/authorize"
            op.issuer: "https://xxx/"
            op.jwkset_path: "https://xxx/.well-known/jwks.json"
            op.token_endpoint: "https://xxx/oauth2/token"
            op.userinfo_endpoint: "https://xxx/oauth2/userinfo"
            order: 2
            rp.client_id: "xxxxxxxx"
            rp.redirect_uri: "https://xxx:9243/api/security/v1/oidc"
            rp.response_type: code

and got this error on "trace" mode on the elastic:

[instance-0000000000] Authentication to realm oidc1 failed - Failed to authenticate user with OpenID Connect (Caused by ElasticsearchSecurityException[Failed to parse or validate the ID Token]; nested: BadJOSEException[Signed JWT rejected: Another algorithm expected, or no matching key(s) found];)
|Feb 5, 2021, 3:02:55 AM UTC|WARN|i0@ap-southeast-1c|[instance-0000000000] Authentication to realm oidc1 failed - Failed to authenticate user with OpenID Connect (Caused by ElasticsearchSecurityException[Failed to parse or validate the ID Token]; nested: BadJOSEException[Signed JWT rejected: Another algorithm expected, or no matching key(s) found];)|
|Feb 5, 2021, 3:02:55 AM UTC|DEBUG|i0@ap-southeast-1c|[instance-0000000000] Failed to consume the OpenIdConnectToken org.elasticsearch.ElasticsearchSecurityException: Failed to parse or validate the ID Token at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator.getUserClaims(OpenIdConnectAuthenticator.java:264) [x-pack-security-7.10.2.jar:7.10.2] at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator.lambda$getUserClaims$1(OpenIdConnectAuthenticator.java:258) [x-pack-security-7.10.2.jar:7.10.2] at org.elasticsearch.action.ActionListener$1.onResponse(ActionListener.java:63) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.ListenableFuture$1.doRun(ListenableFuture.java:112) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.EsExecutors$DirectExecutorService.execute(EsExecutors.java:224) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.ListenableFuture.notifyListener(ListenableFuture.java:106) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.ListenableFuture.lambda$done$0(ListenableFuture.java:98) [elasticsearch-7.10.2.jar:7.10.2] at java.util.ArrayList.forEach(ArrayList.java:1511) [?:?] at org.elasticsearch.common.util.concurrent.ListenableFuture.done(ListenableFuture.java:98) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.BaseFuture.set(BaseFuture.java:144) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.ListenableFuture.onResponse(ListenableFuture.java:127) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator$ReloadableJWKSource$1.completed(OpenIdConnectAuthenticator.java:828) [x-pack-security-7.10.2.jar:7.10.2] at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator$ReloadableJWKSource$1.completed(OpenIdConnectAuthenticator.java:820) [x-pack-security-7.10.2.jar:7.10.2] at org.apache.http.concurrent.BasicFuture.completed(BasicFuture.java:122) [httpcore-4.4.12.jar:4.4.12] at|

And this is what I get on the browser:

{"statusCode":401,"error":"Unauthorized","message":"[security_exception] unable to authenticate user [<OIDC Token>] for action [cluster:admin/xpack/security/oidc/authenticate], with { header={ WWW-Authenticate={ 0=\"Basic realm=\\\"security\\\" charset=\\\"UTF-8\\\"\" & 1=\"Bearer realm=\\\"security\\\"\" & 2=\"ApiKey\" } } }"}

Is there any missing part or common mistake for this setup?

References:

It looks like we cannot valdidate the signature of the ID token. As the message says this means that either the OP is not configured correctly and it uses a key to produce the signature that is not the one it refers to in "https://xxx/.well-known/jwks.json" or that it uses a different algorithm than the one that Elasticsearch is configured with ( by default RS256 ) .
Could you post here the contents of
"https://xxx/.well-known/openid-configuration ? There shouldn't be anything private there, this metadata is supposed to be public.

Two things that might be worth looking at:

  • you can change the issuer claim by going to "Tenants -> Your Tenant -> General" and changing the "Issuer" value. That defaults to "fusionauth.io". I've had some systems need the issuer to match the fusionauth server (where the .well-known links live)
  • by default FusionAuth uses the HMAC256 algorithm as the signing algo for JWTs. Since this is symmetric, it is not published at the JWKs endpoint. This can be changed easily, but you need to create or import an RSA key and then change the signing key setting in either the Tenant or Application settings.

Hope that helps!

@ikakavas here is the content of .well-known/openid-configuration

{
  "authorization_endpoint" : "https://xxx.ngrok.io/oauth2/authorize",
  "backchannel_logout_supported" : false,
  "claims_supported" : [
    "applicationId",
    "at_hash",
    "aud",
    "authenticationType",
    "birthdate",
    "c_hash",
    "email",
    "email_verified",
    "exp",
    "family_name",
    "given_name",
    "iat",
    "iss",
    "jti",
    "middle_name",
    "name",
    "nbf",
    "nonce",
    "phone_number",
    "picture",
    "preferred_username",
    "roles",
    "sub"
  ],
  "device_authorization_endpoint" : "https://xxx.ngrok.io/oauth2/device_authorize",
  "end_session_endpoint" : "https://xxx.ngrok.io/oauth2/logout",
  "frontchannel_logout_supported" : true,
  "grant_types_supported" : [
    "authorization_code",
    "password",
    "implicit",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "id_token_signing_alg_values_supported" : [
    "ES256",
    "ES384",
    "ES512",
    "HS256",
    "HS384",
    "HS512",
    "RS256",
    "RS384",
    "RS512"
  ],
  "issuer" : "https://xxx.ngrok.io",
  "jwks_uri" : "https://xxx.ngrok.io/.well-known/jwks.json",
  "response_modes_supported" : [
    "form_post",
    "fragment",
    "query"
  ],
  "response_types_supported" : [
    "code",
    "id_token",
    "token id_token"
  ],
  "scopes_supported" : [
    "openid",
    "offline_access"
  ],
  "subject_types_supported" : [
    "public"
  ],
  "token_endpoint" : "https://xxx.ngrok.io/oauth2/token",
  "token_endpoint_auth_methods_supported" : [
    "client_secret_basic",
    "client_secret_post",
    "none"
  ],
  "userinfo_endpoint" : "https://xxx.ngrok.io/oauth2/userinfo",
  "userinfo_signing_alg_values_supported" : [
    "ES256",
    "ES384",
    "ES512",
    "RS256",
    "RS384",
    "RS512",
    "HS256",
    "HS384",
    "HS512"
  ]
}

Also on the FusionAuth JWT configuration, I have adjusted the algorithm to RS256, like @Dan_Moore said, the default is HMAC256.

@Dan_Moore

Yes, the issuer on the tenant setting already changed to my URL. In this case, I'm running the FusionAuth on my local (docker-compose) and make it public by ngrok endpoint.

And for the algorithm, you mean by adjusting the key on JWT configuration right? I am choose auto generate it when creating the application, so yeah I think it's already RS256 not the default.

And still got the error

[instance-0000000001] Authentication to realm oidc1 failed - Failed to authenticate user with OpenID Connect (Caused by ElasticsearchSecurityException[Failed to parse or validate the ID Token]; nested: BadJWTException[Unexpected JWT issuer: https://xxxx.ngrok.io];)
[instance-0000000001] Failed to consume the OpenIdConnectToken org.elasticsearch.ElasticsearchSecurityException: Failed to parse or validate the ID Token at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator.getUserClaims(OpenIdConnectAuthenticator.java:264) [x-pack-security-7.10.2.jar:7.10.2] at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator.lambda$getUserClaims$1(OpenIdConnectAuthenticator.java:258) [x-pack-security-7.10.2.jar:7.10.2] at org.elasticsearch.action.ActionListener$1.onResponse(ActionListener.java:63) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.ListenableFuture$1.doRun(ListenableFuture.java:112) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.EsExecutors$DirectExecutorService.execute(EsExecutors.java:224) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.ListenableFuture.notifyListener(ListenableFuture.java:106) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.ListenableFuture.lambda$done$0(ListenableFuture.java:98) [elasticsearch-7.10.2.jar:7.10.2] at java.util.ArrayList.forEach(ArrayList.java:1511) [?:?] at org.elasticsearch.common.util.concurrent.ListenableFuture.done(ListenableFuture.java:98) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.BaseFuture.set(BaseFuture.java:144) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.common.util.concurrent.ListenableFuture.onResponse(ListenableFuture.java:127) [elasticsearch-7.10.2.jar:7.10.2] at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator$ReloadableJWKSource$1.completed(OpenIdConnectAuthenticator.java:828) [x-pack-security-7.10.2.jar:7.10.2] at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator$ReloadableJWKSource$1.completed(OpenIdConnectAuthenticator.java:820) [x-pack-security-7.10.2.jar:7.10.2] 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:832) [?:?] Caused by: com.nimbusds.jwt.proc.BadJWTException: Unexpected JWT issuer: https://6eb54ccafa30.ngrok.io at com.nimbusds.openid.connect.sdk.validators.IDTokenClaimsVerifier.verify(IDTokenClaimsVerifier.java:173) ~[?:?] at com.nimbusds.jwt.proc.DefaultJWTProcessor.verifyClaims(DefaultJWTProcessor.java:295) ~[?:?] at com.nimbusds.jwt.proc.DefaultJWTProcessor.process(DefaultJWTProcessor.java:400) ~[?:?] at com.nimbusds.openid.connect.sdk.validators.IDTokenValidator.validate(IDTokenValidator.java:288) ~[?:?] at com.nimbusds.openid.connect.sdk.validators.IDTokenValidator.validate(IDTokenValidator.java:224) ~[?:?] at org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectAuthenticator.getUserClaims(OpenIdConnectAuthenticator.java:231) ~[?:?] ... 28 more

What does your JWT look like, @vexana ? It sure looks like the issuer in the JWT doesn't match what Elastic is expecting. But it also looks like your settings are correct. Please share the JWT you are generating (removing any sensitive information, of course).

I wonder if there are some claims you need for elastic that you need to set in a JWT populate lambda? I ran into this with an AWS API gateway JWT authorizer; doesn't fit the error messages, but might be worth reviewing.

I agree with Dan here, a misconfiguration of your issuer seems to be the issue.

Unexpected JWT issuer: https://xxxx.ngrok.io

means that you have configured https://xxxx.ngrok.io in fusionAuth but something different in

op.issuer: "https://xxx/"

Looking at the masked value, I think this is because of the trailing / that you have in that value in the elastic stack config. issuer is most commonly a URL but the equivalence check is string matching not canonicalized url matching, so

https://xxxx.ngrok.io is not the same as https://xxxx.ngrok.io/

FWIW, this is not peculiarity of "some systems" but a very well defined requirement of the OpenID Connect specification.

Thanks a lot, @ikakavas @Dan_Moore, for your help. The last problem was because the "/" on the issuer URL, must exactly same.

So the point for anyone that wants to integrate FusionAuth OpenID with Elastic can do double-check this point if facing similar problems:

  1. Change the issuer claim by going to "Tenants -> Your Tenant -> General" and changing the "Issuer" value. Use your FusionAuth server URL. It was "acme.org" by default on my configuration.
  2. Use autogenerate access and id token on JWT configuration using RS256, because the default using HMAC256 algorithm.
  3. Make sure the issuer URL on FusionAuth config (Tenant or Application) exactly the same as the issuer on elasticsearch.yml.