From 267ff63b2a02b13b4af8383f4be854549d49c42d Mon Sep 17 00:00:00 2001 From: Dirk Van Haerenborgh Date: Thu, 18 Feb 2021 10:26:23 +0100 Subject: [PATCH] Add support for arbitrary claims for authorization subjects Fixes #512 Signed-off-by: Dirk Van Haerenborgh read config from file Signed-off-by: Dirk Van Haerenborgh fix test Signed-off-by: Dirk Van Haerenborgh add getter function Signed-off-by: Dirk Van Haerenborgh rename Signed-off-by: Dirk Van Haerenborgh wire up templates Signed-off-by: Dirk Van Haerenborgh more test Signed-off-by: Dirk Van Haerenborgh fix license header Signed-off-by: Dirk Van Haerenborgh add unit test for subjectsprovider Signed-off-by: Dirk Van Haerenborgh Update services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultSubjectIssuerConfig.java Co-authored-by: Thomas Jaeckle Signed-off-by: Dirk Van Haerenborgh Update services/gateway/util/src/test/resources/oauth-test.conf Co-authored-by: Thomas Jaeckle Signed-off-by: Dirk Van Haerenborgh Update services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultSubjectIssuerConfig.java Co-authored-by: Thomas Jaeckle Signed-off-by: Dirk Van Haerenborgh Update services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfig.java Co-authored-by: Thomas Jaeckle Signed-off-by: Dirk Van Haerenborgh Update services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuerConfig.java Co-authored-by: Thomas Jaeckle Signed-off-by: Dirk Van Haerenborgh Update services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProvider.java Co-authored-by: Thomas Jaeckle Signed-off-by: Dirk Van Haerenborgh move SubjectIssuerConfig Signed-off-by: Dirk Van Haerenborgh fix devops-test config resource Signed-off-by: Dirk Van Haerenborgh fix Config reading Signed-off-by: Dirk Van Haerenborgh allow json structures in jwt Signed-off-by: Dirk Van Haerenborgh Update services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/SubjectIssuerConfig.java Co-authored-by: Thomas Jaeckle Signed-off-by: Dirk Van Haerenborgh add test for json in jwt Signed-off-by: Dirk Van Haerenborgh move config parsing into SubjectIssuerConfig interface Signed-off-by: Dirk Van Haerenborgh correctly use config fallback values Signed-off-by: Dirk Van Haerenborgh fix config syntax Signed-off-by: Dirk Van Haerenborgh --- ...OAuthTokenIntegrationSubjectIdFactory.java | 2 +- .../TokenIntegrationSubjectIdFactory.java | 43 ------ ...DittoJwtAuthorizationSubjectsProvider.java | 16 +- .../authentication/jwt/JwtPlaceholder.java | 41 ++++++ .../jwt/JwtSubjectIssuerConfig.java | 46 +++++- .../jwt/JwtSubjectIssuersConfig.java | 2 +- ...oJwtAuthorizationSubjectsProviderTest.java | 137 ++++++++++++++++++ .../jwt/DittoPublicKeyProviderTest.java | 2 +- .../jwt/JwtSubjectIssuersConfigTest.java | 21 ++- .../src/test/resources/oauth-test.conf | 16 +- .../config/security/DefaultOAuthConfig.java | 28 ++-- .../security/DefaultSubjectIssuerConfig.java | 104 +++++++++++++ .../util/config/security/OAuthConfig.java | 4 +- .../config/security/SubjectIssuerConfig.java | 65 +++++++++ .../security/DefaultOAuthConfigTest.java | 19 ++- .../util/src/test/resources/devops-test.conf | 4 +- .../util/src/test/resources/oauth-test.conf | 18 ++- 17 files changed, 481 insertions(+), 87 deletions(-) create mode 100644 services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProviderTest.java create mode 100644 services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultSubjectIssuerConfig.java create mode 100644 services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/SubjectIssuerConfig.java diff --git a/services/gateway/endpoints/src/main/java/org/eclipse/ditto/services/gateway/endpoints/routes/policies/OAuthTokenIntegrationSubjectIdFactory.java b/services/gateway/endpoints/src/main/java/org/eclipse/ditto/services/gateway/endpoints/routes/policies/OAuthTokenIntegrationSubjectIdFactory.java index 1a6e1505f4..0ec3168b1e 100755 --- a/services/gateway/endpoints/src/main/java/org/eclipse/ditto/services/gateway/endpoints/routes/policies/OAuthTokenIntegrationSubjectIdFactory.java +++ b/services/gateway/endpoints/src/main/java/org/eclipse/ditto/services/gateway/endpoints/routes/policies/OAuthTokenIntegrationSubjectIdFactory.java @@ -52,7 +52,7 @@ public Set getSubjectIds(final DittoHeaders dittoHeaders, final JsonW PlaceholderFactory.newPlaceholderResolver(JwtPlaceholder.getInstance(), jwt) ); final String issuerWithSubject = expressionResolver.resolvePartially(subjectTemplate); - return TokenIntegrationSubjectIdFactory.expandJsonArraysInResolvedSubject(issuerWithSubject) + return JwtPlaceholder.expandJsonArraysInResolvedSubject(issuerWithSubject) .map(SubjectId::newInstance) .collect(Collectors.toCollection(LinkedHashSet::new)); } diff --git a/services/gateway/endpoints/src/main/java/org/eclipse/ditto/services/gateway/endpoints/routes/policies/TokenIntegrationSubjectIdFactory.java b/services/gateway/endpoints/src/main/java/org/eclipse/ditto/services/gateway/endpoints/routes/policies/TokenIntegrationSubjectIdFactory.java index 4c2cf4e45d..a71c829389 100755 --- a/services/gateway/endpoints/src/main/java/org/eclipse/ditto/services/gateway/endpoints/routes/policies/TokenIntegrationSubjectIdFactory.java +++ b/services/gateway/endpoints/src/main/java/org/eclipse/ditto/services/gateway/endpoints/routes/policies/TokenIntegrationSubjectIdFactory.java @@ -13,12 +13,7 @@ package org.eclipse.ditto.services.gateway.endpoints.routes.policies; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; -import org.eclipse.ditto.json.JsonArray; -import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.model.base.headers.DittoHeaders; import org.eclipse.ditto.model.jwt.JsonWebToken; import org.eclipse.ditto.model.policies.SubjectId; @@ -28,12 +23,6 @@ */ public interface TokenIntegrationSubjectIdFactory { - /** - * Compiled Pattern of a string containing any unresolved non-empty JsonArray-String notations inside. - * All strings matching this pattern are valid JSON arrays. Not all JSON arrays match this pattern. - */ - Pattern JSON_ARRAY_PATTERN = Pattern.compile("(\\[\"(?:\\\\\"|[^\"])*+\"(?:,\"(?:\\\\\"|[^\"])*+\")*+])"); - /** * Compute the token integration subject IDs from headers and JWT. * @@ -42,36 +31,4 @@ public interface TokenIntegrationSubjectIdFactory { * @return the computed subject IDs. */ Set getSubjectIds(DittoHeaders dittoHeaders, JsonWebToken jwt); - - /** - * Checks whether the passed {@code resolvedSubject} (resolved via JWT and header placeholder mechanism) contains - * JsonArrays ({@code ["..."]} and expands those JsonArrays to multiple resolved subjects returned as resulting - * stream of this operation. - *

