mirror of https://git.sr.ht/~rjarry/aerc
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
2.8 KiB
Go
124 lines
2.8 KiB
Go
//
|
|
// 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"
|
|
"os"
|
|
|
|
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
|
"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 SaveRefreshToken(refreshToken string, acct string) error {
|
|
p := xdg.CachePath("aerc", acct+"-xoauth2.token")
|
|
_ = os.MkdirAll(xdg.CachePath("aerc"), 0o700)
|
|
|
|
return os.WriteFile(
|
|
p,
|
|
[]byte(refreshToken),
|
|
0o600,
|
|
)
|
|
}
|
|
|
|
func GetRefreshToken(acct string) ([]byte, error) {
|
|
p := xdg.CachePath("aerc", acct+"-xoauth2.token")
|
|
return os.ReadFile(p)
|
|
}
|
|
|
|
func (c *Xoauth2) Authenticate(
|
|
username string,
|
|
password string,
|
|
account 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 != "" {
|
|
usedCache := false
|
|
if r, err := GetRefreshToken(account); err == nil && len(r) > 0 {
|
|
password = string(r)
|
|
usedCache = true
|
|
}
|
|
|
|
token, err := c.ExchangeRefreshToken(password)
|
|
if err != nil {
|
|
if usedCache {
|
|
return fmt.Errorf("try removing cached refresh token. %w", err)
|
|
}
|
|
return err
|
|
}
|
|
password = token.AccessToken
|
|
if err := SaveRefreshToken(token.RefreshToken, account); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
saslClient := NewXoauth2Client(username, password)
|
|
|
|
return client.Authenticate(saslClient)
|
|
}
|