Skip to content

Commit

Permalink
Feat: Improves the authentication flow #15 #24 #38 #68
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chripo committed Feb 3, 2023
1 parent 3282f94 commit 8b1408f
Show file tree
Hide file tree
Showing 6 changed files with 450 additions and 233 deletions.
228 changes: 133 additions & 95 deletions README.md

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions auth.go
@@ -0,0 +1,202 @@
package gowebdav

import (
"bytes"
"io"
"net/http"
"strings"
"sync"
)

// AlgoChangedErr must be thrown from the Verify method
// to trigger a re-authentication with a new algorithm.
type AlgoChangedErr struct{}

func (e AlgoChangedErr) Error() string {
return "AuthChangedErr"
}

// AuthFactory prototype function to create a new Authenticator
type AuthFactory func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error)

// Authorizer a Authenticator factory
type Authorizer interface {
NewAuthenticator(body io.Reader) (Authenticator, io.Reader)
AddAuthenticator(key string, fn AuthFactory)
}

// Authenticator stub
type Authenticator interface {
Authorize(c *http.Client, rq *http.Request, method string, path string) error
Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error)
Clone() Authenticator
io.Closer
}

// authorizer structure holds our Authenticator create functions
type authorizer struct {
factories map[string]AuthFactory
defAuthMux sync.Mutex
defAuth Authenticator
}

// authShim structure that wraps the real Authenticator
type authShim struct {
factory AuthFactory
body io.Reader
auth Authenticator
}

// nullAuth initializes the whole authentication flow
type nullAuth struct{}

// NewAutoAuth creates an auto Authenticator factory
func NewAutoAuth(login string, secret string) Authorizer {
fmap := make(map[string]AuthFactory)
az := &authorizer{fmap, sync.Mutex{}, &nullAuth{}}

az.AddAuthenticator("basic", func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) {
return &BasicAuth{login, secret}, nil
})

az.AddAuthenticator("digest", func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) {
return &DigestAuth{login, secret, digestParts(rs)}, nil
})

return az
}

// NewAuthenticator creates an Authenticator (Shim) per request
func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
var retryBuf io.Reader = body
if body != nil {
// If the authorization fails, we will need to restart reading
// from the passed body stream.
// When body is seekable, use seek to reset the streams
// cursor to the start.
// Otherwise, copy the stream into a buffer while uploading
// and use the buffers content on retry.
if _, ok := retryBuf.(io.Seeker); ok {
body = io.NopCloser(body)
} else {
buff := &bytes.Buffer{}
retryBuf = buff
body = io.TeeReader(body, buff)
}
}
a.defAuthMux.Lock()
defAuth := a.defAuth.Clone()
a.defAuthMux.Unlock()

return &authShim{a.factory, retryBuf, defAuth}, body
}

func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) {
a.factories[key] = fn
}

// factory creates an Authenticator instance based on the WWW-Authenticate header
func (a *authorizer) factory(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) {
header := strings.ToLower(rs.Header.Get("Www-Authenticate"))
for k, fn := range a.factories {
if strings.Contains(header, k) {
if auth, err = fn(rq, rs, method, path); err != nil {
return
}
break
}
}
if auth == nil {
return nil, newPathError("NoAuthenticator", path, rs.StatusCode)
}

a.defAuthMux.Lock()
a.defAuth = auth
a.defAuthMux.Unlock()

return auth, nil
}

// Authorize the current request
func (s *authShim) Authorize(c *http.Client, rq *http.Request, method string, path string) error {
if err := s.auth.Authorize(c, rq, method, path); err != nil {
return err
}
body := s.body
rq.GetBody = func() (io.ReadCloser, error) {
if body != nil {
if sk, ok := body.(io.Seeker); ok {
if _, err := sk.Seek(0, io.SeekStart); err != nil {
return nil, err
}
}
return io.NopCloser(body), nil
}
return nil, nil
}
return nil
}

// Verify checks for authentication issues and may trigger a re-authentication.
// Catches AlgoChangedErr to update the current Authenticator
func (s *authShim) Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) {
reauth, err = s.auth.Verify(rq, rs, method, path)
if err != nil {
if _, ok := err.(AlgoChangedErr); ok {
if auth, aerr := s.factory(rq, rs, method, path); aerr == nil {
s.auth = auth
return true, nil
} else {
err = aerr
}
}
}
return
}

