Zitadel & Spring Boot Oauth2 Client & Resource servers

Karanbir Singh
6 min readMar 2, 2023

Zitadel is a new age sibling of Keycloak, available both as self managed & zitadel managed options. It is straightforward enough to setup and manage.

I very recently started using it, along with it I just did integration with the Spring Boot 3 Oauth2 client/ resource server configuration.

The present blog post is focused on the, Spring Boot + Zitadel integration. It includes code, setup instructions, etc. but better(more logical) minds will learn easily.

Prerequisites

  • JDK 17 & any IDE.
  • Above Average knowledge of OAUTH2, JWT, Java, Spring, Spring Boot, Git, etc.
  • Some knowledge of Reactive programming

Source Code

The source code consists of two separate repositories :-
- Oauth2 Client — a Spring boot application exposed on port 8081
- Oauth2 Resource — a Spring boot application exposed on port 8082

There are two types of Oauth2 access tokens that zitadel supports
- JWT access token
- opaque access tokens

The oauth2 resource server has security config done in a way so that:-

— urls starting matching /v1/** are protected by JWT based access token
urls starting matching /v2/** are protected by opaque access token

JWT based access token client flow — upstream url http://localhost:8081/check/v1/ping

  1. The upstream endpoint is GET http://localhost:8081/check/v1/ping.
  2. Oauth2Client creates and sends the request for access_token generation to zitadel’s /oauth/v2/token.
  3. Oauth2Client receives back the access token along with expiry.
  4. The oauth2Client appends the access token to the actual resource server request as part of the Authorization header and hits http://localhost:8082/v1/ping
  5. The resource server running on port 8082 is configured with issuer details and it hits Zitadel’s openid-configuration url to ultimately fetch the JWKS via jwks url.
  6. zitadel replies back with JWKS
  7. resource server validates the input Authorization JWT access token received from oauth2 client server application.
  8. after successful validation the resource server constructs the Pong response and replies back to oauth2 client server
  9. oauth2 client server passes back the response back to end consumer / client as below:-
{
"ping": "pong",
"status": true,
"timestamp": 1677673951752,
"method": "GET",
"version": "v1"
}

Opaque access token client flow — upstream url http://localhost:8081/check/v2/ping

  1. The upstream endpoint is GET http://localhost:8081/check/v2/ping.
  2. Oauth2Client creates and send a request for access_token generation to zitadel over to /oauth/v2/token.
  3. Oauth2Client receives the access token along with expiry.
  4. The oauth2Client appends the access token to the actual resource server call as part of the Authorization header and hits http://localhost:8082/v1/ping
  5. The resource server running on port 8082 is configured with instrospection endpoint details, with client id and secret and it hits Zitadel’s to validate the access token.
  6. zitadel replies back active equals true along with other details like audience issuer, etc.
  7. resource server validates if access token active equals true.
  8. after successful validation of access token the resource server constructs the response and responds back to oauth2 client server
  9. oauth2 client server passes back the response back to end consumer / client as below:-
{
"ping": "pong",
"status": true,
"timestamp": 1677673951752,
"method": "GET",
"version": "v2"
}

The code & configuration

The oauth2 client configuration comprises of following important snippets:-

# replace with domain of zitadel instance
zitadel.base.url=https://zitadel_instance_domain
zitadel.jwt.registration.name=jwt # the name of the client registration for JWT tokens
zitadel.opaque.registration.name=opaque # the name of the client registration for opaque tokens

# downstream base path
resource.server.basepath=http://localhost:8082

# client configuration for the JWT based access token
# jwt is a dynamic value after spring.security.oauth2.client.registration.*
spring.security.oauth2.client.registration.jwt.provider=zitadel-jwt-provider
spring.security.oauth2.client.registration.jwt.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.jwt.client-id=<client_id_for_jwt_token_client>
spring.security.oauth2.client.registration.jwt.client-secret=<client_secret_for_jwt_token_client>

# client configuration for the opaque access token
# opaque is a dynamic value after spring.security.oauth2.client.registration.*
spring.security.oauth2.client.registration.opaque.provider=zitadel-opaque-provider
spring.security.oauth2.client.registration.opaque.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.opaque.client-id=<client_id_for_opaque_token>
spring.security.oauth2.client.registration.opaque.client-secret=<client_secret_for_opaque_token>
spring.security.oauth2.client.registration.opaque.scope[0]=urn:zitadel:iam:org:project:id:<project_resource_id_put_here>:aud

# the providers for the token uri configs, these are referred above in the client registration
spring.security.oauth2.client.provider.zitadel-jwt-provider.token-uri=${zitadel.base.url}/oauth/v2/token
spring.security.oauth2.client.provider.zitadel-opaque-provider.token-uri=${zitadel.base.url}/oauth/v2/token

Zitadel Configuration — Create service user/ client creds for the opaque access token

  1. Create a new user.

2. Make sure Access Token Type is Bearer.

3. Generate Client secret and secure the secret for later use and replace them in the application.properties for the below properties.

spring.security.oauth2.client.registration.opaque.client-id=<client_id_for_opaque_token>
spring.security.oauth2.client.registration.opaque.client-secret=<client_secret_for_opaque_token>

4. Create a new project & add the previously created service user to Authorizations of the project (add roles if required for authorization, else you may skip adding roles)

5. In the general tab of the project, copy the project resource id and replace <project_resource_id_put_here> with it in the following property of application.properties file:-

spring.security.oauth2.client.registration.opaque.scope[0]=urn:zitadel:iam:org:project:id:<project_resource_id_put_here>:aud

Zitadel — Create service user/ client creds for the jwt access token

  1. Create a new service user.

⚠️ Make sure to set access token type to JWT.

2. Generate the client secret.

3. update the application.properties in the source code with the client and secret, in the following properties:-

spring.security.oauth2.client.registration.jwt.client-id=<client_id_for_jwt_token_client>
spring.security.oauth2.client.registration.jwt.client-secret=<client_secret_for_jwt_token_client>

Configuration needed for the resource server.

The application.properties are:-

# replace with domain of zitadel instance
zitadel.base.url=https://zitadel_instance_domain

spring.security.oauth2.resourceserver.jwt.issuer-uri=${zitadel.base.url}

spring.security.oauth2.resourceserver.opaquetoken.client-id=<client_id_of_the_zitadel_api_app>
spring.security.oauth2.resourceserver.opaquetoken.client-secret=<client_secret_of_the_zitadel_api_app>
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=${zitadel.base.url}/oauth/v2/introspect
  1. Create a new app in the project you previously created above, Create that new app as below:-

2. Enter a name and make sure the type of application is API

3. Make sure the Authentication method is BASIC & click on Continue and then Create

4. next screen, copy the client id and secret and place them in the following properties of the resource server’s application.properties

spring.security.oauth2.resourceserver.opaquetoken.client-id=<client_id_of_the_zitadel_api_app>
spring.security.oauth2.resourceserver.opaquetoken.client-secret=<client_secret_of_the_zitadel_api_app>

I hope everything is done perfectly.

Fire up the servers, the port 8081 should expose the client server, while port 8082 will expose the resource server.

If all good when you hit -> http://localhost:8081/check/v1/ping -> internally it will hit -> http://localhost:8082/v1/ping (which is protected by JWT access token) & it will reply back with following response:-

{
"ping": "pong",
"status": true,
"timestamp": 1677779122014,
"method": "GET",
"version": "v1"
}

Further if you hit -> http://localhost:8081/check/v2/ping -> internally it will hit -> http://localhost:8082/v2/ping (which is protected by opaque access token) & it will reply back with following response:-

{
"ping": "pong",
"status": true,
"timestamp": 1677779132053,
"method": "GET",
"version": "v2"
}

--

--

Karanbir Singh

API developer + Web Application developer + Devops Engineer = Full Stack Developer