Skip to content

Improves the authentication flow #71

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 22, 2023
Merged

Improves the authentication flow #71

merged 10 commits into from
Jun 22, 2023

Conversation

chripo
Copy link
Member

@chripo chripo commented Feb 3, 2023

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.

@chripo chripo changed the title Improves the authentication flow Improves the authentication flow #15 #24 #38 #68 Feb 3, 2023
@chripo chripo changed the title Improves the authentication flow #15 #24 #38 #68 Improves the authentication flow Feb 3, 2023
@chripo chripo requested a review from zekroTJA February 3, 2023 15:38
@chripo chripo force-pushed the next branch 2 times, most recently from d9821d2 to 4fd65a0 Compare February 3, 2023 15:59
@chripo
Copy link
Member Author

chripo commented Feb 3, 2023

Thanks @ueffel!
Any thoughts on the basic strategy of the game?
Is it clear or easier to gasp?

@ueffel
Copy link
Collaborator

ueffel commented Feb 3, 2023

The Authorizer and Authenticator look easy enough to implement for a third party. Maybe @Gamer92000 can checkout if this new API simplifies #70.
Overall it looks good to me.

@ueffel
Copy link
Collaborator

ueffel commented Feb 3, 2023

It may be interesting to try implementing Kerberos authentication without wrapping the http.Transport (which is need with github.com/dpotapov/go-spnego)
I think I will try that to check how it goes.

@ueffel
Copy link
Collaborator

ueffel commented Feb 3, 2023

I think there should be a way to create an Authorizer without the Basic and Digest methods, so one can just add (AddAuthenticator) the Authenticators, that are wanted.

func NewEmptyAuth() Authorizer {
	fmap := make(map[string]AuthFactory)
	az := &authorizer{fmap, sync.Mutex{}, &nullAuth{}}
	return az
}

And maybe one Authorizer that does authenticates preemptively:

func NewPreemptiveAuth(auther Authenticator) Authorizer {
	fmap := make(map[string]AuthFactory)
	az := &authorizer{fmap, sync.Mutex{}, auther}
	return az
}

Minor inconvience newPathError and newPathErrorErr could be public, so they can be reused.

Sample kerberos authentication
package main

import (
	"net/http"
	"os"

	"github.com/dpotapov/go-spnego"
	"github.com/studio-b12/gowebdav"
)

type krb5Auth struct {
	sspi           spnego.Provider
	NoCanonicalize bool
}

func NewKrb5Auth() *krb5Auth {
	return &krb5Auth{sspi: spnego.New()}
}

func (k *krb5Auth) Authorize(c *http.Client, rq *http.Request, method string, path string) error {
	return k.sspi.SetSPNEGOHeader(rq, !k.NoCanonicalize)
}

func (k *krb5Auth) Verify(rq *http.Request, rs *http.Response, method string, path string) (bool, error) {
	if rs.StatusCode == http.StatusUnauthorized {
		return false, &os.PathError{
			Op:   "Authorize",
			Path: path,
			Err:  gowebdav.StatusError{Status: rs.StatusCode},
		}
	}
	return false, nil
}

func (k *krb5Auth) Clone() gowebdav.Authenticator {
	auther := NewKrb5Auth()
	auther.NoCanonicalize = k.NoCanonicalize
	return auther
}

func (k *krb5Auth) Close() error {
	return nil
}

var _ gowebdav.Authenticator = (*krb5Auth)(nil)

Usage like this

authorizer := gowebdav.NewEmptyAuth()
authorizer.AddAuthenticator("negotiate", func(rq *http.Request, rs *http.Response, method, path string) (auth gowebdav.Authenticator, err error) {
	return NewKrb5Auth(), nil
})

or

authorizer := gowebdav.NewPreemptiveAuth(NewKrb5Auth())

@chripo
Copy link
Member Author

chripo commented Feb 3, 2023

awesome!
for kerberos only auth it could shipped with a authorizer like the BasicAuth does.

func NewBasicAuth(login, secret string) Authorizer {
    return &basicAuthAuthorizer{&BasicAuth{login, secret}}
}

@ueffel
Copy link
Collaborator

ueffel commented Feb 3, 2023

awesome! for kerberos only auth it could shipped with a authorizer like the BasicAuth does.

func NewBasicAuth(login, secret string) Authorizer {
    return &basicAuthAuthorizer{&BasicAuth{login, secret}}
}

Yeah, but this requires far more code 😇.

Also I thought about what would happen, if a server offers multiple ways to authenticate (multiple Www-Authenticate headers) and the first one (what rs.Header.Get() returns) is not the one that should be taken by the client.
I think right now this wouldn't work.

