auth.go 7.26 KB
Newer Older
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
1
2
3
package auth

import (
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
4
	"context"
5
	"encoding/base64"
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
6
7
8
	"encoding/json"
	"errors"
	"fmt"
9
	"net"
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
10
11
	"net/http"
	"os"
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
12
13
	"strings"
	"time"
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
14

15
	"github.com/nicolaspernoud/vestibule/pkg/common"
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
16
	"github.com/nicolaspernoud/vestibule/pkg/tokens"
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
17
18
)

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
19
20
type key int

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
21
22
const (
	authTokenKey string = "auth_token"
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
23
24
	// ContextData is the user
	ContextData key = 0
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
25
26
)

27
28
var (
	// AdminRole represents the role reserved for admins
29
30
	AdminRole = common.StringValueFromEnv("ADMIN_ROLE", "ADMINS")
	hostname  = common.StringValueFromEnv("HOSTNAME", "vestibule.127.0.0.1.nip.io")
31
32
)

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
33
34
// User represents a logged in user
type User struct {
35
	ID           string   `json:"id,omitempty"`
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
36
37
	Login        string   `json:"login"`
	DisplayName  string   `json:"displayName,omitempty"`
38
	Email        string   `json:"email,omitempty"`
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
39
40
41
42
43
44
45
46
	Roles        []string `json:"memberOf"`
	IsAdmin      bool     `json:"isAdmin,omitempty"`
	Name         string   `json:"name,omitempty"`
	Surname      string   `json:"surname,omitempty"`
	PasswordHash string   `json:"passwordHash,omitempty"`
	Password     string   `json:"password,omitempty"`
}

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
47
48
49
50
51
52
53
54
55
56
57
// TokenData represents the data held into a token
type TokenData struct {
	User
	URL              string `json:"url,omitempty"`
	ReadOnly         bool   `json:"readonly,omitempty"`
	SharingUserLogin string `json:"sharinguserlogin,omitempty"`
	XSRFToken        string `json:"xsrftoken,omitempty"`
}

