├── .github └── workflows │ └── maven.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── devsupport ├── haauthcodegeneration │ └── create-database-and-user.sql └── keycloak │ ├── create-database-and-user.sql │ └── realm-bag-pts-localhost.json ├── docker-compose.yml ├── docker └── docker-compose.yml ├── lombok.config ├── pom.xml └── src ├── main ├── java │ └── ch │ │ └── admin │ │ └── bag │ │ └── covidcode │ │ └── authcodegeneration │ │ ├── AuthCodeGenerationServiceApplication.java │ │ ├── api │ │ ├── AuthorizationCodeCreateDto.java │ │ ├── AuthorizationCodeOnsetResponseDto.java │ │ ├── AuthorizationCodeResponseDto.java │ │ ├── AuthorizationCodeVerificationDto.java │ │ ├── AuthorizationCodeVerifyResponseDto.java │ │ ├── AuthorizationCodeVerifyResponseDtoWrapper.java │ │ └── TokenType.java │ │ ├── config │ │ ├── RestConfig.java │ │ └── security │ │ │ ├── AuthorizationServerConfigProperties.java │ │ │ ├── DefaultDenyAllWebSecurityConfiguration.java │ │ │ ├── OAuth2SecuredWebConfiguration.java │ │ │ ├── ResourceServerConfigProperties.java │ │ │ ├── authentication │ │ │ ├── JeapAuthenticationContext.java │ │ │ ├── JeapAuthenticationConverter.java │ │ │ ├── JeapAuthenticationToken.java │ │ │ └── ServletJeapAuthorization.java │ │ │ └── validation │ │ │ ├── AudienceJwtValidator.java │ │ │ ├── ContextIssuerJwtValidator.java │ │ │ ├── JeapJwtDecoder.java │ │ │ ├── JeapJwtDecoderFactory.java │ │ │ └── RawJwtTokenParser.java │ │ ├── domain │ │ ├── AuthorizationCode.java │ │ └── AuthorizationCodeRepository.java │ │ ├── lockdown │ │ ├── LockdownException.java │ │ ├── LockdownInterceptor.java │ │ ├── ResponseStatusExceptionHandler.java │ │ └── config │ │ │ ├── Endpoint.java │ │ │ └── LockdownConfig.java │ │ ├── service │ │ ├── AuthCodeDeletionService.java │ │ ├── AuthCodeGenerationService.java │ │ ├── AuthCodeVerificationService.java │ │ └── CustomTokenProvider.java │ │ └── web │ │ ├── config │ │ └── OpenApiConfig.java │ │ ├── controller │ │ ├── AuthCodeGenerationController.java │ │ ├── AuthCodeVerificationController.java │ │ └── AuthCodeVerificationControllerV2.java │ │ ├── monitoring │ │ ├── ActuatorConfig.java │ │ ├── ActuatorSecurity.java │ │ ├── AuthorizationCodeCountMeter.java │ │ └── HealthMetricsConfig.java │ │ └── security │ │ ├── HttpResponseHeaderFilter.java │ │ └── WebSecurityConfig.java └── resources │ ├── application-abn.yml │ ├── application-dev.yml │ ├── application-keycloak-local.yml │ ├── application-local.yml │ ├── application-lockdown.yml │ ├── application-prod.yml │ ├── application-test.yml │ ├── application.yml │ ├── db │ └── migration │ │ ├── common │ │ ├── V1_0_0__create-schema.sql │ │ ├── V1_0_1__alter_varchar_length_of_code_column.sql │ │ └── V1_0_2__new_original_onset_date_column.sql │ │ └── postgresql │ │ └── afterMigrate.sql │ └── logback-spring.xml └── test └── java └── ch └── admin └── bag └── covidcode └── authcodegeneration ├── domain └── AuthorizationCodeRepositoryTest.java ├── lockdown ├── LockdownConfigTest.java └── LockdownInterceptorTest.java ├── service ├── AuthCodeDeletionServiceITTest.java ├── AuthCodeDeletionServiceTest.java ├── AuthCodeGenerationServiceTest.java ├── AuthCodeVerificationServiceTest.java └── CustomTokenProviderTest.java ├── testutil ├── JwtTestUtil.java ├── KeyPairTestUtil.java ├── LocalDateSerializer.java └── LoggerTestUtil.java └── web ├── controller ├── AuthCodeGenerationControllerSecurityTest.java ├── AuthCodeGenerationControllerTest.java ├── AuthCodeVerificationControllerSecurityTest.java ├── AuthCodeVerificationControllerTest.java ├── AuthCodeVerificationControllerV2SecurityTest.java └── AuthCodeVerificationControllerV2Test.java └── monitoring └── AuthorizationCodeCountMeterTest.java /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | name: Java CI with Maven 4 | 5 | on: 6 | push: 7 | branches: [ develop ] 8 | pull_request: 9 | branches: [ develop ] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 17.0.4 23 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 24 | - name: Build with Maven 25 | run: 26 | mvn install 27 | env: 28 | GITHUB_TOKEN: ${{ github.token }} 29 | - name: Echo the current ref 30 | run: echo "${{ github.ref }}" 31 | - name: Create Snapshot Release 32 | uses: ncipollo/release-action@v1 33 | if: github.ref == 'refs/heads/develop' 34 | id: create_release 35 | with: 36 | name: Snapshot Release ${{ github.ref }} 37 | tag: SNAPSHOT 38 | artifacts: "target/ha-authcode-generation-service.jar" 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | draft: false 41 | prerelease: false 42 | allowUpdates: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea/* 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | 33 | *.log 34 | /log.log* 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk11 5 | 6 | # see https://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure 7 | sudo: false 8 | 9 | # cache the build tool's caches 10 | cache: 11 | directories: 12 | - $HOME/.m2 13 | - $HOME/.gradle 14 | 15 | addons: 16 | sonarcloud: 17 | organization: "admin-ch" 18 | 19 | script: 20 | - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent verify sonar:sonar 21 | 22 | before_deploy: 23 | # Set up git user name and tag this commit 24 | - export TZ=Europe/Zurich 25 | - export TRAVIS_TAG=${TRAVIS_TAG:-$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)} 26 | - git tag $TRAVIS_TAG 27 | 28 | deploy: 29 | provider: releases 30 | api_key: $GITHUB_TOKEN 31 | file: target/ha-authcode-generation-service.jar 32 | skip_cleanup: true 33 | on: 34 | branch: master 35 | 36 | branches: 37 | except: 38 | - /^[v]*\d+\.\d+(\.\d+)?(-\S*)?$ 39 | - /^\d{14}-\S*$ 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Swiss Admin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HA-AuthCode-Generation-Service (CovidCode-Service) 2 | HA-AuthCode-Generation-Service is an authorization code generation service for the CovidCode-UI and the proximity tracing app. 3 | 4 | # Reproducible Builds 5 | In order to have reproducible builds the io.github.zlika maven plugin is used. It replaces all timestamp with the timestamp of the last commit, and orders the entries in the JAR alphabetically. The github action then computes the sha256sum of the resulting JAR and adds the output as an build artifact. 6 | 7 | # Developer Instructions 8 | 9 | ## Initial setup 10 | 11 | Do this once: 12 | 13 | 1. Install a JDK (tested with Oracle JDK v11 and OpenjDK 1.8.0) 14 | 1. [Install Maven](https://maven.apache.org/install.html) 15 | 1. Install [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) 16 | 1. Check out [CovidCode-UI](https://github.com/admin-ch/CovidCode-UI) in another directory 17 | 18 | ## Development Cycle 19 | 20 | Do this at the beginning of your session: 21 | 1. Run
docker-compose up -d
 22 | docker-compose logs -f
and wait for the logs to become quiescent 23 | 1. Run CovidCode-UI in another window (`ng serve`) 24 | 25 | To run manual tests, you can run CovidCode-Service with the `local` 26 | and `keycloak-local` Spring profiles using the following command: 27 | ``` 28 | mvn compile exec:java 29 | ``` 30 | (or the equivalent using your IDE's Maven functionality, if you 31 | require access to a debugger) 32 | 33 | To run the test suite: 34 | ``` 35 | mvn verify 36 | ``` 37 | 38 | To perform a clean build, and run the test suite with full code coverage 39 | and upload the data to a locally-running SonarQube: 40 | ``` 41 | mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent verify sonar:sonar 42 | ``` 43 | SonarQube results are thereafter visible at http://localhost:9000/ 44 | 45 | To tear down the development support environment (but retain its state on-disk): 46 | ``` 47 | docker-compose down 48 | ``` 49 | 50 | To wipe everything: 51 | ``` 52 | docker-compose down 53 | docker volume rm covidcode_dbdata 54 | mvn clean 55 | ``` 56 | 57 | ## Swagger-UI 58 | Swagger-UI is running on http://localhost:8113/swagger-ui.html. 59 | 60 | ## Local KeyCloak instance 61 | 62 | If CovidCode-Service is being run as suggested above, it will perform 63 | authentication and access control against an OIDC / OAuth server 64 | running on http://localhost:8180/ (and so will CovidCode-UI in its 65 | default development configuration). 66 | 67 | The credentials for the KeyCloak administrator are visible in 68 | docker-compose.yml in section `keycloak:`. Additionally, KeyCloak is 69 | automatically pre-populated with a `bag-pts` realm, containing a 70 | `doctor` account (password `doctor`) that enjoys access to both 71 | CovidCode-UI and CovidCode-Service. 72 | 73 | ## PostgreSQL database 74 | 75 | docker-compose runs a new PostgreSQL database on port 3113 and takes 76 | care of setting it up. The superuser credentials are in 77 | `docker-compose.yml`. 78 | 79 | The "local" Spring profile should be used to run the application (see above). 80 | The other profiles run the script afterMigrate to reassign the owner of the objects. 81 | 82 | ### Dockerfile 83 | The docker file is provided only to run the application locally without DB. This configuration starts a PostgreSQL 11 on port 3113. 84 | Docker Official Image from https://hub.docker.com/_/postgres. 85 | 86 | ## JWT Generation 87 | JWT generation uses a custom generator with library JJWT. 88 | 89 | ## Lombok 90 | Project uses Lombok. Configure your IDE with lombok plugin. 91 | 92 | ## Security 93 | The API is secured and a valid JWT should be provided. Note that these 2 values are needed 94 | - ctx:USER 95 | - the audience must be set to ha-authcodegeneration 96 | 97 | ## Configuration 98 | These parameters can be configured. You can find example values in application-local.yml. 99 | 100 | The validity of the generated JWT: 101 | authcodegeneration.jwt.token-validity 102 | 103 | The issuer to set in the generated JWT: 104 | authcodegeneration.jwt.issuer 105 | 106 | The private key to sign the generated JWT: 107 | authcodegeneration.jwt.privateKey 108 | 109 | The Prometheus actuator endpoint is secured with username and password: 110 | authcodegeneration.monitor.prometheus.user 111 | authcodegeneration.monitor.prometheus.password 112 | 113 | The allowed origin configuration for the authcode generation: 114 | ha-authcode-generation-service.allowed-origin 115 | -------------------------------------------------------------------------------- /devsupport/haauthcodegeneration/create-database-and-user.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE haauthcodegeneration; 2 | CREATE USER haauthcodegeneration WITH PASSWORD 'secret'; 3 | ALTER USER haauthcodegeneration WITH SUPERUSER; 4 | GRANT ALL ON DATABASE haauthcodegeneration TO haauthcodegeneration; 5 | 6 | CREATE ROLE haauthcodegeneration_role_full; 7 | GRANT ALL ON DATABASE haauthcodegeneration TO haauthcodegeneration_role_full; 8 | -------------------------------------------------------------------------------- /devsupport/keycloak/create-database-and-user.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE keycloak; 2 | CREATE USER keycloak WITH PASSWORD 'keycloak'; 3 | GRANT ALL ON DATABASE keycloak TO keycloak; 4 | -------------------------------------------------------------------------------- /devsupport/keycloak/realm-bag-pts-localhost.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "BAG-PTS", 3 | "realm": "bag-pts", 4 | "enabled": true, 5 | "clients": [ 6 | { 7 | "clientId": "ha-ui-web-client", 8 | "rootUrl": "https://www.covidcode-d.admin.ch", 9 | "adminUrl": "", 10 | "publicClient": true, 11 | "surrogateAuthRequired": false, 12 | "enabled": true, 13 | "redirectUris": [ 14 | "http://localhost:4200/*" 15 | ], 16 | "webOrigins": [ 17 | "http://localhost:4200" 18 | ], 19 | "protocolMappers": [ 20 | { 21 | "id": "showRolesInUserinfoAsUserroles", 22 | "name": "Realm Mapper", 23 | "protocol": "openid-connect", 24 | "protocolMapper": "oidc-usermodel-realm-role-mapper", 25 | "consentRequired": false, 26 | "config": { 27 | "usermodel.clientRoleMapping.rolePrefix": "bag-pts-", 28 | "multivalued": "true", 29 | "userinfo.token.claim": "true", 30 | "id.token.claim": "true", 31 | "access.token.claim": "true", 32 | "claim.name": "userroles", 33 | "jsonType.label": "String" 34 | } 35 | }, 36 | { 37 | "id": "hardcodedCtxClaim", 38 | "name": "Context Claim", 39 | "protocol": "openid-connect", 40 | "protocolMapper": "oidc-hardcoded-claim-mapper", 41 | "consentRequired": false, 42 | "config": { 43 | "userinfo.token.claim": "false", 44 | "id.token.claim": "true", 45 | "access.token.claim": "true", 46 | "claim.name": "ctx", 47 | "claim.value": "USER", 48 | "jsonType.label": "String" 49 | } 50 | } 51 | ] 52 | } 53 | ], 54 | "roles": { 55 | "realm": [ 56 | { 57 | "name": "bag-pts-allow", 58 | "description": "Grant this role to users, so that they can use ha-ui", 59 | "composite": false, 60 | "clientRole": false, 61 | "containerId": "BAG-PTS", 62 | "attributes": {} 63 | } 64 | ] 65 | }, 66 | "users" : [ 67 | { 68 | "username" : "doctor", 69 | "enabled": true, 70 | "email" : "doctor@example.com", 71 | "firstName": "Doctor", 72 | "lastName": "Example", 73 | "credentials" : [ 74 | { "type" : "password", 75 | "value" : "doctor" } 76 | ], 77 | "realmRoles": [ "bag-pts-allow" ] 78 | } 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml for developer support 2 | # 3 | # Usage: 4 | # 5 | # docker-compose up -d 6 | # 7 | # Port allocation scheme: 8 | # 3113 The PostgreSQL database 9 | # 4200 The Angular UI (not part of this project) 10 | # 8113 The covidcode back-end server (not managed by docker-compose) 11 | # 8180 The Keycloak server, exposed through Træfik with some URL rewriting 12 | # 9000 SonarQube, a source code linter and metrics renderer (e.g. for test coverage) 13 | 14 | version: "3" 15 | 16 | # To purge all state, stop all containers and say 17 | # 18 | # docker volume rm covidcode_dbdata 19 | # 20 | # This will erase the PostgreSQL database. Then start everything again 21 | volumes: 22 | dbdata: 23 | 24 | services: 25 | 26 | db: 27 | image: "postgres:11" 28 | container_name: "dp3t_postgres" 29 | ports: 30 | - "3113:5432" 31 | environment: 32 | POSTGRES_PASSWORD: secret 33 | volumes: 34 | - dbdata:/var/lib/postgresql/data 35 | - ./devsupport/keycloak/create-database-and-user.sql:/docker-entrypoint-initdb.d/create-keycloak-database-and-user.sql 36 | - ./devsupport/haauthcodegeneration/create-database-and-user.sql:/docker-entrypoint-initdb.d/create-haauthcodegeneration-database-and-user.sql 37 | 38 | keycloak: 39 | image: jboss/keycloak 40 | container_name: "keycloak" 41 | environment: 42 | # https://hub.docker.com/r/jboss/keycloak 43 | KEYCLOAK_USER: admin 44 | KEYCLOAK_PASSWORD: masterPassword 45 | DB_VENDOR: postgres 46 | DB_ADDR: db 47 | DB_DATABASE: keycloak 48 | DB_USER: keycloak 49 | DB_PASSWORD: keycloak 50 | KEYCLOAK_IMPORT: /tmp/realm-bag-pts-localhost.json 51 | volumes: 52 | - ./devsupport/keycloak/realm-bag-pts-localhost.json:/tmp/realm-bag-pts-localhost.json 53 | labels: 54 | - "traefik.enable=true" 55 | - "traefik.http.routers.keycloak.entrypoints=web" 56 | - "traefik.http.routers.keycloak.rule=PathPrefix(`/`)" # i.e. accept anything 57 | # Rewrite URLs so that e.g. 58 | # http://localhost:8180/.well-known/openid-configuration 59 | # works (as expected by ha-ui in its dev configuration): 60 | - "traefik.http.routers.keycloak.middlewares=rewrite-url-oidc" 61 | - "traefik.http.middlewares.rewrite-url-oidc.replacepathregex.regex=^/(\\.well-known/.*)$$" 62 | - "traefik.http.middlewares.rewrite-url-oidc.replacepathregex.replacement=/auth/realms/bag-pts/$$1" 63 | 64 | traefik: 65 | image: traefik:2.2.1 66 | volumes: 67 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 68 | command: 69 | - "--entrypoints.web.address=:80" 70 | ## Enable docker provider 71 | - "--providers.docker=true" 72 | ## Do not expose containers unless explicitly told so 73 | - "--providers.docker.exposedbydefault=false" 74 | ## Uncomment the following two lines to turn on the Træfik 75 | ## dashboard (handy for troubleshooting errors in the 76 | ## `traefik.*` labels, above): 77 | # - "--api.dashboard=true" 78 | # - "--api.insecure=true" 79 | ports: 80 | - "8180:80" 81 | ## Uncomment the following line to expose the Træfik dashboard 82 | ## on port 8080: 83 | # - "8080:8080" 84 | 85 | sonarqube: 86 | image: sonarqube:community 87 | ports: 88 | - "9000:9000" 89 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db-authcodegeneration: 4 | image: postgres 5 | ports: 6 | - "3113:5432" 7 | environment: 8 | - POSTGRES_USER=haauthcodegeneration 9 | - POSTGRES_PASSWORD=secret 10 | - POSTGRES_DB=haauthcodegeneration 11 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling = true 2 | lombok.addLombokGeneratedAnnotation = true -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.6 9 | 10 | 11 | ch.admin.bag.covidcode 12 | ha-authcode-generation-service 13 | 1.0.5-SNAPSHOT 14 | ha-authcode-generation-service 15 | Service for generating an authorization code for the proximity tracing app 16 | 17 | 18 | 17 19 | 20 | 2021.0.4 21 | 3.1.5 22 | 7.2 23 | 3.1.9 24 | 0.11.5 25 | 1.33 26 | 2.4.1 27 | 28 | 1.6.14 29 | 0.8.7 30 | 0.12 31 | 32 | true 33 | 34 | 35 | **/org/**/*.java,**/com/**/*.java,**/config/security/**/*.java 36 | 37 | 38 | **/*Dto.java 39 | 40 | 41 | **/*Dto.java,**/config/*,**/*Exception.java,**/*Constants.java,**/*Registry.java,**/*Config.java,**/*Mock*,**/*Application.java,**/*HttpResponseHeaderFilter.java,**/*ActuatorSecurity.java 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.cloud 49 | spring-cloud-dependencies 50 | ${spring-cloud.version} 51 | pom 52 | import 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-data-jpa 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-data-rest 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-web 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-configuration-processor 74 | true 75 | 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-actuator 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-starter-security 85 | 86 | 87 | io.micrometer 88 | micrometer-registry-prometheus 89 | 90 | 91 | 92 | 93 | org.springframework.cloud 94 | spring-cloud-starter-sleuth 95 | ${spring-cloud-sleuth.version} 96 | 97 | 98 | net.logstash.logback 99 | logstash-logback-encoder 100 | ${logstash.version} 101 | 102 | 103 | org.codehaus.janino 104 | janino 105 | ${janino.version} 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-starter-oauth2-resource-server 111 | 112 | 113 | org.springframework.boot 114 | spring-boot-starter-oauth2-client 115 | 116 | 117 | javax.servlet 118 | javax.servlet-api 119 | provided 120 | 121 | 122 | 123 | org.flywaydb 124 | flyway-core 125 | 126 | 127 | org.postgresql 128 | postgresql 129 | runtime 130 | 131 | 132 | 133 | org.apache.commons 134 | commons-lang3 135 | 136 | 137 | 138 | org.springdoc 139 | springdoc-openapi-ui 140 | ${springdoc.version} 141 | 142 | 143 | org.springdoc 144 | springdoc-openapi-webmvc-core 145 | ${springdoc.version} 146 | 147 | 148 | 149 | org.projectlombok 150 | lombok 151 | 152 | 153 | 154 | io.jsonwebtoken 155 | jjwt-api 156 | ${jsonwebtoken.version} 157 | 158 | 159 | io.jsonwebtoken 160 | jjwt-impl 161 | ${jsonwebtoken.version} 162 | runtime 163 | 164 | 165 | io.jsonwebtoken 166 | jjwt-jackson 167 | ${jsonwebtoken.version} 168 | runtime 169 | 170 | 171 | 172 | org.yaml 173 | snakeyaml 174 | ${snakeyaml.version} 175 | 176 | 177 | 178 | io.pivotal.cfenv 179 | java-cfenv-boot 180 | ${java-cfenv-boot.version} 181 | 182 | 183 | 184 | 185 | org.springframework.boot 186 | spring-boot-starter-test 187 | test 188 | 189 | 190 | junit-vintage-engine 191 | org.junit.vintage 192 | 193 | 194 | 195 | 196 | com.h2database 197 | h2 198 | test 199 | 200 | 201 | net.therore.logback 202 | therore-logback 203 | 1.0.0 204 | test 205 | 206 | 207 | com.github.tomakehurst 208 | wiremock-standalone 209 | 2.16.0 210 | test 211 | 212 | 213 | 214 | 215 | ha-authcode-generation-service 216 | 217 | 218 | org.springframework.boot 219 | spring-boot-maven-plugin 220 | 221 | 222 | org.jacoco 223 | jacoco-maven-plugin 224 | 225 | 226 | report 227 | 228 | report 229 | 230 | verify 231 | 232 | 233 | 234 | 235 | org.apache.maven.plugins 236 | maven-surefire-plugin 237 | ${maven-surefire-plugin.version} 238 | 239 | false 240 | 241 | 242 | 243 | 244 | org.codehaus.mojo 245 | exec-maven-plugin 246 | 1.2.1 247 | 248 | ch.admin.bag.covidcode.authcodegeneration.AuthCodeGenerationServiceApplication 249 | 250 | --spring.profiles.active=local,keycloak-local 251 | 252 | 253 | 254 | 255 | org.apache.maven.plugins 256 | maven-failsafe-plugin 257 | ${maven-failsafe-plugin.version} 258 | 259 | 260 | 261 | io.github.zlika 262 | reproducible-build-maven-plugin 263 | ${reproducible-build-maven-plugin.version} 264 | 265 | 266 | strip-jar 267 | package 268 | 269 | strip-jar 270 | 271 | 272 | 273 | strip-archive 274 | pre-integration-test 275 | 276 | strip-jar 277 | 278 | 279 | ${git.commit.time} 280 | 281 | 282 | 283 | 284 | 285 | 286 | pl.project13.maven 287 | git-commit-id-plugin 288 | 289 | 290 | retrieve-git-info 291 | prepare-package 292 | 293 | revision 294 | 295 | 296 | 297 | 298 | true 299 | true 300 | false 301 | yyyyMMddHHmmss 302 | UTC 303 | false 304 | 305 | 306 | 307 | 308 | org.apache.maven.plugins 309 | maven-jar-plugin 310 | 311 | 312 | 313 | ${git.commit.id} 314 | ${git.commit.time} 315 | true 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | org.jacoco 325 | jacoco-maven-plugin 326 | ${jacoco-maven-plugin.version} 327 | 328 | 329 | 330 | 331 | 332 | 333 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/AuthCodeGenerationServiceApplication.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.boot.web.servlet.ServletComponentScan; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | 11 | @ServletComponentScan 12 | @SpringBootApplication 13 | @EnableConfigurationProperties 14 | @EnableScheduling 15 | @Slf4j 16 | public class AuthCodeGenerationServiceApplication { 17 | 18 | public static void main(String[] args) { 19 | 20 | Environment env = SpringApplication.run(AuthCodeGenerationServiceApplication.class, args).getEnvironment(); 21 | 22 | String protocol = "http"; 23 | if (env.getProperty("server.ssl.key-store") != null) { 24 | protocol = "https"; 25 | } 26 | log.info("\n----------------------------------------------------------\n\t" + 27 | "Yeah!!! {} is running! \n\t" + 28 | "\n" + 29 | "\tSwaggerUI: \t{}://localhost:{}/swagger-ui.html\n\t" + 30 | "Profile(s): \t{}" + 31 | "\n----------------------------------------------------------", 32 | env.getProperty("spring.application.name"), 33 | protocol, 34 | env.getProperty("server.port"), 35 | env.getActiveProfiles()); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/api/AuthorizationCodeCreateDto.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.api; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.time.LocalDate; 10 | 11 | @Getter 12 | @AllArgsConstructor 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @Schema(description = "Dto with information for creating an authorization code.") 15 | public class AuthorizationCodeCreateDto { 16 | 17 | @Schema(description = "Infection date") 18 | private LocalDate onsetDate; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/api/AuthorizationCodeOnsetResponseDto.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.api; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | public class AuthorizationCodeOnsetResponseDto { 12 | 13 | private String onset; 14 | 15 | } -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/api/AuthorizationCodeResponseDto.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.api; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @AllArgsConstructor 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @Schema(description = "Response dto with a 9 digit authorization code.") 13 | public class AuthorizationCodeResponseDto { 14 | 15 | @Schema(description = "9 digit authorization code") 16 | private String authorizationCode; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/api/AuthorizationCodeVerificationDto.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.api; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | public class AuthorizationCodeVerificationDto { 12 | 13 | private String authorizationCode; 14 | 15 | private String fake; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/api/AuthorizationCodeVerifyResponseDto.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.api; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | public class AuthorizationCodeVerifyResponseDto { 12 | 13 | private String accessToken; 14 | 15 | } -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/api/AuthorizationCodeVerifyResponseDtoWrapper.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.api; 2 | 3 | public class AuthorizationCodeVerifyResponseDtoWrapper { 4 | 5 | private AuthorizationCodeVerifyResponseDto dp3tAccessToken; 6 | private AuthorizationCodeVerifyResponseDto checkInAccessToken; 7 | 8 | public AuthorizationCodeVerifyResponseDtoWrapper(AuthorizationCodeVerifyResponseDto dp3tAccessToken, AuthorizationCodeVerifyResponseDto checkInAccessToken) { 9 | this.dp3tAccessToken = dp3tAccessToken; 10 | this.checkInAccessToken = checkInAccessToken; 11 | } 12 | 13 | public AuthorizationCodeVerifyResponseDtoWrapper() {} 14 | 15 | 16 | public AuthorizationCodeVerifyResponseDto getDP3TAccessToken() { 17 | return dp3tAccessToken; 18 | } 19 | 20 | public void setDP3TAccessToken(AuthorizationCodeVerifyResponseDto dp3tAccessToken) { 21 | this.dp3tAccessToken = dp3tAccessToken; 22 | } 23 | 24 | public AuthorizationCodeVerifyResponseDto getCheckInAccessToken() { 25 | return checkInAccessToken; 26 | } 27 | 28 | public void setCheckInAccessToken(AuthorizationCodeVerifyResponseDto checkInAccessToken) { 29 | this.checkInAccessToken = checkInAccessToken; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/api/TokenType.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.api; 2 | 3 | public enum TokenType { 4 | 5 | DP3T_TOKEN("dp3t", "exposed"), CHECKIN_USERUPLOAD_TOKEN("checkin", "userupload"); 6 | 7 | private final String scope; 8 | private final String audience; 9 | 10 | TokenType(String audience, String scope) { 11 | this.audience = audience; 12 | this.scope = scope; 13 | } 14 | 15 | public String getAudience() { 16 | return audience; 17 | } 18 | 19 | public String getScope() { 20 | return scope; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/RestConfig.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.web.client.RestTemplateBuilder; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | import java.time.Duration; 10 | 11 | @Configuration 12 | public class RestConfig { 13 | 14 | @Value("${authcodegeneration.rest.connectTimeoutSeconds}") 15 | private int connectTimeout; 16 | 17 | @Value("${authcodegeneration.rest.readTimeoutSeconds}") 18 | private int readTimeout; 19 | 20 | @Bean 21 | public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { 22 | return restTemplateBuilder 23 | .setConnectTimeout(Duration.ofSeconds(connectTimeout)) 24 | .setReadTimeout(Duration.ofSeconds(readTimeout)) 25 | .build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/AuthorizationServerConfigProperties.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security; 2 | 3 | import com.nimbusds.oauth2.sdk.util.StringUtils; 4 | import lombok.Data; 5 | 6 | /** 7 | * Configuration properties to configure the authorization server that the OAuth2 resource server will accept tokens from. 8 | */ 9 | @Data 10 | public class AuthorizationServerConfigProperties { 11 | 12 | private static final String JWK_SET_URI_SUBPATH = "/protocol/openid-connect/certs"; 13 | 14 | private String issuer; 15 | 16 | private String jwkSetUri; 17 | 18 | public String getJwkSetUri() { 19 | return StringUtils.isNotBlank(jwkSetUri) ? jwkSetUri : issuer + JWK_SET_URI_SUBPATH; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/DefaultDenyAllWebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.ServletJeapAuthorization; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.core.annotation.Order; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | import org.springframework.security.web.authentication.HttpStatusEntryPoint; 15 | 16 | /** 17 | * If the security starter has been added as a dependency but the OAuth2 secured web configuration has not been 18 | * activated e.g. because the needed configuration properties are missing, then deny access to all web endpoints as a 19 | * secure default web security configuration. 20 | */ 21 | @Configuration 22 | @Slf4j 23 | @AutoConfigureAfter(OAuth2SecuredWebConfiguration.class) 24 | public class DefaultDenyAllWebSecurityConfiguration { 25 | 26 | private static final String DENY_ALL_MESSAGE = "jeap-spring-boot-security-starter did not activate OAuth2 resource security " + 27 | "for web endpoints. Activating a 'deny-all' configuration as secure fallback. " + 28 | "Override the 'deny-all' configuration with your own web security configuration " + 29 | "or define the configuration properties needed for the OAuth2 resource security."; 30 | 31 | @Configuration 32 | @Order(500) 33 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 34 | @ConditionalOnMissingBean(ServletJeapAuthorization.class) 35 | @EnableWebSecurity 36 | public static class WebMvcDenyAllWebSecurityConfiguration extends WebSecurityConfigurerAdapter { 37 | 38 | @Override 39 | protected void configure(HttpSecurity http) throws Exception { 40 | log.debug(DENY_ALL_MESSAGE); 41 | http.authorizeRequests().anyRequest().denyAll(). 42 | and(). 43 | exceptionHandling().authenticationEntryPoint((new HttpStatusEntryPoint(HttpStatus.FORBIDDEN))); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/OAuth2SecuredWebConfiguration.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.JeapAuthenticationContext; 4 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.JeapAuthenticationConverter; 5 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.ServletJeapAuthorization; 6 | import ch.admin.bag.covidcode.authcodegeneration.config.security.validation.AudienceJwtValidator; 7 | import ch.admin.bag.covidcode.authcodegeneration.config.security.validation.ContextIssuerJwtValidator; 8 | import ch.admin.bag.covidcode.authcodegeneration.config.security.validation.JeapJwtDecoderFactory; 9 | import com.nimbusds.oauth2.sdk.util.StringUtils; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 15 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 16 | import org.springframework.context.annotation.Bean; 17 | import org.springframework.context.annotation.Configuration; 18 | import org.springframework.core.annotation.Order; 19 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 20 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 21 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 22 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 23 | import org.springframework.security.config.http.SessionCreationPolicy; 24 | import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; 25 | import org.springframework.security.oauth2.core.OAuth2TokenValidator; 26 | import org.springframework.security.oauth2.jwt.Jwt; 27 | import org.springframework.security.oauth2.jwt.JwtDecoder; 28 | import org.springframework.security.oauth2.jwt.JwtTimestampValidator; 29 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 30 | 31 | import java.time.Duration; 32 | import java.util.HashMap; 33 | import java.util.Map; 34 | 35 | @Configuration 36 | @ConditionalOnProperty("jeap.security.oauth2.resourceserver.authorization-server.issuer") 37 | @Slf4j 38 | public class OAuth2SecuredWebConfiguration { 39 | 40 | @Configuration 41 | @EnableConfigurationProperties({ResourceServerConfigProperties.class}) 42 | public static class OAuth2SecuredWebCommonConfigurationProperties { 43 | 44 | private String applicationName; 45 | private ResourceServerConfigProperties resourceServer; 46 | 47 | public OAuth2SecuredWebCommonConfigurationProperties( 48 | @Value("${spring.application.name}") String applicationName, 49 | ResourceServerConfigProperties resourceServer) { 50 | this.applicationName = applicationName; 51 | this.resourceServer = resourceServer; 52 | } 53 | 54 | public String getResourceIdWithFallbackToApplicationName() { 55 | return StringUtils.isNotBlank(resourceServer.getResourceId()) ? resourceServer.getResourceId() : applicationName; 56 | } 57 | 58 | public ResourceServerConfigProperties getResourceServer() { 59 | return resourceServer; 60 | } 61 | } 62 | 63 | @Configuration 64 | @Order(499) 65 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 66 | @EnableWebSecurity 67 | @EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true) 68 | @RequiredArgsConstructor 69 | public static class OAuth2SecuredWebMvcConfiguration extends WebSecurityConfigurerAdapter { 70 | 71 | private final OAuth2SecuredWebCommonConfigurationProperties commonConfiguration; 72 | 73 | @Override 74 | public void configure(HttpSecurity http) throws Exception { 75 | 76 | //All requests must be authenticated 77 | http.authorizeRequests() 78 | .anyRequest() 79 | .fullyAuthenticated(); 80 | 81 | //Enable CORS 82 | http.cors(); 83 | 84 | //Enable CSRF with CookieCsrfTokenRepository as can be used from Angular 85 | http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); 86 | 87 | //No session management is needed, we want stateless 88 | http.sessionManagement() 89 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS); 90 | 91 | //Treat endpoints as OAuth2 resources 92 | http.oauth2ResourceServer(). 93 | jwt(). 94 | decoder(createJwtDecoder()). 95 | jwtAuthenticationConverter(new JeapAuthenticationConverter()); 96 | } 97 | 98 | @Bean 99 | public ServletJeapAuthorization jeapAuthorization() { 100 | return new ServletJeapAuthorization(); 101 | } 102 | 103 | private JwtDecoder createJwtDecoder() { 104 | final String authorizationJwkSetUri = commonConfiguration.getResourceServer().getAuthorizationServer().getJwkSetUri(); 105 | return JeapJwtDecoderFactory.createJwtDecoder(authorizationJwkSetUri, createTokenValidator(commonConfiguration)); 106 | } 107 | } 108 | 109 | static OAuth2TokenValidator createTokenValidator(OAuth2SecuredWebCommonConfigurationProperties commonConfiguration) { 110 | return new DelegatingOAuth2TokenValidator<>( 111 | new JwtTimestampValidator(Duration.ofSeconds(30)), 112 | new AudienceJwtValidator(commonConfiguration.getResourceIdWithFallbackToApplicationName()), 113 | createContextIssuerJwtValidator(commonConfiguration.getResourceServer()) 114 | ); 115 | } 116 | 117 | static ContextIssuerJwtValidator createContextIssuerJwtValidator(ResourceServerConfigProperties resourceServerConfigProperties) { 118 | Map contextIssuers = new HashMap<>(); 119 | final String authorizationServer = resourceServerConfigProperties.getAuthorizationServer().getIssuer(); 120 | contextIssuers.put(JeapAuthenticationContext.USER, authorizationServer); 121 | return new ContextIssuerJwtValidator(contextIssuers); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/ResourceServerConfigProperties.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | /** 8 | * Configuration properties to configure the OAuth2 resource server. 9 | */ 10 | @Setter 11 | @Getter 12 | @ConfigurationProperties("jeap.security.oauth2.resourceserver") 13 | public class ResourceServerConfigProperties { 14 | private String resourceId; 15 | private AuthorizationServerConfigProperties authorizationServer; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/authentication/JeapAuthenticationContext.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.authentication; 2 | 3 | import org.springframework.security.oauth2.jwt.Jwt; 4 | 5 | /** 6 | * The supported authentication contexts. 7 | */ 8 | public enum JeapAuthenticationContext { 9 | 10 | USER; 11 | 12 | private static final String CONTEXT_CLAIM_NAME = "ctx"; 13 | 14 | public static JeapAuthenticationContext readFromJwt(Jwt jwt) { 15 | String context = jwt.getClaimAsString(CONTEXT_CLAIM_NAME); 16 | if(context == null) { 17 | throw new IllegalArgumentException("Context claim '" + CONTEXT_CLAIM_NAME + "' is missing from the JWT."); 18 | } 19 | return JeapAuthenticationContext.valueOf(context); 20 | } 21 | 22 | public static String getContextJwtClaimName() { 23 | return CONTEXT_CLAIM_NAME; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/authentication/JeapAuthenticationConverter.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.authentication; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.lang.NonNull; 6 | import org.springframework.security.authentication.AbstractAuthenticationToken; 7 | import org.springframework.security.oauth2.jwt.Jwt; 8 | 9 | import java.util.*; 10 | 11 | 12 | @Slf4j 13 | public class JeapAuthenticationConverter implements Converter { 14 | 15 | private static final String USER_ROLES_CLAIM = "userroles"; 16 | 17 | @Override 18 | public AbstractAuthenticationToken convert(@NonNull Jwt jwt) { 19 | return new JeapAuthenticationToken(jwt, extractUserRoles(jwt)); 20 | } 21 | 22 | private Set extractUserRoles(Jwt jwt) { 23 | List userrolesClaim = Optional.of(jwt) 24 | .map(Jwt::getClaims) 25 | .flatMap(map -> getIfPossible(map, USER_ROLES_CLAIM, List.class)) 26 | .orElse(Collections.emptyList()); 27 | 28 | Set userRoles = new HashSet<>(); 29 | userrolesClaim.forEach( userroleObject -> { 30 | try { 31 | userRoles.add((String) userroleObject); 32 | } catch (ClassCastException e) { 33 | log.warn("Ignoring non String user role."); 34 | } 35 | }); 36 | 37 | return userRoles; 38 | } 39 | 40 | private Optional getIfPossible(Map map, String key, Class klass) { 41 | Object value = map.get(key); 42 | if (value == null) { 43 | return Optional.empty(); 44 | } 45 | try { 46 | return Optional.of(klass.cast(value)); 47 | } catch (ClassCastException e) { 48 | log.warn("Unable to map value of entry {} to class {}, ignoring the entry.", key, klass.getSimpleName()); 49 | return Optional.empty(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/authentication/JeapAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.authentication; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 5 | import org.springframework.security.oauth2.jwt.Jwt; 6 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 7 | 8 | import java.util.Collection; 9 | import java.util.Collections; 10 | import java.util.Set; 11 | import java.util.stream.Collectors; 12 | 13 | public class JeapAuthenticationToken extends JwtAuthenticationToken { 14 | 15 | private static final String ROLE_PREFIX = "ROLE_"; 16 | 17 | private final Set userRoles; 18 | 19 | public JeapAuthenticationToken(Jwt jwt, Set userRoles) { 20 | super(jwt, deriveAuthoritiesFromRoles(userRoles)); 21 | this.userRoles = Collections.unmodifiableSet(userRoles); 22 | } 23 | 24 | /** 25 | * Get the client id specified in this token. 26 | * 27 | * @return The client id specified in this token. 28 | */ 29 | public String getClientId() { 30 | return getToken().getClaimAsString("clientId"); 31 | } 32 | 33 | /** 34 | * Get the name specified in this token. 35 | * 36 | * @return The name specified in this token. 37 | */ 38 | public String getTokenName() { 39 | return getToken().getClaimAsString("name"); 40 | } 41 | 42 | /** 43 | * Get the given name specified in this token. 44 | * 45 | * @return The given name specified in this token. 46 | */ 47 | public String getTokenGivenName() { 48 | return getToken().getClaimAsString("given_name"); 49 | } 50 | 51 | /** 52 | * Get the family name specified in this token. 53 | * 54 | * @return The family name specified in this token. 55 | */ 56 | public String getTokenFamilyName() { 57 | return getToken().getClaimAsString("family_name"); 58 | } 59 | 60 | /** 61 | * Get the subject specified in this token. 62 | * 63 | * @return The subject specified in this token. 64 | */ 65 | public String getTokenSubject() { 66 | return getToken().getClaimAsString("sub"); 67 | } 68 | 69 | /** 70 | * Get the locale specified in this token. 71 | * 72 | * @return The locale specified in this token. 73 | */ 74 | public String getTokenLocale() { 75 | return getToken().getClaimAsString("locale"); 76 | } 77 | 78 | /** 79 | * Get the jeap authentication context specified in this token. 80 | * 81 | * @return The jeap authentication context specified in this token. 82 | */ 83 | public JeapAuthenticationContext getJeapAuthenticationContext() { 84 | return JeapAuthenticationContext.readFromJwt(getToken()); 85 | } 86 | 87 | /** 88 | * Get the user roles listed in this token. 89 | * 90 | * @return The user roles 91 | */ 92 | public Set getUserRoles() { 93 | return userRoles; 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return String.format( 99 | "JeapAuthenticationToken{ subject (calling user): %s, client (calling system): %s, authorities (all roles): %s, user roles: %s}", 100 | getName(), getClientId(), authoritiesToString(), userRolesToString()); 101 | } 102 | 103 | private static Collection deriveAuthoritiesFromRoles(Set userRoles) { 104 | return userRoles.stream().map(s -> ROLE_PREFIX + s) 105 | .map(SimpleGrantedAuthority::new) 106 | .collect(Collectors.toSet()); 107 | } 108 | 109 | private String authoritiesToString() { 110 | return getAuthorities().stream() 111 | .map(GrantedAuthority::getAuthority) 112 | .map(a -> "'" + a + "'") 113 | .collect(Collectors.joining(",")); 114 | } 115 | 116 | 117 | private String userRolesToString() { 118 | return getUserRoles().stream(). 119 | map( r -> "'" + r + "'"). 120 | collect(Collectors.joining(", ")); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/authentication/ServletJeapAuthorization.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.authentication; 2 | 3 | import org.springframework.security.core.context.SecurityContextHolder; 4 | 5 | /** 6 | * This class provides methods to support authorization needs based on the current security context for Spring WebMvc applications. 7 | */ 8 | public class ServletJeapAuthorization { 9 | 10 | /** 11 | * Fetch the JeapAuthenticationToken from the current security context. 12 | * 13 | * @return The JeapAuthenticationToken extracted from the current security context. 14 | */ 15 | public JeapAuthenticationToken getJeapAuthenticationToken() { 16 | return (JeapAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/validation/AudienceJwtValidator.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.validation; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.security.oauth2.core.OAuth2Error; 5 | import org.springframework.security.oauth2.core.OAuth2TokenValidator; 6 | import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; 7 | import org.springframework.security.oauth2.jwt.Jwt; 8 | 9 | @Slf4j 10 | public class AudienceJwtValidator implements OAuth2TokenValidator { 11 | 12 | private final String audience; 13 | private final OAuth2Error error; 14 | 15 | public AudienceJwtValidator(String audience) { 16 | this.audience = audience; 17 | this.error = new OAuth2Error("invalid_token", "The token is is not valid for audience '" + audience + "'.", null); 18 | } 19 | 20 | public OAuth2TokenValidatorResult validate(Jwt jwt) { 21 | if(jwt.getAudience() == null || jwt.getAudience().isEmpty()) { 22 | //If audience is missing this means token is valid for every system 23 | return OAuth2TokenValidatorResult.success(); 24 | } 25 | if (jwt.getAudience().contains(audience)) { 26 | return OAuth2TokenValidatorResult.success(); 27 | } else { 28 | log.warn(error.getDescription()); 29 | return OAuth2TokenValidatorResult.failure(error); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/validation/ContextIssuerJwtValidator.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.validation; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.JeapAuthenticationContext; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.security.oauth2.core.OAuth2Error; 6 | import org.springframework.security.oauth2.core.OAuth2TokenValidator; 7 | import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; 8 | import org.springframework.security.oauth2.jwt.Jwt; 9 | import org.springframework.security.oauth2.jwt.JwtIssuerValidator; 10 | 11 | import java.util.Collections; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | @Slf4j 16 | /** 17 | * This class implements a JwtValidator that checks if the access token for a given context has been issued by 18 | * a given issuer. 19 | */ 20 | public class ContextIssuerJwtValidator implements OAuth2TokenValidator { 21 | 22 | private final Map> contextValidatorMap; 23 | 24 | public ContextIssuerJwtValidator(Map contextIssuerMap) { 25 | Map> hashMap = new HashMap<>(); 26 | for(JeapAuthenticationContext context : contextIssuerMap.keySet()) { 27 | JwtIssuerValidator validator = new JwtIssuerValidator(contextIssuerMap.get(context)); 28 | hashMap.put(context, validator); 29 | } 30 | this.contextValidatorMap = Collections.unmodifiableMap(hashMap); 31 | } 32 | 33 | @Override 34 | public OAuth2TokenValidatorResult validate(Jwt jwt) { 35 | try { 36 | JeapAuthenticationContext context = JeapAuthenticationContext.readFromJwt(jwt); 37 | OAuth2TokenValidator contextIssuerJwtValidator = contextValidatorMap.get(context); 38 | if (contextIssuerJwtValidator != null) { 39 | return contextIssuerJwtValidator.validate(jwt); 40 | } else { 41 | return createErrorResult("Unsupported context claim value '" + context + "'."); 42 | } 43 | } catch (IllegalArgumentException e) { 44 | //This is the case if the context is not valid 45 | return createErrorResult(e.getMessage()); 46 | } 47 | } 48 | 49 | private OAuth2TokenValidatorResult createErrorResult(String errorMessage) { 50 | OAuth2Error error = new OAuth2Error("invalid_token", errorMessage, null); 51 | log.warn(error.getDescription()); 52 | return OAuth2TokenValidatorResult.failure(error); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/validation/JeapJwtDecoder.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.validation; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.security.oauth2.jwt.Jwt; 5 | import org.springframework.security.oauth2.jwt.JwtDecoder; 6 | 7 | @RequiredArgsConstructor 8 | class JeapJwtDecoder implements JwtDecoder { 9 | 10 | private final JwtDecoder authenticationServerJwtDecoder; 11 | 12 | @Override 13 | public Jwt decode(String token) { 14 | return authenticationServerJwtDecoder.decode(token); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/validation/JeapJwtDecoderFactory.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.validation; 2 | 3 | import org.springframework.security.oauth2.core.OAuth2TokenValidator; 4 | import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; 5 | import org.springframework.security.oauth2.jwt.Jwt; 6 | import org.springframework.security.oauth2.jwt.JwtDecoder; 7 | import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; 8 | 9 | public class JeapJwtDecoderFactory { 10 | 11 | public static JwtDecoder createJwtDecoder(String authorizationServerJwkSetUri, OAuth2TokenValidator jwtValidator) { 12 | return createDefaultJwtDecoder(authorizationServerJwkSetUri, jwtValidator); 13 | } 14 | 15 | private static JwtDecoder createDefaultJwtDecoder(String jwkSetUri, OAuth2TokenValidator jwtValidator) { 16 | NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder. 17 | withJwkSetUri(jwkSetUri). 18 | jwsAlgorithm(SignatureAlgorithm.RS256). 19 | jwsAlgorithm(SignatureAlgorithm.RS512). 20 | build(); 21 | jwtDecoder.setJwtValidator(jwtValidator); 22 | return jwtDecoder; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/config/security/validation/RawJwtTokenParser.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.config.security.validation; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.JeapAuthenticationContext; 4 | import com.nimbusds.jwt.JWT; 5 | import com.nimbusds.jwt.JWTParser; 6 | 7 | class RawJwtTokenParser { 8 | 9 | static JeapAuthenticationContext extractAuthenticationContext(String token) { 10 | try { 11 | JWT jwt = parse(token); 12 | String contextClaimValue = jwt.getJWTClaimsSet().getStringClaim(JeapAuthenticationContext.getContextJwtClaimName()); 13 | return JeapAuthenticationContext.valueOf(contextClaimValue); 14 | } 15 | catch (Exception e) { 16 | throw new IllegalArgumentException(String.format("No valid authentication context extractable from JWT: %s.", e.getMessage()), e); 17 | } 18 | } 19 | 20 | private static JWT parse(String token) { 21 | try { 22 | return JWTParser.parse(token); 23 | } catch (Exception ex) { 24 | throw new IllegalArgumentException(String.format("Unparseable JWT: %s", ex.getMessage()), ex); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/domain/AuthorizationCode.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.domain; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import javax.persistence.UniqueConstraint; 11 | import javax.validation.constraints.NotNull; 12 | import javax.validation.constraints.Size; 13 | import java.time.LocalDate; 14 | import java.time.ZonedDateTime; 15 | import java.util.UUID; 16 | 17 | @Entity 18 | @Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"code"}, name = "UQ_AUTHORIZATION_CODE_CODE")}) 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA 20 | @Getter 21 | public class AuthorizationCode { 22 | 23 | @Id 24 | private UUID id; 25 | 26 | @NotNull 27 | @Size(min = 12, max = 12) 28 | private String code; 29 | 30 | @NotNull 31 | private LocalDate originalOnsetDate; 32 | 33 | @NotNull 34 | private LocalDate onsetDate; 35 | 36 | @NotNull 37 | private ZonedDateTime expiryDate; 38 | 39 | @NotNull 40 | private ZonedDateTime creationDateTime; 41 | 42 | @NotNull 43 | private Integer callCount; 44 | 45 | public AuthorizationCode(String code, LocalDate originalOnsetDate, LocalDate onsetDate, ZonedDateTime expiryDate) { 46 | this.id = UUID.randomUUID(); 47 | this.creationDateTime = ZonedDateTime.now(); 48 | this.code = code; 49 | this.originalOnsetDate = originalOnsetDate; 50 | this.onsetDate = onsetDate; 51 | this.expiryDate = expiryDate; 52 | this.callCount = 0; 53 | } 54 | 55 | public static AuthorizationCode createFake(){ 56 | return new AuthorizationCode(null, LocalDate.of(1900, 1, 1), LocalDate.of(1900, 1, 1), null); 57 | } 58 | 59 | @SuppressWarnings("findbugs:DLS_DEAD_LOCAL_STORE") 60 | public void incrementCallCount(){ 61 | this.callCount++; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/domain/AuthorizationCodeRepository.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 5 | import org.springframework.data.jpa.repository.Lock; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import javax.persistence.LockModeType; 9 | import java.time.ZonedDateTime; 10 | import java.util.List; 11 | import java.util.Optional; 12 | import java.util.UUID; 13 | 14 | @Repository 15 | public interface AuthorizationCodeRepository extends JpaRepository, JpaSpecificationExecutor { 16 | 17 | @Lock(LockModeType.PESSIMISTIC_WRITE) 18 | Optional findByCode(String code); 19 | 20 | boolean existsByCode(String code); 21 | 22 | List findByExpiryDateBefore(ZonedDateTime now); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/lockdown/LockdownException.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.lockdown; 2 | 3 | import org.springframework.core.NestedRuntimeException; 4 | 5 | public class LockdownException extends NestedRuntimeException { 6 | 7 | public LockdownException(String path) { 8 | 9 | super(String.format("The access towards URI '%s' is deactivated", path)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/lockdown/LockdownInterceptor.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.lockdown; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.lockdown.config.Endpoint; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.apache.commons.lang3.Range; 7 | import org.springframework.util.StringUtils; 8 | import org.springframework.web.servlet.HandlerInterceptor; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.time.LocalDateTime; 13 | import java.util.List; 14 | import java.util.Optional; 15 | 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | public class LockdownInterceptor implements HandlerInterceptor { 19 | 20 | private final List endpoints; 21 | 22 | @Override 23 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 24 | 25 | isUriLocked(LocalDateTime.now(), request.getRequestURI(), endpoints); 26 | 27 | return true; 28 | } 29 | 30 | protected boolean isUriLocked(LocalDateTime now, String requestUri, List endpoints) { 31 | 32 | String uri = removeTrailingLeading(requestUri, '/'); 33 | log.debug("intercepting call to uri '{}'", uri); 34 | 35 | Optional result = endpoints.stream() 36 | // check for restrictions on given url 37 | .filter(endpoint -> sameUri(uri, endpoint)) 38 | // change to the endpoints range restrictions 39 | .flatMap(endpoint -> endpoint.getApplicable().stream()) 40 | // and check if range is active 41 | .filter(fromUntil -> isInRange(now, fromUntil)) 42 | .findFirst(); 43 | 44 | // any result rejects the call 45 | if (result.isPresent()) { 46 | throw new LockdownException(requestUri); 47 | } 48 | 49 | return false; 50 | } 51 | 52 | private boolean isInRange(LocalDateTime now, Endpoint.FromUntil fromUntil) { 53 | 54 | LocalDateTime from = (fromUntil.getFrom() == null ? LocalDateTime.MIN : fromUntil.getFrom()); 55 | LocalDateTime until = (fromUntil.getUntil() == null ? LocalDateTime.MAX : fromUntil.getUntil()); 56 | 57 | boolean isBetween = Range.between(from, until).contains(now); 58 | return isBetween; 59 | } 60 | 61 | private boolean sameUri(String requestUri, Endpoint endpoint) { 62 | 63 | boolean sameUri = false; 64 | 65 | if (requestUri != null && endpoint != null) { 66 | String endpointUri = removeTrailingLeading(endpoint.getUri(), '/'); 67 | sameUri = (requestUri.equalsIgnoreCase(endpointUri)); 68 | } 69 | 70 | return sameUri; 71 | } 72 | 73 | private String removeTrailingLeading(String text, Character character) { 74 | 75 | String result = text; 76 | 77 | if (result != null) { 78 | result = StringUtils.trimLeadingCharacter(result, character); 79 | result = StringUtils.trimTrailingCharacter(result, character); 80 | } 81 | 82 | return result; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/lockdown/ResponseStatusExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.lockdown; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Profile; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | @Profile("lockdown") 11 | @Slf4j 12 | @ControllerAdvice 13 | public class ResponseStatusExceptionHandler { 14 | 15 | @ExceptionHandler(value = {LockdownException.class}) 16 | protected ResponseEntity handleLockdownException(LockdownException e) { 17 | log.error(e.getMessage()); 18 | return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/lockdown/config/Endpoint.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.lockdown.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.NestedConfigurationProperty; 5 | import org.springframework.format.annotation.DateTimeFormat; 6 | 7 | import java.time.LocalDateTime; 8 | import java.util.List; 9 | 10 | @Data 11 | public class Endpoint { 12 | 13 | private String uri; 14 | @NestedConfigurationProperty 15 | private List applicable; 16 | 17 | @Data 18 | public static class FromUntil { 19 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) 20 | private LocalDateTime from; 21 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) 22 | private LocalDateTime until; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/lockdown/config/LockdownConfig.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.lockdown.config; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.lockdown.LockdownInterceptor; 4 | import lombok.Data; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.boot.context.properties.NestedConfigurationProperty; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 12 | 13 | import javax.annotation.PostConstruct; 14 | import javax.naming.ConfigurationException; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | 19 | @Profile("lockdown") 20 | @Slf4j 21 | @Data 22 | @Configuration 23 | @ConfigurationProperties(prefix = "lockdown") 24 | public class LockdownConfig implements WebMvcConfigurer { 25 | 26 | @NestedConfigurationProperty 27 | private List endpoints = Collections.emptyList(); 28 | 29 | @Override 30 | public void addInterceptors(InterceptorRegistry registry) { 31 | registry.addInterceptor(new LockdownInterceptor(endpoints)); 32 | } 33 | 34 | @PostConstruct 35 | public void validate() throws ConfigurationException { 36 | if (endpoints.isEmpty()) { 37 | log.warn("no active lockdowns - please consider deactivating profile 'lockdown'!"); 38 | } else { 39 | for (Endpoint endpoint : endpoints) { 40 | if (endpoint.getApplicable().isEmpty()) { 41 | log.warn("no active lockdowns for uri '{}' - please consider removing this endpoint!", endpoint.getUri()); 42 | } else { 43 | for (Endpoint.FromUntil fromUntil : endpoint.getApplicable()) { 44 | if (fromUntil.getUntil()==null && fromUntil.getFrom()==null) { 45 | log.warn("endlessly active lockdowns for endpoint '{}' - please consider removing this from/until range!", endpoint.getUri()); 46 | } else if (fromUntil.getUntil()!=null && fromUntil.getFrom()!=null) { 47 | if (!fromUntil.getFrom().isBefore(fromUntil.getUntil())) { 48 | log.error("endpoint '{}': invalid active range from '{}' until '{}'", endpoint.getUri(), fromUntil.getFrom(), fromUntil.getUntil()); 49 | throw new ConfigurationException("Invalid range from="+fromUntil.getFrom()+" until="+fromUntil.getUntil()+" for Endpoint '"+endpoint.getUri()+"'"); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/service/AuthCodeDeletionService.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCode; 4 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 9 | import org.springframework.scheduling.annotation.Scheduled; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import java.time.ZonedDateTime; 14 | import java.time.format.DateTimeFormatter; 15 | import java.util.List; 16 | 17 | import static net.logstash.logback.argument.StructuredArguments.kv; 18 | 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | @Service 22 | @ConditionalOnProperty(value = "CF_INSTANCE_INDEX", havingValue = "0") 23 | public class AuthCodeDeletionService { 24 | 25 | private final AuthorizationCodeRepository authorizationCodeRepository; 26 | 27 | @Value("${authcodegeneration.service.sleepLogInterval}") 28 | private int sleepLogInterval; 29 | 30 | @Transactional 31 | @Scheduled(cron = "${authcodegeneration.service.deletionCron}") 32 | public void deleteOldAuthCode() { 33 | ZonedDateTime now = ZonedDateTime.now(); 34 | log.info("Delete old AuthCodes expired before '{}'.", now); 35 | List expiredAuthCodes = authorizationCodeRepository.findByExpiryDateBefore(now); 36 | 37 | log.info("Found {} AuthCodes to delete.", expiredAuthCodes.size()); 38 | 39 | expiredAuthCodes.forEach(ac -> { 40 | 41 | try { 42 | Thread.sleep(sleepLogInterval); 43 | } catch (InterruptedException e) { 44 | log.error("Exception during sleep", e); 45 | Thread.currentThread().interrupt(); 46 | throw new IllegalStateException(e); 47 | } 48 | 49 | log.info("AuthorizationCode-Statistic '{}', '{}', '{}', '{}', '{}', '{}'", 50 | kv("id", ac.getId()), 51 | kv("callCount", ac.getCallCount()), 52 | kv("creationDateTime", ac.getCreationDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)), 53 | kv("onsetDate", ac.getOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE)), 54 | kv("originalOnsetDate", ac.getOriginalOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE)), 55 | kv("expiryDate", ac.getExpiryDate().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))); 56 | 57 | authorizationCodeRepository.delete(ac); 58 | }); 59 | 60 | log.info("All old AuthCodes expired before '{}' are now deleted.", now); 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/service/AuthCodeGenerationService.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeCreateDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeResponseDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCode; 6 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.transaction.annotation.Transactional; 13 | import org.springframework.web.server.ResponseStatusException; 14 | 15 | import java.security.SecureRandom; 16 | import java.time.LocalDate; 17 | import java.time.ZonedDateTime; 18 | import java.time.format.DateTimeFormatter; 19 | 20 | import static net.logstash.logback.argument.StructuredArguments.kv; 21 | 22 | @Service 23 | @Transactional(readOnly = true) 24 | @Slf4j 25 | @RequiredArgsConstructor 26 | public class AuthCodeGenerationService { 27 | 28 | private final AuthorizationCodeRepository authorizationCodeRepository; 29 | private static final SecureRandom RANDOM = new SecureRandom(); 30 | private static final int RANDOM_NUMBER_LENGTH = 12; 31 | 32 | @Value("${authcodegeneration.service.onsetSubtractionDays}") 33 | private int onsetSubtractionDays; 34 | 35 | @Value("${authcodegeneration.service.codeExpirationDelay}") 36 | private int codeExpirationDelay; 37 | 38 | @Transactional 39 | public AuthorizationCodeResponseDto create(AuthorizationCodeCreateDto createDto) { 40 | validateOnsetDate(createDto.getOnsetDate()); 41 | String authCode = generateAuthCode(); 42 | 43 | while (authorizationCodeRepository.existsByCode(authCode)) { 44 | log.error("Created a duplicate AuthCode: {}", authCode); 45 | authCode = generateAuthCode(); 46 | } 47 | 48 | AuthorizationCode authorizationCode = new AuthorizationCode(authCode, createDto.getOnsetDate(), createDto.getOnsetDate().minusDays(onsetSubtractionDays), ZonedDateTime.now().plusMinutes(codeExpirationDelay)); 49 | authorizationCodeRepository.saveAndFlush(authorizationCode); 50 | log.info("New authorizationCode saved: {}, {}, {}.", kv("id", authorizationCode.getId()), kv("creationDateTime", authorizationCode.getCreationDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)), kv("onsetDate",authorizationCode.getOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE))); 51 | return new AuthorizationCodeResponseDto(authorizationCode.getCode()); 52 | } 53 | 54 | private String generateAuthCode(){ 55 | return String.format ("%012d", generateRandom(RANDOM_NUMBER_LENGTH)); // 12-digit random numeric code 56 | } 57 | 58 | private void validateOnsetDate(LocalDate onsetDate) { 59 | if (onsetDate.isBefore(LocalDate.now().minusWeeks(4))) { 60 | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Onset date: " + onsetDate + " should not be more than 4 weeks in the past!"); 61 | } 62 | 63 | if (onsetDate.isAfter(LocalDate.now())) { 64 | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Onset date: " + onsetDate + " should not be in the future!"); 65 | } 66 | } 67 | 68 | private static long generateRandom(int length) { 69 | char[] digits = new char[length]; 70 | for (int i = 0; i < length; i++) { 71 | digits[i] = (char) (RANDOM.nextInt(10) + '0'); 72 | } 73 | return Long.parseLong(new String(digits)); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/service/AuthCodeVerificationService.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeOnsetResponseDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDtoWrapper; 6 | import ch.admin.bag.covidcode.authcodegeneration.api.TokenType; 7 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCode; 8 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.time.ZonedDateTime; 16 | import java.time.format.DateTimeFormatter; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.CHECKIN_USERUPLOAD_TOKEN; 21 | import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.DP3T_TOKEN; 22 | import static net.logstash.logback.argument.StructuredArguments.kv; 23 | 24 | @Service 25 | @Transactional(readOnly = true) 26 | @Slf4j 27 | @RequiredArgsConstructor 28 | public class AuthCodeVerificationService { 29 | 30 | private static final String FAKE_STRING = "1"; 31 | private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 32 | private final AuthorizationCodeRepository authorizationCodeRepository; 33 | private final CustomTokenProvider tokenProvider; 34 | 35 | @Value("${authcodegeneration.service.callCountLimit}") 36 | private int callCountLimit; 37 | 38 | @Transactional 39 | public AuthorizationCodeVerifyResponseDto verify(String code, String fake) { 40 | final var accessTokens = verify(code, fake, false); 41 | return accessTokens.getDP3TAccessToken(); 42 | } 43 | 44 | /** 45 | * @param code Authorization code as provided by the health authority 46 | * @param fake String to request fake token 47 | * @param needCheckInToken Needs a second token for purple (checkIn) backend 48 | * @return a wrapper containing two access tokens, which are null if authCode is invalid 49 | */ 50 | @Transactional 51 | public AuthorizationCodeVerifyResponseDtoWrapper verify( 52 | String code, String fake, boolean needCheckInToken) { 53 | final var accessTokens = new AuthorizationCodeVerifyResponseDtoWrapper(); 54 | if (FAKE_STRING.equals(fake)) { 55 | final var dp3tToken = 56 | new AuthorizationCodeVerifyResponseDto( 57 | tokenProvider.createToken( 58 | AuthorizationCode.createFake().getOnsetDate().format(DATE_FORMATTER), 59 | FAKE_STRING, 60 | DP3T_TOKEN)); 61 | accessTokens.setDP3TAccessToken(dp3tToken); 62 | if (needCheckInToken) { 63 | final var checkInToken = 64 | new AuthorizationCodeVerifyResponseDto( 65 | tokenProvider.createToken( 66 | AuthorizationCode.createFake().getOnsetDate().format(DATE_FORMATTER), 67 | FAKE_STRING, 68 | CHECKIN_USERUPLOAD_TOKEN)); 69 | accessTokens.setCheckInAccessToken(checkInToken); 70 | } 71 | return accessTokens; 72 | } 73 | 74 | AuthorizationCode existingCode = authorizationCodeRepository.findByCode(code).orElse(null); 75 | 76 | if (existingCode == null) { 77 | log.error("No AuthCode found with code '{}'", code); 78 | return accessTokens; 79 | } else if (codeValidityHasExpired(existingCode.getExpiryDate())) { 80 | log.error("AuthCode '{}' expired at {}", code, existingCode.getExpiryDate()); 81 | return accessTokens; 82 | } else if (existingCode.getCallCount() >= this.callCountLimit) { 83 | log.error("AuthCode '{}' reached call limit {}", code, existingCode.getCallCount()); 84 | return accessTokens; 85 | } 86 | 87 | existingCode.incrementCallCount(); 88 | log.debug( 89 | "AuthorizationCode verified: '{}', '{}', '{}', '{}', '{}'", 90 | kv("id", existingCode.getId()), 91 | kv("callCount", existingCode.getCallCount()), 92 | kv( 93 | "creationDateTime", 94 | existingCode.getCreationDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)), 95 | kv("onsetDate", existingCode.getOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE)), 96 | kv( 97 | "originalOnsetDate", 98 | existingCode.getOriginalOnsetDate().format(DateTimeFormatter.ISO_LOCAL_DATE))); 99 | final var swissCovidToken = 100 | new AuthorizationCodeVerifyResponseDto( 101 | tokenProvider.createToken( 102 | existingCode.getOnsetDate().format(DATE_FORMATTER), fake, DP3T_TOKEN)); 103 | accessTokens.setDP3TAccessToken(swissCovidToken); 104 | if (needCheckInToken) { 105 | final var checkInToken = 106 | new AuthorizationCodeVerifyResponseDto( 107 | tokenProvider.createToken( 108 | existingCode.getOnsetDate().format(DATE_FORMATTER), fake, CHECKIN_USERUPLOAD_TOKEN)); 109 | accessTokens.setCheckInAccessToken(checkInToken); 110 | } 111 | return accessTokens; 112 | } 113 | 114 | /** 115 | * @param authorizationCode Authorization code as provided by the health authority 116 | * @param fake String to request fake token 117 | * @return object containing a formatted string representing the onset date, or null if auth code is invalid 118 | */ 119 | @Transactional(readOnly = false) // it only reads, but doing so we do not need to dupplicate the findByCode method in the repository 120 | public AuthorizationCodeOnsetResponseDto getOnsetForAuthCode(String authorizationCode, String fake) { 121 | if (FAKE_STRING.equals(fake)) { 122 | return new AuthorizationCodeOnsetResponseDto(AuthorizationCode.createFake().getOnsetDate().format(DATE_FORMATTER)); 123 | } 124 | AuthorizationCode existingCode = authorizationCodeRepository.findByCode(authorizationCode).orElse(null); 125 | if (existingCode == null) { 126 | log.error("No AuthCode found with code '{}'", authorizationCode); 127 | return new AuthorizationCodeOnsetResponseDto(null); 128 | } else if (codeValidityHasExpired(existingCode.getExpiryDate())) { 129 | log.error("AuthCode '{}' expired at {}", authorizationCode, existingCode.getExpiryDate()); 130 | return new AuthorizationCodeOnsetResponseDto(null); 131 | } else if (existingCode.getCallCount() >= this.callCountLimit) { 132 | log.error("AuthCode '{}' reached call limit {}", authorizationCode, existingCode.getCallCount()); 133 | return new AuthorizationCodeOnsetResponseDto(null); 134 | } 135 | return new AuthorizationCodeOnsetResponseDto(existingCode.getOnsetDate().format(DATE_FORMATTER)); 136 | } 137 | 138 | private boolean codeValidityHasExpired(ZonedDateTime expiryDate) { 139 | return expiryDate.isBefore(ZonedDateTime.now()); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/service/CustomTokenProvider.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.TokenType; 4 | import io.jsonwebtoken.Header; 5 | import io.jsonwebtoken.Jwts; 6 | import io.jsonwebtoken.io.Decoders; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.annotation.PostConstruct; 12 | import java.security.Key; 13 | import java.security.KeyFactory; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.security.spec.InvalidKeySpecException; 16 | import java.security.spec.PKCS8EncodedKeySpec; 17 | import java.util.Date; 18 | import java.util.UUID; 19 | 20 | import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.DP3T_TOKEN; 21 | 22 | @Component 23 | @Slf4j 24 | public class CustomTokenProvider { 25 | 26 | @Value("${authcodegeneration.jwt.token-validity}") 27 | private long tokenValidity; 28 | 29 | @Value("${authcodegeneration.jwt.issuer}") 30 | private String issuer; 31 | 32 | @Value("${authcodegeneration.jwt.privateKey}") 33 | private String privateKey; 34 | 35 | private KeyFactory rsa; 36 | 37 | @PostConstruct 38 | public void init() throws NoSuchAlgorithmException { 39 | rsa = KeyFactory.getInstance("RSA"); 40 | } 41 | 42 | public String createToken(String onsetDate, String fake) { 43 | return createToken(onsetDate, fake, DP3T_TOKEN); 44 | } 45 | 46 | public String createToken(String onsetDate, String fake, TokenType tokenType) { 47 | final long nowMillis = System.currentTimeMillis(); 48 | final Date now = new Date(nowMillis); 49 | 50 | final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Decoders.BASE64.decode(privateKey)); 51 | final Key signingKey; 52 | 53 | try { 54 | signingKey = rsa.generatePrivate(spec); 55 | } catch (InvalidKeySpecException e) { 56 | log.error("Error during generate private key", e); 57 | throw new IllegalStateException(e); 58 | } 59 | 60 | return Jwts.builder() 61 | .setId(UUID.randomUUID().toString()) 62 | .setIssuer(issuer) 63 | .setIssuedAt(now) 64 | .setNotBefore(now) 65 | .setHeaderParam(Header.TYPE, Header.JWT_TYPE) 66 | .claim("aud", tokenType.getAudience()) 67 | .claim("scope", tokenType.getScope()) 68 | .claim("fake", fake) 69 | .claim("onset", onsetDate) 70 | .signWith(signingKey) 71 | .setExpiration(new Date(nowMillis + tokenValidity)) 72 | .compact(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/config/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.config; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import io.swagger.v3.oas.models.info.License; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | public class OpenApiConfig { 12 | 13 | @Bean 14 | public OpenAPI customOpenAPI() { 15 | return new OpenAPI() 16 | .components(new Components()) 17 | .info(new Info() 18 | .title("HA AuthCode Generation Service") 19 | .description("Rest API for HA AuthCode Generation Service.") 20 | .version("0.0.1") 21 | .license(new License().name("Apache 2.0")) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeGenerationController.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeCreateDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeResponseDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.JeapAuthenticationToken; 6 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.ServletJeapAuthorization; 7 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeGenerationService; 8 | import io.swagger.v3.oas.annotations.Operation; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.security.access.AccessDeniedException; 12 | import org.springframework.security.access.prepost.PreAuthorize; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import javax.servlet.http.HttpServletRequest; 19 | import javax.validation.Valid; 20 | 21 | @RestController 22 | @RequestMapping("/v1/authcode") 23 | @RequiredArgsConstructor 24 | @Slf4j 25 | public class AuthCodeGenerationController { 26 | 27 | private final ServletJeapAuthorization jeapAuthorization; 28 | 29 | private final AuthCodeGenerationService authCodeGenerationService; 30 | 31 | @Operation(summary = "Authorization code generation method") 32 | @PostMapping() 33 | @PreAuthorize("hasRole('bag-pts-allow')") 34 | public AuthorizationCodeResponseDto create(@Valid @RequestBody AuthorizationCodeCreateDto createDto, HttpServletRequest request) { 35 | log.debug("Call of Create with onset date '{}'.", createDto.getOnsetDate()); 36 | logAuthorizationInfo(request); 37 | return authCodeGenerationService.create(createDto); 38 | } 39 | 40 | private void logAuthorizationInfo(HttpServletRequest request) { 41 | // A request to the OAuth2 protected resource includes the access token in the 'Authorization' header. 42 | // This token is the base of the Spring Security Authentication associated with the authenticated request. 43 | log.debug("Access token: {}.", request.getHeader("Authorization")); 44 | 45 | // Access the Spring Security Authentication as JeapAuthenticationToken 46 | JeapAuthenticationToken jeapAuthenticationToken = jeapAuthorization.getJeapAuthenticationToken(); 47 | log.debug(jeapAuthenticationToken.toString()); 48 | 49 | String displayName = jeapAuthenticationToken.getToken().getClaimAsString("displayName"); 50 | 51 | if (displayName == null) { 52 | displayName = jeapAuthenticationToken.getTokenName(); 53 | } 54 | 55 | if ("E-ID CH-LOGIN".equals(jeapAuthenticationToken.getToken().getClaimAsString("homeName")) && jeapAuthenticationToken.getToken().getClaimAsString("unitName").startsWith("HIN")) { 56 | throw new AccessDeniedException("Access denied for HIN with CH-Login"); 57 | } 58 | 59 | log.info("Authenticated User is '{}'.", displayName); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeVerificationController.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerificationDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeVerificationService; 6 | import io.swagger.v3.oas.annotations.Operation; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.data.rest.webmvc.ResourceNotFoundException; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import javax.validation.Valid; 16 | import java.time.Duration; 17 | import java.time.Instant; 18 | 19 | @RestController 20 | @RequestMapping("/v1/onset") 21 | @Slf4j 22 | public class AuthCodeVerificationController { 23 | 24 | private final AuthCodeVerificationService authCodeVerificationService; 25 | private final Duration requestTime; 26 | 27 | public AuthCodeVerificationController(AuthCodeVerificationService authCodeVerificationService, @Value("${authcodegeneration.service.requestTime}") long requestTime) { 28 | this.authCodeVerificationService = authCodeVerificationService; 29 | this.requestTime = Duration.ofMillis(requestTime); 30 | } 31 | 32 | @Operation(summary = "Authorization code verification method") 33 | @PostMapping() 34 | public AuthorizationCodeVerifyResponseDto verify(@Valid @RequestBody AuthorizationCodeVerificationDto verificationDto) { 35 | var now = Instant.now().toEpochMilli(); 36 | log.debug("Call of Verify with authCode '{}'.", verificationDto.getAuthorizationCode()); 37 | AuthorizationCodeVerifyResponseDto responseDto = authCodeVerificationService.verify(verificationDto.getAuthorizationCode(), verificationDto.getFake()); 38 | normalizeRequestTime(now); 39 | if (responseDto == null) { 40 | throw new ResourceNotFoundException(null); 41 | } 42 | return responseDto; 43 | } 44 | 45 | private void normalizeRequestTime(long now) { 46 | long after = Instant.now().toEpochMilli(); 47 | long duration = after - now; 48 | try { 49 | Thread.sleep(Math.max(requestTime.minusMillis(duration).toMillis(), 0)); 50 | } catch (Exception ex) { 51 | log.error("Error during sleep", ex); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeVerificationControllerV2.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeOnsetResponseDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerificationDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto; 6 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDtoWrapper; 7 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeVerificationService; 8 | import io.swagger.v3.oas.annotations.Operation; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.data.rest.webmvc.ResourceNotFoundException; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import javax.validation.Valid; 16 | import java.time.Duration; 17 | import java.time.Instant; 18 | import java.util.List; 19 | 20 | @RestController 21 | @RequestMapping("/v2/onset") 22 | @Slf4j 23 | public class AuthCodeVerificationControllerV2 { 24 | 25 | private final AuthCodeVerificationService authCodeVerificationService; 26 | private final Duration requestTime; 27 | 28 | public AuthCodeVerificationControllerV2(AuthCodeVerificationService authCodeVerificationService, @Value("${authcodegeneration.service.requestTime}") long requestTime) { 29 | this.authCodeVerificationService = authCodeVerificationService; 30 | this.requestTime = Duration.ofMillis(requestTime); 31 | } 32 | 33 | @Operation(summary = "Authorization code verification method") 34 | @PostMapping() 35 | public ResponseEntity verify(@Valid @RequestBody AuthorizationCodeVerificationDto verificationDto) { 36 | var now = Instant.now().toEpochMilli(); 37 | log.debug("Call of Verify with authCode '{}'.", verificationDto.getAuthorizationCode()); 38 | final AuthorizationCodeVerifyResponseDtoWrapper accessTokenWrapper = authCodeVerificationService.verify(verificationDto.getAuthorizationCode(), verificationDto.getFake(), true); 39 | normalizeRequestTime(now); 40 | if (accessTokenWrapper == null || accessTokenWrapper.getDP3TAccessToken() == null || accessTokenWrapper.getCheckInAccessToken() == null) { 41 | throw new ResourceNotFoundException(null); 42 | } 43 | return ResponseEntity.ok().body(accessTokenWrapper); 44 | } 45 | 46 | @Operation(summary = "Get onset date for authorization code") 47 | @PostMapping(value="/date") 48 | public ResponseEntity getOnset(@Valid @RequestBody AuthorizationCodeVerificationDto verificationDto) { 49 | var now = Instant.now().toEpochMilli(); 50 | log.debug("Call of getOnset with authCode '{}'.", verificationDto.getAuthorizationCode()); 51 | final AuthorizationCodeOnsetResponseDto onsetWrapper = 52 | authCodeVerificationService.getOnsetForAuthCode( 53 | verificationDto.getAuthorizationCode(), verificationDto.getFake()); 54 | normalizeRequestTime(now); 55 | if (onsetWrapper == null || onsetWrapper.getOnset() == null) { 56 | throw new ResourceNotFoundException(null); 57 | } 58 | return ResponseEntity.ok().body(onsetWrapper); 59 | } 60 | 61 | private void normalizeRequestTime(long now) { 62 | long after = Instant.now().toEpochMilli(); 63 | long duration = after - now; 64 | try { 65 | Thread.sleep(Math.max(requestTime.minusMillis(duration).toMillis(), 0)); 66 | } catch (Exception ex) { 67 | log.error("Error during sleep", ex); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/monitoring/ActuatorConfig.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.monitoring; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Lazy; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Lazy 11 | @Component 12 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 13 | public class ActuatorConfig { 14 | 15 | @Bean 16 | AuthorizationCodeCountMeter dataSourceStatusProbe(AuthorizationCodeRepository authorizationCodeRepository) { 17 | return new AuthorizationCodeCountMeter(authorizationCodeRepository); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/monitoring/ActuatorSecurity.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.monitoring; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.actuate.health.HealthEndpoint; 5 | import org.springframework.boot.actuate.info.InfoEndpoint; 6 | import org.springframework.boot.actuate.logging.LoggersEndpoint; 7 | import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.core.Ordered; 10 | import org.springframework.core.annotation.Order; 11 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 12 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | 15 | @Configuration 16 | @Order(Ordered.HIGHEST_PRECEDENCE + 9) 17 | public class ActuatorSecurity extends WebSecurityConfigurerAdapter { 18 | 19 | private static final String PROMETHEUS_ROLE = "PROMETHEUS"; 20 | 21 | @Value("${authcodegeneration.monitor.prometheus.user}") 22 | private String user; 23 | @Value("${authcodegeneration.monitor.prometheus.password}") 24 | private String password; 25 | 26 | @Override 27 | protected void configure(HttpSecurity http) throws Exception { 28 | http.requestMatcher(org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.toAnyEndpoint()). 29 | authorizeRequests(). 30 | requestMatchers(org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.to(HealthEndpoint.class)).permitAll(). 31 | requestMatchers(org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.to(InfoEndpoint.class)).permitAll(). 32 | requestMatchers(org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.to(LoggersEndpoint.class)).hasRole(PROMETHEUS_ROLE). 33 | requestMatchers(org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.to(PrometheusScrapeEndpoint.class)).hasRole(PROMETHEUS_ROLE). 34 | anyRequest().denyAll(). 35 | and(). 36 | httpBasic(); 37 | 38 | http.csrf().ignoringAntMatchers("/actuator/loggers/**"); 39 | } 40 | 41 | @Override 42 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 43 | auth.inMemoryAuthentication().withUser(user).password(password).roles(PROMETHEUS_ROLE); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/monitoring/AuthorizationCodeCountMeter.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.monitoring; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 4 | import io.micrometer.core.instrument.Gauge; 5 | import io.micrometer.core.instrument.MeterRegistry; 6 | import io.micrometer.core.instrument.binder.MeterBinder; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.util.Objects; 10 | 11 | @Slf4j 12 | public class AuthorizationCodeCountMeter implements MeterBinder { 13 | 14 | private final String name; 15 | private final String description; 16 | 17 | private final AuthorizationCodeRepository authorizationCodeRepository; 18 | 19 | public AuthorizationCodeCountMeter(final AuthorizationCodeRepository authorizationCodeRepository) { 20 | Objects.requireNonNull(authorizationCodeRepository, "authorizationCodeRepository cannot be null"); 21 | this.authorizationCodeRepository = authorizationCodeRepository; 22 | this.name = "authcode_stats"; 23 | this.description = "AuthorizationCode Statistics"; 24 | } 25 | 26 | @Override 27 | public void bindTo(final MeterRegistry meterRegistry) { 28 | Gauge.builder(name, this, value -> authorizationCodeRepository.count() ) 29 | .description(description) 30 | .baseUnit("count") 31 | .register(meterRegistry); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/monitoring/HealthMetricsConfig.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.monitoring; 2 | 3 | 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; 6 | import org.springframework.boot.actuate.health.HealthEndpoint; 7 | import org.springframework.boot.actuate.health.Status; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | 12 | @Configuration 13 | class HealthMetricsConfig { 14 | 15 | @Bean 16 | public MeterRegistryCustomizer prometheusHealthCheck(HealthEndpoint healthEndpoint) { 17 | return registry -> registry.gauge("health", healthEndpoint, HealthMetricsConfig::healthToCode); 18 | } 19 | 20 | private static int healthToCode(HealthEndpoint ep) { 21 | Status status = ep.health().getStatus(); 22 | return status.equals(Status.UP) ? 1 : 0; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/security/HttpResponseHeaderFilter.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.security; 2 | 3 | import javax.servlet.*; 4 | import javax.servlet.annotation.WebFilter; 5 | import javax.servlet.http.HttpServletResponse; 6 | import java.io.IOException; 7 | 8 | @WebFilter(urlPatterns = {"/v1/onset/*", "/v2/onset/*", "/v1/authcode/*"}) 9 | public class HttpResponseHeaderFilter implements Filter { 10 | @Override 11 | public void doFilter(ServletRequest request, ServletResponse response, 12 | FilterChain chain) throws IOException, ServletException { 13 | HttpServletResponse httpServletResponse = (HttpServletResponse) response; 14 | httpServletResponse.setHeader("Content-Security-Policy", "default-src 'self'"); 15 | httpServletResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); 16 | httpServletResponse.setHeader("Feature-Policy", "microphone 'none'; payment 'none'; camera 'none'"); 17 | chain.doFilter(request, response); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/ch/admin/bag/covidcode/authcodegeneration/web/security/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.security; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 | import org.springframework.web.cors.CorsConfiguration; 9 | import org.springframework.web.cors.CorsConfigurationSource; 10 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * When including the jeap-spring-boot-security-starter dependency and providing the matching configuration properties 16 | * all web endpoints of the application will be automatically protected by OAuth2 as a default. If in addition web endpoints 17 | * with different protection (i.e. basic auth or no protection at all) must be provided at the same time by the application 18 | * an additional WebSecurityConfigurerAdapter configuration (like the one below) needs to explicitly punch a hole into 19 | * the jeap-spring-boot-security-starter OAuth2 protection with an appropriate HttpSecurity configuration. 20 | * Note: jeap-spring-boot-monitoring-starter already does exactly that for the prometheus actuator endpoint. 21 | */ 22 | @Configuration 23 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 24 | 25 | @Value("${ha-authcode-generation-service.allowed-origin}") 26 | private String allowedOrigin; 27 | 28 | 29 | @Override 30 | protected void configure(HttpSecurity http) throws Exception { 31 | http.requestMatchers(). 32 | antMatchers("/actuator/**", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/v1/onset/**", "/v2/onset/**"). 33 | and(). 34 | authorizeRequests().anyRequest().permitAll(); 35 | 36 | http.csrf().ignoringAntMatchers("/v1/onset/**", "/v2/onset/**"); 37 | } 38 | 39 | @Bean 40 | CorsConfigurationSource corsConfigurationSource() { 41 | CorsConfiguration configuration = new CorsConfiguration(); 42 | configuration.setAllowedOrigins(List.of(allowedOrigin)); 43 | configuration.setAllowedHeaders(List.of("*")); 44 | configuration.setAllowedMethods(List.of("*")); 45 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 46 | source.registerCorsConfiguration("/**", configuration); 47 | return source; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/application-abn.yml: -------------------------------------------------------------------------------- 1 | authcodegeneration: 2 | jwt: 3 | issuer: "https://identity-a.bit.admin.ch/realms/bag-pts" 4 | token-validity: 300000 5 | privateKey: ${vcap.services.signingKey_abn.credentials.privateKey} 6 | monitor: 7 | prometheus: 8 | user: "prometheus" 9 | password: ${vcap.services.ha_prometheus_abn.credentials.password} 10 | 11 | jeap: 12 | security: 13 | oauth2: 14 | resourceserver: 15 | authorization-server: 16 | issuer: "https://identity-a.bit.admin.ch/realms/bag-pts" 17 | 18 | ha-authcode-generation-service: 19 | allowed-origin: "https://www.covidcode-a.admin.ch" 20 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | authcodegeneration: 2 | jwt: 3 | issuer: "https://identity-r.bit.admin.ch/realms/bag-pts" 4 | token-validity: 300000 5 | privateKey: ${vcap.services.signingKey_dev.credentials.privateKey} 6 | monitor: 7 | prometheus: 8 | user: "prometheus" 9 | password: ${vcap.services.ha_prometheus_dev.credentials.password} 10 | 11 | jeap: 12 | security: 13 | oauth2: 14 | resourceserver: 15 | authorization-server: 16 | issuer: "https://identity-r.bit.admin.ch/realms/bag-pts" 17 | 18 | ha-authcode-generation-service: 19 | allowed-origin: "https://www.covidcode-d.admin.ch" 20 | -------------------------------------------------------------------------------- /src/main/resources/application-keycloak-local.yml: -------------------------------------------------------------------------------- 1 | # Specific configuration for running CovidCode-Service so that it 2 | # talks to the local Keycloak (the one of ../../../docker-compose.yml) 3 | 4 | jeap: 5 | security: 6 | oauth2: 7 | resourceserver: 8 | authorization-server: 9 | issuer: "http://localhost:8180/auth/realms/bag-pts" 10 | jwk-set-uri: ~ 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | flyway: 3 | locations: classpath:db/migration/common 4 | 5 | authcodegeneration: 6 | jwt: 7 | issuer: "http://localhost:8113" 8 | token-validity: 300000 9 | privateKey: "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDT5jbu96Mf5UvkGrp3sj0Spoh0Cf664/ksSs5fOGM6ZFRh4IkjDRokfEu7nWbGSYftrfVLAatL1At+Jc+yEe29RKFzAHjatmLSCb59BtWoJAHa3+gPm2Essk2F93iAuRiWPVeJ3uz1fqPCYG9Gca65YSzOvlPUN7+Nih/0DeJOfrtq138c5Thv+YgJbzih1p2T+9iEZ9QqE77Xf0oNFJCpWcW8Fu2JKCHuDPYCjXdEHlONClHUskICqmheppsaSqdSIXxbj9ZylHUqTp/zLsL7m91Z0/Fy3ZUgYCKJSopCaWJSZJIq4YEM5OMwjArCB1/UZyWzARsdjj5j8cRYc+BdAgMBAAECggEARLKLNrgkM5AEJaSgtXOcpzJEZNJkujR0sO5jr605RlIGpWDFNQ7nXdLKPr4N9tUZ822Fa9bTsRbCzxf1GPcFC2p3qTAK/mVI7m1oS2Ju3D8oNsyGkKDARVxdE8SiVaEsnnCus60JR6HR94+KI91xVvpxK2m7Bb85I+sW5umlZ+rI59XgnP9q3EtOxnC4GQ/QfTjFIgAZHUJwzh0SZ+I2GPXLw9WkFGyxgW6wKaNQOUz/jjJyPNTl3jg0cW9kcHrviyHZK+IPVwQINj7JEzRZ5rCXBjEBE8ht1g1vu7QPQD1x7ZGCrjWAXfr01rNSJMPNEIi1CEaVbz63j6ewUVZV4QKBgQD7GNOnDorqimmGO+sJ6LV3DqrBa2TPCfclcgSiKBu+A/B1nBBLAsNVXN59BTtYHilj+Xhdt1cmOHBo9a0ZCrqHjU6xWyl9iOxh8rhpE0FZ/3g3aWM7aII+SyheMrFtfBh4XrvH5ZJLrzJiiGg+TDTuFApksmb6qf/SE+ZQFZNRaQKBgQDYCXKs051beqn1kHWVwTcb1UTqrTuclgnNZ3sPKjVwvGCqUIRsyWZOV8E7PFeU2F9Ek/3nvLw5pTY+F5Aq0Tfaxt3yS7nRhRfWLL4wMyezYrE/qPFN5L8bhejcuSjRieIWXQU8vMI0+YIIV45JmP44M9cjbcrkPuzvl2soY8+E1QKBgQCOC2NgM9feCmLbrvWta1mMel2agXhLryWCp1d7rBjVi0DyJ1EIPg3mMl0ieF0z4gwkJDI1QcwpMPBWT/SWH/2ZRRTpO9riyxx95GLx/hSQJvcI0bNzHhHfz4CMmTzJ5NOq9FxiHrp92iQ0nVnrNA0VSXz/rfSXhKfVXbCCSVJHUQKBgQCe+CzTMhCLvTKNiZSM8xW7PG8vBPRloB5scGYkXZnfcC7thLw9VOIcagS9swR7edB4pTHkMYSMIp9Mh4hFiZjBOy8c2U5N99L3fgsharMfFFN7lbSi7d0Wwq38pZ98uSqN7DsrW3bJBoUB4HPKgnMnJjZ8UpFG7WrqTxDCMtgEVQKBgFFFy9AypTsQgdQf1VWdUGX7uJ8TYTuhMVzcOThtQeuVnTEqp9gUyuarc6xOL2i7wD6cdnb48NhqJ66r/F8bxMAUMOaElrAo7IIZXfSJdpPVFMwGVz022lk//XdjA/VcJv5tNnBoKP29cRCkf6WU1SEdlyoVu/vzPNMGbNZMyt8s" 10 | monitor: 11 | prometheus: 12 | user: "prometheus" 13 | password: "{noop}secret" 14 | service: 15 | sleepLogInterval: 1 16 | 17 | jeap: 18 | security: 19 | oauth2: 20 | resourceserver: 21 | authorization-server: 22 | issuer: "http://localhost:8180" 23 | jwk-set-uri: "http://localhost:8180/.well-known/jwks.json" 24 | 25 | ha-authcode-generation-service: 26 | allowed-origin: "http://localhost:4200" 27 | 28 | ## Uncomment the following to increase logging; then issue 29 | ## `mvn compile` to copy this configuration under target/ 30 | # server: 31 | # tomcat: 32 | # basedir: /tmp 33 | # accesslog: 34 | # enabled: true 35 | # directory: /dev 36 | # prefix: stdout 37 | # buffered: false 38 | # suffix: 39 | # file-date-format: 40 | # 41 | # logging: 42 | # level: 43 | # org.apache.tomcat: DEBUG 44 | # org.apache.catalina: DEBUG 45 | # org: 46 | # apache: 47 | # tomcat: DEBUG 48 | # catalina: DEBUG 49 | -------------------------------------------------------------------------------- /src/main/resources/application-lockdown.yml: -------------------------------------------------------------------------------- 1 | 2 | # the order of active profiles is IMPORTANT: SPRING_PROFILES_ACTIVE: prod,lockdown 3 | # ONLY this activates the lockdowwn configuration for the profile prod 4 | # reversed order will simply activate lockdown configuration of abn for EVERY environment! 5 | 6 | --- 7 | spring.profiles: prod 8 | lockdown: 9 | endpoints: 10 | - 11 | uri: v1/authcode 12 | applicable: 13 | - 14 | from: 2022-04-01T00:00:00.000Z 15 | - 16 | uri: v1/onset 17 | applicable: 18 | - 19 | from: 2022-04-01T00:00:00.000Z 20 | - 21 | uri: v2/onset 22 | applicable: 23 | - 24 | from: 2022-04-01T00:00:00.000Z 25 | 26 | --- 27 | spring.profiles: abn 28 | lockdown: 29 | endpoints: 30 | - 31 | uri: v1/authcode 32 | applicable: 33 | - 34 | from: 2022-03-22T00:00:00.000Z 35 | until: 2022-03-23T23:59:59.000Z 36 | - 37 | from: 2022-03-28T00:00:00.000Z 38 | until: 2022-06-30T23:59:59.000Z 39 | - 40 | uri: v1/onset 41 | applicable: 42 | - 43 | from: 2022-03-22T00:00:00.000Z 44 | until: 2022-03-23T23:59:59.000Z 45 | - 46 | from: 2022-03-28T00:00:00.000Z 47 | until: 2022-06-30T23:59:59.000Z 48 | - 49 | uri: v2/onset 50 | applicable: 51 | - 52 | from: 2022-03-22T00:00:00.000Z 53 | until: 2022-03-23T23:59:59.000Z 54 | - 55 | from: 2022-03-28T00:00:00.000Z 56 | until: 2022-06-30T23:59:59.000Z 57 | --- 58 | spring.profiles: dev,local 59 | lockdown: 60 | endpoints: 61 | - 62 | uri: v1/authcode 63 | applicable: 64 | - 65 | from: 2022-03-22T00:00:00.000Z 66 | until: 2022-03-23T23:59:59.000Z 67 | - 68 | from: 2022-03-28T00:00:00.000Z 69 | until: 2022-06-12T23:59:59.000Z 70 | - 71 | uri: v1/onset 72 | applicable: 73 | - 74 | from: 2022-03-22T00:00:00.000Z 75 | until: 2022-03-23T23:59:59.000Z 76 | - 77 | from: 2022-03-28T00:00:00.000Z 78 | until: 2022-06-12T23:59:59.000Z 79 | - 80 | uri: v2/onset 81 | applicable: 82 | - 83 | from: 2022-03-22T00:00:00.000Z 84 | until: 2022-03-23T23:59:59.000Z 85 | - 86 | from: 2022-03-28T00:00:00.000Z 87 | until: 2022-06-12T23:59:59.000Z 88 | -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | ch: 4 | admin: 5 | bit: 6 | jeap: INFO 7 | bag: INFO 8 | 9 | authcodegeneration: 10 | jwt: 11 | issuer: "https://identity.bit.admin.ch/realms/bag-pts" 12 | token-validity: 300000 13 | privateKey: ${vcap.services.signingKey_prod.credentials.privateKey} 14 | monitor: 15 | prometheus: 16 | user: "prometheus" 17 | password: ${vcap.services.ha_prometheus_prod.credentials.password} 18 | 19 | jeap: 20 | security: 21 | oauth2: 22 | resourceserver: 23 | authorization-server: 24 | issuer: "https://identity.bit.admin.ch/realms/bag-pts" 25 | 26 | ha-authcode-generation-service: 27 | allowed-origin: "https://www.covidcode.admin.ch" 28 | -------------------------------------------------------------------------------- /src/main/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | authcodegeneration: 2 | jwt: 3 | issuer: "https://identity-a.bit.admin.ch/realms/bag-pts-test" 4 | token-validity: 300000 5 | privateKey: ${vcap.services.signingKey_test.credentials.privateKey} 6 | monitor: 7 | prometheus: 8 | user: "prometheus" 9 | password: ${vcap.services.ha_prometheus_test.credentials.password} 10 | 11 | jeap: 12 | security: 13 | oauth2: 14 | resourceserver: 15 | authorization-server: 16 | issuer: "https://identity-a.bit.admin.ch/realms/bag-pts-test" 17 | 18 | ha-authcode-generation-service: 19 | allowed-origin: "https://www.covidcode-t.admin.ch" 20 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | info: 2 | build: 3 | artifact: '@project.artifactId@' 4 | description: '@project.description@' 5 | name: '@project.name@' 6 | version: '@project.version@' 7 | logging: 8 | level: 9 | ch: 10 | admin: 11 | bit: 12 | jeap: DEBUG 13 | bag: DEBUG 14 | io: 15 | swagger: 16 | models: 17 | parameters: 18 | AbstractSerializableParameter: ERROR 19 | org: 20 | hibernate: ERROR 21 | springframework: 22 | security: 23 | authentication: 24 | event: 25 | LoggerListener: ERROR 26 | oauth2: 27 | server: 28 | resource: 29 | web: 30 | BearerTokenAuthenticationFilter: INFO 31 | web: 32 | servlet: 33 | resource: 34 | ResourceHttpRequestHandler: INFO 35 | filter: 36 | CommonsRequestLoggingFilter: INFO 37 | springfox: 38 | documentation: 39 | spring: 40 | web: 41 | readers: 42 | operation: 43 | CachingOperationNameGenerator: ERROR 44 | pattern: 45 | level: '[%X{correlationId}] %5p' 46 | config: classpath:logback-spring.xml 47 | file: 48 | name: log.log 49 | server: 50 | port: 8113 51 | servlet: 52 | context-path: / 53 | spring: 54 | application: 55 | name: ha-authcodegeneration 56 | datasource: 57 | type: com.zaxxer.hikari.HikariDataSource 58 | driver-class-name: org.postgresql.Driver 59 | url: jdbc:postgresql://localhost:3113/haauthcodegeneration 60 | username: haauthcodegeneration 61 | password: secret 62 | hikari: 63 | maximum-pool-size: 10 64 | minimum-idle: 2 65 | pool-name: hikari-cp-${spring.application.name} 66 | jpa: 67 | hibernate: 68 | ddl-auto: validate 69 | properties: 70 | hibernate: 71 | dialect: org.hibernate.dialect.PostgreSQL10Dialect 72 | show-sql: false 73 | open-in-view: false 74 | flyway: 75 | enabled: true 76 | clean-on-validation-error: false 77 | locations: classpath:db/migration/common, classpath:db/migration/postgresql 78 | 79 | messages: 80 | basename: mail-messages,validation-messages 81 | encoding: UTF-8 82 | fallback-to-system-locale: false 83 | 84 | servlet: 85 | multipart: 86 | max-file-size: 10MB 87 | max-request-size: 10MB 88 | session: 89 | store-type: none 90 | data: 91 | rest: 92 | base-path: /api 93 | max-page-size: 100 94 | default-page-size: 20 95 | main: 96 | banner-mode: off 97 | 98 | management: 99 | endpoints: 100 | web: 101 | exposure: 102 | include: '*' 103 | endpoint: 104 | jolokia: 105 | enabled: true 106 | health: 107 | show-details: always 108 | flyway: 109 | enabled: true 110 | 111 | authcodegeneration: 112 | rest: 113 | connectTimeoutSeconds: 5 114 | readTimeoutSeconds: 5 115 | service: 116 | callCountLimit: 1 117 | codeExpirationDelay: 1440 118 | deletionCron: "0 55 1 * * ?" 119 | onsetSubtractionDays: 2 120 | requestTime: 500 121 | sleepLogInterval: 30000 122 | monitor: 123 | prometheus: 124 | secure: false 125 | 126 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/common/V1_0_0__create-schema.sql: -------------------------------------------------------------------------------- 1 | create table authorization_code 2 | ( 3 | id uuid not null 4 | constraint authorization_code_pkey 5 | primary key, 6 | code varchar(9) not null 7 | constraint uq_authorization_code_code 8 | unique, 9 | creation_date_time timestamp not null, 10 | expiry_date timestamp not null, 11 | onset_date date not null, 12 | call_count integer not null 13 | ); 14 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/common/V1_0_1__alter_varchar_length_of_code_column.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE authorization_code ALTER COLUMN code TYPE varchar(12); 2 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/common/V1_0_2__new_original_onset_date_column.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE authorization_code RENAME COLUMN onset_date TO original_onset_date; 2 | ALTER TABLE authorization_code ADD COLUMN onset_date date; 3 | UPDATE authorization_code SET onset_date = (original_onset_date - INTERVAL '3' DAY); 4 | ALTER TABLE authorization_code ALTER COLUMN onset_date SET not null; 5 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/postgresql/afterMigrate.sql: -------------------------------------------------------------------------------- 1 | create or replace procedure reassign_objects_ownership() 2 | LANGUAGE 'plpgsql' 3 | as $BODY$ 4 | BEGIN 5 | execute format('reassign owned by %s to %s_role_full', user, current_database()); 6 | END 7 | $BODY$; 8 | 9 | call reassign_objects_ownership(); 10 | 11 | drop procedure reassign_objects_ownership(); 12 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | UTF-8 10 | %d %highlight(%-5level) [${app},%X{X-B3-TraceId:-}] %cyan(%logger{35}) - %msg %marker%n 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | logger 20 | 20 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | exception-hash 30 | 31 | 32 | exception 33 | 34 | 40 35 | 4096 36 | 20 37 | true 38 | sun\.reflect\..*\.invoke.* 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | log.log 50 | 51 | log.log.%i 52 | 1 53 | 3 54 | 55 | 56 | 2MB 57 | 58 | 59 | UTF-8 60 | %d %highlight(%-5level) [${app},%X{X-B3-TraceId:-}] %cyan(%logger{35}) - %msg %marker%n 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/domain/AuthorizationCodeRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.annotation.DirtiesContext; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import javax.persistence.EntityManager; 11 | import javax.persistence.PersistenceContext; 12 | import java.time.LocalDate; 13 | import java.time.ZonedDateTime; 14 | import java.util.Optional; 15 | 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.core.Is.is; 18 | 19 | 20 | @SpringBootTest(properties = { 21 | "spring.jpa.hibernate.ddl-auto=create", 22 | "spring.datasource.driver-class-name=org.h2.Driver", 23 | "spring.datasource.url=jdbc:h2:mem:test;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE", 24 | "spring.datasource.username=sa", 25 | "spring.datasource.password=sa" 26 | }) 27 | @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) 28 | @ActiveProfiles("local") 29 | class AuthorizationCodeRepositoryTest { 30 | 31 | @Autowired 32 | private AuthorizationCodeRepository repository; 33 | 34 | @PersistenceContext 35 | private EntityManager entityManager; 36 | 37 | @Test 38 | @Transactional 39 | void findById_FoundOne() { 40 | //given 41 | AuthorizationCode initial = new AuthorizationCode("123456789000", LocalDate.now(), LocalDate.now(), ZonedDateTime.now()); 42 | entityManager.persist(initial); 43 | //when 44 | Optional process = repository.findById(initial.getId()); 45 | //then 46 | assertThat(process.isPresent(), is(true)); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/lockdown/LockdownConfigTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.lockdown; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.lockdown.config.Endpoint; 4 | import ch.admin.bag.covidcode.authcodegeneration.lockdown.config.LockdownConfig; 5 | import lombok.Builder; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | import org.springframework.test.context.TestPropertySource; 12 | 13 | import javax.naming.ConfigurationException; 14 | import java.time.LocalDateTime; 15 | import java.util.*; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertFalse; 18 | import static org.junit.jupiter.api.Assertions.assertThrows; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class LockdownConfigTest { 22 | 23 | private LockdownConfig lockdownConfig; 24 | 25 | @BeforeEach 26 | private void init() { 27 | lockdownConfig = new LockdownConfig(); 28 | } 29 | 30 | @Test 31 | public void endpointWithRangeError() { 32 | List endpoints = EndpointBuilder.have() 33 | .endpoint("xyz").fromUntil(LocalDateTime.MAX, LocalDateTime.MIN).build(); 34 | 35 | lockdownConfig.setEndpoints(endpoints); 36 | 37 | assertThrows(ConfigurationException.class, () -> { 38 | lockdownConfig.validate(); 39 | }); 40 | } 41 | 42 | 43 | static class EndpointBuilder { 44 | private Map> endpoints = new TreeMap<>(); 45 | private List fromUntils = new ArrayList<>(); 46 | 47 | 48 | protected static EndpointBuilder have() { 49 | return new EndpointBuilder(); 50 | } 51 | 52 | protected EndpointBuilder fromUntil(LocalDateTime from, LocalDateTime until) { 53 | 54 | Endpoint.FromUntil fromUntil = new Endpoint.FromUntil(); 55 | fromUntil.setFrom(from); 56 | fromUntil.setUntil(until); 57 | fromUntils.add(fromUntil); 58 | 59 | return this; 60 | } 61 | 62 | protected EndpointBuilder endpoint(String uri) { 63 | 64 | List current; 65 | if (endpoints.containsKey(uri)) { 66 | // endpoint followed by ranges 67 | current = endpoints.get(uri); 68 | if (current != fromUntils) { 69 | current.addAll(fromUntils); 70 | } 71 | fromUntils = new ArrayList<>(); 72 | } else { 73 | // ranges followed by endpoint 74 | endpoints.put(uri, fromUntils); 75 | } 76 | 77 | return this; 78 | } 79 | 80 | protected List build() { 81 | List result = new ArrayList<>(); 82 | for (String uri : endpoints.keySet()) { 83 | Endpoint endpoint = new Endpoint(); 84 | endpoint.setUri(uri); 85 | endpoint.setApplicable(endpoints.get(uri)); 86 | result.add(endpoint); 87 | } 88 | 89 | this.endpoints = new TreeMap<>(); 90 | this.fromUntils = new ArrayList<>(); 91 | 92 | return result; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/lockdown/LockdownInterceptorTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.lockdown; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.lockdown.config.Endpoint; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | public class LockdownInterceptorTest { 19 | 20 | private LockdownInterceptor lockdownInterceptor; 21 | 22 | @BeforeEach 23 | private void init() { 24 | lockdownInterceptor = new LockdownInterceptor(Collections.emptyList()); 25 | } 26 | 27 | @Test 28 | public void withoutConfiguredRestrictions() { 29 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.now(), "any-uri", Collections.emptyList())); 30 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.MIN, "any-uri", Collections.emptyList())); 31 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.MAX, "any-uri", Collections.emptyList())); 32 | } 33 | 34 | @Test 35 | public void withNonUriMatchingRestrictions() { 36 | Endpoint endpoint = create("any-other-uri"); 37 | List endpoints = Arrays.asList(endpoint); 38 | 39 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.now(), "any-uri", endpoints)); 40 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.MIN, "any-uri", endpoints)); 41 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.MAX, "any-uri", endpoints)); 42 | } 43 | 44 | @Test 45 | public void withUriMatchingRestrictions_withoutFromUntil() { 46 | Endpoint endpoint = create("any-uri"); 47 | List endpoints = Arrays.asList(endpoint); 48 | 49 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.now(), "any-uri", endpoints)); 50 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.MIN, "any-uri", endpoints)); 51 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.MAX, "any-uri", endpoints)); 52 | } 53 | 54 | @Test 55 | public void withUriMatchingRestrictions_withOpenMatchingFromUntil() { 56 | Endpoint endpoint = create("any-uri"); 57 | LocalDateTime now = LocalDateTime.now(); 58 | // from TOMORROW until end of time 59 | addFromUntil(endpoint, now.plusDays(1), null); 60 | // from start of time until YESTERDAY 61 | addFromUntil(endpoint, null, now.minusDays(1)); 62 | 63 | List endpoints = Arrays.asList(endpoint); 64 | 65 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.now(), "any-uri", endpoints)); 66 | assertExceptionWith(LocalDateTime.MIN, "any-uri", endpoints); 67 | assertExceptionWith(LocalDateTime.MAX, "any-uri", endpoints); 68 | } 69 | 70 | @Test 71 | public void withUriMatchingRestrictions_withMatchingFromUntil() { 72 | Endpoint endpoint = create("any-uri"); 73 | LocalDateTime now = LocalDateTime.now(); 74 | // from TOMORROW until end of time 75 | addFromUntil(endpoint, now.plusDays(1), LocalDateTime.MAX); 76 | // from start of time until YESTERDAY 77 | addFromUntil(endpoint, LocalDateTime.MIN, now.minusDays(1)); 78 | 79 | List endpoints = Arrays.asList(endpoint); 80 | 81 | assertFalse(lockdownInterceptor.isUriLocked(LocalDateTime.now(), "any-uri", endpoints)); 82 | assertExceptionWith(LocalDateTime.MIN, "any-uri", endpoints); 83 | assertExceptionWith(LocalDateTime.MAX, "any-uri", endpoints); 84 | } 85 | 86 | @Test 87 | public void withUriMatchingRestrictions_withBlocksOfFromUntil() { 88 | Endpoint endpoint = create("any-uri"); 89 | LocalDateTime now = LocalDateTime.now(); 90 | // from +2 until end +4 days 91 | addFromUntil(endpoint, now.plusDays(2), now.plusDays(4)); 92 | // from -4 until end +2 days 93 | addFromUntil(endpoint, now.minusDays(4), now.minusDays(2)); 94 | 95 | List endpoints = Arrays.asList(endpoint); 96 | 97 | // all points-in-time are outside of the locked blocks 98 | assertFalse(lockdownInterceptor.isUriLocked(now, "any-uri", endpoints)); 99 | assertFalse(lockdownInterceptor.isUriLocked(now.plusDays(5), "any-uri", endpoints)); 100 | assertFalse(lockdownInterceptor.isUriLocked(now.minusDays(5), "any-uri", endpoints)); 101 | 102 | // all points-in-time are inside of locked blocks 103 | assertExceptionWith(now.plusDays(3), "any-uri", endpoints); 104 | assertExceptionWith(now.minusDays(3), "any-uri", endpoints); 105 | } 106 | 107 | private void assertExceptionWith(LocalDateTime now, String requestUri, List endpoints) { 108 | assertThrows(LockdownException.class, () -> { 109 | lockdownInterceptor.isUriLocked(now, requestUri, endpoints); 110 | }); 111 | } 112 | 113 | private Endpoint create(String uri) { 114 | 115 | Endpoint endpoint = new Endpoint(); 116 | endpoint.setUri(uri); 117 | endpoint.setApplicable(new ArrayList<>()); 118 | 119 | return endpoint; 120 | } 121 | 122 | private Endpoint addFromUntil(Endpoint endpoint, LocalDateTime from, LocalDateTime until) { 123 | 124 | Endpoint.FromUntil fromUntil = new Endpoint.FromUntil(); 125 | fromUntil.setFrom(from); 126 | fromUntil.setUntil(until); 127 | 128 | endpoint.getApplicable().add(fromUntil); 129 | 130 | return endpoint; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/service/AuthCodeDeletionServiceITTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCode; 4 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.annotation.DirtiesContext; 9 | import org.springframework.test.context.ActiveProfiles; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.time.LocalDate; 13 | import java.time.ZonedDateTime; 14 | 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | import static org.hamcrest.core.Is.is; 17 | 18 | @SpringBootTest(properties = { 19 | "spring.jpa.hibernate.ddl-auto=create", 20 | "spring.datasource.driver-class-name=org.h2.Driver", 21 | "spring.datasource.url=jdbc:h2:mem:test;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE", 22 | "spring.datasource.username=sa", 23 | "spring.datasource.password=sa", 24 | "CF_INSTANCE_INDEX=0" 25 | }) 26 | @ActiveProfiles("local") 27 | @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) 28 | class AuthCodeDeletionServiceITTest { 29 | 30 | @Autowired 31 | private AuthorizationCodeRepository authorizationCodeRepository; 32 | 33 | @Autowired 34 | private AuthCodeDeletionService authCodeDeletionService; 35 | 36 | @Test 37 | @Transactional 38 | void deleteOldAuthCode_foundTwo_deleteTwo() { 39 | //given 40 | String codeInThePast = "123456789123"; 41 | String codeInTheFuture= "111111111111"; 42 | authorizationCodeRepository.saveAndFlush(generateAuthCode(codeInThePast, ZonedDateTime.now().minusMinutes(1))); 43 | authorizationCodeRepository.saveAndFlush(generateAuthCode(codeInTheFuture, ZonedDateTime.now().plusMinutes(5))); 44 | assertThat(authorizationCodeRepository.findAll().size(), is(2)); 45 | 46 | //when 47 | authCodeDeletionService.deleteOldAuthCode(); 48 | 49 | //then 50 | assertThat(authorizationCodeRepository.findByCode(codeInThePast).isPresent(), is(false)); 51 | assertThat(authorizationCodeRepository.findByCode(codeInTheFuture).isPresent(), is(true)); 52 | } 53 | 54 | private AuthorizationCode generateAuthCode(String code, ZonedDateTime expiryDate){ 55 | return new AuthorizationCode(code, LocalDate.now(), LocalDate.now().minusDays(3), expiryDate); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/service/AuthCodeDeletionServiceTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCode; 4 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.InjectMocks; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.jupiter.MockitoExtension; 10 | import org.springframework.test.context.ActiveProfiles; 11 | 12 | import java.time.LocalDate; 13 | import java.time.ZonedDateTime; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import static org.mockito.Mockito.*; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | @ActiveProfiles("local") 21 | class AuthCodeDeletionServiceTest { 22 | 23 | @Mock 24 | private AuthorizationCodeRepository repository; 25 | 26 | @InjectMocks 27 | private AuthCodeDeletionService authCodeDeletionService; 28 | 29 | 30 | @Test 31 | void deleteOldAuthCode_foundTwo_deleteTwo() { 32 | //given 33 | List codes = new ArrayList<>(); 34 | codes.add(createAuthorizationCode()); 35 | codes.add(createAuthorizationCode()); 36 | when(repository.findByExpiryDateBefore(any(ZonedDateTime.class))).thenReturn(codes); 37 | 38 | //when 39 | authCodeDeletionService.deleteOldAuthCode(); 40 | 41 | //then 42 | verify(repository, times(2)).delete(any(AuthorizationCode.class)); 43 | } 44 | 45 | private AuthorizationCode createAuthorizationCode() { 46 | AuthorizationCode authorizationCode = mock(AuthorizationCode.class); 47 | when(authorizationCode.getCreationDateTime()).thenReturn(ZonedDateTime.now()); 48 | when(authorizationCode.getExpiryDate()).thenReturn(ZonedDateTime.now()); 49 | when(authorizationCode.getOnsetDate()).thenReturn(LocalDate.now()); 50 | when(authorizationCode.getOriginalOnsetDate()).thenReturn(LocalDate.now()); 51 | return authorizationCode; 52 | } 53 | 54 | @Test 55 | void deleteOldAuthCode_foundNone_deleteNone() { 56 | //given 57 | List codes = new ArrayList<>(); 58 | when(repository.findByExpiryDateBefore(any(ZonedDateTime.class))).thenReturn(codes); 59 | 60 | //when 61 | authCodeDeletionService.deleteOldAuthCode(); 62 | 63 | //then 64 | verify(repository, never()).delete(any(AuthorizationCode.class)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/service/AuthCodeGenerationServiceTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeResponseDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeCreateDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCode; 6 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.ArgumentMatchers; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import org.springframework.web.server.ResponseStatusException; 14 | 15 | import java.time.LocalDate; 16 | 17 | import static org.junit.jupiter.api.Assertions.*; 18 | import static org.mockito.ArgumentMatchers.anyString; 19 | import static org.mockito.Mockito.*; 20 | 21 | @ExtendWith(MockitoExtension.class) 22 | class AuthCodeGenerationServiceTest { 23 | 24 | @Mock 25 | private AuthorizationCodeRepository repository; 26 | @InjectMocks 27 | private AuthCodeGenerationService testee; 28 | 29 | @Test 30 | void test_create() { 31 | //given 32 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now().minusWeeks(2)); 33 | when(repository.existsByCode(anyString())).thenReturn(false); 34 | 35 | //when 36 | AuthorizationCodeResponseDto responseDto = testee.create(createDto); 37 | //then 38 | assertNotNull(responseDto.getAuthorizationCode()); 39 | assertTrue(responseDto.getAuthorizationCode().matches("\\d{12}")); 40 | verify(repository, times(1)).saveAndFlush(ArgumentMatchers.any(AuthorizationCode.class)); 41 | } 42 | 43 | @Test 44 | void test_create_code_already_exists() { 45 | //given 46 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now()); 47 | when(repository.existsByCode(anyString())).thenReturn(true).thenReturn(false); 48 | 49 | //when 50 | AuthorizationCodeResponseDto responseDto = testee.create(createDto); 51 | //then 52 | assertNotNull(responseDto.getAuthorizationCode()); 53 | assertTrue(responseDto.getAuthorizationCode().matches("\\d{12}")); 54 | verify(repository, times(1)).saveAndFlush(ArgumentMatchers.any(AuthorizationCode.class)); 55 | } 56 | 57 | @Test 58 | void test_create_invalid_onset_date_in_future() { 59 | //given 60 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now().plusDays(1)); 61 | //when 62 | //then 63 | assertThrows(ResponseStatusException.class, () -> testee.create(createDto)); 64 | } 65 | 66 | @Test 67 | void test_create_invalid_onset_date_too_far_back() { 68 | //given 69 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.of(2017,7,7)); 70 | //when 71 | //then 72 | assertThrows(ResponseStatusException.class, () -> testee.create(createDto)); 73 | } 74 | 75 | @Test 76 | void test_create_invalid_onset_date_4_weeks_plus_one_day_back() { 77 | //given 78 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now().minusWeeks(4).minusDays(1)); 79 | //when 80 | //then 81 | assertThrows(ResponseStatusException.class, () -> testee.create(createDto)); 82 | } 83 | 84 | @Test 85 | void test_create_valid_onset_date_exactly_4_weeks_back() { 86 | //given 87 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now().minusWeeks(4)); 88 | //when 89 | AuthorizationCodeResponseDto responseDto = testee.create(createDto); 90 | //then 91 | assertNotNull(responseDto.getAuthorizationCode()); 92 | assertTrue(responseDto.getAuthorizationCode().matches("\\d{12}")); 93 | verify(repository, times(1)).saveAndFlush(ArgumentMatchers.any(AuthorizationCode.class)); 94 | } 95 | 96 | @Test 97 | void test_create_valid_onset_date_exactly_now() { 98 | //given 99 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now()); 100 | //when 101 | AuthorizationCodeResponseDto responseDto = testee.create(createDto); 102 | //then 103 | assertNotNull(responseDto.getAuthorizationCode()); 104 | assertTrue(responseDto.getAuthorizationCode().matches("\\d{12}")); 105 | verify(repository, times(1)).saveAndFlush(ArgumentMatchers.any(AuthorizationCode.class)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/service/AuthCodeVerificationServiceTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeOnsetResponseDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.api.TokenType; 6 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCode; 7 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import org.springframework.data.rest.webmvc.ResourceNotFoundException; 14 | import org.springframework.test.util.ReflectionTestUtils; 15 | import org.yaml.snakeyaml.Yaml; 16 | 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.time.LocalDate; 20 | import java.time.ZonedDateTime; 21 | import java.time.format.DateTimeFormatter; 22 | import java.util.Map; 23 | import java.util.Optional; 24 | 25 | import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.DP3T_TOKEN; 26 | import static org.junit.jupiter.api.Assertions.*; 27 | import static org.mockito.ArgumentMatchers.*; 28 | import static org.mockito.Mockito.verify; 29 | import static org.mockito.Mockito.when; 30 | 31 | @ExtendWith(MockitoExtension.class) 32 | class AuthCodeVerificationServiceTest { 33 | 34 | private static final String FAKE_NOT_FAKE = "0"; 35 | private static final String FAKE_FAKE = "1"; 36 | private static final String CALL_COUNT_LIMIT_KEY = "callCountLimit"; 37 | private static final String TEST_AUTHORIZATION_CODE = "123456789"; 38 | private static final String TEST_ACCESS_TOKEN = "QRMwjii77"; 39 | private static final int CODE_EXPIRATION_DELAY_IN_SECONDS = 10; 40 | private static final int CALL_COUNT_LIMIT = 3; 41 | 42 | @Mock 43 | private AuthorizationCodeRepository repository; 44 | 45 | @Mock 46 | private CustomTokenProvider tokenProvider; 47 | 48 | @InjectMocks 49 | private AuthCodeVerificationService testee; 50 | 51 | @Test 52 | void test_verify() { 53 | //given 54 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), LocalDate.now().minusDays(3), ZonedDateTime.now().plusSeconds(CODE_EXPIRATION_DELAY_IN_SECONDS)); 55 | ReflectionTestUtils.setField(testee, CALL_COUNT_LIMIT_KEY, CALL_COUNT_LIMIT); 56 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 57 | when(tokenProvider.createToken(anyString(), anyString(), any())).thenReturn(TEST_ACCESS_TOKEN); 58 | 59 | 60 | //when 61 | AuthorizationCodeVerifyResponseDto responseDto = testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 62 | //then 63 | assertNotNull(responseDto.getAccessToken()); 64 | assertEquals(TEST_ACCESS_TOKEN, responseDto.getAccessToken()); 65 | } 66 | 67 | @Test 68 | void test_verify_with_yml_prop_callCountLimit() throws Exception { 69 | //setup 70 | Path file = Path.of("", "src/main/resources").resolve("application.yml"); 71 | Map yamlMaps = new Yaml().load(Files.readString(file)); 72 | final Map> obj = (Map>) yamlMaps.get("authcodegeneration"); 73 | int callCountLimit = Integer.parseInt(obj.get("service").get(CALL_COUNT_LIMIT_KEY).toString()); 74 | //given 75 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), LocalDate.now().minusDays(3), ZonedDateTime.now().plusSeconds(CODE_EXPIRATION_DELAY_IN_SECONDS)); 76 | ReflectionTestUtils.setField(testee, CALL_COUNT_LIMIT_KEY, callCountLimit); 77 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 78 | when(tokenProvider.createToken(anyString(), anyString(), any())).thenReturn(TEST_ACCESS_TOKEN); 79 | 80 | //when 81 | AuthorizationCodeVerifyResponseDto responseDto = testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 82 | //then 83 | assertNotNull(responseDto.getAccessToken()); 84 | assertEquals(TEST_ACCESS_TOKEN, responseDto.getAccessToken()); 85 | } 86 | 87 | @Test 88 | void test_verify_token_onset_date_is_equal_original_minus_3_days() { 89 | //given 90 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), LocalDate.now().minusDays(3), ZonedDateTime.now().plusSeconds(CODE_EXPIRATION_DELAY_IN_SECONDS)); 91 | ReflectionTestUtils.setField(testee, CALL_COUNT_LIMIT_KEY, CALL_COUNT_LIMIT); 92 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 93 | when(tokenProvider.createToken(anyString(), anyString(), any())).thenReturn(TEST_ACCESS_TOKEN); 94 | 95 | 96 | //when 97 | AuthorizationCodeVerifyResponseDto responseDto = testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 98 | //then 99 | verify(tokenProvider).createToken(eq(LocalDate.now().minusDays(3).toString()), anyString(), eq(DP3T_TOKEN)); 100 | assertNotNull(responseDto.getAccessToken()); 101 | assertEquals(TEST_ACCESS_TOKEN, responseDto.getAccessToken()); 102 | } 103 | 104 | @Test 105 | void test_verify_call_count_reached() { 106 | //given 107 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), LocalDate.now().minusDays(3), ZonedDateTime.now().plusSeconds(CODE_EXPIRATION_DELAY_IN_SECONDS)); 108 | ReflectionTestUtils.setField(testee, CALL_COUNT_LIMIT_KEY, CALL_COUNT_LIMIT); 109 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 110 | when(tokenProvider.createToken(anyString(), anyString(), any())).thenReturn(TEST_ACCESS_TOKEN); 111 | 112 | 113 | //when 114 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 115 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 116 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 117 | //then 118 | assertNull(testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE)); 119 | } 120 | 121 | 122 | @Test 123 | void test_verify_call_fake_count_never_reached() { 124 | //given 125 | ReflectionTestUtils.setField(testee, CALL_COUNT_LIMIT_KEY, CALL_COUNT_LIMIT); 126 | when(tokenProvider.createToken(anyString(), anyString(), any())).thenReturn(TEST_ACCESS_TOKEN); 127 | 128 | 129 | //when 130 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_FAKE); 131 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_FAKE); 132 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_FAKE); 133 | //then 134 | AuthorizationCodeVerifyResponseDto verify = testee.verify(TEST_AUTHORIZATION_CODE, FAKE_FAKE); 135 | 136 | assertNotNull(verify); 137 | } 138 | 139 | 140 | @Test 141 | void test_verify_code_not_found() { 142 | //given 143 | when(repository.findByCode(anyString())).thenReturn(Optional.empty()); 144 | //when 145 | //then 146 | assertNull(testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE)); 147 | } 148 | 149 | @Test 150 | void test_verify_code_validity_expired() { 151 | //given 152 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), LocalDate.now().minusDays(3), ZonedDateTime.now()); 153 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 154 | //when 155 | //then 156 | assertNull(testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE)); 157 | } 158 | 159 | @Test 160 | void test_getOnset() { 161 | //given 162 | final var onsetAsDate = LocalDate.now().minusDays(3); 163 | final var onsetAsString = onsetAsDate.format(DateTimeFormatter.ofPattern("YYYY-MM-dd")); 164 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), onsetAsDate, ZonedDateTime.now().plusSeconds(CODE_EXPIRATION_DELAY_IN_SECONDS)); 165 | ReflectionTestUtils.setField(testee, CALL_COUNT_LIMIT_KEY, CALL_COUNT_LIMIT); 166 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 167 | //when 168 | String onset = testee.getOnsetForAuthCode(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE).getOnset(); 169 | //then 170 | assertNotNull(onset); 171 | assertEquals(onsetAsString, onset); 172 | } 173 | 174 | @Test 175 | void test_getOnset_code_not_found() { 176 | //given 177 | when(repository.findByCode(anyString())).thenReturn(Optional.empty()); 178 | //when 179 | //then 180 | assertNull(testee.getOnsetForAuthCode(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE).getOnset()); 181 | } 182 | 183 | @Test 184 | void test_getOnset_code_validy_expired() { 185 | //given 186 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), LocalDate.now().minusDays(3), ZonedDateTime.now()); 187 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 188 | //when 189 | //then 190 | assertNull(testee.getOnsetForAuthCode(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE).getOnset()); 191 | } 192 | 193 | @Test 194 | void test_getOnset_code_call_count_reached() { 195 | //given 196 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), LocalDate.now().minusDays(3), ZonedDateTime.now().plusSeconds(CODE_EXPIRATION_DELAY_IN_SECONDS)); 197 | ReflectionTestUtils.setField(testee, CALL_COUNT_LIMIT_KEY, CALL_COUNT_LIMIT); 198 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 199 | when(tokenProvider.createToken(anyString(), anyString(), any())).thenReturn(TEST_ACCESS_TOKEN); 200 | //when 201 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 202 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 203 | testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 204 | //then 205 | assertNull(testee.getOnsetForAuthCode(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE).getOnset()); 206 | } 207 | 208 | @Test 209 | void test_getOnset_and_verify() { 210 | //given 211 | final var onsetAsDate = LocalDate.now().minusDays(3); 212 | final var onsetAsString = onsetAsDate.format(DateTimeFormatter.ofPattern("YYYY-MM-dd")); 213 | AuthorizationCode authCode = new AuthorizationCode(TEST_AUTHORIZATION_CODE, LocalDate.now(), onsetAsDate, ZonedDateTime.now().plusSeconds(CODE_EXPIRATION_DELAY_IN_SECONDS)); 214 | ReflectionTestUtils.setField(testee, CALL_COUNT_LIMIT_KEY, 1); 215 | when(repository.findByCode(anyString())).thenReturn(Optional.of(authCode)); 216 | when(tokenProvider.createToken(anyString(), anyString(), any())).thenReturn(TEST_ACCESS_TOKEN); 217 | //when 218 | final AuthorizationCodeOnsetResponseDto onsetResponse = testee.getOnsetForAuthCode(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 219 | final AuthorizationCodeVerifyResponseDto verifyResponse = testee.verify(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 220 | //then 221 | assertNotNull(onsetResponse.getOnset()); 222 | assertEquals(onsetAsString, onsetResponse.getOnset()); 223 | assertNotNull(verifyResponse.getAccessToken()); 224 | assertEquals(TEST_ACCESS_TOKEN, verifyResponse.getAccessToken()); 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/service/CustomTokenProviderTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.service; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.TokenType; 4 | import ch.admin.bag.covidcode.authcodegeneration.service.CustomTokenProvider; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.test.util.ReflectionTestUtils; 8 | 9 | import java.security.NoSuchAlgorithmException; 10 | 11 | import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.CHECKIN_USERUPLOAD_TOKEN; 12 | import static ch.admin.bag.covidcode.authcodegeneration.api.TokenType.DP3T_TOKEN; 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertNotNull; 15 | 16 | @Slf4j 17 | class CustomTokenProviderTest { 18 | 19 | @Test 20 | void createToken() throws NoSuchAlgorithmException { 21 | 22 | CustomTokenProvider tokenProvider = new CustomTokenProvider(); 23 | tokenProvider.init(); 24 | 25 | ReflectionTestUtils.setField(tokenProvider, "issuer", "http://localhost:8113"); 26 | ReflectionTestUtils.setField(tokenProvider, "tokenValidity", 300000); 27 | ReflectionTestUtils.setField(tokenProvider, "privateKey", "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDT5jbu96Mf5UvkGrp3sj0Spoh0Cf664/ksSs5fOGM6ZFRh4IkjDRokfEu7nWbGSYftrfVLAatL1At+Jc+yEe29RKFzAHjatmLSCb59BtWoJAHa3+gPm2Essk2F93iAuRiWPVeJ3uz1fqPCYG9Gca65YSzOvlPUN7+Nih/0DeJOfrtq138c5Thv+YgJbzih1p2T+9iEZ9QqE77Xf0oNFJCpWcW8Fu2JKCHuDPYCjXdEHlONClHUskICqmheppsaSqdSIXxbj9ZylHUqTp/zLsL7m91Z0/Fy3ZUgYCKJSopCaWJSZJIq4YEM5OMwjArCB1/UZyWzARsdjj5j8cRYc+BdAgMBAAECggEARLKLNrgkM5AEJaSgtXOcpzJEZNJkujR0sO5jr605RlIGpWDFNQ7nXdLKPr4N9tUZ822Fa9bTsRbCzxf1GPcFC2p3qTAK/mVI7m1oS2Ju3D8oNsyGkKDARVxdE8SiVaEsnnCus60JR6HR94+KI91xVvpxK2m7Bb85I+sW5umlZ+rI59XgnP9q3EtOxnC4GQ/QfTjFIgAZHUJwzh0SZ+I2GPXLw9WkFGyxgW6wKaNQOUz/jjJyPNTl3jg0cW9kcHrviyHZK+IPVwQINj7JEzRZ5rCXBjEBE8ht1g1vu7QPQD1x7ZGCrjWAXfr01rNSJMPNEIi1CEaVbz63j6ewUVZV4QKBgQD7GNOnDorqimmGO+sJ6LV3DqrBa2TPCfclcgSiKBu+A/B1nBBLAsNVXN59BTtYHilj+Xhdt1cmOHBo9a0ZCrqHjU6xWyl9iOxh8rhpE0FZ/3g3aWM7aII+SyheMrFtfBh4XrvH5ZJLrzJiiGg+TDTuFApksmb6qf/SE+ZQFZNRaQKBgQDYCXKs051beqn1kHWVwTcb1UTqrTuclgnNZ3sPKjVwvGCqUIRsyWZOV8E7PFeU2F9Ek/3nvLw5pTY+F5Aq0Tfaxt3yS7nRhRfWLL4wMyezYrE/qPFN5L8bhejcuSjRieIWXQU8vMI0+YIIV45JmP44M9cjbcrkPuzvl2soY8+E1QKBgQCOC2NgM9feCmLbrvWta1mMel2agXhLryWCp1d7rBjVi0DyJ1EIPg3mMl0ieF0z4gwkJDI1QcwpMPBWT/SWH/2ZRRTpO9riyxx95GLx/hSQJvcI0bNzHhHfz4CMmTzJ5NOq9FxiHrp92iQ0nVnrNA0VSXz/rfSXhKfVXbCCSVJHUQKBgQCe+CzTMhCLvTKNiZSM8xW7PG8vBPRloB5scGYkXZnfcC7thLw9VOIcagS9swR7edB4pTHkMYSMIp9Mh4hFiZjBOy8c2U5N99L3fgsharMfFFN7lbSi7d0Wwq38pZ98uSqN7DsrW3bJBoUB4HPKgnMnJjZ8UpFG7WrqTxDCMtgEVQKBgFFFy9AypTsQgdQf1VWdUGX7uJ8TYTuhMVzcOThtQeuVnTEqp9gUyuarc6xOL2i7wD6cdnb48NhqJ66r/F8bxMAUMOaElrAo7IIZXfSJdpPVFMwGVz022lk//XdjA/VcJv5tNnBoKP29cRCkf6WU1SEdlyoVu/vzPNMGbNZMyt8s"); 28 | 29 | String dp3tToken = tokenProvider.createToken("2020-08-15", "0", DP3T_TOKEN); 30 | String dp3tFakeToken = tokenProvider.createToken("2020-08-15", "1", DP3T_TOKEN); 31 | String checkInToken = tokenProvider.createToken("2020-08-15", "0", CHECKIN_USERUPLOAD_TOKEN); 32 | String checkInFakeToken = tokenProvider.createToken("2020-08-15", "1", CHECKIN_USERUPLOAD_TOKEN); 33 | 34 | assertNotNull(dp3tToken); 35 | assertNotNull(dp3tFakeToken); 36 | assertNotNull(checkInToken); 37 | assertNotNull(checkInFakeToken); 38 | 39 | // TODO Decode tokens and make some useful assertions here 40 | 41 | log.debug(dp3tToken); 42 | log.debug(checkInToken); 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/testutil/JwtTestUtil.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.testutil; 2 | 3 | import io.jsonwebtoken.Header; 4 | import io.jsonwebtoken.Jwts; 5 | import io.jsonwebtoken.SignatureAlgorithm; 6 | 7 | import java.security.KeyFactory; 8 | import java.security.PrivateKey; 9 | import java.security.spec.KeySpec; 10 | import java.security.spec.PKCS8EncodedKeySpec; 11 | import java.time.LocalDateTime; 12 | import java.time.ZoneId; 13 | import java.util.*; 14 | 15 | public class JwtTestUtil { 16 | private static final String JWT_CONTEXT = "USER"; 17 | private static final String FIRST_NAME = "Henriette"; 18 | private static final String LAST_NAME = "Muster"; 19 | private static final String PREFERRED_USERNAME = "12345"; 20 | private static final String LOCALE_DE = "DE"; 21 | private static final String CLIENT_ID = "ha-ui"; 22 | private static final String CRYPTO_ALGORITHM = "RSA"; 23 | private static final String ISSUER = "http://localhost:8180"; 24 | 25 | public static String getJwtTestToken(String privateKey, LocalDateTime expiration, String userRole) throws Exception { 26 | KeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)); 27 | KeyFactory kf = KeyFactory.getInstance(CRYPTO_ALGORITHM); 28 | PrivateKey privateKeyToSignWith = kf.generatePrivate(keySpec); 29 | Map claims = new LinkedHashMap<>(); 30 | claims.put("user_name", "user"); 31 | claims.put("ctx", JWT_CONTEXT); 32 | claims.put("iss", ISSUER); 33 | claims.put("preferred_username", PREFERRED_USERNAME); 34 | claims.put("given_name", FIRST_NAME); 35 | claims.put("locale", LOCALE_DE); 36 | claims.put("client_id", CLIENT_ID); 37 | claims.put("bproles", new HashMap<>()); 38 | claims.put("userroles", Collections.singletonList(userRole)); 39 | claims.put("scope", Arrays.asList("email", "openid", "profile")); 40 | claims.put("name", FIRST_NAME + " " + LAST_NAME); 41 | claims.put("exp", convertToDateViaInstant(expiration)); 42 | claims.put("family_name", LAST_NAME); 43 | claims.put("jti", UUID.randomUUID().toString()); 44 | 45 | return Jwts.builder() 46 | .setId(UUID.randomUUID().toString()) 47 | .setIssuer(ISSUER) 48 | .setIssuedAt(new Date(System.currentTimeMillis())) 49 | .setSubject(UUID.randomUUID().toString()) 50 | .setHeaderParam(Header.TYPE, Header.JWT_TYPE) 51 | .setClaims(claims) 52 | .signWith(privateKeyToSignWith, SignatureAlgorithm.RS256) 53 | .compact(); 54 | } 55 | 56 | private static Date convertToDateViaInstant(LocalDateTime dateToConvert) { 57 | return java.util.Date 58 | .from(dateToConvert.atZone(ZoneId.systemDefault()) 59 | .toInstant()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/testutil/KeyPairTestUtil.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.testutil; 2 | 3 | import com.nimbusds.jose.JWSAlgorithm; 4 | import com.nimbusds.jose.jwk.JWKSet; 5 | import com.nimbusds.jose.jwk.KeyUse; 6 | import com.nimbusds.jose.jwk.RSAKey; 7 | 8 | import java.security.KeyFactory; 9 | import java.security.KeyPair; 10 | import java.security.KeyPairGenerator; 11 | import java.security.NoSuchAlgorithmException; 12 | import java.security.interfaces.RSAPublicKey; 13 | import java.security.spec.InvalidKeySpecException; 14 | import java.security.spec.X509EncodedKeySpec; 15 | import java.util.Base64; 16 | 17 | public class KeyPairTestUtil { 18 | 19 | private static final String CRYPTO_ALGORITHM = "RSA"; 20 | private static final int KEY_SIZE = 2048; 21 | private static final String KEY_ID = "test-id"; 22 | 23 | private KeyPair keyPair; 24 | 25 | public KeyPairTestUtil() { 26 | try { 27 | this.keyPair = getKeyPair(); 28 | } catch (NoSuchAlgorithmException e) { 29 | e.printStackTrace(); 30 | } 31 | } 32 | 33 | public String getPrivateKey() { 34 | return Base64.getEncoder().encodeToString(this.keyPair.getPrivate().getEncoded()); 35 | } 36 | 37 | public String getJwks() throws NoSuchAlgorithmException, InvalidKeySpecException { 38 | KeyFactory keyFactory = KeyFactory.getInstance(CRYPTO_ALGORITHM); 39 | X509EncodedKeySpec spec = new X509EncodedKeySpec(this.keyPair.getPublic().getEncoded()); 40 | RSAPublicKey publicKeyObj = (RSAPublicKey) keyFactory.generatePublic(spec); 41 | RSAKey.Builder builder = new RSAKey.Builder(publicKeyObj) 42 | .keyUse(KeyUse.SIGNATURE) 43 | .algorithm(JWSAlgorithm.RS256) 44 | .keyID(KEY_ID); 45 | return new JWKSet(builder.build()).toString(); 46 | } 47 | 48 | private KeyPair getKeyPair() throws NoSuchAlgorithmException { 49 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(CRYPTO_ALGORITHM); 50 | keyPairGenerator.initialize(KEY_SIZE); 51 | return keyPairGenerator.generateKeyPair(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/testutil/LocalDateSerializer.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.testutil; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.databind.SerializerProvider; 5 | import com.fasterxml.jackson.databind.ser.std.StdSerializer; 6 | 7 | import java.io.IOException; 8 | import java.time.LocalDate; 9 | import java.time.format.DateTimeFormatter; 10 | 11 | public class LocalDateSerializer extends StdSerializer { 12 | 13 | public LocalDateSerializer() { 14 | super(LocalDate.class); 15 | } 16 | 17 | @Override 18 | public void serialize(LocalDate value, JsonGenerator generator, SerializerProvider provider) throws IOException { 19 | generator.writeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/testutil/LoggerTestUtil.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.testutil; 2 | 3 | import ch.qos.logback.classic.Logger; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import ch.qos.logback.core.read.ListAppender; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public class LoggerTestUtil { 9 | public static ListAppender getListAppenderForClass(Class clazz) { 10 | Logger logger = (Logger) LoggerFactory.getLogger(clazz); 11 | 12 | ListAppender loggingEventListAppender = new ListAppender<>(); 13 | loggingEventListAppender.start(); 14 | 15 | logger.addAppender(loggingEventListAppender); 16 | 17 | return loggingEventListAppender; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeGenerationControllerSecurityTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeCreateDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.config.security.OAuth2SecuredWebConfiguration; 5 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeGenerationService; 6 | import ch.admin.bag.covidcode.authcodegeneration.testutil.JwtTestUtil; 7 | import ch.admin.bag.covidcode.authcodegeneration.testutil.KeyPairTestUtil; 8 | import ch.admin.bag.covidcode.authcodegeneration.testutil.LocalDateSerializer; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import com.fasterxml.jackson.databind.module.SimpleModule; 11 | import com.github.tomakehurst.wiremock.WireMockServer; 12 | import org.junit.jupiter.api.AfterAll; 13 | import org.junit.jupiter.api.BeforeAll; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 17 | import org.springframework.boot.test.mock.mockito.MockBean; 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.test.context.ActiveProfiles; 21 | import org.springframework.test.web.servlet.MockMvc; 22 | import org.springframework.test.web.servlet.ResultMatcher; 23 | 24 | import java.time.LocalDate; 25 | import java.time.LocalDateTime; 26 | 27 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 28 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; 29 | import static org.mockito.ArgumentMatchers.any; 30 | import static org.mockito.Mockito.times; 31 | import static org.mockito.Mockito.verify; 32 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 34 | 35 | @WebMvcTest(value = {AuthCodeGenerationController.class, OAuth2SecuredWebConfiguration.class}, 36 | properties="jeap.security.oauth2.resourceserver.authorization-server.jwk-set-uri=http://localhost:8182/.well-known/jwks.json") // Avoid port 8180, see below 37 | @ActiveProfiles("local") 38 | class AuthCodeGenerationControllerSecurityTest { 39 | 40 | private static final String URL = "/v1/authcode"; 41 | private static final String VALID_USER_ROLE = "bag-pts-allow"; 42 | private static final String INVALID_USER_ROLE = "invalid-role"; 43 | // Avoid port 8180, which is likely used by the local KeyCloak: 44 | private static final int MOCK_SERVER_PORT = 8182; 45 | 46 | @Autowired 47 | private MockMvc mockMvc; 48 | 49 | @MockBean 50 | private AuthCodeGenerationService service; 51 | 52 | private static final ObjectMapper MAPPER = new ObjectMapper(); 53 | private static final KeyPairTestUtil KEY_PAIR_TEST_UTIL = new KeyPairTestUtil(); 54 | private static final String PRIVATE_KEY = KEY_PAIR_TEST_UTIL.getPrivateKey(); 55 | private static final LocalDateTime EXPIRED_IN_FUTURE = LocalDateTime.now().plusDays(1); 56 | private static final LocalDateTime EXPIRED_IN_PAST = LocalDateTime.now().minusDays(1); 57 | 58 | private static WireMockServer wireMockServer = 59 | // new WireMockServer(options().dynamicPort()); 60 | new WireMockServer(options().port(MOCK_SERVER_PORT)); 61 | 62 | @BeforeAll 63 | static void setup() throws Exception { 64 | wireMockServer.start(); 65 | wireMockServer.stubFor(get(urlPathEqualTo("/.well-known/jwks.json")).willReturn(aResponse() 66 | .withStatus(HttpStatus.OK.value()) 67 | .withHeader("Content-Type", "application/json") 68 | .withBody(KEY_PAIR_TEST_UTIL.getJwks()))); 69 | SimpleModule module = new SimpleModule(); 70 | module.addSerializer(LocalDate.class, new LocalDateSerializer()); 71 | MAPPER.registerModule(module); 72 | } 73 | 74 | @AfterAll 75 | static void teardown() { 76 | wireMockServer.stop(); 77 | } 78 | 79 | @Test 80 | void test_create_authorization_with_valid_token() throws Exception { 81 | test_call_create_with_token(EXPIRED_IN_FUTURE, VALID_USER_ROLE, HttpStatus.OK); 82 | verify(service, times(1)).create(any(AuthorizationCodeCreateDto.class)); 83 | } 84 | 85 | @Test 86 | void test_create_authorization_with_valid_token_but_wrong_userrole() throws Exception { 87 | test_call_create_with_token(EXPIRED_IN_FUTURE, INVALID_USER_ROLE, HttpStatus.FORBIDDEN); 88 | verify(service, times(0)).create(any(AuthorizationCodeCreateDto.class)); 89 | } 90 | 91 | @Test 92 | void test_create_authorization_with_expired_token() throws Exception { 93 | test_call_create_with_token(EXPIRED_IN_PAST, VALID_USER_ROLE, HttpStatus.UNAUTHORIZED); 94 | verify(service, times(0)).create(any(AuthorizationCodeCreateDto.class)); 95 | } 96 | 97 | 98 | private void test_call_create_with_token(LocalDateTime tokenExpiration, String userRole, HttpStatus status) throws Exception { 99 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now()); 100 | String token = JwtTestUtil.getJwtTestToken(PRIVATE_KEY, tokenExpiration, userRole); 101 | mockMvc.perform(post(URL) 102 | .accept(MediaType.APPLICATION_JSON_VALUE) 103 | .contentType(MediaType.APPLICATION_JSON_VALUE) 104 | .header("Authorization", "Bearer " + token) 105 | .content(MAPPER.writeValueAsString(createDto))) 106 | .andExpect(getResultMatcher(status)); 107 | } 108 | 109 | private ResultMatcher getResultMatcher(HttpStatus status) { 110 | switch(status) { 111 | case OK: 112 | return status().isOk(); 113 | case FORBIDDEN: 114 | return status().isForbidden(); 115 | case UNAUTHORIZED: 116 | return status().isUnauthorized(); 117 | default: 118 | throw new IllegalArgumentException("HttpStatus not found!"); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeGenerationControllerTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeCreateDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeResponseDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.JeapAuthenticationToken; 6 | import ch.admin.bag.covidcode.authcodegeneration.config.security.authentication.ServletJeapAuthorization; 7 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeGenerationService; 8 | import ch.admin.bag.covidcode.authcodegeneration.testutil.LocalDateSerializer; 9 | import ch.admin.bag.covidcode.authcodegeneration.testutil.LoggerTestUtil; 10 | import ch.qos.logback.classic.Level; 11 | import ch.qos.logback.classic.spi.ILoggingEvent; 12 | import ch.qos.logback.core.read.ListAppender; 13 | import com.fasterxml.jackson.core.JsonProcessingException; 14 | import com.fasterxml.jackson.databind.ObjectMapper; 15 | import com.fasterxml.jackson.databind.module.SimpleModule; 16 | import org.assertj.core.groups.Tuple; 17 | import org.junit.Assert; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.ExtendWith; 21 | import org.mockito.InjectMocks; 22 | import org.mockito.Mock; 23 | import org.mockito.junit.jupiter.MockitoExtension; 24 | import org.springframework.http.HttpStatus; 25 | import org.springframework.http.MediaType; 26 | import org.springframework.security.access.AccessDeniedException; 27 | import org.springframework.security.oauth2.jwt.Jwt; 28 | import org.springframework.test.web.servlet.MockMvc; 29 | import org.springframework.test.web.servlet.MvcResult; 30 | import org.springframework.web.server.ResponseStatusException; 31 | 32 | import java.time.LocalDate; 33 | import java.util.Collections; 34 | 35 | import static org.assertj.core.api.Assertions.assertThat; 36 | import static org.junit.jupiter.api.Assertions.assertEquals; 37 | import static org.mockito.ArgumentMatchers.any; 38 | import static org.mockito.Mockito.when; 39 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 40 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 41 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; 42 | 43 | @ExtendWith(MockitoExtension.class) 44 | class AuthCodeGenerationControllerTest { 45 | 46 | private static final String URL = "/v1/authcode"; 47 | private static final String TEST_AUTHORIZATION_CODE = "123456789"; 48 | private static final String DUMMY_STR = "test"; 49 | private static final String DISPLAY_NAME_STR = "displayName"; 50 | private static final Jwt JWT = Jwt.withTokenValue(DUMMY_STR) 51 | .header(DUMMY_STR, null).claim(DUMMY_STR, null).build(); 52 | private static final Jwt JWT_WITH_CLAIM_DISPLAY_NAME = Jwt.withTokenValue(DUMMY_STR) 53 | .header(DUMMY_STR, null).claim(DISPLAY_NAME_STR, DUMMY_STR).build(); 54 | private static final String JEAP_AUTHORIZATION_LOG_MESSAGE = "Authenticated User is 'test'."; 55 | 56 | @Mock 57 | private ServletJeapAuthorization jeapAuthorization; 58 | @Mock 59 | private AuthCodeGenerationService authCodeGenerationService; 60 | @InjectMocks 61 | private AuthCodeGenerationController controller; 62 | private MockMvc mockMvc; 63 | private ObjectMapper mapper = new ObjectMapper(); 64 | 65 | @BeforeEach 66 | void setup() { 67 | this.mockMvc = standaloneSetup(controller).build(); 68 | SimpleModule module = new SimpleModule(); 69 | module.addSerializer(LocalDate.class, new LocalDateSerializer()); 70 | mapper.registerModule(module); 71 | } 72 | 73 | @Test 74 | void test_create() throws Exception { 75 | //given 76 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now()); 77 | AuthorizationCodeResponseDto responseDto = new AuthorizationCodeResponseDto(TEST_AUTHORIZATION_CODE); 78 | when(jeapAuthorization.getJeapAuthenticationToken()).thenReturn(new JeapAuthenticationToken(JWT, Collections.emptySet())); 79 | when(authCodeGenerationService.create(any(AuthorizationCodeCreateDto.class))).thenReturn(responseDto); 80 | 81 | //when 82 | MvcResult result = mockMvc.perform(post(URL) 83 | .accept(MediaType.APPLICATION_JSON_VALUE) 84 | .contentType(MediaType.APPLICATION_JSON_VALUE) 85 | .header("Authorization", DUMMY_STR) 86 | .content(mapper.writeValueAsString(createDto))) 87 | .andExpect(status().isOk()) 88 | .andReturn(); 89 | 90 | //then 91 | AuthorizationCodeResponseDto expectedDto = mapper.readValue(result.getResponse().getContentAsString(), AuthorizationCodeResponseDto.class); 92 | assertEquals(TEST_AUTHORIZATION_CODE, expectedDto.getAuthorizationCode()); 93 | } 94 | 95 | @Test 96 | void test_create_with_claim_display_name() throws Exception { 97 | //setup 98 | ListAppender loggingEventListAppender = LoggerTestUtil.getListAppenderForClass(AuthCodeGenerationController.class); 99 | //given 100 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now()); 101 | AuthorizationCodeResponseDto responseDto = new AuthorizationCodeResponseDto(TEST_AUTHORIZATION_CODE); 102 | when(jeapAuthorization.getJeapAuthenticationToken()).thenReturn(new JeapAuthenticationToken(JWT_WITH_CLAIM_DISPLAY_NAME, Collections.emptySet())); 103 | when(authCodeGenerationService.create(any(AuthorizationCodeCreateDto.class))).thenReturn(responseDto); 104 | 105 | //when 106 | MvcResult result = mockMvc.perform(post(URL) 107 | .accept(MediaType.APPLICATION_JSON_VALUE) 108 | .contentType(MediaType.APPLICATION_JSON_VALUE) 109 | .header("Authorization", DUMMY_STR) 110 | .content(mapper.writeValueAsString(createDto))) 111 | .andExpect(status().isOk()) 112 | .andReturn(); 113 | 114 | //then 115 | AuthorizationCodeResponseDto expectedDto = mapper.readValue(result.getResponse().getContentAsString(), AuthorizationCodeResponseDto.class); 116 | assertEquals(TEST_AUTHORIZATION_CODE, expectedDto.getAuthorizationCode()); 117 | assertThat(loggingEventListAppender.list).extracting(ILoggingEvent::getFormattedMessage, ILoggingEvent::getLevel) 118 | .contains(Tuple.tuple(JEAP_AUTHORIZATION_LOG_MESSAGE, Level.INFO)); 119 | } 120 | 121 | @Test 122 | void test_create_bad_request_exception() throws Exception { 123 | //given 124 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now().plusDays(1)); 125 | when(jeapAuthorization.getJeapAuthenticationToken()).thenReturn(new JeapAuthenticationToken(JWT, Collections.emptySet())); 126 | when(authCodeGenerationService.create(any(AuthorizationCodeCreateDto.class))).thenThrow(new ResponseStatusException(HttpStatus.BAD_REQUEST)); 127 | 128 | //when 129 | mockMvc.perform(post(URL) 130 | .accept(MediaType.APPLICATION_JSON_VALUE) 131 | .contentType(MediaType.APPLICATION_JSON_VALUE) 132 | .header("Authorization", DUMMY_STR) 133 | .content(mapper.writeValueAsString(createDto))) 134 | .andExpect(status().is(400)); 135 | } 136 | 137 | @Test 138 | void test_create_hin_access_denied() throws JsonProcessingException { 139 | //given 140 | Jwt jwt = Jwt.withTokenValue(DUMMY_STR).header(DUMMY_STR, null).claim("homeName", "E-ID CH-LOGIN").claim("unitName", "HIN").build(); 141 | AuthorizationCodeCreateDto createDto = new AuthorizationCodeCreateDto(LocalDate.now().plusDays(1)); 142 | when(jeapAuthorization.getJeapAuthenticationToken()).thenReturn(new JeapAuthenticationToken(jwt, Collections.emptySet())); 143 | 144 | final String request = mapper.writeValueAsString(createDto); 145 | 146 | //when 147 | try { 148 | mockMvc.perform(post(URL) 149 | .accept(MediaType.APPLICATION_JSON_VALUE) 150 | .contentType(MediaType.APPLICATION_JSON_VALUE) 151 | .header("Authorization", DUMMY_STR) 152 | .content(request)); 153 | Assert.fail(); 154 | } catch (Exception e) { 155 | Assert.assertTrue(e.getCause() instanceof AccessDeniedException); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeVerificationControllerSecurityTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerificationDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.config.security.OAuth2SecuredWebConfiguration; 6 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeVerificationService; 7 | import ch.admin.bag.covidcode.authcodegeneration.testutil.LocalDateSerializer; 8 | import ch.admin.bag.covidcode.authcodegeneration.web.security.WebSecurityConfig; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import com.fasterxml.jackson.databind.module.SimpleModule; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 15 | import org.springframework.boot.test.mock.mockito.MockBean; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.test.context.ActiveProfiles; 18 | import org.springframework.test.web.servlet.MockMvc; 19 | 20 | import java.time.LocalDate; 21 | 22 | import static org.mockito.ArgumentMatchers.anyString; 23 | import static org.mockito.Mockito.*; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 26 | 27 | @WebMvcTest(value = {AuthCodeVerificationController.class, OAuth2SecuredWebConfiguration.class, WebSecurityConfig.class}) 28 | @ActiveProfiles("local") 29 | class AuthCodeVerificationControllerSecurityTest { 30 | 31 | private static final String URL = "/v1/onset"; 32 | private static final String TEST_AUTHORIZATION_CODE = "123456789"; 33 | private static final String FAKE_NOT_FAKE = "0"; 34 | 35 | @Autowired 36 | private MockMvc mockMvc; 37 | 38 | @MockBean 39 | private AuthCodeVerificationService service; 40 | 41 | private static final ObjectMapper MAPPER = new ObjectMapper(); 42 | 43 | @BeforeAll 44 | static void setup() { 45 | SimpleModule module = new SimpleModule(); 46 | module.addSerializer(LocalDate.class, new LocalDateSerializer()); 47 | MAPPER.registerModule(module); 48 | } 49 | 50 | @Test 51 | void test_verify_authorization_without_token_is_permitted() throws Exception { 52 | when(service.verify(anyString(), anyString())).thenReturn(new AuthorizationCodeVerifyResponseDto("token")); 53 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 54 | mockMvc.perform(post(URL) 55 | .accept(MediaType.APPLICATION_JSON_VALUE) 56 | .contentType(MediaType.APPLICATION_JSON_VALUE) 57 | .content(MAPPER.writeValueAsString(verificationDto))) 58 | .andExpect(status().isOk()); 59 | 60 | verify(service, times(1)).verify(anyString(), anyString()); 61 | } 62 | 63 | @Test 64 | void test_verify_authorization_without_token_is_permitted_return_404() throws Exception { 65 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 66 | mockMvc.perform(post(URL) 67 | .accept(MediaType.APPLICATION_JSON_VALUE) 68 | .contentType(MediaType.APPLICATION_JSON_VALUE) 69 | .content(MAPPER.writeValueAsString(verificationDto))) 70 | .andExpect(status().is(404)); 71 | 72 | verify(service, times(1)).verify(anyString(), anyString()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeVerificationControllerTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerificationDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeVerificationService; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import org.springframework.data.rest.webmvc.ResourceNotFoundException; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | import org.springframework.test.web.servlet.MvcResult; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | import static org.mockito.ArgumentMatchers.anyString; 20 | import static org.mockito.Mockito.when; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 23 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; 24 | 25 | @ExtendWith(MockitoExtension.class) 26 | class AuthCodeVerificationControllerTest { 27 | 28 | private static final String URL = "/v1/onset"; 29 | private static final String TEST_AUTHORIZATION_CODE = "123456789"; 30 | private static final String DUMMY_STR = "test"; 31 | private static final String FAKE_NOT_FAKE = "0"; 32 | 33 | @Mock 34 | private AuthCodeVerificationService authCodeVerificationService; 35 | 36 | private MockMvc mockMvc; 37 | private ObjectMapper mapper = new ObjectMapper(); 38 | 39 | @BeforeEach 40 | void setup() { 41 | this.mockMvc = standaloneSetup(new AuthCodeVerificationController(authCodeVerificationService, 0)).build(); 42 | } 43 | 44 | @Test 45 | void test_verify() throws Exception { 46 | //given 47 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 48 | AuthorizationCodeVerifyResponseDto responseDto = new AuthorizationCodeVerifyResponseDto(DUMMY_STR); 49 | 50 | when(authCodeVerificationService.verify(anyString(), anyString())).thenReturn(responseDto); 51 | 52 | //when 53 | MvcResult result = mockMvc.perform(post(URL) 54 | .accept(MediaType.APPLICATION_JSON_VALUE) 55 | .contentType(MediaType.APPLICATION_JSON_VALUE) 56 | .header("Authorization", DUMMY_STR) 57 | .content(mapper.writeValueAsString(verificationDto))) 58 | .andExpect(status().isOk()) 59 | .andReturn(); 60 | 61 | //then 62 | AuthorizationCodeVerifyResponseDto expectedDto = mapper.readValue(result.getResponse().getContentAsString(), AuthorizationCodeVerifyResponseDto.class); 63 | assertEquals(DUMMY_STR, expectedDto.getAccessToken()); 64 | } 65 | 66 | @Test 67 | void test_verify_not_found_exception() throws Exception { 68 | //given 69 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 70 | 71 | when(authCodeVerificationService.verify(anyString(), anyString())).thenThrow(new ResourceNotFoundException()); 72 | 73 | //when 74 | mockMvc.perform(post(URL) 75 | .accept(MediaType.APPLICATION_JSON_VALUE) 76 | .contentType(MediaType.APPLICATION_JSON_VALUE) 77 | .header("Authorization", DUMMY_STR) 78 | .content(mapper.writeValueAsString(verificationDto))) 79 | .andExpect(status().is(404)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeVerificationControllerV2SecurityTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import static org.mockito.ArgumentMatchers.anyBoolean; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.times; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 10 | 11 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerificationDto; 12 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto; 13 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDtoWrapper; 14 | import ch.admin.bag.covidcode.authcodegeneration.config.security.OAuth2SecuredWebConfiguration; 15 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeVerificationService; 16 | import ch.admin.bag.covidcode.authcodegeneration.testutil.LocalDateSerializer; 17 | import ch.admin.bag.covidcode.authcodegeneration.web.security.WebSecurityConfig; 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import com.fasterxml.jackson.databind.module.SimpleModule; 20 | import java.time.LocalDate; 21 | import org.junit.jupiter.api.BeforeAll; 22 | import org.junit.jupiter.api.Test; 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 25 | import org.springframework.boot.test.mock.mockito.MockBean; 26 | import org.springframework.http.MediaType; 27 | import org.springframework.test.context.ActiveProfiles; 28 | import org.springframework.test.web.servlet.MockMvc; 29 | 30 | @WebMvcTest(value = {AuthCodeVerificationControllerV2.class, OAuth2SecuredWebConfiguration.class, WebSecurityConfig.class}) 31 | @ActiveProfiles("local") 32 | class AuthCodeVerificationControllerV2SecurityTest { 33 | 34 | private static final String URL = "/v2/onset"; 35 | private static final String TEST_AUTHORIZATION_CODE = "123456789"; 36 | private static final String FAKE = "0"; 37 | private static final String DUMMY_FOO = "foo"; 38 | private static final String DUMMY_BAR = "bar"; 39 | 40 | @Autowired 41 | private MockMvc mockMvc; 42 | 43 | @MockBean 44 | private AuthCodeVerificationService service; 45 | 46 | private static final ObjectMapper MAPPER = new ObjectMapper(); 47 | 48 | @BeforeAll 49 | static void setup() { 50 | SimpleModule module = new SimpleModule(); 51 | module.addSerializer(LocalDate.class, new LocalDateSerializer()); 52 | MAPPER.registerModule(module); 53 | } 54 | 55 | @Test 56 | void test_verify_authorization_without_token_is_permitted() throws Exception { 57 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE); 58 | AuthorizationCodeVerifyResponseDto dp3tResponseDto = new AuthorizationCodeVerifyResponseDto(DUMMY_FOO); 59 | AuthorizationCodeVerifyResponseDto checkInResponseDto = new AuthorizationCodeVerifyResponseDto(DUMMY_BAR); 60 | final var expectedWrapper = new AuthorizationCodeVerifyResponseDtoWrapper(dp3tResponseDto, checkInResponseDto); 61 | when(service.verify(anyString(), anyString(), anyBoolean())).thenReturn(expectedWrapper); 62 | 63 | mockMvc.perform(post(URL) 64 | .accept(MediaType.APPLICATION_JSON_VALUE) 65 | .contentType(MediaType.APPLICATION_JSON_VALUE) 66 | .content(MAPPER.writeValueAsString(verificationDto))) 67 | .andExpect(status().isOk()); 68 | 69 | verify(service, times(1)).verify(anyString(), anyString(), anyBoolean()); 70 | } 71 | 72 | @Test 73 | void test_verify_authorization_without_token_is_permitted_return_404() throws Exception { 74 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE); 75 | mockMvc.perform(post(URL) 76 | .accept(MediaType.APPLICATION_JSON_VALUE) 77 | .contentType(MediaType.APPLICATION_JSON_VALUE) 78 | .content(MAPPER.writeValueAsString(verificationDto))) 79 | .andExpect(status().is(404)); 80 | 81 | verify(service, times(1)).verify(anyString(), anyString(), anyBoolean()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/web/controller/AuthCodeVerificationControllerV2Test.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.controller; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeOnsetResponseDto; 4 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerificationDto; 5 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDto; 6 | import ch.admin.bag.covidcode.authcodegeneration.api.AuthorizationCodeVerifyResponseDtoWrapper; 7 | import ch.admin.bag.covidcode.authcodegeneration.service.AuthCodeVerificationService; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | import org.springframework.test.web.servlet.MvcResult; 17 | 18 | import java.util.Arrays; 19 | 20 | import static org.junit.jupiter.api.Assertions.*; 21 | import static org.mockito.ArgumentMatchers.*; 22 | import static org.mockito.Mockito.lenient; 23 | import static org.mockito.Mockito.when; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 26 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; 27 | 28 | @ExtendWith(MockitoExtension.class) 29 | class AuthCodeVerificationControllerV2Test { 30 | 31 | private static final String URL = "/v2/onset"; 32 | private static final String URL_ONSET = "/v2/onset/date"; 33 | private static final String TEST_AUTHORIZATION_CODE = "123456789"; 34 | private static final String TEST_ONSET_DATE = "1970-01-01"; 35 | private static final String DUMMY_FOO = "foo"; 36 | private static final String DUMMY_BAR = "bar"; 37 | private static final String FAKE_NOT_FAKE = "0"; 38 | 39 | @Mock 40 | private AuthCodeVerificationService authCodeVerificationService; 41 | 42 | private MockMvc mockMvc; 43 | private ObjectMapper mapper = new ObjectMapper(); 44 | 45 | @BeforeEach 46 | void setup() { 47 | this.mockMvc = standaloneSetup(new AuthCodeVerificationControllerV2(authCodeVerificationService, 0)).build(); 48 | } 49 | 50 | @Test 51 | void test_verify() throws Exception { 52 | //given 53 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 54 | AuthorizationCodeVerifyResponseDto dp3tResponseDto = new AuthorizationCodeVerifyResponseDto(DUMMY_FOO); 55 | AuthorizationCodeVerifyResponseDto checkInResponseDto = new AuthorizationCodeVerifyResponseDto(DUMMY_BAR); 56 | final var expectedWrapper = new AuthorizationCodeVerifyResponseDtoWrapper(dp3tResponseDto, checkInResponseDto); 57 | when(authCodeVerificationService.verify(anyString(), anyString(), anyBoolean())).thenReturn(expectedWrapper); 58 | 59 | //when 60 | MvcResult result = mockMvc.perform(post(URL) 61 | .accept(MediaType.APPLICATION_JSON_VALUE) 62 | .contentType(MediaType.APPLICATION_JSON_VALUE) 63 | .header("Authorization", TEST_AUTHORIZATION_CODE) 64 | .content(mapper.writeValueAsString(verificationDto))) 65 | .andExpect(status().isOk()) 66 | .andReturn(); 67 | 68 | //then 69 | final var actualWrapper = mapper.readValue(result.getResponse().getContentAsString(), AuthorizationCodeVerifyResponseDtoWrapper.class); 70 | final var dp3tAccessToken = actualWrapper.getDP3TAccessToken(); 71 | final var checkInAccessToken = actualWrapper.getCheckInAccessToken(); 72 | assertNotNull(dp3tAccessToken, "Should return exactly two tokens for swissCovid and notifyMe backend"); 73 | assertNotNull(checkInAccessToken, "Should return exactly two tokens for swissCovid and notifyMe backend"); 74 | assertEquals(DUMMY_FOO, dp3tAccessToken.getAccessToken()); 75 | assertEquals(DUMMY_BAR, checkInAccessToken.getAccessToken()); 76 | } 77 | 78 | @Test 79 | void test_verify_not_found_exception() throws Exception { 80 | //given 81 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 82 | 83 | lenient().when(authCodeVerificationService.verify(anyString(), anyString(), anyBoolean())).thenReturn(new AuthorizationCodeVerifyResponseDtoWrapper()); 84 | 85 | //when 86 | mockMvc.perform(post(URL) 87 | .accept(MediaType.APPLICATION_JSON_VALUE) 88 | .contentType(MediaType.APPLICATION_JSON_VALUE) 89 | .header("Authorization", TEST_AUTHORIZATION_CODE) 90 | .content(mapper.writeValueAsString(verificationDto))) 91 | .andExpect(status().is(404)); 92 | } 93 | 94 | @Test 95 | void test_getOnset() throws Exception { 96 | //given 97 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 98 | final var expectedResponse = new AuthorizationCodeOnsetResponseDto(TEST_ONSET_DATE); 99 | when(authCodeVerificationService.getOnsetForAuthCode(anyString(), anyString())).thenReturn(expectedResponse); 100 | 101 | //when 102 | MvcResult result = mockMvc.perform(post(URL_ONSET) 103 | .accept(MediaType.APPLICATION_JSON_VALUE) 104 | .contentType(MediaType.APPLICATION_JSON_VALUE) 105 | .header("Authorization", TEST_AUTHORIZATION_CODE) 106 | .content(mapper.writeValueAsString(verificationDto))) 107 | .andExpect(status().isOk()) 108 | .andReturn(); 109 | 110 | //then 111 | final var actualResponse = mapper.readValue(result.getResponse().getContentAsString(), AuthorizationCodeOnsetResponseDto.class); 112 | final var onset = actualResponse.getOnset(); 113 | assertNotNull(onset, "Should return a non-null onset date"); 114 | assertEquals(TEST_ONSET_DATE, onset); 115 | } 116 | 117 | @Test 118 | void test_getOnset_not_found_exception() throws Exception { 119 | //given 120 | AuthorizationCodeVerificationDto verificationDto = new AuthorizationCodeVerificationDto(TEST_AUTHORIZATION_CODE, FAKE_NOT_FAKE); 121 | 122 | when(authCodeVerificationService.getOnsetForAuthCode(anyString(), anyString())).thenReturn(new AuthorizationCodeOnsetResponseDto(null)); 123 | 124 | //when 125 | mockMvc.perform(post(URL_ONSET) 126 | .accept(MediaType.APPLICATION_JSON_VALUE) 127 | .contentType(MediaType.APPLICATION_JSON_VALUE) 128 | .header("Authorization", TEST_AUTHORIZATION_CODE) 129 | .content(mapper.writeValueAsString(verificationDto))) 130 | .andExpect(status().is(404)); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/ch/admin/bag/covidcode/authcodegeneration/web/monitoring/AuthorizationCodeCountMeterTest.java: -------------------------------------------------------------------------------- 1 | package ch.admin.bag.covidcode.authcodegeneration.web.monitoring; 2 | 3 | import ch.admin.bag.covidcode.authcodegeneration.domain.AuthorizationCodeRepository; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | class AuthorizationCodeCountMeterTest { 14 | 15 | @Mock 16 | private AuthorizationCodeRepository authorizationCodeRepository; 17 | 18 | @Mock 19 | private MeterRegistry meterRegistry; 20 | 21 | @Test 22 | void bindTo_meterRegistry_ok() { 23 | AuthorizationCodeCountMeter authorizationCodeCountMeter = new AuthorizationCodeCountMeter(authorizationCodeRepository); 24 | authorizationCodeCountMeter.bindTo(meterRegistry); 25 | assertNotNull(authorizationCodeCountMeter); 26 | } 27 | } 28 | --------------------------------------------------------------------------------