fi.evolver.azure.entraid.EntraIdCertificateSignedJwtAssertionFactory Maven / Gradle / Ivy
package fi.evolver.azure.entraid;
import java.io.ByteArrayInputStream;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
/**
* A factory used to create Microsoft Entra JWT assertion signed with a
* certificate.
*
* @author Rujun Chen
* @see
* Certificate credentials
* @since 2022-02-18
*/
public class EntraIdCertificateSignedJwtAssertionFactory {
private final JWSSigner signer;
private final JWSHeader header;
private final JWTClaimsSet templateClaims;
/**
* @param file Path of certificate file. The file should contain encrypted
* private key and certificate. And the file name should have
* ".pfx" as suffix.
* @param password The password of the encrypted private key in certificate
* file.
* @throws EntraIdAssertionException if failed to create factory.
*/
public EntraIdCertificateSignedJwtAssertionFactory(
String privateKeyPEM,
String certificatePEM,
String tenantId,
String clientId) throws EntraIdAssertionException {
try {
byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
byte[] decodedCert = Base64.getDecoder().decode(certificatePEM);
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(decodedCert));
PublicKey publicKey = x509Certificate.getPublicKey();
signer = createJWSSigner(publicKey, privateKey);
header = createJWSHeader(x509Certificate);
templateClaims = createTemplateJWTClaims(tenantId, clientId);
} catch (InvalidKeySpecException | CertificateException | NoSuchAlgorithmException | JOSEException exception) {
throw new EntraIdAssertionException("Failed to create factory.", exception);
}
}
/**
* Create JWT assertion
*
* @throws EntraIdAssertionException If failed to create assertion.
*/
public String createJwtAssertion() throws EntraIdAssertionException {
JWTClaimsSet claims = createJWTClaimsSet();
SignedJWT signedJwt = new SignedJWT(header, claims);
try {
signedJwt.sign(signer);
} catch (JOSEException exception) {
throw new EntraIdAssertionException("Failed to sign JWT.", exception);
}
return signedJwt.serialize();
}
private static JWSSigner createJWSSigner(PublicKey publicKey, PrivateKey privateKey) throws JOSEException {
// Microsoft Entra ID currently supports only RSA.
// Refs:
// https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-self-signed-certificate
JWK jwk = new RSAKey.Builder((RSAPublicKey) publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString()).build();
return new DefaultJWSSignerFactory().createJWSSigner(jwk);
}
@SuppressWarnings("deprecation")
private static JWSHeader createJWSHeader(X509Certificate x509Certificate) throws CertificateEncodingException, NoSuchAlgorithmException {
return new JWSHeader.Builder(JWSAlgorithm.RS256)
.type(JOSEObjectType.JWT)
.x509CertThumbprint(Base64URL.encode(getX5t(x509Certificate))).build();
}
private static byte[] getX5t(X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] der = cert.getEncoded();
digest.update(der);
return digest.digest();
}
private static JWTClaimsSet createTemplateJWTClaims(String tenantId, String clientId) {
return new JWTClaimsSet.Builder()
.audience("https://login.microsoftonline.com/%s/v2.0".formatted(tenantId))
.issuer(clientId).subject(clientId).build();
}
private JWTClaimsSet createJWTClaimsSet() {
Date currentTime = new Date();
return new JWTClaimsSet.Builder(templateClaims)
.expirationTime(Date.from(currentTime.toInstant().plusSeconds(300))) // 5 minutes after currentTime.
.jwtID(UUID.randomUUID().toString()).notBeforeTime(currentTime).issueTime(currentTime).build();
}
}