Skip to content

Commit 146dd6c

Browse files
authored
Merge pull request #2216 from beyonnex-io/feature/2145-jwt-claim-header-injection
#2145: support configuration of JWT claim injection into headers
2 parents 4bb3098 + 9d416d0 commit 146dd6c

File tree

18 files changed

+228
-32
lines changed

18 files changed

+228
-32
lines changed

deployment/helm/ditto/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ description: |
1616
A digital twin is a virtual, cloud based, representation of his real world counterpart
1717
(real world “Things”, e.g. devices like sensors, smart heating, connected cars, smart grids, EV charging stations etc).
1818
type: application
19-
version: 3.8.0-M7 # chart version is effectively set by release-job
20-
appVersion: 3.8.0-M4
19+
version: 3.8.0-M8 # chart version is effectively set by release-job
20+
appVersion: 3.8.0-M5
2121
keywords:
2222
- iot-chart
2323
- digital-twin

deployment/helm/ditto/service-config/gateway-extension.conf.tpl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ ditto {
1212
"{{$subject}}"
1313
{{- end }}
1414
]
15+
inject-claims-into-headers = {
16+
{{- range $claimKey, $claimValue := $value.injectClaimsIntoHeaders }}
17+
{{$claimKey}} = "{{$claimValue}}"
18+
{{- end }}
19+
}
1520
}
1621
{{- end }}
1722
}
@@ -28,6 +33,11 @@ ditto {
2833
"{{$subject}}"
2934
{{- end }}
3035
]
36+
inject-claims-into-headers = {
37+
{{- range $claimKey, $claimValue := $value.injectClaimsIntoHeaders }}
38+
{{$claimKey}} = "{{$claimValue}}"
39+
{{- end }}
40+
}
3141
}
3242
{{- end }}
3343
}

deployment/helm/ditto/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,6 +2013,9 @@ gateway:
20132013
# authSubjects:
20142014
# - "{{ jwt:sub }}"
20152015
# - "{{ jwt:groups }}"
2016+
# injectClaimsIntoHeaders:
2017+
# user_email: "{{ jwt:email }}"
2018+
# user_name: "{{ jwt:name }}"
20162019
# configure the subject to inject in policy action activateTokenIntegration
20172020
tokenIntegrationSubject: "integration:{{policy-entry:label}}:{{jwt:aud}}"
20182021
# devops contains the configuration of the gateway's "/devops" API, e.g. access to it
@@ -2034,6 +2037,9 @@ gateway:
20342037
# authSubjects:
20352038
# - "{{ jwt:sub }}"
20362039
# - "{{ jwt:groups }}"
2040+
# injectClaimsIntoHeaders:
2041+
# user_email: "{{ jwt:email }}"
2042+
# user_name: "{{ jwt:name }}"
20372043
# oauthSubjects contains list of subjects authorized to use "/devops" and "/api/2/connections" resources
20382044
oauthSubjects:
20392045
# - "example-ops:devops-admin"

documentation/src/main/resources/pages/ditto/installation-operating.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ When `auth-subjects` is not provided, the `"sub"` claim (`{%raw%}{{ jwt:sub }}{%
164164
Please read [more details on the OpenId Connect configuration placeholder](basic-placeholders.html#scope-openid-connect-configuration)
165165
to find out what is possible when defining the `auth-subjects`.
166166

167+
Since Ditto 3.8.0, it is in addition possible to configure `inject-claims-into-headers`. This is a configuration which
168+
takes a map of HTTP header names to JWT claim placeholders. Valid JWT tokens will be resolved and added to the headers
169+
of the command as custom headers.
170+
Ditto by default forwards custom headers and preserves them, e.g. when forwarding a [message](basic-messages.html) or
171+
when emitting an [event](basic-signals-event.html) after a command changes a thing.
172+
Using `inject-claims-into-headers`, it e.g. is possible to add the email address of the authenticated user as a custom
173+
header to a command so that e.g. in logging it can be determined which user caused a change.
174+
167175

168176
```
169177
ditto.gateway.authentication {
@@ -182,6 +190,10 @@ ditto.gateway.authentication {
182190
"{%raw%}{{ jwt:sub }}/{{ jwt:scp }}@{{ jwt:non_existing }}{%endraw%}",
183191
"{%raw%}{{ jwt:roles/support }}{%endraw%}"
184192
]
193+
inject-claims-into-headers = {
194+
user-email = "{%raw%}{{ jwt:email }}{%endraw%}"
195+
user-name = "{%raw%}{{ jwt:name }}{%endraw%}"
196+
}
185197
}
186198
}
187199
}