(Edit: wouldn't work)

@chripo
Copy link
Member Author

chripo commented Feb 3, 2023

Yeah, but this requires far more code innocent.

I LIKE U

Also I thought about what would happen, if a server offers multiple ways to authenticate (multiple Www-Authenticate headers) ....
we ship this since 2014, so it works(tm)
but it's a good thought. we need to sort the Authenticators and pull out all headers.
PR welcome.

@chripo
Copy link
Member Author

chripo commented Feb 3, 2023

Minor inconvience newPathError and newPathErrorErr could be public, so they can be reused.

we will take care about that too.

@ueffel
Copy link
Collaborator

ueffel commented Feb 8, 2023

@chripo
Have a look at the "next-plus" branch
I tried myself at supporting multiple Www-Authenticate header but i'm not happy with the solution. Too many new struct members for the authorizer. Maybe you have an idea to make it better?

@chripo
Copy link
Member Author

chripo commented Feb 8, 2023

@ueffel
indeed that a lot of changes.
take a look at 9da199c this should be enough to favor the order if the server supports multiple authentication methods.

@ueffel
Copy link
Collaborator

ueffel commented Feb 8, 2023

take a look at 9da199c this should be enough to favor the order if the server supports multiple authentication methods.

but this still only looks at the first authentication header header := strings.ToLower(rs.Header.Get("Www-Authenticate")) instead of all of them headers := rs.Header.Values("Www-Authenticate")

@chripo
Copy link
Member Author

chripo commented Feb 8, 2023

In fact, I was looking up in the wrong docs, somehow...
This needs some more work, I've got an idea.

@chripo
Copy link
Member Author

chripo commented Feb 9, 2023

Well, I'm done so far.
@ueffel What do you think about the last changes?

What needs to be done to get merged:

  • add some auth flow / API documentation and examples of how to enroll a custom authentication (Readme)
  • clarify how to deal with 3rd party authentication that pulls in dependencies
  • more reviews
  • feat(auth): add support for MS-PASS #70 may show up with more changes, hopefully not

Copy link
Collaborator

@ueffel ueffel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are on a very good way to make this awesome :)

Also: At least the multiAuthTest from my branch made it :D

@chripo
Copy link
Member Author

chripo commented Feb 10, 2023

thank you again @ueffel! Absolut Fett!
your tests made it awesome.
I'll pull them off at the final squash / end, that you can apply them by our own to preserve ownership!!

The changes simplify the `req` method by moving the
authentication-related code into the API.
This makes it easy to add additional authentication methods.

The API introduces an `Authorizer` that acts as an
authenticator factory. The authentication flow itself
is divided down into `Authorize` and `Verify` steps in order
to encapsulate and control complex authentication challenges.

The default `NewAutoAuth` negotiates the algorithms.
Under the hood, it creates an authenticator shim per request,
which delegates the authentication flow to our authenticators.

The `NewEmptyAuth` and `NewPreemptiveAuth` authorizers
allow you to have more control over algorithms and resources.

The API also allows interception of the redirect mechanism by setting
the `XInhibitRedirect` header.

This closes: #15 #24 #38
@chripo
Copy link
Member Author

chripo commented Feb 13, 2023

@zekroTJA Do you have time for a feature review?

@zekroTJA
Copy link
Member

zekroTJA commented Feb 15, 2023

@zekroTJA Do you have time for a feature review?

Yeah, I would take a look into it in the next couple of days if that fits. I am not that deep into the WebDav protocol itself but I'll take a look into it. :)

Copy link
Member

@zekroTJA zekroTJA left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far. Just some code style things and proposes for avoiding panics in the package.

Some of my annotations are based on personal preference and may be a bit nitpicky, but I am open for discussion. :)

}

// Do not Clone the shim, it ends badly. In any case for you.
func (s *authShim) Clone() Authenticator {
Copy link
Member

@zekroTJA zekroTJA Feb 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would strongly avoid panicing in packages. Especially in a method which is "hidden" behind an interface. I would split the Authenticatior interface into something like the following.

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

type ClonableAuthenticator interface {
	Authorize(c *http.Client, rq *http.Request, path string) error
	Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
	io.Closer
	Clone() ClonableAuthenticator
}

Another approach would be to make Authenticators not clonable by default, adding a Clonable interface and checking the clonability when required.

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

type Clonable interface {
	Clone() interface{}
}

Then, the clone call could look something like this.

var a, cloned Authenticator = // ...
if ac, ok := a.(Clonable); ok {
	cloned = a.Clone().(Authenticator)
}

Another approach would be to let Clone() return the cloned Authenticator as well as an error. Then, this implementation could return an "not implemented" error. But actually, I am not a huge fan of introducing an error return just for the sake of covering "not implemented" errors.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestions.
I'm not so comfortable with it because it adds more code and introduces checks in other parts of our code.
In this particular case, it now returns a noAuth instance instead of a panic.


// AddAuthenticator A preemptive authorizer may only have a single authentication method
func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) {
panic("You're funny! A preemptive authorizer may only have a single authentication method")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here like with Clone. Maybe proceed here as I proposed above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would an user expect?

b:= &BasicAuth{user: "user", pw: "password"}
a:= NewPreemptiveAuth(b)
a.AddAuthenticator("fooAuth", func(...) (auth Authenticator, err error) {
    return &fooAuth{...}, nil
})

In this particular case I'm opt for the panic, because it's only a convenience method which has to be setup manually.

@Gamer92000
Copy link
Contributor

Hey, sorry to bother you. Could you get this resolved and merged? This is needed for #70 which in turn would benefit a lot of projects downstream (e.g. rmfakecloud).

@chripo chripo self-assigned this Jun 7, 2023
@chripo chripo merged commit 036581a into master Jun 22, 2023
@chripo chripo deleted the next branch June 22, 2023 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants