Authenticating to OIM SCIM server using an OAM-generated SAML identity assertion

In a previous post previous post I provided a brief introduction to SCIM. In this post I’m going to dive right in and give an example of using the OIM SCIM services and securing them with OAM.

Why would you want to use OIM SCIM services?

There are many reasons, however I will focus on one particular use case in this post – building a custom UI to access OIM. OIM’s out-of-the-box UI provides a broad set of features to cover most use cases, and includes a customisation facility which enables it to be extended in various ways: you can change the visual appearance (logo, colour scheme), add new fields, change the labels of fields and buttons, add additional instructional text, etc. However, if you have very involved plans for UI customisation – such as changing the flow between screens, etc. – there comes a point when building your own custom UI from scratch works out to be a better solution than customising the out-of-the-box UI.

Now, when it comes to building a custom UI, or indeed any custom code which needs to access OIM services, you have two main options:

  1. Calling the OIM Java APIs
  2. Calling the SCIM web services

Up until 11.1.2.3, (2) was not an option. The advantages of SCIM web services over the OIM Java API:

  • Calling the OIM Java APIs is only supported from clients written in Java. So if you want to write your custom UI in a non-JVM based language, such as node.js or C#, that isn’t possible.
  • Calling the OIM Java APIs works well provided your custom UI is hosted on the same WebLogic version and the same Java version. However, as soon as you try to run your custom UI on a different Java version (e.g. Java 8 instead of Java 7) or a different app server (e.g. Glassfish, Tomcat, JBOSS), or a different version of WebLogic (e.g. WLS 12c instead of WLS 10.3.6), you may run into difficulties which can be difficult or impossible to solve.
  • SCIM is an industry standard technology; the OIM Java API is proprietary.
  • Oracle Identity Cloud Service (IDCS) uses SCIM. By writing your custom UI to talk to SCIM, you potentially could switch over the backend from OIM to IDCS in the future (if you decide to migrate your on-premise identity solution to the cloud.)

I should make clear that I am talking here about code which runs external to OIM and which needs to access OIM services. If you are talking about extension code which runs inside OIM – such as a a custom event handler or a custom ICF connector – in those cases you should use the appropriate Java APIs for that type of extension, not SCIM.

How do you authenticate to OIM SCIM REST services?

OIM SCIM REST services relies on OWSM (Oracle Web Services Manager) for security. OWSM is Oracle’s strategic product for securing web services (including REST services) at the endpoint-level.

Central to OWSM is a powerful language for defining policies that control how a web service is secured; many policies are shipped out-of-the-box, and new policies can be defined to meet your specific needs. These policies are then attached to web service endpoints.

OIM ships with a policy known as oracle/multi_token_noauth_rest_service_policy to protect the SCIM REST web services. This policy is an OR combination of two out-of-the-box policies:

  • oracle/multi_token_rest_service_policy: this policy supports protecting REST web services with any of the following authentication mechanisms: HTTP Basic Authentication, SAML 2.0 Bearer Token, OAM Webgate, SPNEGO (Kerberos/WNA), and JWT
  • oracle/no_authentication_service_policy: this policy permits anonymous (unauthenticated) access when none of the above authentication mechanisms are used

Note that even though this policy permits unauthenticated access at the OWSM layer, the OIM SCIM server itself will restrict unauthenticated access to most of its services; the exception is those services specifically designed to be accessed without authentication, such as password reset.

Building a sample SCIM client app

In the following steps, I will walk you through building a very basic client app for the OIM SCIM server. This client app will just be a JSP page which displays some of the attributes of the logged in user. To secure this client app, we will use a SAML assertion generated by the OAM server; we will then configure the OIM SCIM server to accept that assertion.

The sample app in this example is a JSP. I have tested it on WebLogic, but it should run on other Java app servers and servlet containers too. But, even though this example is Java-based, the same basic steps can be followed on any other development platform – e.g. node.js or ASP.NET.

Prerequisites

