/
session.go
231 lines (197 loc) · 6.7 KB
/
session.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Package session defines the opertions for supporting user sessions within
// a power-league application.
package session
import (
"context"
"encoding/gob"
"errors"
"fmt"
"net/http"
"time"
"github.com/Forestmb/goff"
"github.com/golang/glog"
"github.com/gorilla/sessions"
"github.com/pborman/uuid"
lru "github.com/youtube/vitess/go/cache"
"golang.org/x/oauth2"
)
const (
// SessionName is used to update the client session
SessionName = "client-session"
// AccessTokenKey updates the access token for the current session
AccessTokenKey = "access-token"
// SessionIDKey sets the ID for each session
SessionIDKey = "session-id"
)
// oauthState ensures the login response matches the request
var oauthState = uuid.New()
//
// Manager interface
//
// Manager provides an interface to managing sessions for power rankings users
type Manager interface {
Login(w http.ResponseWriter, r *http.Request) (loginURL string)
Authenticate(w http.ResponseWriter, r *http.Request) error
Logout(w http.ResponseWriter, r *http.Request) error
IsLoggedIn(r *http.Request) bool
GetClient(w http.ResponseWriter, r *http.Request) (*goff.Client, error)
}
// defaultManager is the default implementation of Manager
type defaultManager struct {
consumerProvider ConsumerProvider
store sessions.Store
cache *lru.LRUCache
userCacheDurationSeconds int
}
// NewManager creates a new Manager that uses the given consumer for OAuth
// authentication and store to persist the sessions across requests. Each
// session client returned by `Manager.GetClient` will cache responses for up
// to 6 hours.
//
// See NewManagerWithCache
func NewManager(cp ConsumerProvider, s sessions.Store) Manager {
return NewManagerWithCache(cp, s, 6*60*60, 10000)
}
// NewManagerWithCache creates a new Manager that uses the given consumer
// provider for OAuth authentication and store to persist the sessions across
// requests. Each session client returned by `Manager.GetClient` will cache
// responses for up to `userCacheDurationSeconds` seconds.
func NewManagerWithCache(
cp ConsumerProvider,
s sessions.Store,
userCacheDurationSeconds int,
cacheSize int64) Manager {
gob.Register(&oauth2.Token{})
gob.Register(&time.Time{})
cache := lru.NewLRUCache(cacheSize)
return &defaultManager{
consumerProvider: cp,
store: s,
cache: cache,
userCacheDurationSeconds: userCacheDurationSeconds,
}
}
//
// Consumer interface
//
// ConsumerProvider creates Consumers to handle authentication on behalf of a
// given request
type ConsumerProvider interface {
Get(r *http.Request) Consumer
}
// Consumer is the interface to an OAuth2 consumer
type Consumer interface {
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)
Client(ctx context.Context, token *oauth2.Token) *http.Client
}
//
// Client auth
//
// Login starts a new user session within the given request and returns the URL
// that must be accessed by the user to grant authentication
func (d *defaultManager) Login(w http.ResponseWriter, r *http.Request) (loginURL string) {
config := d.consumerProvider.Get(r)
return config.AuthCodeURL(oauthState)
}
// Logout ends a user session
func (d *defaultManager) Logout(w http.ResponseWriter, r *http.Request) error {
session, _ := d.store.Get(r, SessionName)
session.Values = make(map[interface{}]interface{})
err := session.Save(r, w)
if err != nil {
glog.Warningf("error saving client logout in session: %s", err)
return err
}
glog.V(3).Infoln("client logout saved in session")
return nil
}
// IsLoggedIn returns whether or not the user represented by the given request
// is logged in.
func (d *defaultManager) IsLoggedIn(req *http.Request) bool {
session, _ := d.store.Get(req, SessionName)
_, ok := session.Values[AccessTokenKey].(*oauth2.Token)
return ok
}
// Authenticate uses the verification code in the request and a request token to
// authenticate the user and create an access token.
func (d *defaultManager) Authenticate(w http.ResponseWriter, req *http.Request) error {
session, err := d.store.Get(req, SessionName)
if err != nil {
glog.Warningf("error getting session: %s", err)
// continue since a new one should have been created
}
state := req.FormValue("state")
if state != oauthState {
return fmt.Errorf("invalid state returned for authorization, expecing '%s' got '%s'",
oauthState,
state)
}
verificationCode := req.FormValue("code")
if verificationCode == "" {
glog.V(2).Infoln("client not authenticated")
return fmt.Errorf("unable to create goff client for request, "+
"no verification code in request: %+v", req.Form)
}
glog.V(2).Infof("authenticating client with verification code: %s",
verificationCode)
consumer := d.consumerProvider.Get(req)
accessToken, err := consumer.Exchange(req.Context(), verificationCode)
if err != nil {
glog.Warningf("error authorizing token: %s", err)
return errors.New("unable to create goff client for request, " +
"failure when authorizing request token")
}
session.Values = map[interface{}]interface{}{
AccessTokenKey: accessToken,
SessionIDKey: uuid.New(),
}
err = session.Save(req, w)
if err != nil {
glog.Warningf("error saving client session: %s", err)
return err
}
glog.Infoln("client authenticated")
return nil
}
// GetClient returns the goff.Client for the user represented by the given
// request. The return value can be used to make fantasy API requests
func (d *defaultManager) GetClient(w http.ResponseWriter, req *http.Request) (*goff.Client, error) {
session, err := d.store.Get(req, SessionName)
if err != nil {
glog.Warningf("error getting session: %s", err)
// continue since a new one should have been created
}
accessToken, ok := session.Values[AccessTokenKey].(*oauth2.Token)
// No access token, try creating one if being verified by request
if !ok {
glog.V(2).Infoln("client not authenticated")
return nil, errors.New("no access token in client session")
}
id, ok := session.Values[SessionIDKey].(string)
if !ok {
id = uuid.New()
glog.Warningf("generating new ID, no '%s' in session -- id=%s",
SessionIDKey,
id)
}
session.Values = map[interface{}]interface{}{
AccessTokenKey: accessToken,
SessionIDKey: id,
}
err = session.Save(req, w)
if err != nil {
glog.Warningf("error saving client session: %s", err)
return nil, err
}
consumer := d.consumerProvider.Get(req)
oauthClient := consumer.Client(req.Context(), accessToken)
client := goff.NewCachedClient(
goff.NewLRUCache(
id,
time.Duration(d.userCacheDurationSeconds)*time.Second,
d.cache),
oauthClient)
glog.V(3).Infoln("client created successfully")
return client, nil
}