Skip to content

Commit

Permalink
feat(js): generate state, code verifier and code challenge (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
IceHe authored Jan 19, 2022
1 parent 07322cb commit 9784f3a
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 14 deletions.
3 changes: 2 additions & 1 deletion packages/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"report": "WITH_REPORT=true npm run package"
},
"dependencies": {
"@silverhand/essentials": "^1.1.2"
"@silverhand/essentials": "^1.1.2",
"js-base64": "^3.7.2"
},
"devDependencies": {
"@peculiar/webcrypto": "^1.1.7",
Expand Down
9 changes: 0 additions & 9 deletions packages/js/src/index.test.ts

This file was deleted.

5 changes: 1 addition & 4 deletions packages/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
// Temporary content, for `pnpm build` to pass CI, will be deleted later
import { Optional } from '@silverhand/essentials';

export type TODO = Optional<string>;
export * from './utils/generators';
84 changes: 84 additions & 0 deletions packages/js/src/utils/generators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @jest-environment node
*/
import { toUint8Array } from 'js-base64';

import { generateCodeChallenge, generateCodeVerifier, generateState } from './generators';

const isUrlSafe = (input: string) => /^[\w-]*$/.test(input);

describe('generateState', () => {
test('should be random value', () => {
const state1 = generateState();
const state2 = generateState();
expect(state1).not.toEqual(state2);
});

test('should be url-safe', () => {
const state = generateState();
expect(isUrlSafe(state)).toBeTruthy();
});

test('raw random data length should be length 64', () => {
const state = generateState();
expect(toUint8Array(state).length).toEqual(64);
});
});

describe('generateCodeVerifier', () => {
test('should be random value', () => {
const codeVerifier1 = generateCodeVerifier();
const codeVerifier2 = generateCodeVerifier();
expect(codeVerifier1).not.toEqual(codeVerifier2);
});

test('should be url-safe', () => {
const codeVerifier = generateCodeVerifier();
expect(isUrlSafe(codeVerifier)).toBeTruthy();
});

test('raw random data length should be length 64', () => {
const codeVerifier = generateCodeVerifier();
expect(toUint8Array(codeVerifier).length).toEqual(64);
});
});

describe('generateCodeChallenge', () => {
test('dealing with different code verifiers should not be equal', async () => {
const codeVerifier1 = generateCodeVerifier();
const codeChallenge1 = await generateCodeChallenge(codeVerifier1);
const codeVerifier2 = generateCodeVerifier();
const codeChallenge2 = await generateCodeChallenge(codeVerifier2);
expect(codeChallenge1).not.toEqual(codeChallenge2);
});

test('dealing with same code verifier should be equal', async () => {
const codeVerifier = generateCodeVerifier();
const codeChallenge1 = await generateCodeChallenge(codeVerifier);
const codeChallenge2 = await generateCodeChallenge(codeVerifier);
expect(codeChallenge1).toEqual(codeChallenge2);
});

describe('dealing with static code verifier should not throw', () => {
test('dealing with url-safe code verifier should not throw', async () => {
const codeVerifier =
'tO6MabnMFRAatnlMa1DdSstypzzkgalL1-k8Hr_GdfTj-VXGiEACqAkSkDhFuAuD8FOU8lMishaXjt29Xt2Oww';
const codeChallenge = await generateCodeChallenge(codeVerifier);
expect(codeChallenge).toEqual('0K3SLeGlNNzFswYJjcVzcN4C76m_8NZORxFJLBJWGwg');
});

describe('dealing with non-url-safe code verifier should not throw', () => {
test('latin1 character', async () => {
const codeVerifier = 'Á';
const codeChallenge = await generateCodeChallenge(codeVerifier);
expect(codeChallenge).toEqual('p3yvZiKYauPicLIDZ0W1peDz4Z9KFC-9uxtDfoO1KOQ');
});

test('emoji character', async () => {
const codeVerifier = '🚀';
const codeChallenge = await generateCodeChallenge(codeVerifier);
expect(codeChallenge).toEqual('67wLKHDrMj8rbP-lxJPO74GufrNq_HPU4DZzAWMdrsU');
});
});
});
});
33 changes: 33 additions & 0 deletions packages/js/src/utils/generators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/** @link [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636) */

import { fromUint8Array } from 'js-base64';

/**
* @param length The length of the raw random data.
*/
const generateRandomString = (length = 64) =>
fromUint8Array(crypto.getRandomValues(new Uint8Array(length)), true);

/**
* Generates random string for state and encodes them in url safe base64
*/
export const generateState = () => generateRandomString();

/**
* Generates code verifier
*
* @link [Client Creates a Code Verifier](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1)
*/
export const generateCodeVerifier = () => generateRandomString();

/**
* Calculates the S256 PKCE code challenge for an arbitrary code verifier and encodes it in url safe base64
*
* @param {String} codeVerifier Code verifier to calculate the S256 code challenge for
* @link [Client Creates the Code Challenge](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2)
*/
export const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier);
const codeChallenge = new Uint8Array(await crypto.subtle.digest('SHA-256', encodedCodeVerifier));
return fromUint8Array(codeChallenge, true);
};
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9784f3a

Please sign in to comment.