Skip to content

Commit

Permalink
feat: Merge PR #255
Browse files Browse the repository at this point in the history
Feature: token validation and authorization
  • Loading branch information
borisrizov-zf authored Feb 27, 2024
2 parents 524aad5 + e9569a7 commit a0d9bdf
Show file tree
Hide file tree
Showing 9 changed files with 997 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* *******************************************************************************
* 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.constant;


public enum TokenValidationErrors {

ACCESS_TOKEN_MISSING,
ISS_AND_SUB_NOT_EQUAL,
SUB_NOT_MATCH_ANY_DID,
SUB_NOT_DID,
EXP_MISSING,
TOKEN_ALREADY_EXPIRED,
IAT_AFTER_EXPIRATION,
CURRENT_TIME_BEFORE_IAT,
AUD_MISSING,
AUD_NOT_DID,
AUD_CLAIMS_NOT_EQUAL,
NONCE_MISSING,
NONCE_CLAIMS_NOT_EQUAL,
SIGNATURE_NOT_VERIFIED,
IAT_MISSING
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* *******************************************************************************
* 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.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors;

import java.util.List;

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class ValidationResult {

private boolean isValid;

private List<TokenValidationErrors> errors;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* *******************************************************************************
* 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.jose.JOSEException;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors;
import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult;
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;

import static org.eclipse.tractusx.managedidentitywallets.utils.TokenValidationUtils.NONCE;

@Service
@Slf4j
@RequiredArgsConstructor
public class STSTokenValidationService {

private final DidDocumentResolverService didDocumentResolverService;
private final CustomSignedJWTVerifier customSignedJWTverifier;
private final TokenValidationUtils tokenValidationUtils;
private static final String ACCESS_TOKEN = "access_token";
private static final String PARSING_TOKEN_ERROR = "Could not parse jwt token";

/**
* Validates SI token and Access token.
*
* @param token token in a String format
* @return boolean result of validation
*/
public ValidationResult validateToken(String token) {
List<ValidationResult> validationResults = new ArrayList<>();

SignedJWT jwtSI = parseToken(token);
JWTClaimsSet claimsSI = getClaimsSet(jwtSI);

validationResults.add(tokenValidationUtils.checkIfSubjectValidAndEqualsDid(claimsSI));
validationResults.add(tokenValidationUtils.checkIfIssuerEqualsSubject(claimsSI));
validationResults.add(tokenValidationUtils.checkTokenExpiry(claimsSI));

Optional<String> accessToken = getAccessToken(claimsSI);
if (accessToken.isPresent()) {
SignedJWT jwtAT = parseToken(accessToken.get());
JWTClaimsSet claimsAT = getClaimsSet(jwtAT);

validationResults.add(tokenValidationUtils.checkIfAudienceClaimsAreEqual(claimsSI.getAudience(), claimsAT.getAudience()));
try {
validationResults.add(tokenValidationUtils.checkIfNonceClaimsAreEqual(claimsSI.getStringClaim(NONCE),
claimsAT.getStringClaim(NONCE)));
} catch (ParseException e) {
throw new BadDataException("Could not parse 'nonce' claim in token", e);
}

String didForOuter = claimsAT.getAudience().get(0);
validationResults.add(verifySignature(didForOuter, jwtSI));

String didForInner = claimsAT.getIssuer();
validationResults.add(verifySignature(didForInner, jwtAT));
} else {
validationResults.add(tokenValidationUtils.getInvalidResult(TokenValidationErrors.ACCESS_TOKEN_MISSING));
}
return combineValidationResults(validationResults);
}

private JWTClaimsSet getClaimsSet(SignedJWT tokenParsed) {
try {
return tokenParsed.getJWTClaimsSet();
} catch (ParseException e) {
throw new BadDataException(PARSING_TOKEN_ERROR, e);
}
}

private SignedJWT parseToken(String token) {
try {
return SignedJWT.parse(token);
} catch (ParseException e) {
throw new BadDataException(PARSING_TOKEN_ERROR, e);
}
}

private Optional<String> getAccessToken(JWTClaimsSet claims) {
try {
String accessTokenValue = claims.getStringClaim(ACCESS_TOKEN);
return accessTokenValue == null ? Optional.empty() : Optional.of(accessTokenValue);
} catch (ParseException e) {
throw new BadDataException(PARSING_TOKEN_ERROR, e);
}
}

private ValidationResult verifySignature(String did, SignedJWT signedJWT) {
try {
customSignedJWTverifier.setDidResolver(didDocumentResolverService.getCompositeDidResolver());
return customSignedJWTverifier.verify(did, signedJWT)
? tokenValidationUtils.getValidResult()
: tokenValidationUtils.getInvalidResult(TokenValidationErrors.SIGNATURE_NOT_VERIFIED);
} catch (JOSEException ex) {
throw new BadDataException("Could not verify signature of jwt", ex);
}
}

private ValidationResult combineValidationResults(List<ValidationResult> validationResults) {
List<TokenValidationErrors> errorsList = new ArrayList<>();
for (ValidationResult result : validationResults) {
List<TokenValidationErrors> errors = result.getErrors();
if (null != errors) {
errorsList.add(errors.get(0));
}
}
ValidationResult finalResult = ValidationResult.builder().errors(errorsList).build();
finalResult.setValid(errorsList.isEmpty());
return finalResult;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* *******************************************************************************
* 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 {
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();
return jwt.verify(new Ed25519Verifier(keyPair));

} 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();
return jwt.verify(new Ed25519Verifier(keyPair));
}
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");
}
}
Loading

0 comments on commit a0d9bdf

Please sign in to comment.