I assume you have the following installed:

  • OIM 11.1.2.3 and OAM 11.1.2.3, running in separate WebLogic domains
  • OHS instance fronting both OIM and OAM
  • OAM WebGate installed in that OHS instance

Step 1: Creating the example application

First off, we will create a directory to hold our application. I’d generally recommend doing this using an IDE such as JDeveloper or Netbeans or Eclipse, and using a build tool such as Maven or Gradle. However, to keep this tutorial simple, we’ll just manually create the required files and ZIP them up into a WAR by hand, so all you will need to follow this tutorial will be a ZIP utility, your favourite text editor, and admin access to OIM, OAM and OHS instances.

I’m going to refer to the root directory of our sample app as $APP_TOP. Now you need to create subdirectories $APP_TOP/WEB-INF and $APP_TOP/WEB-INF/lib.

Since SCIM is a JSON-based protocol, we are going to need a JSON parsing library. There are many available; for this example, I’ve chosen to use the javax.json API (JSR 353) with the Glassfish implementation. If you prefer to use another JSON library (e.g. Douglas Crockford’s org.json reference implementation), it is not much work to change my example appropriately. Download the following two JAR files and place them in the $APP_TOP/WEB-INF/lib/ directory: http://central.maven.org/maven2/org/glassfish/javax.json/1.0.4/javax.json-1.0.4.jar and http://central.maven.org/maven2/javax/json/javax.json-api/1.0/javax.json-api-1.0.jar.

Next, create a file $APP_TOP/WEB-INF/web.xml and insert the following content:

<?xml version="1.0" encoding="UTF-8"?>
<web-app
        version="2.5"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
</web-app>

Now we create the file $APP_TOP/index.jsp and insert the following content:

<%@page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.io.*" %>
<%@ page import="java.net.*" %>
<%@ page import="java.nio.charset.*" %>
<%@ page import="java.util.*" %>
<%@ page import="java.util.zip.*" %>
<%@ page import="javax.xml.bind.*" %>
<%@ page import="javax.json.*" %>
<%@ page import="javax.json.stream.*" %>
<%!

public static String escapeHTML(String s) {
	return s.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll("\"","&quot;");
}

public static String gzipBase64(String s) throws Exception {
	byte[] b = s.getBytes(StandardCharsets.UTF_8);
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	GZIPOutputStream gzos = new GZIPOutputStream(baos);
	gzos.write(b);
	gzos.flush();
	gzos.finish();
	return DatatypeConverter.printBase64Binary(baos.toByteArray());
}

public static JsonObject getMyProfile(String token) throws Exception {
	URL url = new URL("http://OIMHOST:14000/idaas/im/scim/v1/Me");
	HttpURLConnection conn = (HttpURLConnection)url.openConnection();
	conn.setRequestProperty("Authorization","oit " + token);
	StringBuilder sb = new StringBuilder();
	JsonReader rdr = Json.createReader(conn.getInputStream());
        try {
		return rdr.readObject();
	} finally {
		rdr.close();
	}
}

public static String prettyPrint(JsonObject obj) {
	Map<String, Object> cfg = new HashMap<String,Object>(1);
	cfg.put(JsonGenerator.PRETTY_PRINTING, true);
	StringWriter sw = new StringWriter();
	JsonWriterFactory wf = Json.createWriterFactory(cfg);
	JsonWriter jw = wf.createWriter(sw);
	jw.writeObject(obj);
	jw.close();
	return sw.toString();
}

public static String htmlesc(String input) {
	return input.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;").replace("\"","&quot;");
}
	
%>
<%
	JsonObject profile = getMyProfile(gzipBase64(request.getHeader("OAM_IDENTITY_ASSERTION")));

