Hello,
here how I solved my issue
First, I declared “No Auth”, this option let me create an “Custom Operation” operation
<entry key="authenticationMethod" value="No Auth"/>
and I uploaded the public and private key (PEM format)
<entry key="aggregationType" value="account"/>
<entry key="authenticationMethod" value="No Auth"/>
<entry key="clientCertAuthEnabled">
<value>
<Boolean>true</Boolean>
</value>
</entry>
<entry key="clientCertificate" value="[PUBLIC_KEY]"/>
<entry key="clientKeySpec" value="[PRIVATE_KEY]"/>
The first operation is “Custom authentication”, it calls Entra ID using a beforeRule stript and store the result in customaccesstoken parameter.
op: Custom Authentication
<Map>
<entry key="afterRule"/>
<entry key="beforeRule" value="[WEB_APP_NAME] Custom Authentication"/>
<entry key="body">
<value>
<Map>
<entry key="bodyFormData"/>
<entry key="bodyFormat" value="raw"/>
<entry key="jsonBody"/>
</Map>
</value>
</entry>
<entry key="contextUrl"/>
<entry key="curlCommand"/>
<entry key="curlEnabled">
<value>
<Boolean></Boolean>
</value>
</entry>
<entry key="customAuthUrl" value="https://login.microsoftonline.com/[TENANT_ID]/oauth2/v2.0/token"/>
<entry key="header">
<value>
<Map>
<entry key="Content-Type" value="application/x-www-form-urlencoded"/>
</Map>
</value>
</entry>
<entry key="httpMethodType" value="POST"/>
<entry key="operationType" value="Custom Authentication"/>
<entry key="paginationSteps"/>
<entry key="pagingInitialOffset">
<value>
<Integer>0</Integer>
</value>
</entry>
<entry key="pagingSize">
<value>
<Integer>50</Integer>
</value>
</entry>
<entry key="parentEndpointName"/>
<entry key="resMappingObj">
<value>
<Map>
<entry key="customaccesstoken" value="access_token"/>
</Map>
</value>
</entry>
<entry key="responseCode">
<value>
<List>
<String>2**</String>
</List>
</value>
</entry>
<entry key="rootPath" value="$"/>
<entry key="sequenceNumberForEndpoint" value="1"/>
<entry key="uniqueNameForEndPoint" value="Get Access Token"/>
<entry key="xpathNamespaces"/>
</Map>
In the other operations, I used “header” parameter to provide the “Bearer $application.customaccesstoken$”
op: Test Connection
<Map>
<entry key="afterRule"/>
<entry key="beforeRule"/>
<entry key="body">
<value>
<Map>
<entry key="bodyFormData"/>
<entry key="bodyFormat" value="raw"/>
<entry key="jsonBody"/>
</Map>
</value>
</entry>
<entry key="contextUrl" value="/api/swagger.json"/>
<entry key="curlCommand"/>
<entry key="curlEnabled">
<value>
<Boolean></Boolean>
</value>
</entry>
<entry key="customAuthUrl"/>
<entry key="header">
<value>
<Map>
<entry key="Accept" value="application/json"/>
<entry key="Authorization" value="Bearer $application.customaccesstoken$"/>
<entry key="x-api-key" value="api key"/>
</Map>
</value>
</entry>
<entry key="httpMethodType" value="GET"/>
<entry key="operationType" value="Test Connection"/>
<entry key="paginationSteps"/>
<entry key="pagingInitialOffset">
<value>
<Integer>0</Integer>
</value>
</entry>
<entry key="pagingSize">
<value>
<Integer>50</Integer>
</value>
</entry>
<entry key="parentEndpointName"/>
<entry key="resMappingObj"/>
<entry key="rootPath"/>
<entry key="sequenceNumberForEndpoint" value="2"/>
<entry key="uniqueNameForEndPoint" value="Test Connection"/>
<entry key="xpathNamespaces"/>
</Map>
finally I used oAuthJwtHeader and oAuthJwtPayload to store all the others parameters required by Microsoft.
alg=RS256 means “Generate a JWT with RSA keys”
<entry key="oAuthJwtHeader">
<value>
<Map>
<entry key="alg" value="RS256"/>
</Map>
</value>
</entry>
<entry key="oAuthJwtPayload">
<value>
<Map>
<entry key="aud" value="https://login.microsoftonline.com/[TENANT_ID]/oauth2/v2.0/token"/>
<entry key="exp" value="3600"/>
<entry key="iss" value="[CLIENT_ID]"/>
<entry key="sub" value="[APPLICATION_ID]"/>
</Map>
</value>
</entry>
and here part of the class called by the beforeRule in the Custom Autentication operation:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.time.temporal.ChronoUnit;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import io.jsonwebtoken.Jwts;
import sailpoint.tools.GeneralException;
import sailpoint.object.Application;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.json.JSONException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.MessageDigest;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateFactory;
public Map<String,String> generateToken(Application application) throws GeneralException, NumberFormatException, NoSuchAlgorithmException, InvalidKeySpecException, CertificateException, IOException {
String methodName = "[generateToken()]";
if (log.isDebugEnabled()) {
log.debug(methodName + "Custom Authentication : Rule Started");
}
//get parametes
this.clientCertificate = context.decrypt(application.getStringAttributeValue("clientCertificate"));
this.clientKeySpec = context.decrypt(application.getStringAttributeValue("clientKeySpec"));
this.oAuthJwtPayloadMap = convertStringToMap(application.getStringAttributeValue("oAuthJwtPayload"));
if (log.isDebugEnabled()) {
log.debug(methodName + "oAuthJwtPayloadMap " + oAuthJwtPayloadMap );
}
String token = generateJWT();
String jsonBody = jsonObjectCreator(token);
Map<String, String> bodyMap = new HashMap<>();
bodyMap.put("jsonBody", jsonBody);
bodyMap.put("bodyFormat", "raw");
return bodyMap;
}
public String generateJWT() throws NumberFormatException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, CertificateException {
String methodName = "[generateJWT()]";
if (log.isDebugEnabled()) {
log.debug(methodName + "Custom Authentication : generateJWT Started" );
}
//Set provider
java.security.Security.addProvider(new BouncyCastleProvider());
long exp = Integer.parseInt(oAuthJwtPayloadMap.get("exp"));
Instant now = Instant.now();
String jwtToken = Jwts.builder()
.setHeaderParam("typ","JWT")
.setHeaderParam("x5t",getThumbprint())
.setHeaderParam("alg","PS256")
.claim("iss", oAuthJwtPayloadMap.get("iss"))
.claim("aud", oAuthJwtPayloadMap.get("aud"))
.claim("sub", oAuthJwtPayloadMap.get("iss"))
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(exp, ChronoUnit.SECONDS)))
.signWith(getPrivateKey())
.compact();
if (log.isDebugEnabled()) {
log.debug(methodName + "jwtToken " + jwtToken );
}
return jwtToken;
}
private String getThumbprint() throws CertificateException, NoSuchAlgorithmException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream inputStream = new ByteArrayInputStream(clientCertificate.getBytes());
X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
return Base64.getEncoder().encodeToString(getHash(cert));
}
private byte[] getHash(X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
final MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(cert.getEncoded());
return md.digest();
}
private RSAPrivateKey getPrivateKey()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
// Creating instance of PEM reader to get the Secret Key
PemReader pemReader = new PemReader(new StringReader(clientKeySpec));
PemObject pemObject = pemReader.readPemObject();
// Getting the RSA instance for KeyFactory
KeyFactory factory = KeyFactory.getInstance("RSA");
byte[] content = pemObject.getContent();
PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content);
return privateKey = (RSAPrivateKey) factory.generatePrivate(privKeySpec);
}
private String jsonObjectCreator(String token) throws JSONException {
String methodName = "[jsonObjectCreator()]";
StringBuilder sb = new StringBuilder("scope=" + oAuthJwtPayloadMap.get("sub") + "/.default");
sb.append("&client_id=" + oAuthJwtPayloadMap.get("iss"));
sb.append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
sb.append("&client_assertion=" + token);
sb.append("&grant_type=client_credentials");
if (log.isDebugEnabled()) {
log.debug(methodName + "Message body " + sb.toString());
}
return sb.toString();
}
public Map<String, String> convertStringToMap(String data) {
data = data.replaceAll("[{} ]", "");
Map<String, String> map = new HashMap<>();
StringTokenizer tokenizer = new StringTokenizer(data, ",");
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
String[] keyValue = token.split("=");
map.put(keyValue[0], keyValue[1]);
}
return map;
}
If you want verify the token generated, get custom_auth_token_info, decrypt it, and use https://jwt.io/ to see what it contains.
<entry key="custom_auth_token_info" value="####### GENERATED GWT TOKEN #######"/>
I hope this is helpfull!