aboutsummaryrefslogtreecommitdiffstats
path: root/lib/xoauth2.go
blob: c0f654b815a49f96cc4118584a8b21849ff0fad7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//
// 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"
	"path"

	"github.com/emersion/go-imap/client"
	"github.com/emersion/go-sasl"
	"github.com/kyoh86/xdg"
	"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 := path.Join(xdg.CacheHome(), "aerc", acct+"-xoauth2.token")
	if _, err := os.Stat(p); os.IsNotExist(err) {
		_ = os.MkdirAll(path.Join(xdg.CacheHome(), "aerc"), 0o700)
	}

	return os.WriteFile(
		p,
		[]byte(refreshToken),
		0o600,
	)
}

func GetRefreshToken(acct string) ([]byte, error) {
	p := path.Join(xdg.CacheHome(), "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)
}