Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/tri 1611 make oauth claim configurable #579

Merged
merged 5 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- Deprecated query parameter 'jobStates' was removed from GET {{IRS_HOST}}/irs/jobs endpoint
- Moved OAuth2 JWT token claim to configuration. The fields can be configured with `oauth.resourceClaim`, `oauth.irsNamespace`, `oauth.roles`.

## [3.5.3] - 2023-10-09
### Fixed
Expand Down
2 changes: 2 additions & 0 deletions charts/irs-helm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Added configuration for IRS OAuth2 JWT token claim

## [6.8.0] - 2023-10-09
### Changed
Expand Down
5 changes: 5 additions & 0 deletions charts/irs-helm/templates/configmap-spring-app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ data:

apiAllowedBpn: {{ tpl (.Values.bpn | default "") . | quote }}

oauth:
resourceClaim: {{ tpl (.Values.oauth.resourceClaim | default "resource_access") . | quote }}
irsNamespace: {{ tpl (.Values.oauth.irsNamespace | default "") . | quote }}
roles: {{ tpl (.Values.oauth.roles | default "roles") . | quote }}

{{- if .Values.config.content }}
{{- tpl (toYaml .Values.config.content) . | nindent 4 }}
{{- end }}
5 changes: 5 additions & 0 deletions charts/irs-helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ ess:
mockRecursiveEdcAsset: # List of BPNs for which the special, mocked notification asset should be used
managementPath: "/management/v2" # EDC management API path - used for notification asset creation

oauth:
resourceClaim: "resource_access" # Name of the JWT claim for roles
irsNamespace: "Cl20-CX-IRS" # Namespace for the IRS roles
roles: "roles" # Name of the list of roles within the IRS namespace

config:
# If true, the config provided below will completely replace the configmap.
# In this case, you need to provide all required config values defined above yourself!
Expand Down
59 changes: 53 additions & 6 deletions docs/src/docs/arc42/cross-cutting/safety-security.adoc
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
= Safety and security concepts

== Authentication / Authorization

=== IRS API
The IRS is secured using OAuth2.0 / Open ID Connect. Every request to the IRS API requires a valid bearer token.

The IRS is secured using OAuth2.0 / Open ID Connect.
Every request to the IRS API requires a valid bearer token.
JWT token should also contain two claims:

- 'bpn' which is equal to the configuration value from `API_ALLOWED_BPN` property
- 'resource_access' with the specific 'Cl20-CX-IRS' key for C-X environments. The list of values will be converted to roles by IRS. Currently, IRS API handles two roles: **'admin_irs'** and **'view_irs'.** A valid token with the **'admin_irs'** role can access any endpoint exposed by the IRS API, while a token with the **'view_irs'** role does not have access to policies endpoints and can operate only on resources it owns. That means that he only has access to the resources he has created, e.g. jobs and batches. This behavior is shown in the table below.
- 'resource_access' with the specific 'Cl20-CX-IRS' key for C-X environments. (The keys are configurable. For more details see chapter "IRS OAuth2 JWT Token").
The list of values will be converted to roles by IRS.
Currently, IRS API handles two roles: **'admin_irs'** and **'view_irs'.** A valid token with the **'admin_irs'** role can access any endpoint exposed by the IRS API, while a token with the **'view_irs'** role does not have access to policies endpoints and can operate only on resources it owns.
That means that he only has access to the resources he has created, e.g. jobs and batches.
This behavior is shown in the table below.

==== Rights and Roles Matrix of IRS

Expand All @@ -30,20 +37,60 @@ Social Standards | Register investigation job | POST /ess/bpn/investigations
| | Get investigation job | GET /ess/bpn/investigations{id} | (x) | x
| | Accept notifications | POST /ess/notification/receive | x | x
|===
Legend: x = full access to all resources, (x) = access to the resources he owns

Legend: x = full access to all resources, (x) = access to the resources he owns

=== IRS OAuth2 JWT Token

IRS expects the JWT access token to have the following structure to be able to extract role information:

[source,json]
----
{
...
"resource_access": {
"Cl20-CX-IRS": {
"roles": [
"view_irs",
"admin_irs"
]
}
},
...
}
----

The field names can be configured via application.yaml:

