Pergunta

I am in the process of designing 3 components that will work in symphony with one another:

  • A RESTful web service which requires BasicAuth over HTTPS on all calls, and which is what actually does all the heavy lifting for my system (does the work)
  • A web UI that translates end user actions into API calls to the above-mentioned web service; hence the UI is "backed by" the WS
  • A command-line interface (CLI) tool that developers can install and run locally, that also translates commands into API calls to the WS (hence it too is "backed by" the WS)

One of the first hurdles I'm trying to cross is with respect to authentication and authorization.

Let's pretend that the WS uses a LDAP/directory service (like AD or perhaps Apache DS) as its authentication realm. Meaning, when an API call come in over the wire (say, an HTTPS GET for some resource), the BasicAuth credentials are extracted from the request, and forwarded on to the LDAP service to determine whether this is a valid user or not. If they are authenticated, then let's say that a separate authorization realm, perhaps a database, is used for determining whether or not the identified user can do what they are attempting in the HTTPS request. So far, so good.

In the case of the CLI tool, the user will have to authenticate prior to running any commands, and so this model works just fine, as a single user will only ever be operating the same CLI instance at a given time.

The problem comes when we try integrating the web app (UI) with the WS, because many people could be logged on to the app at the same time, all with different permissions dictating which underlying API calls they're allowed to make.

As far as I see it, it looks like I have but 4 options here:

  • Cached Credentials: After logging into the app, the credentials are somehow, somewhere cached (such that the app has access to them), and the app doesn't enforce any kind of authorization policy itself. When users attempt to do things that generate API calls under the hood, their credentials are looked up from the cache, and forwarded on with the API calls. If the WS determines they aren't authorized, it sends back an error.
  • Service-Level Accounts: The app and the WS both use the same authentication/authorization realms, except the web UI now enforces authorization on what users can actually see and do inside the app. If they're allowed to do something that generates an underlying API call, the app sends service account credentials (e.g. myapp-admin-user) with each API call on the user's behalf.
  • OAuthv2: I have no idea what OAuth is or if its applicable for this scenario, but feel it may be a solution here somehow.
  • Token Servers: Use a token server such as CAS or maybe Kerberos to vouch for users, in a similar way as the Service-Level Account option behaves. Here, when a user logs into the app successfully, the token server sends the app back a session UUID, and also registers that UUID with the WS. Every time the app generates an API call, it tacks the UUID on to the request, which is then validates on the WS side.

The "Cached Credentials" option just feels like an aberration of everything that is good and wholesome in security-land. It just feels wrong to cache credentials anywhere, ever.

The "Token Server" option seems valid for an SSO type setup, but not in this particular case and feels awkward to me. I also think there's no good way to use the session UUID concept and BasicAuth/HTTPS at the same time.

So this leaves OAuthv2, which I know nothing about, and "Service-Level Account (SLA)*" as the only remaining options. The SLA option seems OK, but has a few following drawbacks:

  • It requires the service account to basically have "god privileges" over the WS. In other words, once the app deems a user is allowed to click a button or do something in the UI, that translates to an unconditional API call by the service account being used by the UI. This just feels bad, mkay?
  • It occurs to me that maintaining two sets of permissions (the permission set for each user of the app, and then the permission set for the service account used by the app against the WS) can result with the permissions getting out of synch with each other somehow

So it appears that I don't really have any good options here. Surely I can't be the first dev to run into this, but asking the Google Gods have not helped me much here. Any ideas?

Foi útil?

Solução

There are many reasons not to use basic authentication scheme to protect Web API services.

In order to use the service, the client needs to keep the password somewhere in clear text to send it along with each request.

The verification of a password should be very slow (to counter brute force attacks), which would hamper scalability of your service. On the other hand, security token validation can be quick (digital signature verification).

OAuth2 does offer solutions for each of your use cases. Your web application can use the code grant, which gives it an access token it can use to talk to your API.

Your web application will redirect the user's browser to the authorization server. It will prompt the user for credentials (or smart card, or two-factor auth code) and return a code to the browser, which the client (your web application) can use to get an access token from the authorization server.

