OAM Protected SPAs and Same-Origin Policy

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.

Deployment Topology

Deployment Topology

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:

OAM Front End Host

OAM Front End Host

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.

OPTIONS Request

OPTIONS Request

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:

 

Options OFF

Options OFF

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.

Comments

  1. Narsimha says:

    Thanks Andre,
    Given that our SPA is evolving into many such distributed APIs we would be better off moving to OAG so multiple SPAs can easily leverage those APIs.

    I would be interested in a blog from you on second factor auth in OAM PS3 without using OAAM, PS3 provides a 2nd factor auth options page , but unfortunately does not allow any customization of screen or if it is available via DCC. Would know ways/options around that ?

    Another blog I would be interested in is , if we put a thought into it, I am sure we can use OIM to do user access certifications only (just as sailpoint do) without having to request/approval workflow. Because OIM separate out request/approval workflows from access provisioning workflow, OIM should be able to take external feed of approved access and kick off access provisioning process – any blog on that ?

    Thank you for your blogs , enjoy reading them.

    Thanks
    Narsimha

  2. Narsimha says:

    Thank you Andre,
    We have recently implemented exactly this approach, however we faced one problem for POST which we have to work around by removing CORS for POST.
    Wonder if you overcame this.
    The problem is this, I am taking your example to explain the problem.
    When user first logs on and loads myapp.ateam.com. Now in the browser there is OAMAUthNCookie for this.
    Now the user clicks on the link that invokes the CORS call to myservice.ateam.com, so first a preflight request is made, and than a POST is made, but when the POST is made, myservice.ateam.com redirects to http://:/oam/server/obrareq.cgi with OAM_ID, but according to CORS specification, a redirection cannot be allowed on a POST, so this fails.
    A workaround is either do a GET on myserver.ateam.com (it can be hidden protected gif on myserver,ateam.com OHS) so that the OAMAuthnNCookie for myservice is placed in the browser and then make a POST call that will not result in a 302. This is awkward .
    Another way is we proxied the myservice resource via myapp for all POST/PUT/DELETE operations and only have GET on myservice OHS. This too is awkward.
    Did you not encounter such a issue ?

    Thanks again.
    Narsimha

    • Hello Narsimha,

      in my sample I have a GET executed before the POST, so I do not see the issue you report.

      But, as you point out, I’ve just implemented your scenario and I do acknowledge the issue when POST is the first request to myservice.ateam.com in all browsers tested (Firefox, Chrome and Safari. IE testing coming soon). However, I see different behaviors on each browser. Firefox seems to be the only one preflighting the POST request. I don’t see Chrome and Safari preflighting the POST.

      In Firefox and Chrome, the redirect on POST is allowed. But they are both blocked when GETting /obrar.cgi on myservice.ateam.com due to an absent Access-Control-Allow-Origin header.

      Safari (9.1.1) doesn’t preflight the POST, but it does preflight the following GET to /oam/server/obrareq.cgi on mylogin.ateam.com. However, it fails right there with messagge “Failed to load resource: Cannot make any requests from null.” Safari actually seems very particular on CORS. It seems to preflight any redirect requests (including GETs), and fails with the aforementioned message in all cases.

      With that said, it doesn’t seem too bad or expensive performing a single dummy GET request to myservice.ateam.com in the first place before any POST (works in Firefox and Chrome). Safari still a case deserving more study.

      Thanks,

Add Your Comment