[source,yaml]
----
# OAuth2 JWT token parse config. This configures the structure IRS expects when parsing the IRS role of an access token.
oauth:
resourceClaim: "resource_access" # Name of the JWT claim for roles
irsNamespace: "Cl20-CX-IRS" # Namespace for the IRS roles
roles: "roles" # Name of the list of roles within the IRS namespace
----

=== IRS as DTR client
The IRS acts as a client for the Digital Twin Registry (DTR), which is also secured using OAuth2.0 / Open ID Connect. The IRS uses client credentials to authenticate requests to the DTR. Due to this, the IRS account needs to have access to every item in the DTR, unrelated to the permissions of the account calling the IRS API.

The IRS acts as a client for the Digital Twin Registry (DTR), which is also secured using OAuth2.0 / Open ID Connect.
The IRS uses client credentials to authenticate requests to the DTR.
Due to this, the IRS account needs to have access to every item in the DTR, unrelated to the permissions of the account calling the IRS API.

=== IRS as decentralized DTR client
In a decentralized network, IRS uses the EDC client to access the provider DTR. This way, no authentication, other than the EDC contract negotiation, is needed to access the DTR.

In a decentralized network, IRS uses the EDC client to access the provider DTR.
This way, no authentication, other than the EDC contract negotiation, is needed to access the DTR.

=== IRS as EDC client
The IRS accesses the Catena-X network via the EDC consumer connector. This component requires authentication via a Verifiable Credential (VC), which is provided to the EDC via the Managed Identity Wallet.

The IRS accesses the Catena-X network via the EDC consumer connector.
This component requires authentication via a Verifiable Credential (VC), which is provided to the EDC via the Managed Identity Wallet.

The VC identifies and authenticates the EDC and is used to acquire access permissions for the data transferred via EDC.

== Credentials

Credentials must never be stored in Git!


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
import java.util.List;

import org.eclipse.tractusx.irs.common.ApiConstants;
import org.eclipse.tractusx.irs.configuration.converter.IrsTokenParser;
import org.eclipse.tractusx.irs.configuration.converter.JwtAuthenticationConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
Expand All @@ -38,6 +40,7 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter;
import org.springframework.security.web.header.writers.PermissionsPolicyHeaderWriter;
Expand Down Expand Up @@ -76,7 +79,8 @@ public class SecurityConfiguration {

@SuppressWarnings("PMD.SignatureDeclareThrowsException")
@Bean
/* package */ SecurityFilterChain securityFilterChain(final HttpSecurity httpSecurity) throws Exception {
/* package */ SecurityFilterChain securityFilterChain(final HttpSecurity httpSecurity,
final JwtAuthenticationConverter jwtAuthenticationConverter) throws Exception {
httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
httpSecurity.csrf(AbstractHttpConfigurer::disable);
Expand Down Expand Up @@ -111,7 +115,7 @@ public class SecurityConfiguration {

httpSecurity.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.jwt(jwt ->
jwt.jwtAuthenticationConverter(new JwtAuthenticationConverter())))
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter)))
.oauth2Client(Customizer.withDefaults());

return httpSecurity.build();
Expand All @@ -131,4 +135,15 @@ public class SecurityConfiguration {
return source;
}

@Bean
/* package */ IrsTokenParser irsTokenParser(@Value("${oauth.resourceClaim}") final String resourceAccessClaim,
@Value("${oauth.irsNamespace}") final String irsResourceAccess,
@Value("${oauth.roles}") final String roles) {
return new IrsTokenParser(resourceAccessClaim, irsResourceAccess, roles);
}

