diff --git a/scripts/build.ts b/scripts/build.ts index eab69848..03fe1ee5 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -19,7 +19,12 @@ fileReplace('dist/es/Constants.js', '__STANZAJS_VERSION__', Pkg.version); Child('npm run compile:rollup'); -Child('mkdir dist/npm'); +if (!FS.existsSync("dist")) { + FS.mkdirSync("dist"); +} +if (!FS.existsSync("dist/npm")) { + FS.mkdirSync("dist/npm"); +} Child('cp -r dist/cjs/* dist/npm/'); Child('cp dist/es/index.module.js dist/npm/module.js'); Child(`cp ${__dirname}/../*.md dist/npm`); diff --git a/src/Client.ts b/src/Client.ts index b29734e0..7f7ecb6e 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -62,7 +62,7 @@ export default class Client extends EventEmitter { this.sasl.register('SCRAM-SHA-1', SASL.SCRAM, 200); this.sasl.register('DIGEST-MD5', SASL.DIGEST, 100); this.sasl.register('OAUTHBEARER', SASL.OAUTH, 100); - this.sasl.register('X-OAUTH2', SASL.PLAIN, 50); + this.sasl.register('X-OAUTH2', SASL.X_OAUTH2, 50); this.sasl.register('PLAIN', SASL.PLAIN, 1); this.sasl.register('ANONYMOUS', SASL.ANONYMOUS, 0); diff --git a/src/lib/sasl/index.ts b/src/lib/sasl/index.ts index 85a5b28a..2f3fe81d 100644 --- a/src/lib/sasl/index.ts +++ b/src/lib/sasl/index.ts @@ -24,7 +24,7 @@ export interface CacheableCredentials extends Credentials { } export interface ExpectedCredentials { - required: string[]; + required: string[][]; optional: string[]; } @@ -109,12 +109,17 @@ export class Factory { this.mechanisms = this.mechanisms.filter(mech => mech.name !== mechName); } - public createMechanism(names: string[]): Mechanism | null { + public createMechanism(names: string[], credentials: Credentials): Mechanism | null { const availableNames = names.map(name => name.toUpperCase()); for (const knownMech of this.mechanisms) { for (const availableMechName of availableNames) { if (availableMechName === knownMech.name) { - return new knownMech.constructor(knownMech.name); + const mech = new knownMech.constructor(knownMech.name); + for (const requiredCredentials of mech.getExpectedCredentials().required as (keyof Credentials)[][]) { + if (requiredCredentials.every(req => credentials[req] !== undefined)) { + return mech; + } + } } } } @@ -194,7 +199,7 @@ function escapeUsername(name: string): string { export class ANONYMOUS extends SimpleMech implements Mechanism { public getExpectedCredentials(): ExpectedCredentials { - return { optional: ['trace'], required: [] }; + return { optional: ['trace'], required: [[]] }; } public createResponse(credentials: Credentials): Buffer { @@ -208,7 +213,7 @@ export class ANONYMOUS extends SimpleMech implements Mechanism { export class EXTERNAL extends SimpleMech implements Mechanism { public getExpectedCredentials(): ExpectedCredentials { - return { optional: ['authzid'], required: [] }; + return { optional: ['authzid'], required: [[]] }; } public createResponse(credentials: Credentials): Buffer { @@ -224,7 +229,7 @@ export class PLAIN extends SimpleMech implements Mechanism { public getExpectedCredentials(): ExpectedCredentials { return { optional: ['authzid'], - required: ['username', 'password'] + required: [['username', 'password']] }; } @@ -239,6 +244,29 @@ export class PLAIN extends SimpleMech implements Mechanism { } } +// ==================================================================== +// X-OAUTH2 +// ==================================================================== + +export class X_OAUTH2 extends SimpleMech implements Mechanism { + public getExpectedCredentials(): ExpectedCredentials { + return { + optional: ['authzid'], + required: [['username', 'token']], + }; + } + + public createResponse(credentials: Credentials): Buffer { + return Buffer.from( + (credentials.authzid || '') + + '\x00' + + credentials.username + + '\x00' + + credentials.token + ); + } +} + // ==================================================================== // OAUTHBEARER // ==================================================================== @@ -254,7 +282,7 @@ export class OAUTH extends SimpleMech implements Mechanism { public getExpectedCredentials(): ExpectedCredentials { return { optional: ['authzid'], - required: ['token'] + required: [['token']] }; } @@ -311,7 +339,7 @@ export class DIGEST extends SimpleMech implements Mechanism { public getExpectedCredentials(): ExpectedCredentials { return { optional: ['authzid', 'clientNonce', 'realm'], - required: ['host', 'password', 'serviceName', 'serviceType', 'username'] + required: [['host', 'password', 'serviceName', 'serviceType', 'username']] }; } @@ -412,9 +440,9 @@ export class SCRAM implements Mechanism { public getExpectedCredentials(): ExpectedCredentials { const optional = ['authzid', 'clientNonce']; - const required = ['username', 'password']; + const required = [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']]; if (this.useChannelBinding) { - required.push('tlsUnique'); + required.forEach(x => x.push('tlsUnique')); } return { optional, diff --git a/src/plugins/sasl.ts b/src/plugins/sasl.ts index 632b1ae4..4a60c208 100644 --- a/src/plugins/sasl.ts +++ b/src/plugins/sasl.ts @@ -27,7 +27,7 @@ declare module '../' { export default function (client: Agent): void { client.registerFeature('sasl', 100, async (features, done) => { - const mech = client.sasl.createMechanism(features.sasl!.mechanisms); + const mech = client.sasl.createMechanism(features.sasl!.mechanisms, (await client.getCredentials()) as Credentials); const saslHandler = async (sasl: SASL) => { if (!mech) { diff --git a/test/sasl/anonymous.ts b/test/sasl/anonymous.ts index 6113a0cd..41f6e197 100644 --- a/test/sasl/anonymous.ts +++ b/test/sasl/anonymous.ts @@ -4,7 +4,8 @@ test('SASL - ANONYMOUS', () => { const factory = new SASL.Factory(); factory.register('ANONYMOUS', SASL.ANONYMOUS, 10); - const mech = factory.createMechanism(['ANONYMOUS'])!; + const creds: SASL.Credentials = {}; + const mech = factory.createMechanism(['ANONYMOUS'], creds)!; expect(mech.name).toBe('ANONYMOUS'); expect(mech.providesMutualAuthentication).toBeFalsy(); @@ -12,11 +13,9 @@ test('SASL - ANONYMOUS', () => { const neededCreds = mech.getExpectedCredentials(); expect(neededCreds).toStrictEqual({ optional: ['trace'], - required: [] + required: [[]] }); - const creds: SASL.Credentials = {}; - const response = mech.createResponse(creds)!; expect(response.toString('base64')).toBe(''); @@ -33,7 +32,7 @@ test('SASL - ANONYMOUS with trace', () => { const factory = new SASL.Factory(); factory.register('ANONYMOUS', SASL.ANONYMOUS, 10); - const mech = factory.createMechanism(['ANONYMOUS'])!; + const mech = factory.createMechanism(['ANONYMOUS'], {})!; expect(mech.name).toBe('ANONYMOUS'); expect(mech.providesMutualAuthentication).toBeFalsy(); @@ -41,7 +40,7 @@ test('SASL - ANONYMOUS with trace', () => { const neededCreds = mech.getExpectedCredentials(); expect(neededCreds).toStrictEqual({ optional: ['trace'], - required: [] + required: [[]] }); const creds: SASL.Credentials = { diff --git a/test/sasl/digest.ts b/test/sasl/digest.ts index b0881016..e2ece3af 100644 --- a/test/sasl/digest.ts +++ b/test/sasl/digest.ts @@ -4,17 +4,6 @@ test('SASL - DIGEST-MD5', () => { const factory = new SASL.Factory(); factory.register('DIGEST-MD5', SASL.DIGEST, 10); - const mech = factory.createMechanism(['DIGEST-MD5'])!; - - expect(mech.name).toBe('DIGEST-MD5'); - expect(mech.providesMutualAuthentication).toBe(false); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce', 'realm'], - required: ['host', 'password', 'serviceName', 'serviceType', 'username'] - }); - const clientNonce = 'random-client-nonce'; const creds: SASL.Credentials = { clientNonce: clientNonce, @@ -24,6 +13,17 @@ test('SASL - DIGEST-MD5', () => { serviceType: 'xmpp', username: 'user' }; + const mech = factory.createMechanism(['DIGEST-MD5'], creds)!; + + expect(mech.name).toBe('DIGEST-MD5'); + expect(mech.providesMutualAuthentication).toBe(false); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce', 'realm'], + required: [['host', 'password', 'serviceName', 'serviceType', 'username']] + }); + const response1 = mech.createResponse(creds)!; expect(response1).toBeNull(); @@ -56,17 +56,6 @@ test('SASL - DIGEST-MD5 with authzid', () => { const factory = new SASL.Factory(); factory.register('DIGEST-MD5', SASL.DIGEST, 10); - const mech = factory.createMechanism(['DIGEST-MD5'])!; - - expect(mech.name).toBe('DIGEST-MD5'); - expect(mech.providesMutualAuthentication).toBe(false); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce', 'realm'], - required: ['host', 'password', 'serviceName', 'serviceType', 'username'] - }); - const clientNonce = 'random-client-nonce'; const creds: SASL.Credentials = { authzid: 'authorize-as', @@ -77,6 +66,16 @@ test('SASL - DIGEST-MD5 with authzid', () => { serviceType: 'xmpp', username: 'user' }; + const mech = factory.createMechanism(['DIGEST-MD5'], creds)!; + + expect(mech.name).toBe('DIGEST-MD5'); + expect(mech.providesMutualAuthentication).toBe(false); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce', 'realm'], + required: [['host', 'password', 'serviceName', 'serviceType', 'username']] + }); const response1 = mech.createResponse(creds)!; expect(response1).toBeNull(); @@ -109,17 +108,6 @@ test('SASL - DIGEST-MD5 with different service name and host', () => { const factory = new SASL.Factory(); factory.register('DIGEST-MD5', SASL.DIGEST, 10); - const mech = factory.createMechanism(['DIGEST-MD5'])!; - - expect(mech.name).toBe('DIGEST-MD5'); - expect(mech.providesMutualAuthentication).toBe(false); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce', 'realm'], - required: ['host', 'password', 'serviceName', 'serviceType', 'username'] - }); - const clientNonce = 'random-client-nonce'; const creds: SASL.Credentials = { authzid: 'authorize-as', @@ -130,6 +118,16 @@ test('SASL - DIGEST-MD5 with different service name and host', () => { serviceType: 'xmpp', username: 'user' }; + const mech = factory.createMechanism(['DIGEST-MD5'], creds)!; + + expect(mech.name).toBe('DIGEST-MD5'); + expect(mech.providesMutualAuthentication).toBe(false); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce', 'realm'], + required: [['host', 'password', 'serviceName', 'serviceType', 'username']] + }); const response1 = mech.createResponse(creds)!; expect(response1).toBeNull(); diff --git a/test/sasl/external.ts b/test/sasl/external.ts index f43de946..9a95f4af 100644 --- a/test/sasl/external.ts +++ b/test/sasl/external.ts @@ -4,7 +4,8 @@ test('SASL - EXTERNAL', () => { const factory = new SASL.Factory(); factory.register('EXTERNAL', SASL.EXTERNAL, 10); - const mech = factory.createMechanism(['EXTERNAL'])!; + const creds: SASL.Credentials = {}; + const mech = factory.createMechanism(['EXTERNAL'], creds)!; expect(mech.name).toBe('EXTERNAL'); expect(mech.providesMutualAuthentication).toBeFalsy(); @@ -12,11 +13,9 @@ test('SASL - EXTERNAL', () => { const neededCreds = mech.getExpectedCredentials(); expect(neededCreds).toStrictEqual({ optional: ['authzid'], - required: [] + required: [[]] }); - const creds: SASL.Credentials = {}; - const response = mech.createResponse(creds)!; expect(response.toString('base64')).toBe(''); @@ -36,7 +35,7 @@ test('SASL - EXTERNAL with authzid', () => { const factory = new SASL.Factory(); factory.register('EXTERNAL', SASL.EXTERNAL, 10); - const mech = factory.createMechanism(['EXTERNAL'])!; + const mech = factory.createMechanism(['EXTERNAL'], {})!; expect(mech.name).toBe('EXTERNAL'); expect(mech.providesMutualAuthentication).toBeFalsy(); @@ -44,7 +43,7 @@ test('SASL - EXTERNAL with authzid', () => { const neededCreds = mech.getExpectedCredentials(); expect(neededCreds).toStrictEqual({ optional: ['authzid'], - required: [] + required: [[]] }); const creds: SASL.Credentials = { diff --git a/test/sasl/factory.ts b/test/sasl/factory.ts index 600affda..9c308001 100644 --- a/test/sasl/factory.ts +++ b/test/sasl/factory.ts @@ -8,7 +8,7 @@ test('SASL - Factory.register should sort by priority', () => { factory.register('SCRAM-SHA-256-PLUS', SASL.SCRAM, 350); factory.register('SCRAM-SHA-256', SASL.SCRAM, 300); factory.register('SCRAM-SHA-1', SASL.SCRAM, 200); - factory.register('X-OAUTH2', SASL.PLAIN, 50); + factory.register('X-OAUTH2', SASL.X_OAUTH2, 50); factory.register('DIGEST-MD5', SASL.DIGEST, 100); factory.register('ANONYMOUS', SASL.ANONYMOUS, 0); factory.register('OAUTHBEARER', SASL.OAUTH, 100); diff --git a/test/sasl/mech-selection.ts b/test/sasl/mech-selection.ts index 7ef342a6..f4fa9fb9 100644 --- a/test/sasl/mech-selection.ts +++ b/test/sasl/mech-selection.ts @@ -6,7 +6,7 @@ test('Mech selection', () => { factory.register('PLAIN', SASL.PLAIN, 100); factory.register('SUPER-PLAIN', SASL.PLAIN, 200); - const mech = factory.createMechanism(['SUPER-PLAIN'])!; + const mech = factory.createMechanism(['SUPER-PLAIN'], { username: "", password: "" })!; expect(mech.name).toBe('SUPER-PLAIN'); }); @@ -17,7 +17,7 @@ test('Mech selection', () => { factory.register('PLAIN', SASL.PLAIN, 100); factory.register('SUPER-PLAIN', SASL.PLAIN, 200); - const mech = factory.createMechanism(['SUPER-PLAIN', 'BASIC-PLAIN'])!; + const mech = factory.createMechanism(['SUPER-PLAIN', 'BASIC-PLAIN'], { username: "", password: "" })!; expect(mech.name).toBe('SUPER-PLAIN'); }); @@ -28,7 +28,7 @@ test('Mech selection', () => { factory.register('PLAIN', SASL.PLAIN, 100); factory.register('SUPER-PLAIN', SASL.PLAIN, 200); - const mech = factory.createMechanism(['BASIC-PLAIN', 'SUPER-PLAIN'])!; + const mech = factory.createMechanism(['BASIC-PLAIN', 'SUPER-PLAIN'], { username: "", password: "" })!; expect(mech.name).toBe('SUPER-PLAIN'); }); @@ -39,7 +39,7 @@ test('Mech selection', () => { factory.register('PLAIN', SASL.PLAIN, 100); factory.register('SUPER-PLAIN', SASL.PLAIN, 200); - const mech = factory.createMechanism(['UNKNOWN', 'SUPER-PLAIN'])!; + const mech = factory.createMechanism(['UNKNOWN', 'SUPER-PLAIN'], { username: "", password: "" })!; expect(mech.name).toBe('SUPER-PLAIN'); }); @@ -50,7 +50,7 @@ test('Mech selection', () => { factory.register('PLAIN', SASL.PLAIN, 100); factory.register('SUPER-PLAIN', SASL.PLAIN, 200); - const mech = factory.createMechanism(['UNKNOWN'])!; + const mech = factory.createMechanism(['UNKNOWN'], { username: "", password: "" })!; expect(mech).toBeNull(); }); @@ -63,7 +63,7 @@ test('Mech disabled', () => { factory.disable('SUPER-PLAIN'); - const mech = factory.createMechanism(['BASIC-PLAIN', 'PLAIN', 'SUPER-PLAIN'])!; + const mech = factory.createMechanism(['BASIC-PLAIN', 'PLAIN', 'SUPER-PLAIN'], { username: "", password: "" })!; expect(mech.name).toBe('PLAIN'); }); diff --git a/test/sasl/oauthbearer.ts b/test/sasl/oauthbearer.ts index 88c34c26..fa98f56b 100644 --- a/test/sasl/oauthbearer.ts +++ b/test/sasl/oauthbearer.ts @@ -4,7 +4,10 @@ test('SASL - OAUTHBEARER', () => { const factory = new SASL.Factory(); factory.register('OAUTHBEARER', SASL.OAUTH, 10); - const mech = factory.createMechanism(['OAUTHBEARER'])!; + const creds: SASL.Credentials = { + token: 'bearer-token' + }; + const mech = factory.createMechanism(['OAUTHBEARER'], creds)!; expect(mech.name).toBe('OAUTHBEARER'); expect(mech.providesMutualAuthentication).toBeFalsy(); @@ -12,13 +15,9 @@ test('SASL - OAUTHBEARER', () => { const neededCreds = mech.getExpectedCredentials(); expect(neededCreds).toStrictEqual({ optional: ['authzid'], - required: ['token'] + required: [['token']] }); - const creds: SASL.Credentials = { - token: 'bearer-token' - }; - const response = mech.createResponse(creds)!; expect(response.toString('utf8')).toBe('n,,\u0001auth=Bearer bearer-token\u0001\u0001'); @@ -38,7 +37,10 @@ test('SASL - OAUTHBEARER, failed', () => { const factory = new SASL.Factory(); factory.register('OAUTHBEARER', SASL.OAUTH, 10); - const mech = factory.createMechanism(['OAUTHBEARER'])!; + const creds: SASL.Credentials = { + token: 'bearer-token' + }; + const mech = factory.createMechanism(['OAUTHBEARER'], creds)!; expect(mech.name).toBe('OAUTHBEARER'); expect(mech.providesMutualAuthentication).toBeFalsy(); @@ -46,13 +48,9 @@ test('SASL - OAUTHBEARER, failed', () => { const neededCreds = mech.getExpectedCredentials(); expect(neededCreds).toStrictEqual({ optional: ['authzid'], - required: ['token'] + required: [['token']] }); - const creds: SASL.Credentials = { - token: 'bearer-token' - }; - const response = mech.createResponse(creds)!; expect(response.toString('utf8')).toBe('n,,\u0001auth=Bearer bearer-token\u0001\u0001'); diff --git a/test/sasl/plain.ts b/test/sasl/plain.ts index b8d2b764..f36e10b3 100644 --- a/test/sasl/plain.ts +++ b/test/sasl/plain.ts @@ -4,22 +4,21 @@ test('SASL - PLAIN', () => { const factory = new SASL.Factory(); factory.register('PLAIN', SASL.PLAIN, 10); - const mech = factory.createMechanism(['PLAIN'])!; + const creds: SASL.Credentials = { + username: 'user', + password: 'hunter2' + }; + const mech = factory.createMechanism(['PLAIN'], creds)!; expect(mech.name).toBe('PLAIN'); expect(mech.providesMutualAuthentication).toBeFalsy(); const neededCreds = mech.getExpectedCredentials(); expect(neededCreds).toStrictEqual({ - required: ['username', 'password'], - optional: ['authzid'] + optional: ['authzid'], + required: [['username', 'password']] }); - const creds: SASL.Credentials = { - username: 'user', - password: 'hunter2' - }; - const response = mech.createResponse(creds)!; expect(response.toString('utf8')).toBe('\x00user\x00hunter2'); @@ -39,23 +38,22 @@ test('SASL - PLAIN with authzid', () => { const factory = new SASL.Factory(); factory.register('PLAIN', SASL.PLAIN, 10); - const mech = factory.createMechanism(['PLAIN'])!; + const creds: SASL.Credentials = { + authzid: 'authorize-as@domain', + username: 'user', + password: 'hunter2' + }; + const mech = factory.createMechanism(['PLAIN'], creds)!; expect(mech.name).toBe('PLAIN'); expect(mech.providesMutualAuthentication).toBeFalsy(); const neededCreds = mech.getExpectedCredentials(); expect(neededCreds).toStrictEqual({ - required: ['username', 'password'], - optional: ['authzid'] + optional: ['authzid'], + required: [['username', 'password']] }); - const creds: SASL.Credentials = { - authzid: 'authorize-as@domain', - username: 'user', - password: 'hunter2' - }; - const response = mech.createResponse(creds)!; expect(response.toString('utf8')).toBe('authorize-as@domain\x00user\x00hunter2'); diff --git a/test/sasl/scram.ts b/test/sasl/scram.ts index 5b21d7b5..a44e2eae 100644 --- a/test/sasl/scram.ts +++ b/test/sasl/scram.ts @@ -5,17 +5,6 @@ test('SASL - SCRAM-SHA-1', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1'])!; - - expect(mech.name).toBe('SCRAM-SHA-1'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -25,6 +14,16 @@ test('SASL - SCRAM-SHA-1', () => { password: 'hunter2', clientNonce: clientNonce }; + const mech = factory.createMechanism(['SCRAM-SHA-1'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const saltedPassword = SASL.Hi( Buffer.from(saslprep(creds.password || '')), @@ -76,17 +75,6 @@ test('SASL - SCRAM-SHA-1 (with tlsUnique available)', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1'])!; - - expect(mech.name).toBe('SCRAM-SHA-1'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -97,6 +85,16 @@ test('SASL - SCRAM-SHA-1 (with tlsUnique available)', () => { clientNonce: clientNonce, tlsUnique: Buffer.from('tls-unique-data') }; + const mech = factory.createMechanism(['SCRAM-SHA-1'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const saltedPassword = SASL.Hi( Buffer.from(saslprep(creds.password || '')), @@ -149,17 +147,6 @@ test('SASL - SCRAM-SHA-1-PLUS', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1-PLUS', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1-PLUS'])!; - - expect(mech.name).toBe('SCRAM-SHA-1-PLUS'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password', 'tlsUnique'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -170,6 +157,16 @@ test('SASL - SCRAM-SHA-1-PLUS', () => { clientNonce: clientNonce, tlsUnique: Buffer.from('tls-unique-data') }; + const mech = factory.createMechanism(['SCRAM-SHA-1-PLUS'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1-PLUS'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password', 'tlsUnique'], ['username', 'saltedPassword', 'tlsUnique'], ['clientKey', 'serverKey', 'tlsUnique']] + }); const saltedPassword = SASL.Hi( Buffer.from(saslprep(creds.password || '')), @@ -222,17 +219,6 @@ test('SASL - SCRAM-SHA-1, with escaped username', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1'])!; - - expect(mech.name).toBe('SCRAM-SHA-1'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -242,6 +228,16 @@ test('SASL - SCRAM-SHA-1, with escaped username', () => { password: 'hunter2', clientNonce: clientNonce }; + const mech = factory.createMechanism(['SCRAM-SHA-1'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const saltedPassword = SASL.Hi( Buffer.from(saslprep(creds.password || '')), @@ -293,21 +289,10 @@ test('SASL - SCRAM-SHA-1 with salted password', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1'])!; - - expect(mech.name).toBe('SCRAM-SHA-1'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); - + const saltedPassword = SASL.Hi( Buffer.from(saslprep('hunter2')), Buffer.from(salt, 'base64'), @@ -326,6 +311,16 @@ test('SASL - SCRAM-SHA-1 with salted password', () => { salt: Buffer.from(salt, 'base64'), saltedPassword }; + const mech = factory.createMechanism(['SCRAM-SHA-1'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const response1 = mech.createResponse(creds)!; expect(response1.toString('utf8')).toBe(`n,,n=user,r=${clientNonce}`); @@ -364,17 +359,6 @@ test('SASL - SCRAM-SHA-1 with client and server keys', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1'])!; - - expect(mech.name).toBe('SCRAM-SHA-1'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -398,6 +382,16 @@ test('SASL - SCRAM-SHA-1 with client and server keys', () => { salt: Buffer.from(salt, 'base64'), serverKey: Buffer.from(serverKey, 'base64') }; + const mech = factory.createMechanism(['SCRAM-SHA-1'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const response1 = mech.createResponse(creds)!; expect(response1.toString('utf8')).toBe(`n,,n=user,r=${clientNonce}`); @@ -433,17 +427,6 @@ test('SASL - SCRAM-SHA-1 with authzid', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1'])!; - - expect(mech.name).toBe('SCRAM-SHA-1'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -454,6 +437,16 @@ test('SASL - SCRAM-SHA-1 with authzid', () => { password: 'hunter2', clientNonce: clientNonce }; + const mech = factory.createMechanism(['SCRAM-SHA-1'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const saltedPassword = SASL.Hi( Buffer.from(saslprep(creds.password || '')), @@ -506,17 +499,6 @@ test('SASL - SCRAM-SHA-256', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-256', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-256'])!; - - expect(mech.name).toBe('SCRAM-SHA-256'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -526,6 +508,16 @@ test('SASL - SCRAM-SHA-256', () => { password: 'hunter2', clientNonce: clientNonce }; + const mech = factory.createMechanism(['SCRAM-SHA-256'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-256'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const saltedPassword = SASL.Hi( Buffer.from(saslprep(creds.password || '')), @@ -578,17 +570,6 @@ test('SASL - SCRAM-SHA-1 server error', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1'])!; - - expect(mech.name).toBe('SCRAM-SHA-1'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -598,6 +579,16 @@ test('SASL - SCRAM-SHA-1 server error', () => { password: 'hunter2', clientNonce: clientNonce }; + const mech = factory.createMechanism(['SCRAM-SHA-1'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const response1 = mech.createResponse(creds)!; expect(response1.toString('utf8')).toBe(`n,,n=user,r=${clientNonce}`); @@ -623,17 +614,6 @@ test('SASL - SCRAM-SHA-1 invalid signature', () => { const factory = new SASL.Factory(); factory.register('SCRAM-SHA-1', SASL.SCRAM, 10); - const mech = factory.createMechanism(['SCRAM-SHA-1'])!; - - expect(mech.name).toBe('SCRAM-SHA-1'); - expect(mech.providesMutualAuthentication).toBe(true); - - const neededCreds = mech.getExpectedCredentials(); - expect(neededCreds).toStrictEqual({ - optional: ['authzid', 'clientNonce'], - required: ['username', 'password'] - }); - const salt = Buffer.from('salt').toString('base64'); const serverNonce = Buffer.from('client-random-noncerandom-server-nonce').toString('base64'); const clientNonce = Buffer.from('random-client-nonce').toString('base64'); @@ -643,6 +623,16 @@ test('SASL - SCRAM-SHA-1 invalid signature', () => { password: 'hunter2', clientNonce: clientNonce }; + const mech = factory.createMechanism(['SCRAM-SHA-1'], creds)!; + + expect(mech.name).toBe('SCRAM-SHA-1'); + expect(mech.providesMutualAuthentication).toBe(true); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid', 'clientNonce'], + required: [['username', 'password'], ['username', 'saltedPassword'], ['clientKey', 'serverKey']] + }); const response1 = mech.createResponse(creds)!; expect(response1.toString('utf8')).toBe(`n,,n=user,r=${clientNonce}`); diff --git a/test/sasl/x-oauth2.ts b/test/sasl/x-oauth2.ts new file mode 100644 index 00000000..7b894158 --- /dev/null +++ b/test/sasl/x-oauth2.ts @@ -0,0 +1,70 @@ +import * as SASL from '../../src/lib/sasl'; + +test('SASL - X-OAUTH2', () => { + const factory = new SASL.Factory(); + factory.register('X-OAUTH2', SASL.X_OAUTH2, 50); + + const creds = { + username: 'user', + token: 'oauth-token' + }; + const mech = factory.createMechanism(['X-OAUTH2'], creds)!; + + expect(mech.name).toBe('X-OAUTH2'); + expect(mech.providesMutualAuthentication).toBeFalsy(); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid'], + required: [['username', 'token']] + }); + + const response = mech.createResponse(creds)!; + expect(response.toString("utf8")).toBe('\x00user\x00oauth-token'); + + mech.processSuccess(); + + const result = mech.finalize(creds); + expect(result).toStrictEqual({ + authenticated: true, + mutuallyAuthenticated: false + }); + + const cacheable = mech.getCacheableCredentials()!; + expect(cacheable).toBeNull(); +}); + +test('SASL - X-OAUTH2 with authzid', () => { + const factory = new SASL.Factory(); + factory.register('X-OAUTH2', SASL.X_OAUTH2, 50); + + const creds = { + authzid: 'authorize-as@domain', + username: 'user', + token: 'oauth-token' + }; + const mech = factory.createMechanism(['X-OAUTH2'], creds)!; + + expect(mech.name).toBe('X-OAUTH2'); + expect(mech.providesMutualAuthentication).toBeFalsy(); + + const neededCreds = mech.getExpectedCredentials(); + expect(neededCreds).toStrictEqual({ + optional: ['authzid'], + required: [['username', 'token']] + }); + + const response = mech.createResponse(creds)!; + expect(response.toString("utf8")).toBe('authorize-as@domain\x00user\x00oauth-token'); + + mech.processSuccess(); + + const result = mech.finalize(creds); + expect(result).toStrictEqual({ + authenticated: true, + mutuallyAuthenticated: false + }); + + const cacheable = mech.getCacheableCredentials()!; + expect(cacheable).toBeNull(); +}); \ No newline at end of file