-
Notifications
You must be signed in to change notification settings - Fork 143
Expand file tree
/
Copy pathsession_api.go
More file actions
342 lines (294 loc) · 10.2 KB
/
session_api.go
File metadata and controls
342 lines (294 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
// Copyright 2013-Present Couchbase, Inc.
//
// Use of this software is governed by the Business Source License included
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
// in that file, in accordance with the Business Source License, use of this
// software will be governed by the Apache License, Version 2.0, included in
// the file licenses/APL2.txt.
package rest
import (
"net/http"
"time"
"github.com/couchbase/sync_gateway/auth"
"github.com/couchbase/sync_gateway/base"
"github.com/couchbase/sync_gateway/channels"
"github.com/couchbase/sync_gateway/db"
)
const kDefaultSessionTTL = 24 * time.Hour
// Respond with a JSON struct containing info about the current login session
func (h *handler) respondWithSessionInfo() error {
response := h.formatSessionResponse(h.user)
h.writeJSON(response)
return nil
}
// GET /_session returns info about the current user
func (h *handler) handleSessionGET() error {
return h.respondWithSessionInfo()
}
// POST /_session creates a login session and sets its cookie
func (h *handler) handleSessionPOST() error {
// CORS not allowed for login #115 #762
originHeader := h.rq.Header["Origin"]
if len(originHeader) > 0 {
matched := ""
if h.server.Config.API.CORS != nil {
matched = auth.MatchedOrigin(h.server.Config.API.CORS.LoginOrigin, originHeader)
}
if matched == "" {
return base.HTTPErrorf(http.StatusBadRequest, "No CORS")
}
}
// NOTE: handleSessionPOST doesn't handle creating users from OIDC - checkPublicAuth calls out into AuthenticateUntrustedJWT.
// Therefore, if by this point `h.user` is guest, this isn't creating a session from OIDC.
if h.db.Options.DisablePasswordAuthentication && (h.user == nil || h.user.Name() == "") {
return ErrLoginRequired
}
user, err := h.getUserFromSessionRequestBody()
// If we fail to get a user from the body and we've got a non-GUEST authenticated user, create the session based on that user
if user == nil && h.user != nil && h.user.Name() != "" {
return h.makeSession(h.user)
} else {
if err != nil {
return err
}
return h.makeSession(user)
}
}
func (h *handler) getUserFromSessionRequestBody() (auth.User, error) {
var params struct {
Name string `json:"name"`
Password string `json:"password"`
}
err := h.readJSONInto(¶ms)
if err != nil {
return nil, err
}
var user auth.User
user, err = h.db.Authenticator(h.ctx()).GetUser(params.Name)
if err != nil {
return nil, err
}
if user == nil {
base.InfofCtx(h.ctx(), base.KeyAuth, "Couldn't create session for user %q: not found", base.UD(params.Name))
return nil, nil
}
authenticated, reason := user.AuthenticateWithReason(params.Password)
if !authenticated {
base.InfofCtx(h.ctx(), base.KeyAuth, "Couldn't create session for user %q: %s", base.UD(params.Name), reason)
return nil, nil
}
return user, nil
}
// DELETE /_session logs out the current session
func (h *handler) handleSessionDELETE() error {
// CORS not allowed for login #115 #762
originHeader := h.rq.Header["Origin"]
if len(originHeader) > 0 {
matched := ""
if h.server.Config.API.CORS != nil {
matched = auth.MatchedOrigin(h.server.Config.API.CORS.LoginOrigin, originHeader)
}
if matched == "" {
return base.HTTPErrorf(http.StatusBadRequest, "No CORS")
}
}
cookie := h.db.Authenticator(h.ctx()).DeleteSessionForCookie(h.ctx(), h.rq)
if cookie == nil {
return base.HTTPErrorf(http.StatusNotFound, "no session")
}
http.SetCookie(h.response, cookie)
return nil
}
func (h *handler) makeSession(user auth.User) error {
_, err := h.makeSessionWithTTL(user, kDefaultSessionTTL)
if err != nil {
return err
}
return h.respondWithSessionInfo()
}
// Creates a session with TTL and adds to the response. Does NOT return the session info response.
func (h *handler) makeSessionWithTTL(user auth.User, expiry time.Duration) (sessionID string, err error) {
if user == nil {
return "", ErrInvalidLogin
}
h.user = user
auth := h.db.Authenticator(h.ctx())
session, err := auth.CreateSession(h.ctx(), h.user, expiry)
if err != nil {
return "", err
}
cookie := auth.MakeSessionCookie(session, h.db.Options.SecureCookieOverride, h.db.Options.SessionCookieHttpOnly)
base.AddDbPathToCookie(h.rq, cookie)
http.SetCookie(h.response, cookie)
return session.ID, nil
}
// MakeSessionFromUserAndEmail first attempts to find the user by username. If found, updates the users's
// email if different. If no match for username, attempts to find by the user by email.
// If not found, and createUserIfNeeded=true, creates a new user based on username, email.
func (h *handler) makeSessionFromNameAndEmail(username, email string, createUserIfNeeded bool) error {
// First attempt lookup by username and make a login session for her.
user, err := h.db.Authenticator(h.ctx()).GetUser(username)
if err != nil {
return err
}
// Attempt email updates/lookups if an email is provided.
if len(email) > 0 {
if user != nil {
// User found, check whether the email needs to be updated
// (e.g. user has changed email in external auth system)
if email != user.Email() {
if err = h.db.Authenticator(h.ctx()).UpdateUserEmail(user, email); err != nil {
// Failure to update email during session creation is non-critical, log and continue.
base.InfofCtx(h.ctx(), base.KeyAuth, "Unable to update email for user %s during session creation. Session will still be created. Error:%v,", base.UD(username), err)
}
}
} else {
// User not found by username. Attempt user lookup by email. This provides backward
// compatibility for users that were originally created with id = email
if user, err = h.db.Authenticator(h.ctx()).GetUserByEmail(email); err != nil {
return err
}
}
}
// Couldn't find existing user.
if user == nil {
if !createUserIfNeeded {
return base.HTTPErrorf(http.StatusUnauthorized, "No such user")
}
// Create a User with the given username, email address, and a random password.
// CAS mismatch indicates the user has been created by another request underneath us, can continue with session creation
user, err = h.db.Authenticator(h.ctx()).RegisterNewUser(username, email)
if err != nil && !base.IsCasMismatch(err) {
return err
}
}
return h.makeSession(user)
}
// ADMIN API: Generates a login session for a user and returns the session ID and cookie name.
func (h *handler) createUserSession() error {
h.assertAdminOnly()
var params struct {
Name string `json:"name"`
TTL int `json:"ttl"`
}
params.TTL = int(kDefaultSessionTTL / time.Second)
err := h.readJSONInto(¶ms)
if err != nil {
return err
} else if params.Name == "" || params.Name == base.GuestUsername || !auth.IsValidPrincipalName(params.Name) {
return base.HTTPErrorf(http.StatusBadRequest, "Invalid or missing user name")
}
authenticator := h.db.Authenticator(h.ctx())
user, err := authenticator.GetUser(params.Name)
if user == nil {
if err == nil {
err = base.HTTPErrorf(http.StatusNotFound, "No such user %q", params.Name)
}
return err
}
ttl := time.Duration(params.TTL) * time.Second
if ttl < 1.0 {
return base.HTTPErrorf(http.StatusBadRequest, "Invalid or missing ttl")
}
session, err := authenticator.CreateSession(h.ctx(), user, ttl)
if err != nil {
return err
}
var response struct {
SessionID string `json:"session_id"`
Expires string `json:"expires"`
CookieName string `json:"cookie_name"`
}
response.SessionID = session.ID
response.Expires = session.Expiration.UTC().Format(time.RFC3339)
response.CookieName = authenticator.SessionCookieName
h.writeJSON(response)
return nil
}
func (h *handler) getUserSession() error {
h.assertAdminOnly()
session, err := h.db.Authenticator(h.ctx()).GetSession(h.PathVar("sessionid"))
if err != nil {
return err
}
return h.respondWithSessionInfoForSession(session)
}
// ADMIN API: Deletes a specified session. If username is present on the request, validates
// that the session being deleted is associated with the user.
func (h *handler) deleteUserSession() error {
h.assertAdminOnly()
userName := h.PathVar("name")
if userName != "" {
return h.deleteUserSessionWithValidation(h.PathVar("sessionid"), userName)
} else {
return h.db.Authenticator(h.ctx()).DeleteSession(h.ctx(), h.PathVar("sessionid"), "")
}
}
// ADMIN API: Deletes all sessions for a user
func (h *handler) deleteUserSessions() error {
h.assertAdminOnly()
userName := h.PathVar("name")
auth := h.db.Authenticator(h.ctx())
user, err := auth.GetUser(userName)
if err != nil {
return err
}
if user == nil {
return nil
}
user.UpdateSessionUUID()
user.SetUpdatedAt()
err = auth.Save(user)
if err == nil {
base.Audit(h.ctx(), base.AuditIDPublicUserSessionDeleteAll, base.AuditFields{base.AuditFieldUserName: userName})
}
return err
}
// Delete a session if associated with the user provided
func (h *handler) deleteUserSessionWithValidation(sessionId string, userName string) error {
// Validate that the session being deleted belongs to the user. This adds some
// overhead - for user-agnostic session deletion should use deleteSession
session, getErr := h.db.Authenticator(h.ctx()).GetSession(sessionId)
if getErr != nil {
return getErr
}
if session.Username == userName {
delErr := h.db.Authenticator(h.ctx()).DeleteSession(h.ctx(), sessionId, userName)
if delErr != nil {
return delErr
}
} else {
return kNotFoundError
}
return nil
}
// Respond with a JSON struct containing info about the current login session
func (h *handler) respondWithSessionInfoForSession(session *auth.LoginSession) error {
user, err := h.db.Authenticator(h.ctx()).GetUser(session.Username)
// let the empty user case succeed
if err != nil {
return err
}
response := h.formatSessionResponse(user)
if response != nil {
h.writeJSON(response)
}
return nil
}
// Formats session response similar to what is returned by CouchDB
func (h *handler) formatSessionResponse(user auth.User) db.Body {
var name *string
allChannels := channels.TimedSet{}
if user != nil {
userName := user.Name()
if userName != "" {
name = &userName
}
allChannels = user.Channels()
}
// Return a JSON struct similar to what CouchDB returns:
userCtx := db.Body{"name": name, "channels": allChannels}
handlers := []string{"default", "cookie"}
response := db.Body{"ok": true, "userCtx": userCtx, "authentication_handlers": handlers}
return response
}