Skip to content

Commit 297edaf

Browse files
committed
Feat: Improves the authentication flow #15 #24 #38 #68
The main goal was to simplify the `req` method in `request.go` and making it easier to add more authentication methods. All the magic went into the `auth.go` file. This feature introduces an `Authorizer` which acts as an `Authenticator` factory. Under the hood it creates an authenticator shim per request, which delegates the authentication flow to our authenticators. The authentication flow itself is broken down into `Authorize' and `Verify' steps to encapsulate and control complex authentication challenges. Furthermore, the default `NewAutoAuth' authenticator can be overridden by a custom implementation for more control over flow and resources. The `NewBacisAuth` Authorizer gives us the feel of the old days.
1 parent 9284351 commit 297edaf

File tree

6 files changed

+453
-232
lines changed

6 files changed

+453
-232
lines changed

README.md

+133-95
Large diffs are not rendered by default.

auth.go

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package gowebdav
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"strings"
8+
"sync"
9+
)
10+
11+
// AlgoChangedErr must be thrown from the Verify method
12+
// to trigger a re-authentication with a new algorithm.
13+
type AlgoChangedErr struct{}
14+
15+
func (e AlgoChangedErr) Error() string {
16+
return "AuthChangedErr"
17+
}
18+
19+
// AuthFactory prototype function to create a new Authenticator
20+
type AuthFactory func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error)
21+
22+
// Authorizer a Authenticator factory
23+
type Authorizer interface {
24+
NewAuthenticator(body io.Reader) (Authenticator, io.Reader)
25+
AddAuthenticator(key string, fn AuthFactory)
26+
}
27+
28+
// Authenticator stub
29+
type Authenticator interface {
30+
Authorize(c *http.Client, rq *http.Request, method string, path string) error
31+
Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error)
32+
Clone() Authenticator
33+
io.Closer
34+
}
35+
36+
// authorizer structure holds our Authenticator create functions
37+
type authorizer struct {
38+
factories map[string]AuthFactory
39+
defAuthMux sync.Mutex
40+
defAuth Authenticator
41+
}
42+
43+
// authShim structure that wraps the real Authenticator
44+
type authShim struct {
45+
factory AuthFactory
46+
body io.Reader
47+
auth Authenticator
48+
}
49+
50+
// nullAuth initializes the whole authentication flow
51+
type nullAuth struct{}
52+
53+
// NewAutoAuth creates an auto Authenticator factory
54+
func NewAutoAuth(login string, secret string) Authorizer {
55+
fmap := make(map[string]AuthFactory)
56+
az := &authorizer{fmap, sync.Mutex{}, &nullAuth{}}
57+
58+
az.AddAuthenticator("basic", func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) {
59+
return &BasicAuth{login, secret}, nil
60+
})
61+
62+
az.AddAuthenticator("digest", func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) {
63+
return &DigestAuth{login, secret, digestParts(rs)}, nil
64+
})
65+
66+
return az
67+
}
68+
69+
// NewAuthenticator creates an Authenticator (Shim) per request
70+
func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
71+
var retryBuf io.Reader = body
72+
if body != nil {
73+
// If the authorization fails, we will need to restart reading
74+
// from the passed body stream.
75+
// When body is seekable, use seek to reset the streams
76+
// cursor to the start.
77+
// Otherwise, copy the stream into a buffer while uploading
78+
// and use the buffers content on retry.
79+
if _, ok := retryBuf.(io.Seeker); ok {
80+
body = io.NopCloser(body)
81+
} else {
82+
buff := &bytes.Buffer{}
83+
retryBuf = buff
84+
body = io.TeeReader(body, buff)
85+
}
86+
}
87+
a.defAuthMux.Lock()
88+
defAuth := a.defAuth.Clone()
89+
a.defAuthMux.Unlock()
90+
91+
return &authShim{a.factory, retryBuf, defAuth}, body
92+
}
93+
94+
func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) {
95+
a.factories[key] = fn
96+
}
97+
98+
// factory creates an Authenticator instance based on the WWW-Authenticate header
99+
func (a *authorizer) factory(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) {
100+
header := strings.ToLower(rs.Header.Get("Www-Authenticate"))
101+
for k, fn := range a.factories {
102+
if strings.Contains(header, k) {
103+
if auth, err = fn(rq, rs, method, path); err != nil {
104+
return
105+
}
106+
break
107+
}
108+
}
109+
if auth == nil {
110+
return nil, newPathError("NoAuthenticator", path, rs.StatusCode)
111+
}
112+
113+
a.defAuthMux.Lock()
114+
a.defAuth = auth
115+
a.defAuthMux.Unlock()
116+
117+
return auth, nil
118+
}
119+
120+
// Authorize the current request
121+
func (s *authShim) Authorize(c *http.Client, rq *http.Request, method string, path string) error {
122+
if err := s.auth.Authorize(c, rq, method, path); err != nil {
123+
return err
124+
}
125+
body := s.body
126+
rq.GetBody = func() (io.ReadCloser, error) {
127+
if body != nil {
128+
if sk, ok := body.(io.Seeker); ok {
129+
if _, err := sk.Seek(0, io.SeekStart); err != nil {
130+
return nil, err
131+
}
132+
}
133+
return io.NopCloser(body), nil
134+
}
135+
return nil, nil
136+
}
137+
return nil
138+
}
139+
140+
// Verify checks for authentication issues and may trigger a re-authentication.
141+
// Catches AlgoChangedErr to update the current Authenticator
142+
func (s *authShim) Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) {
143+
reauth, err = s.auth.Verify(rq, rs, method, path)
144+
if err != nil {
145+
if _, ok := err.(AlgoChangedErr); ok {
146+
if auth, aerr := s.factory(rq, rs, method, path); aerr == nil {
147+
s.auth = auth
148+
return true, nil
149+
} else {
150+
err = aerr
151+
}
152+
}
153+
}
154+
return
155+
}
156+
157+
// Close closes all resources
158+
func (s *authShim) Close() error {
159+
if s.body != nil {
160+
if closer, ok := s.body.(io.Closer); ok {
161+
return closer.Close()
162+
}
163+
}
164+
return nil
165+
}
166+
167+
// Clone creates a copy of itself
168+
func (s *authShim) Clone() Authenticator {
169+
// panic?
170+
return nil
171+
}
172+
173+
// String toString
174+
func (s *authShim) String() string {
175+
return "AuthShim"
176+
}
177+
178+
// Authorize the current request
179+
func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, method string, path string) error {
180+
return nil
181+
}
182+
183+
// Verify checks for authentication issues and may trigger a re-authentication
184+
func (n *nullAuth) Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) {
185+
return true, AlgoChangedErr{}
186+
}
187+
188+
// Close closes all resources
189+
func (n *nullAuth) Close() error {
190+
return nil
191+
}
192+
193+
// Clone creates a copy of itself
194+
func (n *nullAuth) Clone() Authenticator {
195+
// no copy due to read only access
196+
return n
197+
}
198+
199+
// String toString
200+
func (n *nullAuth) String() string {
201+
return "NullAuth"
202+
}