Your application will also get back a refresh token with which it can get a new access token if the current token expires.

Your CLI application can use the resource owner credentials grant. You will prompt the user for credentials and submit them to the authorization server to acquire an access and refresh token. Once your CLI application has the token, you can discard the user's password in memory.

Both clients (web app and command-line client) need to be registered up front with the authorization server.

Your authorization server may well talk to a LDAP/directory service (identity provider or IdP) to do the actual authentication.

Your web API service only needs to verify the incoming JWT token and establish what the user is allowed to do (authorization).

If you are the victim of a man-in-the-middle attack and you lose your access token, the attacker only has a limited time (token lifetime) to use it. A password is typically valid for much longer. Refresh tokens can be revoked in case they get lost.

Outras dicas

I'm working on a somewhat-similar system right now, actually; I'd be lying if I said that I knew the "right" way to make this work since I'm still experimenting but maybe going over what I've found to work might help. The setup is pretty heavily inspired by OAuth2 despite its drawbacks, some of which I'll discuss.

DISCLAIMER: I'm not a security guy by trade, and what I've built I've built with the help of Google and as many examples as I could find.

When I first started researching how I'd build the web API that would back the client application(s) I decided that I wanted to try and make the API as stateless as possible. Part of me was tempted to reach for HTTP basic auth and make users authenticate on every request, but two issues popped up that made that solution not seem viable:

  1. The lookup to validate the credentials is a non-trivial time expense, as it would involve at least a database call with each request
  2. The system is a multi-tenant system; identifying which of the tenants the user belonged to would require a third parameter and that isn't supported by HTTP basic auth (1)

The complexities involved in authentication made me opt for a token system, where the user would make an authentication request to an endpoint which would issue back an identifying token and then store that somewhere the server could later use it to validate requests and link it up with some necessary user data. It's not perfectly stateless, and I've been looking at JSON web tokens as an alternative approach, but the token lookup can be made very fast. (2)

Clients then hang on to that token until the server no longer accepts the token. The client then attempts to reauthenticate with the server and retrieve a new token to authenticate future requests with. This is what your post references as the cached credentials strategy, and we've opted to use it because it allows us to maintain more control over access to the application. Provided the client can be trusted to hold its own authorization information and is only connecting over a secure connection (we force HTTPS-only access for that reason) that's not necessarily a bad way to handle things, if only from a UX perspective. For the web service we actually hold on to the token in browser local storage; since it's only a temporary identification and not a user's actual username/password combination then we've deemed this "good enough" if not actually good.

The tokens are then sent to the web API as part of an Authorization header, or as a GET parameter for clients where custom HTTP headers are unavailable. This is important because it allows for a greater amount of flexibility in how we access the API from a wide range of potential client applications, a lot like how you need to support a CLI and a web app. Bearer tokens are a fairly common thing but they aren't exactly perfect. Our application's security concerns aren't quite important enough to devote additional time to improving this yet though.

Once the token is validated, authorization comes into play. What that entails can vary widely, but by that point in the application the user's identity is known and so an authorization service of some sort just needs to be given that user's identity and the object/action to check against.

At the very least, if you do want to use this kind of strategy, there's a lot of libraries which are designed to implement OAuth and OAuth2 out there; unless you're like us and have some very fringe requirements, I highly recommend using a trusted third-party security library because you will very likely not get things right the first time you try. I still look around for a third-party alternative to replace our current authentication system because I know it's full of holes and edge cases I can't even begin to imagine.


Footnotes

  1. This wouldn't be necessary if our system was built differently, say by using different points of entry for each client; alternatively I also considered getting clever and prefixing the username with a tenant identifier
  2. I've had some ideas on how to make the token string easy to validate purely computationally instead of having to do an I/O lookup as a long-term target for improvement; at the very least tokens contain a version byte which will allow for upgrades down the line if/when the decoding process for it changes
Licenciado em: CC-BY-SA com atribuição
scroll top