-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathauthenticator.go
231 lines (202 loc) · 6.45 KB
/
authenticator.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 api
import (
"context"
"errors"
"net/http"
"sync"
"time"
)
const (
SessionCookieToken = "JSESSIONID"
)
type (
AuthCAS uint64
Authenticator interface {
// Decorate must do one of two things:
//
// If the internal state of this authenticator is such that it currently has whatever is needed to modify a
// given request with the appropriate authentication cookie / token / header / etc., then it must do so,
// returning the current CAS and a nil error
//
// If the internal state is such that decoration CANNOT happen, it must return the current CAS and an error,
// describing the reason it cannot decorate the request. If the error is not nil, Refresh will be called with
// the CAS value returned by this method.
//
// In all cases, the current CAS must be returned.
//
// Arguments:
//
// 0. the context provided to original API call
// 1. the raw http request created as part of api call chain
Decorate(context.Context, *http.Request) (AuthCAS, error)
// Refresh must do one of two things:
//
// If the provided CAS value is current, it must assume that its current state is no longer valid and try to do
// what is needed to get back to a state that Decorate is able to do what it needs to do.
//
// If the provided CAS value is NOT equal to the current state, it must assume that a refresh attempt has
// already been made in another process, and just return the current CAS value with no error.
//
// The client will only attempt a maximum of 2 times per execution:
//
// 1. If the FIRST Decorate call fails
// 2. If initial Decorate did not fail but VSZ returned a 401, causing an Invalidate -> Refresh -> Decorate loop
// that will execute exactly 1 times.
//
// Arguments:
//
// 0. the context provided to the original API call
// 1. the current VSZ API client
// 2. the CAS value seen from the calling routine's most recent action (could be from either Decorate or
// Invalidate)
Refresh(context.Context, *Client, AuthCAS) (AuthCAS, error)
// Invalidate will only be called if a 401 is seen after a refresh has been attempted, and should indicate to
// the implementor that whatever decoration the authenticator is current using is no longer considered valid by
// the VSZ being queried
//
// Arguments:
//
// 0. the context provided to the original API call
// 1. the CAS value seen from the calling routine's most recent action (could be from either Decorate or
// Refresh)
Invalidate(context.Context, AuthCAS) (AuthCAS, error)
}
)
// PasswordAuthenticator is a simple example implementation of an Authenticator that will decorate a given request
// with a session id bearing cookie if one exists, and attempt to create one if it doesn't.
type PasswordAuthenticator struct {
username string
password string
mu sync.RWMutex
cas uint64
refreshed time.Time
cookieTTL time.Duration
cookie *http.Cookie
}
func NewPasswordAuthenticator(username, password string, cookieTTL time.Duration) *PasswordAuthenticator {
pa := &PasswordAuthenticator{
username: username,
password: password,
cookieTTL: cookieTTL,
}
return pa
}
func (pa *PasswordAuthenticator) Username() string {
return pa.username
}
func (pa *PasswordAuthenticator) Password() string {
return pa.password
}
func (pa *PasswordAuthenticator) Decorate(ctx context.Context, httpRequest *http.Request) (AuthCAS, error) {
if debug {
log.Printf("[pw-auth-%s] Decorate called for request \"%s %s\"", pa.username, httpRequest.Method, httpRequest.URL)
}
pa.mu.RLock()
cas := pa.cas
if httpRequest == nil {
// TODO: yell a bit more if the request is nil?
pa.mu.RUnlock()
return AuthCAS(cas), errors.New("httpRequest cannot be nil")
}
// is context still valid?
if err := ctx.Err(); err != nil {
pa.mu.RUnlock()
return AuthCAS(cas), err
}
cookie := pa.cookie
// TODO improve efficiency of this?
if cookie != nil && !pa.refreshed.IsZero() && pa.refreshed.Add(pa.cookieTTL).After(time.Now()) {
httpRequest.AddCookie(cookie)
pa.mu.RUnlock()
return AuthCAS(cas), nil
}
pa.mu.RUnlock()
return AuthCAS(cas), errors.New("cookie requires refresh")
}
func (pa *PasswordAuthenticator) Refresh(ctx context.Context, client *Client, cas AuthCAS) (AuthCAS, error) {
if debug {
log.Printf("[pw-auth-%s] Refresh called", pa.username)
}
pa.mu.Lock()
ccas := pa.cas
if client == nil {
pa.mu.Unlock()
return AuthCAS(ccas), errors.New("client cannot be nil")
}
// is context still valid?
if err := ctx.Err(); err != nil {
pa.mu.Unlock()
return AuthCAS(ccas), err
}
// if the passed cas value is greater than the internal CAS, assume weirdness and return current CAS and an error
if ccas < uint64(cas) {
pa.mu.Unlock()
return AuthCAS(ccas), errors.New("provided cas value is greater than possible")
}
// if the passed in CAS value is less than the currently stored one, assume another routine called either Refresh
// or Invalidate and just return current cas
if ccas > uint64(cas) {
pa.mu.Unlock()
return AuthCAS(ccas), nil
}
// if cas matches internal...
// try to execute logon
username := pa.username
password := pa.password
resp, _, err := client.Session().LoginSessionLogonPost(ctx, &LoginSessionLogonPostRequest{
Username: &username,
Password: &password,
})
if resp != nil {
resp.Body.Close()
}
if err != nil {
pa.cookie = nil
pa.cas++
ncas := pa.cas
pa.mu.Unlock()
return AuthCAS(ncas), err
}
cookie := TryExtractSessionCookie(resp)
if cookie == nil {
pa.cookie = nil
pa.cas++
ncas := pa.cas
pa.mu.Unlock()
return AuthCAS(ncas), errors.New("unable to locate cookie in response")
}
pa.cookie = cookie
pa.refreshed = time.Now()
pa.cas++
ncas := pa.cas
pa.mu.Unlock()
return AuthCAS(ncas), nil
}
func (pa *PasswordAuthenticator) Invalidate(ctx context.Context, cas AuthCAS) (AuthCAS, error) {
if debug {
log.Printf("[pw-auth-%s] Invalidate called", pa.username)
}
pa.mu.Lock()
ccas := pa.cas
// is context still valid?
if err := ctx.Err(); err != nil {
pa.mu.Unlock()
return AuthCAS(ccas), err
}
// if current cas is less than provided, assume insanity.
if ccas < uint64(cas) {
pa.mu.Unlock()
return AuthCAS(ccas), errors.New("provided cas value greater than possible")
}
// if current cas is greater than provided, assume Refresh or Invalidate has already been called.
if ccas > uint64(cas) {
pa.mu.Unlock()
return AuthCAS(ccas), nil
}
pa.cas++
ncas := pa.cas
pa.cookie = nil
pa.refreshed = time.Now()
pa.mu.Unlock()
return AuthCAS(ncas), nil
}