basicAuth.go

+43-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package gowebdav
22

33
import (
4-
"encoding/base64"
4+
"fmt"
5+
"io"
56
"net/http"
67
)
78

@@ -11,24 +12,51 @@ type BasicAuth struct {
1112
pw string
1213
}
1314

14-
// Type identifies the BasicAuthenticator
15-
func (b *BasicAuth) Type() string {
16-
return "BasicAuth"
15+
// Authorize the current request
16+
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, method string, path string) error {
17+
rq.SetBasicAuth(b.user, b.pw)
18+
return nil
1719
}
1820

19-
// User holds the BasicAuth username
20-
func (b *BasicAuth) User() string {
21-
return b.user
21+
// Verify verifies if the authentication
22+
func (b *BasicAuth) Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) {
23+
if rs.StatusCode == 401 {
24+
err = newPathError("Authorize", path, rs.StatusCode)
25+
}
26+
return
2227
}
2328

24-
// Pass holds the BasicAuth password
25-
func (b *BasicAuth) Pass() string {
26-
return b.pw
29+
// Close cleans up all resources
30+
func (b *BasicAuth) Close() error {
31+
return nil
2732
}
2833

29-
// Authorize the current request
30-
func (b *BasicAuth) Authorize(req *http.Request, method string, path string) {
31-
a := b.user + ":" + b.pw
32-
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a))
33-
req.Header.Set("Authorization", auth)
34+
// Clone creates a Copy of itself
35+
func (b *BasicAuth) Clone() Authenticator {
36+
// no copy due to read only access
37+
return b
38+
}
39+
40+
// String toString
41+
func (b *BasicAuth) String() string {
42+
return fmt.Sprintf("BasicAuth login: %s", b.user)
43+
}
44+
45+
// NewBasicAuth creates a plain BasicAuth Authorizer
46+
// no fancy body buffering, no magic at all
47+
// just dump as if it were on tag 8
48+
func NewBasicAuth(login, secret string) Authorizer {
49+
return &basicAuthAuthorizer{&BasicAuth{login, secret}}
50+
}
51+
52+
type basicAuthAuthorizer struct {
53+
auth *BasicAuth
54+
}
55+
56+
func (b *basicAuthAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
57+
return b.auth, body
58+
}
59+
60+
func (b *basicAuthAuthorizer) AddAuthenticator(key string, fn AuthFactory) {
61+
panic("not implemented")
3462
}