%>
<html>
<head>
<title>Show My Profile</title>
</head>
<body>
<h1>Show My Profile</h1>
Find below your user profile.
<table border="1" cellspacing="0">
<tr><th align="left" valign="top">User Name</th><td><%=htmlesc(profile.getString("userName"))%></td></tr>
<tr><th align="left" valign="top">Display Name</th><td><%=htmlesc(profile.getString("displayName"))%></td></tr>
<tr><th align="left" valign="top">User Type</th><td><%=htmlesc(profile.getString("userType"))%></td></tr>
<tr><th align="left" valign="top">Active?</th><td><%=htmlesc(String.valueOf(profile.getBoolean("active")))%></td></tr>
<tr><th align="left" valign="top">JSON Profile</th><td><pre><%=htmlesc(prettyPrint(profile))%></pre></td></tr>
</table>
</body>
</html>

Replace OIMHOST in the above with the hostname of your OIM managed server.

Now we ZIP up our app into a WAR file:
cd $APP_TOP
zip -r ../ShowMyProfile.war *

Then we go to the OIM domain WLS console and deploy our WAR to the OIM managed server (e.g. oim_server1 or wls_oim1). Note that in Production use, it is advisable to deploy the custom app to its own managed servers in a separate WebLogic domain; however, for simplicity we are just using the OIM domain here.

Step 2: Modify the OHS Config

You need to have an OHS instance in front of the OIM WebLogic domain in order to protect it with the OAM WebGate. In this example, we are going to use the same OHS instance and WebGate for our custom UI. You could use a separate OHS for the custom UI instead of relying on the same one used for OIM – that would have the advantage of minimising the dependencies of the custom UI on the OIM environment, which might simplify upgrades and patching. However, to keep things simple, in this example we will use the same OHS.

Steps:

  1. Edit the file $IDM_TOP/config/instances/ohs1/config/OHS/ohs1/moduleconf/idm.conf.
  2. Look for the section <VirtualHost *:7778>.
  3. At the end of that section, just before the closing </VirtualHost>, insert the following block:
    <Location /ShowMyProfile>
       SetHandler weblogic-handler
       WebLogicHost myoimserver1.example.com
       WebLogicPort 14000
    </Location>
  4. Replace myoimserver1.example.com with the hostname of your OIM managed server machine, and if necessary replace 14000 with the correct managed server port. Save the file.
  5. Restart OHS by running $IDM_TOP/config/instances/ohs1/bin/opmnctl stopall followed by $IDM_TOP/config/instances/ohs1/bin/opmnctl startall

Step 3: Protect our custom app in OAM

