diff --git a/browser/crypto.js b/browser/crypto.js index d19939c..fc9f7b4 100644 --- a/browser/crypto.js +++ b/browser/crypto.js @@ -2,7 +2,7 @@ export function randomBytes(_) { return { toString(_) { // not quite the same but it's only used for nonce generation - return window.crypto.randomUUID(); + return window.crypto.randomUUID().slice(0, 64); }, }; } diff --git a/lib/client.ts b/lib/client.ts index 1ab9ca7..a4c2a75 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -18,6 +18,7 @@ import { } from './types.js'; import { toAuthHeader } from './oauth.js'; import { hasProperty } from './helpers-internal.js'; +import * as crypto from 'crypto'; const version = process.env.VERSION_NUMBER || 'dev'; const homepage = 'https://github.com/lionralfs/discogs-client'; @@ -197,7 +198,9 @@ export class DiscogsClient { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.auth.accessToken!, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.auth.accessTokenSecret! + this.auth.accessTokenSecret!, + { now: () => Date.now() }, + crypto ); } else if (this.auth.method === 'discogs') { authHeader = 'Discogs'; diff --git a/lib/oauth.ts b/lib/oauth.ts index 4bfd069..0ec9337 100644 --- a/lib/oauth.ts +++ b/lib/oauth.ts @@ -108,9 +108,14 @@ export function toAuthHeader( consumerKey: string, consumerSecret: string, accessToken: string, - accessTokenSecret: string + accessTokenSecret: string, + clock: { now: () => number }, + _crypto: typeof crypto ) { - const nonce = crypto.randomBytes(64).toString('hex'); - const timestamp = Date.now(); + // generate 32 bytes which is a string of 64 characters in hex encoding + // because any request that includes a nonce string of length > 64 characters is rejected + const nonce = _crypto.randomBytes(32).toString('hex', 0, 64); + // timestamp is expected in seconds + const timestamp = Math.floor(clock.now() / 1000); return `OAuth oauth_consumer_key="${consumerKey}", oauth_token="${accessToken}", oauth_signature_method="PLAINTEXT", oauth_signature="${consumerSecret}&${accessTokenSecret}", oauth_timestamp="${timestamp}", oauth_nonce="${nonce}", oauth_token_secret="${accessTokenSecret}", oauth_version="1.0"`; } diff --git a/test/unit/oauth.test.ts b/test/unit/oauth.test.ts index 3ec95bf..6ca4e34 100644 --- a/test/unit/oauth.test.ts +++ b/test/unit/oauth.test.ts @@ -1,9 +1,33 @@ import test from 'ava'; import { toAuthHeader } from '@lib/oauth.js'; +import type * as crypto from 'crypto'; + +const fakeCrypto: typeof crypto = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + randomBytes(size: number) { + return { + toString(encoding: string, start: number, end: number) { + if (size === 32 && encoding === 'hex' && start === 0 && end === 64) { + return 'correct'; + } else { + return 'incorrect'; + } + }, + }; + }, +}; test('OAuth: toAuthHeader', t => { t.regex( - toAuthHeader('consumer_key', 'consumer_secret', 'access_token', 'access_token_secret'), - /^OAuth oauth_consumer_key="consumer_key", oauth_token="access_token", oauth_signature_method="PLAINTEXT", oauth_signature="consumer_secret&access_token_secret", oauth_timestamp="\d+", oauth_nonce=".+", oauth_token_secret="access_token_secret", oauth_version="1.0"$/ + toAuthHeader( + 'consumer_key', + 'consumer_secret', + 'access_token', + 'access_token_secret', + { now: () => 12345 }, + fakeCrypto + ), + /^OAuth oauth_consumer_key="consumer_key", oauth_token="access_token", oauth_signature_method="PLAINTEXT", oauth_signature="consumer_secret&access_token_secret", oauth_timestamp="12", oauth_nonce="correct", oauth_token_secret="access_token_secret", oauth_version="1.0"$/ ); });