├── .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