DEV Community

loading...

Apache Pulsar OAuth2 dev setup with Docker and KeyCloak

Haris Secic
software developer doing some architecture
・6 min read

So I've been eager to use something easier to set up than generating those TLS certificates and such for development environment, yet still somewhat more secure than just hardcoded JWT inside a file in Pulsar VM (container in this case). If you're not familiar with JWT setup for Pulsar, it's not necessary, but you can think of it as lesser OAuth2 since they both rely mainly on verifying JWT but process for logging in with Brokers and Clients can be different.

KeyCloak setup

KeyCloak requires only client app to be set up for this. App needs to Service Accounts Enabled to be on which is basically client_credentials grant type in OAuth2. In order to see this option Access Type has to be set to "confidential".
Alt Text

Next thing to set it the "specialised" claim. This claim would be easy to set as realm role or client role but Apache Pulsar has issues reading complex types in JWT by default, and so to skip writing custom authentication resolver we must enforce claim inside JWT to be simple string. Problem is that role claims by default go into array even if specified as non-multivalued. So one could get something like

"role": "[admin]" or
"role": ["admin"]
Enter fullscreen mode Exit fullscreen mode

but we need

"role": "admin"
Enter fullscreen mode Exit fullscreen mode

for Pulsar to play nice with defaults.
which is unexpected given that it should be only 1 string. To skip this hassle go and set-up Hardcoded claim.
Alt Text
Alt Text

Token claim name can be role and value can be for this purpose superuser. These values are important for Pulsar setup so keep track of what was put in here. Pulsar will require you to set up role name for the superuser which is dedicated role for managing all Pulsar stuff. On the other hand other roles might be used but then you need to remember to setup tenant namespaces to be accessible by that role. This means one would have to manage that pulsar instance manually through REST or CLI or something else to setup tenant namespaces manageable by user role you wish to use by certain app.

Next, get the public key of the realm. Easy you might thing? Well no, again because of Pulsar. Go to Realm Settings and into Keys tab.
Alt Text
There you should find RS256 under active tab if all defaults were left as-is. If you wish to use different algorithm please also mark that as important and keep track of it as well as of the key value. If not then please continue and click on Public Key of that RS256 row.
Alt Text
You should get some Base64 text. It will look like gibberish of alphanumeric and some special characters.

Now copy that value and somehow convert it to bytes and store as file. Pulsar lib for java at 2.7 will try to read it as x509 which will fail as it needs to be decoded into bytes prior to this. Steps to do it are:

  1. Copy base64 value
  2. Decode it back to bytes using either something online or simply program your own mini script
  3. Store bytes into file A Groovy example:
byte[] key = "MACakje21/adkjwp9qmk4231/ea\d;qwdq=="..decodeBase64()
File f = new File("yourfilename")
f.bytes = key
Enter fullscreen mode Exit fullscreen mode

Why Groovy? Well it was fast to write and has all those .decode and .bytes and... you can execute it as script through IntelliJ - Tools -> Groovy Console. I was mainly using JVM things with pulsar and Python but Python fails to use KeyCloak settings because of some previous bug which was fixed for Java and C++ but apparently not for Python which should rely on C++ lib.

Pulsar image

After setting up KeyCloak and storing that key to some file we can build a customised image that uses OAuth2 in standalone mode.

Now create a file that you will later copy into image. Please visit Pulsar docs if you want more info. If not then just treat this as credentials file for OAuth2 client app so client ID and secret from KeyCloak (go to App then first tab contains ID and Credentials tab has the secret). Below is simple example

{
    "type": "client_credentials",
    "client_id": "pulsar",
    "client_secret": "dadada7a7a7-a7ad77ad7da-da7a7ad7ad77",
    "issuer_url": "https://host.docker.internal/auth/realms/test"
}
Enter fullscreen mode Exit fullscreen mode

I named it oauth2.json. _Download files conf/standalone.conf and conf/client.conf from Pulsar repository. These files will be changed to configure Pulsar image to use Authentication.

Configure standalone.conf. Set properties in that file to something like in below example@

authenticationProviders=org.apache.pulsar.broker.authentication.AuthenticationProviderToken
# here goes the role you've picked in case you want KeyCloak client app to behave in superuser or else just leave default
superUserRoles=superuser

#brokers need to login to other things so they must also be set up
brokerClientAuthenticationPlugin=org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2
#oauth2.json path should be set to wherever Dockerfile is copying it
brokerClientAuthenticationParameters={"issuerUrl": "https://host.docker.internal/auth/realms/test","privateKey": "/pulsar/oauth2.json","audience": "pulsar"}
#bytes from base64, file path must be as defined by Dockerfile
tokenPublicKey=file:///pulsar/oauth_public.key
Enter fullscreen mode Exit fullscreen mode

This will tell brokers to use JSON passed as a parameter, extract file under privateKey and post it's content as payload to isssuerUrl. Thus generating the JWT and refreshing when necessary. brokerClientAuthenticationPlugin is different from the authenticationProviders because broker generates JWT every now and then by authenticating to KeyCloak while main provider will only verify that token is valid (non-expired and signature is good). tokenPublicKey will instruct which file to use to verify JWT signature. It's the same as for simple JWT setup without the OAuth2. What's different here is that we need public key from the server while JWT can have key pair generated for Pulsar only and used by it or symmetric key for the same purpose. OAuth2 should not share it's private keys thus symmetric key is not a good option. KeyCloak by default uses asymmetric so it helps quite a bit.

Alternatively, if KeyCloak is set to never expire tokens, one can be generated in advance for Pulsar brokers and then copied into Pulsar container. This could be used instead of AuthenticationOAuth2 and parameters for broker would be file path to JWT token file. I would not advice alternative approach as it forces you to use never-expiring tokens on all of your realm not just that particular app.

Don't forget to configure your CLI tools. These are needed to run, add tenants/namespaces..., test topics, and do any kind of administration through CLI.

authPlugin=org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2

authParams={"issuerUrl": "https://host.docker.internal/auth/realms/test","privateKey": "/pulsar/oauth2.json","audience": "pulsar"}
Enter fullscreen mode Exit fullscreen mode

So same values are used as for broker configuration plugin and params.

And finally Dockerfile. I made something like this:

FROM apachepulsar/pulsar-standalone:2.7.0
WORKDIR /pulsar
#using workdir /pulsar we copy public key and it's path will be /pulsar/oauth_public.key and this value must be same as in properties for standalone
COPY oauth_public.key oauth_public.key
#note oauth2.json is being copied from into workdir /pulsar which is linked in brokerClientAuthenticationParameters
COPY oauth2.json oauth2.json
#client for CLI tools
COPY client.conf conf/client.conf
#
COPY standalone.conf conf/standalone.conf
Enter fullscreen mode Exit fullscreen mode

If you used same naming then you should be good to go with this image. One note is that I'm relying on -standalone instead of just pulsar or pulsar-all. If you choose something else you might want to add start-up command at the end to run pulsar when you trigger container run from docker. Standalone version runs automatically but others I used didn't so optionally use at the end

CMD [ "/pulsar/bin/pulsar", "standalone"]
Enter fullscreen mode Exit fullscreen mode

if you used other image as a base.

Build image and run

This should be sufficient to run it. I made something similar and tested it with CLI, Java, and Python clients where I've noticed that Python complains about .well-known/... which was exception I got when Java library was not fixed to respect path of Issuer URL. What was wrong with Java lib is that URL was stripped of path and only root part was used so if you have http://keycloak/realms/test it would only take http://keycloak/ so keep that in mind while testing.

Hope it goes well.

Discussion (6)

Collapse
rrrrrr111 profile image
Roman Churganov

You can convert Keycloak public certificate into Pulsar key format with openssl like $> openssl rsa -in pulsar_admin.key -pubout -outform DER -out pulsar_admin_pub.key

Collapse
greenroommate profile image
Haris Secic Author

If someone finds the post useful they will hopefully scroll to comment section to see this if they dislike Groovy approach. I just wanted to offer any example but I think openssl is a bit better. Thanks

Collapse
fjod profile image
Michael

This will sound stupid, but can I use auth0.com/ instead of KeyCloak ?

Collapse
greenroommate profile image
Haris Secic Author

Should be possible. OAuth2 is the protocol used in the end so any provider would work. Just grab the correct parameters for issuer URI and such and you're good to go.

Collapse
fjod profile image
Michael

Thank you. Can you provide some more guidance on this topic? There are only 2 docs about pulsar ouath2, this article and pulsar.apache.org/docs/en/next/sec... . So I have jwt token in my client and return it to broker on demand. I need to authenticate broker on the same ouath2 server, so it can validate client's token? Also I must include role field in client's jwt so broker can grant some permissions? I dont understand the role of broker's authentication on oauth server.

Thread Thread
greenroommate profile image
Haris Secic Author

This is a bit different approach than that. Think of it as Facebook or Google apps. You create a client app on the OAuth2 side like you would for an app that uses Facebook to login. So you have client ID and a secret with other necessary info like provider URL and such. That you store either in file like I made in this example or in other forms if you have more experience with service registries and such. That file is NOT a JWT. That is something like a username and a pass for applications to login to auth service. After setting it up with Pulsar including public key and such Pulsar brokers will contact the auth server for JWT for internal usages. From the consumer perspective you need yet another OAuth2 app or for playing around use the same one as for Pulsar. What happens now is that your app goes to OAuth2 server and asks for a JWT then sends that JWT when communicating with Pulsar brokers. Public key is used to verify JWT because Pulsar doesn't support yet JWKS. If you don't know much about JWKS think of it as providing a way for services to verify token locally by downloading those public keys on startup instead of having them in files. So your consumers will authenticate with auth0 and use JWT given by auth0 to login to Pulsar. Then Pulsar verifies it by public key so it knows it's valid JWT but also gets claims for it and then checks if role is good enough to listen to requested tenant or namespace or topic.

Bottom line no users are used but apps behave like users in their special way. Brokers and others need to communicate internally so they use client app to get that JWT but it's not used for much more than that. At least that I got from their docs and my really limited knowledge of OAuth2. I think they will try to provide more robust approach like with the JWKS but until then you need that public key inside of the pulsar or direct link to someplace where pulsar can fetch it. Example is file://auth0.com/mysomething/.../publickey. However I never tested it