@Bean
/* package */ JwtAuthenticationConverter jwtAuthenticationConverter(final IrsTokenParser irsTokenParser) {
return new JwtAuthenticationConverter(new JwtGrantedAuthoritiesConverter(), irsTokenParser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/********************************************************************************
* Copyright (c) 2021,2022,2023
* 2022: ZF Friedrichshafen AG
* 2022: ISTOS GmbH
* 2022,2023: Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
* 2022,2023: BOSCH AG
* Copyright (c) 2021,2022,2023 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 Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
package org.eclipse.tractusx.irs.configuration.converter;

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;

/**
* Parsing JWT - retrieving resource_access claim with IRS roles.
*/
@Slf4j
@AllArgsConstructor
public class IrsTokenParser {

private String resourceAccessClaim;
private String irsResourceAccess;
private String roles;

/**
* Parsing JWT - retrieving resource_access claim with IRS roles.
*
* @param jwt source
* @return set of roles from token
*/
public Set<SimpleGrantedAuthority> extractIrsRolesFromToken(final Jwt jwt) {
return Optional.ofNullable(jwt.getClaim(resourceAccessClaim))
.map(LinkedTreeMap.class::cast)
.map(accesses -> accesses.get(irsResourceAccess))
.map(LinkedTreeMap.class::cast)
.map(irsAccesses -> irsAccesses.get(roles))
.map(irsRoles -> ((List<String>) irsRoles).stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet()))
.orElse(Collections.emptySet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,26 @@
package org.eclipse.tractusx.irs.configuration.converter;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
import lombok.extern.slf4j.Slf4j;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

/**
* JWT Converter
*/
@AllArgsConstructor
public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final IrsTokenParser irsTokenParser = new IrsTokenParser();
private JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter;
private IrsTokenParser irsTokenParser;

@Override
public AbstractAuthenticationToken convert(final @NotNull Jwt source) {
Expand All @@ -60,35 +55,5 @@ public AbstractAuthenticationToken convert(final @NotNull Jwt source) {

return new JwtAuthenticationToken(source, authorities);
}

/**
* Parsing JWT - retrieving resource_access claim with IRS roles.
*/
@Slf4j
/* package */ static class IrsTokenParser {

private static final String RESOURCE_ACCESS_CLAIM = "resource_access";
private static final String IRS_RESOURCE_ACCESS = "Cl20-CX-IRS";
private static final String ROLES = "roles";

/**
* Parsing JWT - retrieving resource_access claim with IRS roles.
*
* @param jwt source
* @return list of roles from token
*/
public Set<SimpleGrantedAuthority> extractIrsRolesFromToken(final Jwt jwt) {
return Optional.ofNullable(jwt.getClaim(RESOURCE_ACCESS_CLAIM))
.map(LinkedTreeMap.class::cast)
.map(accesses -> accesses.get(IRS_RESOURCE_ACCESS))
.map(LinkedTreeMap.class::cast)
.map(irsAccesses -> irsAccesses.get(ROLES))
.map(roles -> ((List<String>) roles).stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet()))
.orElse(Collections.emptySet());
}
}

}

6 changes: 6 additions & 0 deletions irs-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,9 @@ ess:
mockRecursiveEdcAsset: # Mocked BPN Recursive Investigation results

apiAllowedBpn: ${API_ALLOWED_BPN:BPNL00000001CRHK} # BPN value that is allowed to access IRS API

# OAuth2 JWT token parse config. This configures the structure IRS expects when parsing the IRS role of an access token.
oauth:
resourceClaim: "resource_access" # Name of the JWT claim for roles
irsNamespace: "Cl20-CX-IRS" # Namespace for the IRS roles
roles: "roles" # Name of the list of roles within the IRS namespace
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,34 @@
********************************************************************************/
package org.eclipse.tractusx.irs.configuration.converter;

import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;

import java.time.Instant;
import java.util.List;
import java.util.Map;

import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
import org.eclipse.tractusx.irs.common.auth.IrsRoles;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

class JwtAuthenticationConverterTest {

private final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
private JwtAuthenticationConverter jwtAuthenticationConverter;

@BeforeEach
void setUp() {
final String resourceAccessClaim = "resource_access";
final String irsResourceAccess = "Cl20-CX-IRS";
final String roles = "roles";
jwtAuthenticationConverter = new JwtAuthenticationConverter(new JwtGrantedAuthoritiesConverter(),
new IrsTokenParser(resourceAccessClaim, irsResourceAccess, roles));
}

@Test
void shouldParseJwtTokenAndFindViewIrsRole() {
Expand Down Expand Up @@ -98,9 +108,8 @@ void shouldParseJwtTokenAndNotFindIrsRolesWhenWrongRolesKey() {

Jwt jwt(final Map<String, Object> irsResourceAccess) {
final Map<String, Object> claims = new LinkedTreeMap<>();
claims.putAll(Map.of("resource_access", irsResourceAccess, SUB, "sub", "clientId", "clientId"));
claims.putAll(Map.of("resource_access", irsResourceAccess, SUB, "sub", "clientId", "clientId"));

return new Jwt("token", Instant.now(), Instant.now().plusSeconds(30), Map.of("alg", "none"),
claims);
return new Jwt("token", Instant.now(), Instant.now().plusSeconds(30), Map.of("alg", "none"), claims);
}
}