Add support for (X)OAuth2 IMAP authentication.

This commit is contained in:
Michael James Gratton 2018-05-29 06:12:36 +02:00
parent ada7f3fdbb
commit 865f23beba
8 changed files with 237 additions and 10 deletions

View file

@ -294,6 +294,7 @@ src/engine/imap/message/imap-tag.vala
src/engine/imap/message/imap-uid.vala
src/engine/imap/message/imap-uid-validity.vala
src/engine/imap/parameter/imap-atom-parameter.vala
src/engine/imap/parameter/imap-continuation-parameter.vala
src/engine/imap/parameter/imap-list-parameter.vala
src/engine/imap/parameter/imap-literal-parameter.vala
src/engine/imap/parameter/imap-nil-parameter.vala

View file

@ -96,6 +96,7 @@ engine/imap/api/imap-folder-root.vala
engine/imap/api/imap-folder-session.vala
engine/imap/api/imap-session-object.vala
engine/imap/command/imap-append-command.vala
engine/imap/command/imap-authenticate-command.vala
engine/imap/command/imap-capability-command.vala
engine/imap/command/imap-close-command.vala
engine/imap/command/imap-command.vala
@ -140,6 +141,7 @@ engine/imap/message/imap-tag.vala
engine/imap/message/imap-uid.vala
engine/imap/message/imap-uid-validity.vala
engine/imap/parameter/imap-atom-parameter.vala
engine/imap/parameter/imap-continuation-parameter.vala
engine/imap/parameter/imap-list-parameter.vala
engine/imap/parameter/imap-literal-parameter.vala
engine/imap/parameter/imap-nil-parameter.vala

View file

