/
authorization.go
161 lines (126 loc) 路 4.4 KB
/
authorization.go
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package calendar
import (
"context"
"crypto/sha256"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
"time"
"github.com/dvsekhvalnov/jose2go/base64url"
"github.com/google/uuid"
"github.com/pkg/browser"
"golang.org/x/oauth2"
"google.golang.org/api/calendar/v3"
)
const port = 36169
type response struct {
values url.Values
err error
}
func NewConfig(clientID string, clientSecret string) *oauth2.Config {
return &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: "http://localhost:" + strconv.Itoa(port),
Scopes: []string{calendar.CalendarScope},
Endpoint: oauth2.Endpoint{
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://oauth2.googleapis.com/token",
},
}
}
func GetToken(config *oauth2.Config) (*oauth2.Token, error) {
state := uuid.New().String()
challengeRaw, err := randomStringURLSafe(96)
if err != nil {
return nil, fmt.Errorf("cannot generate a random string for the challenge: %w", err)
}
challengeSha256 := sha256.Sum256([]byte(challengeRaw))
challengeURLEncoded := base64url.Encode(challengeSha256[:])
codeChallenge := oauth2.SetAuthURLParam("code_challenge", challengeURLEncoded)
codeChallengeMethod := oauth2.SetAuthURLParam("code_challenge_method", "S256")
authURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline, codeChallenge, codeChallengeMethod)
log.Println("open the browser and start the authorization server")
if err := browser.OpenURL(authURL); err != nil {
return nil, fmt.Errorf("cannot open a browser to handle the authorization flow: %w", err)
}
res := <-callback("127.0.0.1:" + strconv.Itoa(port))
if errorCode := res.values.Get("error"); errorCode != "" {
return nil, fmt.Errorf("the user did not grant the required permissions")
}
actual := res.values.Get("state")
if state != actual {
return nil, fmt.Errorf("state does not match the original one, something nasty happened")
}
code := res.values.Get("code")
verifier := oauth2.SetAuthURLParam("code_verifier", challengeRaw)
token, err := config.Exchange(context.Background(), code, verifier)
if err != nil {
return nil, fmt.Errorf("cannot exchange the OAuth 2 code for an access token: %w", err)
}
return token, nil
}
// nolint:gocognit // this function is only slightly more complex than the allowed threshold
func callback(address string) chan *response {
responseCh, shutdownCh, interruptCh := make(chan *response), make(chan bool), make(chan bool)
server := &http.Server{Addr: address}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
var msg string
if r.URL.Query().Get("code") != "" {
msg = "Alfred TimeTracker authenticated correctly, you can now close this window."
} else {
msg = "Something went wrong with the authorization workflow. Please try again."
}
if _, err := w.Write([]byte(msg)); err != nil {
log.Printf("http.ResponseWriter write failed: %v", err)
}
interruptCh <- true
responseCh <- &response{values: r.URL.Query()}
shutdownCh <- true
})
// run the server
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("error running the authorization server: %s\n", err)
}
}()
// shutdown the server after a timeout
go func() {
select {
case <-interruptCh:
case <-time.After(10 * time.Minute):
responseCh <- &response{err: fmt.Errorf("timeout to complete the authorization flow expired")}
shutdownCh <- false
}
}()
// shutdown the server gracefully
go func() {
done := <-shutdownCh
if done {
log.Println("authorization flow done, shutting down the authorization server")
} else {
log.Println("timeout to done the authorization flow expired, shutting down the HTTP server")
}
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("authorization server could not shutdown gracefully: %v", err)
}
}()
return responseCh
}
func RevokeToken(token *oauth2.Token) error {
data := url.Values{"token": {token.RefreshToken}}
resp, err := http.PostForm("https://accounts.google.com/o/oauth2/revoke", data)
if err != nil {
return fmt.Errorf("networking error while trying to revoke the token: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("revoke endpoint returned a %d status code", resp.StatusCode)
}
return nil
}