UKRDC OIDC

An (extremely) brief summary of auth protocols

Authentication vs Authorization

We’ll use these terms a lot, so it’s worth mentioning the distinction between the two.

Authentication asserts that a user is who they claim to be. 

Authorization asserts that a user is allowed to access a particular resource.

In an ideally separated environment, authentication doesn’t care about access restrictions, and authorisation doesn’t care about who the user is, only what they’re allowed to access

OAuth

OAuth (now on OAuth2) is a widely used system for delegated resource access, that is, it allows a website/app to securely access a users information from another service without ever having a password stored.

For example, you might be writing a Twitter client. You need to have access to read/write a users tweets, but don’t want to store their password. To do this, we add an OAuth2-powered “Sign in with Twitter” button.

When you sign in through that button, it triggers a sign-in flow which gives the website restricted access to your Twitter account without you entering your password on the website itself. The website is given an access token from Twitter, with which it can interact with the Twitter API.

Note, OAuth2 is really just an authorization protocol: By default, the website does not know who the user is. Rather, upon signing in, the website is given a token providing access to e.g. Twitter, with which it can request user information. Twitter, in this case, is providing authentication. Their login page asserts you are who you say you are.

Importantly, the OAuth2 flow allows the website to request access to specific resources (scopes), for example email address, name, read/write tweets etc. The user must explicitly grant the website access to these scopes.

OpenID Connect (OIDC)

As far as I can tell, OIDC is an extension of OAuth2 with a greater focus on 1. Single-sign-on, and 2. Authentication by default.

The example I gave above for OAuth2 is likely actually using OIDC, since in that particular example the website likely wants user profile information immediately. OIDC includes basic profile scopes by default, and returns not only an access token, but also an ID token.

For example, whenever you use a “Sign in with Google” (or similar) button, it’s usually powered by OIDC. The website wants to populate your account information without needing to worry about storing credentials. When you sign in through that button, it triggers a sign-in flow which gives the website restricted access to your Google account information, and returns an ID token. This is similar to an access token, however it provides information about who the user is (name, email, basic profile info etc) rather than granting access to any API.

An access token provides the website with secure access to the authentication server’s resources. The ID token on the other hand should not be used for authorization, but rather includes information about the user’s identity in the token itself, and is typically only consumed by the client application itself, not passed around to any API.

Both tokens are useful for different things, however for the following documentation we will focus heavily on access tokens, as we can use these to also secure our internal API.

Using access tokens to secure an “internal” API

As mentioned, an OAuth2 access token can be used to grant access to the authentication-server’s API, but we can also use the access token ourselves internally.

At this point we should note, we are currently using Okta to provide our authentication server, so I’ll be referring to them specifically a bit.

We need to make sure our backend API is secure, only allowing signed-in users to access any resources, and providing a permissions system to manage access to specific resources. This is a super difficult problem to solve well, and so like all good development teams, we generously let someone else do the hard work for us (hello, Okta).

We don’t want to manage a database of users. We also, if at all possible, don’t want to deal with managing user sessions. These are both huge attack vectors and are difficult to do right. So, we’ll piggyback Okta’s access tokens to do all of this for us.

When a user logs in from our client web app, they are sent to Okta to log in, and after some extra steps discussed later, are returned to the web app with an access token from Okta. The web app then keeps this access token in memory, and includes it in the headers of any requests to our API server.

Our API then validates this token, checks user info, permissions etc, and uses this to determine if access to the resource should be granted.

One key thing to mention here: Since each request involves an independent check of the access token, we don’t need to store any sessions. Each API request is totally independent. This means we don’t need to worry about implementing sessions, keeping sessions between server reboots, and allows efficient load-balancing since multiple API instances can be spun up without having to synchronise sessions between each. Good stuff.

Useful links:

What does an access token look like?

I’m going to include a real, but expired, access token here (with some secure values nevertheless redacted), and go through each section and how we use it.

Note: The token itself is a JSON-web-token which is shown below decoded, however in real applications it is base64 encoded and typically included as a header in HTTP requests.

{   "header": {     "kid": "4FVzGd1uTGb-6OxSuf0-pyBwYm1tYCRvl4bStl60ynM",     "alg": "RS256"   },   "payload": {     "ver": 1,     "jti": "AT.hB01pndM9U0pRPYZD5iQSxpDpVOY.............",     "iss": "https://dev-58161221.okta.com/oauth2/ausn7fa9zfh1DC2La5d6",     "aud": "api://ukrdc",     "iat": 1620121132,     "exp": 1620124732,     "cid": "0oan75eooLX2DcdQK5d6",     "uid": "00un7817wk71QU2NE5d6",     "scp": [       "openid",       "email",       "profile"     ],     "sub": "joel.collins@renalregistry.nhs.uk",     "org.ukrdc.permissions": [       "ukrdc:records:read",       "ukrdc:workitems:write",       "ukrdc:mirth:write",       "ukrdc:mirth:read",       "ukrdc:records:write",       "ukrdc:empi:write",       "ukrdc:empi:read",       "ukrdc:workitems:read",       "ukrdc:superusers"     ]   },   "signature": "YDTNTyp8z0YgaryCY3xxEtsCFLeG7JYXg3wxzha_................." }

The JWT is split into 3 sections, a header, payload, and signature. While the specifics of each section differ from service to service, we’ll focus on what our Okta tokens for the UKRDC look like. Anything non-standard will be highlighted.

The header generally contains information describing how the signature was generated, and how to validate it. In our case, “kid” tells us/Okta which signing key was used to sign the JWT, and “alg” gives the signing algorithm used.

Signature

I’m skipping payload for now because the signature is so important to this working for us at all. We are going to be using these access tokens to manage access to secure resources, and so we need to be 100% confident the token has not been forged or tampered with. The token issuer (Okta) signs the token using the parameters given in the header, and the payload content. Any changes to the payload will invalidate this signature, and likewise valid tokens cannot be “minted” by any other authentication server.

Any time a user accesses a secure API resource, they must provide an access token which we then verify before allowing access. The signature is the key to this verification process.

Payload

The “meat” of the token. We’ll actually go through this attribute by attribute.

JTI: JWT ID. A unique ID for this token in particular

ISS: Issuer - The auth server that minted the token

AUD: API audience. This basically states which services/resources the token relates to

IAT: Issues-at time

EXP: Token expiration time (more on this later)

CID: Client ID. This is the ID of the application that will be requesting resources. For example, if we signed in to the UKRDC web app, we’ll get a different CID than if we signed in to the interactive API documentation web app.

UID: User ID. 

SCP: OAuth scopes granted by the token.

SUB: Token subject. We use this to identify the subject user in a more useful format (their email address)

ORG.UKRDC.PERMISSIONS

This is a custom private claim we add to the token to provide information about the granular permissions the user holds. We draw a clear distinction between a claim and a permission here: A claim gives information about which resources we can use from the auth server. A permission gives us custom information about what internal resources the user can access. Critically, this is provided by the auth server (Okta), managed by admins, and cannot be modified without invalidating the token signature. This custom claim is critical to our APIs secure functionality.

Note: We namespace this claim to provide collision-resistance

Token expiry and login persistence 

While it’s great that we don’t need to be passing around and storing user credentials, this does pose a new security issue. If an attacker somehow gets hold of a valid access token, perhaps through XSS or similar, then they in principal have eternal access to our API resources. Our API server has no session state, and so if a token is valid, it will grant API access.

We have two systems in place to mitigate this.

Firstly, we are able to check token validity online via Okta. Rather than checking the signature locally, we have the option to pass the token to Okta, who will do this check while also checking if the session that created the token has ended. E.g. if the user logs out, or if an attack is known and the admins invalidate all active tokens.

Secondly, access tokens are short-lived. Each token has a lifetime of an hour or less, meaning that even in a worst-case scenario, the token cannot be used in perpetuity by a bad actor.

Token expiry is great for security, but can potentially destroy end-user experience, requiring login once per hour. Luckily, OAuth2 has a system in place to manage this. Unfortunately, we can’t use it securely.

Refresh tokens

To mitigate the issue of token expiry, one common flow is for an auth server to return both a short-lived access token, as well as a long-lived refresh token. Once an access token has expired, the refresh token can be exchanged for a new access token. Refresh tokens cannot themselves be used to grant API access. Additionally, refresh tokens require a “client secret” in order to be generated. This client secret must not be publicly available in any capacity. 

While this works great for backend services able to keep refresh tokens and client secrets secure and private, we are using a web app where the source code is intrinsically publicly available. This would include any client secrets. Additionally, a malicious XSS attack could just grab the refresh token instead of the access token, and use that to keep obtaining new access tokens. 

In summary, refresh tokens are wonderful if used properly, but using them properly is extremely difficult in public web apps. 

Rotating refresh tokens

To mitigate the issues surrounding stolen refresh tokens, we make use of single-use "rotating" refresh tokens. An excellent overview of these tokens can be found at https://developer.okta.com/docs/guides/refresh-tokens/refresh-token-rotation/

Essentially, any refresh token is single-use. The refresh token can be exchanged for a 15-minute access token, and another new refresh token. As soon as a refresh token has been used once, it is invalidated and cannot be used again. Likewise, once a refresh token has been unused for some time (1 hour for us), it is invalidated and the application requires re-authentication.

Importantly (from the link above, verbatim) "If a previously used refresh token is used again with the token request, the Authorization Server automatically detects the attempted reuse of the refresh token. As a result, Okta immediately invalidates the most recently issued refresh token and all access tokens issued since the user authenticated. This protects your application from token compromise and replay attacks."

Using Okta to manage sessions

When a user is required to re-authenticate, their Okta session may be used to speed up the process. If, when signing in, the user checks the "Remember me" option, their session will be stored and managed by Okta. This way, even if their refresh token expires, they can be logged in without direct user-interaction. We can then configure Okta to log out inactive users after a timeout, requiring re-entering a password.