-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
273 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,8 @@ | ||
package com.microsoft.graph.core.models; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.nio.charset.Charset; | ||
import java.nio.charset.StandardCharsets; | ||
import java.security.interfaces.RSAPrivateKey; | ||
import java.security.Key; | ||
import java.util.Arrays; | ||
import java.util.Base64; | ||
import java.util.Objects; | ||
|
@@ -21,6 +20,9 @@ | |
|
||
import jakarta.annotation.Nonnull; | ||
|
||
/** | ||
* DecryptableContent interface | ||
*/ | ||
public interface DecryptableContent { | ||
|
||
/** | ||
|
@@ -81,14 +83,14 @@ public interface DecryptableContent { | |
* | ||
* @param <T> Parsable type to return | ||
* @param decryptableContent instance of DecryptableContent | ||
* @param privateKeyProvider provides an RSA Private Key for the certificate provided when subscribing | ||
* @param certificateKeyProvider provides an RSA Private Key for the certificate provided when subscribing | ||
* @param factory ParsableFactory for the return type | ||
* @return | ||
* @throws Exception | ||
* @return decrypted resource data | ||
* @throws Exception if an error occurs while decrypting the data | ||
*/ | ||
public static <T extends Parsable> T decrypt(@Nonnull final DecryptableContent decryptableContent, @Nonnull final PrivateKeyProvider privateKeyProvider, @Nonnull final ParsableFactory<T> factory) throws Exception { | ||
Objects.requireNonNull(privateKeyProvider); | ||
final String decryptedContent = decryptAsString(decryptableContent, privateKeyProvider); | ||
public static <T extends Parsable> T decrypt(@Nonnull final DecryptableContent decryptableContent, @Nonnull final CertificateKeyProvider certificateKeyProvider, @Nonnull final ParsableFactory<T> factory) throws Exception { | ||
Objects.requireNonNull(certificateKeyProvider); | ||
final String decryptedContent = decryptAsString(decryptableContent, certificateKeyProvider); | ||
final ParseNode rootParseNode = ParseNodeFactoryRegistry.defaultInstance.getParseNode( | ||
"application/json", new ByteArrayInputStream(decryptedContent.getBytes(StandardCharsets.UTF_8))); | ||
return rootParseNode.getObjectValue(factory); | ||
|
@@ -99,14 +101,14 @@ public static <T extends Parsable> T decrypt(@Nonnull final DecryptableContent d | |
* https://learn.microsoft.com/en-us/graph/change-notifications-with-resource-data?tabs=csharp#decrypting-resource-data-from-change-notifications | ||
* | ||
* @param content instance of DecryptableContent | ||
* @param privateKeyProvider provides an RSA Private Key for the certificate provided when subscribing | ||
* @param certificateKeyProvider provides an RSA Private Key for the certificate provided when subscribing | ||
* @return decrypted resource data | ||
* @throws Exception | ||
* @throws Exception if an error occurs while decrypting the data | ||
*/ | ||
public static String decryptAsString(@Nonnull final DecryptableContent content, @Nonnull final PrivateKeyProvider privateKeyProvider) throws Exception { | ||
Objects.requireNonNull(privateKeyProvider); | ||
final RSAPrivateKey privateKey = privateKeyProvider.getCertificatePrivateKey(content.getEncryptionCertificateId(), content.getEncryptionCertificateThumbprint()); | ||
final Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); | ||
public static String decryptAsString(@Nonnull final DecryptableContent content, @Nonnull final CertificateKeyProvider certificateKeyProvider) throws Exception { | ||
Objects.requireNonNull(certificateKeyProvider); | ||
final Key privateKey = certificateKeyProvider.getCertificateKey(content.getEncryptionCertificateId(), content.getEncryptionCertificateThumbprint()); | ||
final Cipher cipher = Cipher.getInstance("RSA/None/OAEPWithSHA1AndMGF1Padding"); | ||
cipher.init(Cipher.DECRYPT_MODE, privateKey); | ||
final byte[] decryptedSymmetricKey = cipher.doFinal(Base64.getDecoder().decode(content.getDataKey())); | ||
|
||
|
@@ -126,10 +128,13 @@ public static String decryptAsString(@Nonnull final DecryptableContent content, | |
* @param data Base-64 decoded resource data | ||
* @param key Decrypted symmetric key from DecryptableContent.getDataKey() | ||
* @return decrypted resource data | ||
* @throws Exception | ||
* @throws Exception if an error occurs while decrypting the data | ||
*/ | ||
public static byte[] aesDecrypt(byte[] data, byte[] key) throws Exception { | ||
try { | ||
@SuppressWarnings("java:S3329") | ||
// Sonar warns that a random IV should be used for encryption | ||
// but we are decrypting here. | ||
final IvParameterSpec ivSpec = new IvParameterSpec(Arrays.copyOf(key, 16)); | ||
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | ||
Check failure Code scanning / SonarCloud Encryption algorithms should be used with secure mode and padding scheme High
Use another cipher mode or disable padding. See more on SonarQube Cloud
Check failure Code scanning / CodeQL Use of a broken or risky cryptographic algorithm High
Cryptographic algorithm
AES/CBC/PKCS5Padding Error loading related location Loading |
||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), ivSpec); | ||
|
@@ -140,17 +145,17 @@ public static byte[] aesDecrypt(byte[] data, byte[] key) throws Exception { | |
} | ||
|
||
/** | ||
* Provides an RSA Private Key for the certificate with the ID provided when creating the | ||
* Provides a private key for the certificate with the ID provided when creating the | ||
* subscription and the thumbprint. | ||
*/ | ||
@FunctionalInterface | ||
public interface PrivateKeyProvider { | ||
public interface CertificateKeyProvider { | ||
/** | ||
* Returns the RSAPrivateKey for an X.509 certificate with the given id and thumbprint | ||
* Returns the private key for an X.509 certificate with the given id and thumbprint | ||
* @param certificateId certificate Id provided when subscribing | ||
* @param certtificateThumbprint certificate thumbprint | ||
* @return RSA private key used to sign the certificate | ||
* @param certificateThumbprint certificate thumbprint | ||
* @return Private key used to sign the certificate | ||
*/ | ||
public RSAPrivateKey getCertificatePrivateKey(String certificateId, String certtificateThumbprint); | ||
public Key getCertificateKey(String certificateId, String certificateThumbprint); | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
src/main/java/com/microsoft/graph/core/models/DiscoverUrlAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package com.microsoft.graph.core.models; | ||
|
||
import java.net.MalformedURLException; | ||
import java.net.URI; | ||
import java.net.URISyntaxException; | ||
import java.security.Key; | ||
import java.util.Objects; | ||
|
||
import org.slf4j.LoggerFactory; | ||
|
||
import com.auth0.jwk.Jwk; | ||
import com.auth0.jwk.JwkProvider; | ||
import com.auth0.jwk.UrlJwkProvider; | ||
|
||
import io.jsonwebtoken.JweHeader; | ||
import io.jsonwebtoken.JwsHeader; | ||
import io.jsonwebtoken.LocatorAdapter; | ||
import jakarta.annotation.Nonnull; | ||
|
||
/** | ||
* DiscoverUrlAdapter class | ||
*/ | ||
public class DiscoverUrlAdapter extends LocatorAdapter<Key> { | ||
|
||
/** | ||
* Key store | ||
*/ | ||
private final JwkProvider keyStore; | ||
|
||
/** | ||
* Constructor | ||
* @param keyDiscoveryUrl the JWKS endpoint to use to retrieve signing keys | ||
* @throws URISyntaxException if uri is invalid | ||
* @throws MalformedURLException if url is invalid | ||
*/ | ||
public DiscoverUrlAdapter(@Nonnull final String keyDiscoveryUrl) | ||
throws URISyntaxException, MalformedURLException { | ||
this.keyStore = | ||
new UrlJwkProvider(new URI(Objects.requireNonNull(keyDiscoveryUrl)).toURL()); | ||
} | ||
|
||
@Override | ||
protected Key locate(JwsHeader header) { | ||
Objects.requireNonNull(header); | ||
try { | ||
String keyId = header.getKeyId(); | ||
Jwk publicKey = keyStore.get(keyId); | ||
return publicKey.getPublicKey(); | ||
} catch (final Exception e) { | ||
throw new IllegalArgumentException("Could not locate key", e); | ||
} | ||
} | ||
|
||
@Override | ||
protected Key locate(JweHeader header) { | ||
return null; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 178 additions & 0 deletions
178
src/main/java/com/microsoft/graph/core/models/TokenValidable.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
package com.microsoft.graph.core.models; | ||
|
||
import java.security.Key; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
import java.util.UUID; | ||
|
||
import jakarta.annotation.Nonnull; | ||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jws; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.Locator; | ||
|
||
/** | ||
* TokenValidable interface | ||
*/ | ||
public interface TokenValidable<U extends DecryptableContent, T extends EncryptedContentBearer<U>> { | ||
|
||
/** | ||
* Graph notification publisher. Ensures that a different app that isn't Microsoft Graph did not send the change notifications | ||
*/ | ||
public static final String graphNotificationPublisher = "0bf30f3b-4a52-48df-9a82-234910c4a086"; | ||
|
||
/** | ||
* Sets collection of validation tokens | ||
* @param validationTokens tokens | ||
*/ | ||
public void setValidationTokens(List<String> validationTokens); | ||
|
||
/** | ||
* Returns validation tokens | ||
* @return list of tokens | ||
*/ | ||
public List<String> getValidationTokens(); | ||
|
||
/** | ||
* Sets collection of encrypted token bearers | ||
* @param value collection of encrypted token bearers | ||
*/ | ||
public void setValue(List<T> value); | ||
|
||
/** | ||
* Get collection of encrypted token bearers | ||
* @return encrypted token bearers | ||
*/ | ||
public List<T> getValue(); | ||
|
||
/** | ||
* Validates the tokens | ||
* @param <U> DecryptableContent | ||
* @param <T> EncryptedContentBearer | ||
* @param collection collection of encrypted token bearers | ||
* @param tenantIds tenant ids | ||
* @param appIds app ids | ||
* @param keyDiscoveryUrl the JWKS endpoint to use to retrieve signing keys | ||
* @return true if the tokens are valid | ||
* @throws IllegalArgumentException if one of the tokens are invalid | ||
*/ | ||
public static <U extends DecryptableContent, T extends EncryptedContentBearer<U>> | ||
boolean areTokensValid( | ||
@Nonnull final TokenValidable<U,T> collection, | ||
@Nonnull final List<UUID> tenantIds, | ||
@Nonnull final List<UUID> appIds, | ||
@Nonnull final String keyDiscoveryUrl) { | ||
|
||
Objects.requireNonNull(collection); | ||
Objects.requireNonNull(tenantIds); | ||
Objects.requireNonNull(appIds); | ||
Objects.requireNonNull(keyDiscoveryUrl); | ||
|
||
if (collection.getValidationTokens().isEmpty() | ||
|| collection.getValue().stream().allMatch(x -> x.getEncryptedContent() == null)) { | ||
return true; | ||
} | ||
|
||
if (tenantIds.isEmpty() || appIds.isEmpty()) { | ||
throw new IllegalArgumentException("tenantIds, appIds and issuer formats must be provided"); | ||
} | ||
|
||
for (final String token : collection.getValidationTokens()) { | ||
if (!isTokenValid(token, tenantIds, appIds, keyDiscoveryUrl)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* Validates the tokens | ||
* @param <U> DecryptableContent | ||
* @param <T> EncryptedContentBearer | ||
* @param collection collection of encrypted token bearers | ||
* @param tenantIds tenant ids | ||
* @param appIds app ids | ||
* @return true if the tokens are valid | ||
*/ | ||
public static <U extends DecryptableContent, T extends EncryptedContentBearer<U>> | ||
boolean areTokensValid( | ||
@Nonnull final TokenValidable<U,T> collection, | ||
@Nonnull final List<UUID> tenantIds, | ||
@Nonnull final List<UUID> appIds) { | ||
|
||
final String keyDiscoveryUrl = "https://login.microsoftonline.com/common/discovery/keys"; | ||
return areTokensValid(collection, tenantIds, appIds, keyDiscoveryUrl); | ||
} | ||
|
||
/** | ||
* Validates the token | ||
* @param <U> DecryptableContent | ||
* @param <T> EncryptedContentBearer | ||
* @param token token | ||
* @param tenantIds tenant ids | ||
* @param appIds app ids | ||
* @param keyDiscoveryUrl the JWKS endpoint to use to retrieve signing keys | ||
* @return true if the token is valid | ||
* @throws IllegalArgumentException if the token is invalid | ||
*/ | ||
public static <U extends DecryptableContent, T extends EncryptedContentBearer<U>> | ||
boolean isTokenValid( | ||
@Nonnull final String token, | ||
@Nonnull final List<UUID> tenantIds, | ||
@Nonnull final List<UUID> appIds, | ||
@Nonnull final String keyDiscoveryUrl) { | ||
|
||
Objects.requireNonNull(token); | ||
Objects.requireNonNull(tenantIds); | ||
Objects.requireNonNull(appIds); | ||
Objects.requireNonNull(keyDiscoveryUrl); | ||
|
||
if (tenantIds.isEmpty() || appIds.isEmpty()) { | ||
throw new IllegalArgumentException("tenantIds, appIds and issuer formats must be provided"); | ||
} | ||
|
||
try { | ||
Locator<Key> discoverUrlAdapter = new DiscoverUrlAdapter(keyDiscoveryUrl); | ||
// As part of this process, the signature is validated | ||
// This throws if the signature is invalid | ||
Jws<Claims> parsedToken = Jwts.parser().keyLocator(discoverUrlAdapter).build().parseSignedClaims(token); | ||
|
||
Claims body = parsedToken.getPayload(); | ||
|
||
if (body.getExpiration().before(new java.util.Date())) { | ||
throw new IllegalArgumentException("Token is expired"); | ||
} | ||
|
||
String issuer = body.getIssuer(); | ||
Set<String> audience = body.getAudience(); | ||
|
||
boolean isAudienceValid = false; | ||
for (final UUID appId : appIds) { | ||
if (audience.contains(appId.toString())) { | ||
isAudienceValid = true; | ||
break; | ||
} | ||
} | ||
|
||
boolean isIssuerValid = false; | ||
for (final UUID tenantId : tenantIds) { | ||
if (issuer.contains(tenantId.toString())) { | ||
isIssuerValid = true; | ||
break; | ||
} | ||
} | ||
|
||
if (body.get("azp", String.class) != graphNotificationPublisher) { | ||
throw new IllegalArgumentException("Invalid token publisher. Expected Graph notification publisher (azp): " + graphNotificationPublisher); | ||
} | ||
|
||
return isAudienceValid && isIssuerValid; | ||
|
||
} catch (final Exception e) { | ||
throw new IllegalArgumentException("Invalid token", e); | ||
} | ||
|
||
} | ||
|
||
} |