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
This commit is contained in:
parent
036581a6c8
commit
ea9c8490dd
4
auth.go
4
auth.go
@ -118,6 +118,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
|
||||
}
|
||||
|
||||
|
181
passportAuth.go
Normal file
181
passportAuth.go
Normal file
@ -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
|
||||
}
|
66
passportAuth_test.go
Normal file
66
passportAuth_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user