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