Skip to content

Instantly share code, notes, and snippets.

@nicc777
Last active April 28, 2021 06:55
Show Gist options
  • Save nicc777/a3de9e9fcb3d3f7c0b94e71d0f6873a8 to your computer and use it in GitHub Desktop.
Save nicc777/a3de9e9fcb3d3f7c0b94e71d0f6873a8 to your computer and use it in GitHub Desktop.
Generating PKCE Code Verifier and Code Challange

Generating PKCE Code Verifier and Code Challange

Python

The following example was taken from openstack-archive/deb-python-oauth2client which was an archived project at the time of writing.

# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth
Public Clients

See RFC7636.
"""

import base64
import hashlib
import os


def code_verifier(n_bytes=64):
    """
    Generates a 'code_verifier' as described in section 4.1 of RFC 7636.

    This is a 'high-entropy cryptographic random string' that will be
    impractical for an attacker to guess.

    Args:
        n_bytes: integer between 31 and 96, inclusive. default: 64
            number of bytes of entropy to include in verifier.

    Returns:
        Bytestring, representing urlsafe base64-encoded random data.
    """
    verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=')
    # https://tools.ietf.org/html/rfc7636#section-4.1
    # minimum length of 43 characters and a maximum length of 128 characters.
    if len(verifier) < 43:
        raise ValueError("Verifier too short. n_bytes must be > 30.")
    elif len(verifier) > 128:
        raise ValueError("Verifier too long. n_bytes must be < 97.")
    else:
        return verifier


def code_challenge(verifier):
    """
    Creates a 'code_challenge' as described in section 4.2 of RFC 7636
    by taking the sha256 hash of the verifier and then urlsafe
    base64-encoding it.

    Args:
        verifier: bytestring, representing a code_verifier as generated by
            code_verifier().

    Returns:
        Bytestring, representing a urlsafe base64-encoded sha256 hash digest,
            without '=' padding.
    """
    digest = hashlib.sha256(verifier).digest()
    return base64.urlsafe_b64encode(digest).rstrip(b'=')

Using the oauth.com playground, I was able to verify that the above code produce the expected results.

If you need to create your own test case, you can use the following test data:

code_verifier = 'g7Vj5BmtbO5ywHJWlY6ex9yc6RjnS03NGTu-nNDLkwI8VbGr'
expected_code_challange = 'jcmRyMHQb1S4SzgwjfbFzUepgZFVE01P5TLUiTH8oe0'
assert(expected_code_challange == code_challenge(code_verifier.encode('ascii')).decode())

React

Used the example from Uber5/react-pkce, from the NPM Package "react-pkce".

Steps:

  1. Create a normal blank ReactJS app
  2. Add the file src/pkce.js
  3. Update src/App.js
  4. Update src/App.test.js

Step 1

npx create-react-app code_verifier_generator 

Step 2

In src/pkce.js, I had the following:

import crypto from 'crypto'

function base64URLEncode(str) {
  return str.toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
}

// var verifier = base64URLEncode(crypto.randomBytes(32));

function sha256(buffer) {
  return crypto.createHash('sha256').update(buffer).digest();
}
// var challenge = base64URLEncode(sha256(verifier));

export {base64URLEncode, sha256}

Step 3

The file src/App.js was only lightly updated:

import './App.css';
import { base64URLEncode, sha256 } from './pkce';

function App() {

  var verifier = 'g7Vj5BmtbO5ywHJWlY6ex9yc6RjnS03NGTu-nNDLkwI8VbGr';
  var challenge = base64URLEncode(sha256(verifier));

  return (
    <div className="App">
      <header className="App-header">
        <p>
          challenge = {challenge}
        </p>
      </header>
    </div>
  );
}

export default App;

The above should result in the challange being printed on the web page with the value jcmRyMHQb1S4SzgwjfbFzUepgZFVE01P5TLUiTH8oe0.

Step 4

Update the test file src/App.test.js as follow:

import { render, screen } from '@testing-library/react';
import App from './App';

test('Validate the challenge value', () => {
  var expected = 'jcmRyMHQb1S4SzgwjfbFzUepgZFVE01P5TLUiTH8oe0';
  render(<App />);
  const linkElement = screen.getByText(/challenge/i);
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveTextContent(expected);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment