Authorization Code Flow with PKCE in Spring Security OAuth
RFC 7636: Proof Key for Code Exchange (PKCE, pronounced “pixy”) describes an extension to the Authorization Code flow to protect public clients from authorization code interception attack.
In this tutorial, we are going to look at how to implement this extension in an OAuth 2.0 authorization server built using Spring Security OAuth, which does not support it out of the box.
Spring Security OAuth is deprecated and is not recommended for use in new projects.
1. Authorization Server
Let’s start by creating an authorization server.
We create a configuration class for the authorization server and configure an in-memory client store with two initial clients, public and private:
Then we create a configuration class in which we configure http security and an in-memory authentication manager with one user:
To test the authorization server, we can enter the address in a browser:
We will be redirected to the login page and after entering credentials (john/pass) we will be redirected
to the client redirect uri with the authorization code in the request parameter, e.g.:
We can exchange this code for an access token, for example, using curl:
Note that in the case of a private client, you will also need to add its secret to the request: -d client_secret=secret.
2. Code Challenge
Now we can move on to implementing the PKCE extension and we will start from the Authorization Request.
By the standard a client needs to create and record a secret named the code_verifier from which it derives a transformed version
called code_challenge, which is sent in the Authorization Request along with the transformation method called code_challenge_method.
The standard provides two transformation methods, plain and S256, but if the client is capable of using S256, it must use S256.
In case of code_challenge_method is not present in the request transformation method defaults to plain.
In order to make our server handle these two parameters, we need to implement custom AuthorizationCodeServices.
For simplicity, we will use an in-memory storage implemented using Map.
The difference from the standard InMemoryAuthorizationCodeServices provided by Spring is that in addition to saving the code and authentication,
we also need to save the code challenge and transformation method.
First, we create enumeration for our transformation methods, having also provided value for private clients who do not need it:
Next, create a container class to hold authentication and its associated code_challenge and code_challenge_method.
Here we also provide a constructor for clients who do not require these parameters:
Finally, we implement our AuthorizationCodeServices, or rather the createAuthorizationCode method,
and we will leave consumeAuthorizationCode for later.
We will require the code_challenge parameter only from public clients.
We could add a parameter to client details about this requirement (for example, if for some reason we do not want to force
all public clients to use PKCE or even use it for some private clients), but in this example we will consider all clients
with a missing secret as requiring it, and in accordance with the standard, we will return an “invalid_request” error if code_challenge is not sent.
Also, according to the standard, we will use the “plain” transformation method if code_challenge_method is not in the request,
and return an error if the unsupported transformation is requested.
It remains only to configure the server to use our implementation of AuthorizationCodeServices:
3. Code Verifier
Having received the authorization code, the client must exchange it for a token by sending a request with
the code_verifier parameter to the token endpoint.
After receiving an access token request, the server needs to calculate the code challenge from the received code_verifier
according to transformation method and compare it with the previously associated code_challenge.
First, we will teach each of the transformation methods to calculate code_challenge from code_verifier,
for this we will add an abstract method to enum and implement it in each method according to the standard:
Then we add a method to PkceProtectedAuthentication class for getting authentication, which compares the result of the code_verifier transformation with code_challenge
and returns authentication if they match, otherwise it returns an error response indicating invalid_grant by throwing an exception:
We cannot override and use AuthorizationCodeServices#consumeAuthorizationCode(String), since we also need to pass a code verifier.
Therefore, we create another method that gets an instance of PkceProtectedAuthentication from the store using an authorization code
and tries to extract OAuth2Authentication from it by providing a code verifier:
Finally, we need to implement a token granter for the authorization code grant type that will support the additional code_verifier parameter.
We do this by extending AuthorizationCodeTokenGranter with the only difference that it reads code_verifier from the request parameters
and uses it to obtain the stored authentication:
To test our flow, we can first try to authorize using a public client without the code_challenge: