Introduction
On a previous post, I described the usage of OAM’s SAML Identity Assertion in the context of SPA (Single Page Applications) and how easy it is to take advantage of it for securely propagating the end user identity from the client to the backend services. However, that post is written with the assumption that both the JavaScript code and the REST services are protected by the same WebGate. Speaking in HTTP protocol terms, we say they have same origin.
Modern web browsers natively take a security measure called Same-Origin policy, enforcing that a script can only invoke a service if both the script and service are served from the same host. This is done for avoiding a 3rd-party web site sending a malicious script to the user web browser, taking advantage of any browser session data (including http cookies), thus making itself able for executing remote calls to legitimate services.
In OAM real world deployment scenarios, it’s totally ok that customers want to use separate WebGates for JavaScript and REST services. This breaks the Same-Origin policy right away. The XHRs (XML HTTP Requests) made by the browser on behalf of the JavaScript would be automatically denied. This post describes how to deal with this by using the CORS (Cross Object Request Sharing) mechanism, as well as how to handle pre-flight requests and HTTP redirects in the context of REST services protected by OAM.
Setting the basis for this discussion, the deployment topology is depicted in the following diagram. It’s true that we could have a Reverse Proxy in front of the two WebGates as well. That would obviously moot this discussion, but that’s not what we want, since having WebGates directly exposed to clients is totally valid.
1 – The user first loads a JavaScript into the browser by accessing a resource protected by a WebGate running on host myapp.ateam.com.
2 – The Javascript (an AngularJS application) makes XHRs (GET and POST) to REST services protected by a Webgate running on host myservice.ateam.com.
It looks simple. But there are a few devils on the way.
CORS – Cross Object Request Sharing
Refer to this document for the CORS specification.
In a nutshell, CORS is a mechanism defined around the notion of allowing user-defined resources to relax the Same-Origin policy. These resources can basically tell the browser which are the origins they accept requests from. In our scenario, the REST services running on host myservice.ateam.com tell the browser to accept requests from myapp.ateam.com. There are many other aspects in CORS, like methods and headers allowed, credentials propagation and pre-flight requests. We’ll certainly touch upon them here, but it is highly recommended that you take a read on the CORS specification for fully understanding their semantics.
Handling Same-Origin Policy
Let’s assume the user has been authenticated and the AngularJS code, served by myapp.ateam.com, is loaded by the browser. It’s now going to invoke a REST service on myservice.ateam.com.
partsInvApp.controller('partsInvController', function ($scope, $http){ $http.get('http://myservice.ateam.com:7777/services/partsinventory/parts').success(function(data) { $scope.parts = data.result; });
For relaxing the Same-Origin policy, we need the following ‘Header’ directives in the routing rule for the backend services in mod_wl_ohs.conf of myservice.ateam.com’s OHS:
<Location /services> ## Handling the internal forward for the WebLogic server actually hosting the services SetHandler weblogic-handler WebLogicHost int.us.oracle.com WeblogicPort 8003 ## The following 'Header always set' directives are mandatory for cross domain XHR SetEnvIf Origin "(http://myapp.ateam.com:7777|null)" ACAO=$0 Header always set Access-Control-Allow-Origin %{ACAO}e env=ACAO Header always set Access-Control-Allow-Credentials "true" Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS" Header always set Access-Control-Allow-Headers "Origin, Content-Type, Accept" </Location>
Let’s first focus on the SetEnvIf directive and the 1st Header directive. The SetEnvIf is whitelisting “http://myapp.ateam.com:7777” and “null” values in the Origin request header. Only these two values are echoed back in Access-Control-Allow-Origin response header. Handling “null” is necessary due to HTTP redirects between the WebGate and OAM server. The browser sets the Origin header to “null” when a redirect is made to a different server than the one originally requested. That’s what happens, for instance, when the browser is redirected from http://myservice.ateam.com:7777/services/partsinventory/parts to http://<oam_server>:<port>/oam/server/obrareq.cgi.
Accepting “null” origins may ease the way for CSRF (Cross Site Request Forgery) attacks. In general, for guarding against CSRF attacks, do not rely on the Origin header, do not allow the “safe” operations like GET, HEAD and OPTIONS to change server-side data and use CSRF tokens in those considered “unsafe” operations, like POST, PUT, PATCH and DELETE.
When a request hits the WebGate on myservice.ateam.com, it will be detected there’s no OAM authentication cookie for that WebGate. Hence, an HTTP redirect is made to http://<oam_server>:<port>/oam/server/obrareq.cgi with OAM_ID cookie. OAM verifies the cookie and does another HTTP redirect, this time to myservice.ateam.com WebGate on /obrar.cgi, where the OAM authentication cookie is generated. Finally, the browser is redirected to the originally requested resource. This is just the way OAM works. It’s not at all particular to the use case here described.
From the standpoint of the JavaScript code, it doesn’t need to follow all those redirects. Even in the context of an XHR, the redirects are natively handled by the browser. Let’s notice this: in the context of the XHR. As such, the redirects are like just another XHR. As a consequence, CORS headers also need to be defined for them. So, re-reading the second last paragraph, here is the sequence of redirects that takes place:
1 – http://myservice.ateam.com:7777/services/partsinventory/parts -> http://<oam_server>:<oam_port>/oam/server/obrareq.cgi
2 – http://<oam_server>:<oam_port>/oam/server/obrareq.cgi -> http://myservice.ateam.com:7777/obrar.cgi
3 – http://myservice.ateam.com:7777/obrar.cgi -> http://myservice.ateam.com:7777/services/partsinventory/parts
Those redirects tell us we need CORS headers for the OAM server itself. How do we deal with this? We have to front-end OAM server with a reverse proxy that allows us to set those headers. Actually, this is a recommended approach for not exposing OAM server in the DMZ for internet facing web applications.
In my setup, I’ve used OHS (mylogin.ateam.com:7778) for the purpose and set CORS headers within mod_wl_ohs.conf:
# Handling OAM redirects in the context of XML HTTP Requests <Location /oam> SetHandler weblogic-handler WebLogicHost oamserver.us.oracle.com WeblogicPort 14100 SetEnvIf Origin "(http://myapp.ateam.com:7777|null)" ACAO=$0 Header always set Access-Control-Allow-Origin %{ACAO}e env=ACAO Header always set Access-Control-Allow-Credentials "true" Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS" Header always set Access-Control-Allow-Headers "Origin, Content-Type, Accept" </Location>
When front-ending OAM, it’s imperative to update OAM server host and port in OAM Console, per image below:
To fully satisfy the browser in that sequence of redirects, we also need CORS headers for http://myservice.ateam.com:7777/obrar.cgi. I’ve defined them as follows in myservice.ateam.com OHS httpd.conf:
# Handling OAM redirects in the context of XML HTTP Requests <Location /obrar.cgi> SetEnvIf Origin "(http://myapp.ateam.com:7777|null)" ACAO=$0 Header always set Access-Control-Allow-Credentials "true" Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS" Header always set Access-Control-Allow-Headers "Origin, Content-Type, Accept" Header always set Access-Control-Allow-Origin %{ACAO}e env=ACAO </Location>
With these in place, we satisfy requests with the GET method.
Handling POSTs
For handling POSTs, we need to know that browsers may decide to preflight the request. A pre-flight request is a preliminary inquiry to ensure the actual request is safe to be sent. So the browser first sends a request with the OPTIONS method, and the server replies back with the appropriate CORS headers, either allowing or denying the request.
Verify in this Firefox screenshot how an OPTIONS request is sent right before the actual POST, basically asking for authorization to submit the POST. This is evidenced by “Access-Control-Request-Method” request header. The server authorizes by issuing back the “Access-Control-Allowed-Methods” response header.
Now, a little devil here: do notice that OAM may also be protecting OPTIONS requests as well. As such, it would expect cookies in the request. The thing is that preflight requests, per CORS specification, exclude user credentials (including HTTP cookies). That basically means an anonymous request to an OAM-proteced resource, initiating an authentication flow, definitely not what our application expects. The solution to this is simply excluding OPTIONS as one of the supported methods in the protected resource. In fact, there’s no need for us to worry about the OPTIONS method in OAM, as long as the backend REST services either don’t support the OPTIONS method or don’t implement it incorrectly.
For excluding the OPTIONS method from OAM oversight, edit the corresponding resource in OAM Console:
We can even take the precaution of handling OPTIONS in OHS, thus preventing the request hitting the REST service endpoint in Weblogic server altogether. This can be implemented with the following directives within <Location /services> in mod_wl_ohs.conf:
RewriteEngine On RewriteCond %{REQUEST_METHOD} OPTIONS RewriteRule ^(.*) $1 [R=200,L]
We’re essentially returning a 200 HTTP status code for any OPTIONS request.
Handling OAM Timeouts
It might be the case that the end user leaves the application idle for some time. Upon her return, OAM might have timed out, either due to OAM idle timeout or OAM session expiration. The act of clicking some UI element (like a button or link) that triggers a REST service call must be handled in context by the application. One approach is forcing a new user login for the application as whole. Some people may feel this as disruptive. I like to think that an OAM-protected REST service is just another OAM-protected server-side resource within traditional web applications. And given the intrinsic stateless orientation of REST-based services, it should be fine that the user interface calls out a relogin.
Upon timeout, OAM does an HTTP redirect to http://<oam_server>:<oam_port>/oam/server/obrareq.cgi, which in turn brings in the SSO login page. Therefore, we have to handle this in OHS. And this has been already taken care when we dealt with the redirects that takes place during normal processing of REST services requests in the OAM front-end host.
# Handling OAM redirects in the context of XML HTTP Requests <Location /oam> SetHandler weblogic-handler WebLogicHost oamserver.us.oracle.com WeblogicPort 14100 SetEnvIf Origin "(http://myapp.ateam.com:7777|null)" ACAO=$0 Header always set Access-Control-Allow-Origin %{ACAO}e env=ACAO Header always set Access-Control-Allow-Credentials "true" Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS" Header always set Access-Control-Allow-Headers "Origin, Content-Type, Accept" </Location>
The AngularJS code must capture the redirect outcome (which is the SSO page) and preferrably redirect the browser window to the protected server-side resource in which the call is being executed. Yes, I agree that figuring out this context may vary in complexity depending on how the application has been designed. My use case here is the simplest, since I have only one HTML page serving the AngularJS code.
My sample employs an AngularJS interceptor, as follows. It basically looks for a specific string that I know is present in OAM’s login page. In finding it, it redirects the browser window to partsInventory.html location, the resource that is actually embedding the AngularJS code. Just remember to register the interceptor with AngularJS $httpProvider.
partsInvApp.factory('redirectInterceptor', function($q,$location,$window){ return { 'response': function(response){ if (typeof response.data === 'string' && response.data.indexOf("Enter your Single Sign-On credentials below") > -1) { $window.location = 'partsInventory.html'; return $q.reject(response); } else { return response || $q.when(response); } } } }); partsInvApp.config(['$httpProvider', function($httpProvider) { $httpProvider.defaults.withCredentials = true; $httpProvider.interceptors.push('redirectInterceptor'); }]);
Notice the line
$httpProvider.defaults.withCredentials = true;
This is what makes the browser sending over cookies along with HTTP requests in the context of XHR in AngularJS, a key requirement for OAM-protected applications.
Sample Code
Here’s the HTML code of my suboptimal Inventory application:
<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> <script src="./jquery-1.12.3.min.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="30%">Description</th> <th width="15%">Price</th> <th width="10%">Quantity</th> <th width="15%"> </th> </tr> </thead> <tbody> <tr ng-repeat="part in parts"> <td width="15%">{{part.uniqueid}}</td> <td width="15%">{{part.name}}</td> <td width="30%">{{part.desc}}</td> <td width="15%">{{part.price}}</td> <td width="10%" valign="top"> <input type="text" name="amt" ng-model="amt" size="3"> </td> <td width="15%" valign="top"> <button ng-click="orderPart(part.uniqueid, amt)" class="btn btn-sm btn-primary" ng-disabled="orderForm.$invalid">Order</button> </td> </tr> </tbody> </table> <h4 align="center" ng-if="PostDataResponse"> <span class="label label-success"> {{PostDataResponse}} </span> </h4> </div> </body> </html>
And its AngularJS code:
var partsInvApp = angular.module('partsInvApp', []); partsInvApp.factory('redirectInterceptor', function($q,$location,$window){ return { 'response': function(response){ if (typeof response.data === 'string' && response.data.indexOf("Enter your Single Sign-On credentials below") > -1) { $window.location = 'partsInventory.html'; return $q.reject(response); } else { return response || $q.when(response); } } } }); partsInvApp.config(['$httpProvider', function($httpProvider) { $httpProvider.defaults.withCredentials = true; $httpProvider.interceptors.push('redirectInterceptor'); }]); partsInvApp.controller('partsInvController', function ($scope, $http){ $http.get('http://myservice.ateam.com:7777/services/partsinventory/parts').success(function(data) { $scope.parts = data.result; }); $scope.orderPart = function(partId,amt) { if (!amt) { alert ("Please inform quantity."); return; } console.log("Placing part order for " + amt + " items of part " + partId); var data = $.param({partId: partId,amount: amt}); console.log(data); var config = { headers : { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8;' } }; $http.post('http://myservice.ateam.com:7777/services/partsinventory/order', data, config) .success(function (data, status, headers, config) { $scope.PostDataResponse = data.result; }) .error(function (data, status, header, config) { $scope.ResponseDetails = "Data: " + data + "<hr/>status: " + status + "<hr/>headers: " + header + "<hr/>config: " + config; }); }; }); partsInvApp.controller('userProfileController', function ($scope, $http) { $http.get('http://myservice.ateam.com:7777/services/userprofile/userinfo').success(function(data) { userinfo = data.result; $scope.firstName = userinfo[0].firstName; $scope.lastName = userinfo[0].lastName; }); });
Conclusion
In this post I’ve demonstrated how to handle the Same-Origin policy implemented by web browsers in the context of an SPA under the protection of Oracle Access Manager by using CORS headers plus the understanding of OAM-specific behavior. Combined with the usage of OAM’s Identity Assertion for secure identity propagation, this can be used in real world implementation scenarios, showcasing OAM again as a powerful tool for building modern and secure web applications.
All content listed on this page is the property of Oracle Corp. Redistribution not allowed without written permission