Auth0, Expo, and React Native: Authorization Code Grant Flow with PKCE

One of the most devious software problems I’ve encountered in some time.

Image for post
Image for post

Motivation: Never Log In Again!

Introducing PKCE: Proof Key for Code Exchange

import { AuthSession } from 'expo';
import * as Crypto from 'expo-crypto';
import * as Random from 'expo-random';
function URLEncode(str) {
return str.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
async function sha256(buffer) {
return await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, buffer, { encoding: Crypto.CryptoEncoding.BASE64 });
}
const randomBytes = await Random.getRandomBytesAsync(32);
let verifier = URLEncode(btoa(randomBytes.toString()));
let challenge = URLEncode(await sha256(verifier));
const redirectUrl = AuthSession.getRedirectUrl();
let authUrl = `${auth0Domain}/authorize?` + this.toQueryString({
audience: `${auth0Audience}`,
client_id: `${auth0ClientID}`,
connection: 'facebook',
scope: 'openid profile email offline_access',
redirect_uri: redirectUrl,
response_type: 'code',
code_challenge: challenge,
code_challenge_method: "S256"
nonce: 'test',
});
const result = await AuthSession.startAsync({
authUrl: authUrl
});
console.log(JSON.stringify(result));
mkdir crypto-test
cd crypto-test
npm init -y
const crypto = require("crypto");function base64URLEncode(str) {
return str
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function sha256(buffer) {
return crypto.createHash("sha256").update(buffer).digest();
}
const code_verifier = base64URLEncode(crypto.randomBytes(32));
const code_challenge = base64URLEncode(sha256(code_verifier));
console.log("code_verifier: " + code_verifier);
console.log("code_challenge: " + code_challenge);
code_verifier: XtXt_0n2w4k5mpty1noXyBN-M7DkK6FuCBXfgIa3TrQ
code_challenge: j564Gfuqd2DhQy4l_N9X-fHxxcUejgms6fKvTWBUEA8

The Problem: Always Be Sure About the Types you are Working with!

const base64String = Buffer.from(yourUint8Array).toString('base64');
import { Buffer } from 'buffer';
npm install --save buffer
const randomBytes = await Random.getRandomBytesAsync(32);
const verifier = URLEncode(btoa(randomBytes.toString()));
const randomBytes = await Random.getRandomBytesAsync(32);
const base64String = Buffer.from(randomBytes).toString('base64');
const code_verifier = URLEncode(base64String);
import * as AuthSession from 'expo-auth-session';
import * as Crypto from 'expo-crypto';
import * as Random from 'expo-random';
import { AuthSessionResult } from 'expo-auth-session';
import { Buffer } from 'buffer';
import { generateShortUUID } from '../helpers/utilHelpers';
import AsyncStorageService from './AsyncStorageService';
import { getServer } from '../helpers/serverHelpers';
interface StringMap {
[key: string]: string;
}
function toQueryString(params: StringMap): string {
return '?' +
Object.entries(params)
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
)
.join('&');
}
function URLEncode(str): string {
return str
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
async function sha256(buffer): Promise<string> {
return await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
buffer,
{ encoding: Crypto.CryptoEncoding.BASE64 }
);
}
export async function loginPKCEFlow(): Promise<void> {
const state = generateShortUUID();
const randomBytes = await Random.getRandomBytesAsync(32);
const base64String = Buffer.from(randomBytes).toString('base64');
const code_verifier = URLEncode(base64String);
const code_challenge = URLEncode(await sha256(code_verifier));
const redirectUrl = AuthSession.getRedirectUrl();
const authenticationOptions = {
response_type: 'code',
code_challenge: code_challenge,
code_challenge_method: 'S256',
client_id: process.env.AUTH0_CLIENT_ID,
redirect_uri: redirectUrl,
scope: 'openid profile email offline_access',
audience: process.env.ROOT_API_URL,
state,
};
const authUrl =
`${process.env.AUTH0_DOMAIN}authorize` +
toQueryString(authenticationOptions);
const result = await AuthSession.startAsync({
authUrl: authUrl,
});
if (
result.type === 'success' &&
result.params &&
result.params.code &&
result.params.state === state
) {
const code = result.params.code;
const authorizationCodeResponse = await getServer(
'authorization-code',
{
code,
codeVerifier: code_verifier,
redirectUrl,
}
);
if (authorizationCodeResponse.data.accessToken) {
await AsyncStorageService.setAccessToken(
authorizationCodeResponse.data.accessToken
);
}
}
}
export function generateShortUUID(): string {
return Math.random()
.toString(36)
.substring(2, 15);
}

Footnotes

Written by

https://chrisfrew.in 👨‍💻Full Stack Software Engineer 🏠Austria/USA 🍺Homebrewer ⛷🏃‍Outdoorsman

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store