ea9c8490dd
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
182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
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
|
|
}
|