Okta’s Not So Secret, Less Secure, API Authentication Method

Chaim Sanders
7 min readJun 23, 2023

--

Every week, almost without fail, I come across one thing that confuses, entertains, or most commonly infuriates me. I’ve decided to keep a record of my adventures.

Occasionally, when surrounded by the right folks (and right drinks) we’ll collectively travel down the rabbit hole of ‘the biggest issues with security programs’. Undoubtedly, one of my top answer is organizations’ ‘failure to play with their toys’. By this I mean that teams buy vendor tools, but really don’t use them to their full potential.

APIs (Author’s Note: which should always be freely included) allow customers to build the missing integrations that vendors haven’t gotten to (or won’t get to) themselves. As a result, I work with a lot of APIs and have pretty strong opinions about their design. Today, we’re gonna talk about Okta’s API and specifically some (arguably) bad authentication decisions the… authentication company made.

Background

Most APIs are pretty straightforward. This is by design; APIs represent well-specified interfaces to data and actions within a platform. In support of this, most modern APIs are structured in a RESTy manner. Authentication to APIs is most commonly done via API tokens or OAuth. OAuth (in many flavors) has the benefit of supporting things like MFA as part of the authentication process and has therefore become the more favored approach, however API tokens are still ubiquitous.

What is less common is cookie-based authentication. At the risk of rekindling existing philosophical debates, cookies are certainly the least REST-compatible authentication mechanism. I won’t delve too far into the specific reasons I believe this, but, I’ll summarize my argument as: It’s difficult to reasonably separate cookies from state, which makes them inherently less ‘stateless’.

The Problem

Okta’s API has a strange, somewhat abusive, relationship with cookies. The most obvious example of this is that all Okta’s read APIs can be called with a valid session cookie, directly from the browser. In fact, ALL Okta’s APIs I tested could be called with cookie based authentication, but there’s a catch, as we’ll see. You’d be forgiven for not knowing this, as it’s not documented and before you even ask, there is no way to disable or limit this. Don’t believe me? Try accessing https://{yourtenant}.okta.com/api/v1/users.

When used, session cookies act like API tokens in that APIs accessed with cookies don’t require MFA, device trust, or posture checking, even if the admin UI requires that. This is perhaps the number one reason that this capability shouldn’t exist. For the average (or even above average) Okta admin, it’s not straight forward to understand the ramification of cookie/session theft. Said another way, its tempting to think that the Authentication Policy that exists for the Okta Admin UI is a security boundary, when in fact its not. I’m going to talk a little bit more about this, but first, let me elaborate on the problem.

Recall above I split this issue into ‘read API calls (GET requests) from a browser’ and ‘all other API calls (POST, DELETE, PUT, etc.)’. The reason I did that is that Okta relies on the Same Origin Policy (SOP) and by extension Cross Origin Resource Sharing (CORS) to gate access to POST, DELETE, and PUT methods.

The astute reader, who is also overly familiar with CORS, would be a bit confused here. Their concern is justified. Standard CORS restricts communication by leveraging ‘preflight requests’ in combination with headers to determine if a subsequent cross origin request should be sent. Not a very robust system for API restrictions, right?

There are two types of CORS request, ‘simple requests’ (this includes basic GETs and POSTs), which don’t require preflights, and ‘advanced requests’, which do. Now, you might be tempted to say that all Okta APIs require application/json content-type headers, which makes them ‘advanced requests’, sorta true (turns out that’s not always the case — but more in a bit). So, given all this, the roundabout question is, how is Okta’s API preventing a browser making a POST request from say an HTML form, which is not beholden to CORS? Said another way, how is this not vulnerable to Cross Site Request Forgery (CSRF)?

The answer is: the way they utilize the ‘origin’ header. This is actual somewhat clever (and they’re not the first to think of this). Because browsers won’t let Javascript, forms, or any other cross-origin request control the ‘origin’ header. If you, being Okta, only allow requests with an ‘Origin’ header value from “trusted-domains” (which includes {tenant}.okta.com), then you are “guaranteed” that any attacker prompted request made in a victim’s browser won’t be successful. Right?