We need to protect our custom app with OAM. We also need to configure OAM to provide our custom app with a SAML assertion for the authenticated user. Steps:

  1. Go to OAM console, i.e. http://OAMHOST:PORT/oamconsole
  2. Login as OAM system administration user
  3. On application security tab, Launch Pad, select “Application Domains” under the “Access Manager” tile
  4. Click “Search”
  5. Click on the “IAM Suite” link to edit it
  6. Go to Resources tab, click “Create”
  7. Fill out the “Create Resource” screen as follows, then click “Apply”:
    1. Type = HTTP
    2. Host Identifier =
      choose the appropriate host identifier
      (if you are using the default IAM Suite app domain,
      you probably want to choose IAMSuiteAgent as the host identifier)
    3. Resource URL = /ShowMyProfile/.../*
    4. Protection Level = Protected
    5. Authentication Policy = Protected HigherLevel Policy
    6. Authorization Policy = Protected Resource Policy
  8. Go back to you Application Domain tab, e.g. IAM Suite, then select the “Authorization Policies” tab
  9. Click on “Protected Resource Policy” to edit it
  10. Go to the “Responses” tab
  11. Check if the “Identity Assertion” checkbox is ticked
    If it is unticked, tick it then click “Apply” to save

Step 4: Import the OAM SAML certificate into the OIM domain

When the “Identity Assertion” checkbox is enabled in our authorization policy, the OAM WebGate will inject a special HTTP Header called OAM_IDENTITY_ASSERTION containing a SAML assertion. We use this SAML assertion to authenticate to OIM SCIM REST – we GZIP it, Base64 encode it (see the gzipBase64 method in index.jsp), then insert it as an Authorization: oit ... HTTP header in our SCIM request. The SAML assertion generated by OAM is signed using a private key; in order for OIM to trust that digital signature, we need to export the corresponding digital certificate from the OAM domain and import it into the OIM domain:

  1. Find out what your .oamkeystore password is using the following steps:
    1. Start $OAM_ORACLE_HOME/common/bin/wlst.sh
    2. Enter the command connect(), then enter your WebLogic admin credentials (e.g. weblogic user and associated password), and your OAM domain AdminServer URL (e.g. t3://OAMHOST1:7001)
    3. Enter the command print(mbs.invoke(ObjectName('com.oracle.jps:type=JpsCredentialStore'),"getPortableCredential",["OAM_STORE","jks"],["java.lang.String","java.lang.String"]).get("password"))
      (Note this command is all on one line. Also note that all we are doing here is calling a publically documented API using WLST, oracle.security.jps.mas.mgmt.jmx.credstore.JpsCredentialMXBean.getPortableCredential(String,String). See also the My Oracle Support Note Unable To Retrieve .oamkeystore Password In OAM 11.1.2.3.0 Using ListCred (Doc ID 2031132.1) which contains steps to call the same API using the EM System MBean Browser instead of WLST.)
    4. Note down the OAM keystore password printed, for example 3mredj8x4hg506xoifx1sk7snt
  2. Validate the keystore password retrieved is correct by running:
    $JAVA_HOME/bin/keytool -list -keystore $OAM_DOMAIN_HOME/config/fmwconfig/.oamkeystore -storetype JCEKS -storepass 3mredj8x4hg506xoifx1sk7snt -alias assertion-cert
    (Replace 3mredj8x4hg506xoifx1sk7snt with the actual password retrieved above)
  3. To export the assertion-cert certificate from $OAM_DOMAIN_HOME/config/fmwconfig/.oamkeystore, run the following command: $JAVA_HOME/bin/keytool -exportcert -keystore $OAM_DOMAIN_HOME/config/fmwconfig/.oamkeystore -storetype JCEKS -storepass 3mredj8x4hg506xoifx1sk7snt -alias assertion-cert -file /tmp/assertion-cert.cer
  4. Next we need to repeat the steps in (1) for the OIM domain. So start WLST again and connect to the OIM AdminServer instead of the OAM AdminServer, still as the weblogic user. This time the command to enter is: print(mbs.invoke(ObjectName('com.oracle.jps:type=JpsCredentialStore'),"getPortableCredential",["oracle.wsm.security","keystore-csf-key"],["java.lang.String","java.lang.String"]).get("password"))
    (Note this command is all on one line. Also note that it is the same command which we used for the OAM domain, just the map name is oracle.wsm.security instead of OAM_STORE, and the key entry is called keystore-csf-key instead of jks.)
  5. Note down the password printed in the previous step, for example tljig9hest0jpdgwnoqdl8dd45
  6. Validate the keystore password retrieved is correct by running:
    $JAVA_HOME/bin/keytool -list -keystore $OIM_DOMAIN_HOME/config/fmwconfig/default-keystore.jks -storetype JKS -storepass tljig9hest0jpdgwnoqdl8dd45
    (Replace tljig9hest0jpdgwnoqdl8dd45 with the actual password retrieved above)
  7. Import the certificate into the OIM domain keystore: $JAVA_HOME/bin/keytool -importcert -keystore $OIM_DOMAIN_HOME/config/fmwconfig/default-keystore.jks -storetype JKS -storepass tljig9hest0jpdgwnoqdl8dd45 -file /tmp/assertion-cert.cer -alias assertion-cert -trustcacerts

Step 5: Configure OWSM to trust the SAML issuer “OAM User Assertion Token”

OAM issues SAML assertions with the issuer set to OAM User Assertion Token. However, by default, OWSM only accepts www.oracle.com as a valid SAML issuer. So in this step, we need to tell OWSM to accept OAM User Assertion Token as a SAML issuer:

  1. Start $OIM_ORACLE_HOME/common/bin/wlst.sh
  2. Enter the command connect(), then enter your WebLogic admin credentials (e.g. weblogic user and associated password), and your OIM domain AdminServer URL (e.g. t3://OIMHOST1:7101)
  3. Enter the command setWSMTokenIssuerTrust("dns.hok","OAM User Assertion Issuer",[])
    (Note this command is documented in the OWSM 11.1.1.9 Admin Guide, chapter 14, section “Configuring an Issuer and its DN List Using WLST”)
  4. You will need to wait approximately 10 minutes due to caching in OWSM before this setting takes effect. (Alternatively, you can restart your OIM managed server.)

(Acknowledgement: Thanks to my colleagues Jiandong Guo and Andre Correa for recommending this approach to me.)

Step 6: Configure SAML2 Login Module to use correct attribute for username mapping

You must supply the LDAP attribute used to store usernames for login to OIM. By default this is “uid”, but you may have configured it to a different value in your environment. You can check this setting by going to WLS Console on the OIM domains’ AdminServer, and then checking Security Realms > myrealm > Providers tab > [name of your LDAP authenticator, e.g. OUDAuthenticator] > Provider Specific > “User Name Attribute”.

  1. In EM of the OIM domain, go to Farm > WebLogic Domain > [your OIM WebLogic domain name, e.g. IAMGovernanceDomain].
  2. On the “WebLogic domain” menu, go to Security > Security Provider Configuration
  3. Under “Web Services Manager Authentication Providers” > “Login Modules”, click on the row for “saml2.loginmodule” and click the “Edit” button
  4. Under “Custom Properties”, add the following two properties:
                saml.user.mapping.attribute=uid
                oracle.security.jps.dn.mapping.attribute=uid

     

  5. Click “Apply”
  6. You can double check the $DOMAIN_HOME/config/fmwconfig/jps-config.xml file to confirm the above settings change has taken effect. You should see a section like the following:
     
            <serviceInstance name="saml2.loginmodule" provider="jaas.login.provider">
                <description>SAML2 Login Module</description>
                <property name="log.level" value="FINE"/>
                <property name="saml.user.mapping.attribute" value="uid"/>
                <property name="oracle.security.jps.dn.mapping.attribute" value="uid"/>
                <property name="addAllRoles" value="true"/>
                <property name="loginModuleClassName" value="oracle.security.jps.internal.jaas.module.saml.JpsSAML2LoginModule"/>
                <property name="debug" value="true"/>
                <property name="jaas.login.controlFlag" value="REQUIRED"/>
                <propertySetRef ref="saml.trusted.issuers.1"/>
            </serviceInstance>
    
  7. Restart all your OIM managed servers to ensure this change has taken effect

Step 7: Testing

  1. In your web browser go to http://OHSHOST:OHSPORT/ShowMyProfile/. Replace OHSHOST with your OHS server hostname, and OHS port with your OHS port (e.g. 7778)
  2. You should see the OAM login page. Log in as a valid user (e.g. oamadmin)
  3. You should see a page showing some of your user attributes (“User Name”, “Display Name”, “User Type”, “Active?”), and also the pretty-printed JSON of your full SCIM user profile.
  4. Make sure to test with users whose CN and UID do not match. For example, create a user called “Test User1” with Display Name (CN) of “Test User1” and login (UID) of “testuser1”. If Step 6 is implemented correctly, the test will work for such a user. If Step 6 is not implemented correctly, the test will fail for such a user with a “LoginException” in the OIM managed server diagnostic log, but the test will still work for users such as xelsysadm for whom UID and CN are the same.

Note

The URLs and file paths in this post are from a LCM-based OAM-OIM non-clustered deployment, please make sure you use the proper URLs and file paths from your environment when implementing the solution describe in this post.

Add Your Comment