client.go

+7-38
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"os"
1111
pathpkg "path"
1212
"strings"
13-
"sync"
1413
"time"
1514
)
1615

@@ -20,47 +19,17 @@ type Client struct {
2019
headers http.Header
2120
interceptor func(method string, rq *http.Request)
2221
c *http.Client
23-
24-
authMutex sync.Mutex
25-
auth Authenticator
26-
}
27-
28-
// Authenticator stub
29-
type Authenticator interface {
30-
Type() string
31-
User() string
32-
Pass() string
33-
Authorize(*http.Request, string, string)
34-
}
35-
36-
// NoAuth structure holds our credentials
37-
type NoAuth struct {
38-
user string
39-
pw string
40-
}
41-
42-
// Type identifies the authenticator
43-
func (n *NoAuth) Type() string {
44-
return "NoAuth"
45-
}
46-
47-
// User returns the current user
48-
func (n *NoAuth) User() string {
49-
return n.user
50-
}
51-
52-
// Pass returns the current password
53-
func (n *NoAuth) Pass() string {
54-
return n.pw
55-
}
56-
57-
// Authorize the current request
58-
func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
22+
auth Authorizer
5923
}
6024

6125
// NewClient creates a new instance of client
6226
func NewClient(uri, user, pw string) *Client {
63-
return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}}
27+
return NewAuthClient(uri, NewAutoAuth(user, pw))
28+
}
29+
30+
// NewAuthClient creates a new client instance with a custom Authorizer
31+
func NewAuthClient(uri string, auth Authorizer) *Client {
32+
return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, auth}
6433
}
6534

6635
// SetHeader lets us set arbitrary headers for a given client

digestAuth.go

+29-16
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,41 @@ type DigestAuth struct {
1717
digestParts map[string]string
1818
}
1919

20-
// Type identifies the DigestAuthenticator
21-
func (d *DigestAuth) Type() string {
22-
return "DigestAuth"
20+
// Authorize the current request
21+
func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, method string, path string) error {
22+
d.digestParts["uri"] = path
23+
d.digestParts["method"] = method
24+
d.digestParts["username"] = d.user
25+
d.digestParts["password"] = d.pw
26+
rq.Header.Set("Authorization", getDigestAuthorization(d.digestParts))
27+
return nil
2328
}
2429

25-
// User holds the DigestAuth username
26-
func (d *DigestAuth) User() string {
27-
return d.user
30+
// Verify checks for authentication issues and may trigger a re-authentication
31+
func (d *DigestAuth) Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) {
32+
if rs.StatusCode == 401 {
33+
err = newPathError("Authorize", path, rs.StatusCode)
34+
}
35+
return
2836
}
2937

30-
// Pass holds the DigestAuth password
31-
func (d *DigestAuth) Pass() string {
32-
return d.pw
38+
// Close cleans up all resources
39+
func (d *DigestAuth) Close() error {
40+
return nil
3341
}
3442

35-
// Authorize the current request
36-
func (d *DigestAuth) Authorize(req *http.Request, method string, path string) {
37-
d.digestParts["uri"] = path
38-
d.digestParts["method"] = method
39-
d.digestParts["username"] = d.user
40-
d.digestParts["password"] = d.pw
41-
req.Header.Set("Authorization", getDigestAuthorization(d.digestParts))
43+
// Clone creates a copy of itself
44+
func (d *DigestAuth) Clone() Authenticator {
45+
parts := make(map[string]string, len(d.digestParts))
46+
for k, v := range d.digestParts {
47+
parts[k] = v
48+
}
49+
return &DigestAuth{d.user, d.pw, parts}
50+
}
51+
52+
// String toString
53+
func (d *DigestAuth) String() string {
54+
return fmt.Sprintf("DigestAuth login: %s", d.user)
4255
}
4356

4457
func digestParts(resp *http.Response) map[string]string {

0 commit comments

Comments
 (0)