Product
Platform
Platform
Platform
Developers
Quickstarts
Resources
Explore
Pricing
Download
get a demoLogin

Evolving auth standards exist because attackers have spent more than a decade finding the gaps between what OAuth permits and what developers actually deploy. This webinar explains how OAuth 2.0, OpenID Connect, PKCE, sender-constrained tokens, refresh token rotation, and application architecture fit together. It also shows how those controls prevent stolen credentials, injected authorization codes, replayed tokens, and browsers from becoming token storage facilities with a user interface.

.png)
Hey, folks. Thanks for joining me today to learn about evolving auth security and modern best practices. I want to begin this presentation with just a little bit of prerequisite vocabulary, and that's to cover the difference between authentication and authorization.
Authentication is about proving that you are who you say you are, and authorization refers to whether or not you have permission to access resources or perform actions. Here's an analogy.
Authentication is like a passport. It verifies who you are and provides attributes about your identity, like your citizenship or your date of birth.
Authorization is like a concert ticket. The ticket holder gets access into the venue and permission to sit in a specific seat, but the ticket grants that access to whoever presents it. That could be you, or you could give your ticket to a friend. Your identity doesn't matter at the admission booth, only that you bear authorization for admittance.
Now this concept of authorization brings us to the OAuth specification. Fortunately for us, OAuth standards are very clearly defined. Unfortunately, they're also very dense, and they can be difficult to grok. So let's take a moment to demystify OAuth.
Why was OAuth 2.0 created? Or more specifically, what problem does OAuth solve? Well, in order to answer this question, we kind of have to dig into why standards are so crucial for security and have a little bit of a history lesson. Before OAuth, if an app wanted to use another app's resources, this required sharing user credentials and access.
For example, let's say that you use a ticket sales app and you buy a concert ticket, and then the ticket app wants to connect to your calendar app account to add the concert event to your calendar. Before OAuth, the ticket app would ask you for your calendar app username and password. The ticket app then used your calendar app credentials to access your calendar app account to add the event on your behalf. This meant not only does the ticket app have your calendar app login info, it also had complete access to your calendar app account and had permission to do anything in it that you could do.
I hope you can see how problematic this was. This approach relied on sharing a user's login info from one app with a completely different app, and this is just incredibly insecure. The ticket app also had way too much access to the calendar app's API. It could do everything that you could do, like view private events or access your contacts, modify payment info, and you can imagine how much more.
So we needed a better solution, and this solution was documented as the OAuth standards. OAuth 2.0 is an open standard for performing delegated authorization. It's a way for third-party apps to access APIs without exposing user login info, and it does this using an authorization server. An authorization server is a set of endpoints to issue tokens in order to perform delegated authorization, which allows a party that holds some authorization to delegate a subset of that authorization to another party without requiring either party to disclose user credentials to the other.
So let's come back to our ticket and calendar app example, only now the calendar app has an authorization server. Say you logged in to the ticket app and you bought a concert ticket, and now the ticket app wants to add the event to your calendar app. With OAuth, using the browser, the ticket app sends an authorization request to the calendar app's authorization server. The calendar app's authorization server prompts you to log in. You're going to authenticate with the calendar app, and the authorization server will prompt you for your consent to allow the ticket app to access the calendar app's resource server to add an event on your behalf, but not do more than that.
If you agree, then the calendar app's authorization server sends an authorization code to the ticket app in the browser. The ticket app sends back its app credentials along with the code, and it does this server side. The calendar app's authorization server issues an access token and sends the token to the client server side. The ticket app can now use this access token to authorize access to set calendar events for you with the calendar app's API.
Now we see that issues with sharing credentials and too much access are no longer a problem. The calendar app is asking you to authenticate with the calendar app. The ticket app is not asking you for your calendar app password, and the ticket app holds an access token that authorizes it to only create events, not do things like read your data or change your settings.
Access tokens have no defined format in OAuth 2.0, but many authorization servers use the JSON Web Token because the format enables validation. The API can validate tokens without interacting directly with the authorization server. The access tokens are opaque to the client — they're for resource servers. The content in an access token could change at any time. They grant access to a third party, so they should have a short expiration time. Resource access might also change throughout the user session as well, so the client should never contain code that relies on content in an access token.
At the API, delegated authorization with an access token looks like this. Say the user is logged in to the ticket application, which is the client. The client contacts the calendar app's authorization server to request an access token to authorize the ticket app to interact with the calendar resource server. When the ticket app calls the calendar API, it attaches the access token to the request and sends the request to the API. The resource server validates the token using middleware and then completes the request, adding the calendar event to the user's calendar.
OAuth 2.0 access tokens can be bearer tokens, meaning that access is granted to whomever bears the token — like the analogy to the concert ticket from earlier. Bearer access tokens can be used to access resources from any resource servers that are configured to accept and validate tokens sent by specific apps and issued by a particular authorization server. These could be an app's own local APIs or cross-domain third-party resource servers.
But when we make an authorized request, how does the resource server know what level of access to grant? Tokens communicate this with strings called scopes. Scopes limit what an app can do on the user's behalf. They can't grant access that the user doesn't already have. So if we revisit the ticket and calendar apps again, we only want the ticket app to use the calendar API to create calendar events for the user, but not more. Scopes are permissions in the access token that tell the calendar API what level of access to allow. We delegate access control to the resource server, which is then responsible for confirming the access token scopes fall within first-party user privileges.
Say the ticket app wants to access the third-party calendar app API, and we've already acquired an access token from the authorization server. This access token contains data including a subject, which is the unique user identifier; an audience, saying that the token was issued for use by the ticket app as the client; and a scope for permission to write calendar events. The client sends the request to the API, and this token contains the scope granting write events permission. The resource server, however, can access the calendars of all of its users. So this token shouldn't give the ticket app access to write events for all users. In addition to looking at the token scope, the resource server must use the subject identifier to only grant access to data belonging to the user ID specified as the subject of the token and not allow creation of events on other users' calendars.
In the context of delegated authorization, scopes express what an app can do on a user's behalf. An app can only perform a subset of the user's total capabilities. So remember when we prompted the user for their consent for the calendar app to allow the ticket app to write events? Realistically, this consent dialogue might look something like this. The ticket app could ask for a variety of scopes, but you shouldn't overload scopes with user-specific permissions. Scopes are intended for app-level permissions.
All of this is great. OAuth gave us a way to delegate authorization to call third-party resource servers safely. However, OAuth peaked interest in delegating more than just authorization — we wanted to log users in with third-party apps. For example, maybe the ticket app wants a calendar app user to be logged in to the ticket app if they have a calendar app access token, even if they don't have a ticket app account. The problem here is that OAuth 2.0 is for delegated access. It's not an authentication protocol.
If the ticket app assumes that successfully calling the calendar app's resource server with an access token means the user can be considered authenticated in the ticket app, then we start running into problems. Access tokens are for the resource server. They're opaque to the client. Remember, an access token is like a concert ticket — it grants access and permissions, but the concert venue doesn't care who holds the ticket as long as the ticket itself is valid. Someone could have stolen the token. The token could be for a completely different user, or your token could be injected into someone else's ticket app session. We call this the confused deputy problem. The ticket app client doesn't know where this token came from or who it was issued for because it doesn't read the access token. The token might grant access to data that belongs to the token's subject, but the resource server doesn't know if the user who actually requested that data is the subject of the token. Authentication is about verifying the user is who they say they are, and the ticket app cannot verify the user's identity simply by possessing this opaque access token, which is for authorized access to the calendar app's API.
OAuth specifies how to use an authorization server to grant access without sharing the user's credentials with the client, but it doesn't address authentication or verifying user identities. However, this didn't stop organizations from trying to use OAuth for authentication anyway.
To solve the delegated authentication problem, we needed to formalize authentication on top of OAuth. These days, we log into modern apps with third parties all the time — like Google login — and the problem of delegated authentication was solved by the creation of another spec called OpenID Connect, or OIDC. OAuth 2.0 is a specification for authorization, and OIDC adds an identity layer on top of OAuth 2.0 for authentication.
How does OIDC accomplish that? On both specs, the role of the authorization server is to issue tokens. For authentication, the authorization server issues ID tokens the same way it issues access tokens. ID tokens identify the user, and they can be decoded and used in the client to provide user information. Unlike access tokens, which are commonly JSON Web Tokens but actually have no defined format in the OAuth 2.0 spec, OIDC declares a fixed format for ID tokens. ID tokens are JSON Web Tokens and are intended to be decoded by the client to provide statements about the authentication event and about the user using standardized data attributes called claims.
Standard authentication claims might include things like the issuer — which we saw in the access token earlier — the authorization server that issued the token; the audience, which is the ID of the app the token was issued for; the time the token is going to be invalidated; the time the token was issued; and potentially something called a nonce. A nonce is a string that binds the client to the ID token that it receives. The client generates the nonce and sends it to the authorization server when it makes its authorization request. The authorization server then includes the nonce from that request as a data claim in the token that it issues. When the client decodes the token, it verifies that the nonce inside the token is the same nonce the client sent with the authorization request. This protects against token injection — that is, tokens coming from somewhere other than the authorization server.
Authentication is about confirming the user's identity, so ID tokens also contain info about the user. Some standard user claims are things like subject, which is the unique user identifier — another claim we saw the calendar resource server use from the access token — plus user profile information like name, address, phone number, and more.
With OAuth 2.0 and OIDC, we can delegate both authorization and authentication using access tokens and ID tokens respectively.
Finally, we have a third token type: refresh tokens. Refresh tokens are used to extend the user's session. They're credentials for obtaining fresh access tokens from the authorization server without forcing the user to manually log in again. When a client requests an access token from the authorization server, the authorization server can optionally issue a refresh token too. To reduce attack surface area, access tokens should have a short expiration time. When an access token expires — or right before it expires — the client can send an authorization request with the refresh token to the authorization server. The authorization server authenticates the client and validates the refresh token, then issues new tokens and sends them to the client, where the user's authentication and authorization state is maintained or restored.
Let's pause for a second and review what we've covered so far. We began by talking about the dangerous third-party access situation in the days before OAuth. This led to OAuth and how authorization servers enabled safer delegated authorization. We covered how access is delegated with access tokens and scopes, and then we addressed problems with using OAuth for login and the genesis of OpenID Connect and ID tokens for delegated authentication. Finally, we talked about using refresh tokens for maintaining or restoring access.
The OAuth 2.0 specification was published in October 2012, and we still use it today. That makes OAuth pretty old in Internet timeline terms. But work on OAuth hasn't stopped. Working groups like the Internet Engineering Task Force, or IETF, and the OpenID Foundation continually strive to make the Internet a better, safer place. For OAuth, this includes Internet drafts and RFCs building on the OAuth 2.0 authorization framework, expanding and amending the spec to deprecate outdated practices, close security gaps, and add new guidance. Many of these updates will be consolidated in OAuth 2.1.
With that said, let's take a look at some of the newer guidance in RFC 9700, which is best current practice for OAuth 2.0 security. What's changed since OAuth 2.0 was published? A decade is more than enough time for attackers to study and discover how to exploit OAuth implementation weaknesses and anti-patterns. OAuth is also now used in environments with high security requirements like banking, healthcare, and government. Architecture is more complex and dynamic now — we frequently see multitenancy, and many apps need access to multiple service providers. Finally, browser technology has changed as well, and the best current practice addresses these evolutions in several ways.
To start, it provides guidance on protecting redirect flows in OAuth. In the authorization code flow we saw earlier, when the client sends an authorization request to the authorization server, it includes a redirect URI. This is the app URL that the authorization server is going to redirect back to when it's delivering the authorization code in a query parameter. If an attacker manipulates the redirect URI, they could redirect the browser somewhere they can tamper with the authorization code before redirecting back to your app. To protect redirect flows, you must use exact string matching when configuring allowed redirect URIs at your authorization server. This mitigates the risk of using unsafe matching patterns.
If the authorization response is redirected to a malicious site, an attacker can steal the authorization code and exchange it for the victim's tokens, gaining access to their account, or they could inject a stolen authorization code into their own OAuth flow to impersonate a victim. In either case, the compromised authorization response is redirected to your app, and the redirect came from the attacker, not from the authorization server. When an attacker tricks a user's browser into making illegitimate requests, this is called cross-site request forgery, or CSRF. CSRF and authorization code injection need to be mitigated.
Code injection attacks can be prevented by an extension to the authorization code flow called Proof Key for Code Exchange, or PKCE. Before making the authorization request, the client generates a random string called the code verifier. It then uses a hashing function called the code challenge method to hash the code verifier. This resulting hash is called the code challenge. To initiate the authorization grant, the client redirects to the authorization server's authorization endpoint with the code challenge and the code challenge method included as query parameters. The user will log in with the authorization server. The authorization server generates a code, which it sends to the client. The client then makes a POST request to the authorization server's token endpoint and includes the code verifier and the code in the request payload. The authorization server verifies that it did indeed issue this code and that the code is valid. Then it hashes the code verifier to compute the code challenge. It compares this new code challenge with the challenge it received from the client with the authorization request earlier to make sure that they match. Once this challenge is successful, the authorization server issues tokens and sends them to the client.
If an attacker injects a stolen code into a token request — either into their own flow or a victim's flow — when the authorization server hashes the code verifier, the result isn't going to match the code challenge from the initial authorization request, and the attack will fail because the challenge mismatch reveals that the authorization request and the token request didn't originate from the same place.
Public clients are downloaded to and run on a user's browser or device. This includes single-page or statically generated applications, native apps, and mobile apps. In best current practice, all public clients must implement PKCE with the authorization code grant. Confidential clients like traditional server-side web apps should use PKCE for added protection. In OAuth 2.1, all clients using the authorization code grant will be required to use PKCE.
Once tokens have been issued, the best current practice recommends ways to prevent what we call token replay. Token replay is when an attacker attempts to reuse another user's token. They might steal and try to reuse an access token to gain access to protected resources, or a refresh token to request and abuse new tokens. Access tokens can be bearer tokens, meaning they behave like a concert ticket — the bearer token might be intercepted or stolen, but it still grants access to anyone who possesses it.
There are a few ways to reduce the risk of stolen token usage. One method is to use sender constraint, which can be implemented at different network protocol layers. Demonstrating Proof of Possession, or DPoP, describes how to use sender constraint at the application layer. DPoP binds a token to the client and to the specific client request. Before making a request, the client generates an ephemeral public and private key pair. It then creates a DPoP JSON Web Token, which contains information about the token request like the HTTP method and the endpoint URL, an ID, a timestamp, and the public key. It then signs the DPoP JSON Web Token with the private key. This signed token is the DPoP proof. The client includes this proof in the header of the token request, which it sends to the authorization server. The authorization server validates the DPoP proof token and issues an access token, which it binds to a hash of the public key from the DPoP proof, and then it sends the access token to the client.
To call a resource server, the client creates another DPoP proof for the API request, this time also including a hash of the access token. It includes the DPoP proof in the API request header along with an authorization header with the access token — and now we have a token type of DPoP instead of bearer. The resource server uses the public key to verify the DPoP proof signature. It validates that the access token public key binding matches the public key in the DPoP proof and confirms that the access token hash in the DPoP proof matches the access token. It does all of this before granting access to any resources. If the access token is stolen, it's going to fail verification because the token is scoped to the client that holds the private key and the specific request made by that client.
Another method of sender constraint is mutual transport layer security, or mTLS. In standard TLS, the client connects to the server, the server sends its certificate to the client, the client validates the certificate, and if successful, a secure connection is established. In mutual TLS, both the server and the client present and then validate each other's certificates in order to establish a secure connection. For OAuth, the authorization server and the client establish an mTLS connection for the authorization server's token endpoint. The authorization server issues the access token and binds the token to the client certificate — it might do this by embedding the certificate hash in the token. The authorization server then sends the access token to the client. To fetch API resources, the client and the resource server exchange and validate each other's certificates and then establish a secure connection. When the client sends the access token to the API, the resource server validates the token and verifies that the token certificate binding matches the client certificate from the mTLS connection before granting any access to resources. mTLS provides sender constraint because the client certificate securing the connection between the client and the resource server is the same certificate securing the connection between the client and the authorization server. The access token is bound to the certificate, proving that it was issued for and sent from the same client using the same connection security.
Refresh token rotation is another mechanism to reduce the risk of token replay attacks. Public clients must either use sender constraint, or refresh tokens must be rotated by the authorization server. Recall that refresh tokens are exchanged for fresh access tokens, extending sessions and making them very powerful, long-lived credentials that mandate safeguarding. Without rotation, the same refresh token can be used to get new access tokens for days or even weeks, with a long window for potential theft. With rotation, a new refresh token is sent every time the access token is issued or refreshed, and the previous refresh token is then revoked. Since access tokens are short-lived, this shrinks the opportunity for refresh token replay attacks. Rotated refresh tokens keep the same expiration timestamp as the initial refresh token issued at login, meaning the refresh token's time to live is not extended each time a new token is issued. If a previous invalid refresh token is used, then the authorization server revokes the currently active refresh token, and the user will need to log in again.
Access token privileges should be restricted to the minimum required for the app or the use case — we actually covered this earlier while learning about authorization scopes. To recap, access tokens issued to a third-party client can't elevate a user's first-party permissions. A resource server should validate that it is indeed the intended audience for the access token and that requests to perform actions are allowed by permissions in that access token.
The best current practice also deprecates two authorization grant types from OAuth 2.0: implicit grant and resource owner password credentials grant. Since these grant types should not be used, I'm not going to get too deep into how they work, but I'll share why they're unsafe. Implicit grant exposes access tokens in the URL redirect from the authorization endpoint instead of using the back-channel token endpoint. Resource owner password credentials grant exposes usernames and passwords to the client, increasing the risk of credential leaks. This is kind of the step above the situation before OAuth where third parties ask for first-party passwords — only instead of storing those credentials in the client, an access token is issued. And as we know, sharing credentials is bad.
Last but not least, let's talk about architecture. App architecture has become much more complex and dynamic than it was in the early days of OAuth 2.0. This is especially true of browser-based public clients with the proliferation of JavaScript applications. App architecture has a significant impact on auth security. I want to talk about three architecture patterns in order of most secure to least secure, starting with the most secure architecture: back end for front end.
In back end for front end auth, the back end proxies auth functionality and the front end never actually interacts directly with the authorization server. When a user initiates login, the front-end client calls a login endpoint provided by the back-end auth proxy API, and then the back end makes the request to the authorization server. It handles the code grant and stores the tokens it receives in a user session. The back end also handles session management and token refresh. The back end sets a secure HTTP-only cookie in the browser containing the user's session ID. This cookie can't be read by JavaScript in the front end. To access API resources, the front end attaches the session ID cookie to the request and sends it to a proxy API in the back end. The back end reads the session ID in the cookie and looks up the user's session. If the API is a same-site component of your back end, the validated user session can authorize access to the API. If the API lives outside your domain or if it expects an authorization request header, the back end retrieves the user's access token and uses that to call the API. In both cases, successfully returned API data is sent to the back end and then forwarded to the front end. In a back end for front end auth architecture, no tokens touch the browser, and auth flows and API calls are all proxied through the back end. FusionAuth provides a back end for front end called the hosted back end, and this is possible because FusionAuth is downloadable and can be deployed on your infrastructure as an integrated part of your same-domain architecture.
The next architecture pattern is called token mediating back end. This architecture works similarly to back end for front end in some respects. Auth is managed in the back end with an auth proxy API, and calls to the auth API are still secured with a session ID cookie. If the front end needs to interact with the authorization server — for example, to get updated user info or to log out — that's handled through the back end. Where token mediating back end differs from back end for front end is that a token mediating back end also sends the access token directly to the client. The front end can then make API requests directly without using a proxy API via the back end. Since access tokens are delivered to the browser, token mediating back end is not as secure as back end for front end, but auth processes and sessions are still managed in the back end. A token mediating back end is often implemented by SaaS authorization providers because they can host a back-end auth proxy, but they live outside your architecture and don't know anything about your resource APIs, so they can't provide API proxies for you. Unless you have a custom domain configured, their session cookies are not going to work with your API anyway, because cookies only work on the same domain, and SaaS cloud providers exist outside your core infrastructure.
The final and least secure architecture is browser-based OAuth 2.0 client. This is a front end, maybe an API, and an authorization server, but no code-executing back end. All authorization flows and API calls happen in the browser. That means access tokens and refresh tokens are both stored in the front-end client, and the front end authorizes API requests with the access token directly. The best practices we covered earlier are critical if you implement browser-based OAuth in the client: you must protect redirects, you must use PKCE, access tokens should be sender constrained, and refresh tokens must either be sender constrained or rotated. Browser-based OAuth client should really only be used when auth is used for convenience but isn't essential to security. Examples of this type of thing might be a game score leaderboard or a URL shortener. If you're interested in seeing a complete working demo of all of these architectures in code, plus a demo of FusionAuth's hosted back end, you can check out this GitHub repo.
So what have we covered since the last summary? We talked about how OAuth is over a decade old, but standards continue to evolve with changing technology. We covered protecting redirect flows with exact string matching, PKCE, and preventing injection. Next, we talked about how to mitigate the risk of token replay with sender constraint and token rotation. We touched on why implicit grant and resource owner password credentials grant are not secure. And finally, we ended with a security comparison of three architecture patterns.
We covered a lot of information in this session, and I hope our time together has equipped you with the knowledge you need to be more comfortable with OAuth, with modern best practices, and with the impact of architecture on auth security. My name is Kim Maida. I lead developer relations at FusionAuth. You can connect with me on the platform of your choice at maida.kim/connect, and I'm honored that you chose to spend this time with me today. Thank you very much.
I'm going to take a look and see if anybody has questions. Do I have a YouTube short for the client credentials flow? I do not yet. Since that was mentioned, I do like to create shorts on YouTube explaining concepts from OAuth 2.0 and OIDC. The goal for these Shorts is to be under ninety seconds if possible, which goes to show that while auth is complex and deep, it doesn't actually take hours, days, weeks, months, or years to understand some of the core essential concepts. I do plan on making more YouTube Shorts, so I'm hoping to get to the client credentials flow at some point. If there's anything anybody would like to see explained in under ninety seconds in those Shorts, please let me know.
A YouTube Short for the back end for front end — since that's something we can share with our own customers. Yes, I am hoping to do YouTube Shorts on all three of the architecture patterns. I also have a series of blog posts coming out soon — hopefully within the next couple of months — that go into much more depth on exactly how auth is managed in back end for front end, in token mediating back end, and in browser-based OAuth for the client. These blog posts will be accompanied, hopefully, by a talk recording, because I'm going to be giving a flagship talk this fall starting in October on those three architectures: exactly how they work, all of the calls they make, what the responses look like, where security is a risk, and that type of thing. Those go as a companion with the GitHub repo I had up on the screen earlier, which is a full working code demo of all three architectures along with a working demo of FusionAuth's hosted back end for front end.
If there aren't any more questions, thank you so much for spending the better part of this past hour with me.