Source file ezSessionServer.ml
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
open EzAPIServerUtils
open EzSession.TYPES
open Lwt.Infix
let max_challenges = 10_000
let challenge_size = 30
let initial_hashtbl_size = 100
exception UserAlreadyDefined
exception NoPasswordProvided
let randomChars =
"abcdefghijklmnopqrstuvxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let randomCharsLen = String.length randomChars
let random_challenge () =
String.init challenge_size (fun _ -> randomChars.[Random.int randomCharsLen])
module type SessionStore = sig
type user_id
val create_session : ?foreign:foreign_info ->
login:string -> req:Req.t -> user_id -> user_id session Lwt.t
val get_session : ?req:Req.t -> string -> user_id session option Lwt.t
val remove_session : user_id -> token:string -> unit Lwt.t
end
module type Arg = sig
module SessionArg : EzSession.TYPES.SessionArg
module SessionStore : SessionStore with type user_id = SessionArg.user_id
val find_user : login:string ->
(string option * SessionArg.user_id * SessionArg.user_info) option Lwt.t
val check_foreign :
origin:string -> token:string ->
(string, int * string option) result Lwt.t
val register_foreign :
origin:string -> token:string ->
(SessionArg.user_id * SessionArg.user_info option, int * string option) result Lwt.t
end
let default_check_foreign ~origin ~token =
ignore (origin, token);
Lwt.return (Error (400, Some "Foreign authentication not implemented"))
let default_register_foreign ~origin ~token =
ignore (origin, token);
Lwt.return (Error (400, Some "Foreign registration not implemented"))
module Make(S: Arg) : sig
val register_handlers :
Directory.t -> Directory.t
val get_request_session :
Req.t -> S.SessionArg.user_id session option Lwt.t
end = struct
let find_user = S.find_user
let check_foreign = S.check_foreign
let register_foreign = S.register_foreign
open S.SessionStore
module S = S.SessionArg
module M = EzSession.Make(S)
include M
let cookie_of_param req (`Query { EzAPI.Security.name = param ; _}) =
Req.find_param param req
let cookie_of_cookie req (`Cookie { EzAPI.Security.name ; _ }) =
try Some (StringMap.find name (EzCookieServer.get req))
with Not_found -> None
let req (`Header { EzAPI.Security.name ; _ }) =
let name = String.lowercase_ascii name in
match StringMap.find name req.Req.req_headers with
| exception Not_found -> None
| [] -> None
| cookie :: _ -> Some cookie
let get_request_session security req =
List.map (function
| `Query _ as s -> cookie_of_param req s
| `Cookie _ as s -> cookie_of_cookie req s
| `Header _ as s -> cookie_of_header req s
) security
|> Lwt_list.fold_left_s (function
| Some s -> fun _ -> Lwt.return_some s
| None -> function
| None -> Lwt.return_none
| Some cookie -> get_session ~req cookie
) None
module Handler = struct
let challenges = Hashtbl.create initial_hashtbl_size
let challenge_queue = Queue.create ()
let rec new_challenge req =
let challenge_id = random_challenge () in
if Hashtbl.mem challenges challenge_id then
new_challenge req
else
let challenge = random_challenge () in
if Queue.length challenge_queue > max_challenges then begin
let challenge_id = Queue.take challenge_queue in
Hashtbl.remove challenges challenge_id
end;
Hashtbl.add challenges challenge_id (challenge, req.Req.req_time);
Queue.add challenge_id challenge_queue;
{ challenge_id; challenge }
let add_auth_header ?token req =
match S.token_kind with
| `Cookie name ->
begin match token with
| None -> []
| Some value ->
[ EzCookieServer.set req ~name ~value ]
end
| `CSRF ->
[ "access-control-allow-headers", header ]
let request_auth_base req f =
let = add_auth_header req in
let res, code = f @@ new_challenge req in
return ?code ~headers res
let request_auth req =
request_auth_base req (fun auth_needed ->
Ok (AuthNeeded auth_needed), Some 200
)
let request_error ~code req msg =
let = add_auth_header req in
return ~code ~headers (Error msg)
let return_auth_base req ?token ?foreign ~login user_id user_info f =
begin match token with
| Some token -> Lwt.return token
| None ->
create_session ?foreign ~login ~req user_id >>= function s ->
Lwt.return s.session_token
end
>>= function token ->
let = add_auth_header ~token req in
let auth = {
auth_login = login;
auth_user_id = user_id;
auth_token = token;
auth_user_info = user_info } in
return ~headers (f auth)
let return_auth req ?token ?foreign ~login user_id user_info =
match user_info with
| Some user_info ->
return_auth_base req ?token ?foreign ~login user_id user_info
(fun auth -> Ok (LoginOk auth))
| None ->
return (Ok (LoginWait user_id))
let connect req security () =
get_request_session security req >>= function
| None -> request_auth req
| Some { session_token = token; session_login = login; session_foreign = foreign; _ } ->
find_user ~login >>= function
| None -> request_error req ~code:440 `Session_expired
| Some (pwhash, user_id, user_info) ->
match pwhash, foreign with
| Some _pwhash, None ->
return_auth_base req ~token ~login user_id user_info (fun a -> Ok (AuthOk a))
| None, Some {foreign_origin = origin; foreign_token = token} ->
check_foreign ~origin ~token >>= (function
| Error _e -> request_error req ~code:440 `Session_expired
| Ok foreign_id ->
if login = foreign_id then
return_auth_base req ~token ~login ?foreign user_id user_info
(fun a -> Ok (AuthOk a))
else
request_error req ~code:400 (`Invalid_session_connect "wrong user"))
| _ -> request_error req ~code:400 (`Invalid_session_connect "wrong type of authentication")
let login req _ = function
| Local { login_user; login_challenge_id; login_challenge_reply } ->
begin
find_user ~login:login_user >>= function
| Some (Some pwhash, user_id, user_info) ->
begin match Hashtbl.find challenges login_challenge_id with
| exception Not_found ->
debug ~v:1 "/login: could not find challenge\n%!";
request_error req ~code:401
(`Challenge_not_found_or_expired login_challenge_id)
| (challenge, _t0) ->
let expected_reply = EzSession.Hash.challenge ~challenge ~pwhash in
if expected_reply <> login_challenge_reply then begin
debug ~v:1 "/login: challenge failed";
request_error req ~code:401 `Bad_user_or_password
end else begin
Hashtbl.remove challenges login_challenge_id;
return_auth req ~login:login_user user_id (Some user_info)
end
end
| _ ->
debug ~v:1 "/login: could not find user %S" login_user;
request_error req ~code:401 `Bad_user_or_password
end
| Foreign {foreign_origin; foreign_token} ->
check_foreign ~origin:foreign_origin ~token:foreign_token >>= function
| Error _ -> request_error req ~code:400 (`Invalid_session_login "foreign authentication fail")
| Ok foreign_login ->
find_user ~login:foreign_login >>= function
| Some (_, user_id, user_info) ->
return_auth req ~login:foreign_login ~foreign:{foreign_origin; foreign_token}
user_id (Some user_info)
| None ->
register_foreign ~origin:foreign_origin ~token:foreign_token >>= function
| Ok (user_id, user_info) ->
return_auth req ~login:foreign_login ~foreign:{foreign_origin; foreign_token}
user_id user_info
| Error _ ->
debug ~v:1 "/login: could not register foreign user";
request_error req ~code:400 `User_not_registered
let logout req security () =
get_request_session security req >>= function
| None -> return ~code:400 (Error (`Invalid_session_logout "session doesn't exist"))
| Some { session_user_id ; session_token = token; _ } ->
remove_session session_user_id ~token >>= fun () ->
request_auth_base req (fun auth_needed -> Ok auth_needed, None)
end
let register_handlers dir =
dir
|> register Service.connect Handler.connect
|> register Service.login Handler.login
|> register Service.logout Handler.logout
let get_request_session req =
get_request_session Service.security req
end
module SessionStoreInMemory :
SessionStore with type user_id = string = struct
type user_id = string
let (session_by_token : (string, user_id session) Hashtbl.t) =
Hashtbl.create initial_hashtbl_size
let rec create_session ?foreign ~login ~req user_id =
let token = random_challenge () in
if Hashtbl.mem session_by_token token then
create_session ~login ~req user_id
else begin
let s = {
session_login = login;
session_user_id = user_id;
session_token = token;
session_foreign = foreign;
session_last = req.Req.req_time;
} in
Hashtbl.add session_by_token token s;
Lwt.return s
end
let get_session ?req token =
match Hashtbl.find session_by_token token with
| exception Not_found ->
Lwt.return None
| s ->
let s = match req with None -> s | Some req -> { s with session_last = req.Req.req_time } in
Lwt.return (Some s)
let remove_session user_id ~token =
get_session token >>= function
| None -> Lwt.return ()
| Some s ->
if s.session_user_id = user_id then
Hashtbl.remove session_by_token token;
Lwt.return ()
end
module UserStoreInMemory(
S : EzSession.TYPES.SessionArg
with type user_id = string) : sig
val create_user :
?pwhash:string -> ?password:string -> ?kind:string ->
login:string -> S.user_info -> unit
val remove_user : login:string -> unit
val find_user : login:string -> (string option * S.user_id * S.user_info) option Lwt.t
val check_foreign : origin:string -> token:string ->
(string, int * string option) result Lwt.t
val register_foreign : origin:string -> token:string ->
(S.user_id * S.user_info option, int * string option) result Lwt.t
module SessionArg : EzSession.TYPES.SessionArg
with type user_info = S.user_info
and type user_id = S.user_id
module SessionStore : SessionStore with type user_id = S.user_id
end = struct
module SessionArg = S
module SessionStore =
(SessionStoreInMemory : SessionStore with type user_id = S.user_id)
type user = {
login : string;
user_id : S.user_id;
pwhash : string;
user_info : S.user_info;
kind : string option;
}
let (users : (string, user) Hashtbl.t) =
Hashtbl.create initial_hashtbl_size
let create_user ?pwhash ?password ?kind ~login user_info =
debug ~v:1 "create_user %S ?" login;
if Hashtbl.mem users login then raise UserAlreadyDefined;
match kind with
| Some _ ->
debug ~v:1 "create_user %S ok" login;
Hashtbl.add users login
{ login; pwhash = ""; user_id = login; user_info; kind }
| None ->
let pwhash = match pwhash with
| Some pwhash -> pwhash
| None ->
match password with
| None -> raise NoPasswordProvided
| Some password ->
EzSession.Hash.password ~login ~password in
debug ~v:1 "create_user %S ok" login;
Hashtbl.add users login
{ login; pwhash; user_id = login; user_info; kind }
let find_user ~login =
debug ~v:1 "find_user %S ?" login;
match Hashtbl.find users login with
| exception Not_found ->
Lwt.return None
| u ->
debug ~v:1 "find_user %S ok" login;
let pwhash = match u.kind with None -> Some u.pwhash | Some _k -> None in
Lwt.return ( Some (pwhash, u.user_id, u.user_info) )
let check_foreign ~origin ~token =
debug ~v:1 "check_foreign %S ?" (origin ^ "-" ^ token);
match Hashtbl.find users (origin ^ "-" ^ token) with
| exception Not_found -> Lwt.return (Error (500, Some "User not found"))
| u ->
debug ~v:1 "check_foreign %S ok" (origin ^ "-" ^ token);
Lwt.return (Ok u.login)
let register_foreign = default_register_foreign
let remove_user ~login =
Hashtbl.remove users login
end