Skip to content

Commit

Permalink
feat: PKCE support improvements.
Browse files Browse the repository at this point in the history
Now you can omit PKCE code verifier/challenge params for authorization code flows. They will be generated automatically. Be aware the API of the createAuthorizationUrl method changed as a result. It now has a PKCE param
  • Loading branch information
nklomp committed Jan 23, 2024
1 parent e3c1601 commit 5d5cb06
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 0 deletions.
20 changes: 20 additions & 0 deletions packages/client/lib/functions/AuthorizationUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { assertValidCodeVerifier, CodeChallengeMethod, createCodeChallenge, generateCodeVerifier } from '@sphereon/oid4vci-common';

import { PKCEOpts } from '../types';

export const createPKCEOpts = (pkce: PKCEOpts) => {
if (pkce.disabled) {
return pkce;
}
if (!pkce.codeChallengeMethod) {
pkce.codeChallengeMethod = CodeChallengeMethod.S256;
}
if (!pkce.codeVerifier) {
pkce.codeVerifier = generateCodeVerifier();
}
assertValidCodeVerifier(pkce.codeVerifier);
if (!pkce.codeChallenge) {
pkce.codeChallenge = createCodeChallenge(pkce.codeVerifier, pkce.codeChallengeMethod);
}
return pkce;
};
15 changes: 15 additions & 0 deletions packages/common/lib/__tests__/randomBytes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { randomBytes } from '../functions';

import { UNIT_TEST_TIMEOUT } from './CredentialOfferUtil.spec';

describe('randomBytes should', () => {
it(
'generate random bytes of length 32',
() => {
const bytes = randomBytes(32);
expect(bytes).toBeDefined();
expect(bytes.length).toEqual(32);
},
UNIT_TEST_TIMEOUT,
);
});
43 changes: 43 additions & 0 deletions packages/common/lib/functions/RandomUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import SHA from 'sha.js';
import * as u8a from 'uint8arrays';
import { SupportedEncodings } from 'uint8arrays/to-string';

import { CodeChallengeMethod } from '../types';

import { randomBytes } from './randomBytes';

export const CODE_VERIFIER_DEFAULT_LENGTH = 128;
export const NONCE_LENGTH = 32;

export const generateRandomString = (length: number, encoding?: SupportedEncodings): string => {
return u8a.toString(randomBytes(length), encoding).slice(0, length);
};

export const generateNonce = (length?: number): string => {
return generateRandomString(length ?? NONCE_LENGTH);
};
export const generateCodeVerifier = (length?: number): string => {
const codeVerifier = generateRandomString(length ?? CODE_VERIFIER_DEFAULT_LENGTH, 'base64url');
assertValidCodeVerifier(codeVerifier);
return codeVerifier;
};

export const createCodeChallenge = (codeVerifier: string, codeChallengeMethod?: CodeChallengeMethod): string => {
if (codeChallengeMethod === CodeChallengeMethod.plain) {
return codeVerifier;
} else if (!codeChallengeMethod || codeChallengeMethod === CodeChallengeMethod.S256) {
return u8a.toString(SHA('sha256').update(codeVerifier).digest(), 'base64url');
} else {
// Just a precaution if a new method would be introduced
throw Error(`code challenge method ${codeChallengeMethod} not implemented`);
}
};

export const assertValidCodeVerifier = (codeVerifier: string) => {
const length = codeVerifier.length;
if (length < 43) {
throw Error(`code_verifier should have a minimum length of 43; see rfc7636`);
} else if (length > 128) {
throw Error(`code_verifier should have a maximum length of 128; see rfc7636`);
}
};
61 changes: 61 additions & 0 deletions packages/common/lib/functions/randomBytes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict';

// limit of Crypto.getRandomValues()
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
const MAX_BYTES = 65536;

// Node supports requesting up to this number of bytes
// https://github.com/nodejs/node/blob/master/lib/internal/crypto/random.js#L48
const MAX_UINT32 = 4294967295;

function oldBrowser() {
throw new Error('Secure random number generation is not supported by this browser.\nUse Chrome, Firefox or Internet Explorer 11');
}

// eslint-disable-next-line no-undef
const _global = typeof globalThis !== 'undefined' ? globalThis : global;

let crypto = _global.crypto || _global.msCrypto;
if (!crypto) {
try {
// eslint-disable-next-line no-undef
crypto = require('crypto');
} catch (err) {
throw Error('crypto module is not available');
}
}

if (crypto && crypto.getRandomValues) {
// eslint-disable-next-line no-undef
module.exports = randomBytes;
} else {
// eslint-disable-next-line no-undef
module.exports = oldBrowser;
}

function randomBytes(size) {
// phantomjs needs to throw
if (size > MAX_UINT32) throw new Error('requested too many random bytes');

// eslint-disable-next-line no-undef
const bytes = Buffer.allocUnsafe(size);

if (size > 0) {
// getRandomValues fails on IE if size == 0
if (size > MAX_BYTES) {
// this is the max bytes crypto.getRandomValues
// can do at once see https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues
for (let generated = 0; generated < size; generated += MAX_BYTES) {
// buffer.slice automatically checks if the end is past the end of
// the buffer so we don't have to here
crypto.getRandomValues(bytes.slice(generated, generated + MAX_BYTES));
}
} else {
crypto.getRandomValues(bytes);
}
}
return Uint8Array.from(bytes);
}

// eslint-disable-next-line no-undef
module.exports = { randomBytes };

0 comments on commit 5d5cb06

Please sign in to comment.