This post assumes basic knowledge about Sessions, login methods, State, Cookies and protocols such as OAuth, OpenID Connect and SAML. If any of these are not familiar to you, you may want to read up on them first.
During work on some projects lately I have come to ask myself one queston: 'Why don't we have a clear solution for stateless authentication and authorizaton for first-party clients?'
The world of authentication and authorization seems so simple:
- For first-parties we have the trusted (and widely used) sessions and cookies approach for maintaining login sessions, where the permissions are locally determined on the server, often based on some ACL (Access Control List).
- For third-parties we have OAuth(2) and OpenID Connect, or SAML which allow secure access to user details and data, and provide amazing benefits, such as being nearly stateless (including JWTs), having a decoupled Authentication Server and Relying Party (which allows for easy updating of the authentication methods, no updating of clients necessary) and more precise control over what permissions are requested (using scopes).
This does not cover all use-cases. We have skipped the third-parties using the cookies and sessions approach, which existed before OAuth, and is thus being fased out and deprecated.
However, more importantly, we have also skipped the use-case where we want our first-party app to use the benefits of the OAuth and OpenID Connect system. This post is about that last case, and the my eternal search for the best solution of implementing such a stateless system for both authentication and authorization in first-party apps.
That was, and still is my (first) reaction to this question. Shouldn't we have a clear answer to this question yet? After all this talk of microservices and deconstructing the monolithic applications into small parts, someone should have already solved this. Right?
First attempt: OAuth2 is unnecessarily complicated for this, I can do better
As anyone would, I started this journey thinking I could do better, I could write something easier than OAuth2 and OpenID Connect, while still providing all the benefits, but removing all the overhead not necessary for first-party clients.
First, I will provide some context on what I was working on: I was tasked to create a first-party mobile app, which should use Office 365 (through Azure AD) to allow employees to login. Eventually, they wanted to extend the app to a web-app and the manager should be able to login without using Office 365, and thus using local accounts. After login through Office 365, the user details would be sent back to the server, to be coupled with some other backend system data on that user. All that data combined would then form the user profile, thus also determening user rights.
From this client story, the following requirements emerge:
- User data from Office 365 should be sent from Microsofts servers, through the mobile app, to our servers, without being modified, because that may influence user permissions.
- I had the choice of either using SAML or OpenID Connect in Azure AD, but I would have to integrate the browser on the mobile device anyway, as I was (logically) not allowed to access user credentials for Office 365 directly.
My first solution was thus to use OpenID Connect with Azure AD (by integrating Microsofts ADAL library into the app) and have the mobile client send it's id token (from Azure AD) to the backend server on some /token endpoint, where it would be exchanged for a JWT (to maintain the statelessness), which the mobile client would use for communicating with our APIs. Our backend server would first verify that id token, and then combine the provided data with our data on that user.
I would implement the Azure AD login into the mobile app first, and then into the web app seperately. The login system for our management system would be seperate to cater to the need of password login (instead of Azure AD login).
Hmm, that seems, eh.. a bit... repetetory and non-futureproof?
Yes, yes it is. What would happen if they would have wanted Office 365 login on the management portal anyway? I would have to implement the same Azure AD system there as well (for the third time). What if Azure would make breaking changes to their OpenID Connect implementation? I would have to change it in two (or even three) places. What if they added or removed claims?
Also, I was implementing a token endpoint with the function of converting a JWT bearer (id) token from Azure AD into my own JWT format, and I would be using that same endpoint as well for password authentication from that management app. Essentially, I would have implemented the OAuth2 Resource Owner Credentials Flow and the OAuth2 JWT Bearer Token Flow anyway.
To summerize I had to build the following in my first model:
- Token Endpoint accepting both username & password (like the normal first-party authentication method we all know (and love?)) and the JWT token from Azure AD
- Either an implementation of a library (ADAL) or my own solution to handling the authorization process, such that I could obtain an id token from Azure AD. Because I was using Cordova, and I really wanted to use the secure Chrome Browser Tabs (not the Webview ADAL was using), I was already stuck with the second option.
- Custom JWT issuing implementation to issue my own stateless JWT access tokens after the user succesfully authenticated to the token endpoint.
Second attempt: What about, just using OAuth2 for first-party clients as well?
After a while it occured to me that I had essentially rebuilt OAuth2, without the implicit grant and auth code grant, but including my own custom JWT implementation. I already already had the code for these grants in most of my clients.
Wasn't it better to just use an off the shelf audited OAuth2 Server library then to create all of it yourself? That's when I started thinking about a way to do first-party authentication with OAuth2 itself (adding some things to make OAuth2 usable for authentication, for instance OpenID Connect).
I would just use the Auth Code Flow (with PKCE and without client_secret) for the mobile app, just like I would with Azure AD, but this time with my own /authorize endpoint as well, directly obtaining a JWT access token and id token, without the extra call to our API exchanging Azure ADs id token for our tokens. I could also use Implicit Flow for both our management app and the web app.
No need for the Resource Owner Credentials Grant anymore. We also gain SSO (logging in for the webapp can also mean automatic login for the management portal) benefits, because we are using our own /authorize endpoint.
We have also solved the previous issues: if the client wants Office 365 login on the management portal, I will just enable that for the management portal client in my OAuth2 Server implementation and it's done. If Azure AD changes, then I just edit my implementation once in the login system for my OAuth2 Server. No need to change all clients, they just keep using the same /authorize and /token endpoints the exact same way, as my JWTs have not changed at all. I even have the benefit of reusing my OAuth2 Server implementation in other projects because it's possible to build as a generalized module, where I can make all authentication providers (Office 365, password or even Google and Facebook) function as extensions.
So, we are done?
Woah, not so fast! Who said OAuth2 actually works in a first-party usecase? According to the spec (which is deliberately vague), this should work fine. During my actual implementation however, I encountered some fun problems, which I will describe in my next post in this series.
Some other thing to note is that, as I didn't want to provide a way to integrate my login into other apps as well (I don't trust my implementation that much over audited servers), I decided not to use OpenID Connect to provide information about the user and authentication, but to include that information into my JWT accesstoken instead. I still provide information about the user and authentication method, time and security in a legiable and signed way, so essentially it can still be used for authentication. Remember, OAuth2 alone is not enough for authentication, as you don't get any information about the authentication itself and when it occurred. The user may not be present anymore at all (which is often the case with third-party access).
You may not need all of OpenID Connect as well, depending on your use-case. You should however strive to implement as much according to the standards anyway, as implementing these sorts of things is always hard, and making mistakes is easy and may lead to data breaches.