@@ -9,7 +9,8 @@ const path = require('path');
99const { NONCE_BYTES } = require ( '../lib/consts' ) ;
1010
1111// Regex that validates OAuth state nonces (from workers/api.js security fix)
12- const NONCE_VALIDATION_REGEX = / ^ [ A - Z a - z 0 - 9 _ - ] { 21 , 22 } $ / ;
12+ // Accepts both base64url and base64 encoding for backward compatibility
13+ const NONCE_VALIDATION_REGEX = / ^ [ A - Z a - z 0 - 9 _ \- + / ] { 21 , 22 } = { 0 , 2 } $ / ;
1314
1415test ( 'OAuth nonce encoding tests' , async t => {
1516 await t . test ( 'base64url encoded nonce matches validation regex' , async ( ) => {
@@ -20,40 +21,39 @@ test('OAuth nonce encoding tests', async t => {
2021 }
2122 } ) ;
2223
23- await t . test ( 'standard base64 encoded nonce fails validation regex' , async ( ) => {
24- // Standard base64 can contain +, /, and = which are not in the validation regex
25- let foundInvalidChar = false ;
26- for ( let i = 0 ; i < 1000 && ! foundInvalidChar ; i ++ ) {
24+ await t . test ( 'standard base64 encoded nonce passes validation regex' , async ( ) => {
25+ // Standard base64 can contain +, /, and = which are now accepted for backward compatibility
26+ for ( let i = 0 ; i < 100 ; i ++ ) {
2727 const nonce = crypto . randomBytes ( NONCE_BYTES ) . toString ( 'base64' ) ;
28- if ( ! NONCE_VALIDATION_REGEX . test ( nonce ) ) {
29- foundInvalidChar = true ;
30- }
28+ assert . ok ( NONCE_VALIDATION_REGEX . test ( nonce ) , `base64 nonce should match validation regex: ${ nonce } ` ) ;
3129 }
32- assert . ok ( foundInvalidChar , 'standard base64 should eventually produce characters that fail validation (+=/) ' ) ;
3330 } ) ;
3431
35- await t . test ( 'no NONCE_BYTES usage with plain base64 encoding in codebase' , async ( ) => {
36- // Static analysis: scan source files for incorrect nonce encoding
37- // This prevents regression of the base64 vs base64url bug
32+ await t . test ( 'new nonce generation uses base64url encoding in codebase' , async ( ) => {
33+ // Static analysis: scan source files for nonce generation
34+ // New nonces should use base64url, but validation accepts both for backward compatibility
3835 const sourceFiles = [ 'workers/api.js' , 'lib/routes-ui.js' , 'lib/ui-routes/account-routes.js' , 'lib/api-routes/account-routes.js' ] ;
3936
40- const problematicPattern = / N O N C E _ B Y T E S \) \. t o S t r i n g \( [ ' " ] b a s e 6 4 [ ' " ] \) / ;
41- const violations = [ ] ;
37+ const base64urlPattern = / N O N C E _ B Y T E S \) \. t o S t r i n g \( [ ' " ] b a s e 6 4 u r l [ ' " ] \) / ;
38+ const base64Pattern = / N O N C E _ B Y T E S \) \. t o S t r i n g \( [ ' " ] b a s e 6 4 [ ' " ] \) / ;
39+ const usesBase64url = [ ] ;
40+ const usesBase64 = [ ] ;
4241
4342 for ( const file of sourceFiles ) {
4443 const filePath = path . join ( __dirname , '..' , file ) ;
4544 if ( fs . existsSync ( filePath ) ) {
4645 const content = fs . readFileSync ( filePath , 'utf8' ) ;
47- const lines = content . split ( '\n' ) ;
48- lines . forEach ( ( line , index ) => {
49- if ( problematicPattern . test ( line ) ) {
50- violations . push ( ` ${ file } : ${ index + 1 } : ${ line . trim ( ) } ` ) ;
51- }
52- } ) ;
46+ if ( base64urlPattern . test ( content ) ) {
47+ usesBase64url . push ( file ) ;
48+ }
49+ if ( base64Pattern . test ( content ) ) {
50+ usesBase64 . push ( file ) ;
51+ }
5352 }
5453 }
5554
56- assert . strictEqual ( violations . length , 0 , `Found NONCE_BYTES with plain base64 encoding (should use base64url):\n${ violations . join ( '\n' ) } ` ) ;
55+ // New nonce generation should use base64url (old base64 nonces are only accepted for backward compat)
56+ assert . strictEqual ( usesBase64 . length , 0 , `Found NONCE_BYTES with plain base64 encoding (new code should use base64url):\n${ usesBase64 . join ( '\n' ) } ` ) ;
5757 } ) ;
5858
5959 await t . test ( 'all NONCE_BYTES usages use base64url encoding' , async ( ) => {
@@ -97,12 +97,12 @@ test('OAuth nonce encoding tests', async t => {
9797 assert . strictEqual ( incorrectUsages . length , 0 , `Found NONCE_BYTES with incorrect encoding:\n${ incorrectUsages . join ( '\n' ) } ` ) ;
9898 } ) ;
9999
100- await t . test ( 'nonce validation with error for invalid format' , async ( ) => {
101- // Verify that files using data.n validate the format and throw error if invalid
102- // This rejects old cached URLs with standard base64 nonces
100+ await t . test ( 'nonce validation accepts both base64 and base64url formats' , async ( ) => {
101+ // Verify that files using data.n validate the format with backward-compatible regex
103102 const filesToCheck = [ 'lib/routes-ui.js' , 'lib/ui-routes/account-routes.js' ] ;
104103
105104 // Pattern: validates nonce and throws Boom error for invalid format
105+ // Now accepts both base64url and base64 encoding
106106 const validationPattern = / i f .* ! .* t e s t \( n o n c e \) .* \{ [ \s \S ] * ?B o o m \. b o o m i f y .* I n v a l i d n o n c e f o r m a t / ;
107107
108108 const missingValidation = [ ] ;
@@ -119,4 +119,31 @@ test('OAuth nonce encoding tests', async t => {
119119
120120 assert . strictEqual ( missingValidation . length , 0 , `Files using data.n must validate and reject invalid nonces:\n${ missingValidation . join ( '\n' ) } ` ) ;
121121 } ) ;
122+
123+ await t . test ( 'invalid nonces are rejected' , async ( ) => {
124+ // Test that completely invalid nonces are still rejected
125+ const invalidNonces = [
126+ '' , // empty
127+ 'short' , // too short
128+ 'a' . repeat ( 30 ) , // too long (no padding scenario)
129+ '!@#$%^&*(){}[]|\\' , // invalid characters
130+ 'valid123456789012345===' , // too much padding
131+ 'valid12345678901234567890====' // way too long with padding
132+ ] ;
133+
134+ for ( const nonce of invalidNonces ) {
135+ assert . ok ( ! NONCE_VALIDATION_REGEX . test ( nonce ) , `Invalid nonce should be rejected: ${ nonce } ` ) ;
136+ }
137+ } ) ;
138+
139+ await t . test ( 'edge cases for base64/base64url characters' , async ( ) => {
140+ // Test specific edge cases for the character differences
141+ const base64urlOnlyChars = 'AAAAAAAAAAAAAAAAAAA_-' ; // 21 chars with _ and -
142+ const base64OnlyChars = 'AAAAAAAAAAAAAAAAAAA+/==' ; // 21 chars + padding with + and /
143+ const mixedChars = 'AAAAAAAAAAAAAAAA_-+/' ; // mixed characters (20 chars)
144+
145+ assert . ok ( NONCE_VALIDATION_REGEX . test ( base64urlOnlyChars ) , 'base64url-only chars should pass' ) ;
146+ assert . ok ( NONCE_VALIDATION_REGEX . test ( base64OnlyChars ) , 'base64-only chars with padding should pass' ) ;
147+ assert . ok ( ! NONCE_VALIDATION_REGEX . test ( mixedChars ) , 'too short nonce should fail' ) ;
148+ } ) ;
122149} ) ;
0 commit comments