gateway/service/src/main/java/org/eclipse/ditto/gateway/service/security/authentication/AuthenticationChain.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,12 @@ private AuthResultAccumulator appendResult(final AuthenticationProvider<?> authe
134134
final AuthenticationResult nextResult) {
135135
if (nextResult.isSuccess()) {
136136
logSuccess(authenticationProvider);
137-
return new AuthResultAccumulator(nextResult, failureResults, requestContext, dittoHeaders);
137+
return new AuthResultAccumulator(nextResult, failureResults, requestContext, nextResult.getDittoHeaders());
138138
} else {
139139
logFailure(authenticationProvider, nextResult);
140140
final var newFailureResults =
141141
Stream.concat(failureResults.stream(), Stream.of(nextResult)).toList();
142-
return new AuthResultAccumulator(successResult, newFailureResults, requestContext, dittoHeaders);
142+
return new AuthResultAccumulator(successResult, newFailureResults, requestContext, nextResult.getDittoHeaders());
143143
}
144144
}
145145

gateway/service/src/main/java/org/eclipse/ditto/gateway/service/security/authentication/jwt/DefaultJwtAuthenticationResultProvider.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package org.eclipse.ditto.gateway.service.security.authentication.jwt;
1414

1515
import java.util.List;
16+
import java.util.Map;
1617
import java.util.concurrent.CompletableFuture;
1718
import java.util.concurrent.CompletionStage;
1819

