Proof Key for Code Exchange by OAuth Public Clients

This RFC7636 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.

Using RFC7636 Grant in Server

In order to apply proof key for code exchange, you need to replace the original AuthorizationCodeGrant in RFC6749 with the one in RFC7636:

from authlib.specs.rfc6749.grants import AuthorizationCodeGrant
# use the one below
from authlib.specs.rfc7636 import AuthorizationCodeGrant

It requires two more methods to be implemented. But before that, we need to re-design our AuthorizationCode database. If you are using Flask, check the section Authorization Code Grant.

The new database could contain two more columns:

  1. code_challenge: A VARCHAR
  2. code_challenge_method: A VARCHAR

The new AuthorizationCodeGrant implementation should be something like:

from authlib.specs.rfc7636 import AuthorizationCodeGrant

class MyAuthorizationCodeGrant(AuthorizationCodeGrant):
    def create_authorization_code(self, client, grant_user, request):
        code = generate_token(48)

        # NOTICE BELOW
        code_challenge = request.data.get('code_challenge')
        code_challenge_method = request.data.get('code_challenge_method')
        item = AuthorizationCode(
            code=code,
            client_id=client.client_id,
            redirect_uri=request.redirect_uri,
            scope=request.scope,
            user_id=grant_user.get_user_id(),
            code_challenge=code_challenge,
            code_challenge_method=code_challenge_method,
        )
        db.session.add(item)
        db.session.commit()
        return code

    def parse_authorization_code(self, code, client):
        item = AuthorizationCode.query.filter_by(
            code=code, client_id=client.client_id).first()
        if item and not item.is_expired():
            return item

    def delete_authorization_code(self, authorization_code):
        db.session.delete(authorization_code)
        db.session.commit()

    def authenticate_user(self, authorization_code):
        return User.query.get(authorization_code.user_id)

    # NOTICE BELOW
    def get_authorization_code_challenge(self, authorization_code):
        return authorization_code.code_challenge

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

server.register_grant(MyAuthorizationCodeGrant)

You can also save code_challenge and code_challenge_method elsewhere.

Using code_challenge in Client

Read the Code Challenge section in the framework integrations:

  1. Flask OAuth Client.
  2. Django OAuth Client.

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

>>> from authlib.specs.rfc7636 import create_s256_code_challenge
>>> code_verifier = 'generate-a-urlsafe-string'
>>> code_challenge = create_s256_code_challenge(code_verifier)
>>> uri, state = session.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_access_token(access_token_url, authorization_response=authorization_response, code_verifier=code_verifier)

The authorization flow is the same as in OAuth 2 Session, what you need to do is:

  1. adding code_challenge and code_challenge_method in authorization_url().
  2. adding code_verifier in fetch_access_token().

API Reference

class authlib.specs.rfc7636.AuthorizationCodeGrant(request, server)
DEFAULT_CODE_CHALLENGE_METHOD = 'plain'

defaults to “plain” if not present in the request

validate_authorization_request()

The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI using the “application/x-www-form-urlencoded” format. Per Section 4.1.1.

response_type
REQUIRED. Value MUST be set to “code”.
client_id
REQUIRED. The client identifier as described in Section 2.2.
redirect_uri
OPTIONAL. As described in Section 3.1.2.
scope
OPTIONAL. The scope of the access request as described by Section 3.3.
state
RECOMMENDED. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12.

The client directs the resource owner to the constructed URI using an HTTP redirection response, or by other means available to it via the user-agent.

For example, the client directs the user-agent to make the following HTTP request using TLS (with extra line breaks for display purposes only):

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

The authorization server validates the request to ensure that all required parameters are present and valid. If the request is valid, the authorization server authenticates the resource owner and obtains an authorization decision (by asking the resource owner or by establishing approval via other means).

validate_token_request()

The client makes a request to the token endpoint by sending the following parameters using the “application/x-www-form-urlencoded” format per Section 4.1.3:

grant_type
REQUIRED. Value MUST be set to “authorization_code”.
code
REQUIRED. The authorization code received from the authorization server.
redirect_uri
REQUIRED, if the “redirect_uri” parameter was included in the authorization request as described in Section 4.1.1, and their values MUST be identical.
client_id
REQUIRED, if the client is not authenticating with the authorization server as described in Section 3.2.1.

If the client type is confidential or the client was issued client credentials (or assigned other authentication requirements), the client MUST authenticate with the authorization server as described in Section 3.2.1.

For example, the client makes the following HTTP request using TLS:

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
get_authorization_code_challenge(authorization_code)

Get “code_challenge” associated with this authorization code. Developers MUST implement it in subclass, e.g.:

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 MUST implement it in subclass, e.g.:

def get_authorization_code_challenge_method(self, authorization_code):
    return authorization_code.code_challenge_method
Parameters:authorization_code – the instance of authorization_code