// ValidateAuthMiddleware validates that the token is valid and that the user has the correct roles
func ValidateAuthMiddleware(next http.Handler, allowedRoles []string, checkXSRF bool) http.Handler {
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
58
	roleChecker := func(w http.ResponseWriter, r *http.Request) {
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
59
		user := TokenData{}
Nicolas Pernoud's avatar
Nicolas Pernoud committed
60
		checkXSRF, err := tokens.ExtractAndValidateToken(r, authTokenKey, &user, checkXSRF)
61
62
63
64
65
		// Handle WebDav authentication
		if err != nil && isWebdav(r.UserAgent()) {
			// Test if the user password is directly given in the request, if so populate the user
			user, err = getUserDirectly(r.Header.Get("Authorization"))
			if err != nil {
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
66
				w.Header().Set("WWW-Authenticate", `Basic realm="server"`)
67
				http.Error(w, "webdav client authentication", http.StatusUnauthorized)
68
				return
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
69
			}
70
71
		}
		if err != nil {
72
73
74
75
76
			// Handle CORS preflight requests
			if r.Method == "OPTIONS" {
				return
			}
			// Default to redirect to authentication
77
			redirectTo := hostname
78
79
80
81
			_, port, perr := net.SplitHostPort(r.Host)
			if perr == nil {
				redirectTo += ":" + port
			}
82
			// Write the requested url in a cookie
83
84
			if r.Host != redirectTo && r.URL.Path != "/favicon.ico" {
				cookie := http.Cookie{Name: "redirectAfterLogin", Path: "/", Domain: hostname, Value: r.Host + r.URL.Path + "?" + r.URL.RawQuery, MaxAge: 30, Secure: true, HttpOnly: false, SameSite: http.SameSiteLaxMode}
85
86
				http.SetCookie(w, &cookie)
			}
87
88
			w.Header().Set("Content-Type", "text/html")
			w.WriteHeader(http.StatusUnauthorized)
89
90
			responseContent := fmt.Sprintf("error extracting token: %v<meta http-equiv=\"Refresh\" content=\"0; url=https://%v#login\"/>", err.Error(), redirectTo)
			fmt.Fprint(w, responseContent)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
91
			return
92

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
93
94
		}
		// Check XSRF Token
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
95
		if checkXSRF && r.Header.Get("XSRF-TOKEN") != user.XSRFToken {
96
			http.Error(w, "XSRF protection triggered", http.StatusUnauthorized)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
97
98
99
100
			return
		}
		err = checkUserHasRole(user, allowedRoles)
		if err != nil {
101
			http.Error(w, err.Error(), http.StatusForbidden)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
102
103
			return
		}
104
		err = checkUserHasRole(user, []string{AdminRole})
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
105
106
107
108
		if err == nil {
			user.IsAdmin = true
		}
		// Check for url
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
109
110
111
		if user.URL != "" {
			requestURL := strings.Split(r.Host, ":")[0] + r.URL.EscapedPath()
			if user.URL != requestURL {
112
				http.Error(w, "token restricted to url: "+user.URL, http.StatusUnauthorized)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
113
114
				return
			}
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
115
116
117
		}
		// Check for method
		if user.ReadOnly && r.Method != http.MethodGet {
118
			http.Error(w, "token is read only", http.StatusForbidden)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
119
120
121
122
			return
		}
		ctx := context.WithValue(r.Context(), ContextData, user)
		next.ServeHTTP(w, r.WithContext(ctx))
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
123
	}
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
124
	return http.HandlerFunc(roleChecker)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
125
126
}

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
127
128
// HandleLogout remove the user from the cookie store
func (m Manager) HandleLogout(w http.ResponseWriter, r *http.Request) {
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
129
130
131
	// Delete the auth cookie
	c := http.Cookie{
		Name:   authTokenKey,
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
132
		Domain: m.Hostname,
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
133
134
135
136
137
138
		MaxAge: -1,
	}
	http.SetCookie(w, &c)
	http.Redirect(w, r, os.Getenv("LOGOUT_URL"), http.StatusTemporaryRedirect)
}

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
139
// WhoAmI returns the user data
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
140
141
142
143
func WhoAmI() http.Handler {
	whoAmI := func(w http.ResponseWriter, r *http.Request) {
		user, err := GetTokenData(r)
		if err != nil {
144
			http.Error(w, err.Error(), http.StatusBadRequest)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
145
146
147
			return
		}
		json.NewEncoder(w).Encode(user)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
148
	}
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
149
	return http.HandlerFunc(whoAmI)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
150
151
}

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
152
// checkUserHasRole checks if the user has the required role
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
153
func checkUserHasRole(user TokenData, allowedRoles []string) error {
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
154
	for _, allowedRole := range allowedRoles {
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
155
156
157
		if allowedRole == "*" {
			return nil
		}
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
158
159
160
161
162
163
164
165
166
		for _, userRole := range user.Roles {
			if userRole != "" && (userRole == allowedRole) {
				return nil
			}
		}
	}
	return fmt.Errorf("no user role among %v is in allowed roles (%v)", user.Roles, allowedRoles)
}

Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
167
168
169
170
//GetShareToken gets a share token for a given ressource
func GetShareToken(w http.ResponseWriter, r *http.Request) {
	user, err := GetTokenData(r)
	if err != nil {
171
		http.Error(w, err.Error(), http.StatusBadRequest)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
172
173
174
175
		return
	}

	if r.Method != http.MethodPost {
176
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
177
178
179
180
181
182
183
184
185
		return
	}
	var wantedToken struct {
		Sharedfor string `json:"sharedfor"`
		URL       string `json:"url"`
		Lifespan  int    `json:"lifespan"`
		ReadOnly  bool   `json:"readonly,omitempty"`
	}
	err = json.NewDecoder(r.Body).Decode(&wantedToken)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
186
	if err != nil {
187
		http.Error(w, err.Error(), http.StatusBadRequest)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
188
189
190
		return
	}
	if wantedToken.URL == "" {
191
		http.Error(w, "url cannot be empty", http.StatusBadRequest)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
192
		return
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
193
	}
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
194
195
196
197
	user.Login = user.Login + "_share_for_" + wantedToken.Sharedfor
	user.URL = wantedToken.URL
	user.ReadOnly = wantedToken.ReadOnly
	user.SharingUserLogin = wantedToken.Sharedfor
Nicolas Pernoud's avatar
Nicolas Pernoud committed
198
	token, err := tokens.CreateToken(user, time.Now().Add(time.Hour*time.Duration(24*wantedToken.Lifespan)))
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
199
	if err != nil {
200
		http.Error(w, err.Error(), http.StatusBadRequest)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
201
		return
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
202
	}
203
	fmt.Fprint(w, token)
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
204
205
206
207
208
209
}

// GetTokenData gets an user from a request
func GetTokenData(r *http.Request) (TokenData, error) {
	user, ok := r.Context().Value(ContextData).(TokenData)
	if !ok {
210
		return user, errors.New("user could not be got from context")
Nicolas PERNOUD's avatar
Nicolas PERNOUD committed
211
212
213
	}
	return user, nil
}
214
215
216

// isWebdav works out if an user agent is a webdav user agent
func isWebdav(ua string) bool {
217
	for _, a := range []string{"vfs", "Microsoft-WebDAV", "Konqueror", "LibreOffice", "Rei.Fs.WebDAV"} {
218
219
220
221
222
223
		if strings.Contains(ua, a) {
			return true
		}
	}
	return false
}
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241

// getUserDirectly directly checks if an user is allowed to connect
func getUserDirectly(authorizationHeader string) (TokenData, error) {
	authHeader := strings.Split(authorizationHeader, " ")
	var user User
	if authHeader[0] == "Basic" && len(authHeader) == 2 {
		decoded, err := base64.StdEncoding.DecodeString(authHeader[1])
		if err == nil {
			auth := strings.Split(string(decoded), ":")
			sentUser := User{Login: auth[0], Password: auth[1]}
			foundUser, err := MatchUser(sentUser)
			if err == nil {
				return (TokenData{User: foundUser}), nil
			}
		}
	}
	return TokenData{User: user}, errors.New("could not retrieve user directly from basic auth header")
}