Skip to content

Commit

Permalink
feat: add JWT verification and extend tests
Browse files Browse the repository at this point in the history
  • Loading branch information
andreibogus committed Feb 22, 2024
1 parent ce75056 commit 5ae223d
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";

Expand All @@ -50,52 +55,54 @@ public class STSTokenValidationService {
*/
public boolean validateToken(String token) {
List<String> 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<String> 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));
}

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<String> getAccessToken(JWTClaimsSet claims) {
try {
String accessTokenValue = claims.getStringClaim(ACCESS_TOKEN);
Expand All @@ -104,4 +111,14 @@ private Optional<String> getAccessToken(JWTClaimsSet claims) {
throw new BadDataException("Could not parse jwt token", e);
}
}

private Optional<String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

Original file line number Diff line number Diff line change
@@ -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<String, Object> 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
Expand Down

0 comments on commit 5ae223d

Please sign in to comment.