From 9217dbeea45830c1d1d3d8453b495b6792bc38ca Mon Sep 17 00:00:00 2001 From: Julian Pidancet Date: Wed, 28 Sep 2022 19:49:11 +0200 Subject: [PATCH] imap,smtp: add XOAUTH2 support Add XOAUTH2 authentication support for IMAP and SMTP. Although XOAUTH2 is now deprecated in favor of OAuthBearer, it is the only way to connect to Office365 since Basic Auth is now completely removed. Since XOAUTH2 is very similar to OAuthBearer and uses the same configuration parameters, this is basically a copy-paste of the existing OAuthBearer code. However, XOAUTH2 support was removed from go-sasl library, so this change reimports the code that was removed from go-sasl and offers it a new home in lib/xoauth2.go. Hopefully it shouldn't be too hard to maintain, being less than 50 SLOC. Link: https://github.com/emersion/go-sasl/commit/7bfe0ed36a21 Implements: https://todo.sr.ht/~rjarry/aerc/78 Signed-off-by: Julian Pidancet Tested-by: Inwit Acked-by: Tim Culverhouse --- CHANGELOG.md | 1 + commands/compose/send.go | 22 ++++++++++ doc/aerc-imap.5.scd | 6 ++- doc/aerc-smtp.5.scd | 6 ++- lib/xoauth2.go | 88 ++++++++++++++++++++++++++++++++++++++++ worker/imap/configure.go | 15 +++++++ worker/imap/connect.go | 5 +++ worker/imap/worker.go | 1 + 8 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 lib/xoauth2.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb0cb1..4602f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Override `:open` handler on a per-MIME-type basis in `aerc.conf`. - Specify opener as the first `:open` param instead of always using default handler (i.e. `:open gimp` to open attachment in GIMP). +- Restored XOAUTH2 support for IMAP and SMTP. ### Changed diff --git a/commands/compose/send.go b/commands/compose/send.go index ec9e06b..3786721 100644 --- a/commands/compose/send.go +++ b/commands/compose/send.go @@ -302,6 +302,28 @@ func newSaslClient(auth string, uri *url.URL) (sasl.Client, error) { Username: uri.User.Username(), Token: password, }) + case "xoauth2": + q := uri.Query() + oauth2 := &oauth2.Config{} + if q.Get("token_endpoint") != "" { + oauth2.ClientID = q.Get("client_id") + oauth2.ClientSecret = q.Get("client_secret") + oauth2.Scopes = []string{q.Get("scope")} + oauth2.Endpoint.TokenURL = q.Get("token_endpoint") + } + password, _ := uri.User.Password() + bearer := lib.Xoauth2{ + OAuth2: oauth2, + Enabled: true, + } + if bearer.OAuth2.Endpoint.TokenURL != "" { + token, err := bearer.ExchangeRefreshToken(password) + if err != nil { + return nil, err + } + password = token.AccessToken + } + saslClient = lib.NewXoauth2Client(uri.User.Username(), password) default: return nil, fmt.Errorf("Unsupported auth mechanism %s", auth) } diff --git a/doc/aerc-imap.5.scd b/doc/aerc-imap.5.scd index 39ac337..2a5f6c8 100644 --- a/doc/aerc-imap.5.scd +++ b/doc/aerc-imap.5.scd @@ -19,7 +19,7 @@ In accounts.conf (see *aerc-config*(5)), the following IMAP-specific options are available: *source* - imap[s][+insecure|+oauthbearer]://username[:password]@hostname[:port]?[:oauth2_params] + imap[s][+insecure|+oauthbearer|+xoauth2]://username[:password]@hostname[:port]?[:oauth2_params] Remember that all fields must be URL encoded. The "@" symbol, when URL encoded, is *%40*. @@ -53,6 +53,10 @@ available: Example: imaps+oauthbearer://...?token_endpoint=https://...&client_id= + *imaps+xoauth2://* + IMAP with TLS/SSL using XOAUTH2 Authentication. Parameters are + the same as OAUTHBEARER. + *source-cred-cmd* Specifies the command to run to get the password for the IMAP account. This command will be run using `sh -c [command]`. If a diff --git a/doc/aerc-smtp.5.scd b/doc/aerc-smtp.5.scd index 16ff605..ee2aa17 100644 --- a/doc/aerc-smtp.5.scd +++ b/doc/aerc-smtp.5.scd @@ -16,7 +16,7 @@ In accounts.conf (see *aerc-config*(5)), the following SMTP-specific options are available: *outgoing* - smtp[s][+plain|+login|+none|+oauthbearer]://username[:password]@hostname[:port]?[:oauth2_params] + smtp[s][+plain|+login|+none|+oauthbearer|+xoauth2]://username[:password]@hostname[:port]?[:oauth2_params] Remember that all fields must be URL encoded. The "@" symbol, when URL encoded, is *%40*. @@ -47,6 +47,10 @@ available: SMTP with TLS/SSL using OAUTHBEARER Authentication. See documentation in *aerc-imap*(5) for usage. + *+xoauth2*: + SMTP with TLS/SSL using XOAUTH2 Authentication. See documentation in + *aerc-imap*(5) for usage. + *outgoing-cred-cmd* Specifies the command to run to get the password for the SMTP account. This command will be run using `sh -c [command]`. If a diff --git a/lib/xoauth2.go b/lib/xoauth2.go new file mode 100644 index 0000000..83f0630 --- /dev/null +++ b/lib/xoauth2.go @@ -0,0 +1,88 @@ +// +// This code is derived from the go-sasl library. +// +// Copyright (c) 2016 emersion +// Copyright (c) 2022, Oracle and/or its affiliates. +// +// SPDX-License-Identifier: MIT + +package lib + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/emersion/go-imap/client" + "github.com/emersion/go-sasl" + "golang.org/x/oauth2" +) + +// An XOAUTH2 error. +type Xoauth2Error struct { + Status string `json:"status"` + Schemes string `json:"schemes"` + Scope string `json:"scope"` +} + +// Implements error. +func (err *Xoauth2Error) Error() string { + return fmt.Sprintf("XOAUTH2 authentication error (%v)", err.Status) +} + +type xoauth2Client struct { + Username string + Token string +} + +func (a *xoauth2Client) Start() (mech string, ir []byte, err error) { + mech = "XOAUTH2" + ir = []byte("user=" + a.Username + "\x01auth=Bearer " + a.Token + "\x01\x01") + return +} + +func (a *xoauth2Client) Next(challenge []byte) ([]byte, error) { + // Server sent an error response + xoauth2Err := &Xoauth2Error{} + if err := json.Unmarshal(challenge, xoauth2Err); err != nil { + return nil, err + } else { + return nil, xoauth2Err + } +} + +// An implementation of the XOAUTH2 authentication mechanism, as +// described in https://developers.google.com/gmail/xoauth2_protocol. +func NewXoauth2Client(username, token string) sasl.Client { + return &xoauth2Client{username, token} +} + +type Xoauth2 struct { + OAuth2 *oauth2.Config + Enabled bool +} + +func (c *Xoauth2) ExchangeRefreshToken(refreshToken string) (*oauth2.Token, error) { + token := new(oauth2.Token) + token.RefreshToken = refreshToken + token.TokenType = "Bearer" + return c.OAuth2.TokenSource(context.TODO(), token).Token() +} + +func (c *Xoauth2) Authenticate(username string, password string, client *client.Client) error { + if ok, err := client.SupportAuth("XOAUTH2"); err != nil || !ok { + return fmt.Errorf("Xoauth2 not supported %w", err) + } + + if c.OAuth2.Endpoint.TokenURL != "" { + token, err := c.ExchangeRefreshToken(password) + if err != nil { + return err + } + password = token.AccessToken + } + + saslClient := NewXoauth2Client(username, password) + + return client.Authenticate(saslClient) +} diff --git a/worker/imap/configure.go b/worker/imap/configure.go index 691e0d7..a9689f6 100644 --- a/worker/imap/configure.go +++ b/worker/imap/configure.go @@ -38,6 +38,21 @@ func (w *IMAPWorker) handleConfigure(msg *types.Configure) error { w.config.oauthBearer.OAuth2 = oauth2 } + if strings.HasSuffix(w.config.scheme, "+xoauth2") { + w.config.scheme = strings.TrimSuffix(w.config.scheme, "+xoauth2") + w.config.xoauth2.Enabled = true + q := u.Query() + + oauth2 := &oauth2.Config{} + if q.Get("token_endpoint") != "" { + oauth2.ClientID = q.Get("client_id") + oauth2.ClientSecret = q.Get("client_secret") + oauth2.Scopes = []string{q.Get("scope")} + oauth2.Endpoint.TokenURL = q.Get("token_endpoint") + } + w.config.xoauth2.OAuth2 = oauth2 + } + w.config.addr = u.Host if !strings.ContainsRune(w.config.addr, ':') { w.config.addr += ":" + w.config.scheme diff --git a/worker/imap/connect.go b/worker/imap/connect.go index 7c43b56..035feab 100644 --- a/worker/imap/connect.go +++ b/worker/imap/connect.go @@ -80,6 +80,11 @@ func (w *IMAPWorker) connect() (*client.Client, error) { username, password, c); err != nil { return nil, err } + } else if w.config.xoauth2.Enabled { + if err := w.config.xoauth2.Authenticate( + username, password, c); err != nil { + return nil, err + } } else if err := c.Login(username, password); err != nil { return nil, err } diff --git a/worker/imap/worker.go b/worker/imap/worker.go index 66e4cdf..c5032a0 100644 --- a/worker/imap/worker.go +++ b/worker/imap/worker.go @@ -43,6 +43,7 @@ type imapConfig struct { user *url.Userinfo folders []string oauthBearer lib.OAuthBearer + xoauth2 lib.Xoauth2 idle_timeout time.Duration idle_debounce time.Duration reconnect_maxwait time.Duration