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())
Used the example from Uber5/react-pkce, from the NPM Package "react-pkce".
Steps:
- Create a normal blank ReactJS app
- Add the file
src/pkce.js
- Update
src/App.js
- Update
src/App.test.js
npx create-react-app code_verifier_generator
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}
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
.
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);
});