OAuth and PKCE flow - Part 1
- HariharanOAuth 2.0 (referred to as OAuth from now on) is an open standard for secure access delegation and it provides mechanisms for clients to obtain access to resources on a server on behalf of the user. To achieve this OAuth uses access tokens. An access token, simply put, is a string representing the granted permissions. The permissions represented in the access token are called scopes in OAuth speak. OAuth defines different flows to obtain an access token. PKCE is one such flow. To understand PKCE, one has to have a basic understanding of the implicit flow and the code grant flow to see why PKCE exists and when to use it.
Implicit Flow
Implicit flow is the oldest of the bunch, created around 10 years ago. The web landscape was significantly different back then. Browsers were not allowed to make cross-origin requests. So JavaScript applications could not make requests to a different domain. To overcome these limitations implicit flow was proposed which eliminated the need to make requests to external domains by returning the token in the redirect url as a query param.
Steps to obtain an access token using implicit flow
- The application opens a browser to redirect the user to the OAuth server.
- The user enters the credentials and approves the application’s request
- The user is redirected back to the application with an access token in the URL
As you would have noticed by now, this isn’t very secure. OAuth 2.0 spec does not provide any mechanism for clients to verify that the token was issued to them. This exposes a risk of impersonation attacks. There are many ways the redirect could fail or be intercepted thus exposing the access token to malicious agents.
It is also difficult to implement this flow in native applications.
Authorization Code Grant Flow
Authorization code grant flow tried to solve some problems with the implicit flow.
Steps to obtain an access token in the code grant flow
- The application opens a browser and redirects the user to the OAuth server.
- The user sees the authorization prompt and approves the application, giving it the necessary permissions.
- The user is redirected back to the application with an authorization code in the query string.
- The application exchanges the authorization code for an access token.
The application makes a POST request to exchange the code for an access token. By the time this flow was proposed, browsers allowed cross-origin requests. The code can only be used once to obtain the access token. It is worth noting that a refresh token is also returned with the access token in this flow. The refresh token can be used to obtain a new access token. Refresh token allows a user session to continue without the need to sign-in every so often.
It is important to note that in order to obtain the authorization code, the client has to pass a client ID and client secret to the OAuth server. These credentials can be obtained when setting up the application on the OAuth server. This proposes a new challenge when building web applications such SPA, storing client secrets in the frontend. Doing so would expose it and cause a major security flaw.
PKCE Flow
PKCE (Proof Key for Code Exchange) builds on top of the code grant flow and addresses issues such as authorization code interception and eliminates the need for client secrets. That being said, you can still use client secrets with PKCE flow to prevent just any client from potentially sending requests to your OAuth server.
When an application needs to perform an authentication against the OAuth server, instead of directly opening the browser and redirecting the user to the OAuth server, the client first creates a code verifier. This is a cryptographically random string that can be made of characters A-Z, a-z, 0-9 and punctuation characters - hyphen, period, underscore, and tilde. If the device is capable of performing a SHA256 hash, the code verifier is hashed. The code verifier could also be used as plain text if the device can’t perform a SHA256 hash.
The client then makes an authorization request with the following parameters -
- response_type=code – Indicates that the application expects an authorization code
- client_id – Client ID for the application
- redirect_uri – Indicates the URL to return the user to after authentication is complete
- code_challenge – The code challenge generated as previously described
- code_challenge_method=S256 – either plain or S256, depending on whether the challenge is the plain verifier string or the SHA256 hash of the string. Default is plain.
It is worth noting that the only additional parameters here as compared to the Authorization Code Flow are code_challenge and code_challenge_method. The authorization server recognizes the code_challenge parameter in the request and associates it with the authorization code and stores it. It then returns an authorization code as normal.
The client then makes a code exchange request with the following parameters -
- grant_type=authorization_code – Grant type of this token request
- code – Authorization code that the application received in the revious step
- redirect_uri – The redirect URL that was used in the initial authorization request
- client_id – The application’s registered client ID
- code_verifier – The code verifier that the application generated before making the autho-rization request
The authorization server first checks the code_challenge_method that it received in the previous request. If the method is plain, the server can just check the code_verifier against the code_challenge it received earlier. Else if the method is S256, the server must take the code_verifier and transform it into a SHA256 hash and base64 encode it before a code_challenge match is performed. The server only returns an access token if the code_verifier is correctly matched.
Why PKCE?
PKCE was initially designed for native apps (Android, IOS, electron etc). To utilize OAuth in these cases a browser based login is required and you would be leaving the security context of your application. You might not have a secure redirect to your application from the browser and the authorization code could be intercepted. PKCE solves this by establishing a shared secret between the application and the OAuth server. Even if the authorization code is intercepted, without the code verifier the requests to the OAuth server will be invalid.
PKCE flow is great for public clients (clients that can’t safely store client secrets). PKCE flow’s ultimate aim is to avoid leaked authorization codes from being useful.
In part 2 of this post we will look at an example client in Golang that uses the PKCE flow to obtain an access token. So stay tuned for that!