From 84b4c3655fea5cce37b3485c7b6888559baaff53 Mon Sep 17 00:00:00 2001 From: Julian Imhof Date: Sat, 11 Feb 2023 01:15:00 +0100 Subject: [PATCH] feat(auth): add support for MS-PASS typo: fix comment fix: fix verify flow and precompile cookies fix: implement requested changes fix: implement requested changes fix: implement requested changes test(auth): add test for MS-PASS fix: remove pointless return --- auth.go | 4 + passportAuth.go | 181 +++++++++++++++++++++++++++++++++++++++++++ passportAuth_test.go | 66 ++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 passportAuth.go create mode 100644 passportAuth_test.go diff --git a/auth.go b/auth.go index 3584360..7e1a0b6 100644 --- a/auth.go +++ b/auth.go @@ -108,6 +108,10 @@ func NewAutoAuth(login string, secret string) Authorizer { return NewDigestAuth(login, secret, rs) }) + az.AddAuthenticator("passport1.4", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) { + return NewPassportAuth(c, login, secret, rs.Request.URL.String(), &rs.Header) + }) + return az } diff --git a/passportAuth.go b/passportAuth.go new file mode 100644 index 0000000..5e55c53 --- /dev/null +++ b/passportAuth.go @@ -0,0 +1,181 @@ +package gowebdav + +import ( + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// PassportAuth structure holds our credentials +type PassportAuth struct { + user string + pw string + cookies []http.Cookie + inhibitRedirect bool +} + +// constructor for PassportAuth creates a new PassportAuth object and +// automatically authenticates against the given partnerURL +func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error) { + p := &PassportAuth{ + user: user, + pw: pw, + inhibitRedirect: true, + } + err := p.genCookies(c, partnerURL, header) + return p, err +} + +// Authorize the current request +func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + // prevent redirects to detect subsequent authentication requests + if p.inhibitRedirect { + rq.Header.Set(XInhibitRedirect, "1") + } else { + p.inhibitRedirect = true + } + for _, cookie := range p.cookies { + rq.AddCookie(&cookie) + } + return nil +} + +// Verify verifies if the authentication is good +func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + switch rs.StatusCode { + case 301, 302, 307, 308: + redo = true + if rs.Header.Get("Www-Authenticate") != "" { + // re-authentication required as we are redirected to the login page + err = p.genCookies(c, rs.Request.URL.String(), &rs.Header) + } else { + // just a redirect, follow it + p.inhibitRedirect = false + } + case 401: + err = NewPathError("Authorize", path, rs.StatusCode) + } + return +} + +// Close cleans up all resources +func (p *PassportAuth) Close() error { + return nil +} + +// Clone creates a Copy of itself +func (p *PassportAuth) Clone() Authenticator { + // create a copy to allow independent cookie updates + clonedCookies := make([]http.Cookie, len(p.cookies)) + copy(clonedCookies, p.cookies) + + return &PassportAuth{ + user: p.user, + pw: p.pw, + cookies: clonedCookies, + inhibitRedirect: true, + } +} + +// String toString +func (p *PassportAuth) String() string { + return fmt.Sprintf("PassportAuth login: %s", p.user) +} + +func (p *PassportAuth) genCookies(c *http.Client, partnerUrl string, header *http.Header) error { + // For more details refer to: + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pass/2c80637d-438c-4d4b-adc5-903170a779f3 + // Skipping step 1 and 2 as we already have the partner server challenge + + baseAuthenticationServer := header.Get("Location") + baseAuthenticationServerURL, err := url.Parse(baseAuthenticationServer) + if err != nil { + return err + } + + // Skipping step 3 and 4 as we already know that we need and have the user's credentials + // Step 5 (Sign-in request) + authenticationServerUrl := url.URL{ + Scheme: baseAuthenticationServerURL.Scheme, + Host: baseAuthenticationServerURL.Host, + Path: "/login2.srf", + } + + partnerServerChallenge := strings.Split(header.Get("Www-Authenticate"), " ")[1] + + req := http.Request{ + Method: "GET", + URL: &authenticationServerUrl, + Header: http.Header{ + "Authorization": []string{"Passport1.4 sign-in=" + url.QueryEscape(p.user) + ",pwd=" + url.QueryEscape(p.pw) + ",OrgVerb=GET,OrgUrl=" + partnerUrl + "," + partnerServerChallenge}, + }, + } + + rs, err := c.Do(&req) + if err != nil { + return err + } + io.Copy(io.Discard, rs.Body) + rs.Body.Close() + if rs.StatusCode != 200 { + return NewPathError("Authorize", "/", rs.StatusCode) + } + + // Step 6 (Token Response from Authentication Server) + tokenResponseHeader := rs.Header.Get("Authentication-Info") + if tokenResponseHeader == "" { + return NewPathError("Authorize", "/", 401) + } + tokenResponseHeaderList := strings.Split(tokenResponseHeader, ",") + token := "" + for _, tokenResponseHeader := range tokenResponseHeaderList { + if strings.HasPrefix(tokenResponseHeader, "from-PP='") { + token = tokenResponseHeader + break + } + } + if token == "" { + return NewPathError("Authorize", "/", 401) + } + + // Step 7 (First Authentication Request to Partner Server) + origUrl, err := url.Parse(partnerUrl) + if err != nil { + return err + } + req = http.Request{ + Method: "GET", + URL: origUrl, + Header: http.Header{ + "Authorization": []string{"Passport1.4 " + token}, + }, + } + + rs, err = c.Do(&req) + if err != nil { + return err + } + io.Copy(io.Discard, rs.Body) + rs.Body.Close() + if rs.StatusCode != 200 && rs.StatusCode != 302 { + return NewPathError("Authorize", "/", rs.StatusCode) + } + + // Step 8 (Set Token Message from Partner Server) + cookies := rs.Header.Values("Set-Cookie") + p.cookies = make([]http.Cookie, len(cookies)) + for i, cookie := range cookies { + cookieParts := strings.Split(cookie, ";") + cookieName := strings.Split(cookieParts[0], "=")[0] + cookieValue := strings.Split(cookieParts[0], "=")[1] + + p.cookies[i] = http.Cookie{ + Name: cookieName, + Value: cookieValue, + } + } + + return nil +} diff --git a/passportAuth_test.go b/passportAuth_test.go new file mode 100644 index 0000000..c53b5b8 --- /dev/null +++ b/passportAuth_test.go @@ -0,0 +1,66 @@ +package gowebdav + +import ( + "bytes" + "net/http" + "net/url" + "regexp" + "testing" +) + +// testing the creation is enough as it handles the authorization during init +func TestNewPassportAuth(t *testing.T) { + user := "user" + pass := "password" + p1 := "some,comma,separated,values" + token := "from-PP='token'" + + authHandler := func(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + reg, err := regexp.Compile("Passport1\\.4 sign-in=" + url.QueryEscape(user) + ",pwd=" + url.QueryEscape(pass) + ",OrgVerb=GET,OrgUrl=.*," + p1) + if err != nil { + t.Error(err) + } + if reg.MatchString(r.Header.Get("Authorization")) { + w.Header().Set("Authentication-Info", token) + w.WriteHeader(200) + return + } + } + } + authsrv, _, _ := newAuthSrv(t, authHandler) + defer authsrv.Close() + + dataHandler := func(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + reg, err := regexp.Compile("Passport1\\.4 " + token) + if err != nil { + t.Error(err) + } + if reg.MatchString(r.Header.Get("Authorization")) { + w.Header().Set("Set-Cookie", "Pass=port") + h.ServeHTTP(w, r) + return + } + for _, c := range r.Cookies() { + if c.Name == "Pass" && c.Value == "port" { + h.ServeHTTP(w, r) + return + } + } + w.Header().Set("Www-Authenticate", "Passport1.4 "+p1) + http.Redirect(w, r, authsrv.URL+"/", 302) + } + } + srv, _, _ := newAuthSrv(t, dataHandler) + defer srv.Close() + + cli := NewClient(srv.URL, user, pass) + data, err := cli.Read("/hello.txt") + if err != nil { + t.Errorf("got error=%v; want nil", err) + } + if !bytes.Equal(data, []byte("hello gowebdav\n")) { + t.Logf("got data=%v; want=hello gowebdav", data) + } +}