diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java index 4e88fea26..e76adc5fd 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java @@ -21,15 +21,27 @@ package org.eclipse.tractusx.managedidentitywallets.service; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.utils.TokenValidationUtils; import org.springframework.stereotype.Service; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + @Service @Slf4j @RequiredArgsConstructor public class STSTokenValidationService { + private final TokenValidationUtils tokenValidationUtils; + public static final String ACCESS_TOKEN = "access_token"; + /** * Validates SI token and Access token. * @@ -37,6 +49,62 @@ public class STSTokenValidationService { * @return boolean result of validation */ public boolean validateToken(String token) { - return true; + List errors = new ArrayList<>(); + + JWTClaimsSet claimsSI = getClaimsSet(token); + + tokenValidationUtils.checkIfIssuerEqualsSubject(claimsSI).ifPresent(errors::add); + tokenValidationUtils.checkTokenExpiry(claimsSI).ifPresent(errors::add); + tokenValidationUtils.checkIfSubjectValidAndEqualsDid(claimsSI).ifPresent(errors::add); + + Optional accessToken = getAccessToken(claimsSI); + if (accessToken.isPresent()) { + String accessTokenValue = accessToken.get(); + JWTClaimsSet claimsAT = getClaimsSet(accessTokenValue); + tokenValidationUtils.checkIfAudienceClaimsEquals(claimsSI, claimsAT).ifPresent(errors::add); + tokenValidationUtils.checkIfNonceClaimsEquals(claimsSI, claimsAT).ifPresent(errors::add); + } else { + errors.add("The '%s' claim must not be null.".formatted(ACCESS_TOKEN)); + } + + if (errors.isEmpty()) { + return true; + } else { + log.error(errors.toString()); + return false; + } + } + + /** + * Parses the token and gets claim set from it. + * + * @param token token in a String format + * @return the set of JWT claims + */ + private JWTClaimsSet getClaimsSet(String token) { + try { + SignedJWT tokenParsed = SignedJWT.parse(token); + return tokenParsed.getJWTClaimsSet(); + } catch (ParseException e) { + throw new BadDataException("Could not parse jwt token", e); + } + } + + /** + * Gets access token from SI token. + * + * @param claims set of claims of SI token + * @return the value of token + */ + private Optional getAccessToken(JWTClaimsSet claims) { + try { + String accessTokenValue = claims.getStringClaim(ACCESS_TOKEN); + if (accessTokenValue == null) { + return Optional.empty(); + } + return Optional.of(accessTokenValue); + } catch (ParseException e) { + throw new BadDataException("Could not parse jwt token", e); + } } } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtils.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtils.java new file mode 100644 index 000000000..608c0aa50 --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtils.java @@ -0,0 +1,130 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,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.managedidentitywallets.utils; + +import com.nimbusds.jwt.JWTClaimsSet; +import lombok.RequiredArgsConstructor; +import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.service.DidDocumentService; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.text.ParseException; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static java.time.ZoneOffset.UTC; + +/** + * Methods for validating token claims. + */ +@Component +@RequiredArgsConstructor +public class TokenValidationUtils { + + private final DidDocumentService service; + + public static final String NONCE = "nonce"; + public static final String DID_FORMAT = "did:"; + private static final int MAX_TOKEN_AGE = 60; + + public Optional checkIfIssuerEqualsSubject(JWTClaimsSet claims) { + String iss = claims.getIssuer(); + String sub = claims.getSubject(); + if (!(iss != null && Objects.equals(iss, sub))) { + return Optional.of("The 'iss' and 'sub' claims must be non-null and identical."); + } + return Optional.empty(); + } + + public Optional checkIfSubjectValidAndEqualsDid(JWTClaimsSet claims) { + String sub = claims.getSubject(); + if ((sub != null && sub.startsWith(DID_FORMAT))) { + URI id = service.getDidDocument(sub).getId(); + if (!(id != null && Objects.equals(id.toString(), sub))) { + return Optional.of("The 'sub' claim must be identical to the id of existing DID document."); + } + return Optional.empty(); + } + return Optional.of("The 'sub' claim must be in did format."); + } + + public Optional checkTokenExpiry(JWTClaimsSet claims) { + Instant now = Instant.now(); + Date expires = claims.getExpirationTime(); + if (expires == null) { + return Optional.of("Required expiration time (exp) claim is missing in token"); + } else if (now.isAfter(convertDateToUtcTime(expires))) { + return Optional.of("Token has expired (exp)"); + } + + Date issuedAt = claims.getIssueTime(); + if (issuedAt != null) { + Instant issuedAtInst = convertDateToUtcTime(issuedAt); + if (issuedAtInst.isAfter(convertDateToUtcTime(expires))) { + return Optional.of("Issued at (iat) claim is after expiration time (exp) claim in token"); + } else if (now.plusSeconds(MAX_TOKEN_AGE).isBefore(issuedAtInst)) { + return Optional.of("Current date/time before issued at (iat) claim in token"); + } + } + return Optional.empty(); + } + + private Instant convertDateToUtcTime(Date date) { + return date.toInstant().atOffset(UTC).toInstant(); + } + + public Optional checkIfAudienceClaimsEquals(JWTClaimsSet claimsSI, JWTClaimsSet claimsAT) { + List audienceSI = claimsSI.getAudience(); + List audienceAccess = claimsAT.getAudience(); + if (!(audienceSI.isEmpty() && audienceAccess.isEmpty())) { + String audSI = audienceSI.get(0); + String audAT = audienceAccess.get(0); + if (!(audSI.equals(audAT))) { + return Optional.of("The 'aud' claims must be equals in SI and Access tokens."); + } + return Optional.empty(); + } else { + return Optional.of("The 'aud' claim must not be empty."); + } + } + + public Optional checkIfNonceClaimsEquals(JWTClaimsSet claimsSI, JWTClaimsSet claimsAT) { + try { + String nonceSI = claimsSI.getStringClaim(NONCE); + String nonceAccess = claimsAT.getStringClaim(NONCE); + if (!(nonceSI == null) && !(nonceAccess == null)) { + if (!(nonceSI.equals(nonceAccess))) { + return Optional.of("The 'nonce' claims must be equals in SI and Access tokens."); + } + return Optional.empty(); + } else { + return Optional.of("The 'nonce' claim must not be empty."); + } + } catch (ParseException e) { + throw new BadDataException("Could not parse 'nonce' claim in token", e); + } + } +}