OIDC and the Circle of Trust
In the first post, I spent time describing what a JWT is and how it is signed with an x509 certificate. That will be useful in understanding this second piece. In this second part of OIDC for SRE and DevOps level, I’ll cover the primary entities involved and their relationships for identity authentication. As I mentioned in the first post, OIDC is an extension of OAuth. Without OAuth, there is no OIDC. OIDC is actually a very small addition to the OAuth specification. So a lot of the explanation here requires explaining OAuth. I will get to OIDC by the end.
First thing to know: OAuth has no concept of Identity on its own. It is concerned with Authorization (delegated authorization to be specific). It intentionally omits identity based on its intended purpose.
Second thing to know: OAuth only works with an http/s redirect. It was designed to mitigate the risks associated with giving your username and password for one web service to other third party web services. So it involves redirects between http/s URLs as you interact with the Authorization server and the service you want to provide delegated authorization to.
If you’ve been around long enough, and still have enough memory cells left (I’m beginning to wane on the second part of that), you will recall when a web app would literally ask for your username and password to your email account. Then it promised to only access some small part of your data and forget your credentials. That’s a terrible plan, and I doubt anyone would do that today.
As this series of posts is intended to provide enough detail on OIDC for a DevOps or SRE to understand and configure it, I’d prefer to avoid going too deep on OAuth grant types/flows. But to understand what various configs could be required, you will need to develop an understanding of them. The Authorization Code Flow is used with Confidential Clients and is the most secure. In this grant type, the access token negotiation is performed on a back channel. More on this ahead.
OAuth grant types/flows depend largely on the type of Client/application we are integrating. Whether or not it can keep something secret from the public is paramount.
So the third thig we need to understand is that there are two categorizations of OAuth Clients. Confidential Clients are those that can keep a Client Secret protected. Public are those that cannot. Either type will have a publicly viewable Client ID. Only a Confidential Client can use a Client Secret.
An example of a Confidential Client would be a front end/back end web application with the OAuth negotiation occurring on a backend server. In this case, we can place the Client Secret in the config.
So, the next logical question is: “What is the Client secret and what is it used for?”. And I’ll do my best to answer this. This is seemingly one of the least well understood OAuth concepts, apparent if you Google it.
The Client Secret is essentially just a shared secret between the Client and Auth Server that is created by the Auth Server when we configured the application/Client.
The Client Secret is intended for one purpose; to prevent Client impersonation. When a Client authenticates to the Auth Server with a Client Secret, the Auth Server has verified it is the Client it claims to be. So, that means an Auth Server has no way to authenticate and verify the identity of a Public Client, and therefore, no way to prevent someone from impersonating a valid Public Client.
From what I’ve found and read, there seems to be a number of players attempting to solve this. But it seems for now (afaik), it is still a potential hole in the specification.
Another concept that seems to be often misunderstood is Proof Key for Code Exchange (PKCE). Many people think PKCE is the Public Client substitute for Confidential Client secret. It is not. Without going into great detail before I get into the grant types and flows, PKCE is intended to prevent something called authorization code injection.
Authorization code injection is simply a malicious application that registers a spoofed URL on the host an Client/application is running on. It is then able to intercept an Access Token. Originally added to the OAuth spec for mobile apps that tend to use localhost redirect/callback URIs, it is now recommended for use in all cases.
Ok, so I got into the weeds a little bit there. But it is necessary in order for us to understand the configuration options we’re presented with when adding an app to an Auth Server.
Back to OAuth basics. The following are terms we will need to know:
- Resource Owner – You, the person that types in your username and password to access your resources.
- Client – The application/service that you delegate authorization to. The thing you approve to access your resources without your username and password. Also referred to as Application in some contexts.
- Authorization Server – The API/service the Client receives credentials from to access the resources that you approve. Also referred to as ‘Issuer’ in come contexts.
- Resource Server – The API/service providing access to your resources.
- Scope – A set of read and/or write API permissions being granted via the Authorization Server API to access the Resource Server API endpoints. This will always be a subset of the Resource Owner’s permissions. (With OIDC, Scope will contain groups we’re members of)
- Token – AJWT used to send and persist bearer tokens, scopes, refresh tokens, etc.
The OAuth Circle of Trust:
This ‘Circle of Trust’ analogy is a bit off. We don’t have a circle that all entities reside in, and trust every other entity. It’s more of a circular flow of trust.
The Authorization Server (AS) and Resource Server (RS) can be on the same workload, or separate. The RS is where the resources reside (e.g. email apps, calendar apps, IoT, etc.), accessible via a REST like API. The RS trusts valid Access and ID Tokens signed by the AS.
The Authorization Server (AS) is where we as Resource Owners authenticate to consent to authorization requests, resulting in an Access Token being granted to a Client.. Access Tokens are what our Clients present to the RS so it knows what we are allowed to do (this is also referred to as API security). ID Tokens (minted when using OIDC) are what we provide to the RS to prove our identity and authentication status. The AS provides the CL with a JWT that contains these as bearer tokens and our Client presents these bearer tokens when we interact with the RS API.
An Access Token provides a list (Scope) of API endpoint privileges and a bearer token to authenticate with. The Scope is built from a list of API endpoints we have configured within the AS’ Application config. This is OAuth.
An ID token shows the remote server we are actively authenticated as a specific user. It also contains group membership with Scopes. Any server that trusts the AS can then use that ID and Scopes to apply to its internal ACLs/RBAC. This is OIDC.
So OAuth gives preconfigured permissions to Clients, so they can perform tasks on our behalf. OIDC provides proof of authentication so a service can apply our ID to any privileges it manages.
The first step in the circle of trust is to define an Application at the Auth Server. Within this definition, we give the application a name, a redirect (aka callback) URI (this is the URL of the Client application we should return to after authenticating to the Auth Server and consenting to the authorization request, grant type, and so on. As a result of this, our Auth Server will produce a Client ID and possibly a Client Secret.
The next step is to configure our Client with the Client ID, Secret (if it’s a Confidential Client), and any other info required for the Client to communicate with and trust the Auth Server.
We then also need our Resource Server(s) to expose REST like APIs that accept an OAuth Access Token signed by our Auth Server.
At this point, we have an Auth Server that trusts the Client, and a Client and Resource Server that trust the Auth Server. The JWTs the AS generates are signed by the AS with its certificate (discussed in previous post). This is how an RS trusts Access and ID Tokens it receives.
As a user (Resource Owner), I have resources on the Resource Server. I’ll use a calendar as an example with the Authorization Code Flow.
I visit a website that offers to a service to send me SMS messages whenever a meeting is updated. For it to do this, it requires (among some other things) access to read my calendar. Since my calendar is hosted by Google, I click on the allow access link for Google.
When I click on the link, the Client creates an Authorization Code Request (ACR). Within this ACR, it basically says “hey Google, I have no idea who this person is but they said I can read their calendar, here’s an identifier for the request. If you see them, send them back to this URL when you two are done”.
Simultaneously, the Client gives me the same identifier and redirects me to the Google Auth Server where I authenticate basically say “hey Google, here’s an identifier for something I have going on with some app you know about”.
Google Auth Server says “Yep, I see this ACR, it says it wants to have the following privileges (Scope). Do you consent?”. I say click yes and am redirected to the website.
Having consented, Google Auth Server sends an Authorization Code (AC) to the Client. The Client responds with the AC plus its Client ID and Client Secret. Google Auth Server validates the response and returns an Access Token to the Client. At no point in time have I disclosed my identity to the website application.
If we add OIDC to the equation, everything that happened above still happens, we simply provide additional information in JWT. Specifically, user identity in the form of an ID Token.
The Client can now present the Access Token to the Resource Server with the privileges to read my calendar. We will also likely prefer the Access Token periodically expire and require refresh. For this, the Auth Server can set the expiry time and provide an additional Refresh Token in the above process. This allows me as a user to remove consent at any point, stopping the next refresh.
The Client trusts the Auth Server, the Auth Server trusts the Client (If Confidential Client), the Resource Owner trusts that Auth Server, the Resource Server trusts the Auth Server. This is the circle of trust.
So that’s a lot more than I was planning to cover in one post. I’ll stop here for this one. I’ll try to get to covering how all of this config relates to a Kubernetes cluster in my next post. Along with some online resources you can use to play with the topics covered.