While that’s an interesting question, the real question here is, “is this an effective protection against CSRF?” Yes, but there are a couple of gotcha situations to using CORS/SOP in this manner that make it less than ideal.

  1. Origin headers aren’t sent on GET and HEAD requests, this probably explains why these succeed (see https://{yourtenant}.okta.com/api/v1/users). Okta’s options were either, they never work, or they always work — Okta chose the less secure option.
  2. If there is an XSS issue in a trusted origin you have effectively exposed your entire IdP. This is obviously bad, we’ll be delving into this more next week!
  3. Some ‘simple requests’ aren’t beholden to these ‘origin’ restrictions (as we saw in 1). If these requests can modify content, they needs to be VERY carefully considered. Surely no one would add these to the API, right? Well, there are a few examples of non JSON POSTs already, but fortunately they all require an origin header (which also makes them useless via this access method in pretty much all cases, except as an attacker).
  4. Most importantly, if the user session of an admin is stolen (the cookie for {tenant}.okta.com, not your {tenant}-admin.okta.com cookie/session), an attacker is able to act as an admin. Because CORS is only a browser limitation, any attacker with a stolen session cookie can simply add an Origin header. These weaker user sessions are controlled by Global Session Policy (GSP). In OIE GSP access is frequently set to be one factor with subsequent access to applications being controlled as a second factor (in fact this is the only way to support some apps using ‘password only’ while other apps use MFA). I want to belabor this point, in the event an Okta admins regular Okta session is compromised (again, not their session with {tenant}-admin.okta.com), any admin API can be called. As a result, Authentication Policies for the admin panel matter significantly less, pretty much not at all, because of this Okta architectural decision.

I suppose its worth demonstrating that this is true. Here is an example where a session cookie is used to call the user delete API. Again, every API tested works with this authentication method.

While testing this, I discovered a couple interesting things. For starters, POST requests that require content-type’s of application/json but don’t actually take parameters are more than happy to respond to application/x-www-form-urlencode. That could be handy if you can only inject HTML into a compromised trusted origin.

Additionally, not every API requires application/json as a content-type, regardless of what the docs say, some APIs accept multipart forms. These still required an Origin header to use in testing, which is good, but seems like an interesting anti-pattern. They are:

  • /api/v1/apps/{appId}/logo
  • /api/v1/brands/{brandId}/themes/{themeId}/background-image
  • /api/v1/brands/{brandId}/themes/{themeId}/favicon
  • /api/v1/brands/{brandId}/themes/{themeId}/logo
  • /api/v1/org/logo

So on the bright side, it appears that the enforcement of the origin requirement is consistent. On the down side, this opens up the possibility of error by introducing a GET based API that undertakes an action or has a side effect. I think its pretty unlikely that any of these would appear in the user facing APIs, however the undocumented APIs are a different, and unexplored, story.

The Solution

There are three general issues with Okta’s approach here:

  1. The first and most important issue. There is pretty much never a reasonable need for cookie-based API calls, and certainly not in a situation where they can’t be limited. As one of the most trusted systems, more attention should be paid to well-designed interfaces for administrative actions
  2. {tenant}.okta.com is not a trusted location for admin actions. This is probably a historical decision, but its a bad one. If you really want to allow API based authentication via cookie, it should be the session cookie from {tenant}-admin.okta.com.
  3. Don’t use CORS for purposes other than what it was designed for. It was designed as a mechanism to expand the same-origin policy, not limit access to APIs, using things outside their intended use case pretty much always ends up in a problem.

To be fair to Okta, there are common sense actions admins should take to prevent such issues.

  • Admin users should be relegated to a dedicated user that is only used for admin activity. Even though this is normally a recommendation, the big giant ‘admin’ button might seem awfully tempting as a security boundary, don’t listen!.
  • Don’t configure CORS Trusted Origins. In almost all cases whatever you are configuring is not needed. Even worse, what is being configured significantly reduces the level of security of your Okta tenant, just don’t do it!

Fun right?

--

--