]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_auth.c
LP#1539084: webstaff: sort column picker entries by class path and column labels
[Evergreen.git] / Open-ILS / src / c-apps / oils_auth.c
1 #include "opensrf/osrf_app_session.h"
2 #include "opensrf/osrf_application.h"
3 #include "opensrf/osrf_settings.h"
4 #include "opensrf/osrf_json.h"
5 #include "opensrf/log.h"
6 #include "openils/oils_utils.h"
7 #include "openils/oils_constants.h"
8 #include "openils/oils_event.h"
9 #include <pcre.h>
10
11 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
12 #define OILS_AUTH_COUNT_SFFX "_count"
13
14 #define MODULENAME "open-ils.auth"
15
16 #define OILS_AUTH_OPAC "opac"
17 #define OILS_AUTH_STAFF "staff"
18 #define OILS_AUTH_TEMP "temp"
19 #define OILS_AUTH_PERSIST "persist"
20
21 // Default time for extending a persistent session: ten minutes
22 #define DEFAULT_RESET_INTERVAL 10 * 60
23
24 int osrfAppInitialize();
25 int osrfAppChildInit();
26
27 static long _oilsAuthSeedTimeout = 0;
28 static long _oilsAuthBlockTimeout = 0;
29 static long _oilsAuthBlockCount = 0;
30
31
32 /**
33         @brief Initialize the application by registering functions for method calls.
34         @return Zero in all cases.
35 */
36 int osrfAppInitialize() {
37
38         osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
39
40         /* load and parse the IDL */
41         if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
42
43         osrfAppRegisterMethod(
44                 MODULENAME,
45                 "open-ils.auth.authenticate.init",
46                 "oilsAuthInit",
47                 "Start the authentication process and returns the intermediate authentication seed"
48                 " PARAMS( username )", 1, 0 );
49
50     osrfAppRegisterMethod(
51         MODULENAME,
52         "open-ils.auth.authenticate.init.barcode",
53         "oilsAuthInitBarcode",
54         "Start the authentication process using a patron barcode and return "
55         "the intermediate authentication seed. PARAMS(barcode)", 1, 0);
56
57     osrfAppRegisterMethod(
58         MODULENAME,
59         "open-ils.auth.authenticate.init.username",
60         "oilsAuthInitUsername",
61         "Start the authentication process using a patron username and return "
62         "the intermediate authentication seed. PARAMS(username)", 1, 0);
63
64         osrfAppRegisterMethod(
65                 MODULENAME,
66                 "open-ils.auth.authenticate.complete",
67                 "oilsAuthComplete",
68                 "Completes the authentication process.  Returns an object like so: "
69                 "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
70                 "token and authtime is the number of seconds the session will be active"
71                 "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
72                 "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
73                 "org_id is the location at which the login should be considered "
74                 "active for login timeout purposes", 1, 0 );
75
76         osrfAppRegisterMethod(
77                 MODULENAME,
78                 "open-ils.auth.authenticate.verify",
79                 "oilsAuthComplete",
80                 "Verifies the user provided a valid username and password."
81                 "Params and are the same as open-ils.auth.authenticate.complete."
82                 "Returns SUCCESS event on success, failure event on failure", 1, 0);
83
84
85         osrfAppRegisterMethod(
86                 MODULENAME,
87                 "open-ils.auth.session.retrieve",
88                 "oilsAuthSessionRetrieve",
89                 "Pass in the auth token and this retrieves the user object.  The auth "
90                 "timeout is reset when this call is made "
91                 "Returns the user object (password blanked) for the given login session "
92                 "PARAMS( authToken )", 1, 0 );
93
94         osrfAppRegisterMethod(
95                 MODULENAME,
96                 "open-ils.auth.session.delete",
97                 "oilsAuthSessionDelete",
98                 "Destroys the given login session "
99                 "PARAMS( authToken )",  1, 0 );
100
101         osrfAppRegisterMethod(
102                 MODULENAME,
103                 "open-ils.auth.session.reset_timeout",
104                 "oilsAuthResetTimeout",
105                 "Resets the login timeout for the given session "
106                 "Returns an ILS Event with payload = session_timeout of session "
107                 "if found, otherwise returns the NO_SESSION event"
108                 "PARAMS( authToken )", 1, 0 );
109
110         if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
111
112                 jsonObject* value_obj;
113
114                 value_obj = osrf_settings_host_value_object(
115                         "/apps/open-ils.auth/app_settings/auth_limits/seed" );
116                 _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
117                 jsonObjectFree(value_obj);
118                 if( -1 == _oilsAuthSeedTimeout ) {
119                         osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
120                         _oilsAuthSeedTimeout = 30;
121                 }
122
123                 value_obj = osrf_settings_host_value_object(
124                         "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
125                 _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
126                 jsonObjectFree(value_obj);
127                 if( -1 == _oilsAuthBlockTimeout ) {
128                         osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
129                         _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
130                 }
131
132                 value_obj = osrf_settings_host_value_object(
133                         "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
134                 _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
135                 jsonObjectFree(value_obj);
136                 if( -1 == _oilsAuthBlockCount ) {
137                         osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
138                         _oilsAuthBlockCount = 10;
139                 }
140
141                 osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
142                         "seed => %ld : block_timeout => %ld : block_count => %ld",
143                         _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
144         }
145
146         return 0;
147 }
148
149 /**
150         @brief Dummy placeholder for initializing a server drone.
151
152         There is nothing to do, so do nothing.
153 */
154 int osrfAppChildInit() {
155         return 0;
156 }
157
158 // free() response
159 static char* oilsAuthGetSalt(int user_id) {
160     char* salt_str = NULL;
161
162     jsonObject* params = jsonParseFmt( // free
163         "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, "main");
164
165     jsonObject* salt_obj = // free
166         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
167
168     jsonObjectFree(params);
169
170     if (salt_obj) {
171
172         if (salt_obj->type != JSON_NULL) {
173
174             const char* salt_val = jsonObjectGetString(
175                 jsonObjectGetKeyConst(salt_obj, "actor.get_salt"));
176
177             // caller expects a free-able string, could be NULL.
178             if (salt_val) { salt_str = strdup(salt_val); } 
179         }
180
181         jsonObjectFree(salt_obj);
182     }
183
184     return salt_str;
185 }
186
187 // ident is either a username or barcode
188 // Returns the init seed -> requires free();
189 static char* oilsAuthBuildInitCache(
190     int user_id, const char* ident, const char* ident_type, const char* nonce) {
191
192     char* cache_key  = va_list_to_string(
193         "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce);
194
195     char* count_key = va_list_to_string(
196         "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX);
197
198     char* auth_seed;
199     if (user_id == -1) {
200         // user does not exist.  Use a dummy seed
201         auth_seed = strdup("x");
202     } else {
203         auth_seed = oilsAuthGetSalt(user_id);
204     }
205
206     jsonObject* seed_object = jsonParseFmt(
207         "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
208         ident_type, ident, user_id, auth_seed);
209
210     jsonObject* count_object = osrfCacheGetObject(count_key);
211     if(!count_object) {
212         count_object = jsonNewNumberObject((double) 0);
213     }
214
215     osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
216
217     if (user_id != -1) {
218         // Only track login counts for existing users, since a 
219         // login for a nonexistent user will never succeed anyway.
220         osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
221     }
222
223     osrfLogDebug(OSRF_LOG_MARK, 
224         "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
225
226     free(cache_key);
227     free(count_key);
228     jsonObjectFree(count_object);
229     jsonObjectFree(seed_object);
230
231     return auth_seed;
232 }
233
234 static int oilsAuthInitUsernameHandler(
235     osrfMethodContext* ctx, const char* username, const char* nonce) {
236
237     osrfLogInfo(OSRF_LOG_MARK, 
238         "User logging in with username %s", username);
239
240     int user_id = -1;
241     jsonObject* resp = NULL; // free
242     jsonObject* user_obj = oilsUtilsFetchUserByUsername(username); // free
243
244     if (user_obj && user_obj->type != JSON_NULL) 
245         user_id = oilsFMGetObjectId(user_obj);
246
247     jsonObjectFree(user_obj); // NULL OK
248
249     char* seed = oilsAuthBuildInitCache(user_id, username, "username", nonce);
250     resp = jsonNewObject(seed);
251     free(seed);
252
253     osrfAppRespondComplete(ctx, resp);
254     jsonObjectFree(resp);
255     return 0;
256 }
257
258 // open-ils.auth.authenticate.init.username
259 int oilsAuthInitUsername(osrfMethodContext* ctx) {
260     OSRF_METHOD_VERIFY_CONTEXT(ctx);
261
262     char* username =  // free
263         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
264     const char* nonce = 
265         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
266
267     if (!nonce) nonce = "";
268     if (!username) return -1;
269
270     int resp = oilsAuthInitUsernameHandler(ctx, username, nonce);
271
272     free(username);
273     return resp;
274 }
275
276 static int oilsAuthInitBarcodeHandler(
277     osrfMethodContext* ctx, const char* barcode, const char* nonce) {
278
279     osrfLogInfo(OSRF_LOG_MARK, 
280         "User logging in with barcode %s", barcode);
281
282     int user_id = -1;
283     jsonObject* resp = NULL; // free
284     jsonObject* user_obj = oilsUtilsFetchUserByBarcode(barcode); // free
285
286     if (user_obj && user_obj->type != JSON_NULL) 
287         user_id = oilsFMGetObjectId(user_obj);
288
289     jsonObjectFree(user_obj); // NULL OK
290
291     char* seed = oilsAuthBuildInitCache(user_id, barcode, "barcode", nonce);
292     resp = jsonNewObject(seed);
293     free(seed);
294
295     osrfAppRespondComplete(ctx, resp);
296     jsonObjectFree(resp);
297     return 0;
298 }
299
300
301 // open-ils.auth.authenticate.init.barcode
302 int oilsAuthInitBarcode(osrfMethodContext* ctx) {
303     OSRF_METHOD_VERIFY_CONTEXT(ctx);
304
305     char* barcode = // free
306         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
307     const char* nonce = 
308         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
309
310     if (!nonce) nonce = "";
311     if (!barcode) return -1;
312
313     int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce);
314
315     free(barcode);
316     return resp;
317 }
318
319 // returns true if the provided identifier matches the barcode regex.
320 static int oilsAuthIdentIsBarcode(const char* identifier) {
321
322     // Assumes barcode regex is a global setting.
323     // TODO: add an org_unit param to the .init API for future use?
324     char* bc_regex = oilsUtilsFetchOrgSetting(
325         oilsUtilsGetRootOrgId(), "opac.barcode_regex");
326
327     if (!bc_regex) {
328         // if no regex is set, assume any identifier starting
329         // with a number is a barcode.
330         bc_regex = strdup("^\\d"); // dupe for later free'ing
331     }
332
333     const char *err_str;
334     int err_offset, match_ret;
335
336     pcre *compiled = pcre_compile(
337         bc_regex, 0, &err_str, &err_offset, NULL);
338
339     if (compiled == NULL) {
340         osrfLogError(OSRF_LOG_MARK,
341             "Could not compile '%s': %s", bc_regex, err_str);
342         free(bc_regex);
343         pcre_free(compiled);
344         return 0;
345     }
346
347     pcre_extra *extra = pcre_study(compiled, 0, &err_str);
348
349     if(err_str != NULL) {
350         osrfLogError(OSRF_LOG_MARK,
351             "Could not study regex '%s': %s", bc_regex, err_str);
352         free(bc_regex);
353         pcre_free(compiled);
354         return 0;
355     } 
356
357     match_ret = pcre_exec(
358         compiled, extra, identifier, strlen(identifier), 0, 0, NULL, 0);       
359
360     free(bc_regex);
361     pcre_free(compiled);
362     if (extra) pcre_free(extra);
363
364     if (match_ret >= 0) return 1; // regex matched
365
366     if (match_ret != PCRE_ERROR_NOMATCH) 
367         osrfLogError(OSRF_LOG_MARK, "Unknown error processing barcode regex");
368
369     return 0; // regex did not match
370 }
371
372
373 /**
374         @brief Implement the "init" method.
375         @param ctx The method context.
376         @return Zero if successful, or -1 if not.
377
378         Method parameters:
379         - username
380         - nonce : optional login seed (string) provided by the caller which
381                 is added to the auth init cache to differentiate between logins
382                 using the same username and thus avoiding cache collisions for
383                 near-simultaneous logins.
384
385         Return to client: Intermediate authentication seed.
386 */
387 int oilsAuthInit(osrfMethodContext* ctx) {
388     OSRF_METHOD_VERIFY_CONTEXT(ctx);
389     int resp = 0;
390
391     char* identifier = // free
392         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
393     const char* nonce = 
394         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
395
396     if (!nonce) nonce = "";
397     if (!identifier) return -1;  // we need an identifier
398
399     if (oilsAuthIdentIsBarcode(identifier)) {
400         resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce);
401     } else {
402         resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce);
403     }
404
405     free(identifier);
406     return resp;
407 }
408
409 /**
410         Returns 1 if the password provided matches the user's real password
411         Returns 0 otherwise
412         Returns -1 on error
413 */
414 /**
415         @brief Verify the password received from the client.
416         @param ctx The method context.
417         @param userObj An object from the database, representing the user.
418         @param password An obfuscated password received from the client.
419         @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
420
421         (None of the so-called "passwords" used here are in plaintext.  All have been passed
422         through at least one layer of hashing to obfuscate them.)
423
424         Take the password from the user object.  Append it to the username seed from memcache,
425         as stored previously by a call to the init method.  Take an md5 hash of the result.
426         Then compare this hash to the password received from the client.
427
428         In order for the two to match, other than by dumb luck, the client had to construct
429         the password it passed in the same way.  That means it neded to know not only the
430         original password (either hashed or plaintext), but also the seed.  The latter requirement
431         means that the client process needs either to be the same process that called the init
432         method or to receive the seed from the process that did so.
433 */
434 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, 
435         const char* identifier, const char* password, const char* nonce) {
436
437     int verified = 0;
438
439     // We won't be needing the seed again, remove it
440     osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
441
442     // Ask the DB to verify the user's password.
443     // Here, the password is md5(md5(password) + salt)
444
445     jsonObject* params = jsonParseFmt( // free
446         "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}", 
447         user_id, password);
448
449     jsonObject* verify_obj = // free 
450         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
451
452     jsonObjectFree(params);
453
454     if (verify_obj) {
455         verified = oilsUtilsIsDBTrue(
456             jsonObjectGetString(
457                 jsonObjectGetKeyConst(
458                     verify_obj, "actor.verify_passwd")));
459
460         jsonObjectFree(verify_obj);
461     }
462
463     char* countkey = va_list_to_string("%s%s%s", 
464         OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
465     jsonObject* countobject = osrfCacheGetObject( countkey );
466     if(countobject) {
467         long failcount = (long) jsonObjectGetNumber( countobject );
468         if(failcount >= _oilsAuthBlockCount) {
469             verified = 0;
470             osrfLogInfo(OSRF_LOG_MARK, 
471                 "oilsAuth found too many recent failures for '%s' : %i, "
472                 "forcing failure state.", identifier, failcount);
473         }
474         if(verified == 0) {
475             failcount += 1;
476         }
477         jsonObjectSetNumber( countobject, failcount );
478         osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
479         jsonObjectFree(countobject);
480     }
481     free(countkey);
482
483     return verified;
484 }
485
486 /*
487         Adds the authentication token to the user cache.  The timeout for the
488         auth token is based on the type of login as well as (if type=='opac')
489         the org location id.
490         Returns the event that should be returned to the user.
491         Event must be freed
492 */
493 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
494                 const char* type, int orgloc, const char* workstation ) {
495
496         oilsEvent* response = NULL;
497
498     jsonObject* params = jsonNewObject(NULL);
499     jsonObjectSetKey(params, "user_id", 
500         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
501     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
502     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
503     if (workstation) 
504         jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
505
506     jsonObject* authEvt = oilsUtilsQuickReq(
507         "open-ils.auth_internal",
508         "open-ils.auth_internal.session.create", params);
509     jsonObjectFree(params);
510
511     if (authEvt) {
512
513         response = oilsNewEvent2(
514             OSRF_LOG_MARK, 
515             jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
516             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
517         );
518
519         jsonObjectFree(authEvt);
520
521     } else {
522         osrfLogError(OSRF_LOG_MARK, 
523             "Error caching auth session in open-ils.auth_internal");
524     }
525
526     return response;
527 }
528
529
530 /**
531         @brief Implement the "complete" method.
532         @param ctx The method context.
533         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
534         client to indicate completion; a positive integer if successful but no such STATUS
535         message has been sent.
536
537         Method parameters:
538         - a hash with some combination of the following elements:
539                 - "username"
540                 - "barcode"
541                 - "password" (hashed with the cached seed; not plaintext)
542                 - "type"
543                 - "org"
544                 - "workstation"
545                 - "agent" (what software/interface/3rd-party is making the request)
546                 - "nonce" optional login seed to differentiate logins using the same username.
547
548         The password is required.  Either a username or a barcode must also be present.
549
550         Return to client: Intermediate authentication seed.
551
552         Validate the password, using the username if available, or the barcode if not.  The
553         user must be active, and not barred from logging on.  The barcode, if used for
554         authentication, must be active as well.  The workstation, if specified, must be valid.
555
556         Upon deciding whether to allow the logon, return a corresponding event to the client.
557 */
558 int oilsAuthComplete( osrfMethodContext* ctx ) {
559     OSRF_METHOD_VERIFY_CONTEXT(ctx);
560
561     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
562
563     const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
564     const char* identifier  = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
565     const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
566     const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
567     int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
568     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
569     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
570     const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
571     const char* nonce       = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
572
573     const char* ws = (workstation) ? workstation : "";
574     if (!nonce) nonce = "";
575
576     // we no longer care how the identifier reaches us, 
577     // as long as we have one.
578     if (!identifier) {
579         if (uname) {
580             identifier = uname;
581         } else if (barcode) {
582             identifier = barcode;
583         }
584     }
585
586     if (!identifier) {
587         return osrfAppRequestRespondException(ctx->session, ctx->request,
588             "username/barcode and password required for method: %s", 
589             ctx->method->name);
590     }
591
592     osrfLogInfo(OSRF_LOG_MARK, 
593         "Patron completing authentication with identifer %s", identifier);
594
595     /* Use __FILE__, harmless_line_number for creating
596      * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
597      * giving away information about why an authentication attempt failed.
598      */
599     int harmless_line_number = __LINE__;
600
601     if( !type )
602          type = OILS_AUTH_STAFF;
603
604     oilsEvent* response = NULL; // free
605     jsonObject* userObj = NULL; // free
606     int card_active = 1; // boolean; assume active until proven otherwise
607
608     char* cache_key = va_list_to_string(
609         "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
610     jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
611
612     if (!cacheObj) {
613         return osrfAppRequestRespondException(ctx->session,
614             ctx->request, "No authentication seed found. "
615             "open-ils.auth.authenticate.init must be called first "
616             " (check that memcached is running and can be connected to) "
617         );
618     }
619
620     int user_id = jsonObjectGetNumber(
621         jsonObjectGetKeyConst(cacheObj, "user_id"));
622
623     if (user_id == -1) {
624         // User was not found during init.  Clean up and exit early.
625         response = oilsNewEvent(
626             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
627         osrfAppRespondComplete(ctx, oilsEventToJSON(response));
628         oilsEventFree(response); // frees event JSON
629         osrfCacheRemove(cache_key);
630         jsonObjectFree(cacheObj);
631         return 0;
632     }
633
634     jsonObject* param = jsonNewNumberObject(user_id); // free
635     userObj = oilsUtilsCStoreReq(
636         "open-ils.cstore.direct.actor.user.retrieve", param);
637     jsonObjectFree(param);
638
639     char* freeable_uname = NULL;
640     if (!uname) {
641         uname = freeable_uname = oilsFMGetString(userObj, "usrname");
642     }
643
644     // See if the user is allowed to login.
645
646     jsonObject* params = jsonNewObject(NULL);
647     jsonObjectSetKey(params, "user_id", 
648         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
649     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
650     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
651     if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
652
653     jsonObject* authEvt = oilsUtilsQuickReq( // freed after password test
654         "open-ils.auth_internal",
655         "open-ils.auth_internal.user.validate", params);
656     jsonObjectFree(params);
657
658     if (!authEvt) {
659         // Something went seriously wrong.  Get outta here before 
660         // we start segfaulting.
661         jsonObjectFree(userObj);
662         if(freeable_uname) free(freeable_uname);
663         return -1;
664     }
665
666     const char* authEvtCode = 
667         jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
668
669     if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
670         // Received the generic login failure event.
671
672         osrfLogInfo(OSRF_LOG_MARK,  
673             "failed login: username=%s, barcode=%s, workstation=%s",
674             uname, (barcode ? barcode : "(none)"), ws);
675
676         response = oilsNewEvent(
677             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
678     }
679
680     int passOK = 0;
681     
682     if (!response) {
683         // User exists and is not barred, etc.  Test the password.
684
685         passOK = oilsAuthVerifyPassword(
686             ctx, user_id, identifier, password, nonce);
687
688         if (!passOK) {
689             // Password check failed. Return generic login failure.
690
691             response = oilsNewEvent(
692                 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
693
694             osrfLogInfo(OSRF_LOG_MARK,  
695                 "failed login: username=%s, barcode=%s, workstation=%s",
696                     uname, (barcode ? barcode : "(none)"), ws );
697         }
698     }
699
700
701     // Below here, we know the password check succeeded if no response
702     // object is present.
703
704     if (!response && (
705         !strcmp(authEvtCode, "PATRON_INACTIVE") ||
706         !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
707         // Patron and/or card is inactive but the correct password 
708         // was provided.  Alert the caller to the inactive-ness.
709         response = oilsNewEvent2(
710             OSRF_LOG_MARK, authEvtCode,
711             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
712         );
713     }
714
715     if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
716         // Validate API returned an unexpected non-success event.
717         // To be safe, treat this as a generic login failure.
718
719         response = oilsNewEvent(
720             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
721     }
722
723     if (!response) {
724         // password OK and no other events have prevented login completion.
725
726         char* ewhat = "login";
727
728         if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
729             response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
730             ewhat = "verify";
731
732         } else {
733             response = oilsAuthHandleLoginOK(
734                 userObj, uname, type, orgloc, workstation);
735         }
736
737         oilsUtilsTrackUserActivity(
738             oilsFMGetObjectId(userObj), 
739             ewho, ewhat, 
740             osrfAppSessionGetIngress()
741         );
742     }
743
744     // reply
745     osrfAppRespondComplete(ctx, oilsEventToJSON(response));
746
747     // clean up
748     oilsEventFree(response);
749     jsonObjectFree(userObj);
750     jsonObjectFree(authEvt);
751     jsonObjectFree(cacheObj);
752     if(freeable_uname)
753         free(freeable_uname);
754
755         return 0;
756 }
757
758
759
760 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
761         OSRF_METHOD_VERIFY_CONTEXT(ctx);
762
763         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
764         jsonObject* resp = NULL;
765
766         if( authToken ) {
767                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
768                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
769                 osrfCacheRemove(key);
770                 resp = jsonNewObject(authToken); /**/
771                 free(key);
772         }
773
774         osrfAppRespondComplete( ctx, resp );
775         jsonObjectFree(resp);
776         return 0;
777 }
778
779 /**
780  * Fetches the user object from the database and updates the user object in 
781  * the cache object, which then has to be re-inserted into the cache.
782  * User object is retrieved inside a transaction to avoid replication issues.
783  */
784 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
785     int reqid, userId;
786     osrfAppSession* session;
787         osrfMessage* omsg;
788     jsonObject *param, *userObj, *newUserObj = NULL;
789
790     userObj = jsonObjectGetKey( cacheObj, "userobj" );
791     userId = oilsFMGetObjectId( userObj );
792
793     session = osrfAppSessionClientInit( "open-ils.cstore" );
794     osrfAppSessionConnect(session);
795
796     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
797         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
798
799     if(omsg) {
800
801         osrfMessageFree(omsg);
802         param = jsonNewNumberObject(userId);
803         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
804             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
805         jsonObjectFree(param);
806
807         if(omsg) {
808             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
809             osrfMessageFree(omsg);
810             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
811                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
812             osrfMessageFree(omsg);
813         }
814     }
815
816     osrfAppSessionFree(session); // calls disconnect internally
817
818     if(newUserObj) {
819
820         // ws_ou and wsid are ephemeral and need to be manually propagated
821         // oilsFMSetString dupe()'s internally, no need to clone the string
822         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
823         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
824
825         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
826         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
827         return 1;
828     } 
829
830     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
831     return 0;
832 }
833
834 /**
835         Resets the auth login timeout
836         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
837 */
838 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
839         if(!authToken) return NULL;
840
841         oilsEvent* evt = NULL;
842         time_t timeout;
843
844         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
845         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
846         jsonObject* cacheObj = osrfCacheGetObject( key );
847
848         if(!cacheObj) {
849                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
850                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
851
852         } else {
853
854         if(reloadUser) {
855             _oilsAuthReloadUser(cacheObj);
856         }
857
858                 // Determine a new timeout value
859                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
860                 if( endtime_obj ) {
861                         // Extend the current endtime by a fixed amount
862                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
863                         int reset_interval = DEFAULT_RESET_INTERVAL;
864                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
865                                 cacheObj, "reset_interval" );
866                         if( reset_interval_obj ) {
867                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
868                                 if( reset_interval <= 0 )
869                                         reset_interval = DEFAULT_RESET_INTERVAL;
870                         }
871
872                         time_t now = time( NULL );
873                         time_t new_endtime = now + reset_interval;
874                         if( new_endtime > endtime ) {
875                                 // Keep the session alive a little longer
876                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
877                                 timeout = reset_interval;
878                                 osrfCachePutObject( key, cacheObj, timeout );
879                         } else {
880                                 // The session isn't close to expiring, so don't reset anything.
881                                 // Just report the time remaining.
882                                 timeout = endtime - now;
883                         }
884                 } else {
885                         // Reapply the existing timeout from the current time
886                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
887                         osrfCachePutObject( key, cacheObj, timeout );
888                 }
889
890                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
891                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
892                 jsonObjectFree(payload);
893                 jsonObjectFree(cacheObj);
894         }
895
896         free(key);
897         return evt;
898 }
899
900 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
901         OSRF_METHOD_VERIFY_CONTEXT(ctx);
902         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
903     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
904         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
905         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
906         oilsEventFree(evt);
907         return 0;
908 }
909
910
911 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
912         OSRF_METHOD_VERIFY_CONTEXT(ctx);
913     bool returnFull = false;
914
915         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
916
917     if(ctx->params->size > 1) {
918         // caller wants full cached object, with authtime, etc.
919         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
920         if(rt && strcmp(rt, "0") != 0) 
921             returnFull = true;
922     }
923
924         jsonObject* cacheObj = NULL;
925         oilsEvent* evt = NULL;
926
927         if( authToken ){
928
929                 // Reset the timeout to keep the session alive
930                 evt = _oilsAuthResetTimeout(authToken, 0);
931
932                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
933                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
934
935                 } else {
936
937                         // Retrieve the cached session object
938                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
939                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
940                         cacheObj = osrfCacheGetObject( key );
941                         if(cacheObj) {
942                                 // Return a copy of the cached user object
943                 if(returnFull)
944                                     osrfAppRespondComplete( ctx, cacheObj);
945                 else
946                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
947                                 jsonObjectFree(cacheObj);
948                         } else {
949                                 // Auth token is invalid or expired
950                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
951                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
952                                 oilsEventFree(evt2);
953                         }
954                         free(key);
955                 }
956
957         } else {
958
959                 // No session
960                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
961                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
962         }
963
964         if(evt)
965                 oilsEventFree(evt);
966
967         return 0;
968 }