Skip to content

druid extension for OpenID Connect auth using pac4j lib - #8992

Merged
jihoonson merged 55 commits into
apache:masterfrom
himanshug:pac4j_master
Mar 24, 2020
Merged

jihoonson merged 55 commits into
apache:masterfrom
himanshug:pac4j_master

Conversation

@himanshug

@himanshug himanshug commented Dec 5, 2019 •

Copy link
Copy Markdown
Contributor

Fixes #8984

Description

Druid security extension to support authentication based on OpenID Connect spec. Please see added file druid-pac4j.md for further information.

This patch...

  1. adds a new security extension, druid-pac4j, with Authenticator implementation using pac4j library
  2. removes /unified-console.html from default unsecured paths , and introduces UnautorizedResourceFilter similar to existing UnsecuredResourceFilter to avoid "authentication done" check made by PreResponseAuthorizationCheckFilter . This is done so that redirect to authentication server (e.g. Okta) is correctly handled by browser when user visits the web console.

I have tested the it using Okta dev server . Also, a version of this code is running on our internal Druid clusters in prod with enterprise Okta auth server.


This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • been tested in a test Druid cluster.

@himanshug
himanshug requested a review from jon-wei December 12, 2019 21:41
@soumyajose0784

Copy link
Copy Markdown

yes, I am able to visit the the url in browser successfully. I have a trust-store created as well in linux machine with same certificate. But how do we specify the truststore parameter in common.runtime.properties. For eg, for ldap auth, we used to give the truststore path for ldap server as follows
druid.auth.basic.ssl.trustStorePath=/etc/security/truststore_ldap.jks
druid.auth.basic.ssl.trustStorePassword=
druid.auth.basic.ssl.protocol=TLS

@himanshug

Copy link
Copy Markdown
Contributor Author

@soumyajose0784 as a workaround, can you try adding following properties in jvm args of Druid process ...

-Djavax.net.ssl.trustStrore=/etc/security/truststore_ldap.jks -Djavax.net.ssl.trustStorePassword=password

and see if that works

@averma111

Copy link
Copy Markdown

Thank you @himanshug that should be helpful we will try these setting and let you know. Do we need to pass the above java param to all the run time properties file or any specific node.

@himanshug

himanshug commented Apr 6, 2020 •

Copy link
Copy Markdown
Contributor Author

Do we need to pass the above java param to all the run time properties file or any specific node.

they wouldn't go in runtime.properties file but must be added as jvm args to druid process , they should be added to only router node presuming that is one web console which admins would visit.

@himanshug

Copy link
Copy Markdown
Contributor Author

@soumyajose0784 @averma111 also please add this commit ( #9627 ) to your 0.18.0 custom build.

@averma111

Copy link
Copy Markdown

Thanks @himanshug for all the help and support.We will rebuild 0.18 again, I have also raised on more issue #9628.
See if you can comment something on it.

@himanshug

Copy link
Copy Markdown
Contributor Author

@averma111 can you remove the kerberos authenticator and let us try to make this auth working first , then we could add kerberos back in so that you have one set of problems at a time.

@averma111

Copy link
Copy Markdown

@himanshug I am working on two druid instance simultaneously/separately , I have a tight deadline and need to provide solution for SSO by either way. hence exploring all the possible options.

@himanshug

Copy link
Copy Markdown
Contributor Author

@averma111 @soumyajose0784 can you add the commit from https://github.com/apache/druid/pull/9637/files in your build then you should be able to specify custom ssl trustStore via following configuration in runtime.properties file...

druid.extensions.loadList=["druid-pac4j","simple-client-sslcontext",...]
druid.auth.authenticatorChain=["pac4j"]
druid.auth.pac4j.cookiePassphrase=whatever
druid.auth.pac4j.enableCustomSslContext=true

druid.auth.pac4j.oidc.clientID=asdafwqerteg
druid.auth.pac4j.oidc.clientSecret=asadfewgtewr
druid.auth.pac4j.oidc.discoveryURI=https://dev-960309.okta.com/oauth2/default/.well-known/openid-configuration

druid.client.https.protocol=TLS
druid.client.https.trustStorePassword=p@ssword
druid.client.https.trustStorePath=/etc/security/truststore_ldap.jks

@averma111

Copy link
Copy Markdown

Sure, we will rebuild again and follow the instructions.

@averma111

Copy link
Copy Markdown

@himanshug do we have any redirect url setting for Druid SSO, they wanted to know if we have any.They need for authorization code flow.

@himanshug

Copy link
Copy Markdown
Contributor Author

yes in the auth server side you would need to add something like https://druid_router_url:port/druid-ext/druid-pac4j/callback as redirect uri in the auth server client app configuration. this is the url where auth server redirects browser to, after successful login.

@averma111

Copy link
Copy Markdown

@himanshug my team is asking what we want to return such as user id, email address, roles, etc. what should we provide ?

@himanshug

Copy link
Copy Markdown
Contributor Author

pac4j asks for scope=openid+profile+email so I guess that would mean all of above.

@averma111

Copy link
Copy Markdown

@himanshug thank you

@soumyajose0784

Copy link
Copy Markdown

I could bring up Druid services after making the changes highlighted above and its able to hit the ping federate url, but now its giving SSL error accessing the callback url https://:/druid-ext/druid-pac4j/callback. Could you please advise how can this be fixed?Our Druid endpoints are ssl enabled.

Properties set are as follows
druid.extensions.loadList=[...,"druid-pac4j","simple-client-sslcontext"]
#TLS
druid.enablePlaintextPort=false
druid.enableTlsPort=true
druid.server.https.keyStorePath=/etc/security/keystore.jks
druid.server.https.keyStoreType=JKS
druid.server.https.certAlias=
druid.server.https.keyStorePassword=
druid.server.https.requireClientCertificate=false
druid.server.https.requestClientCertificate=false
druid.client.https.trustStorePath=/etc/security/truststore_sso.jks #this truststore contains valid certs for both Ping federate Server and our own Druid servers
druid.client.https.trustStoreType=JKS
druid.client.https.trustStorePassword=
druid.client.https.protocol=TLS
#Pac4j
druid.auth.authenticatorChain=["pac4j"]
druid.auth.authenticator.pac4j.type=pac4j
druid.auth.pac4j.cookiePassphrase=
druid.auth.pac4j.enableCustomSslContext=true
druid.auth.pac4j.oidc.clientID=
druid.auth.pac4j.oidc.clientSecret=
druid.auth.pac4j.oidc.discoveryURI=https://<pingfederate_url>/.well-known/openid-configuration

Error details


HTTP ERROR 500
Problem accessing /druid-ext/druid-pac4j/callback. Reason: Server Error
Caused by:
org.pac4j.core.exception.TechnicalException: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at org.pac4j.oidc.credentials.authenticator.OidcAuthenticator.validate(OidcAuthenticator.java:159)
at org.pac4j.oidc.credentials.authenticator.OidcAuthenticator.validate(OidcAuthenticator.java:35)
at org.pac4j.core.client.BaseClient.retrieveCredentials(BaseClient.java:71)
at org.pac4j.core.client.IndirectClient.getCredentials(IndirectClient.java:140)
at org.pac4j.core.engine.DefaultCallbackLogic.perform(DefaultCallbackLogic.java:89)
at org.apache.druid.security.pac4j.Pac4jFilter.doFilter(Pac4jFilter.java:94)
at org.apache.druid.server.security.AuthenticationWrappingFilter.doFilter(AuthenticationWrappingFilter.java:59)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1642)
at org.apache.druid.server.security.SecuritySanityCheckFilter.doFilter(SecuritySanityCheckFilter.java:86)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1642)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:533)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1595)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1340)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:473)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1564)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1242)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
at org.eclipse.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:740)
at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:61)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
at org.eclipse.jetty.server.Server.handle(Server.java:503)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:364)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
at org.eclipse.jetty.io.ssl.SslConnection$DecryptedEndPoint.onFillable(SslConnection.java:411)
at org.eclipse.jetty.io.ssl.SslConnection.onFillable(SslConnection.java:305)
at org.eclipse.jetty.io.ssl.SslConnection$2.succeeded(SslConnection.java:159)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
at java.lang.Thread.run(Thread.java:748)
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1946)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:316)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:310)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1639)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:223)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1037)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:965)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1064)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at sun.net.www.protocol.http.HttpURLConnection.getOutputStream0(HttpURLConnection.java:1340)
at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1315)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:264)
at com.nimbusds.oauth2.sdk.http.HTTPRequest.toHttpURLConnection(HTTPRequest.java:814)
at com.nimbusds.oauth2.sdk.http.HTTPRequest.send(HTTPRequest.java:882)
at org.pac4j.oidc.credentials.authenticator.OidcAuthenticator.validate(OidcAuthenticator.java:141)
... 41 more
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:450)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:317)
at sun.security.validator.Validator.validate(Validator.java:262)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:330)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:237)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:132)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1621)
... 56 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:445)
... 62 more

@himanshug

Copy link
Copy Markdown
Contributor Author

@soumyajose0784 @averma111 thanks for trying. sorry to hear that, I am looking into it. unfortunately, I have access to 2 okta servers and both have standard ssl certs so custom truststore handling is being a bit difficult for me to verify fully. I think the problem is because pac4j lib is not using single mechanism to make HTTP requests as older issue reported in #8992 (comment) is not the problem anymore.

I will make some change soon and would ask you to test it. I will get that PR merged only after your confirmation of everything working.

@averma111

Copy link
Copy Markdown

@himanshug thank you for the taking this activity. Let us know when we can rebuild package again and test it.

@himanshug

Copy link
Copy Markdown
Contributor Author

@averma111 @soumyajose0784 can you please get the commit from #9695 to your build and try ?

@soumyajose0784

Copy link
Copy Markdown

@himanshug , we have built the commit from https://github.com/himanshug/druid/tree/pac4j_ssl,
and now able to open unified console without SSL error. But the console gives redirect error for coordinator urls, so datasources/supervisor/tasks tabs are not available from unified console. Any thought?
Error from router.log


org.apache.druid.server.router.CoordinatorRuleManager - Exception while polling for rules
org.apache.druid.java.util.common.ISE: Error while polling rules, status[302 Found] content[]
Error from DataSources tab:


Unknown exception / Unexpected response status [302] description [Found] from request url[https://:/druid/coordinator/v1/metadata/segments?includeOvershadowedStatus] / org.apache.druid.java.util.common.RE / on host

@himanshug

Copy link
Copy Markdown
Contributor Author

@soumyajose0784 did you enable pac4j auth on non-router nodes as well? One option would be to not have pac4j auth on non-router nodes. However if you added pac4j on those nodes to be able to have SSO on the web console at coordinator/overlord, then following information is relevant.

Router periodically talks to various other Druid nodes to gather some data that it displays. However, Router needs to be able to authenticate with those nodes to be able to talk to them successfully. That particular auth mechanism is enabled via configuring "Escalator", you can read about generic Druid auth stuff in https://druid.apache.org/docs/latest/design/auth.html#escalator .
Pac4j extension doesn't support programmatic clients, so Router doesn' t have an escalator to talk to other Druid nodes when they only allow pac4j authenticated users.
For that reason, in druid.auth.authenticatorChain=[..., "pac4j"] you should add at least one authenticator that supports programmatic clients e.g. ldap or kerberos or https://druid.apache.org/docs/latest/development/extensions-core/druid-basic-security.html and configure druid.escalator.xx properties at all nodes to be able to authenticate with that authenticator.

@soumyajose0784

Copy link
Copy Markdown

@himanshug , thank you for the great support. We are able to bring up the services (pac4j+basic auth enabled), and router console is now accessible with proper connection to other nodes

@himanshug

Copy link
Copy Markdown
Contributor Author

glad that it worked out, thanks for testing.

@shashisingh

Copy link
Copy Markdown

@himanshug, I want to setup router node with both basic authentication (for REST API access through python) and pac4j (browser access) and non-router nodes with basic authentication. Could you help me with the sample runtime.properties to achieve this kind of setup? I have tried what you had suggested (enable pac4j for router console and disable authentication at non-router nodes) and that setup works fine for browser based access. But I want to expose router and other nodes REST APIs for some automations. Thanks

@himanshug

himanshug commented Sep 11, 2020 •

Copy link
Copy Markdown
Contributor Author

@shashisingh you can have something like below and that should work...

druid.extensions.loadList=[...,"druid-pac4j","druid-basic-security",...]

druid.auth.authenticatorChain=["MyBasicMetadataAuthenticator", "pac4j"]

druid.auth.pac4j.* properties... that you already have and working

druid.auth.authenticator.pac4j.authorizerName=MyBasicMetadataAuthorizer
druid.auth.authenticator.MyBasicMetadataAuthenticator.type=basic
druid.auth.authenticator.MyBasicMetadataAuthenticator.authorizerName=allowAll

druid.auth.authorizers=["MyBasicMetadataAuthorizer", "allowAll"]
druid.auth.authorizer.allowAll.type=allowAll
druid.auth.authorizer.MyBasicMetadataAuthorizer.type=basic
druid.auth.authorizer.MyBasicMetadataAuthorizer.enableCacheNotifications=true
druid.auth.authorizer.MyBasicMetadataAuthorizer.roleProvider.type=context
druid.auth.authorizer.MyBasicMetadataAuthorizer.initialAdminRole=admin

druid.escalator.type=basic
druid.escalator.internalClientUsername=druid_system
druid.escalator.internalClientPassword=desired_password_xx
druid.escalator.authorizerName=allowAll
druid.auth.authenticator.MyBasicMetadataAuthenticator.initialInternalClientPassword=desired_password_xx

also please read the docs in https://druid.apache.org/docs/latest/development/extensions-core/druid-basic-security.html

@bitsofdave

Copy link
Copy Markdown

@himanshug For configuring authorizations using the MyBasicMetadataAuthorizer in your example above, do you have to use the sub attribute to identify a user?

For example, my OIDC provider returns something like the following profile:

profile: #OidcProfile# | id: <id> | attributes: {..., sub=<sub>, ..., preferred_username=<email_address>, ..., given_name=<name>, ..., name=<name>, ..., email=<email>, ...} | roles: [] | permissions: [] | isRemembered: false 

I can create a druid basic user with name <sub> and assign a druid basic security role to it. This seems to work fine.

What I would like to do is create the druid basic user using another attribute such as <email> and assign a druid basic security role to it. When I've tried this, the user I'm logged in as doesn't get any permissions since I'm being identified by <sub>. I'm not sure how to change it to use another field.

Another option, even preferred, is to use roles in the response and assign permissions to them, similar to LDAP group mappings. Is this possible with this extension?

riovic918data pushed a commit to riovic918data/druid that referenced this pull request Jun 12, 2026
* druid pac4j security extension for OpenID Connect OAuth 2.0 authentication

* update version in druid-pac4j pom

* introducing unauthorized resource filter

* authenticated but authorized /unified-webconsole.html

* use httpReq.getRequestURI() for matching callback path

* add documentation

* minor doc addition

* licesne file updates

* make dependency analyze succeed

* fix doc build

* hopefully fixes doc build

* hopefully fixes license check build

* yet another try on fixing license build

* revert unintentional changes to website folder

* update version to 0.18.0-SNAPSHOT

* check session and its expiry on each request

* add crypto service

* code for encrypting the cookie

* update doc with cookiePassphrase

* update license yaml

* make sessionstore in Pac4jFilter private non static

* make Pac4jFilter fields final

* okta: use sha256 for hmac

* remove incubating

* add UTs for crypto util and session store impl

* use standard charsets

* add license header

* remove unused file

* add org.objenesis.objenesis to license.yaml

* a bit of nit changes  in CryptoService  and embedding EncryptionResult for clarity

* rename alg  to cipherAlgName

* take cipher alg name, mode and padding as input

* add java doc  for CryptoService  and make it more understandable

* another  UT for CryptoService

* cache pac4j Config

* use generics clearly in Pac4jSessionStore

* update cookiePassphrase doc to mention PasswordProvider

* mark stuff Nullable where appropriate in Pac4jSessionStore

* update doc to mention jdbc

* add error log on reaching callback resource

* javadoc  for Pac4jCallbackResource

* introduce NOOP_HTTP_ACTION_ADAPTER

* add correct module name in license file

* correct extensions folder name in licenses.yaml

* replace druid-kubernetes-extensions to druid-pac4j

* cache SecureRandom instance

* rename UnauthorizedResourceFilter to AuthenticationOnlyResourceFilter
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Proposal] Authenticator impl for OpenID Connect OAuth2.0 protocol

7 participants