/
webhelper.go
148 lines (131 loc) · 4.22 KB
/
webhelper.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
package oauthorizer
import (
"encoding/base64"
"fmt"
"math/rand"
"net/http"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2"
)
// RedirectError is a type of error that can be handled by redirecting
// a client to a url.
type RedirectError interface {
error
GetURL() string
}
type needsAuth struct {
URL string
}
func (na needsAuth) Error() string {
return fmt.Sprintf("needs authentication error: %s", na.URL)
}
func (na needsAuth) GetURL() string {
return na.URL
}
// WebHelper helps to navigate through the oauth2 process. Typically
// you would call GetClient before needing to access a resource. If
// there it returns nil, you could call GenAuthURL and redirect the
// browser to that.
//
// If the users grants access to the resource, the browser will be
// redirected to the url listed in the Config. In that handler, you
// can call Exchange and if it succeeds, you can redirect back to the
// handler that needs to access the resource.
//
// Below is a sketch that demonstrates an expected use-case.
//
// func HandleMain(w http.ResponseWriter, r *http.Request) {
// ctx := r.Context()
// webHelper := getWebHelper()
//
// client := webHelper.GetClient(ctx)
// if client == nil {
// url, err := webHelper.GenAuthURL(ctx)
// if err != nil {
// writeError(w, http.StatusInternalServerError)
// return
// }
// http.Redirect(w, r, url, http.StatusTemporaryRedirect)
// return
// }
//
// // use client to access our resource
// }
//
// func HandleOauthCallback(w http.ResponseWriter, r *http.Request) {
// ctx := r.Context()
// webHelper := getWebHelper()
//
// if err = oh.Exchange(ctx, r); err != nil {
// writeError(w, http.StatusInternalServerError)
// return
// }
//
// http.Redirect(w, r, urlOfMainHandler, http.StatusTemporaryRedirect)
// }
type WebHelper struct {
Config *oauth2.Config
// Knows how to store and retrieve temporary values that is used
// for the oauth2 state parameter. There will be an active nonce
// for each in-flight authorization request. It is ok to delete
// the nonce after authorization finishes (succeeds or fails).
// This library never deletes a nonce, but will overwrite the last
// one whenever we restart authorization. Required.
NonceStore Storer
// Knows how to store and retrieve oauth2 tokens. Currently these
// are only written once, after authorization succeeds. Required.
TokenStore Storer
// Optional way to configure the authorization url used to start
// the authorization flow.
Opts []oauth2.AuthCodeOption
}
// GetClient returns an http client configured for oauth2. Returns
// nil if there isn't a valid token found.
func (wh *WebHelper) GetClient(ctx context.Context) *http.Client {
tok := restoreToken(ctx, wh.TokenStore)
if tok == nil {
return nil
}
return wh.Config.Client(ctx, tok)
}
// GenAuthURL Generates a new authorization URL. Note that this has
// the side effect of saving the associated nonce.
func (wh *WebHelper) GenAuthURL(ctx context.Context) (string, error) {
nonce := newNonce()
url := wh.Config.AuthCodeURL(nonce, wh.Opts...)
if err := wh.NonceStore.Save(ctx, []byte(nonce)); err != nil {
return "", err
}
return url, nil
}
// Exchange exchanges an authorization code for a token, after
// verifying the state is the same we previously saved. If this
// returns without an error, you can call GetClient to get a client with
// the token configured.
func (wh *WebHelper) Exchange(ctx context.Context, r *http.Request) error {
b, err := wh.NonceStore.Restore(ctx)
if err != nil {
return fmt.Errorf("unable to load nonce: %v", err)
}
nonce := string(b)
state := r.FormValue("state")
if state != nonce {
return fmt.Errorf("expected %q for state but got %q", nonce, state)
}
code := r.FormValue("code")
tok, err := wh.Config.Exchange(ctx, code)
if err != nil {
return fmt.Errorf("code exchange failure: %v", err)
}
if err = saveToken(ctx, wh.TokenStore, tok); err != nil {
return fmt.Errorf("token save failure: %v", err)
}
return nil
}
func newNonce() string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
b := make([]byte, 16)
_, _ = r.Read(b)
return base64.StdEncoding.EncodeToString(b)
}