OpenID Connect is an authentication standard built on top of OAuth2. From my point of view it has the following key features:
- It’s a lot simpler than anything involving SAML. Validating SAML requires a full implementation of XML Signature, which requires an implementation of XML Canonicalization, which requires a full XPath implementation. I’m not anti-XML in general, but I don’t think authenticating a user should require parsing, traversing and rearranging a DOM tree multiple times.
It’s more secure than OAuth2, as we’ll see below.
It can be implemented in situations where your web application can only reach the authentication provider via HTTPS — you don’t need to be able to make LDAP or Active Directory connections.
It’s an open standard.
The downsides of OpenID Connect?
- It’s not very popular. Most Internet authentication providers seem to have rolled their own systems based on OAuth2.
It’s kind of a pain in the ass to implement.
The documentation is long and somewhat unapproachable.
This is a summary of the key information I wish someone had given me before I tried to make sense of OpenID Connect.
An OpenID Connect authentication provider has a set of four key endpoints:
- An authentication endpoint. You direct the user’s browser to this via an HTTP 301 redirect in order to start the authentication process. The user is bounced back to your web application after logging in, with parameters added to that HTTP request. The parameters may be a code, an ID token, an access token, or some combination of the three.
A token endpoint. This is a REST API your application can use to obtain an id token or access token, given a code.
A user info endpoint. This is a REST API your application can use to obtain information about a user, given an access token. Typically it’s used to get information that doesn’t fit inside the ID token, such as the user’s avatar image. It also may not be supported, as it’s totally optional.
An introspection endpoint. This is a REST API you can pass an access token to. It will return information about the token, such as whether it’s valid, when it will expire, and so on.
There are other optional endpoints, but those are the most important.
As mentioned in the above descriptions, OpenID Connect involves passing around three key pieces of information:
id_tokenis what you typically want — key information about the logged-in user’s ID and their name.
tokenin some places) is a token you can use to connect to the token endpoint, userinfo endpoint or introspection endpoint. It therefore allows you to obtain an ID token, get more detailed information about the user, or find out when the granted authentication will expire.
codeis a one-time code you can use, accompanied by a secret ID and password you’ve pre-arranged with the authentication provider, to connect to the token endpoint and get a token.
Given the above, there are several ways to do OpenID Connect.
With Implicit Flow you bounce the user to the authentication endpoint, and when they return you’re passed an ID token (and optionally, an access token so you can look up information that doesn’t fit in the ID token).
The advantage of implicit flow is that it’s really easy to implement. The disadvantage is that it’s not very secure.
For example, browser malware such as a malicious extension can sniff the tokens from the response. Because the ID token is a standard format, it can collect the user’s account information. The stolen access token can be used with the authentication provider’s APIs as well.
There are some ways to mitigate the risk somewhat, but a much better option is…
With Authorization Flow you bounce the user to the authentication endpoint; but when they return, all your app gets is an opaque code. You then need to make an authenticated REST call to the token endpoint, using an ID and password (secret) you obtained by registering with the authentication provider.
Because the tokens are not obtained via the browser, you can avoid being open to token stealing via browser malware. In addition, the application’s ID and secret are never given to the browser, and without those the browser malware can’t obtain tokens from the token endpoint.
The obvious initial downside is that your web application has to make a connection via TLS to the authentication provider.
Hybrid flow is when you make a call which requests a code as well as tokens. It has all the security disadvantages of implicit flow, and I can’t see why you’d want a code if you’re going to be given tokens anyway.
Authorization flow in detail
It’s pretty clear that authentication flow is the one to choose for security reasons. That’s probably why the authentication providers I need to use at work don’t support implicit flow. So, let me go through the authentication flow in detail, from the point of view of a web application. The OpenID Core document has examples of HTTP requests and responses, so I’ll skip those and just try to briefly and clearly summarize the process.
The first step is to bounce the user via HTTP 301 to the authentication endpoint with an OAuth2 request. The request has a scope of
openid and a response type of
code, and a redirection URI pointing back to your web application.
The ID provider then asks the user to log in. This may involve pretty much anything you can imagine. In my case, there’s a company login and password, and then I get asked for a TOTP one-time code from my phone.
Once the user has logged in, the authentication provider bounces them back to the URL that was specified in the request. The
code value is added to the query part of the URL.
Next, your web application takes the code value and uses it to make a call to the token endpoint. The connection is made via HTTPS (TLS), and authenticated using an ID and secret (password) you were given when you registered your application with the authentication provider.
(The ID and secret may be passed as JSON values, or using regular HTTP Basic authentication. The latter is preferred, according to the standards.)
If all goes well and you weren’t given a fake code, you get back a hunk of JSON containing the tokens you requested. The one you’re probably most interested in is the
id_token, and this is where things get a bit tricky.
id_token is encoded and signed. You have to decode it, check the signature using the public key of the ID provider, check the token hasn’t expired, and only then can you rely on the information in it.
The standard to follow for the decoding and signing is JSON Web Tokens (JWT). If you get this far and paste the base64-encoded
id_token string that you got from the token endpoint into the jwt.io web site, you should see the info you want appear as the payload.
So your code needs to load a PEM public key, decode the JSON Web Token, and check its contents using the public key. If the signature is invalid, someone’s trying to impersonate the authentication provider, and you don’t trust the info in the ID token.
If the ID token validation succeeded and you also requested an access token, you might then use the access token to make a call to the profile endpoint, to get additional user information. (Home address, photo, you name it — it’s extensible, like LDAP.)
Libraries you can use
I’d love to say “Go here and use this library”, but at least for Go I haven’t found anything that will do the whole job.
There are many OAuth2 libraries, but none of them seem to support RSA signed JWT tokens.
There’s an openid2go project which looks as if it might work, but it relies on OpenID Connect Discovery being supported. Unfortunately, the authentication providers I need to use don’t have discovery enabled. Furthermore, it doesn’t look to me as if openid2go supports authentication flow.
What about at least handling the JWT part? Well, I took a look at the three Go packages linked from the JWT web site.
jose package describes itself as “A comprehensive set of JWT, JWS and JWE libraries”. Unfortunately the documentation is lacking, and I can’t work out how to perform signature validation against a public key, assuming that’s actually implemented.
Next, jose2go. That one’s nice and simple. Given the
id_token text you got from the token endpoint as a string in the variable
idtoken, the process looks like this:
pubkeydata, err := ioutil.ReadFile("pubkey.crt") // Check error and deal with it here pubkey, err := Rsa.ReadPublic(pubkeydata) // Check error and deal with it here hdr, payload, err := jose.Decode(idtoken, pubkey) // Check error and deal with it here
The ID token info you want is then in the
hdr variable, and
payload can be examined to make sure the expected signature algorithm was used — because you don’t want to accept unsigned tokens. I checked that a tiny change to the payload correctly caused a signature error at the Decode stage.
Finally, there’s jwt-go. This one works too, but the decoding and verifying process is slightly more involved because you need to supply a callback function to look up the appropriate key given the signing method. You get a
Claims map with the values you want, and a
Valid field indicating whether the validation succeeded. Again, a tiny mutation to the data was successfully detected.