RFC7636: Proof Key for Code Exchange by OAuth Public Clients

This RFC 7636 is used to improve the security of Authorization Code flow for public clients by sending extra “code_challenge” and “code_verifier” to the authorization server.

The code_challenge goes to the authorization endpoint, through the browser. The code_verifier goes to the token endpoint, from server to server. Someone who steals the code cannot use it without the code_verifier:

        sequenceDiagram
    autonumber
    participant User as User's browser<br/>(User Agent)
    participant Client
    box rgba(80, 120, 220, 0.12) CodeChallenge
        participant AS as Authorization Server
    end
    participant RS as Resource Server

    Note over Client: Generates a code_verifier<br/>and its code_challenge
    rect rgba(120, 120, 120, 0.08)
        Note over User,AS: Front channel (browser)
        Client-->>User: 302 Found → /authorize
        User->>AS: GET /authorize (code_challenge)
        Note over User,AS: The user logs in and approves
        AS-->>User: 302 Found → redirect_uri?code=…
        User->>Client: GET redirect_uri?code=…
    end
    rect rgba(120, 120, 120, 0.08)
        Note over Client,RS: Back channel (server to server)
        Client->>AS: POST /token (code, code_verifier)
        AS-->>Client: 200 OK + access token
        Client->>RS: GET /api (bearer token)
        RS-->>Client: 200 OK + resource
    end
    
  1. The client redirects the browser to the authorization endpoint.

  2. The request contains code_challenge and code_challenge_method. With S256, code_challenge is the SHA-256 hash of code_verifier, in base64url. CodeChallenge checks the format of code_challenge and the support of code_challenge_method. The save_authorization_code method of the grant stores them with the code.

  3. The authorization server redirects the browser to the client with a code.

  4. The client receives the code.

  5. The client sends the code and the code_verifier. CodeChallenge computes the challenge from code_verifier and compares it with the stored code_challenge. It returns invalid_grant if they differ.

  6. The authorization server returns an access token.

  7. The client calls the API of the Resource Server with the access token, in the Authorization: Bearer header.

  8. The Resource Server returns the protected resource.

The /authorize, /token and /api paths are examples.

Using RFC7636 in Authorization Code Grant

In order to apply proof key for code exchange, you need to register the CodeChallenge extension to AuthorizationCodeGrant. But before that, we need to re-design our AuthorizationCode database.

The new database SHOULD contain two more columns:

  1. code_challenge: A VARCHAR

  2. code_challenge_method: A VARCHAR

And the AuthorizationCodeGrant should record the code_challenge and code_challenge_method into database in save_authorization_code method:

class MyAuthorizationCodeGrant(AuthorizationCodeGrant):
    # YOU MAY NEED TO ADD "none" METHOD FOR AUTHORIZATION WITHOUT CLIENT SECRET
    TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none']

    def save_authorization_code(self, code, request):
        # NOTICE BELOW
        code_challenge = request.payload.data.get('code_challenge')
        code_challenge_method = request.payload.data.get('code_challenge_method')
        auth_code = AuthorizationCode(
            code=code,
            client_id=request.client.client_id,
            redirect_uri=request.redirect_uri,
            scope=request.scope,
            user_id=request.user.id,
            code_challenge=code_challenge,
            code_challenge_method=code_challenge_method,
        )
        auth_code.save()
        return auth_code

Now you can register your AuthorizationCodeGrant with the extension:

from authlib.oauth2.rfc7636 import CodeChallenge
server.register_grant(MyAuthorizationCodeGrant, [CodeChallenge(required=True)])

With required=True, public clients must use PKCE. The check happens at the token endpoint: a public client that sends no code_verifier gets an invalid_request error. The authorization endpoint does not reject a request without code_challenge, but the code it returns cannot be exchanged.

With required=False, PKCE is optional for all clients.

In both modes:

  • If the authorization request contained a code_challenge, the token request must contain a matching code_verifier.

  • If the authorization request contained no code_challenge, a token request with a code_verifier is rejected.

  • Confidential clients may use PKCE, but are not required to.

Using code_challenge in Client

Read the Code Challenge section in the Web Clients.

It is also possible to add code_challenge in OAuth2Session, consider that we already have a session:

>>> from authlib.oauth2.rfc7636 import create_s256_code_challenge
>>> code_verifier = generate_token(48)
>>> code_challenge = create_s256_code_challenge(code_verifier)
>>> uri, state = session.create_authorization_url(authorize_url, code_challenge=code_challenge, code_challenge_method='S256')
>>> # visit uri, get the response
>>> authorization_response = 'https://example.com/auth?code=42..e9&state=d..t'
>>> token = session.fetch_token(token_endpoint, authorization_response=authorization_response, code_verifier=code_verifier)

API Reference

class authlib.oauth2.rfc7636.CodeChallenge(required=True)

CodeChallenge extension to Authorization Code Grant. It is used to improve the security of Authorization Code flow for public clients by sending extra “code_challenge” and “code_verifier” to the authorization server.

The AuthorizationCodeGrant SHOULD save the code_challenge and code_challenge_method into database when save_authorization_code. Then register this extension via:

server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)])
DEFAULT_CODE_CHALLENGE_METHOD = 'plain'

defaults to “plain” if not present in the request

SUPPORTED_CODE_CHALLENGE_METHOD = ['plain', 'S256']

supported code_challenge_method

get_authorization_code_challenge(authorization_code)

Get “code_challenge” associated with this authorization code. Developers MAY re-implement it in subclass, the default logic:

def get_authorization_code_challenge(self, authorization_code):
    return authorization_code.code_challenge
Parameters:

authorization_code – the instance of authorization_code

get_authorization_code_challenge_method(authorization_code)

Get “code_challenge_method” associated with this authorization code. Developers MAY re-implement it in subclass, the default logic:

def get_authorization_code_challenge_method(self, authorization_code):
    return authorization_code.code_challenge_method
Parameters:

authorization_code – the instance of authorization_code