1// Package odrvcookie can fetch authentication cookies for a sharepoint webdav endpoint 2package odrvcookie 3 4import ( 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 24type CookieAuth struct { 25 user string 26 pass string 27 endpoint string 28} 29 30// CookieResponse contains the requested cookies 31type CookieResponse struct { 32 RtFa http.Cookie 33 FedAuth http.Cookie 34} 35 36// SharepointSuccessResponse holds a response from a successful microsoft login 37type 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 43type 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 52type SharepointError struct { 53 XMLName xml.Name `xml:"Envelope"` 54 Body ErrorResponseBody `xml:"Body"` 55} 56 57func (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 62type 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" 70const reqString = `<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" 71xmlns:a="http://www.w3.org/2005/08/addressing" 72xmlns: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 102func 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 114func (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 122func (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(spRoot.Scheme + "://" + 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 163func (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.NewRequestWithContext(ctx, "POST", "https://login.microsoftonline.com/extSTS.srf", buf) 180 if err != nil { 181 return nil, err 182 } 183 184 client := fshttp.NewClient(ctx) 185 resp, err := client.Do(req) 186 if err != nil { 187 return nil, errors.Wrap(err, "Error while logging in to endpoint") 188 } 189 defer fs.CheckClose(resp.Body, &err) 190 191 respBuf := bytes.Buffer{} 192 _, err = respBuf.ReadFrom(resp.Body) 193 if err != nil { 194 return nil, err 195 } 196 s := respBuf.Bytes() 197 198 conf = &SharepointSuccessResponse{} 199 err = xml.Unmarshal(s, conf) 200 if conf.Body.Token == "" { 201 // xml Unmarshal won't fail if the response doesn't contain a token 202 // However, the token will be empty 203 sErr := &SharepointError{} 204 205 errSErr := xml.Unmarshal(s, sErr) 206 if errSErr == nil { 207 return nil, sErr 208 } 209 } 210 211 if err != nil { 212 return nil, errors.Wrap(err, "Error while reading endpoint response") 213 } 214 return 215} 216