@@ -44,7 +45,15 @@ public CompletionStage<JwtAuthenticationResult> getAuthenticationResult(final Js
4445
final DittoHeaders dittoHeaders) {
4546

4647
final List<AuthorizationSubject> authSubjects = authSubjectsProvider.getAuthorizationSubjects(jwt);
47-
return CompletableFuture.completedFuture(JwtAuthenticationResult.successful(dittoHeaders,
48+
final Map<String, String> additionalHeadersToInject =
49+
authSubjectsProvider.getAdditionalHeadersInjectedFromClaims(jwt);
50+
final DittoHeaders adjustedHeaders;
51+
if (!additionalHeadersToInject.isEmpty()) {
52+
adjustedHeaders = dittoHeaders.toBuilder().putHeaders(additionalHeadersToInject).build();
53+
} else {
54+
adjustedHeaders = dittoHeaders;
55+
}
56+
return CompletableFuture.completedFuture(JwtAuthenticationResult.successful(adjustedHeaders,
4857
AuthorizationModelFactory.newAuthContext(DittoAuthorizationContextType.JWT, authSubjects),
4958
jwt));
5059
}

gateway/service/src/main/java/org/eclipse/ditto/gateway/service/security/authentication/jwt/DittoJwtAuthorizationSubjectsProvider.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull;
1616

1717
import java.util.List;
18+
import java.util.Map;
1819
import java.util.Objects;
20+
import java.util.stream.Collectors;
1921

2022
import javax.annotation.Nullable;
2123
import javax.annotation.concurrent.Immutable;
@@ -106,6 +108,29 @@ public List<AuthorizationSubject> getAuthorizationSubjects(final JsonWebToken js
106108
.toList();
107109
}
108110

111+
@Override
112+
public Map<String, String> getAdditionalHeadersInjectedFromClaims(final JsonWebToken jsonWebToken) {
113+
checkNotNull(jsonWebToken);
114+
115+
final String issuer = jsonWebToken.getIssuer();
116+
final JwtSubjectIssuerConfig jwtSubjectIssuerConfig = jwtSubjectIssuersConfig.getConfigItem(issuer)
117+
.orElseThrow(() -> GatewayJwtIssuerNotSupportedException.newBuilder(issuer).build());
118+
119+
final Map<String, String> injectClaimsIntoHeaders = jwtSubjectIssuerConfig.getInjectClaimsIntoHeaders();
120+
if (!injectClaimsIntoHeaders.isEmpty()) {
121+
final ExpressionResolver expressionResolver = PlaceholderFactory.newExpressionResolver(
122+
PlaceholderFactory.newPlaceholderResolver(JwtPlaceholder.getInstance(), jsonWebToken));
123+
124+
return injectClaimsIntoHeaders.entrySet().stream()
125+
.collect(Collectors.toMap(
126+
Map.Entry::getKey,
127+
entry -> expressionResolver.resolve(entry.getValue()).findFirst().orElse(entry.getValue())
128+
));
129+
} else {
130+
return Map.of();
131+
}
132+
}
133+
109134
@Override
110135
public boolean equals(@Nullable final Object o) {
111136
if (this == o) {

gateway/service/src/main/java/org/eclipse/ditto/gateway/service/security/authentication/jwt/JwtAuthorizationSubjectsProvider.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@
1515
import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull;
1616

1717
import java.util.List;
18+
import java.util.Map;
1819

20+
import org.apache.pekko.actor.ActorSystem;
1921
import org.eclipse.ditto.base.model.auth.AuthorizationSubject;
2022
import org.eclipse.ditto.internal.utils.extension.DittoExtensionIds;
2123
import org.eclipse.ditto.internal.utils.extension.DittoExtensionPoint;
2224
import org.eclipse.ditto.jwt.model.JsonWebToken;
2325

2426
import com.typesafe.config.Config;
2527

26-
import org.apache.pekko.actor.ActorSystem;
27-
2828
/**
2929
* A provider for {@link AuthorizationSubject}s contained in a {@link JsonWebToken}.
3030
*/
@@ -39,6 +39,17 @@ public interface JwtAuthorizationSubjectsProvider extends DittoExtensionPoint {
3939
*/
4040
List<AuthorizationSubject> getAuthorizationSubjects(JsonWebToken jsonWebToken);
4141

42+
/**
43+
* Returns a map of additional headers to inject based on the {@code JsonWebToken} (configured statically in Ditto
44+
* via {@code inject-claims-into-headers} config).
45+
*
46+
* @param jsonWebToken the token containing the claims to inject headers from.
47+
* @return the map of additional headers to inject.
48+
* @throws NullPointerException if {@code jsonWebToken} is {@code null}.
49+
* @since 3.8.0
50+
*/
51+
Map<String, String> getAdditionalHeadersInjectedFromClaims(JsonWebToken jsonWebToken);
52+
4253
/**
4354
* Loads the implementation of {@code JwtAuthorizationSubjectsProvider} which is configured for the {@code ActorSystem}.
4455
*

gateway/service/src/main/java/org/eclipse/ditto/gateway/service/security/authentication/jwt/JwtSubjectIssuerConfig.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import java.util.ArrayList;
1818
import java.util.Collection;
1919
import java.util.Collections;
20+
import java.util.HashMap;
2021
import java.util.List;
22+
import java.util.Map;
2123
import java.util.Objects;
2224

2325
import javax.annotation.Nullable;
@@ -34,6 +36,7 @@ public final class JwtSubjectIssuerConfig {
3436
private final SubjectIssuer subjectIssuer;
3537
private final List<String> issuers;
3638
private final List<String> authSubjectTemplates;
39+
private final Map<String, String> injectClaimsIntoHeaders;
3740

3841
private static final List<String> DEFAULT_AUTH_SUBJECT = Collections.singletonList("{{jwt:sub}}");
3942

@@ -45,23 +48,26 @@ public final class JwtSubjectIssuerConfig {
4548
*
4649
*/
4750
public JwtSubjectIssuerConfig(final SubjectIssuer subjectIssuer, final Collection<String> issuers) {
48-
this(subjectIssuer, issuers, DEFAULT_AUTH_SUBJECT);
51+
this(subjectIssuer, issuers, DEFAULT_AUTH_SUBJECT, Map.of());
4952
}
5053

5154
/**
5255
* Constructs a new {@code JwtSubjectIssuerConfig}.
5356
*
5457
* @param subjectIssuer the subject issuer.
55-
* @param issuers the list of issuers.
56-
* @param authSubjectTemplates the authorization subject templates
57-
*
58+
* @param issuers the list of issuers.
59+
* @param authSubjectTemplates the authorization subject templates
60+
* @param injectClaimsIntoHeaders map of header-key to JWT claim placeholder to inject JWT claims into headers.
5861
*/
5962
public JwtSubjectIssuerConfig(final SubjectIssuer subjectIssuer,
6063
final Collection<String> issuers,
61-
final Collection<String> authSubjectTemplates) {
64+
final Collection<String> authSubjectTemplates,
65+
final Map<String, String> injectClaimsIntoHeaders) {
6266
this.subjectIssuer = requireNonNull(subjectIssuer);
6367
this.issuers = Collections.unmodifiableList(new ArrayList<>(requireNonNull(issuers)));
6468
this.authSubjectTemplates = Collections.unmodifiableList(new ArrayList<>(requireNonNull(authSubjectTemplates)));
69+
this.injectClaimsIntoHeaders =
70+
Collections.unmodifiableMap(new HashMap<>(requireNonNull(injectClaimsIntoHeaders)));
6571
}
6672

6773
/**
@@ -91,19 +97,29 @@ public List<String> getAuthorizationSubjectTemplates() {
9197
return authSubjectTemplates;
9298
}
9399

100+
/**
101+
* Returns claims of a token to inject into DittoHeaders (using the map key as key for the custom header to inject).
102+
*
103+
* @return claims of a token to inject into DittoHeaders (using the map key as key for the custom header to inject).
104+
*/
105+
public Map<String, String> getInjectClaimsIntoHeaders() {
106+
return injectClaimsIntoHeaders;
107+
}
108+
94109
@Override
95110
public boolean equals(@Nullable final Object o) {
96111
if (this == o) return true;
97112
if (o == null || getClass() != o.getClass()) return false;
98113
final JwtSubjectIssuerConfig that = (JwtSubjectIssuerConfig) o;
99114
return Objects.equals(issuers, that.issuers) &&
100115
Objects.equals(subjectIssuer, that.subjectIssuer) &&
101-
Objects.equals(authSubjectTemplates, that.authSubjectTemplates);
116+
Objects.equals(authSubjectTemplates, that.authSubjectTemplates) &&
117+
Objects.equals(injectClaimsIntoHeaders, that.injectClaimsIntoHeaders);
102118
}
103119

104120
@Override
105121
public int hashCode() {
106-
return Objects.hash(issuers, subjectIssuer, authSubjectTemplates);
122+
return Objects.hash(issuers, subjectIssuer, authSubjectTemplates, injectClaimsIntoHeaders);
107123
}
108124

109125
@Override
@@ -112,6 +128,7 @@ public String toString() {
112128
"subjectIssuer=" + subjectIssuer +
113129
", issuers=" + issuers +
114130
", authSubjectTemplates=" + authSubjectTemplates +
131+
", injectClaimsIntoHeaders=" + injectClaimsIntoHeaders +
115132
"]";
116133
}
117134

gateway/service/src/main/java/org/eclipse/ditto/gateway/service/security/authentication/jwt/JwtSubjectIssuersConfig.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import javax.annotation.concurrent.Immutable;
2929

3030
import org.eclipse.ditto.gateway.service.util.config.security.OAuthConfig;
31+
import org.eclipse.ditto.gateway.service.util.config.security.SubjectIssuerConfig;
3132
import org.eclipse.ditto.policies.model.SubjectIssuer;
3233

3334
/**
@@ -66,8 +67,15 @@ public static JwtSubjectIssuersConfig fromOAuthConfig(final OAuthConfig config)
6667
// merge the default and extension config
6768
Stream.concat(config.getOpenIdConnectIssuers().entrySet().stream(),
6869
config.getOpenIdConnectIssuersExtension().entrySet().stream())
69-
.map(entry -> new JwtSubjectIssuerConfig(entry.getKey(), entry.getValue().getIssuers(),
70-
entry.getValue().getAuthorizationSubjectTemplates()))
70+
.map(entry -> {
71+
final SubjectIssuerConfig issuerConfig = entry.getValue();
72+
return new JwtSubjectIssuerConfig(
73+
entry.getKey(),
74+
issuerConfig.getIssuers(),
75+
issuerConfig.getAuthorizationSubjectTemplates(),
76+
issuerConfig.getInjectClaimsIntoHeaders()
77+
);
78+
})
7179
.collect(Collectors.toSet());
7280
return new JwtSubjectIssuersConfig(configItems, config.getProtocol());
7381
}

0 commit comments

Comments
 (0)