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