From 5ae223d61f23e71bf426d36ea3255f508abd254b Mon Sep 17 00:00:00 2001 From: andreibogus Date: Mon, 12 Feb 2024 16:08:49 +0100 Subject: [PATCH] feat: add JWT verification and extend tests --- .../config/security/SecurityConfig.java | 2 + .../service/STSTokenValidationService.java | 59 ++++++++---- .../utils/CompositDidResolver.java | 71 ++++++++++++++ .../utils/CustomSignedJWTVerifier.java | 96 +++++++++++++++++++ .../STSTokenValidationServiceTest.java | 40 +++++++- .../utils/TestConstants.java | 2 +- 6 files changed, 245 insertions(+), 25 deletions(-) create mode 100644 src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CompositDidResolver.java create mode 100644 src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CustomSignedJWTVerifier.java 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 b74541da4..e51cda6f1 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 @@ -73,6 +73,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .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("/api/wallets/validate", POST.name())).permitAll() .requestMatchers(new AntPathRequestMatcher("/api/token", POST.name())).permitAll() .requestMatchers(new AntPathRequestMatcher("/actuator/loggers/**")).hasRole(ApplicationRole.ROLE_MANAGE_APP) 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 e045652c9..633f91bfd 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,18 @@ package org.eclipse.tractusx.managedidentitywallets.service; -import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jose.JOSEException; import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.JWTClaimsSet; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.utils.CustomSignedJWTVerifier; 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; @@ -39,6 +42,8 @@ @RequiredArgsConstructor public class STSTokenValidationService { + private final DidDocumentResolverService didDocumentResolverService; + private final CustomSignedJWTVerifier customSignedJWTverifier; private final TokenValidationUtils tokenValidationUtils; private static final String ACCESS_TOKEN = "access_token"; @@ -50,19 +55,26 @@ public class STSTokenValidationService { */ public boolean validateToken(String token) { List errors = new ArrayList<>(); + SignedJWT jwtSI = parseToken(token); + JWTClaimsSet claimsSI = getClaimsSet(jwtSI); - JWTClaimsSet claimsSI = getClaimsSet(token); - + tokenValidationUtils.checkIfSubjectValidAndEqualsDid(claimsSI).ifPresent(errors::add); 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); + SignedJWT jwtAT = parseToken(accessToken.get()); + JWTClaimsSet claimsAT = getClaimsSet(jwtAT); + tokenValidationUtils.checkIfAudienceClaimsAreEqual(claimsSI, claimsAT).ifPresent(errors::add); tokenValidationUtils.checkIfNonceClaimsAreEqual(claimsSI, claimsAT).ifPresent(errors::add); + + String didForOuter = claimsAT.getAudience().get(0); + verifySignature(didForOuter, jwtSI).ifPresent(errors::add); + + String didForInner = claimsAT.getIssuer(); + verifySignature(didForInner, jwtAT).ifPresent(errors::add); } else { errors.add("The '%s' claim must not be null.".formatted(ACCESS_TOKEN)); } @@ -70,32 +82,27 @@ public boolean validateToken(String token) { if (errors.isEmpty()) { return true; } else { - log.error(errors.toString()); + log.debug(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) { + private JWTClaimsSet getClaimsSet(SignedJWT tokenParsed) { 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 SignedJWT parseToken(String token) { + try { + return SignedJWT.parse(token); + } catch (ParseException e) { + throw new BadDataException("Could not parse jwt token", e); + } + } + private Optional getAccessToken(JWTClaimsSet claims) { try { String accessTokenValue = claims.getStringClaim(ACCESS_TOKEN); @@ -104,4 +111,14 @@ private Optional getAccessToken(JWTClaimsSet claims) { throw new BadDataException("Could not parse jwt token", e); } } + + private Optional verifySignature(String did, SignedJWT signedJWT) { + try { + customSignedJWTverifier.setDidResolver(didDocumentResolverService.getCompositeDidResolver()); + return customSignedJWTverifier.verify(did, signedJWT) ? Optional.empty() + : Optional.of("Signature of jwt is not verified"); + } catch (JOSEException ex) { + throw new BadDataException("Can not verify signature of jwt", ex); + } + } } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CompositDidResolver.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CompositDidResolver.java new file mode 100644 index 000000000..49c645d7c --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CompositDidResolver.java @@ -0,0 +1,71 @@ +/* + * ******************************************************************************* + * 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.utils; + +import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; +import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolverException; +import org.eclipse.tractusx.ssi.lib.model.did.Did; +import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Component +public class CompositDidResolver implements DidResolver { + DidResolver[] didResolvers; + + public CompositDidResolver(DidResolver... didResolvers) { + this.didResolvers = didResolvers; + } + + public DidDocument resolve(Did did) throws DidResolverException { + DidResolver[] var2 = this.didResolvers; + int var3 = var2.length; + + for(int var4 = 0; var4 < var3; ++var4) { + DidResolver didResolver = var2[var4]; + if (didResolver.isResolvable(did)) { + try { + DidDocument result = didResolver.resolve(did); + if (result != null) { + return result; + } + } catch (DidResolverException var7) { + throw var7; + } catch (Throwable var8) { + throw new DidResolverException(String.format("Unrecognized exception: %s", var8.getClass().getName()), var8); + } + } + } + + return null; + } + + public boolean isResolvable(Did did) { + return Arrays.stream(this.didResolvers).anyMatch((resolver) -> resolver.isResolvable(did)); + } + + public static org.eclipse.tractusx.ssi.lib.did.resolver.CompositeDidResolver append(DidResolver target, DidResolver toBeAppended) { + return new org.eclipse.tractusx.ssi.lib.did.resolver.CompositeDidResolver(target, toBeAppended); + } +} + diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CustomSignedJWTVerifier.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CustomSignedJWTVerifier.java new file mode 100644 index 000000000..88f958eca --- /dev/null +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CustomSignedJWTVerifier.java @@ -0,0 +1,96 @@ +/* + * ******************************************************************************* + * 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.utils; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.crypto.Ed25519Verifier; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jwt.SignedJWT; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; +import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.service.DidDocumentService; +import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; +import org.eclipse.tractusx.ssi.lib.exception.UnsupportedVerificationMethodException; +import org.eclipse.tractusx.ssi.lib.model.MultibaseString; +import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; +import org.eclipse.tractusx.ssi.lib.model.did.Ed25519VerificationMethod; +import org.eclipse.tractusx.ssi.lib.model.did.JWKVerificationMethod; +import org.eclipse.tractusx.ssi.lib.model.did.VerificationMethod; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Data +public class CustomSignedJWTVerifier { + private DidResolver didResolver; + private final DidDocumentService didDocumentService; + public static final String KID = "kid"; + + public boolean verify(String did, SignedJWT jwt) throws JOSEException { + try { + VerificationMethod verificationMethod = checkVerificationMethod(did, jwt); + if (JWKVerificationMethod.isInstance(verificationMethod)) { + JWKVerificationMethod method = new JWKVerificationMethod(verificationMethod); + String kty = method.getPublicKeyJwk().getKty(); + String crv = method.getPublicKeyJwk().getCrv(); + String x = method.getPublicKeyJwk().getX(); + if (!kty.equals("OKP") || !crv.equals("Ed25519")) { + throw new UnsupportedVerificationMethodException(method, "only kty:OKP with crv:Ed25519 is supported"); + } + + OctetKeyPair keyPair = (new OctetKeyPair.Builder(Curve.Ed25519, Base64URL.from(x))).build(); + if (jwt.verify(new Ed25519Verifier(keyPair))) { + return true; + } + } else if (Ed25519VerificationMethod.isInstance(verificationMethod)) { + Ed25519VerificationMethod method = new Ed25519VerificationMethod(verificationMethod); + MultibaseString multibase = method.getPublicKeyBase58(); + Ed25519PublicKeyParameters publicKeyParameters = new Ed25519PublicKeyParameters(multibase.getDecoded(), 0); + OctetKeyPair keyPair = (new OctetKeyPair.Builder(Curve.Ed25519, Base64URL.encode(publicKeyParameters.getEncoded()))).build(); + if (jwt.verify(new Ed25519Verifier(keyPair))) { + return true; + } + } + } catch (JOSEException var15) { + throw var15; + } + return false; + } + + public VerificationMethod checkVerificationMethod(String did, SignedJWT jwt) { + Map headers = jwt.getHeader().toJSONObject(); + String kid = String.valueOf(headers.get(KID)); + DidDocument didDocument = didDocumentService.getDidDocument(did); + for (VerificationMethod method : didDocument.getVerificationMethods()) { + if (method.getId().toString().contains(kid)) { + return method; + } + } + throw new BadDataException("Verification method doesn't match 'kid' parameter"); + } +} diff --git a/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationServiceTest.java b/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationServiceTest.java index fd0802024..68f40caa2 100644 --- a/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationServiceTest.java +++ b/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationServiceTest.java @@ -22,7 +22,9 @@ package org.eclipse.tractusx.managedidentitywallets.service; import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; import com.nimbusds.jose.util.Base64URL; import com.nimbusds.jwt.JWTClaimsSet; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; @@ -40,9 +42,11 @@ import static com.nimbusds.jose.jwk.Curve.Ed25519; import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.BPN_1; +import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.BPN_2; import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.DID_BPN_1; import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.DID_BPN_2; import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.DID_JSON_STRING_1; +import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.DID_JSON_STRING_2; import static org.eclipse.tractusx.managedidentitywallets.utils.TestUtils.addAccessTokenToClaimsSet; import static org.eclipse.tractusx.managedidentitywallets.utils.TestUtils.buildClaimsSet; import static org.eclipse.tractusx.managedidentitywallets.utils.TestUtils.buildJWTToken; @@ -55,11 +59,13 @@ class STSTokenValidationServiceTest { private static final OctetKeyPair JWK_OUTER = new OctetKeyPair .Builder(Ed25519, new Base64URL("4Q5HCXPyutfcj7gLmbAKlYttlJPkykIkRjh7DH2NtZ0")) .d(new Base64URL("Ktp0sv9dKr_gnzRxpH5V9qpiTgZ1WbkMSv8WtWodewg")) + .keyID("58cb4b32-c2e4-46f0-a3ad-3286e34765ed") .build(); private static final OctetKeyPair JWK_INNER = new OctetKeyPair .Builder(Ed25519, new Base64URL("Z-8DEkN6pw2E01niDWqrp1kROLF-syIPIpFgmyrVUOU")) .d(new Base64URL("MLYxSai_oFzuqEfnB2diA3oDuixLg3kQzZKMyW31-2o")) + .keyID("58cb4b32-c2e4-46f0-a3ad-3286e34765ty") .build(); @Autowired @@ -97,12 +103,40 @@ void validateTokenFailureAccessTokenMissingTest() throws JOSEException { Assertions.assertFalse(isValid); } + @Test + void validateTokenFailureWrongSignatureInnerTokenTest() throws JOSEException { + + OctetKeyPair jwkRandom = new OctetKeyPairGenerator(Curve.Ed25519) + .keyID("58cb4b32-c2e4-46f0-a3ad-3286e34765ty") + .generate(); + + Wallet wallet1 = buildWallet(BPN_1, DID_BPN_1, DID_JSON_STRING_1); + walletRepository.save(wallet1); + + Wallet wallet2 = buildWallet(BPN_2, DID_BPN_2, DID_JSON_STRING_2); + walletRepository.save(wallet2); + + JWTClaimsSet innerSet = buildClaimsSet(DID_BPN_2, DID_BPN_1, DID_BPN_1, "123456", Long.parseLong("2559397136000")); + String accessToken = buildJWTToken(jwkRandom, innerSet); + + JWTClaimsSet outerSet = buildClaimsSet(DID_BPN_1, DID_BPN_1, DID_BPN_1, "123456", Long.parseLong("2559397136000")); + JWTClaimsSet outerSetFull = addAccessTokenToClaimsSet(accessToken, outerSet); + String siToken = buildJWTToken(JWK_OUTER, outerSetFull); + + boolean isValid = stsTokenValidationService.validateToken(siToken); + + Assertions.assertFalse(isValid); + } + @Test void validateTokenSuccessTest() throws JOSEException { - Wallet wallet = buildWallet(BPN_1, DID_BPN_1, DID_JSON_STRING_1); - walletRepository.save(wallet); + Wallet wallet1 = buildWallet(BPN_1, DID_BPN_1, DID_JSON_STRING_1); + walletRepository.save(wallet1); + + Wallet wallet2 = buildWallet(BPN_2, DID_BPN_2, DID_JSON_STRING_2); + walletRepository.save(wallet2); - JWTClaimsSet innerSet = buildClaimsSet(DID_BPN_1, DID_BPN_2, DID_BPN_1, "123456", Long.parseLong("2559397136000")); + JWTClaimsSet innerSet = buildClaimsSet(DID_BPN_2, DID_BPN_1, DID_BPN_1, "123456", Long.parseLong("2559397136000")); String accessToken = buildJWTToken(JWK_INNER, innerSet); JWTClaimsSet outerSet = buildClaimsSet(DID_BPN_1, DID_BPN_1, DID_BPN_1, "123456", Long.parseLong("2559397136000")); diff --git a/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestConstants.java b/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestConstants.java index 0b0266bd4..c9097a244 100644 --- a/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestConstants.java +++ b/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestConstants.java @@ -63,7 +63,7 @@ public class TestConstants { "x": "Z-8DEkN6pw2E01niDWqrp1kROLF-syIPIpFgmyrVUOU" }, "controller": "did:web:localhost:BPNL000000000002", - "id": "did:web:localhost:BPNL000000000001#58cb4b32-c2e4-46f0-a3ad-3286e34765ed", + "id": "did:web:localhost:BPNL000000000001#58cb4b32-c2e4-46f0-a3ad-3286e34765ty", "type": "JsonWebKey2020" } ]