Introduction
OAM (Oracle Access Manager) has an interesting feature that often goes unnoticed to a considerable number of people wishing to tackle the problem of identity propagation. It’s OAM’s ability to generate a secure token embedding user information as a result of successful authentication or authorization. My colleagues Rob Otto and Simon Kissane have talked about it in “Retrieving the OAM SessionID for Fun and Profit” and “Authenticating to OIM SCIM server using an OAM-generated SAML identity assertion”.
Motivated by a recent customer inquiry, on this post I want to talk about such ability from the perspective of browser-based clients invoking REST services, a very common pattern these days. Imagine, for instance, that the end user identity must be securely propagated from an AngularJS-based application to REST services deployed on Weblogic server or JBoss. While there are a few solution options to consider, here I want to focus on this pre-built OAM feature that requires near-to-zero implementation effort. Additionally, I want to say a few words on how to customize that secure token by adding extra information to be eventually consumed by downstream resources.
The use case in question is as simple as this:
In the context of Oracle Fusion Middleware, the Identity Assertion feature usage is covered in Using Identity Context chapter of OAM’s Administrator’s Guide.
The Identity Assertion
A protected resource in OAM is associated with an Authentication policy and, optionally, with an Authorization policy.
In OAM admin console, if you look at the Response tab of either Authentication or Authorization policy, there’s a check box named “Identity Assertion”.
Marking the checkbox makes OAM server issuing a SAML assertion as a result of successful authentication and/or authorization. OAM then adds the assertion as an HTTP response header named “OAM_IDENTITY_ASSERTION” back to the requesting Webgate. This is a very important aspect to understand: the response header is NOT sent back to the browser, which is actually something very welcome, because it makes it very hard for any token hijack attempt. With the request authorized, the Webgate turns the response header into a request header to be forwarded to the downstream resource being invoked by the HTTP Server.
Pretty much the same process that natively happens with OAM_REMOTE_USER HTTP header, who already has the end user identity. So, why bother with OAM_IDENTITY_ASSERTION? For basically three reasons (1 and 2 actually being consequences of 3):
1) it’s safer, because it’s digitally signed by OAM server;
2) it can convey much more information than a simple user id;
3) it’s a standard SAML assertion, therefore, interoperable.
Here’s how it looks like:
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" Version="2.0" ID="fd53fa85-4646-41e3-9d4b-e95bc3c56b33" IssueInstant="2016-03-31T12:49:06Z"> <saml:Issuer>OAM User Assertion Issuer</saml:Issuer> <dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"> <dsig:SignedInfo> <dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> <dsig:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> <dsig:Reference URI="#fd53fa85-4646-41e3-9d4b-e95bc3c56b33"> <dsig:Transforms> <dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> <dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> </dsig:Transforms> <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> <dsig:DigestValue>KWCLd8vBg0KW5fBHD+D7ALxLsh4=</dsig:DigestValue> </dsig:Reference> </dsig:SignedInfo> <dsig:SignatureValue>R9fNdqSqiTQaG5gDDjv5Gue3ziZPNUfLgcT880ViUDiN3HcCpKLJ1L2PIKfQgMIjajZXO/PN/j+IC8SlmBeRZ/bI9BmHF9skqI2A+Q0+uJfgqnyw+Fy/nIPGGraTK3AVsivv5j5tkdeDVJ+dBUfBT+Gf6A/onVp7YSwpAQ48psg=</dsig:SignatureValue> <dsig:KeyInfo> <dsig:X509Data> <dsig:X509Certificate>MIIBxzCCATACAWYwDQYJKoZIhvcNAQEEBQAwLDEqMCgGA1UEAxMhT0FNIFVzZXIgQXNzZXJ0aW9uIElzc3VlciBDQSBSb290MB4XDTE1MTAxMzE1MTIwMVoXDTI0MDMyMjE1MTIwMVowLDEqMCgGA1UEAxMhT0FNIFVzZXIgQXNzZXJ0aW9uIElzc3VlciBDQSBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCX1C6Qrk42DsLD0QC4mx9U0kyl2MD6K1qu13N9qqv/xYHi2nmM6h/M8frFP0Czngjlm7gHzgHDRVLkMBxEiOOOpChOnygF0OhdrmeziwUNd2VxjKf8pDU17YYR06lwj4ad702Z4dFmz+rsBX/MPap8XzfwOa6Dj1DPa/5xC7buswIDAQABMA0GCSqGSIb3DQEBBAUAA4GBADCM5s2fUm4lHenm3BlRwq8JVjj6D31DWKuN4qjMKY1vHluqmfexjofzs2PtAk/4bwZN4DIKJg6qVTs5YqStlGcvDsaBsSJoxEmPOJ8PF7jdDP1bxZfxfz6AajthA4fMfwPfVDu++VGEBZ9AYBc7f9tskIDN/TVyntQlWD1he9Ru</dsig:X509Certificate> <dsig:X509IssuerSerial> <dsig:X509IssuerName>CN=OAM User Assertion Issuer CA Root</dsig:X509IssuerName> <dsig:X509SerialNumber>102</dsig:X509SerialNumber> </dsig:X509IssuerSerial> <dsig:X509SubjectName>CN=OAM User Assertion Issuer CA Root</dsig:X509SubjectName> </dsig:X509Data> </dsig:KeyInfo> </dsig:Signature> <saml:Subject> <saml:NameID NameQualifier="oud_slc09iug" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName" SPProvidedID="SADMIN">uid=SADMIN,cn=Users,dc=oracle,dc=com</saml:NameID> <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml:SubjectConfirmationData Address="10.88.248.71"/> </saml:SubjectConfirmation> </saml:Subject> <saml:Conditions NotBefore="2016-03-31T12:49:06Z" NotOnOrAfter="2016-03-31T20:49:06Z"/> <saml:AuthnStatement AuthnInstant="2016-03-31T12:49:00Z"> <saml:AuthnContext> <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Unspecified</saml:AuthnContextClassRef> </saml:AuthnContext> </saml:AuthnStatement> <saml:AttributeStatement> <saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" Name="urn:oasis:names:tc:SAML:2.0:profiles:session:sessionId"> <saml:AttributeValue xsi:type="xs:string">a316e3d4-0a54-4c28-a398-694dad853b1a|4QCSd0VILGDCLvqf5WH+l566Mbk=</saml:AttributeValue> </saml:Attribute> <saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" Name="urn:oasis:names:tc:SAML:2.0:profiles:session:authenticationStrength"> <saml:AttributeValue xsi:type="xs:integer">2</saml:AttributeValue> </saml:Attribute> <saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" Name="urn:oasis:names:tc:SAML:2.0:profiles:session:timeLastActive"> <saml:AttributeValue xsi:type="xs:dateTime">2016-03-31T12:49:06Z</saml:AttributeValue> </saml:Attribute> <saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" Name="urn:oasis:names:tc:SAML:2.0:profiles:session:tokenFormatVersion"> <saml:AttributeValue xsi:type="xs:string">1.0</saml:AttributeValue> </saml:Attribute> <saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="oracle:idm:claims:ids:attributes"> <saml:AttributeValue xsi:type="xs:string">email=sadmin@oracle.com</saml:AttributeValue> </saml:Attribute> <saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="oracle:idm:claims:tenant:name"> <saml:AttributeValue xsi:type="xs:string">siebel</saml:AttributeValue> </saml:Attribute> </saml:AttributeStatement> </saml:Assertion>
A few important aspects to notice when looking at the Identity Assertion are listed below. All these are valuable information for the component/agent that is going to validate the assertion or make authorization decisions based on user information right before letting the request hit the invoked resource.
1) The SAML issuer: the value always is “OAM User Assertion Issuer”.
2) The Digital Signature, signed with OAM server private key. A required task in verifying the assertion is veryfing the signature. As such, OAM server public key must be exported and made available to the validating agent. Execute the following steps for exporting the public key (courtesy of my colleague Simon Kissane):
a) Find out what your .oamkeystore password is:
Start $OAM_ORACLE_HOME/common/bin/wlst.sh
> cd $OAM_ORACLE_HOME/common/bin > ./wlst.sh > connect(); <enter username and password> > print(mbs.invoke(ObjectName('com.oracle.jps:type=JpsCredentialStore'),"getPortableCredential",["OAM_STORE","jks"],["java.lang.String","java.lang.String"]).get("password")); <copy password output> > exit();
b) To export the assertion-cert certificate from $OAM_DOMAIN_HOME/config/fmwconfig/.oamkeystore, run the following command:
> export OAM_DOMAIN_HOME=<path_to_your_oam_domain_folder> > cd $JAVA_HOME/bin > ./keytool -exportcert -keystore $OAM_DOMAIN_HOME/config/fmwconfig/.oamkeystore -storetype JCEKS -storepass <password_obtained_in_step_a> -alias assertion-cert -file /tmp/assertion-cert.cer
3) The SAML Subject NameID: contains the user’s DN (Distinguished Name) in the underlying LDAP server. If the validating component/agent needs to establish a user context, this is the identity to parse and assert.
4) The SAML AttributeStatement: containing both OAM session information and custom user attributes. Notice the element
<saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" Name="urn:oasis:names:tc:SAML:2.0:profiles:session:sessionId"> <saml:AttributeValue xsi:type="xs:string">a316e3d4-0a54-4c28-a398-694dad853b1a|4QCSd0VILGDCLvqf5WH+l566Mbk=</saml:AttributeValue> </saml:Attribute>
As pointed by Rob Otto on his post, the string a316e3d4-0a54-4c28-a398-694dad853b1a|4QCSd0VILGDCLvqf5WH+l566Mbk= corresponds to the user session id in the OAM server. It could be used by the validating component/agent, for instance, to cross check the identity assertion with a valid OAM session, although I wouldn’t recommended it to be used without considering the security requirements of the service in question being accessed. Reason is performance, since that would involve a remote call back to OAM server on every service access. Verifying the digital signature seems good enough for the whole majority of services, since a compromised OAM’s private key means a deep serious disaster already. But if for some reason we really need to cross check the session id, one idea is doing it selectively, based on some custom attribute in the identity assertion itself.
As mentioned, extra information can be added to the identity assertion. That’s also done in OAM console’s Policy Response tab, by using the “Asserted Attribute” response type and adding the user attribute name in the underlying LDAP server to $user.attr. The screen shot below shows the adding of the user “mail” attribute.
That makes up for the following Attribute element in the assertion:
<saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="oracle:idm:claims:ids:attributes"> <saml:AttributeValue xsi:type="xs:string">email=sadmin@oracle.com</saml:AttributeValue> </saml:Attribute>
For an overall discussion on Policy Responses, look at Introduction to Policy Responses for SSO.
Securing the Application
Now that we have good grasp on the identity assertion feature, integrating it into our use case scenario becomes simpler.
A JavaScript-based client web application is typically hosted by an HTML page. Executed by the web browser, it makes AJAX-like calls to REST services. We want to protect the services and the HTML page itself with OAM. The user typically authenticates to OAM when requesting the HTML page. The AJAX-like calls also go through the Webgate, when an authorization policy is triggered and the identity assertion is issued back to the Webgate, that adds it to the outgoing request to the service endpoint. From OAM’s perspective, the point to consider here is that a REST service is nothing different than a traditional web-based resource typically secured by OAM. The diagram below illustrates the REST service invocation.
From an OAM policy standpoint, there are three resources to protect. The HTML page along with the JavaScript resource and the services. Since we’re only interested in generating the assertion when invoking the service, I’d have two separate authorization policies: one for the HTML and the JavaScript, with no assertion generation; and another one for the services, this time marking the “Identity Assertion” checkbox.
Let me reinforce the importance of protecting the hosting HTML page in the first place. That’s an absolute must, or an unauthenticated request to the REST service will disrupt the user experience, because the redirects during login time will bypass what you intended to provide with your nice and slick JavaScript code. Another aspect worth mentioning is regarding to OAM session timeouts when a REST call is made. The JavaScript must be able to handle such event, possibly sending a request to the hosting HTML page, so that the whole process starts over and the user experience is preserved.
Sample Code and Configuration
I’ve done a small implementation myself. I’ve deployed my REST service on JBoss EAP and used OHS as my HTTP server. If you’ve followed along, understanding the artifacts in this section is straightforward. Please, notice that JBoss has no pre-built agent for validating the identity assertion. As you can see by looking at the REST Service Implementation below, it simply prints the OAM_IDENTITY_ASSERTION request header, just to confirm it indeed comes in. Writing an agent and integrating it with JBoss Login Module is the custom work necessary for this specific scenario. Important to mention this is not the case of Weblogic server in Oracle Fusion Middleware, where you have OWSM to the rescue.
HTML
<html ng-app="partsInvApp"> <head> <meta charset="utf-8"> <title>Parts Inventory Application</title> <link href="bootstrap/css/bootstrap.min.css" rel="stylesheet"> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular-cookies.js"></script> <script src="./partsInvApp.js"></script> </head> <body> <!-- <p ng-controller="userProfileController"> Welcome <b>{{firstName}} {{lastName}}</b>, check out our inventory list</p> --> <div style="width:600px" width="600" class="table-responsive" ng-controller="partsInvController"> <table class="table table-striped" style="width:600px" width="600"> <thead> <tr> <th width="15%">Id</th> <th width="15%">Name</th> <th width="40%">Description</th> <th width="15%">Price</th> <th width="15%"> </th> </tr> </thead> <tbody> <form name="orderForm"> <tr ng-repeat="part in parts"> <td width="15%">{{part.uniqueid}}</td> <td width="15%">{{part.name}}</td> <td width="40%">{{part.desc}}</td> <td width="15%">{{part.price}}</td> <td width="15%" valign="top"> <!-- <form name="orderForm"> --> <input type="hidden" name="partid" value="{{part.uniqueid}}" ng-model="part.uniqueid"> <button type="submit" class="btn btn-sm btn-primary" ng-click="orderPart(part)" ng-disabled="orderForm.$invalid">Order</button> <!-- </form> --> </td> </tr> </form> </tbody> </table> <h4 align="center" ng-if="submitResult"> <span class="label label-success"> {{submitResult}} </span> </h4> </div> </body> </html>
AngularJS
var partsInvApp = angular.module('partsInvApp', []).config(['$httpProvider', function($httpProvider) { $httpProvider.defaults.withCredentials = true; }]); partsInvApp.controller('partsInvController', function ($scope, $http){ $http.get('http://den00hdo.us.oracle.com:7777/services/parts').success(function(data) { //$http.get('services/partsinventory/parts').success(function(data) { $scope.parts = data.result; }); $scope.orderPart = function(part) { var config = { params: { part: part } }; console.log(config.params.part.uniqueid); $scope.submitResult = "Order successfully placed for part id " + config.params.part.uniqueid; }; });
http://den00hdo.us.oracle.com:777/services/parts points to the Oracle HTTP Server, who in turn forwards the request to the backend service. See below.
Oracle HTTP Server mod_proxy Rule
This is within OHS httpd.conf.
NameVirtualHost *.7777 <VirtualHost *:7777> # Routing to REST service ProxyPass /services/parts http://den00hdo.us.oracle.com:8080/services/rest/partsinventory/parts ProxyPassReverse /services/parts http://den00hdo.us.oracle.com:8080/services/rest/partsinventory/parts </VirtualHost>
/services/parts is the resource for which an Authorization Policy with Identity Assertion checkbox marked must be created in OAM console.
REST Service Implementation
package oracle.ateam.sample.partsinvapi; import javax.ws.rs.CookieParam; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.Path; import javax.ws.rs.Produces; @Path("/partsinventory") public class PartsInventory { public PartsInventory() { super(); } @GET @Produces("application/json") @Path("parts") public String getParts(@HeaderParam("OAM_IDENTITY_ASSERTION") String identityAssertion) { System.out.println("** DEBUG: PartsInventory web service: Request header OAM_IDENTITY_ASSERTION: " + identityAssertion); try { String result = "{\"result\":"; result += "["; result += "{\"uniqueid\" : \"123\", \"name\" : \"ABC\", \"desc\" : \"This is part ABC\", \"price\" : \"100.00\"},"; result += "{\"uniqueid\" : \"456\", \"name\" : \"DEF\", \"desc\" : \"This is part DEF\", \"price\" : \"200.00\"},"; result += "{\"uniqueid\" : \"789\", \"name\" : \"GHI\", \"desc\" : \"This is part GHI\", \"price\" : \"300.00\"}"; result += "]"; result += "}"; return result; } catch (Exception e) { e.printStackTrace(); return "{\"error\":\"" + e.getMessage() + "\"}"; } } }
Conclusion
On this post, I’ve demonstrated how to use a sometimes unnoticed yet powerful feature of OAM for implementing identity propagation between a JavaScript-based client web application and REST services, a very common architectural pattern nowadays. So, if you’re already using OAM to secure your traditional server-side web applications, like JavaEE, Perl, PHP, etc, and want to embrace this new application model, you have a tool right at your fingertips to use, and with minimal implementation effort.
All content listed on this page is the property of Oracle Corp. Redistribution not allowed without written permission