@ -0,0 +1,58 @@
/*
* Copyright 2018 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* An IMAP AUTHENTICATE command.
*
* See [[http://tools.ietf.org/html/rfc3501#section-6.2.2]]
*/
public class Geary.Imap.AuthenticateCommand : Command {
public const string NAME = "authenticate";
private const string OAUTH2_METHOD = "xoauth2";
private const string OAUTH2_RESP = "user=%s\001auth=Bearer %s\001\001";
public string method { get; private set; }
private AuthenticateCommand(string method, string data) {
base(NAME, { method, data });
this.method = method;
}
public AuthenticateCommand.oauth2(string user, string token) {
string encoded_token = Base64.encode(
OAUTH2_RESP.printf(user, token).data
);
this(OAUTH2_METHOD, encoded_token);
}
public ContinuationParameter
continuation_requested(ContinuationResponse response)
throws ImapError {
if (this.method != AuthenticateCommand.OAUTH2_METHOD) {
throw new ImapError.INVALID("Unexpected continuation request");
}
// Continuation will be a Base64 encoded JSON blob and which
// indicates a login failure. We don't really care about that
// (do we?) though since once we acknowledge it with a
// zero-length response the server will respond with an IMAP
// error.
return new ContinuationParameter(new uint8[0]);
}
public override string to_string() {
return "%s %s %s <token>".printf(
tag.to_string(), this.name, this.method
);
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2018 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Represents a response to an IMAP continuation request.
*
* Do not use this if you need to send literal data as part of a
* command, add it as a {@link LiteralParameter} to the command
* instead.
*
* See [[http://tools.ietf.org/html/rfc3501#section-7.5]]
*/
public class Geary.Imap.ContinuationParameter : Geary.Imap.Parameter {
private uint8[] data;
/**
* Response to the continuation request.
*
* The given data will be sent to the server as-is. It should not
* contain a trailing EOL.
*/
public ContinuationParameter(uint8[] data) {
this.data = data;
}
public void serialize_continuation(Serializer ser)
throws GLib.Error {
ser.push_unquoted_string(
new Memory.ByteBuffer.take(this.data, this.data.length).to_string()
);
ser.push_eol();
}
/** {@inheritDoc} */
public override void serialize(Serializer ser, Tag tag)
throws GLib.Error {
serialize_continuation(ser);
}
/** {@inheritDoc} */
public override string to_string() {
return new Memory.ByteBuffer.take(this.data, this.data.length).to_string();
}
}

View file

@ -7,6 +7,8 @@
public class Geary.Imap.Capabilities : Geary.GenericCapabilities {
public const string AUTH = "AUTH";
public const string AUTH_XOAUTH2 = "XOAUTH2";
public const string CREATE_SPECIAL_USE = "CREATE-SPECIAL-USE";
public const string COMPRESS = "COMPRESS";
public const string DEFLATE_SETTING = "DEFLATE";

View file

@ -632,7 +632,48 @@ public class Geary.Imap.ClientConnection : BaseObject {
// by this signal, will want to tighten this up a bit in the future
sent_command(cmd);
}
/**
* Sends a reply to an unsolicited continuation request.
*
* Do not use this if you need to send literal data as part of a
* command, add it as a {@link LiteralParameter} to the command
* instead.
*/
public async void send_continuation_reply(ContinuationParameter reply,
Cancellable? cancellable = null)
throws Error {
check_for_connection();
// need to run this in critical section because Serializer requires it (don't want to be
// pushing data while a flush_async() is occurring)
int token = yield send_mutex.claim_async(cancellable);
Error? ser_err = null;
try {
// watch for disconnect while waiting for mutex
if (ser != null) {
reply.serialize_continuation(ser);
} else {
ser_err = new ImapError.NOT_CONNECTED("Send not allowed: connection in %s state",
fsm.get_state_string(fsm.get_state()));
}
} catch (Error err) {
debug("[%s] Error serializing command: %s", to_string(), err.message);
ser_err = err;
}
this.send_mutex.release(ref token);
if (ser_err != null) {
send_failure(ser_err);
throw ser_err;
}
// Reset flush timer so it only fires after n msec after last command pushed out to stream
reschedule_flush_timeout();
}
private void reschedule_flush_timeout() {
unschedule_flush_timeout();
@ -646,7 +687,7 @@ public class Geary.Imap.ClientConnection : BaseObject {
flush_timeout_id = 0;
}
}
private bool on_flush_timeout() {
do_flush_async.begin();

View file

@ -196,13 +196,14 @@ public class Geary.Imap.ClientSession : BaseObject {
CLOSE_MAILBOX,
LOGOUT,
DISCONNECT,
// server events
CONNECTED,
DISCONNECTED,
RECV_STATUS,
RECV_COMPLETION,
RECV_CONTINUATION,
// I/O errors
RECV_ERROR,
SEND_ERROR,
@ -393,6 +394,7 @@ public class Geary.Imap.ClientSession : BaseObject {
new Geary.State.Mapping(State.AUTHORIZING, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_STATUS, on_recv_status),
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_COMPLETION, on_login_recv_completion),
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_CONTINUATION, on_login_recv_continuation),
new Geary.State.Mapping(State.AUTHORIZING, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_ERROR, on_recv_error),
@ -745,6 +747,7 @@ public class Geary.Imap.ClientSession : BaseObject {
cx.send_failure.connect(on_network_send_error);
cx.received_status_response.connect(on_received_status_response);
cx.received_server_data.connect(on_received_server_data);
cx.received_continuation_response.connect(on_received_continuation_response);
cx.received_bytes.connect(on_received_bytes);
cx.received_bad_response.connect(on_received_bad_response);
cx.recv_closed.connect(on_received_closed);
@ -771,6 +774,7 @@ public class Geary.Imap.ClientSession : BaseObject {
cx.send_failure.disconnect(on_network_send_error);
cx.received_status_response.disconnect(on_received_status_response);
cx.received_server_data.disconnect(on_received_server_data);
cx.received_continuation_response.disconnect(on_received_continuation_response);
cx.received_bytes.disconnect(on_received_bytes);
cx.received_bad_response.disconnect(on_received_bad_response);
cx.recv_closed.disconnect(on_received_closed);
@ -855,7 +859,33 @@ public class Geary.Imap.ClientSession : BaseObject {
throw new ImapError.UNAUTHENTICATED("No credentials provided for account: %s", credentials.to_string());
}
LoginCommand cmd = new LoginCommand(credentials.user, credentials.token);
Command? cmd = null;
switch (credentials.supported_method) {
case Geary.Credentials.Method.PASSWORD:
cmd = new LoginCommand(
credentials.user, credentials.token
);
break;
case Geary.Credentials.Method.OAUTH2:
if (!capabilities.has_setting(Capabilities.AUTH,
Capabilities.AUTH_XOAUTH2)) {
throw new ImapError.UNAUTHENTICATED(
"OAuth2 authentication not supported for %s", to_string()
);
}
cmd = new AuthenticateCommand.oauth2(
credentials.user, credentials.token
);
break;
default:
throw new ImapError.UNAUTHENTICATED(
"Credentials method %s not supported for: %s",
credentials.supported_method.to_string(),
to_string()
);
}
MachineParams params = new MachineParams(cmd);
fsm.issue(Event.LOGIN, null, params);
@ -1038,14 +1068,13 @@ public class Geary.Imap.ClientSession : BaseObject {
private uint on_login(uint state, uint event, void *user, Object? object) {
MachineParams params = (MachineParams) object;
assert(params.cmd is LoginCommand);
if (!reserve_state_change_cmd(params, state, event))
return state;
return State.AUTHORIZING;
}
private uint on_logging_in(uint state, uint event, void *user, Object? object) {
MachineParams params = (MachineParams) object;
@ -1074,7 +1103,36 @@ public class Geary.Imap.ClientSession : BaseObject {
return State.NOAUTH;
}
}
private uint on_login_recv_continuation(uint state,
uint event,
void *user,
Object? object) {
ContinuationResponse response = (ContinuationResponse) object;
AuthenticateCommand auth = this.state_change_cmd as AuthenticateCommand;
if (auth != null) {
ContinuationParameter? reply = null;
try {
reply = auth.continuation_requested(response);
} catch (ImapError err) {
debug("[%s] Error handling login continuation request: %s",
to_string(), err.message);
}
if (reply != null) {
// We have to handle the continuation request anyway,
// so just send an empty one.
reply = new ContinuationParameter(new uint8[0]);
}
// XXX Not calling yield here is a nasty hack? Need to get
// a cancellable to this somehow, too.
this.cx.send_continuation_reply.begin(reply, null);
}
return State.AUTHORIZING;
}
//
// keepalives (nop idling to keep the session alive and to periodically receive notifications
// of changes)
@ -1284,6 +1342,7 @@ public class Geary.Imap.ClientSession : BaseObject {
//
// TODO: Convert commands into proper calls to avoid throwing an exception
if (cmd.has_name(LoginCommand.NAME)
|| cmd.has_name(AuthenticateCommand.NAME)
|| cmd.has_name(LogoutCommand.NAME)
|| cmd.has_name(SelectCommand.NAME)
|| cmd.has_name(ExamineCommand.NAME)
@ -1873,6 +1932,15 @@ public class Geary.Imap.ClientSession : BaseObject {
}
}
private void on_received_continuation_response(ContinuationResponse response) {
this.last_seen = GLib.get_real_time();
// reschedule keepalive (traffic seen on channel)
schedule_keepalive();
fsm.issue(Event.RECV_CONTINUATION, null, response, null);
}
private void on_received_bytes(size_t bytes) {
this.last_seen = GLib.get_real_time();

View file

@ -92,6 +92,7 @@ geary_engine_vala_sources = files(
'imap/api/imap-folder-session.vala',
'imap/api/imap-session-object.vala',
'imap/command/imap-append-command.vala',
'imap/command/imap-authenticate-command.vala',
'imap/command/imap-capability-command.vala',
'imap/command/imap-close-command.vala',
'imap/command/imap-command.vala',
@ -136,6 +137,7 @@ geary_engine_vala_sources = files(
'imap/message/imap-uid.vala',
'imap/message/imap-uid-validity.vala',
'imap/parameter/imap-atom-parameter.vala',
'imap/parameter/imap-continuation-parameter.vala',
'imap/parameter/imap-list-parameter.vala',
'imap/parameter/imap-literal-parameter.vala',
'imap/parameter/imap-nil-parameter.vala',