04 Feb 2015 Integrating Single-Page Apps with Oracle Access Manager

A clash of the old-school and new-school
Over the last couple of years at Shine we’ve built a number of Single Page Apps (SPAs) for large businesses. An app we recently built had to integrate with Oracle Access Manager (OAM), an Identity Management System (IDM) from everybody’s favourite enterprise software company. In this post I’m going to talk about a strategy that we used to successfully work around mismatches between the way that SPAs and OAM do things.
The Problem
We were building a SPA with Angular for our client. Our client mandated that OAM be used to secure our API endpoints. In short, this meant that OAM acted as a gateway to ensure that only logged-in users could do certain things. OAM also provided its own endpoints for logging-in and logging-out.
Using OAM was not negotiable – it was very much entrenched within the corporate ecosystem and required for single-sign to work across different parts of the business.
Our particular OAM installation was very old-school in that it assumed that our web app was always passing HTML from the server to the client. This meant that:
- If you weren’t logged in and tried to go to a protected page, it would redirect you to a login page
- When you logged-in successfully, it would redirect you to the logged-in home page
- If your login attempt failed, it would redirect you to the login page with an error code encoded in query parameters
- After you triggered a successful logout, it would redirect you back to the login page
Anybody who’s built a SPA would know that this is not the way SPAs normally work. By definition, SPAs only contain a single page, which usually makes XML HTTP Request (XHR) requests back to a server via a RESTful JSON API. Generally speaking, SPAs don’t make full-page browser requests, and they don’t expect HTML to be returned to them by the server.
Confronted by this incompatibility, it can be tempting to try to shoehorn a SPA into the full-page-reload model. This rapidly gets quite complex. For example, when logging-in you might try having the SPA navigate the whole browser to the login endpoint, then have OAM redirect back to the app URL, then have some server-side code examine the query parameters and inject them into the SPA when it boots.
As well as being both difficult to implement and test, this is also prone to some troublesome edge-cases – especially if the user uses the back button or bookmarks one of the URLs used en-route.
A Solution
Not many people realise that XML HTTP Requests (XHRs) – the HTTP requests that SPAs make from Javascript – follow redirects just like regular browser requests. This opened up an interesting option for us.
Like any IDM, the URLs that OAM redirects to are configurable. So we wondered: what if we configured OAM to redirect to an endpoint that returned some JSON rather than HTML? In a sense, this endpoint would act as a translator between OAM’s way of responding (i.e., redirecting to URLs) and our SPA’s preferred response type (HTTP status codes and JSON).
And where would this endpoint live? In our API, of course!
Some Examples
So what would this magical endpoint look like? I think the best way to understand it is to work through a couple of scenarios with accompanying sequence diagrams.
Access Denied
Firstly, let’s look at the case where the SPA makes a call to a protected /api
endpoint and gets bounced by OAM because the user is not logged in. This might be because they have never logged-in before, or have logged-in previously but their session has timed-out.
Apropos of anything, let’s just say the app wants to get profile information for the user, so calls an /api/profile
endpoint. With our special OAM callback in place, the sequence of calls might look something like this:
Our app submits an XHR to /api/profile
. OAM intercepts it and, because the user is not logged-in, instructs the browser to redirect to our special API endpoint: /api/oamCallback
. In the case of blocked access, OAM will also provide a request_id
query parameter to this endpoint. We’ll need this later if the user does a login, and it’s also useful for letting our callback know what just happened.
The browser follows this redirect and does not yet return control to our SPA. Our implementation of the /api/oamCallback
endpoint checks if the request_id
parameter has been provided and, if so, returns a 401. Our SPA can intercept this 401 and switch itself to its login page.
It’s important to note that the SPA has no idea that a redirect took place – all it did was make a call to /api/profile
and get back a 401, which it was able to process appropriately.
There’s one final important thing to note. You have to configure OAM to not block calls to the /api/oamCallback
endpoint if the user is not logged-in. Otherwise, the redirect to the callback will _always_ be blocked in this scenario, meaning that OAM will try and redirect again, which will put the browser into an infinite redirect loop.
Unsuccessful Login
Now let’s consider a login attempt that fails because bad credentials are provided:
The first thing our app does is submit a login XHR to another special endpoint. This one is implemented by OAM and is called /oam/server/auth_cred_submit
. Because OAM is old-school and is assuming this endpoint will be called from a HTML form, it expects the credentials to be form-encoded. However, it’s not hard for our SPA to craft an XHR that does this. Note also that OAM requires that we include the request_id
in the form.
In the event of an unsuccessful login, OAM will invoke our callback with the request_id
and another query parameter called p_error_code
. Our callback detects that p_error_code
was provided, and thus knows that an unsuccessful login attempt occurred. Consequently, it returns a 401 with a fragment of JSON that maps the error code to a message.
The SPA can detect this 401, extract the error message, and display it to the user.
Successful Login
OK, now it’s time for a successful login:
In this case, OAM redirects to our /api/oam-callback
endpoint again, but this time without a request_id
parameter or a p_error_code
parameter. Our endpoint detects that neither of these parameters have been provided, so it knows that a successful login just occurred and thus returns a 200. Our SPA gets this 200, and is able to assume the login is successful.
A Quick Aside on Performance
At first all of these redirects look like a performance regression. Isn’t the app now making two network calls for each of these scenarios? Well, it’s true that two network requests are being made, but this isn’t a regression. Two requests would be being made even if weren’t using our fancy callback scheme.
Not convinced? Let’s revisit my initial summary of how our OAM installation was configured to behave under different usage scenarios:
- If you weren’t logged in and tried to go to a protected page, it would redirect you to a login page
- When you logged-in successfully, it would redirect you to the logged-in home page
- If your login attempt failed, it would redirect you to the login page with an error code encoded in query parameters
Each of these scenarios still involve a redirect. Two calls are still being made to the server. It’s just that the whole browser window is following the redirect, rather than an XHR request that has been made by a SPA.
Logout
OK, now for our next scenario: logout. Unfortunately our callback scheme falls over for this one, but this is more because of OAM than anything else.
Like login, OAM has its own dedicated logout endpoint: /oam/server/logout
. However, it turns out that some pretty crazy stuff happens when you call this endpoint: it returns a web page with some Javascript in it that executes when the page loads. This Javascript redirects the browser to another page. All of this needs to happen for OAM to continue to work correctly if you’ve used its single sign-on capabilities.
Unfortunately, for this Javascript to be executed when the page loads, you need to load the page in the browser, not via an XHR request. XHR requests won’t execute any Javascript that gets returned.
We experimented with embedding this call within a hidden iFrame, but weren’t able to get it going correctly. It also proved very difficult to debug. In the end we just had to revert to a full-page request to the logout endpoint, and have it redirect back to the app page. However, this didn’t turn out to be too much trouble because logout pretty much amounted to a full reset of the app anyway.
Bootstrapping
So there you have it – a (mostly) simple solution that bridges the gap between the worlds of SPAs and OAM. However, an astute reader may be asking themselves: how does the SPA know when it starts up whether the user has a session in progress or not?
There are a number of scenarios where the SPA can be considered to have to ‘start up’. The most obvious is when the user navigates to the SPA for the very first time and all of the assets for the SPA need to be loaded into the browser. Other scenarios are when the user navigates the browser away from the SPA to another page, then goes back to the SPA. Or it could be something as simple as the user forcing a refresh of the SPA page.
In each of these scenarios, the SPA is effectively starting from scratch. It won’t remember the state it was in the last time it was started, so it won’t remember whether the user was logged in or not. Even if it did (for example, by checking for the presence of a cookie or looking for something in local storage), we can’t really rely on the client to safely keep track of that sort of thing. Furthermore, something may have changed on the server-side that means the session is no longer valid.
In short, to figure out whether the user has an OAM session in progress, the SPA it has to make an API call. Which call? Well, any protected call will do, although it helps if it’s a call that we actually need anyway. A good candidate is something like the /api/profile
endpoint that I used in my example scenarios. In the apps that I’ve built, we pretty much need profile data from the beginning anyway. For example, we often want to display the user’s name somewhere on the page to confirm to them that they’re logged-in.
Conclusion
Whilst the examples that I’ve presented in this post have been specific to OAM, the general strategy could probably be adapted quite well to any sort of web-service that insists on redirecting to URLs rather than returning status codes and/or JSON.
At first, it seems like a problem that can only be worked-around with nasty hacks. However, by introducing an intermediary service, you can bridge the gap in a reasonably elegant manner.
Mike
Posted at 03:25h, 18 NovemberI tried this approach but ran into a snag. When WebGate detects an unauthenticated request, it attempts to redirect to oamhost/oam/server/obrareq?blahblah, but the redirect fails because of the same-origin policy of the browser. We attempted to work around this by configuring a reverse proxy in the application’s web server to map apphost/oam/server/obrareq to oamhost/oam/server/obrareq but that failed when the OAM session was started by a 3rd party application (i.e. the session cookie was not accepted by the WebGate protecting my application). We tried to use CORS instead, but that failed because the origin header is set to “null” when OAM attempts to redirect back to the application. Did you not encounter similar issues?
Prasad Iyer
Posted at 01:42h, 05 DecemberI am running into the exact same issue. I am wondering if there is a way to detect the CORS failure in the JQuery ajax() parameters’ error function, and do a full browser redirect to the obrareq URL.
Ben Teese
Posted at 09:08h, 09 DecemberI’m afraid I’ve never seen that scenario – WebGate and OAM were on the same domain in my case, and we never had sessions being started by 3rd party applications (although we do share existing sessions with 3rd parties). Sorry I couldn’t be more help.
Ye
Posted at 02:11h, 17 MayBen,
Did you successfully configure the OAM+WebGate for this use case:
1) If the user session is timed out, for a service api call, OAM will redirects it to api which will return 401 status code
2) If the user is not logged-in, for a service api call, OAM will redirect it to api which will return 401 status code
Thanks
Ben Teese
Posted at 09:31h, 18 MayYes, I believe those are the use-cases I covered in the post.
Cheers,
Ben
Deva
Posted at 06:19h, 30 MarchHi – this is a very useful post – thank you!
I’m trying to do something very similar. The issue I’m hitting is figuring out how exactly to configure OAM to redirect to my custom endpoint. I tried 2 approaches, but both of those come close but don’t quite do what I want. I would appreciate any tips you might have on configuring OAM correctly.
Approach 1:
challenge type = external
challenge redirect url = http://myhostname:7777/v1/loginCallback
This works like a charm, and redirects to my endpoint. However, I don’t want to hard-code myhostname:7777 in OAM, because I want it to use the server on which the protected resource we are trying to access resides.
Approach2:
challenge type = customHtml
challenge redirect URL = empty
challenge URL = /v1/loginCallback
context value = /
This does send it to the right host/port, but it does not seem to use the ‘/v1/loginCallback’ – instead it sends the request to http://myhostname:7777/obrareq.cgi – which results in a 404 because I don’t have an obrareq.cgi in my application. I could make this work by redirecting obrareq.cgi to /v1/loginCallback in my application, but it would be nice to have a ‘clean’ configuration in OAM which would send the request directly to my /v1/loginCallback endpoint.
Is there any tweak to approach 2 or some other idea you can share to make this work?