OAUTH PKCE: Generate code_verifier and code_challenge in IE11 and modern browsers
If you have a single page application (SPA) and use OpenID Connect to authenticate users, you probably need to use the Authorisation Code Flow with Proof Key for Code Exchange (PKCE).
According to RFC 7636, your application must create a “code_verifier” for EACH OAuth 2.0 authorization request, and your application needs to send the “code_challenge” with the authorization request.
How do you generate these two tokens?
Some of the well-known identity service providers (IDP) show users examples that use the “crypto” module from NodeJS for token generation.
But this is confusing! You have a SPA, and the “crypto” module in their examples is only available in the node environment. These examples, kind of, imply that you hard code the generated code_challenge and use it for all your authentication requests, which is just wrong.
You might have two options:
- Use the popular crypto-js package, but it adds 500+KB of code to the login page, even when you only import the modules you use. (crypto-js/sha256, crypto-js/lib-typedarrays, crypto-js/enc-base64). This might fail your performance testing as the extra 500+KB could simply be unacceptable for user experience/performance.
- Build a backend API endpoint just for generating the code_challenge. Not ideal! Extra API calls, irrelevant logic for your backend API.
- Let’s talk about Option 3: BYO
The task sounds very simple:
- Generate a URL safe random string
- Hash it with SHA-256 and base64url encode the hash
I have created a sequence diagram for this flow here (https://medium.com/@coolgk/open-id-connect-flows-for-spa-api-758cb20470bb)
THE CHALLENGES
Generating the Random String
The first problem to tackle is that you must not use the math.random(). From MDN:
cryptoObj.getRandomValues() is not as simple as it looks. You won’t get a random string by calling
const randomString = cryptoObj.getRandomValues()
To create the random string, you firstly need to have a TypedArray. cryptoObj.getRandomValues() will replace the values in the array with random numbers. You will then need to create a string based on these random numbers. The string will also need to be URL safe as this will be the code_verifier value you send to the auth server.
Creating the SHA-256 hash
This can be done by calling the Web Crypto API
const sha256hash = crypto.subtle.digest('SHA-256', data);
Problem 1:
The digest method does not create hashes from strings. You need to convert the random string you just generated into an ArrayBuffer.
Problem 2:
IE11 uses an old standard of the Web Crypto API, the digest method does not return a promise which happens in all other browsers, instead, it returns a CryptoOperation. There is hardly any reference on the internet about this mysterial operation.
A Solution
npm install oauth-pkce
A small (409-Byte gzipped) zero-dependency helper function for generating a high-entropy cryptographic random “code_verifier” (using Web Crypto API) and its “code_challenge” based on RFC 7636.
import getPkce from 'oauth-pkce';// create a verifier of 43 characters long
getPkce(43, (error, { verifier, challenge }) => {
if (!error) {
console.log({ verifier, challenge });
}
});
This simple helper function takes care of all the problems mentioned above. It creates the tokens using Web Crypto API in IE11 and modern browsers. The source code can be found here.
You can also use it from CDN
https://cdn.jsdelivr.net/npm/oauth-pkce@latest/dist/oauth-pkce.min.js
<script src="https://cdn.jsdelivr.net/npm/oauth-pkce@0.0.2/dist/oauth-pkce.min.js" async defer></script>;
getPkce(43, (error, { verifier, challenge }) => {
if (!error) {
console.log({ verifier, challenge });
}
});
Or in React
import React, { useEffect, useState } from 'react';
import getPkce from 'oauth-pkce';
function Pkce() {
const { pkce, setPkce } = useState({});
useEffect(() => {
// getPkce relies on the window object for its crypto api
// put in in useEffect
getPkce(50, (error, { verifier, challenge }) => {
setPkce({ verifier, challenge });
});
}, []);
return (
<div>
{pkce.verifier} | {pkce.challenge}
</div>
);
}
To verify the code_verifier and code_challenge on the server-side, you could simply use the native “crypto” module from Node.
import crypto from 'crypto';
const base64 = crypto.createHash('sha256').update(code_verifier).digest('base64');
const base64UriEncoded = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const isValid = base64UriEncoded === code_challenge;
code_challenge is a Base64URL encoded string (RFC 4648). To verify the code_verifier
you need to convert the base64 value of crypto.createHash('sha256').update(code_verifier).digest('base64')
to a base64url encoded string.
Do you know it is not safe to store access_token in the localStorage? Cookies have not been retired yet, read more here why you should use cookies instead
https://medium.com/@coolgk/localstorage-vs-cookie-for-jwt-access-token-war-in-short-943fb23239ca