1
0
Files
hs.hsadmin.ng/doc/PR/2026-03-29-PR#220-Fix-exception-with-real-JWT-from-HS-Keycloak-OIDC.md
T
2026-04-24 06:41:02 +02:00

6.7 KiB

PR#220: Fix exception with real JWT from HS Keycloak OIDC

The Problems

Hsadmin-NG Throws an Exception at Startup

A locally running hsadmin-NG app does not work with the real JWT from an HS Keycloak OIDC:

source .unset-environment

export HSADMINNG_POSTGRES_JDBC_URL=jdbc:postgresql://localhost:5432/postgres
export HSADMINNG_POSTGRES_ADMIN_USERNAME=postgres
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=password
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net
export HSADMINNG_MIGRATION_DATA_PATH=migration
export HSADMINNG_OFFICE_DATA_SQL_FILE=

export HSADMINNG_ACCOUNT_PASSWORD_HASH_ALGORITHM='{SSHA}'
export HSADMINNG_JWT_ISSUER=https://login.dev.hsadmin.de/realms/testui
export SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://login.dev.hsadmin.de/realms/testui

export LANG=en_US.UTF-8

export ALLOWED_ORIGINS=http://127.0.0.1:8082

gw bootRun --args='--spring.profiles.active=dev,complete,test-data'

This fails with the following error:

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2026-03-27T15:12:48.630+01:00 ERROR 11995 --- [    restartedMain] o.s.boot.SpringApplication                             : Application run failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0: Error creating bean with name 'securityFilterChain' defined in class path resource [net/hostsharing/hsadminng/config/WebSecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: Error creating bean with name 'jwtDecoder' defined in class path resource [net/hostsharing/hsadminng/config/WebSecurityConfig.class]: Failed to instantiate [org.springframework.security.oauth2.jwt.JwtDecoder]: Factory method 'jwtDecoder' threw exception with message: jwkSetUri cannot be empty
                at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:896) ~[spring-beans-6.2.10.jar:6.2.10]
                at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:849) ~[spring-beans-6.2.10.jar:6.2.10]
                at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:146) ~[spring-beans-6.2.10.jar:6.2.10]
                at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:509) ~[spring-beans-6.2.10.jar:6.2.10]

jwt-curl login does not work

jwt-curl login
Username: superuser-alex@hostsharing.net
Password: password
ERROR: could not get JWT access token: curl: (22) The requested URL returned error: 401
{"error":"invalid_client","error_description":"Invalid client or Invalid client credentials"}

The Cause

Cause of the App-Start Problem

The environment sets HSADMINNG_JWT_ISSUERand SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI, which is redundant. But it does neither set HSADMINNG_JWT_JWKS_URL nor SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWKS_URI.

See also the Spring config in the main/.../application.yml:

    security:
        oauth2:
            resourceserver:
                jwt:
                    issuer-uri: ${HSADMINNG_JWT_ISSUER:}
                    jwk-set-uri: ${HSADMINNG_JWT_JWKS_URL:}

When issuer-uri is in the Spring config at all and HSADMINNG_JWT_JWKS_URL is unset, it becomes an empty string. And an empty value is invalid for NimbusJwtDecoder.withJwkSetUri(...), which is used by BaseWebSecurityConfig, as well as for the default-bean if we make it conditional.

Cause of the jwt-curl Login Problem

jwt-curl needs the envionment variables HSADMINNG_JWT_TOKEN_URL and HSADMINNG_JWT_CLIENT_ID to be set properly set.

The Solution

Improving the README.md to Run the App with a Real JWT

I added a "HOWTO" title above the part in the README.md so it can be found easier by running:

. .aliases
howto real keycloak
# or e.g.
howot real jwt

Fixing the Environment Problem to Run the App with a Real JWT

Remove the redundant environment variable:

export SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://login.dev.hsadmin.de/realms/testui

Set the issuer (required):

export HSADMINNG_JWT_ISSUER=https://login.dev.hsadmin.de/realms/testui

Set the JWKS URL only if you want to override autodetection (optional):

export HSADMINNG_JWT_JWKS_URL=https://login.dev.hsadmin.de/realms/testui/protocol/openid-connect/certs

Otherwise, make sure it's unset, e.g. via unset HSADMINNG_JWT_JWKS_URL.

Now the app starts. Let's also test auth:

CLIENT_ID=... # to be replaced
USERNAME=...  # to be replaced
PASSWORD=...  # to be replaced
BEARER="$(curl -X POST https://login.dev.hsadmin.de/realms/testui/protocol/openid-connect/token \
               -H "Content-Type: application/x-www-form-urlencoded" \
               -d "client_id=$CLIENT_ID" \
               -d "scope=openid \
               -d "grant_type=password" \
               -d "username=$USERNAME" \
               -d "password=$PASSWORD" \
          | jq -r '.refresh_token')"

Now try to fetch the accessible accounts:

curl --no-progress-meter -X GET 'http://127.0.0.1:8080/api/hs/accounts/current' \
     -H "Origin: https://testui.hsngdev.hs-example.de" \
     -H 'Accept: application/json' \
     -H "Authorization: Bearer $BEARER" \
     | jq

Autodetection of jwk-set-uri

The environment variable HSADMINNG_JWT_JWKS_URL is now optional.

BaseWebSecurityConfig now creates the decoder explicitly:

  • if spring.security.oauth2.resourceserver.jwt.jwk-set-uri has text, it uses NimbusJwtDecoder.withJwkSetUri(...)
  • if it is empty but spring.security.oauth2.resourceserver.jwt.issuer-uri has text, it uses JwtDecoders.fromIssuerLocation(...)
  • if both are empty, startup fails fast with a clear IllegalStateException

This prevents Spring Boot auto-configuration from trying to use an empty jwk-set-uri.

Fixing the jwt-curl Login Problem

Properly set the environment variables required by jwt-curl:

export HSADMINNG_JWT_CLIENT_ID=FIXME
export HSADMINNG_JWT_CLIENT_SECRET=FIXME
export HSADMINNG_JWT_USERNAME=superuser-alex@hostsharing.net
export HSADMINNG_JWT_PASSWORD=password
export HSADMINNG_JWT_TOKEN_URL=FIXME

FIXME: Find out the proper values.