Skip to content

Commit

Permalink
fix(auth): Prefer AUTH=LOGIN and AUTH=PLAIN to LOGIN
Browse files Browse the repository at this point in the history
  • Loading branch information
andris9 committed Nov 7, 2024
1 parent f6e7eb6 commit 3efd98a
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 54 deletions.
197 changes: 145 additions & 52 deletions lib/commands/authenticate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,161 @@

const { getStatusCode, getErrorText } = require('../tools.js');

// Authenticates user using LOGIN
module.exports = async (connection, username, accessToken) => {
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
// nothing to do here
return;
async function authOauth(connection, username, accessToken) {
let oauthbearer;
let command;
let breaker;

if (connection.capabilities.has('AUTH=OAUTHBEARER')) {
oauthbearer = [`n,a=${username},`, `host=${connection.servername}`, `port=993`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
command = 'OAUTHBEARER';
breaker = 'AQ==';
} else if (connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
oauthbearer = [`user=${username}`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
command = 'XOAUTH2';
breaker = '';
}

// AUTH=OAUTHBEARER and AUTH=XOAUTH in the context of OAuth2 or very similar so we can handle these together
if (connection.capabilities.has('AUTH=OAUTHBEARER') || connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
let oauthbearer;
let command;
let breaker;

if (connection.capabilities.has('AUTH=OAUTHBEARER')) {
oauthbearer = [`n,a=${username},`, `host=${connection.servername}`, `port=993`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
command = 'OAUTHBEARER';
breaker = 'AQ==';
} else if (connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
oauthbearer = [`user=${username}`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
command = 'XOAUTH2';
breaker = '';
let errorResponse = false;
try {
let response = await connection.exec(
'AUTHENTICATE',
[
{ type: 'ATOM', value: command },
{ type: 'ATOM', value: Buffer.from(oauthbearer).toString('base64'), sensitive: true }
],
{
onPlusTag: async resp => {
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
try {
errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
} catch (err) {
connection.log.debug({ errorResponse: resp.attributes[0].value, err });
}
}

connection.log.debug({ src: 'c', msg: breaker, comment: `Error response for ${command}` });
connection.write(breaker);
}
}
);
response.next();

connection.authCapabilities.set(`AUTH=${command}`, true);

return username;
} catch (err) {
let errorCode = getStatusCode(err.response);
if (errorCode) {
err.serverResponseCode = errorCode;
}
err.authenticationFailed = true;
err.response = await getErrorText(err.response);
if (errorResponse) {
err.oauthError = errorResponse;
}
throw err;
}
}

let errorResponse = false;
try {
let response = await connection.exec(
'AUTHENTICATE',
[
{ type: 'ATOM', value: command },
{ type: 'ATOM', value: Buffer.from(oauthbearer).toString('base64'), sensitive: true }
],
{
onPlusTag: async resp => {
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
try {
errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
} catch (err) {
connection.log.debug({ errorResponse: resp.attributes[0].value, err });
}
async function authLogin(connection, username, password) {
let errorResponse = false;
try {
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'LOGIN' }], {
onPlusTag: async resp => {
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
let question = Buffer.from(resp.attributes[0].value, 'base64').toString();
switch (question.toLowerCase().replace(/:$/, '')) {
case 'username': {
let encodedUsername = Buffer.from(username).toString('base64');
connection.log.debug({ src: 'c', msg: encodedUsername, comment: `Encoded username for AUTH=LOGIN` });
connection.write(encodedUsername);
break;
}
case 'password':
connection.log.debug({ src: 'c', msg: '(* value hidden *)', comment: `Encoded password for AUTH=LOGIN` });
connection.write(Buffer.from(password).toString('base64'));
break;
default: {
let error = new Error(`Unknown LOGIN question "${question}"`);
throw error;
}

connection.log.debug({ src: 'c', msg: breaker, comment: `Error response for ${command}` });
connection.write(breaker);
}
}
);
response.next();
}
});

connection.authCapabilities.set(`AUTH=${command}`, true);
response.next();

return username;
} catch (err) {
let errorCode = getStatusCode(err.response);
if (errorCode) {
err.serverResponseCode = errorCode;
}
err.authenticationFailed = true;
err.response = await getErrorText(err.response);
if (errorResponse) {
err.oauthError = errorResponse;
connection.authCapabilities.set(`AUTH=LOGIN`, true);

return username;
} catch (err) {
let errorCode = getStatusCode(err.response);
if (errorCode) {
err.serverResponseCode = errorCode;
}
err.authenticationFailed = true;
err.response = await getErrorText(err.response);
if (errorResponse) {
err.oauthError = errorResponse;
}
throw err;
}
}

async function authPlain(connection, username, password) {
let errorResponse = false;
try {
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'PLAIN' }], {
onPlusTag: async () => {
let encodedResponse = Buffer.from(['', username, password].join('\x00')).toString('base64');
let loggedResponse = Buffer.from(['', username, '(* value hidden *)'].join('\x00')).toString('base64');
connection.log.debug({ src: 'c', msg: loggedResponse, comment: `Encoded response for AUTH=PLAIN` });
connection.write(encodedResponse);
}
throw err;
});

response.next();

connection.authCapabilities.set(`AUTH=PLAIN`, true);

return username;
} catch (err) {
let errorCode = getStatusCode(err.response);
if (errorCode) {
err.serverResponseCode = errorCode;
}
err.authenticationFailed = true;
err.response = await getErrorText(err.response);
if (errorResponse) {
err.oauthError = errorResponse;
}
throw err;
}
}

// Authenticates user using LOGIN
module.exports = async (connection, username, { accessToken, password }) => {
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
// nothing to do here
return;
}

if (accessToken) {
// AUTH=OAUTHBEARER and AUTH=XOAUTH in the context of OAuth2 or very similar so we can handle these together
if (connection.capabilities.has('AUTH=OAUTHBEARER') || connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
return await authOauth(connection, username, accessToken);
}
}

if (password) {
if (connection.capabilities.has('AUTH=LOGIN')) {
return await authLogin(connection, username, password);
}

if (connection.capabilities.has('AUTH=PLAIN')) {
return await authPlain(connection, username, password);
}
}

Expand Down
8 changes: 6 additions & 2 deletions lib/imap-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -931,9 +931,13 @@ class ImapFlow extends EventEmitter {
this.expectCapabilityUpdate = true;

if (this.options.auth.accessToken) {
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, this.options.auth.accessToken);
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { accessToken: this.options.auth.accessToken });
} else if (this.options.auth.pass) {
this.authenticated = await this.run('LOGIN', this.options.auth.user, this.options.auth.pass);
if (this.capabilities.has('AUTH=LOGIN') || this.capabilities.has('AUTH=PLAIN')) {
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { password: this.options.auth.pass });
} else {
this.authenticated = await this.run('LOGIN', this.options.auth.user, this.options.auth.pass);
}
}

if (this.authenticated) {
Expand Down

0 comments on commit 3efd98a

Please sign in to comment.