// Close closes all resources
func (s *authShim) Close() error {
if s.body != nil {
if closer, ok := s.body.(io.Closer); ok {
return closer.Close()
}
}
return nil
}

// Clone creates a copy of itself
func (s *authShim) Clone() Authenticator {
// panic?
return nil
}

// String toString
func (s *authShim) String() string {
return "AuthShim"
}

// Authorize the current request
func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, method string, path string) error {
return nil
}

// Verify checks for authentication issues and may trigger a re-authentication
func (n *nullAuth) Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) {
return true, AlgoChangedErr{}
}

// Close closes all resources
func (n *nullAuth) Close() error {
return nil
}

// Clone creates a copy of itself
func (n *nullAuth) Clone() Authenticator {
// no copy due to read only access
return n
}

// String toString
func (n *nullAuth) String() string {
return "NullAuth"
}
58 changes: 43 additions & 15 deletions basicAuth.go
@@ -1,7 +1,8 @@
package gowebdav

import (
"encoding/base64"
"fmt"
"io"
"net/http"
)

Expand All @@ -11,24 +12,51 @@ type BasicAuth struct {
pw string
}

// Type identifies the BasicAuthenticator
func (b *BasicAuth) Type() string {
return "BasicAuth"
// Authorize the current request
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, method string, path string) error {
rq.SetBasicAuth(b.user, b.pw)
return nil
}

// User holds the BasicAuth username
func (b *BasicAuth) User() string {
return b.user
// Verify verifies if the authentication
func (b *BasicAuth) Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) {
if rs.StatusCode == 401 {
err = newPathError("Authorize", path, rs.StatusCode)
}
return
}

// Pass holds the BasicAuth password
func (b *BasicAuth) Pass() string {
return b.pw
// Close cleans up all resources
func (b *BasicAuth) Close() error {
return nil
}

// Authorize the current request
func (b *BasicAuth) Authorize(req *http.Request, method string, path string) {
a := b.user + ":" + b.pw
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a))
req.Header.Set("Authorization", auth)
// Clone creates a Copy of itself
func (b *BasicAuth) Clone() Authenticator {
// no copy due to read only access
return b
}

// String toString
func (b *BasicAuth) String() string {
return fmt.Sprintf("BasicAuth login: %s", b.user)
}

// NewBasicAuth creates a plain BasicAuth Authorizer
// no fancy body buffering, no magic at all
// just dump as if it were on tag 8
func NewBasicAuth(login, secret string) Authorizer {
return &basicAuthAuthorizer{&BasicAuth{login, secret}}
}

type basicAuthAuthorizer struct {
auth *BasicAuth
}

func (b *basicAuthAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
return b.auth, body
}

func (b *basicAuthAuthorizer) AddAuthenticator(key string, fn AuthFactory) {
panic("not implemented")
}
45 changes: 7 additions & 38 deletions client.go
Expand Up @@ -10,7 +10,6 @@ import (
"os"
pathpkg "path"
"strings"
"sync"
"time"
)

Expand All @@ -20,47 +19,17 @@ type Client struct {
headers http.Header
interceptor func(method string, rq *http.Request)
c *http.Client

authMutex sync.Mutex
auth Authenticator
}

// Authenticator stub
type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*http.Request, string, string)
}

// NoAuth structure holds our credentials
type NoAuth struct {
user string
pw string
}

// Type identifies the authenticator
func (n *NoAuth) Type() string {
return "NoAuth"
}

// User returns the current user
func (n *NoAuth) User() string {
return n.user
}

// Pass returns the current password
func (n *NoAuth) Pass() string {
return n.pw
}

// Authorize the current request
func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
auth Authorizer
}

// NewClient creates a new instance of client
func NewClient(uri, user, pw string) *Client {
return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}}
return NewAuthClient(uri, NewAutoAuth(user, pw))
}

// NewAuthClient creates a new client instance with a custom Authorizer
func NewAuthClient(uri string, auth Authorizer) *Client {
return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, auth}
}

// SetHeader lets us set arbitrary headers for a given client
Expand Down

0 comments on commit 8b1408f

Please sign in to comment.