From d7a78d0a79e695fb4a5de673b7dc6006ad4e514d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 8 Sep 2025 15:27:28 +0200 Subject: [PATCH] migrate from CAS to Oauth2-JWT Auth (#197) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/197 Reviewed-by: Marc Sandlus --- .aliases | 16 +- .tc-environment | 3 +- .unset-environment | 7 +- README.md | 96 +++--- bin/cas-curl | 261 ---------------- bin/jwt-curl | 279 ++++++++++++++++++ build.gradle.kts | 4 + .../config/BaseWebSecurityConfig.java | 78 +++++ .../config/CasAuthenticationFilter.java | 36 --- .../hsadminng/config/CasAuthenticator.java | 8 - .../config/FakeCasAuthenticator.java | 14 - .../hsadminng/config/FakeJwtController.java | 40 +++ .../config/JsonObjectMapperConfiguration.java | 9 +- .../hsadminng/config/JwtFakeBearer.java | 42 +++ .../config/RealCasAuthenticator.java | 86 ------ .../hsadminng/config/WebSecurityConfig.java | 71 +---- .../HsCredentialsContextsController.java | 5 +- .../hs/accounts/HsCredentialsController.java | 4 +- .../booking/item/HsBookingItemController.java | 4 +- .../project/HsBookingProjectController.java | 4 +- .../asset/HsHostingAssetController.java | 4 +- .../asset/HsHostingAssetPropsController.java | 4 - .../HsOfficeBankAccountController.java | 4 +- .../contact/HsOfficeContactController.java | 4 +- ...OfficeCoopAssetsTransactionController.java | 4 +- ...OfficeCoopSharesTransactionController.java | 4 +- .../debitor/HsOfficeDebitorController.java | 4 +- .../HsOfficeMembershipController.java | 4 +- .../partner/HsOfficePartnerController.java | 4 +- .../person/HsOfficePersonController.java | 4 +- .../relation/HsOfficeRelationController.java | 4 +- .../HsOfficeSepaMandateController.java | 4 +- .../hsadminng/ping/PingController.java | 4 - .../hsadminng/{ => rbac}/context/Context.java | 27 +- .../rbac/context/RbacContextController.java | 3 +- .../rbac/grant/RbacGrantController.java | 4 +- .../rbac/grant/RbacGrantsDiagramService.java | 2 +- .../rbac/role/RbacRoleController.java | 4 +- .../rbac/subject/RbacSubjectController.java | 4 +- .../test/cust/TestCustomerController.java | 4 +- .../rbac/test/pac/TestPackageController.java | 4 +- .../hostsharing/hsadminng/repr/Stringify.java | 14 +- src/main/resources/application.yml | 24 +- .../hsadminng/arch/ArchitectureTest.java | 1 + ...asAuthenticationFilterIntegrationTest.java | 109 ------- .../CustomActuatorEndpointAcceptanceTest.java | 7 +- .../config/DisableSecurityConfig.java | 27 -- ...wtAuthenticationFilterIntegrationTest.java | 77 +++++ ... => MessageTranslatorIntegrationTest.java} | 6 - .../WebSecurityConfigForWebMvcTests.java | 13 + .../WebSecurityConfigIntegrationTest.java | 165 +++-------- ...esponseEntityExceptionHandlerRestTest.java | 12 +- ...sContextRbacRepositoryIntegrationTest.java | 2 +- ...sContextRealRepositoryIntegrationTest.java | 2 +- ...CredentialsContextsControllerRestTest.java | 19 +- ...HsCredentialsControllerAcceptanceTest.java | 30 +- ...sCredentialsRepositoryIntegrationTest.java | 3 +- .../scenarios/CredentialsScenarioTests.java | 40 +-- .../accounts/scenarios/CurrentLoginUser.java | 3 +- .../accounts/scenarios/FetchRbacContext.java | 3 +- ...HsBookingItemControllerAcceptanceTest.java | 33 +-- .../item/HsBookingItemControllerRestTest.java | 21 +- ...sBookingItemRepositoryIntegrationTest.java | 2 +- ...ookingProjectControllerAcceptanceTest.java | 25 +- ...okingProjectRepositoryIntegrationTest.java | 2 +- ...sHostingAssetControllerAcceptanceTest.java | 232 +++++++-------- .../HsHostingAssetControllerRestTest.java | 18 +- ...ingAssetPropsControllerAcceptanceTest.java | 9 +- ...HostingAssetRepositoryIntegrationTest.java | 2 +- ...omainSetupHostingAssetFactoryUnitTest.java | 2 +- .../HsBookingItemCreatedListenerUnitTest.java | 2 +- ...edWebspaceHostingAssetFactoryUnitTest.java | 2 +- .../hs/migration/ImportHostingAssets.java | 2 +- ...ceBankAccountControllerAcceptanceTest.java | 38 +-- ...HsOfficeBankAccountControllerRestTest.java | 16 +- ...eBankAccountRepositoryIntegrationTest.java | 2 +- ...OfficeContactControllerAcceptanceTest.java | 33 +-- ...eContactRbacRepositoryIntegrationTest.java | 2 +- ...tsTransactionControllerAcceptanceTest.java | 48 ++- ...opAssetsTransactionControllerRestTest.java | 21 +- ...sTransactionRepositoryIntegrationTest.java | 2 +- ...esTransactionControllerAcceptanceTest.java | 60 ++-- ...opSharesTransactionControllerRestTest.java | 17 +- ...sTransactionRepositoryIntegrationTest.java | 2 +- ...OfficeDebitorControllerAcceptanceTest.java | 43 ++- ...fficeDebitorRepositoryIntegrationTest.java | 2 +- ...iceMembershipControllerAcceptanceTest.java | 41 +-- .../HsOfficeMembershipControllerRestTest.java | 30 +- ...ceMembershipRepositoryIntegrationTest.java | 2 +- ...OfficePartnerControllerAcceptanceTest.java | 81 +++-- .../HsOfficePartnerControllerRestTest.java | 20 +- ...ePartnerRbacRepositoryIntegrationTest.java | 2 +- ...sOfficePersonControllerAcceptanceTest.java | 35 +-- ...cePersonRbacRepositoryIntegrationTest.java | 2 +- ...cePersonRealRepositoryIntegrationTest.java | 2 +- ...RealRelationRepositoryIntegrationTest.java | 2 +- ...fficeRelationControllerAcceptanceTest.java | 52 ++-- ...ficeRelationRepositoryIntegrationTest.java | 2 +- .../scenarios/HsOfficeScenarioTests.java | 27 +- ...ceSepaMandateControllerAcceptanceTest.java | 51 ++-- ...HsOfficeSepaMandateControllerRestTest.java | 12 +- ...eSepaMandateRepositoryIntegrationTest.java | 2 +- .../hsadminng/hs/scenarios/ScenarioTest.java | 34 ++- .../hsadminng/hs/scenarios/UseCase.java | 9 +- .../TransactionContextIntegrationTest.java | 2 +- .../ping/PingControllerAcceptanceTest.java | 23 +- .../ping/PingControllerRestTest.java | 34 ++- .../rbac/context/ContextBasedTest.java | 1 - .../rbac/context/ContextIntegrationTests.java | 29 +- .../rbac/context/ContextUnitTest.java | 1 - .../RbacContextControllerRestTest.java | 13 +- .../RbacGrantControllerAcceptanceTest.java | 28 +- .../RbacGrantRepositoryIntegrationTest.java | 2 +- ...acGrantsDiagramServiceIntegrationTest.java | 2 +- .../RbacRoleControllerAcceptanceTest.java | 22 +- .../rbac/role/RbacRoleControllerRestTest.java | 19 +- .../RbacRoleRepositoryIntegrationTest.java | 2 +- .../RbacSubjectControllerAcceptanceTest.java | 58 ++-- .../RbacSubjectControllerRestTest.java | 15 +- .../RbacSubjectRepositoryIntegrationTest.java | 2 +- .../TestCustomerControllerAcceptanceTest.java | 27 +- ...TestCustomerRepositoryIntegrationTest.java | 2 +- .../TestPackageControllerAcceptanceTest.java | 25 +- .../TestPackageRepositoryIntegrationTest.java | 2 +- src/test/resources/application.yml | 5 +- 125 files changed, 1537 insertions(+), 1549 deletions(-) delete mode 100755 bin/cas-curl create mode 100755 bin/jwt-curl create mode 100644 src/main/java/net/hostsharing/hsadminng/config/BaseWebSecurityConfig.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/config/CasAuthenticator.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/config/FakeCasAuthenticator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/config/FakeJwtController.java create mode 100644 src/main/java/net/hostsharing/hsadminng/config/JwtFakeBearer.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java rename src/main/java/net/hostsharing/hsadminng/{ => rbac}/context/Context.java (85%) delete mode 100644 src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/config/DisableSecurityConfig.java create mode 100644 src/test/java/net/hostsharing/hsadminng/config/JwtAuthenticationFilterIntegrationTest.java rename src/test/java/net/hostsharing/hsadminng/config/{MessageTranslatorUnitTest.java => MessageTranslatorIntegrationTest.java} (92%) create mode 100644 src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigForWebMvcTests.java diff --git a/.aliases b/.aliases index 2d6b0146..96a0b970 100644 --- a/.aliases +++ b/.aliases @@ -74,11 +74,15 @@ function importLegacyData() { alias gw-importHostingAssets='importLegacyData importHostingAssets' function gradlewBootRun() { - local port=${1:-8080} - shift + local serverPort=${1:-8080}; shift + local managementPort=${2:-$((serverPort + 1))}; shift local additional_args="$@" - echo gw bootRun --args="--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data --server.port=${port} ${additional_args}" - ./gradlew bootRun --args="--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data --server.port=${port} ${additional_args}" + unset HSADMINNG_JWT_ISSUER + unset HSADMINNG_JWT_JWKS_URL + unset HSADMINNG_JWT_TOKEN_URL + set -x + ./gradlew bootRun --args="--spring.profiles.active=dev,fake-jwt,complete,test-data --server.port=${serverPort} --management.server.port=${managementPort} ${additional_args}" + set +x } alias gw-bootRun=gradlewBootRun @@ -97,7 +101,7 @@ alias pg-sql-restore='gunzip --stdout | docker exec -i hsadmin-ng-postgres psql alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l' -alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources' +alias gw-spotless='./gradlew compile spotlessApply -x pitest -x test -x :processResources' alias gw-check='. .aliases; . .tc-environment; gw test check -x pitest' # HOWTO: run all 'normal' tests (by default without scenario+import-tests): `gw-test` @@ -143,7 +147,7 @@ function _gwTest() { alias gw-test=_gwTest alias howto=bin/howto -alias cas-curl=bin/cas-curl +alias jwt-curl=bin/jwt-curl # etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries alias gw-importHostingAssets-in-docker-compose=' diff --git a/.tc-environment b/.tc-environment index 194e6d52..665a2123 100644 --- a/.tc-environment +++ b/.tc-environment @@ -3,6 +3,7 @@ source .unset-environment export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin export HSADMINNG_SUPERUSER=import-superuser@hostsharing.net -export HSADMINNG_CAS_SERVER= +export HSADMINNG_OFFICE_DATA_SQL_FILE +export HSADMINNG_JWT_TOKEN_URL=http://localhost:8080/fake-jwt/token export LANG=en_US.UTF-8 diff --git a/.unset-environment b/.unset-environment index 69a50ee3..4fc29b63 100644 --- a/.unset-environment +++ b/.unset-environment @@ -5,5 +5,10 @@ unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME unset HSADMINNG_SUPERUSER unset HSADMINNG_MIGRATION_DATA_PATH unset HSADMINNG_OFFICE_DATA_SQL_FILE -unset HSADMINNG_CAS_SERVER= + +unset HSADMINNG_JWT_ISSUER +unset HSADMINNG_JWT_JWKS_URL +unset HSADMINNG_JWT_USERNAME +unset HSADMINNG_JWT_PASSWORD +unset HSADMINNG_JWT_TOKEN_URL diff --git a/README.md b/README.md index 0e553c08..a31e3a46 100644 --- a/README.md +++ b/README.md @@ -87,13 +87,10 @@ If you have at least Docker and the Java JDK installed in appropriate versions a # if the container has been built already and you want to keep the data, run this: pg-sql-start -Next, compile and run the application on `localhost:8080` and the management server on `localhost:8081`: +Next, compile and run the application with in dev-mode with all modules, test-data and fake-JWT-authentication:: - # this disables CAS-authentication, for using the REST-API with CAS-authentication, see `bin/cas-curl`. - export HSADMINNG_CAS_SERVER= - - # this runs the application with test-data and all modules: - gw bootRun --args='--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data' + # on `localhost:8080` and the management server on `localhost:8081`: + gw-bootRun # there is also an alias which takes an optional port as an argument: gw-bootRun 8888 @@ -101,53 +98,75 @@ Next, compile and run the application on `localhost:8080` and the management ser The meaning of these profiles is: - **dev**: the PostgreSQL users are created via Liquibase -- **fakeCasAuthenticator**: The username is simply taken from whatever is after "Bearer " in the "Authorization" header. +- **fake-jwt**: the app starts with a build-in fake OAuth2/JWT server - **complete**: all modules are started - **test-data**: some test data inserted -Now we can access the REST API, e.g. using curl: +Now we can access the REST API, e.g. using curl. But you need to use JWT authentication. +To make this a bit easier to handle, we use `bin/jwt-curl` (or `jwt-curl` alias). - # the following command should reply with "pong": - curl -f -s http://localhost:8080/api/ping +Make sure you replace `8080` with the port you used to run the application.` + + # the following command does not need authentication and should reply with "pinged ...". + curl http://localhost:8080/api/ping + + # but when you try endpoints which need authentication, you will get a 401 error: + curl http://localhost:8080/api/pong + + # For the follinging commands we need to be authenticated by a valid JWT token. + # To make JWT handling a bit easier, there is a wrapper scropt `jwt-curl`. + # Make sure the following variable is set to the fake JWT issuer: + export HSADMINNG_JWT_TOKEN_URL=http://localhost:8080/fake-jwt/token + + # optionally, you can set the username and password to in env-vars as well: + export HSADMINNG_JWT_USERNAME=superuser-alex@hostsharing.net + export HSADMINNG_JWT_PASSWORD=whatever-as-its-not-checked-by-fake-jwt-auth + + # also optionally, you can login explicitly: + jwt-curl login + + # now, the following command should reply with "ponged ... superuser-alex@hostsharing.net": + jwt-curl GET http://localhost:8080/api/pong # the following command should return a JSON array with just all customers: - curl -f -s\ - -H 'Authorization: Bearer superuser-alex@hostsharing.net' \ - http://localhost:8080/api/test/customers \ + jwt-curl GET http://localhost:8080/api/test/customers \ | jq # just if `jq` is installed, to prettyprint the output # the following command should return a JSON array with just all packages visible for the admin of the customer yyy: - curl -f -s\ - -H 'Authorization: Bearer superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \ - http://localhost:8080/api/test/packages \ + jwt-curl ASSUME 'rbactest.customer#yyy:ADMIN' + jwt-curl GET http://localhost:8080/api/test/packages \ | jq + jwt-curl UNASSUME # add a new customer - curl -f -s\ - -H 'Authorization: Bearer superuser-alex@hostsharing.net' -H "Content-Type: application/json" \ + jwt-curl POST \ -d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \ - -X POST http://localhost:8080/api/test/customers \ + http://localhost:8080/api/test/customers \ | jq If you wonder who 'superuser-alex@hostsharing.net' and 'superuser-fran@hostsharing.net' are and where the data comes from: -Mike and Sven are just example global admin accounts as part of the example data which is automatically inserted in Testcontainers and Development environments. -Also try for example 'admin@xxx.example.com' or 'unknown@example.org'. +Alex and Fran are just example global admin accounts as part of the example data which is automatically inserted in Testcontainers and Development environments. +Also, for example, try 'admin@xxx.example.com' or 'unknown@example.org'. If you want a formatted JSON output, you can pipe the result to `jq` or similar. -And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html). -For a locally running app without CAS-authentication (export HSADMINNG_CAS_SERVER=''), -authorize using the name of the subject (e.g. "superuser-alex@hostsharing.net" in case of test-data). -Otherwise, use a valid CAS-ticket. +And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html. -If you want to run the application with real CAS-Authentication: +If you want to run the application with real (OAuth2) JWT-authentication: - # set the CAS-SERVER-Root, also see `bin/cas-curl`. - export HSADMINNG_CAS_SERVER=https://login.hostsharing.net # or whatever your CAS-Server-URL you want to use + # set the JWT-issuer URI, e.g. + export HSADMINNG_JWT_ISSUER=https://login.hshsngdev.hs-example.de/realms/HSAdminDEV - # run the application against the real CAS authenticator - gw bootRun --args='--spring.profiles.active=dev,realCasAuthenticator,complete,test-data' + # and the JWT JWKS callback URI: + export HSADMINNG_JWT_JWKS_URL=https://login.hshsngdev.hs-example.de/realms/HSAdminDEV/.well-known/openid-configuration + # as well as the JWT token endpoint URI: + export HSADMINNG_JWT_TOKEN_URL=https://login.hshsngdev.hs-example.de/realms/HSAdminDEV/protocol/openid-connect/token + + # run the application against the specified JWT authenticator, do NOT add the 'fake-jwt' profile: + gw bootRun --args='--spring.profiles.active=dev,complete,test-data' + +Also run `bin/jwt-curl` (or the alias `jwt-curl`) without any parameters to see the available commands. ### PostgreSQL Server @@ -156,7 +175,7 @@ You might amend the port and user settings in `src/main/resources/application.ym But the easiest way to run PostgreSQL is via Docker. -Initially, pull an image compatible to current PostgreSQL version of Hostsharing: +Initially, pull an image compatible to the current PostgreSQL version of Hostsharing: docker pull postgres:15.5-bookworm @@ -674,7 +693,7 @@ howto Add `--args='--spring.profiles.active=...` with the wanted profile selector: ```sh -gw bootRun --args='--spring.profiles.active=fakeCasAuthenticator,external-db,only-prod-schema,without-test-data' +gw bootRun --args='--spring.profiles.active=external-db,only-prod-schema,without-test-data' ``` These profiles mean: @@ -712,7 +731,7 @@ If it's selected, just hit the *bug*-symbol next to it. If you frequently need to run with a fresh database and a clean build, you can use this: ```sh -export HSADMINNG_CAS_SERVER= +# replace `gw bootRun` by the proper command as described above gw clean && pg-sql-reset && sleep 5 && gw bootRun' 2>&1 | tee log ``` @@ -851,9 +870,16 @@ This port can be changed in Or on the command line, add ` --server.port=...` to the `--args` parameter of the `bootRun` task, e.g.: ```sh -gw bootRun --args='--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data --server.port=8888' +gw bootRun --args='--spring.profiles.active=dev,fake-jwt,complete,test-data --server.port=8888' ``` +or, for local development, simply: + +```sh +gw-bootRun 8888 +``` + + ### How to Use a Persistent Database for Integration Tests? Usually, the `DataJpaTest` integration tests run against a database in a temporary docker container. @@ -888,7 +914,7 @@ Therefore, during initial development, it's good approach just to amend the exis ```shell pg-sql-reset -gw bootRun +gw bootRun # with the proper command line arguments ``` **⚠** diff --git a/bin/cas-curl b/bin/cas-curl deleted file mode 100755 index af9cf541..00000000 --- a/bin/cas-curl +++ /dev/null @@ -1,261 +0,0 @@ -#!/bin/bash - -if [ "$2" == "--show-password" ]; then - HSADMINNG_CAS_SHOW_PASSWORD=yes - shift -else - HSADMINNG_CAS_SHOW_PASSWORD= -fi - -if [ "$1" == "--trace" ]; then - function trace() { - echo "$*" >&2 - } - function doCurl() { - set -x - if [ -z "$HSADMINNG_CAS_ASSUME" ]; then - curl --fail-with-body \ - --header "Authorization: $HSADMINNG_CAS_TICKET" \ - "$@" - else - curl --fail-with-body \ - --header "Authorization: $HSADMINNG_CAS_TICKET" \ - --header "assumed-roles: $HSADMINNG_CAS_ASSUME" \ - "$@" - fi - set +x - } - shift -else - function trace() { - : # noop - } - function doCurl() { - curl --fail-with-body --header "Authorization: $HSADMINNG_CAS_TICKET" "$@" - } -fi - -export HSADMINNG_CAS_ASSUME_HEADER -if [ -f ~/.cas-curl-assume ]; then - HSADMINNG_CAS_ASSUME="$(cat ~/.cas-curl-assume)" -else - HSADMINNG_CAS_ASSUME= -fi - -if [ -z "$HSADMINNG_CAS_LOGIN" ] || [ -z "$HSADMINNG_CAS_VALIDATE" ] || \ - [ -z "$HSADMINNG_CAS_SERVICE_ID" ]; then - cat >&2 <> - export HSADMINNG_CAS_SERVICE_ID=https://hsadminng.hostsharing.net:443/ -EOF - exit 1 -fi - -function casCurlDocumentation() { - cat <> [parameters] - - commands: -EOF - # filters out help texts (containing double-# and following lines with leading single-#) from the commands itself - # (the '' makes sure that this line is not found, just the lines with actual help texts) - sed -n '/#''#/ {x; p; x; s/#''#//; p; :a; n; /^[[:space:]]*#/!b; s/^[[:space:]]*#//; p; ba}' <$0 -} - -function casLogin() { - # ticket granting ticket exists and not expired? - if find ~/.cas-login-tgt -type f -size +0c -mmin -60 2>/dev/null | grep -q .; then - return - fi - - if [ -z "$HSADMINNG_CAS_USERNAME" ]; then - read -e -p "Username: " HSADMINNG_CAS_USERNAME - fi - - if [ -z "$HSADMINNG_CAS_PASSWORD" ]; then - read -s -e -p "Password: " HSADMINNG_CAS_PASSWORD - fi - - if [ "$HSADMINNG_CAS_SHOW_PASSWORD" == "--show-password" ]; then - HSADMINNG_CAS_PASSWORD_DISPLAY=$HSADMINNG_CAS_PASSWORD - else - HSADMINNG_CAS_PASSWORD_DISPLAY="<