Identity IIQ 8.4 Web Service Connector witn Microsoft OAuth2 Authentication

Hello,

I’m working on a webservice connector to integrate an internal REST application with IIQ 8.4

{F56CC554-5038-4DFD-ACCC-2886CE5A3FD5}

They decided to delegate the authentication and authorization process to Entra ID using " Access the token request by using a certificate" as described in this doc:

POST /{tenant}/oauth2/v2.0/token HTTP/1.1               // Line breaks for clarity.
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_id=11112222-bbbb-3333-cccc-4444dddd5555
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=aaaaaaaa-0b0b-...
&grant_type=client_credentials

As you can see the “client_assertion” (JWT) is present in the body of the POST request.

My application “General Settings” looks like:

And according to this document,

I provided all the required parameters:

  <entry key="oAuthJwtHeader">
    <value>
      <Map>
        <entry key="alg" value="RS256"/>
        <entry key="x5t#S256" value="<CERTIFICATE_THUMBPRINT>"/>
      </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="15f"/>
        <entry key="iss" value="$application.client_id"/>
        <entry key="scope" value="<APPLICATION_ID>/.default"/>
        <entry key="sub" value="$application.client_id"/>
      </Map>
    </value>
  </entry>
  <entry key="oauth_request_parameters">
    <value>
      <Map>
        <entry key="client_assertion" value="$oauth_token_info$"/>
        <entry key="client_assertion_type" value="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"/>
        <entry key="client_id" value="$application.client_id$"/>
        <entry key="requested_token_use" value="on_behalf_of"/>
        <entry key="scope" value="<APPLICATION_ID>/.default"/>
      </Map>
    </value>
  </entry>

But I’m struggling with these errors:

if I put “client_assertion” key :confused:
Exception occurred in Test Connection. Error: Exception occurred while generating access token: Unable to generate access token. Response returned: {"error":"invalid_request","error_description":"AADSTS50027: JWT token is invalid or malformed.

but without the key :confused:
Exception occurred in Test Connection. Error: Exception occurred while generating access token: Unable to generate access token. Response returned: {"error":"invalid_client","error_description":"AADSTS7000219: 'client_assertion' or 'client_secret' is required for the 'urn:ietf:params:oauth:grant-type:jwt-bearer' grant type.

How can I retrieve the generated JWT to put in the body of my POST?

Or I’m completely in the wrong direction and I need to implement a “Custom Authentication” script like:

Thanks for your help!

Gianfranco

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!

2 Likes