diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/SecureTokenControllerApiDoc.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/SecureTokenControllerApiDoc.java new file mode 100644 index 000000000..266f9be54 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/SecureTokenControllerApiDoc.java @@ -0,0 +1,126 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 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.managedidentitywallets.apidocs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public class SecureTokenControllerApiDoc { + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @RequestBody(content = { + @Content(examples = { + @ExampleObject(name = "Request Secure Token using Scopes", value = """ + { + "audience": "BPNL000000000009", + "client_id": "your_client_id", + "client_secret": "your_client_secret", + "grant_type": "client_credentials", + "bearer_access_scope": "org.eclipse.tractusx.vc.type:ValidCredentialType:read" + } + """ + ), + @ExampleObject(name = "Request Secure Token using Access Token", value = """ + { + "audience": "BPNL000000000009", + "client_id": "your_client_id", + "client_secret": "your_client_secret", + "grant_type": "client_credentials", + "access_token": "a_jwt_token" + } + """ + ) + }) + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", content = { + @Content(examples = { + @ExampleObject(name = "Success response", value = """ + { + "token": "a_jwt_token", + "expiresAt": 1706888709315 + } + """ + ) + }) + }), + + @ApiResponse(responseCode = "400", content = { + @Content(examples = { + @ExampleObject(name = "Unknown BPN", value = """ + { + "error": "UnknownBusinessPartnerNumber", + "errorDescription": "The provided BPN 'BPNL000000000001' is unknown" + } + """ + ), + + @ExampleObject(name = "Wrong Grant Type", value = """ + { + "error": "UnsupportedGrantTypeException", + "errorDescription": "The provided 'grant_type' is not valid. Use 'client_credentials'." + } + """ + ), + + @ExampleObject(name = "Invalid Secure Token Request", value = """ + { + "error": "InvalidSecureTokenRequest", + "errorDescription": "The provided data could not be used to create and sign a token." + } + """ + ) + }) + }), + + @ApiResponse(responseCode = "500", description = "Any other internal server error", content = { + @Content(examples = { + @ExampleObject(name = "Internal server error", value = """ + { + "type": "about:blank", + "title": "Error Title", + "status": 500, + "detail": "Error Details", + "instance": "API endpoint", + "properties": { + "timestamp": 1689762476720 + } + } + """ + ) + }) + }) + }) + @Operation(summary = "Create and Sign Access Tokens", description = "The endpoint for creating and signing access tokens which are to be used during a verifiable presentation flow.") + public @interface PostSecureTokenDoc { + } +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java index aa3eafe0c..5f3dcd368 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java @@ -65,12 +65,15 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) - .headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer.xssProtection(Customizer.withDefaults()).contentSecurityPolicy(contentSecurityPolicyConfig -> contentSecurityPolicyConfig.policyDirectives("script-src 'self'"))) + .headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer + .xssProtection(Customizer.withDefaults()) + .contentSecurityPolicy(contentSecurityPolicyConfig -> contentSecurityPolicyConfig.policyDirectives("script-src 'self'"))) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests.requestMatchers(new AntPathRequestMatcher("/")).permitAll() // forwards to swagger .requestMatchers(new AntPathRequestMatcher("/docs/api-docs/**")).permitAll() .requestMatchers(new AntPathRequestMatcher("/ui/swagger-ui/**")).permitAll() .requestMatchers(new AntPathRequestMatcher("/actuator/health/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/token", POST.name())).permitAll() .requestMatchers(new AntPathRequestMatcher("/actuator/loggers/**")).hasRole(ApplicationRole.ROLE_MANAGE_APP) //did document resolve APIs diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java new file mode 100644 index 000000000..2359d0b0f --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java @@ -0,0 +1,110 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 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.managedidentitywallets.controller; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.apidocs.SecureTokenControllerApiDoc; +import org.eclipse.tractusx.managedidentitywallets.domain.BusinessPartnerNumber; +import org.eclipse.tractusx.managedidentitywallets.domain.IdpTokenResponse; +import org.eclipse.tractusx.managedidentitywallets.domain.StsTokenErrorResponse; +import org.eclipse.tractusx.managedidentitywallets.domain.StsTokenResponse; +import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequest; +import org.eclipse.tractusx.managedidentitywallets.exception.InvalidSecureTokenRequest; +import org.eclipse.tractusx.managedidentitywallets.exception.UnknownBusinessPartnerNumber; +import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedGrantTypeException; +import org.eclipse.tractusx.managedidentitywallets.interfaces.SecureTokenService; +import org.eclipse.tractusx.managedidentitywallets.service.IdpAuthorization; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Set; + +@RestController +@Slf4j +@RequiredArgsConstructor +@Tag(name = "STS") +public class SecureTokenController { + + private final SecureTokenService tokenService; + + private final IdpAuthorization idpAuthorization; + + @SneakyThrows + @PostMapping(path = "/token", consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE }) + @SecureTokenControllerApiDoc.PostSecureTokenDoc + public ResponseEntity store( + @Valid @RequestBody SecureTokenRequest secureTokenRequest + ) { + // handle idp authorization + IdpTokenResponse idpResponse = idpAuthorization.fromSecureTokenRequest(secureTokenRequest); + BusinessPartnerNumber bpn = idpResponse.bpn(); + // todo bri: accept did & bpn + BusinessPartnerNumber partnerBpn = new BusinessPartnerNumber(secureTokenRequest.getAudience()); + + // create the SI token and put/create the access_token inside + JWT responseJwt; + if (secureTokenRequest.assertValidWithAccessToken()) { + log.debug("Signing si token."); + responseJwt = tokenService.issueToken( + bpn, + partnerBpn, + JWTParser.parse(secureTokenRequest.getAccessToken()) + ); + } else if (secureTokenRequest.assertValidWithScopes()) { + log.debug("Creating access token and signing si token."); + responseJwt = tokenService.issueToken( + bpn, + partnerBpn, + Set.of(secureTokenRequest.getBearerAccessScope()) + ); + } else { + throw new InvalidSecureTokenRequest("The provided data could not be used to create and sign a token."); + } + + // create the response + log.debug("Preparing StsTokenResponse."); + StsTokenResponse response = StsTokenResponse.builder() + .token(responseJwt.serialize()) + .expiresAt(responseJwt.getJWTClaimsSet().getExpirationTime().getTime()) + .build(); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @ExceptionHandler({ UnsupportedGrantTypeException.class, InvalidSecureTokenRequest.class, UnknownBusinessPartnerNumber.class }) + public ResponseEntity getErrorResponse(RuntimeException e) { + StsTokenErrorResponse response = new StsTokenErrorResponse(); + response.setError(e.getClass().getSimpleName()); + response.setErrorDescription(e.getMessage()); + return ResponseEntity.badRequest().body(response); + } +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/entity/Wallet.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/entity/Wallet.java index dead498f3..0df48ea6c 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/entity/Wallet.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/entity/Wallet.java @@ -23,8 +23,20 @@ import com.fasterxml.jackson.annotation.JsonIgnore; -import jakarta.persistence.*; -import lombok.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Transient; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.eclipse.tractusx.managedidentitywallets.utils.StringToDidDocumentConverter; import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/interfaces/SecureTokenIssuer.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/interfaces/SecureTokenIssuer.java new file mode 100644 index 000000000..8a54f5f37 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/interfaces/SecureTokenIssuer.java @@ -0,0 +1,35 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 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.managedidentitywallets.interfaces; + +import com.nimbusds.jwt.JWT; +import org.eclipse.tractusx.managedidentitywallets.domain.DID; +import org.eclipse.tractusx.managedidentitywallets.domain.KeyPair; + +import java.time.Instant; +import java.util.Set; + +public interface SecureTokenIssuer { + JWT createAccessToken(KeyPair keyPair, DID self, DID partner, Instant expirationTime, Set scopes); + + JWT createIdToken(KeyPair keyPair, DID self, DID partner, Instant expirationTime, JWT accessToken); +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/interfaces/SecureTokenService.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/interfaces/SecureTokenService.java new file mode 100644 index 000000000..22708870a --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/interfaces/SecureTokenService.java @@ -0,0 +1,39 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 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.managedidentitywallets.interfaces; + +import org.eclipse.tractusx.managedidentitywallets.domain.BusinessPartnerNumber; +import org.eclipse.tractusx.managedidentitywallets.domain.DID; + +import com.nimbusds.jwt.JWT; + +import java.util.Set; + +public interface SecureTokenService { + JWT issueToken(DID self, DID partner, Set scopes); + + JWT issueToken(BusinessPartnerNumber self, BusinessPartnerNumber partner, Set scopes); + + JWT issueToken(DID self, DID partner, JWT accessToken); + + JWT issueToken(BusinessPartnerNumber self, BusinessPartnerNumber partner, JWT accessToken); +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IdpAuthorization.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IdpAuthorization.java new file mode 100644 index 000000000..ece9ff162 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IdpAuthorization.java @@ -0,0 +1,84 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 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.managedidentitywallets.service; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.tractusx.managedidentitywallets.config.security.SecurityConfigProperties; +import org.eclipse.tractusx.managedidentitywallets.domain.IdpTokenResponse; +import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequest; +import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedGrantTypeException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.CLIENT_CREDENTIALS; +import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; +import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_SECRET; +import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.GRANT_TYPE; +import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; +import static org.springframework.security.oauth2.core.oidc.OidcScopes.OPENID; + +@Service +@Slf4j +public class IdpAuthorization { + + private final RestTemplate rest; + + @Autowired + public IdpAuthorization(final SecurityConfigProperties properties, final RestTemplateBuilder restTemplateBuilder) { + String authServerUrl = properties.authServerUrl(); + if (StringUtils.endsWith(authServerUrl, "/")) { + authServerUrl = authServerUrl.substring(0, authServerUrl.length() - 1); + } + String idpRootUrl = authServerUrl + "/realms/" + properties.realm(); + this.rest = restTemplateBuilder + .rootUri(idpRootUrl) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .build(); + } + + public IdpTokenResponse fromSecureTokenRequest(SecureTokenRequest secureTokenRequest) throws UnsupportedGrantTypeException { + // we're ignoring the input, but the protocol requires us to check. + if (!secureTokenRequest.getGrantType().equals(CLIENT_CREDENTIALS)) { + throw new UnsupportedGrantTypeException("The provided 'grant_type' is not valid. Use 'client_credentials'."); + } + MultiValueMap tokenRequest = new LinkedMultiValueMap<>(); + tokenRequest.add(GRANT_TYPE, CLIENT_CREDENTIALS); + tokenRequest.add(SCOPE, OPENID); + tokenRequest.add(CLIENT_ID, secureTokenRequest.getClientId()); + tokenRequest.add(CLIENT_SECRET, secureTokenRequest.getClientSecret()); + log.debug("OAuth Token request for '{}' during secure token request flow.", secureTokenRequest.getClientId()); + IdpTokenResponse idpResponse = rest.postForObject( + "/protocol/openid-connect/token", + tokenRequest, + IdpTokenResponse.class + ); + assert idpResponse != null; + return idpResponse; + } +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/SecureTokenServiceImpl.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/SecureTokenServiceImpl.java new file mode 100644 index 000000000..c609cd5de --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/SecureTokenServiceImpl.java @@ -0,0 +1,103 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 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.managedidentitywallets.service; + +import com.nimbusds.jwt.JWT; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; +import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletKeyRepository; +import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; +import org.eclipse.tractusx.managedidentitywallets.domain.BusinessPartnerNumber; +import org.eclipse.tractusx.managedidentitywallets.domain.DID; +import org.eclipse.tractusx.managedidentitywallets.domain.KeyPair; +import org.eclipse.tractusx.managedidentitywallets.exception.UnknownBusinessPartnerNumber; +import org.eclipse.tractusx.managedidentitywallets.interfaces.SecureTokenIssuer; +import org.eclipse.tractusx.managedidentitywallets.interfaces.SecureTokenService; +import org.eclipse.tractusx.managedidentitywallets.sts.SecureTokenConfigurationProperties; + +import java.time.Instant; +import java.util.Optional; +import java.util.Set; + +@Slf4j +@RequiredArgsConstructor +public class SecureTokenServiceImpl implements SecureTokenService { + + private final WalletKeyRepository walletKeyRepository; + + private final WalletRepository walletRepository; + + private final SecureTokenIssuer tokenIssuer; + + private final SecureTokenConfigurationProperties properties; + + @Override + public JWT issueToken(final DID self, final DID partner, final Set scopes) { + log.debug("'issueToken' using scopes and DID."); + KeyPair keyPair = walletKeyRepository.findFirstByWallet_Did(self.toString()).toDto(); + // IMPORTANT: we re-use the expiration time intentionally to mitigate any kind of timing attacks, + // as we're signing two tokens. + Instant expirationTime = Instant.now().plus(properties.tokenDuration()); + JWT accessToken = this.tokenIssuer.createAccessToken(keyPair, self, partner, expirationTime, scopes); + return this.tokenIssuer.createIdToken(keyPair, self, partner, expirationTime, accessToken); + } + + @Override + public JWT issueToken(DID self, DID partner, JWT accessToken) { + log.debug("'issueToken' using an access_token and DID."); + KeyPair keyPair = walletKeyRepository.findFirstByWallet_Did(self.toString()).toDto(); + Instant expirationTime = Instant.now().plus(properties.tokenDuration()); + return this.tokenIssuer.createIdToken(keyPair, self, partner, expirationTime, accessToken); + } + + @Override + public JWT issueToken(BusinessPartnerNumber self, BusinessPartnerNumber partner, Set scopes) { + log.debug("'issueToken' using scopes and BPN."); + WalletKey walletKey = Optional.of(walletKeyRepository.findFirstByWallet_Bpn(self.toString())) + .orElseThrow(() -> new UnknownBusinessPartnerNumber(String.format("The provided BPN '%s' is unknown", self))); + KeyPair keyPair = walletKey.toDto(); + DID selfDid = new DID(walletKey.getWallet().getDid()); + DID partnerDid = new DID(Optional.ofNullable(walletRepository.getByBpn(partner.toString())) + .orElseThrow(() -> new UnknownBusinessPartnerNumber(String.format("The provided BPN '%s' is unknown", partner))) + .getDid()); + Instant expirationTime = Instant.now().plus(properties.tokenDuration()); + // IMPORTANT: we re-use the expiration time intentionally to mitigate any kind of timing attacks, + // as we're signing two tokens. + JWT accessToken = this.tokenIssuer.createAccessToken(keyPair, selfDid, partnerDid, expirationTime, scopes); + return this.tokenIssuer.createIdToken(keyPair, selfDid, partnerDid, expirationTime, accessToken); + } + + @Override + public JWT issueToken(BusinessPartnerNumber self, BusinessPartnerNumber partner, JWT accessToken) { + log.debug("'issueToken' using an access_token and BPN."); + WalletKey walletKey = Optional.ofNullable(walletKeyRepository.findFirstByWallet_Bpn(self.toString())) + .orElseThrow(() -> new UnknownBusinessPartnerNumber(String.format("The provided BPN '%s' is unknown", self))); + KeyPair keyPair = walletKey.toDto(); + DID selfDid = new DID(walletKey.getWallet().getDid()); + DID partnerDid = new DID(Optional.of(walletRepository.getByBpn(partner.toString())) + .orElseThrow(() -> new UnknownBusinessPartnerNumber(String.format("The provided BPN '%s' is unknown", partner))) + .getDid()); + Instant expirationTime = Instant.now().plus(properties.tokenDuration()); + return this.tokenIssuer.createIdToken(keyPair, selfDid, partnerDid, expirationTime, accessToken); + } +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/SecureTokenBeanConfig.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/SecureTokenBeanConfig.java new file mode 100644 index 000000000..241799249 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/SecureTokenBeanConfig.java @@ -0,0 +1,45 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 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.managedidentitywallets.sts; + +import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletKeyRepository; +import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; +import org.eclipse.tractusx.managedidentitywallets.interfaces.SecureTokenIssuer; +import org.eclipse.tractusx.managedidentitywallets.interfaces.SecureTokenService; +import org.eclipse.tractusx.managedidentitywallets.service.SecureTokenServiceImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SecureTokenBeanConfig { + + @Bean + public SecureTokenService secureTokenService( + WalletKeyRepository keyRepository, + WalletRepository walletRepository, + SecureTokenIssuer issuer, + SecureTokenConfigurationProperties properties + ) { + return new SecureTokenServiceImpl(keyRepository, walletRepository, issuer, properties); + } + +} diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/SecureTokenIssuerImpl.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/SecureTokenIssuerImpl.java new file mode 100644 index 000000000..4afe4bef2 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/SecureTokenIssuerImpl.java @@ -0,0 +1,110 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 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.managedidentitywallets.sts; + +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.Ed25519Signer; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.domain.DID; +import org.eclipse.tractusx.managedidentitywallets.domain.KeyPair; +import org.eclipse.tractusx.managedidentitywallets.interfaces.SecureTokenIssuer; +import org.eclipse.tractusx.managedidentitywallets.utils.EncryptionUtils; +import org.eclipse.tractusx.ssi.lib.crypt.octet.OctetKeyPairFactory; +import org.eclipse.tractusx.ssi.lib.crypt.x21559.x21559PrivateKey; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Date; +import java.util.Set; +import java.util.UUID; + +import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.ACCESS_TOKEN; +import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SecureTokenIssuerImpl implements SecureTokenIssuer { + + private final EncryptionUtils encryptionUtils; + + @Override + public JWT createIdToken(KeyPair keyPair, DID self, DID partner, Instant expirationTime, JWT accessToken) { + log.debug("'createIdToken' using a provided access_token."); + return createSignedJWT(keyPair, new JWTClaimsSet.Builder() + .issuer(self.toString()) + .audience(partner.toString()) + .subject(self.toString()) + .expirationTime(Date.from(expirationTime)) + .claim(ACCESS_TOKEN, accessToken.serialize())); + } + + @Override + public JWT createAccessToken(KeyPair keyPair, DID self, DID partner, Instant expirationTime, Set scopes) { + log.debug("'createAccessToken' using scopes."); + return createSignedJWT(keyPair, new JWTClaimsSet.Builder() + .issuer(self.toString()) + .audience(self.toString()) + .subject(partner.toString()) + .expirationTime(Date.from(expirationTime)) + .claim(SCOPE, String.join(" ", scopes))); + } + + @SneakyThrows + private JWT createSignedJWT(KeyPair keyPair, JWTClaimsSet.Builder builder) { + log.debug("Creating JWS header for issuer '{}' and holder '{}'", builder.getClaims().get("iss"), + builder.getClaims().get("sub")); + // todo bri: we're hard-coding the algorithm for now. Should become dynamic in the future. + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.EdDSA) + .type(JOSEObjectType.JWT) + .keyID(keyPair.keyId()) + .build(); + + log.debug("Creating JWS body for issuer '{}' and holder '{}'", builder.getClaims().get("iss"), + builder.getClaims().get("sub")); + JWTClaimsSet body = builder + .issueTime(Date.from(Instant.now())) + .jwtID(UUID.randomUUID().toString()) + .build(); + + log.debug("Creating JWS signature for issuer '{}' and holder '{}'", builder.getClaims().get("iss"), + builder.getClaims().get("sub")); + SignedJWT signedJWT = new SignedJWT(header, body); + String privateKey = encryptionUtils.decrypt(keyPair.privateKey()); + // todo bri: this should become dynamic in the future, as we want to support more key algos. + OctetKeyPair jwk = new OctetKeyPairFactory().fromPrivateKey(new x21559PrivateKey(privateKey, true)); + Ed25519Signer signer = new Ed25519Signer(jwk); + signedJWT.sign(signer); + log.debug("JWT signed for issuer '{}' and holder '{}'", builder.getClaims().get("iss"), + builder.getClaims().get("sub")); + + return signedJWT; + } +}