LP#1592565 Log successful authtoken and workstation
[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         osrfLogActivity(OSRF_LOG_MARK,
520             "successful login: username=%s, authtoken=%s, workstation=%s",
521             uname,
522             jsonObjectGetString(
523                 jsonObjectGetKeyConst(
524                     jsonObjectGetKeyConst(authEvt, "payload"),
525                     "authtoken"
526                 )
527             ),
528             workstation ? workstation : ""
529         );
530
531         jsonObjectFree(authEvt);
532
533     } else {
534         osrfLogError(OSRF_LOG_MARK, 
535             "Error caching auth session in open-ils.auth_internal");
536     }
537
538     return response;
539 }
540
541
542 /**
543         @brief Implement the "complete" method.
544         @param ctx The method context.
545         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
546         client to indicate completion; a positive integer if successful but no such STATUS
547         message has been sent.
548
549         Method parameters:
550         - a hash with some combination of the following elements:
551                 - "username"
552                 - "barcode"
553                 - "password" (hashed with the cached seed; not plaintext)
554                 - "type"
555                 - "org"
556                 - "workstation"
557                 - "agent" (what software/interface/3rd-party is making the request)
558                 - "nonce" optional login seed to differentiate logins using the same username.
559
560         The password is required.  Either a username or a barcode must also be present.
561
562         Return to client: Intermediate authentication seed.
563
564         Validate the password, using the username if available, or the barcode if not.  The
565         user must be active, and not barred from logging on.  The barcode, if used for
566         authentication, must be active as well.  The workstation, if specified, must be valid.
567
568         Upon deciding whether to allow the logon, return a corresponding event to the client.
569 */
570 int oilsAuthComplete( osrfMethodContext* ctx ) {
571     OSRF_METHOD_VERIFY_CONTEXT(ctx);
572
573     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
574
575     const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
576     const char* identifier  = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
577     const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
578     const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
579     int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
580     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
581     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
582     const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
583     const char* nonce       = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
584
585     const char* ws = (workstation) ? workstation : "";
586     if (!nonce) nonce = "";
587
588     // we no longer care how the identifier reaches us, 
589     // as long as we have one.
590     if (!identifier) {
591         if (uname) {
592             identifier = uname;
593         } else if (barcode) {
594             identifier = barcode;
595         }
596     }
597
598     if (!identifier) {
599         return osrfAppRequestRespondException(ctx->session, ctx->request,
600             "username/barcode and password required for method: %s", 
601             ctx->method->name);
602     }
603
604     osrfLogInfo(OSRF_LOG_MARK, 
605         "Patron completing authentication with identifer %s", identifier);
606
607     /* Use __FILE__, harmless_line_number for creating
608      * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
609      * giving away information about why an authentication attempt failed.
610      */
611     int harmless_line_number = __LINE__;
612
613     if( !type )
614          type = OILS_AUTH_STAFF;
615
616     oilsEvent* response = NULL; // free
617     jsonObject* userObj = NULL; // free
618     int card_active = 1; // boolean; assume active until proven otherwise
619
620     char* cache_key = va_list_to_string(
621         "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
622     jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
623
624     if (!cacheObj) {
625         return osrfAppRequestRespondException(ctx->session,
626             ctx->request, "No authentication seed found. "
627             "open-ils.auth.authenticate.init must be called first "
628             " (check that memcached is running and can be connected to) "
629         );
630     }
631
632     int user_id = jsonObjectGetNumber(
633         jsonObjectGetKeyConst(cacheObj, "user_id"));
634
635     if (user_id == -1) {
636         // User was not found during init.  Clean up and exit early.
637         response = oilsNewEvent(
638             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
639         osrfAppRespondComplete(ctx, oilsEventToJSON(response));
640         oilsEventFree(response); // frees event JSON
641         osrfCacheRemove(cache_key);
642         jsonObjectFree(cacheObj);
643         return 0;
644     }
645
646     jsonObject* param = jsonNewNumberObject(user_id); // free
647     userObj = oilsUtilsCStoreReq(
648         "open-ils.cstore.direct.actor.user.retrieve", param);
649     jsonObjectFree(param);
650
651     char* freeable_uname = NULL;
652     if (!uname) {
653         uname = freeable_uname = oilsFMGetString(userObj, "usrname");
654     }
655
656     // See if the user is allowed to login.
657
658     jsonObject* params = jsonNewObject(NULL);
659     jsonObjectSetKey(params, "user_id", 
660         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
661     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
662     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
663     if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
664
665     jsonObject* authEvt = oilsUtilsQuickReq( // freed after password test
666         "open-ils.auth_internal",
667         "open-ils.auth_internal.user.validate", params);
668     jsonObjectFree(params);
669
670     if (!authEvt) {
671         // Something went seriously wrong.  Get outta here before 
672         // we start segfaulting.
673         jsonObjectFree(userObj);
674         if(freeable_uname) free(freeable_uname);
675         return -1;
676     }
677
678     const char* authEvtCode = 
679         jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
680
681     if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
682         // Received the generic login failure event.
683
684         osrfLogInfo(OSRF_LOG_MARK,  
685             "failed login: username=%s, barcode=%s, workstation=%s",
686             uname, (barcode ? barcode : "(none)"), ws);
687
688         response = oilsNewEvent(
689             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
690     }
691
692     int passOK = 0;
693     
694     if (!response) {
695         // User exists and is not barred, etc.  Test the password.
696
697         passOK = oilsAuthVerifyPassword(
698             ctx, user_id, identifier, password, nonce);
699
700         if (!passOK) {
701             // Password check failed. Return generic login failure.
702
703             response = oilsNewEvent(
704                 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
705
706             osrfLogInfo(OSRF_LOG_MARK,  
707                 "failed login: username=%s, barcode=%s, workstation=%s",
708                     uname, (barcode ? barcode : "(none)"), ws );
709         }
710     }
711
712
713     // Below here, we know the password check succeeded if no response
714     // object is present.
715
716     if (!response && (
717         !strcmp(authEvtCode, "PATRON_INACTIVE") ||
718         !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
719         // Patron and/or card is inactive but the correct password 
720         // was provided.  Alert the caller to the inactive-ness.
721         response = oilsNewEvent2(
722             OSRF_LOG_MARK, authEvtCode,
723             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
724         );
725     }
726
727     if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
728         // Validate API returned an unexpected non-success event.
729         // To be safe, treat this as a generic login failure.
730
731         response = oilsNewEvent(
732             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
733     }
734
735     if (!response) {
736         // password OK and no other events have prevented login completion.
737
738         char* ewhat = "login";
739
740         if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
741             response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
742             ewhat = "verify";
743
744         } else {
745             response = oilsAuthHandleLoginOK(
746                 userObj, uname, type, orgloc, workstation);
747         }
748
749         oilsUtilsTrackUserActivity(
750             oilsFMGetObjectId(userObj), 
751             ewho, ewhat, 
752             osrfAppSessionGetIngress()
753         );
754     }
755
756     // reply
757     osrfAppRespondComplete(ctx, oilsEventToJSON(response));
758
759     // clean up
760     oilsEventFree(response);
761     jsonObjectFree(userObj);
762     jsonObjectFree(authEvt);
763     jsonObjectFree(cacheObj);
764     if(freeable_uname)
765         free(freeable_uname);
766
767         return 0;
768 }
769
770
771
772 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
773         OSRF_METHOD_VERIFY_CONTEXT(ctx);
774
775         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
776         jsonObject* resp = NULL;
777
778         if( authToken ) {
779                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
780                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
781                 osrfCacheRemove(key);
782                 resp = jsonNewObject(authToken); /**/
783                 free(key);
784         }
785
786         osrfAppRespondComplete( ctx, resp );
787         jsonObjectFree(resp);
788         return 0;
789 }
790
791 /**
792  * Fetches the user object from the database and updates the user object in 
793  * the cache object, which then has to be re-inserted into the cache.
794  * User object is retrieved inside a transaction to avoid replication issues.
795  */
796 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
797     int reqid, userId;
798     osrfAppSession* session;
799         osrfMessage* omsg;
800     jsonObject *param, *userObj, *newUserObj = NULL;
801
802     userObj = jsonObjectGetKey( cacheObj, "userobj" );
803     userId = oilsFMGetObjectId( userObj );
804
805     session = osrfAppSessionClientInit( "open-ils.cstore" );
806     osrfAppSessionConnect(session);
807
808     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
809         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
810
811     if(omsg) {
812
813         osrfMessageFree(omsg);
814         param = jsonNewNumberObject(userId);
815         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
816             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
817         jsonObjectFree(param);
818
819         if(omsg) {
820             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
821             osrfMessageFree(omsg);
822             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
823                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
824             osrfMessageFree(omsg);
825         }
826     }
827
828     osrfAppSessionFree(session); // calls disconnect internally
829
830     if(newUserObj) {
831
832         // ws_ou and wsid are ephemeral and need to be manually propagated
833         // oilsFMSetString dupe()'s internally, no need to clone the string
834         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
835         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
836
837         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
838         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
839         return 1;
840     } 
841
842     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
843     return 0;
844 }
845
846 /**
847         Resets the auth login timeout
848         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
849 */
850 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
851         if(!authToken) return NULL;
852
853         oilsEvent* evt = NULL;
854         time_t timeout;
855
856         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
857         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
858         jsonObject* cacheObj = osrfCacheGetObject( key );
859
860         if(!cacheObj) {
861                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
862                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
863
864         } else {
865
866         if(reloadUser) {
867             _oilsAuthReloadUser(cacheObj);
868         }
869
870                 // Determine a new timeout value
871                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
872                 if( endtime_obj ) {
873                         // Extend the current endtime by a fixed amount
874                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
875                         int reset_interval = DEFAULT_RESET_INTERVAL;
876                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
877                                 cacheObj, "reset_interval" );
878                         if( reset_interval_obj ) {
879                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
880                                 if( reset_interval <= 0 )
881                                         reset_interval = DEFAULT_RESET_INTERVAL;
882                         }
883
884                         time_t now = time( NULL );
885                         time_t new_endtime = now + reset_interval;
886                         if( new_endtime > endtime ) {
887                                 // Keep the session alive a little longer
888                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
889                                 timeout = reset_interval;
890                                 osrfCachePutObject( key, cacheObj, timeout );
891                         } else {
892                                 // The session isn't close to expiring, so don't reset anything.
893                                 // Just report the time remaining.
894                                 timeout = endtime - now;
895                         }
896                 } else {
897                         // Reapply the existing timeout from the current time
898                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
899                         osrfCachePutObject( key, cacheObj, timeout );
900                 }
901
902                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
903                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
904                 jsonObjectFree(payload);
905                 jsonObjectFree(cacheObj);
906         }
907
908         free(key);
909         return evt;
910 }
911
912 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
913         OSRF_METHOD_VERIFY_CONTEXT(ctx);
914         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
915     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
916         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
917         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
918         oilsEventFree(evt);
919         return 0;
920 }
921
922
923 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
924         OSRF_METHOD_VERIFY_CONTEXT(ctx);
925     bool returnFull = false;
926
927         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
928
929     if(ctx->params->size > 1) {
930         // caller wants full cached object, with authtime, etc.
931         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
932         if(rt && strcmp(rt, "0") != 0) 
933             returnFull = true;
934     }
935
936         jsonObject* cacheObj = NULL;
937         oilsEvent* evt = NULL;
938
939         if( authToken ){
940
941                 // Reset the timeout to keep the session alive
942                 evt = _oilsAuthResetTimeout(authToken, 0);
943
944                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
945                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
946
947                 } else {
948
949                         // Retrieve the cached session object
950                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
951                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
952                         cacheObj = osrfCacheGetObject( key );
953                         if(cacheObj) {
954                                 // Return a copy of the cached user object
955                 if(returnFull)
956                                     osrfAppRespondComplete( ctx, cacheObj);
957                 else
958                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
959                                 jsonObjectFree(cacheObj);
960                         } else {
961                                 // Auth token is invalid or expired
962                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
963                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
964                                 oilsEventFree(evt2);
965                         }
966                         free(key);
967                 }
968
969         } else {
970
971                 // No session
972                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
973                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
974         }
975
976         if(evt)
977                 oilsEventFree(evt);
978
979         return 0;
980 }