diff --git a/wls-common/security/pom.xml b/wls-common/security/pom.xml
index 0acd8a599..15316914f 100644
--- a/wls-common/security/pom.xml
+++ b/wls-common/security/pom.xml
@@ -29,6 +29,14 @@
org.springframework.security
spring-security-oauth2-client
+
+ org.springframework.security
+ spring-security-oauth2-resource-server
+
+
+ org.springframework.security
+ spring-security-oauth2-jose
+
org.springframework.boot
spring-boot-starter-security
diff --git a/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/Profiles.java b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/Profiles.java
index 834996c62..0075004e1 100644
--- a/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/Profiles.java
+++ b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/Profiles.java
@@ -3,4 +3,6 @@
public interface Profiles {
// Test Profiles
String NO_BEZIRKS_ID_CHECK = "dummy.nobezirkid.check";
+
+ String NO_SECURITY = "no-security";
}
diff --git a/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AnonymousDetailRetriever.java b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AnonymousDetailRetriever.java
new file mode 100644
index 000000000..8b29931fd
--- /dev/null
+++ b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AnonymousDetailRetriever.java
@@ -0,0 +1,27 @@
+package de.muenchen.oss.wahllokalsystem.wls.common.security.authentication;
+
+import de.muenchen.oss.wahllokalsystem.wls.common.security.Profiles;
+import java.util.Optional;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+@Component
+@Profile(Profiles.NO_SECURITY)
+public class AnonymousDetailRetriever implements AuthDetailRetriever {
+
+ @Override
+ public boolean canHandle(Authentication authentication) {
+ Assert.notNull(authentication, "Authentication must not be null");
+ return authentication instanceof AnonymousAuthenticationToken;
+ }
+
+ @Override
+ public Optional getDetail(String detailKey, Authentication authentication) {
+ Assert.notNull(authentication, "Authentication must not be null");
+ Assert.notNull(detailKey, "detailKey must not be null");
+ return Optional.empty();
+ }
+}
diff --git a/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AuthDetailRetriever.java b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AuthDetailRetriever.java
new file mode 100644
index 000000000..e79c134c1
--- /dev/null
+++ b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AuthDetailRetriever.java
@@ -0,0 +1,17 @@
+package de.muenchen.oss.wahllokalsystem.wls.common.security.authentication;
+
+import java.util.Optional;
+import org.springframework.security.core.Authentication;
+
+public interface AuthDetailRetriever {
+
+ /**
+ * @throws IllegalArgumentException when authentication is null
+ */
+ boolean canHandle(Authentication authentication);
+
+ /**
+ * @throws IllegalArgumentException when any parameter is null
+ */
+ Optional getDetail(String detailKey, Authentication authentication);
+}
diff --git a/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/JWTDetailRetriever.java b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/JWTDetailRetriever.java
new file mode 100644
index 000000000..48b539346
--- /dev/null
+++ b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/JWTDetailRetriever.java
@@ -0,0 +1,27 @@
+package de.muenchen.oss.wahllokalsystem.wls.common.security.authentication;
+
+import java.util.Optional;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+@Component
+public class JWTDetailRetriever implements AuthDetailRetriever {
+
+ @Override
+ public boolean canHandle(final Authentication authentication) {
+ Assert.notNull(authentication, "authentication must not be null");
+ return authentication instanceof JwtAuthenticationToken;
+ }
+
+ public Optional getDetail(final String detailKey, final Authentication authentication) {
+ Assert.notNull(authentication, "authentication must not be null");
+ Assert.notNull(detailKey, "detailKey must not be null");
+ if (authentication instanceof JwtAuthenticationToken jwtToken) {
+ return Optional.ofNullable(jwtToken.getToken().getClaimAsString(detailKey));
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/package-info.java b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/package-info.java
new file mode 100644
index 000000000..e8561a3a5
--- /dev/null
+++ b/wls-common/security/src/main/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/package-info.java
@@ -0,0 +1,4 @@
+@NonNullApi
+package de.muenchen.oss.wahllokalsystem.wls.common.security.authentication;
+
+import org.springframework.lang.NonNullApi;
diff --git a/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/ProfilesTest.java b/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/ProfilesTest.java
index f5032ed9c..4ce1b8de1 100644
--- a/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/ProfilesTest.java
+++ b/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/ProfilesTest.java
@@ -1,5 +1,9 @@
package de.muenchen.oss.wahllokalsystem.wls.common.security;
+import de.muenchen.oss.wahllokalsystem.wls.common.security.authentication.AnonymousDetailRetriever;
+import de.muenchen.oss.wahllokalsystem.wls.common.security.authentication.AuthDetailRetriever;
+import de.muenchen.oss.wahllokalsystem.wls.common.security.authentication.JWTDetailRetriever;
+import java.util.Collection;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -38,10 +42,38 @@ class NoSpecialProfile {
@Autowired
private OAuth2TokenInterceptor oAuth2TokenInterceptor;
+ @Autowired
+ private Collection authDetailRetrievers;
+
@Test
void should_haveImplementationWithChecksInContext_when_noAdditionalProfilesAreActive() {
Assertions.assertThat(permissionEvaluator).isExactlyInstanceOf(BezirkIDPermissionEvaluatorImpl.class);
}
+
+ @Test
+ void should_findOnlyJwtHandlerAsAuthenticationHandler_when_contextIsInitalized() {
+ Assertions.assertThat(authDetailRetrievers).hasSize(1);
+ Assertions.assertThat(authDetailRetrievers).allMatch(handler -> handler instanceof JWTDetailRetriever);
+ }
+ }
+
+ @SpringBootTest(
+ properties = { "app.crypto.key = 770A8A65DA156D24EE2A093277530142", "service.info.oid=My app name" }
+ )
+ @ActiveProfiles(Profiles.NO_SECURITY)
+ @Nested
+ class NoSecurityProfile {
+
+ @Autowired
+ private Collection authDetailRetrievers;
+
+ @Test
+ void should_findJwtAndAnonymousHandler_when_contextIsInitialized() {
+ Assertions.assertThat(authDetailRetrievers).hasSize(2);
+ Assertions.assertThat(
+ authDetailRetrievers).allMatch(handler -> handler instanceof JWTDetailRetriever || handler instanceof AnonymousDetailRetriever);
+ }
+
}
@SpringBootApplication(
diff --git a/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AnonymousDetailRetrieverTest.java b/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AnonymousDetailRetrieverTest.java
new file mode 100644
index 000000000..8fcef46c6
--- /dev/null
+++ b/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/AnonymousDetailRetrieverTest.java
@@ -0,0 +1,77 @@
+package de.muenchen.oss.wahllokalsystem.wls.common.security.authentication;
+
+import java.util.List;
+import lombok.val;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+class AnonymousDetailRetrieverTest {
+
+ private final AnonymousDetailRetriever unitUnderTest = new AnonymousDetailRetriever();
+
+ @Nested
+ class CanHandle {
+
+ @Test
+ void should_throwIllegalArgumentException_when_authenticationIsNull() {
+ Assertions.assertThatThrownBy(() -> unitUnderTest.canHandle(null)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void should_returnTrue_when_authenticationIsAnonymousAuthenticationToken() {
+ Assertions.assertThat(unitUnderTest.canHandle(new AnonymousAuthenticationToken("key", "principal", List.of(new SimpleGrantedAuthority("role")))))
+ .isTrue();
+ }
+
+ @Test
+ void should_returnTrue_when_authenticationSubclassOfJwtAuthenticationToken() {
+ Assertions.assertThat(unitUnderTest.canHandle(new AnonymousAuthenticationToken("key", "principal", List.of(new SimpleGrantedAuthority("role"))) {
+ })).isTrue();
+ }
+
+ @Test
+ void should_returnFalse_when_authenticationIsNotJwtAuthenticationToken() {
+ Assertions.assertThat(unitUnderTest.canHandle(new AbstractAuthenticationToken(List.of(new SimpleGrantedAuthority("role"))) {
+ @Override
+ public Object getCredentials() {
+ return null;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return null;
+ }
+ })).isFalse();
+ }
+ }
+
+ @Nested
+ class GetDetail {
+
+ @Test
+ void should_returnEmptyOptional_when_called() {
+ val authentication = new AnonymousAuthenticationToken("key", "principal", List.of(new SimpleGrantedAuthority("role")));
+
+ val result = unitUnderTest.getDetail("key", authentication);
+
+ Assertions.assertThat(result).isEmpty();
+ }
+
+ @Test
+ void should_throwIllegalArgumentException_when_authenticationIsNull() {
+ Assertions.assertThatThrownBy(() -> unitUnderTest.getDetail("key", null)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void should_throwIllegalArgumentException_when_detailKeyIsNull() {
+ Assertions.assertThatThrownBy(
+ () -> unitUnderTest.getDetail(null, new AnonymousAuthenticationToken("key", "principal", List.of(new SimpleGrantedAuthority("role")))))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+ }
+
+}
diff --git a/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/JWTDetailRetrieverTest.java b/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/JWTDetailRetrieverTest.java
new file mode 100644
index 000000000..fe708da32
--- /dev/null
+++ b/wls-common/security/src/test/java/de/muenchen/oss/wahllokalsystem/wls/common/security/authentication/JWTDetailRetrieverTest.java
@@ -0,0 +1,118 @@
+package de.muenchen.oss.wahllokalsystem.wls.common.security.authentication;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import lombok.val;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+
+class JWTDetailRetrieverTest {
+
+ private final JWTDetailRetriever unitUnderTest = new JWTDetailRetriever();
+
+ @Nested
+ class CanHandle {
+
+ @Test
+ void should_throwIllegalArgumentException_when_authenticationIsNull() {
+ Assertions.assertThatThrownBy(() -> unitUnderTest.canHandle(null)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void should_returnTrue_when_authenticationIsJwtAuthenticationToken() {
+ val detailKey = "requestedKey";
+ val detailValue = "detailValue";
+ val jwt = createJWT(Map.of(detailKey, detailValue));
+
+ Assertions.assertThat(unitUnderTest.canHandle(new JwtAuthenticationToken(jwt))).isTrue();
+ }
+
+ @Test
+ void should_returnTrue_when_authenticationSubclassOfJwtAuthenticationToken() {
+ val detailKey = "requestedKey";
+ val detailValue = "detailValue";
+ val jwt = createJWT(Map.of(detailKey, detailValue));
+
+ Assertions.assertThat(unitUnderTest.canHandle(new JwtAuthenticationToken(jwt) {
+ })).isTrue();
+ }
+
+ @Test
+ void should_returnFalse_when_authenticationIsNotJwtAuthenticationToken() {
+ Assertions.assertThat(unitUnderTest.canHandle(new AbstractAuthenticationToken(Collections.emptyList()) {
+ @Override
+ public Object getCredentials() {
+ return null;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return null;
+ }
+ })).isFalse();
+ }
+ }
+
+ @Nested
+ class GetDetail {
+
+ @Test
+ void should_returnEmptyOptional_when_authenticationIsNotInstanceOfJwtAuthenticationToken() {
+ val result = unitUnderTest.getDetail("key", new UsernamePasswordAuthenticationToken("principal", "credentials"));
+
+ Assertions.assertThat(result).isEmpty();
+ }
+
+ @Test
+ void should_returnValues_when_claimWithKeyExists() {
+ val detailKey = "requestedKey";
+ val detailValue = "detailValue";
+
+ val jwt = createJWT(Map.of(detailKey, detailValue));
+
+ val expectedResult = Optional.of(detailValue);
+
+ val result = unitUnderTest.getDetail(detailKey, new JwtAuthenticationToken(jwt));
+
+ Assertions.assertThat(result).isEqualTo(expectedResult);
+ }
+
+ @Test
+ void should_returnValues_when_claimWithKeyDoesNotExists() {
+ val detailKey = "requestedKey";
+
+ val jwt = createJWT(Map.of(detailKey + "extra", detailKey));
+
+ val result = unitUnderTest.getDetail(detailKey, new JwtAuthenticationToken(jwt));
+
+ Assertions.assertThat(result).isEmpty();
+ }
+
+ @Test
+ void should_throwsIllegalArgumentException_when_keyIsNull() {
+ val detailValue = "detailValue";
+
+ val jwt = createJWT(Map.of("detailKey", detailValue));
+
+ Assertions.assertThatThrownBy(() -> unitUnderTest.getDetail(null, new JwtAuthenticationToken(jwt))).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void should_throwsIllegalArgumentException_when_authenticationIsNull() {
+ Assertions.assertThatThrownBy(() -> unitUnderTest.getDetail("key", null)).isInstanceOf(IllegalArgumentException.class);
+ }
+ }
+
+ private Jwt createJWT(final Map claims) {
+ return new Jwt("tokenValue", Instant.now().minus(1, ChronoUnit.HOURS), Instant.now().plus(1, ChronoUnit.HOURS), Map.of("key1", "value1"), claims);
+ }
+
+}
diff --git a/wls-gui-wahllokalsystem/package-lock.json b/wls-gui-wahllokalsystem/package-lock.json
index 9ce82904e..5e9242828 100644
--- a/wls-gui-wahllokalsystem/package-lock.json
+++ b/wls-gui-wahllokalsystem/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@vueuse/core": "11.2.0",
- "pinia": "2.2.6",
+ "pinia": "2.3.0",
"roboto-fontface": "0.10.0",
"vue": "3.5.13",
"vue-router": "4.4.5",
@@ -1709,31 +1709,6 @@
"url": "https://github.com/sponsors/antfu"
}
},
- "node_modules/@vueuse/core/node_modules/vue-demi": {
- "version": "0.14.10",
- "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "vue-demi-fix": "bin/vue-demi-fix.js",
- "vue-demi-switch": "bin/vue-demi-switch.js"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- },
- "peerDependencies": {
- "@vue/composition-api": "^1.0.0-rc.1",
- "vue": "^3.0.0-0 || ^2.6.0"
- },
- "peerDependenciesMeta": {
- "@vue/composition-api": {
- "optional": true
- }
- }
- },
"node_modules/@vueuse/metadata": {
"version": "11.2.0",
"integrity": "sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==",
@@ -1753,31 +1728,6 @@
"url": "https://github.com/sponsors/antfu"
}
},
- "node_modules/@vueuse/shared/node_modules/vue-demi": {
- "version": "0.14.10",
- "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "vue-demi-fix": "bin/vue-demi-fix.js",
- "vue-demi-switch": "bin/vue-demi-switch.js"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- },
- "peerDependencies": {
- "@vue/composition-api": "^1.0.0-rc.1",
- "vue": "^3.0.0-0 || ^2.6.0"
- },
- "peerDependenciesMeta": {
- "@vue/composition-api": {
- "optional": true
- }
- }
- },
"node_modules/abbrev": {
"version": "2.0.0",
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
@@ -3409,8 +3359,8 @@
}
},
"node_modules/pinia": {
- "version": "2.2.6",
- "integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==",
+ "version": "2.3.0",
+ "integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
@@ -3420,44 +3370,15 @@
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
- "@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
- "vue": "^2.6.14 || ^3.5.11"
+ "vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
- "@vue/composition-api": {
- "optional": true
- },
"typescript": {
"optional": true
}
}
},
- "node_modules/pinia/node_modules/vue-demi": {
- "version": "0.14.10",
- "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "vue-demi-fix": "bin/vue-demi-fix.js",
- "vue-demi-switch": "bin/vue-demi-switch.js"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- },
- "peerDependencies": {
- "@vue/composition-api": "^1.0.0-rc.1",
- "vue": "^3.0.0-0 || ^2.6.0"
- },
- "peerDependenciesMeta": {
- "@vue/composition-api": {
- "optional": true
- }
- }
- },
"node_modules/postcss": {
"version": "8.4.49",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
@@ -4338,6 +4259,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/vue-demi": {
+ "version": "0.14.10",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vue-eslint-parser": {
"version": "9.4.3",
"integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
diff --git a/wls-gui-wahllokalsystem/package.json b/wls-gui-wahllokalsystem/package.json
index 9f008d393..38129a06f 100644
--- a/wls-gui-wahllokalsystem/package.json
+++ b/wls-gui-wahllokalsystem/package.json
@@ -16,7 +16,7 @@
},
"dependencies": {
"@vueuse/core": "11.2.0",
- "pinia": "2.2.6",
+ "pinia": "2.3.0",
"roboto-fontface": "0.10.0",
"vue": "3.5.13",
"vue-router": "4.4.5",