diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadata.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadata.java new file mode 100644 index 000000000..3b2587001 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadata.java @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkException; +import com.auth0.jwk.SigningKeyNotFoundException; +import com.auth0.jwk.UrlJwkProvider; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains a cache of OpenID metadata keys. + */ +class CachingOpenIdMetadata implements OpenIdMetadata { + private static final Logger LOGGER = LoggerFactory.getLogger(CachingOpenIdMetadata.class); + private static final int CACHE_DAYS = 5; + + private String url; + private long lastUpdated; + private ObjectMapper mapper; + private Map keyCache = new HashMap<>(); + private final Object sync = new Object(); + + /** + * Constructs a OpenIdMetaData cache for a url. + * + * @param withUrl The url. + */ + CachingOpenIdMetadata(String withUrl) { + url = withUrl; + mapper = new ObjectMapper().findAndRegisterModules(); + } + + /** + * Gets a openid key. + * + *

+ * Note: This could trigger a cache refresh, which will incur network calls. + *

+ * + * @param keyId The JWT key. + * @return The cached key. + */ + @Override + public OpenIdMetadataKey getKey(String keyId) { + synchronized (sync) { + // If keys are more than 5 days old, refresh them + if (lastUpdated < System.currentTimeMillis() - Duration.ofDays(CACHE_DAYS).toMillis()) { + refreshCache(); + } + + // Search the cache even if we failed to refresh + return findKey(keyId); + } + } + + private void refreshCache() { + keyCache.clear(); + + try { + URL openIdUrl = new URL(this.url); + HashMap openIdConf = + this.mapper.readValue(openIdUrl, new TypeReference>() { + }); + URL keysUrl = new URL(openIdConf.get("jwks_uri")); + lastUpdated = System.currentTimeMillis(); + UrlJwkProvider provider = new UrlJwkProvider(keysUrl); + keyCache = provider.getAll().stream().collect(Collectors.toMap(Jwk::getId, jwk -> jwk)); + } catch (IOException e) { + LOGGER.error(String.format("Failed to load openID config: %s", e.getMessage())); + lastUpdated = 0; + } catch (SigningKeyNotFoundException keyexception) { + LOGGER.error("refreshCache", keyexception); + lastUpdated = 0; + } + } + + @SuppressWarnings("unchecked") + private OpenIdMetadataKey findKey(String keyId) { + if (!keyCache.containsKey(keyId)) { + LOGGER.warn("findKey: keyId " + keyId + " doesn't exist."); + return null; + } + + try { + Jwk jwk = keyCache.get(keyId); + OpenIdMetadataKey key = new OpenIdMetadataKey(); + key.key = (RSAPublicKey) jwk.getPublicKey(); + key.endorsements = (List) jwk.getAdditionalAttributes().get("endorsements"); + key.certificateChain = jwk.getCertificateChain(); + return key; + } catch (JwkException e) { + String errorDescription = String.format("Failed to load keys: %s", e.getMessage()); + LOGGER.warn(errorDescription); + } + return null; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadataResolver.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadataResolver.java new file mode 100644 index 000000000..d937e37ff --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CachingOpenIdMetadataResolver.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Maintains a cache of OpenIdMetadata objects. + */ +public class CachingOpenIdMetadataResolver implements OpenIdMetadataResolver { + private static final ConcurrentMap OPENID_METADATA_CACHE = + new ConcurrentHashMap<>(); + + /** + * Gets the OpenIdMetadata object for the specified key. + * @param metadataUrl The key + * @return The OpenIdMetadata object. If the key is not found, an new OpenIdMetadata + * object is created. + */ + @Override + public OpenIdMetadata get(String metadataUrl) { + return OPENID_METADATA_CACHE + .computeIfAbsent(metadataUrl, key -> new CachingOpenIdMetadata(metadataUrl)); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java index 7ef3c2abc..a9890eeba 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/EmulatorValidation.java @@ -23,7 +23,7 @@ private EmulatorValidation() { * TO BOT FROM EMULATOR: Token validation parameters when connecting to a * channel. */ - private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = + public static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() { { this.validateIssuer = true; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java index 552586d80..7c1506050 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/GovernmentChannelValidation.java @@ -20,7 +20,7 @@ public final class GovernmentChannelValidation { * TO BOT FROM GOVERNMENT CHANNEL: Token validation parameters when connecting * to a bot. */ - private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = + public static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() { { this.validateIssuer = true; diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java index 19889eee5..fcd2eedc9 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenExtractor.java @@ -9,6 +9,11 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; import com.microsoft.bot.connector.ExecutorFactory; +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Date; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,19 +21,16 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; /** * Extracts relevant data from JWT Tokens. */ public class JwtTokenExtractor { - private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdMetadata.class); - private static final ConcurrentMap OPENID_METADATA_CACHE = - new ConcurrentHashMap<>(); + private static final Logger LOGGER = LoggerFactory.getLogger(CachingOpenIdMetadata.class); private TokenValidationParameters tokenValidationParameters; private List allowedSigningAlgorithms; + private OpenIdMetadataResolver openIdMetadataResolver; private OpenIdMetadata openIdMetadata; /** @@ -43,13 +45,18 @@ public JwtTokenExtractor( String withMetadataUrl, List withAllowedSigningAlgorithms ) { - this.tokenValidationParameters = new TokenValidationParameters(withTokenValidationParameters); this.tokenValidationParameters.requireSignedTokens = true; this.allowedSigningAlgorithms = withAllowedSigningAlgorithms; - this.openIdMetadata = OPENID_METADATA_CACHE - .computeIfAbsent(withMetadataUrl, key -> new OpenIdMetadata(withMetadataUrl)); + + if (tokenValidationParameters.issuerSigningKeyResolver == null) { + this.openIdMetadataResolver = new CachingOpenIdMetadataResolver(); + } else { + this.openIdMetadataResolver = tokenValidationParameters.issuerSigningKeyResolver; + } + + this.openIdMetadata = this.openIdMetadataResolver.get(withMetadataUrl); } /** @@ -143,13 +150,27 @@ private CompletableFuture validateToken( try { verification.build().verify(token); + // If specified, validate the signing certificate. + if ( + tokenValidationParameters.validateIssuerSigningKey + && key.certificateChain != null + && key.certificateChain.size() > 0 + ) { + // Note that decodeCertificate will return null if the cert could not + // be decoded. This would likely be the case if it were in an unexpected + // encoding. Going to err on the side of ignoring this check. + // May want to reconsider this and throw on null cert. + X509Certificate cert = decodeCertificate(key.certificateChain.get(0)); + if (cert != null && !isCertValid(cert)) { + throw new JWTVerificationException("Signing certificate is not valid"); + } + } + // Note: On the Emulator Code Path, the endorsements collection is null so the - // validation code - // below won't run. This is normal. + // validation code below won't run. This is normal. if (key.endorsements != null) { // Validate Channel / Token Endorsements. For this, the channelID present on the - // Activity - // needs to be matched by an endorsement. + // Activity needs to be matched by an endorsement. boolean isEndorsed = EndorsementsValidator.validate(channelId, key.endorsements); if (!isEndorsed) { @@ -162,8 +183,7 @@ private CompletableFuture validateToken( } // Verify that additional endorsements are satisfied. If no additional - // endorsements are expected, - // the requirement is satisfied as well + // endorsements are expected, the requirement is satisfied as well boolean additionalEndorsementsSatisfied = requiredEndorsements.stream() .allMatch( (endorsement) -> EndorsementsValidator @@ -195,4 +215,22 @@ private CompletableFuture validateToken( } }, ExecutorFactory.getExecutor()); } + + private X509Certificate decodeCertificate(String certStr) { + try { + byte[] decoded = Base64.getDecoder().decode(certStr); + return (X509Certificate) CertificateFactory + .getInstance("X.509").generateCertificate(new ByteArrayInputStream(decoded)); + } catch (Throwable t) { + return null; + } + } + + private boolean isCertValid(X509Certificate cert) { + long now = new Date().getTime(); + long clockskew = tokenValidationParameters.clockSkew.toMillis(); + long startValid = cert.getNotBefore().getTime() - clockskew; + long endValid = cert.getNotAfter().getTime() + clockskew; + return now >= startValid && now <= endValid; + } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java index a5af8e4e4..6af6a8359 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadata.java @@ -3,108 +3,15 @@ package com.microsoft.bot.connector.authentication; -import com.auth0.jwk.Jwk; -import com.auth0.jwk.JwkException; -import com.auth0.jwk.SigningKeyNotFoundException; -import com.auth0.jwk.UrlJwkProvider; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; -import java.net.URL; -import java.security.interfaces.RSAPublicKey; -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** - * Maintains a cache of OpenID metadata keys. + * Fetches Jwk data. */ -class OpenIdMetadata { - private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdMetadata.class); - private static final int CACHE_DAYS = 5; - - private String url; - private long lastUpdated; - private ObjectMapper mapper; - private Map keyCache = new HashMap<>(); - private final Object sync = new Object(); - - /** - * Constructs a OpenIdMetaData cache for a url. - * - * @param withUrl The url. - */ - OpenIdMetadata(String withUrl) { - url = withUrl; - mapper = new ObjectMapper().findAndRegisterModules(); - } +public interface OpenIdMetadata { /** - * Gets a openid key. - * - *

- * Note: This could trigger a cache refresh, which will incur network calls. - *

- * - * @param keyId The JWT key. - * @return The cached key. + * Returns the partial Jwk data for a key. + * @param keyId The key id. + * @return The Jwk data. */ - public OpenIdMetadataKey getKey(String keyId) { - synchronized (sync) { - // If keys are more than 5 days old, refresh them - if (lastUpdated < System.currentTimeMillis() - Duration.ofDays(CACHE_DAYS).toMillis()) { - refreshCache(); - } - - // Search the cache even if we failed to refresh - return findKey(keyId); - } - } - - private void refreshCache() { - keyCache.clear(); - - try { - URL openIdUrl = new URL(this.url); - HashMap openIdConf = - this.mapper.readValue(openIdUrl, new TypeReference>() { - }); - URL keysUrl = new URL(openIdConf.get("jwks_uri")); - lastUpdated = System.currentTimeMillis(); - UrlJwkProvider provider = new UrlJwkProvider(keysUrl); - keyCache = provider.getAll().stream().collect(Collectors.toMap(Jwk::getId, jwk -> jwk)); - } catch (IOException e) { - LOGGER.error(String.format("Failed to load openID config: %s", e.getMessage())); - lastUpdated = 0; - } catch (SigningKeyNotFoundException keyexception) { - LOGGER.error("refreshCache", keyexception); - lastUpdated = 0; - } - } - - @SuppressWarnings("unchecked") - private OpenIdMetadataKey findKey(String keyId) { - if (!keyCache.containsKey(keyId)) { - LOGGER.warn("findKey: keyId " + keyId + " doesn't exist."); - return null; - } - - try { - Jwk jwk = keyCache.get(keyId); - OpenIdMetadataKey key = new OpenIdMetadataKey(); - key.key = (RSAPublicKey) jwk.getPublicKey(); - key.endorsements = (List) jwk.getAdditionalAttributes().get("endorsements"); - return key; - } catch (JwkException e) { - String errorDescription = String.format("Failed to load keys: %s", e.getMessage()); - LOGGER.warn(errorDescription); - } - return null; - } + OpenIdMetadataKey getKey(String keyId); } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataKey.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataKey.java index 5028a6c66..20c1774d9 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataKey.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataKey.java @@ -9,9 +9,11 @@ /** * Wrapper to hold Jwk key data. */ -class OpenIdMetadataKey { +public class OpenIdMetadataKey { @SuppressWarnings("checkstyle:VisibilityModifier") - RSAPublicKey key; + public RSAPublicKey key; @SuppressWarnings("checkstyle:VisibilityModifier") - List endorsements; + public List endorsements; + @SuppressWarnings("checkstyle:VisibilityModifier") + public List certificateChain; } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataResolver.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataResolver.java new file mode 100644 index 000000000..b37cc824f --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OpenIdMetadataResolver.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +/** + * Gets OpenIdMetadata. + */ +public interface OpenIdMetadataResolver { + + /** + * Gets OpenIdMetadata for the specified key. + * @param key The key. + * @return An OpenIdMetadata object. + */ + OpenIdMetadata get(String key); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java index 15eebcc80..c1658ee72 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/TokenValidationParameters.java @@ -41,6 +41,17 @@ public class TokenValidationParameters { */ public boolean requireSignedTokens; + /** + * Optional (and not recommended) Function to return OpenIdMetaData resolver + * for a given url. + */ + public OpenIdMetadataResolver issuerSigningKeyResolver; + + /** + * True to validate the signing cert. + */ + public boolean validateIssuerSigningKey = true; + /** * Default parameters. */ @@ -61,6 +72,8 @@ public TokenValidationParameters(TokenValidationParameters other) { other.clockSkew, other.requireSignedTokens ); + this.issuerSigningKeyResolver = other.issuerSigningKeyResolver; + this.validateIssuerSigningKey = other.validateIssuerSigningKey; } /** diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenExtractorTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenExtractorTests.java new file mode 100644 index 000000000..67b93f347 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/JwtTokenExtractorTests.java @@ -0,0 +1,211 @@ +package com.microsoft.bot.connector; + +import com.auth0.jwt.algorithms.Algorithm; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.ChannelValidation; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.EmulatorValidation; +import com.microsoft.bot.connector.authentication.GovernmentChannelValidation; +import com.microsoft.bot.connector.authentication.JwtTokenExtractor; +import com.microsoft.bot.connector.authentication.OpenIdMetadata; +import com.microsoft.bot.connector.authentication.OpenIdMetadataKey; +import com.microsoft.bot.connector.authentication.TokenValidationParameters; +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.junit.Before; +import org.junit.Test; +import sun.security.x509.AlgorithmId; +import sun.security.x509.CertificateAlgorithmId; +import sun.security.x509.CertificateSerialNumber; +import sun.security.x509.CertificateValidity; +import sun.security.x509.CertificateVersion; +import sun.security.x509.CertificateX509Key; +import sun.security.x509.X500Name; +import sun.security.x509.X509CertImpl; +import sun.security.x509.X509CertInfo; + +public class JwtTokenExtractorTests { + private X509Certificate validCertificate; + private X509Certificate expiredCertificate; + private KeyPair keyPair; + + @Before + public void setup() throws GeneralSecurityException, IOException { + ChannelValidation.TOKENVALIDATIONPARAMETERS.validateLifetime = false; + EmulatorValidation.TOKENVALIDATIONPARAMETERS.validateLifetime = false; + GovernmentChannelValidation.TOKENVALIDATIONPARAMETERS.validateLifetime = false; + + // create keys + keyPair = createKeyPair(); + Date now = new Date(); + Date from = new Date(now.getTime() - (10 * 86400000L)); + + // create expired certificate + Date to = new Date(now.getTime() - (9 * 86400000L)); + expiredCertificate = createSelfSignedCertificate(keyPair, from, to); + + // create valid certificate + to = new Date(now.getTime() + (9 * 86400000L)); + validCertificate = createSelfSignedCertificate(keyPair, from, to); + } + + @Test(expected = CompletionException.class) + public void JwtTokenExtractor_WithExpiredCert_ShouldNotAllowCertSigningKey() { + // this should throw a CompletionException (which contains an AuthenticationException) + buildExtractorAndValidateToken( + expiredCertificate, keyPair.getPrivate() + ).join(); + } + + @Test + public void JwtTokenExtractor_WithValidCert_ShouldAllowCertSigningKey() { + // this should not throw + buildExtractorAndValidateToken( + validCertificate, keyPair.getPrivate() + ).join(); + } + + @Test(expected = CompletionException.class) + public void JwtTokenExtractor_WithExpiredToken_ShouldNotAllow() { + // this should throw a CompletionException (which contains an AuthenticationException) + Date now = new Date(); + Date issuedAt = new Date(now.getTime() - 86400000L); + + buildExtractorAndValidateToken( + expiredCertificate, keyPair.getPrivate(), issuedAt + ).join(); + } + + private CompletableFuture buildExtractorAndValidateToken( + X509Certificate cert, + PrivateKey privateKey + ) { + return buildExtractorAndValidateToken(cert, privateKey, new Date()); + } + + private CompletableFuture buildExtractorAndValidateToken( + X509Certificate cert, + PrivateKey privateKey, + Date issuedAt + ) { + TokenValidationParameters tokenValidationParameters = createTokenValidationParameters(cert); + + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + tokenValidationParameters, + "https://login.botframework.com/v1/.well-known/openidconfiguration", + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + ); + + String token = createTokenForCertificate(cert, privateKey, issuedAt); + + return tokenExtractor.getIdentity("Bearer " + token, "test"); + } + + private static String createTokenForCertificate(X509Certificate cert, PrivateKey privateKey) { + return createTokenForCertificate(cert, privateKey, new Date()); + } + + // creates a token that expires 5 minutes from the 'issuedAt' value. + private static String createTokenForCertificate(X509Certificate cert, PrivateKey privateKey, Date issuedAt) { + RSAPublicKey publicKey = (RSAPublicKey) cert.getPublicKey(); + Algorithm algorithm = Algorithm.RSA256(publicKey, (RSAPrivateKey) privateKey); + return com.auth0.jwt.JWT.create() + .withIssuer("https://api.botframework.com") + .withIssuedAt(issuedAt) + .withNotBefore(issuedAt) + .withExpiresAt(new Date(issuedAt.getTime() + 300000L)) + .sign(algorithm); + } + + private static TokenValidationParameters createTokenValidationParameters(X509Certificate cert) + { + return new TokenValidationParameters() {{ + validateIssuer = false; + validIssuers = Collections.singletonList(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER); + + // Audience validation takes place in JwtTokenExtractor + validateAudience = false; + validateLifetime = true; + clockSkew = Duration.ofMinutes(5); + requireSignedTokens = true; + + // provide a custom resolver so that calls to openid won't happen (which wouldn't + // work for these tests). + issuerSigningKeyResolver = key -> (OpenIdMetadata) keyId -> { + // return our certificate data + OpenIdMetadataKey key1 = new OpenIdMetadataKey(); + key1.key = (RSAPublicKey) cert.getPublicKey(); + key1.certificateChain = Collections.singletonList(encodeCertificate(cert)); + return key1; + }; + }}; + } + + private KeyPair createKeyPair() throws NoSuchAlgorithmException { + // note that this isn't allowing for a "kid" value + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + return generator.generateKeyPair(); + } + + private static X509Certificate createSelfSignedCertificate( + KeyPair pair, Date from, Date to + ) throws GeneralSecurityException, IOException { + String dn = "CN=Bot, OU=BotFramework, O=Microsoft, C=US"; + String algorithm = "SHA256withRSA"; + + PrivateKey privateKey = pair.getPrivate(); + X509CertInfo info = new X509CertInfo(); + + CertificateValidity interval = new CertificateValidity(from, to); + BigInteger sn = new BigInteger(64, new SecureRandom()); + X500Name owner = new X500Name(dn); + + info.set(X509CertInfo.VALIDITY, interval); + info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn)); + info.set(X509CertInfo.SUBJECT, owner); + info.set(X509CertInfo.ISSUER, owner); + info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic())); + info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); + AlgorithmId algo = new AlgorithmId(AlgorithmId.sha256WithRSAEncryption_oid); + info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo)); + + // Sign the cert to identify the algorithm that's used. + X509CertImpl cert = new X509CertImpl(info); + cert.sign(privateKey, algorithm); + + // Update the algorithm, and resign. + algo = (AlgorithmId)cert.get(X509CertImpl.SIG_ALG); + info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo); + cert = new X509CertImpl(info); + cert.sign(privateKey, algorithm); + return cert; + } + + private static String encodeCertificate(Certificate certificate) { + try { + Base64.Encoder encoder = Base64.getEncoder(); + byte[] rawCrtText = certificate.getEncoded(); + return new String(encoder.encode(rawCrtText)); + } catch(CertificateEncodingException e) { + return null; + } + } +}