Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check SASL mechanism credential requirements #489

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
2 changes: 1 addition & 1 deletion src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
48 changes: 38 additions & 10 deletions src/lib/sasl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface CacheableCredentials extends Credentials {
}

export interface ExpectedCredentials {
required: string[];
required: string[][];
optional: string[];
}

Expand Down Expand Up @@ -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;
}
}
}
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -224,7 +229,7 @@ export class PLAIN extends SimpleMech implements Mechanism {
public getExpectedCredentials(): ExpectedCredentials {
return {
optional: ['authzid'],
required: ['username', 'password']
required: [['username', 'password']]
};
}

Expand All @@ -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
// ====================================================================
Expand All @@ -254,7 +282,7 @@ export class OAUTH extends SimpleMech implements Mechanism {
public getExpectedCredentials(): ExpectedCredentials {
return {
optional: ['authzid'],
required: ['token']
required: [['token']]
};
}

Expand Down Expand Up @@ -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']]
};
}

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/sasl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 5 additions & 6 deletions test/sasl/anonymous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ 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();

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('');

Expand All @@ -33,15 +32,15 @@ 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();

const neededCreds = mech.getExpectedCredentials();
expect(neededCreds).toStrictEqual({
optional: ['trace'],
required: []
required: [[]]
});

const creds: SASL.Credentials = {
Expand Down
64 changes: 31 additions & 33 deletions test/sasl/digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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',
Expand All @@ -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();
Expand Down Expand Up @@ -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',
Expand All @@ -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();
Expand Down
11 changes: 5 additions & 6 deletions test/sasl/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ 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();

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('');

Expand All @@ -36,15 +35,15 @@ 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();

const neededCreds = mech.getExpectedCredentials();
expect(neededCreds).toStrictEqual({
optional: ['authzid'],
required: []
required: [[]]
});

const creds: SASL.Credentials = {
Expand Down
2 changes: 1 addition & 1 deletion test/sasl/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 6 additions & 6 deletions test/sasl/mech-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand All @@ -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');
});
Expand All @@ -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');
});
Expand All @@ -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');
});
Expand All @@ -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();
});
Expand All @@ -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');
});
Loading