Logging in to a web service used to mean sending a username and password and getting a cookie in return. This cookie is then sent with every request so that the server knows who is making the request. However, when it became more common for services to interact with each other on a user's behalf, other forms of authorization systems evolved. One such system is OAuth, which uses token-based authentication instead of cookies.
OAuth is an Authorization framework that allows three different parties to interact with a minimum level of trust. This way only one service needs to know your username and password. The service that doesn’t store this information can ask the service that does to verify who the user is and what they are allowed to do. In general, this provides a better, more secure, user experience. A user can maintain their profile and password information in a single place and have that referenced automatically when they connect to other services. No more typing in email addresses to every site and updating it in multiple places if it changes. This is how Login with Facebook or Login with Google buttons work.
Unfortunately, there are a couple of inherent security risks with this approach. One service is not as trustworthy as another. You might want to allow that budgeting app to read your account balance, but you probably don’t want it to empty your checking account. Additionally, the first time services communicate they must go over a public network where anybody can be listening.
In order to protect the credentials and limit what those credentials can do some initial trust needs to be bootstrapped ahead of time. This is done by combining two different components to ensure that the recipient of a temporary public credential is, in fact, the one it is intended for. If you have heard of two-factor authentication (2FA), it's like that but for web services.
There are a couple of methods that use this combination of factors to provide some extra confidence the service getting access is the right one. We’ll take a look at two of those; Authorization Code Flow and Proof of Key Code Exchange.
Authorization Code Flow
Protection by Proxy
Authorization code flow is the classic way to do this when a client that has a server component. This server component protects an OAuth client secret and proxies the token request from the client to the Authorization Server. In this flow, a user is redirected from the service hosting the protected resources to an Authorization Server. The Authorization Server’s job is to authenticate the user and approve access. After successfully authenticating, usually with a username and password, the user is redirected back to the service with some extra URL parameters including an authorization code.
The authorization code is a temporary code needed to retrieve the access token for permission to interact with the relying service's resources. The relying service then exchanges the authorization code combined with their client id and client secret for an access token and optionally a refresh token. The relying service then returns these to the user's client, usually a browser or mobile application. The client then uses this token to tell the resource server who the request is for and what permissions they have.
Proof of Key Code Exchange (PKCE)
Cryptographic Pixie Dust
Why not exchange the password for a token?
What does this buy us?
In both Authorization Code and PKCE flows, two factors must be exchanged for valid credentials. The authorization code, as presented as part of a redirect URL for consumption, along with some additional information posted in the body of the access token request. These two channels of communication mitigate a variety of attacks and misconfigurations where bearer tokens or authorization can be intercepted in flight.
However, with PKCE there is one more consideration to take into account and that is where the Authorization Server redirects after authentication. The client tells the Authorization Server where it wants to receive the code in the URL. If this is not an exact match, it means there is an opening for a malicious client to trick the Authorization Server to send the code somewhere else. That same client can also generate the temporary secret code. While PKCE moves the goalposts in terms of difficulty, it still relies on this redirect as an anchor of trust.
While having a fully qualified redirect URL is a best practice,
Authorization Code flow mitigates an open redirect misconfiguration due to the
fact that the server still holds a predetermined secret. Because the secret used
in PKCE is generated at runtime, a malicious actor capitalizing on an open
redirect can still follow to the protocol to get valid credentials. On mobile
clients, this is an app-specific URL such as
app-foo://auth_code_handler#code=XXX. For SPA’s this would be something like
While PKCE was originally intended for mobile applications the OAuth Best Practices Working Group has recently started to recommend it for SPA’s as well.
To add a PKCE flow to your application take a look at https://appauth.io/ for supported libraries and implementations.