github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/webdav/odrvcookie/fetch.go (about)

     1  // Package odrvcookie can fetch authentication cookies for a sharepoint webdav endpoint
     2  package odrvcookie
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"encoding/xml"
     8  	"fmt"
     9  	"html/template"
    10  	"net/http"
    11  	"net/http/cookiejar"
    12  	"net/url"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/pkg/errors"
    17  	"github.com/rclone/rclone/fs"
    18  	"github.com/rclone/rclone/fs/fshttp"
    19  	"golang.org/x/net/publicsuffix"
    20  )
    21  
    22  // CookieAuth hold the authentication information
    23  // These are username and password as well as the authentication endpoint
    24  type CookieAuth struct {
    25  	user     string
    26  	pass     string
    27  	endpoint string
    28  }
    29  
    30  // CookieResponse contains the requested cookies
    31  type CookieResponse struct {
    32  	RtFa    http.Cookie
    33  	FedAuth http.Cookie
    34  }
    35  
    36  // SharepointSuccessResponse holds a response from a successful microsoft login
    37  type SharepointSuccessResponse struct {
    38  	XMLName xml.Name            `xml:"Envelope"`
    39  	Body    SuccessResponseBody `xml:"Body"`
    40  }
    41  
    42  // SuccessResponseBody is the body of a successful response, it holds the token
    43  type SuccessResponseBody struct {
    44  	XMLName xml.Name
    45  	Type    string    `xml:"RequestSecurityTokenResponse>TokenType"`
    46  	Created time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Created"`
    47  	Expires time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Expires"`
    48  	Token   string    `xml:"RequestSecurityTokenResponse>RequestedSecurityToken>BinarySecurityToken"`
    49  }
    50  
    51  // SharepointError holds an error response microsoft login
    52  type SharepointError struct {
    53  	XMLName xml.Name          `xml:"Envelope"`
    54  	Body    ErrorResponseBody `xml:"Body"`
    55  }
    56  
    57  func (e *SharepointError) Error() string {
    58  	return fmt.Sprintf("%s: %s (%s)", e.Body.FaultCode, e.Body.Reason, e.Body.Detail)
    59  }
    60  
    61  // ErrorResponseBody contains the body of an erroneous response
    62  type ErrorResponseBody struct {
    63  	XMLName   xml.Name
    64  	FaultCode string `xml:"Fault>Code>Subcode>Value"`
    65  	Reason    string `xml:"Fault>Reason>Text"`
    66  	Detail    string `xml:"Fault>Detail>error>internalerror>text"`
    67  }
    68  
    69  // reqString is a template that gets populated with the user data in order to retrieve a "BinarySecurityToken"
    70  const reqString = `<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
    71  xmlns:a="http://www.w3.org/2005/08/addressing"
    72  xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    73  <s:Header>
    74  <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
    75  <a:ReplyTo>
    76  <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    77  </a:ReplyTo>
    78  <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>
    79  <o:Security s:mustUnderstand="1"
    80   xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    81  <o:UsernameToken>
    82    <o:Username>{{ .Username }}</o:Username>
    83    <o:Password>{{ .Password }}</o:Password>
    84  </o:UsernameToken>
    85  </o:Security>
    86  </s:Header>
    87  <s:Body>
    88  <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
    89  <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
    90    <a:EndpointReference>
    91      <a:Address>{{ .Address }}</a:Address>
    92    </a:EndpointReference>
    93  </wsp:AppliesTo>
    94  <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
    95  <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
    96  <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
    97  </t:RequestSecurityToken>
    98  </s:Body>
    99  </s:Envelope>`
   100  
   101  // New creates a new CookieAuth struct
   102  func New(pUser, pPass, pEndpoint string) CookieAuth {
   103  	retStruct := CookieAuth{
   104  		user:     pUser,
   105  		pass:     pPass,
   106  		endpoint: pEndpoint,
   107  	}
   108  
   109  	return retStruct
   110  }
   111  
   112  // Cookies creates a CookieResponse. It fetches the auth token and then
   113  // retrieves the Cookies
   114  func (ca *CookieAuth) Cookies(ctx context.Context) (*CookieResponse, error) {
   115  	tokenResp, err := ca.getSPToken(ctx)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	return ca.getSPCookie(tokenResp)
   120  }
   121  
   122  func (ca *CookieAuth) getSPCookie(conf *SharepointSuccessResponse) (*CookieResponse, error) {
   123  	spRoot, err := url.Parse(ca.endpoint)
   124  	if err != nil {
   125  		return nil, errors.Wrap(err, "Error while constructing endpoint URL")
   126  	}
   127  
   128  	u, err := url.Parse("https://" + spRoot.Host + "/_forms/default.aspx?wa=wsignin1.0")
   129  	if err != nil {
   130  		return nil, errors.Wrap(err, "Error while constructing login URL")
   131  	}
   132  
   133  	// To authenticate with davfs or anything else we need two cookies (rtFa and FedAuth)
   134  	// In order to get them we use the token we got earlier and a cookieJar
   135  	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	client := &http.Client{
   141  		Jar: jar,
   142  	}
   143  
   144  	// Send the previously acquired Token as a Post parameter
   145  	if _, err = client.Post(u.String(), "text/xml", strings.NewReader(conf.Body.Token)); err != nil {
   146  		return nil, errors.Wrap(err, "Error while grabbing cookies from endpoint: %v")
   147  	}
   148  
   149  	cookieResponse := CookieResponse{}
   150  	for _, cookie := range jar.Cookies(u) {
   151  		if (cookie.Name == "rtFa") || (cookie.Name == "FedAuth") {
   152  			switch cookie.Name {
   153  			case "rtFa":
   154  				cookieResponse.RtFa = *cookie
   155  			case "FedAuth":
   156  				cookieResponse.FedAuth = *cookie
   157  			}
   158  		}
   159  	}
   160  	return &cookieResponse, nil
   161  }
   162  
   163  func (ca *CookieAuth) getSPToken(ctx context.Context) (conf *SharepointSuccessResponse, err error) {
   164  	reqData := map[string]interface{}{
   165  		"Username": ca.user,
   166  		"Password": ca.pass,
   167  		"Address":  ca.endpoint,
   168  	}
   169  
   170  	t := template.Must(template.New("authXML").Parse(reqString))
   171  
   172  	buf := &bytes.Buffer{}
   173  	if err := t.Execute(buf, reqData); err != nil {
   174  		return nil, errors.Wrap(err, "Error while filling auth token template")
   175  	}
   176  
   177  	// Create and execute the first request which returns an auth token for the sharepoint service
   178  	// With this token we can authenticate on the login page and save the returned cookies
   179  	req, err := http.NewRequest("POST", "https://login.microsoftonline.com/extSTS.srf", buf)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
   184  
   185  	client := fshttp.NewClient(fs.Config)
   186  	resp, err := client.Do(req)
   187  	if err != nil {
   188  		return nil, errors.Wrap(err, "Error while logging in to endpoint")
   189  	}
   190  	defer fs.CheckClose(resp.Body, &err)
   191  
   192  	respBuf := bytes.Buffer{}
   193  	_, err = respBuf.ReadFrom(resp.Body)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	s := respBuf.Bytes()
   198  
   199  	conf = &SharepointSuccessResponse{}
   200  	err = xml.Unmarshal(s, conf)
   201  	if conf.Body.Token == "" {
   202  		// xml Unmarshal won't fail if the response doesn't contain a token
   203  		// However, the token will be empty
   204  		sErr := &SharepointError{}
   205  
   206  		errSErr := xml.Unmarshal(s, sErr)
   207  		if errSErr == nil {
   208  			return nil, sErr
   209  		}
   210  	}
   211  
   212  	if err != nil {
   213  		return nil, errors.Wrap(err, "Error while reading endpoint response")
   214  	}
   215  	return
   216  }