// Package odrvcookie can fetch authentication cookies for a sharepoint webdav endpoint package odrvcookie import ( "bytes" "context" "encoding/xml" "fmt" "html/template" "net/http" "net/http/cookiejar" "net/url" "strings" "time" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/fshttp" "golang.org/x/net/publicsuffix" ) // CookieAuth hold the authentication information // These are username and password as well as the authentication endpoint type CookieAuth struct { user string pass string endpoint string } // CookieResponse contains the requested cookies type CookieResponse struct { RtFa http.Cookie FedAuth http.Cookie } // SharepointSuccessResponse holds a response from a successful microsoft login type SharepointSuccessResponse struct { XMLName xml.Name `xml:"Envelope"` Body SuccessResponseBody `xml:"Body"` } // SuccessResponseBody is the body of a successful response, it holds the token type SuccessResponseBody struct { XMLName xml.Name Type string `xml:"RequestSecurityTokenResponse>TokenType"` Created time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Created"` Expires time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Expires"` Token string `xml:"RequestSecurityTokenResponse>RequestedSecurityToken>BinarySecurityToken"` } // SharepointError holds an error response microsoft login type SharepointError struct { XMLName xml.Name `xml:"Envelope"` Body ErrorResponseBody `xml:"Body"` } func (e *SharepointError) Error() string { return fmt.Sprintf("%s: %s (%s)", e.Body.FaultCode, e.Body.Reason, e.Body.Detail) } // ErrorResponseBody contains the body of an erroneous response type ErrorResponseBody struct { XMLName xml.Name FaultCode string `xml:"Fault>Code>Subcode>Value"` Reason string `xml:"Fault>Reason>Text"` Detail string `xml:"Fault>Detail>error>internalerror>text"` } // reqString is a template that gets populated with the user data in order to retrieve a "BinarySecurityToken" const reqString = ` http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue http://www.w3.org/2005/08/addressing/anonymous {{ .SPTokenURL }} {{ .Username }} {{ .Password }} {{ .Address }} http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey http://schemas.xmlsoap.org/ws/2005/02/trust/Issue urn:oasis:names:tc:SAML:1.0:assertion ` // New creates a new CookieAuth struct func New(pUser, pPass, pEndpoint string) CookieAuth { retStruct := CookieAuth{ user: pUser, pass: pPass, endpoint: pEndpoint, } return retStruct } // Cookies creates a CookieResponse. It fetches the auth token and then // retrieves the Cookies func (ca *CookieAuth) Cookies(ctx context.Context) (*CookieResponse, error) { tokenResp, err := ca.getSPToken(ctx) if err != nil { return nil, err } return ca.getSPCookie(tokenResp) } func (ca *CookieAuth) getSPCookie(conf *SharepointSuccessResponse) (*CookieResponse, error) { spRoot, err := url.Parse(ca.endpoint) if err != nil { return nil, fmt.Errorf("error while constructing endpoint URL: %w", err) } u, err := url.Parse(spRoot.Scheme + "://" + spRoot.Host + "/_forms/default.aspx?wa=wsignin1.0") if err != nil { return nil, fmt.Errorf("error while constructing login URL: %w", err) } // To authenticate with davfs or anything else we need two cookies (rtFa and FedAuth) // In order to get them we use the token we got earlier and a cookieJar jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) if err != nil { return nil, err } client := &http.Client{ Jar: jar, } // Send the previously acquired Token as a Post parameter if _, err = client.Post(u.String(), "text/xml", strings.NewReader(conf.Body.Token)); err != nil { return nil, fmt.Errorf("error while grabbing cookies from endpoint: %w", err) } cookieResponse := CookieResponse{} for _, cookie := range jar.Cookies(u) { if (cookie.Name == "rtFa") || (cookie.Name == "FedAuth") { switch cookie.Name { case "rtFa": cookieResponse.RtFa = *cookie case "FedAuth": cookieResponse.FedAuth = *cookie } } } return &cookieResponse, nil } var spTokenURLMap = map[string]string{ "com": "https://login.microsoftonline.com", "cn": "https://login.chinacloudapi.cn", "us": "https://login.microsoftonline.us", "de": "https://login.microsoftonline.de", } func getSPTokenURL(endpoint string) (string, error) { spRoot, err := url.Parse(endpoint) if err != nil { return "", fmt.Errorf("error while parse endpoint: %w", err) } domains := strings.Split(spRoot.Host, ".") tld := domains[len(domains)-1] spTokenURL, ok := spTokenURLMap[tld] if !ok { return "", fmt.Errorf("error while get SPToken url, unsupported tld: %s", tld) } return spTokenURL + "/extSTS.srf", nil } func (ca *CookieAuth) getSPToken(ctx context.Context) (conf *SharepointSuccessResponse, err error) { spTokenURL, err := getSPTokenURL(ca.endpoint) if err != nil { return nil, err } reqData := map[string]interface{}{ "Username": ca.user, "Password": ca.pass, "Address": ca.endpoint, "SPTokenURL": spTokenURL, } t := template.Must(template.New("authXML").Parse(reqString)) buf := &bytes.Buffer{} if err := t.Execute(buf, reqData); err != nil { return nil, fmt.Errorf("error while filling auth token template: %w", err) } // Create and execute the first request which returns an auth token for the sharepoint service // With this token we can authenticate on the login page and save the returned cookies req, err := http.NewRequestWithContext(ctx, "POST", spTokenURL, buf) if err != nil { return nil, err } client := fshttp.NewClient(ctx) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error while logging in to endpoint: %w", err) } defer fs.CheckClose(resp.Body, &err) respBuf := bytes.Buffer{} _, err = respBuf.ReadFrom(resp.Body) if err != nil { return nil, err } s := respBuf.Bytes() conf = &SharepointSuccessResponse{} err = xml.Unmarshal(s, conf) if conf.Body.Token == "" { // xml Unmarshal won't fail if the response doesn't contain a token // However, the token will be empty sErr := &SharepointError{} errSErr := xml.Unmarshal(s, sErr) if errSErr == nil { return nil, sErr } } if err != nil { return nil, fmt.Errorf("error while reading endpoint response: %w", err) } return }