- * Is able to handle an arbitrary amount of JsonArrays in the passed resolvedSubjects. - * - * @param resolvedSubject the resolved subjects potentially containing JsonArrays as JsonArray-String values. - * @return a stream of a single subject when the passed in {@code resolvedSubject} did not contain any - * JsonArray-String notation or else a stream of multiple subjects with the JsonArrays being resolved to multiple - * results of the stream. - */ - static Stream expandJsonArraysInResolvedSubject(final String resolvedSubject) { - final Matcher jsonArrayMatcher = JSON_ARRAY_PATTERN.matcher(resolvedSubject); - final int group = 1; - if (jsonArrayMatcher.find()) { - final String beforeMatched = resolvedSubject.substring(0, jsonArrayMatcher.start(group)); - final String matchedStr = - resolvedSubject.substring(jsonArrayMatcher.start(group), jsonArrayMatcher.end(group)); - final String afterMatched = resolvedSubject.substring(jsonArrayMatcher.end(group)); - return JsonArray.of(matchedStr).stream() - .filter(JsonValue::isString) - .map(JsonValue::asString) - .flatMap(arrayStringElem -> expandJsonArraysInResolvedSubject(beforeMatched) // recurse! - .flatMap(before -> expandJsonArraysInResolvedSubject(afterMatched) // recurse! - .map(after -> before.concat(arrayStringElem).concat(after)) - ) - ); - } - return Stream.of(resolvedSubject); - } } diff --git a/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProvider.java b/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProvider.java index 6edf49c326..eb977d494b 100644 --- a/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProvider.java +++ b/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProvider.java @@ -23,6 +23,8 @@ import org.eclipse.ditto.model.base.auth.AuthorizationSubject; import org.eclipse.ditto.model.jwt.JsonWebToken; +import org.eclipse.ditto.model.placeholders.ExpressionResolver; +import org.eclipse.ditto.model.placeholders.PlaceholderFactory; import org.eclipse.ditto.model.policies.SubjectId; import org.eclipse.ditto.signals.commands.base.exceptions.GatewayJwtIssuerNotSupportedException; @@ -58,11 +60,15 @@ public List getAuthorizationSubjects(final JsonWebToken js final JwtSubjectIssuerConfig jwtSubjectIssuerConfig = jwtSubjectIssuersConfig.getConfigItem(issuer) .orElseThrow(() -> GatewayJwtIssuerNotSupportedException.newBuilder(issuer).build()); - return jsonWebToken.getSubjects() - .stream() - .map(subject -> SubjectId.newInstance(jwtSubjectIssuerConfig.getSubjectIssuer(), subject)) - .map(AuthorizationSubject::newInstance) - .collect(Collectors.toList()); + final ExpressionResolver expressionResolver = PlaceholderFactory.newExpressionResolver( + PlaceholderFactory.newPlaceholderResolver(JwtPlaceholder.getInstance(), jsonWebToken)); + + return jwtSubjectIssuerConfig.getAuthorizationSubjectTemplates().stream() + .map(expressionResolver::resolvePartially) + .flatMap((issuerWithSubject) -> JwtPlaceholder.expandJsonArraysInResolvedSubject(issuerWithSubject)) + .map(subject -> SubjectId.newInstance(jwtSubjectIssuerConfig.getSubjectIssuer(), subject)) + .map(AuthorizationSubject::newInstance) + .collect(Collectors.toList()); } @Override diff --git a/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtPlaceholder.java b/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtPlaceholder.java index 6c1e2fdd0b..3e373a9650 100644 --- a/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtPlaceholder.java +++ b/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtPlaceholder.java @@ -17,7 +17,11 @@ import java.util.List; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.model.jwt.JsonWebToken; import org.eclipse.ditto.model.placeholders.Placeholder; @@ -27,6 +31,12 @@ */ public final class JwtPlaceholder implements Placeholder { + /** + * Compiled Pattern of a string containing any unresolved non-empty JsonArray-String notations inside. + * All strings matching this pattern are valid JSON arrays. Not all JSON arrays match this pattern. + */ + private static final Pattern JSON_ARRAY_PATTERN = Pattern.compile("(\\[\"(?:\\\\\"|[^\"])*+\"(?:,\"(?:\\\\\"|[^\"])*+\")*+])"); + private static final JwtPlaceholder INSTANCE = new JwtPlaceholder(); private static final String PREFIX = "jwt"; @@ -62,4 +72,35 @@ public Optional resolve(final JsonWebToken jwt, final String placeholder return jwt.getBody().getValue(placeholder).map(JsonValue::formatAsString); } + /** + * Checks whether the passed {@code resolvedSubject} (resolved via JWT and header placeholder mechanism) contains + * JsonArrays ({@code ["..."]} and expands those JsonArrays to multiple resolved subjects returned as resulting + * stream of this operation. + *

+ * Is able to handle an arbitrary amount of JsonArrays in the passed resolvedSubjects. + * + * @param resolvedSubject the resolved subjects potentially containing JsonArrays as JsonArray-String values. + * @return a stream of a single subject when the passed in {@code resolvedSubject} did not contain any + * JsonArray-String notation or else a stream of multiple subjects with the JsonArrays being resolved to multiple + * results of the stream. + */ + public static Stream expandJsonArraysInResolvedSubject(final String resolvedSubject) { + final Matcher jsonArrayMatcher = JSON_ARRAY_PATTERN.matcher(resolvedSubject); + final int group = 1; + if (jsonArrayMatcher.find()) { + final String beforeMatched = resolvedSubject.substring(0, jsonArrayMatcher.start(group)); + final String matchedStr = + resolvedSubject.substring(jsonArrayMatcher.start(group), jsonArrayMatcher.end(group)); + final String afterMatched = resolvedSubject.substring(jsonArrayMatcher.end(group)); + return JsonArray.of(matchedStr).stream() + .filter(JsonValue::isString) + .map(JsonValue::asString) + .flatMap(arrayStringElem -> expandJsonArraysInResolvedSubject(beforeMatched) // recurse! + .flatMap(before -> expandJsonArraysInResolvedSubject(afterMatched) // recurse! + .map(after -> before.concat(arrayStringElem).concat(after)) + ) + ); + } + return Stream.of(resolvedSubject); + } } diff --git a/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuerConfig.java b/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuerConfig.java index 60b99f9bc7..78a8b9fc58 100644 --- a/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuerConfig.java +++ b/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuerConfig.java @@ -14,6 +14,10 @@ import static java.util.Objects.requireNonNull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; import javax.annotation.Nullable; @@ -27,18 +31,35 @@ @Immutable public final class JwtSubjectIssuerConfig { - private final String issuer; private final SubjectIssuer subjectIssuer; + private final String issuer; + private final List authSubjectTemplates; + + private static final List DEFAULT_AUTH_SUBJECT = Collections.singletonList("{{jwt:sub}}"); /** * Constructs a new {@code JwtSubjectIssuerConfig}. * + * @param subjectIssuer the subject issuer. * @param issuer the issuer. + * + */ + public JwtSubjectIssuerConfig(final SubjectIssuer subjectIssuer, final String issuer) { + this(subjectIssuer, issuer, DEFAULT_AUTH_SUBJECT); + } + + /** + * Constructs a new {@code JwtSubjectIssuerConfig}. + * * @param subjectIssuer the subject issuer. + * @param issuer the issuer. + * @param authSubjectTemplates the authorization subject templates + * */ - public JwtSubjectIssuerConfig(final String issuer, final SubjectIssuer subjectIssuer) { - this.issuer = requireNonNull(issuer); + public JwtSubjectIssuerConfig(final SubjectIssuer subjectIssuer, final String issuer, final List authSubjectTemplates) { this.subjectIssuer = requireNonNull(subjectIssuer); + this.issuer = requireNonNull(issuer); + this.authSubjectTemplates = Collections.unmodifiableList(new ArrayList<>(requireNonNull(authSubjectTemplates))); } /** @@ -59,25 +80,36 @@ public SubjectIssuer getSubjectIssuer() { return subjectIssuer; } + /** + * Returns the authorization subject templates + * + * @return the authorization subject templates + */ + public List getAuthorizationSubjectTemplates() { + return authSubjectTemplates; + } + @Override public boolean equals(@Nullable final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final JwtSubjectIssuerConfig that = (JwtSubjectIssuerConfig) o; return Objects.equals(issuer, that.issuer) && - Objects.equals(subjectIssuer, that.subjectIssuer); + Objects.equals(subjectIssuer, that.subjectIssuer) && + Objects.equals(authSubjectTemplates, that.authSubjectTemplates); } @Override public int hashCode() { - return Objects.hash(issuer, subjectIssuer); + return Objects.hash(issuer, subjectIssuer, authSubjectTemplates); } @Override public String toString() { return getClass().getSimpleName() + " [" + - "issuer=" + issuer + - ", subjectIssuer=" + subjectIssuer + + "subjectIssuer=" + subjectIssuer + + ", issuer=" + issuer + + ", authSubjectTemplates=" + authSubjectTemplates + "]"; } diff --git a/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuersConfig.java b/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuersConfig.java index acd3d3de7d..3d1ceb2243 100644 --- a/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuersConfig.java +++ b/services/gateway/security/src/main/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuersConfig.java @@ -66,7 +66,7 @@ public static JwtSubjectIssuersConfig fromOAuthConfig(final OAuthConfig config) // merge the default and extension config Stream.concat(config.getOpenIdConnectIssuers().entrySet().stream(), config.getOpenIdConnectIssuersExtension().entrySet().stream()) - .map(entry -> new JwtSubjectIssuerConfig(entry.getValue(), entry.getKey())) + .map(entry -> new JwtSubjectIssuerConfig(entry.getKey(), entry.getValue().getIssuer(), entry.getValue().getAuthorizationSubjectTemplates())) .collect(Collectors.toSet()); return new JwtSubjectIssuersConfig(configItems, config.getProtocol()); } diff --git a/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProviderTest.java b/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProviderTest.java new file mode 100644 index 0000000000..6c696de88d --- /dev/null +++ b/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoJwtAuthorizationSubjectsProviderTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2021 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.services.gateway.security.authentication.jwt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.List; + +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.model.base.auth.AuthorizationSubject; +import org.eclipse.ditto.model.jwt.JsonWebToken; +import org.eclipse.ditto.model.policies.SubjectIssuer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Unit test for {@link DittoJwtAuthorizationSubjectsProvider}. + */ +@RunWith(MockitoJUnitRunner.class) +public final class DittoJwtAuthorizationSubjectsProviderTest { + + @Test + public void verifyThatTheDefaultJwtSubjectPlaceholderWorks() { + final String subjectIssuer = "testIssuer"; + final String tokenSubject = "testSubject"; + + final JsonWebToken jsonWebToken = createToken("{\"sub\": \"" + tokenSubject + "\"}"); + final JwtSubjectIssuersConfig subjectIssuersConfig = createSubjectIssuersConfig(subjectIssuer); + + final DittoJwtAuthorizationSubjectsProvider underTest = DittoJwtAuthorizationSubjectsProvider + .of(subjectIssuersConfig); + + final List authSubjects = underTest.getAuthorizationSubjects(jsonWebToken); + + assertThat(authSubjects.size()).isEqualTo(1); + assertThat(authSubjects.get(0)).isEqualTo(AuthorizationSubject.newInstance(subjectIssuer + ":" + tokenSubject)); + } + + @Test + public void verifyThatASingleJwtSubjectPlaceholderWorks() { + final String subjectIssuer = "testIssuer"; + final String tokenAudience = "some-audience"; + + final JsonWebToken jsonWebToken = createToken("{\"aud\": \"" + tokenAudience + "\"}"); + final JwtSubjectIssuersConfig subjectIssuersConfig = createSubjectIssuersConfig(subjectIssuer, List.of("{{ jwt:aud }}")); + + final DittoJwtAuthorizationSubjectsProvider underTest = DittoJwtAuthorizationSubjectsProvider + .of(subjectIssuersConfig); + + final List authSubjects = underTest.getAuthorizationSubjects(jsonWebToken); + + assertThat(authSubjects.size()).isEqualTo(1); + assertThat(authSubjects.get(0)).isEqualTo(AuthorizationSubject.newInstance(subjectIssuer + ":" + tokenAudience)); + } + + @Test + public void verifyThatASingleJwtSubjectPlaceholderWorksWithJsonArray() { + final String subjectIssuer = "testIssuer"; + final String tokenAudience1 = "some-audience"; + final String tokenAudience2 = "other-audience"; + + final JsonWebToken jsonWebToken = createToken( + "{\"aud\": [\"" + tokenAudience1 + "\", \"" + tokenAudience2 + "\"]}"); + final JwtSubjectIssuersConfig subjectIssuersConfig = createSubjectIssuersConfig(subjectIssuer, + List.of("{{ jwt:aud }}")); + + final DittoJwtAuthorizationSubjectsProvider underTest = DittoJwtAuthorizationSubjectsProvider + .of(subjectIssuersConfig); + + final List authSubjects = underTest.getAuthorizationSubjects(jsonWebToken); + + assertThat(authSubjects).containsExactlyInAnyOrder( + AuthorizationSubject.newInstance(subjectIssuer + ":" + tokenAudience1), + AuthorizationSubject.newInstance(subjectIssuer + ":" + tokenAudience2) + ); + } + + @Test + public void verifyThatMultipleJwtSubjectPlaceholdersWork() { + final String subjectIssuer = "testIssuer"; + final String tokenAudience1 = "some-audience"; + final String tokenAudience2 = "other-audience"; + final String tokenGroup = "any-group"; + + final JsonWebToken jsonWebToken = createToken( + "{\"aud\": [\"" + tokenAudience1 + "\", \"" + tokenAudience2 + "\"],\"grp\": \"" + tokenGroup + "\"}"); + + final JwtSubjectIssuersConfig subjectIssuersConfig = createSubjectIssuersConfig(subjectIssuer, + List.of("{{ jwt:aud }}", "{{ jwt:grp }}")); + + final DittoJwtAuthorizationSubjectsProvider underTest = DittoJwtAuthorizationSubjectsProvider + .of(subjectIssuersConfig); + + final List authSubjects = underTest.getAuthorizationSubjects(jsonWebToken); + + assertThat(authSubjects).containsExactlyInAnyOrder( + AuthorizationSubject.newInstance(subjectIssuer + ":" + tokenAudience1), + AuthorizationSubject.newInstance(subjectIssuer + ":" + tokenAudience2), + AuthorizationSubject.newInstance(subjectIssuer + ":" + tokenGroup) + ); + } + + JsonWebToken createToken(final String body) { + final JsonWebToken jsonWebToken = mock(JsonWebToken.class); + when(jsonWebToken.getIssuer()).thenReturn(JwtTestConstants.ISSUER); + when(jsonWebToken.getBody()).thenReturn(JsonObject.of(body)); + return jsonWebToken; + } + + JwtSubjectIssuersConfig createSubjectIssuersConfig(final String subjectIssuer, final List subjectTemplates) { + final JwtSubjectIssuerConfig subjectIssuerConfig = new JwtSubjectIssuerConfig( + SubjectIssuer.newInstance(subjectIssuer), + JwtTestConstants.ISSUER, + subjectTemplates); + return JwtSubjectIssuersConfig.fromJwtSubjectIssuerConfigs(List.of(subjectIssuerConfig)); + } + + JwtSubjectIssuersConfig createSubjectIssuersConfig(final String subjectIssuer) { + final JwtSubjectIssuerConfig subjectIssuerConfig = new JwtSubjectIssuerConfig( + SubjectIssuer.newInstance(subjectIssuer), + JwtTestConstants.ISSUER); + return JwtSubjectIssuersConfig.fromJwtSubjectIssuerConfigs(List.of(subjectIssuerConfig)); + } + +} \ No newline at end of file diff --git a/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoPublicKeyProviderTest.java b/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoPublicKeyProviderTest.java index 6e2860925e..0d035d32c1 100644 --- a/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoPublicKeyProviderTest.java +++ b/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/DittoPublicKeyProviderTest.java @@ -73,7 +73,7 @@ public void setup() { actorSystem = ActorSystem.create(getClass().getSimpleName()); when(httpClientMock.getActorSystem()).thenReturn(actorSystem); final JwtSubjectIssuersConfig subjectIssuersConfig = JwtSubjectIssuersConfig.fromJwtSubjectIssuerConfigs( - Collections.singleton(new JwtSubjectIssuerConfig("google.com", SubjectIssuer.GOOGLE))); + Collections.singleton(new JwtSubjectIssuerConfig(SubjectIssuer.GOOGLE, "google.com"))); when(cacheConfigMock.getMaximumSize()).thenReturn(100L); when(cacheConfigMock.getExpireAfterWrite()).thenReturn(Duration.ofMinutes(3)); underTest = DittoPublicKeyProvider.of(subjectIssuersConfig, httpClientMock, cacheConfigMock, diff --git a/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuersConfigTest.java b/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuersConfigTest.java index 4cb47f8db5..320e4eb221 100644 --- a/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuersConfigTest.java +++ b/services/gateway/security/src/test/java/org/eclipse/ditto/services/gateway/security/authentication/jwt/JwtSubjectIssuersConfigTest.java @@ -18,6 +18,7 @@ import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -41,8 +42,8 @@ public final class JwtSubjectIssuersConfigTest { private static final JwtSubjectIssuersConfig JWT_SUBJECT_ISSUERS_CONFIG; static { - JWT_SUBJECT_ISSUER_CONFIG_GOOGLE = new JwtSubjectIssuerConfig("accounts.google.com", SubjectIssuer.GOOGLE); - JWT_SUBJECT_ISSUER_CONFIG_GOOGLE_DE = new JwtSubjectIssuerConfig("accounts.google.de", SubjectIssuer.GOOGLE); + JWT_SUBJECT_ISSUER_CONFIG_GOOGLE = new JwtSubjectIssuerConfig(SubjectIssuer.GOOGLE, "accounts.google.com"); + JWT_SUBJECT_ISSUER_CONFIG_GOOGLE_DE = new JwtSubjectIssuerConfig(SubjectIssuer.GOOGLE, "accounts.google.de"); JWT_SUBJECT_ISSUER_CONFIGS = new HashSet<>(); JWT_SUBJECT_ISSUER_CONFIGS.add(JWT_SUBJECT_ISSUER_CONFIG_GOOGLE); JWT_SUBJECT_ISSUER_CONFIGS.add(JWT_SUBJECT_ISSUER_CONFIG_GOOGLE_DE); @@ -88,8 +89,20 @@ public void issuerWithMultipleIssuerUrisWorks() { @Test public void fromOAuthConfig() { - final JwtSubjectIssuerConfig googleItem = new JwtSubjectIssuerConfig( "https://accounts.google.com", SubjectIssuer.GOOGLE); - final JwtSubjectIssuerConfig additionalItem = new JwtSubjectIssuerConfig("https://additional.google.com", SubjectIssuer.newInstance("additional")); + final JwtSubjectIssuerConfig googleItem = new JwtSubjectIssuerConfig( + SubjectIssuer.GOOGLE, + "https://accounts.google.com", + List.of( + "{{ jwt:sub }}", + "{{ jwt:sub }}/{{ jwt:scope }}", + "{{ jwt:sub }}/{{ jwt:scope }}@{{ jwt:client_id }}", + "{{ jwt:sub }}/{{ jwt:scope }}@{{ jwt:non_existing }}", + "{{ jwt:roles/support }}" + )); + final JwtSubjectIssuerConfig additionalItem = new JwtSubjectIssuerConfig( + SubjectIssuer.newInstance("additional"), + "https://additional.google.com", + List.of("{{ jwt:sub }}")); final OAuthConfig oAuthConfig = DefaultOAuthConfig.of(ConfigFactory.load("oauth-test.conf")); final JwtSubjectIssuersConfig jwtSubjectIssuersConfig = JwtSubjectIssuersConfig.fromOAuthConfig(oAuthConfig); diff --git a/services/gateway/security/src/test/resources/oauth-test.conf b/services/gateway/security/src/test/resources/oauth-test.conf index 2966f1c2fc..0da9c7f658 100644 --- a/services/gateway/security/src/test/resources/oauth-test.conf +++ b/services/gateway/security/src/test/resources/oauth-test.conf @@ -1,8 +1,20 @@ oauth { openid-connect-issuers = { - google = "https://accounts.google.com" + google = { + issuer = "https://accounts.google.com" + auth-subjects = [ + "{{ jwt:sub }}", + "{{ jwt:sub }}/{{ jwt:scope }}", + "{{ jwt:sub }}/{{ jwt:scope }}@{{ jwt:client_id }}", + "{{ jwt:sub }}/{{ jwt:scope }}@{{ jwt:non_existing }}", + "{{ jwt:roles/support }}" + ] + } } openid-connect-issuers-extension = { - additional = "https://additional.google.com" + additional = { + issuer = "https://additional.google.com" + auth-subjects = [ "{{ jwt:sub }}" ] + } } } \ No newline at end of file diff --git a/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfig.java b/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfig.java index a3d1bb4f05..f7f75bdd0b 100644 --- a/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfig.java +++ b/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfig.java @@ -34,6 +34,8 @@ import org.eclipse.ditto.utils.jsr305.annotations.AllValuesAreNonnullByDefault; import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigValue; /** @@ -46,8 +48,8 @@ public final class DefaultOAuthConfig implements OAuthConfig { private static final String CONFIG_PATH = "oauth"; private final String protocol; - private final Map openIdConnectIssuers; - private final Map openIdConnectIssuersExtension; + private final Map openIdConnectIssuers; + private final Map openIdConnectIssuersExtension; private final String tokenIntegrationSubject; private DefaultOAuthConfig(final ConfigWithFallback configWithFallback) { @@ -59,9 +61,9 @@ private DefaultOAuthConfig(final ConfigWithFallback configWithFallback) { configWithFallback.getString(OAuthConfigValue.TOKEN_INTEGRATION_SUBJECT.getConfigPath()); } - private static Map loadIssuers(final ConfigWithFallback config, + private static Map loadIssuers(final ConfigWithFallback config, final KnownConfigValue configValue) { - final Config issuersConfig = config.getConfig(configValue.getConfigPath()); + final ConfigObject issuersConfig = config.getObject(configValue.getConfigPath()); return issuersConfig.entrySet().stream().collect(SubjectIssuerCollector.toSubjectIssuerMap()); } @@ -82,12 +84,12 @@ public String getProtocol() { } @Override - public Map getOpenIdConnectIssuers() { + public Map getOpenIdConnectIssuers() { return openIdConnectIssuers; } @Override - public Map getOpenIdConnectIssuersExtension() { + public Map getOpenIdConnectIssuersExtension() { return openIdConnectIssuersExtension; } @@ -123,32 +125,32 @@ public String toString() { } private static class SubjectIssuerCollector - implements Collector, Map, Map> { + implements Collector, Map, Map> { private static SubjectIssuerCollector toSubjectIssuerMap() { return new SubjectIssuerCollector(); } @Override - public Supplier> supplier() { + public Supplier> supplier() { return HashMap::new; } @Override - public BiConsumer, Map.Entry> accumulator() { + public BiConsumer, Map.Entry> accumulator() { return (map, entry) -> map.put(SubjectIssuer.newInstance(entry.getKey()), - entry.getValue().unwrapped().toString()); + DefaultSubjectIssuerConfig.of(ConfigFactory.empty().withFallback(entry.getValue()))); } @Override - public BinaryOperator> combiner() { + public BinaryOperator> combiner() { return (left, right) -> Stream.concat(left.entrySet().stream(), right.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } @Override - public Function, Map> finisher() { + public Function, Map> finisher() { return map -> Collections.unmodifiableMap(new HashMap<>(map)); } diff --git a/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultSubjectIssuerConfig.java b/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultSubjectIssuerConfig.java new file mode 100644 index 0000000000..400214b2e5 --- /dev/null +++ b/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultSubjectIssuerConfig.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.services.gateway.util.config.security; + +import static org.eclipse.ditto.model.base.common.ConditionChecker.argumentNotEmpty; +import static org.eclipse.ditto.model.base.common.ConditionChecker.checkNotNull; + +import java.util.List; +import java.util.Objects; + +import com.typesafe.config.Config; + +import org.eclipse.ditto.services.utils.config.ConfigWithFallback; + +public final class DefaultSubjectIssuerConfig implements SubjectIssuerConfig { + + private final String issuer; + + private final List authSubjectTemplates; + + private DefaultSubjectIssuerConfig(final ConfigWithFallback configWithFallback) { + issuer = configWithFallback.getString(SubjectIssuerConfigValue.ISSUER.getConfigPath()); + authSubjectTemplates = configWithFallback.getStringList(SubjectIssuerConfigValue.AUTH_SUBJECTS.getConfigPath()); + } + + private DefaultSubjectIssuerConfig(final String issuer, final List authSubjectTemplates) { + this.issuer = issuer; + this.authSubjectTemplates = authSubjectTemplates; + } + + /** + * Returns an instance of {@code DefaultSubjectIssuerConfig} based on the settings of the specified Config. + * + * @param config is supposed to provide the config for the issuer at its current level. + * @return the instance. + * @throws org.eclipse.ditto.services.utils.config.DittoConfigError if {@code config} is invalid. + */ + public static DefaultSubjectIssuerConfig of(final Config config) { + return new DefaultSubjectIssuerConfig( + ConfigWithFallback.newInstance(config, SubjectIssuerConfigValue.values())); + } + + /** + * Returns a new SubjectIssuerConfig based on the provided strings. + * + * @param issuer the issuer's endpoint {@code issuer}. + * @param authSubjectTemplates list of authorizationsubject placeholder strings + * {@code authSubjectTemplates}. + * @return a new SubjectIssuerConfig. + * @throws NullPointerException if {@code issuer} or {@code authSubjectTemplates} is + * {@code null}. + * @throws IllegalArgumentException if {@code issuer} or {@code authSubjectTemplates} is + * empty. + */ + public static DefaultSubjectIssuerConfig of(final String issuer, final List authSubjectTemplates) { + checkNotNull(issuer, "issuer"); + argumentNotEmpty(authSubjectTemplates, "authSubjectTemplates"); + + return new DefaultSubjectIssuerConfig(issuer, authSubjectTemplates); + } + + public final String getIssuer() { + return issuer; + } + + public final List getAuthorizationSubjectTemplates() { + return authSubjectTemplates; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final DefaultSubjectIssuerConfig that = (DefaultSubjectIssuerConfig) o; + return Objects.equals(issuer, that.issuer) && authSubjectTemplates.equals(that.authSubjectTemplates); + } + + @Override + public int hashCode() { + return Objects.hash(issuer, authSubjectTemplates); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "issuer=" + issuer + + ", authSubjectTemplates=" + authSubjectTemplates + + "]"; + } +} diff --git a/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/OAuthConfig.java b/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/OAuthConfig.java index 8952882fe0..415218662e 100644 --- a/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/OAuthConfig.java +++ b/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/OAuthConfig.java @@ -40,7 +40,7 @@ public interface OAuthConfig { * * @return the issuers. */ - Map getOpenIdConnectIssuers(); + Map getOpenIdConnectIssuers(); /** * Returns all additionally supported openid connect issuers. This can be useful during migration phases e.g. if @@ -48,7 +48,7 @@ public interface OAuthConfig { * * @return the additional issuers. */ - Map getOpenIdConnectIssuersExtension(); + Map getOpenIdConnectIssuersExtension(); /** * Returns the template of the subject activated via token integration. May contain placeholders. diff --git a/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/SubjectIssuerConfig.java b/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/SubjectIssuerConfig.java new file mode 100644 index 0000000000..fce4fd60ec --- /dev/null +++ b/services/gateway/util/src/main/java/org/eclipse/ditto/services/gateway/util/config/security/SubjectIssuerConfig.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.services.gateway.util.config.security; + +import java.util.List; + +import org.eclipse.ditto.services.utils.config.KnownConfigValue; + +/** + * Represents configuration for a {@link org.eclipse.ditto.model.policies.SubjectIssuer}, containing + * issuer endpoint and a list of templates for substituting authorization + * subjects. + * + * @since 2.0.0 + */ +public interface SubjectIssuerConfig { + + /** + * Returns the issuer endpoint. + * + * @return the token issuer endpoint. + */ + String getIssuer(); + + /** + * Returns the authorization subject templates. + * + * @return a list of templates. + */ + List getAuthorizationSubjectTemplates(); + + + enum SubjectIssuerConfigValue implements KnownConfigValue { + ISSUER("issuer", ""), + AUTH_SUBJECTS("auth-subjects", List.of("{{jwt:sub}}")); + + private final String path; + private final Object defaultValue; + + SubjectIssuerConfigValue(final String thePath, final Object theDefaultValue) { + path = thePath; + defaultValue = theDefaultValue; + } + + @Override + public Object getDefaultValue() { + return defaultValue; + } + + @Override + public String getConfigPath() { + return path; + } + } +} diff --git a/services/gateway/util/src/test/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfigTest.java b/services/gateway/util/src/test/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfigTest.java index e2ef3cb323..0ba89a30ba 100644 --- a/services/gateway/util/src/test/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfigTest.java +++ b/services/gateway/util/src/test/java/org/eclipse/ditto/services/gateway/util/config/security/DefaultOAuthConfigTest.java @@ -18,6 +18,7 @@ import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; import java.util.Collections; +import java.util.List; import org.assertj.core.api.JUnitSoftAssertions; import org.eclipse.ditto.model.policies.SubjectIssuer; @@ -89,14 +90,24 @@ public void underTestReturnsValuesOfConfigFile() { softly.assertThat(underTest.getOpenIdConnectIssuers()) .as(OAuthConfig.OAuthConfigValue.OPENID_CONNECT_ISSUERS.getConfigPath()) .isEqualTo( - Collections.singletonMap(SubjectIssuer.newInstance("google"), "https://accounts.google.com")); + Collections.singletonMap( + SubjectIssuer.newInstance("google"), + DefaultSubjectIssuerConfig.of( + "https://accounts.google.com", + List.of( + "{{ jwt:sub }}", + "{{ jwt:sub }}/{{ jwt:scope }}", + "{{ jwt:sub }}/{{ jwt:scope }}@{{ jwt:client_id }}", + "{{ jwt:sub }}/{{ jwt:scope }}@{{ jwt:non_existing }}", + "{{ jwt:roles/support }}" + )))); softly.assertThat(underTest.getOpenIdConnectIssuersExtension()) .as(OAuthConfig.OAuthConfigValue.OPENID_CONNECT_ISSUERS_EXTENSION.getConfigPath()) - .isEqualTo(Collections.singletonMap(SubjectIssuer.newInstance("additional"), - "https://additional.google.com")); + .isEqualTo(Collections.singletonMap( + SubjectIssuer.newInstance("additional"), + DefaultSubjectIssuerConfig.of("https://additional.google.com", List.of("{{ jwt:sub }}")))); softly.assertThat(underTest.getTokenIntegrationSubject()).isEqualTo("ditto:ditto"); } - } diff --git a/services/gateway/util/src/test/resources/devops-test.conf b/services/gateway/util/src/test/resources/devops-test.conf index dc1068653f..99edaf1b31 100644 --- a/services/gateway/util/src/test/resources/devops-test.conf +++ b/services/gateway/util/src/test/resources/devops-test.conf @@ -8,7 +8,9 @@ devops { status-oauth2-subjects = ["someissuer:c"] oauth = { openid-connect-issuers = { - someissuer = "https://example.com" + someissuer = { + issuer = "https://example.com" + } } } } diff --git a/services/gateway/util/src/test/resources/oauth-test.conf b/services/gateway/util/src/test/resources/oauth-test.conf index 1678a41218..5a4a3828a8 100644 --- a/services/gateway/util/src/test/resources/oauth-test.conf +++ b/services/gateway/util/src/test/resources/oauth-test.conf @@ -1,10 +1,22 @@ oauth { protocol = "http" openid-connect-issuers = { - google = "https://accounts.google.com" + google = { + issuer = "https://accounts.google.com" + auth-subjects = [ + "{{ jwt:sub }}", + "{{ jwt:sub }}/{{ jwt:scope }}", + "{{ jwt:sub }}/{{ jwt:scope }}@{{ jwt:client_id }}", + "{{ jwt:sub }}/{{ jwt:scope }}@{{ jwt:non_existing }}", + "{{ jwt:roles/support }}" + ] + } } openid-connect-issuers-extension = { - additional = "https://additional.google.com" + additional = { + issuer = "https://additional.google.com" + auth-subjects = [ "{{ jwt:sub }}" ] + } } token-integration-subject = "ditto:ditto" -} \ No newline at end of file +}