]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_auth.c
LP#1468422 Report inactive card on password OK
[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 = oilsAuthGetSalt(user_id);
199
200     jsonObject* seed_object = jsonParseFmt(
201         "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
202         ident_type, ident, user_id, auth_seed);
203
204     jsonObject* count_object = osrfCacheGetObject(count_key);
205     if(!count_object) {
206         count_object = jsonNewNumberObject((double) 0);
207     }
208
209     osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
210     osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
211
212     osrfLogDebug(OSRF_LOG_MARK, 
213         "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
214
215     free(cache_key);
216     free(count_key);
217     jsonObjectFree(count_object);
218     jsonObjectFree(seed_object);
219
220     return auth_seed;
221 }
222
223 static int oilsAuthInitUsernameHandler(
224     osrfMethodContext* ctx, const char* username, const char* nonce) {
225
226     osrfLogInfo(OSRF_LOG_MARK, 
227         "User logging in with username %s", username);
228
229     jsonObject* resp = NULL; // free
230     jsonObject* user_obj = oilsUtilsFetchUserByUsername(username); // free
231
232     if (user_obj) {
233
234         if (JSON_NULL == user_obj->type) { // user not found
235             resp = jsonNewObject("x");
236
237         } else {
238             char* seed = oilsAuthBuildInitCache(
239                 oilsFMGetObjectId(user_obj), username, "username", nonce);
240             resp = jsonNewObject(seed);
241             free(seed);
242         }
243
244         jsonObjectFree(user_obj);
245
246     } else {
247         resp = jsonNewObject("x");
248     }
249
250     osrfAppRespondComplete(ctx, resp);
251     jsonObjectFree(resp);
252     return 0;
253 }
254
255 // open-ils.auth.authenticate.init.username
256 int oilsAuthInitUsername(osrfMethodContext* ctx) {
257     OSRF_METHOD_VERIFY_CONTEXT(ctx);
258
259     char* username =  // free
260         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
261     const char* nonce = 
262         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
263
264     if (!nonce) nonce = "";
265     if (!username) return -1;
266
267     int resp = oilsAuthInitUsernameHandler(ctx, username, nonce);
268
269     free(username);
270     return resp;
271 }
272
273 static int oilsAuthInitBarcodeHandler(
274     osrfMethodContext* ctx, const char* barcode, const char* nonce) {
275
276     osrfLogInfo(OSRF_LOG_MARK, 
277         "User logging in with barcode %s", barcode);
278
279     jsonObject* resp = NULL; // free
280     jsonObject* user_obj = oilsUtilsFetchUserByBarcode(barcode); // free
281
282     if (user_obj) {
283         if (JSON_NULL == user_obj->type) { // not found
284             resp = jsonNewObject("x");
285         } else {
286             char* seed = oilsAuthBuildInitCache(
287                 oilsFMGetObjectId(user_obj), barcode, "barcode", nonce);
288             resp = jsonNewObject(seed);
289             free(seed);
290         }
291
292         jsonObjectFree(user_obj);
293     } else {
294         resp = jsonNewObject("x");
295     }
296
297     osrfAppRespondComplete(ctx, resp);
298     jsonObjectFree(resp);
299     return 0;
300 }
301
302
303 // open-ils.auth.authenticate.init.barcode
304 int oilsAuthInitBarcode(osrfMethodContext* ctx) {
305     OSRF_METHOD_VERIFY_CONTEXT(ctx);
306
307     char* barcode = // free
308         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
309     const char* nonce = 
310         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
311
312     if (!nonce) nonce = "";
313     if (!barcode) return -1;
314
315     int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce);
316
317     free(barcode);
318     return resp;
319 }
320
321 // returns true if the provided identifier matches the barcode regex.
322 static int oilsAuthIdentIsBarcode(const char* identifier) {
323
324     // before we can fetch the barcode regex unit setting,
325     // first determine what the root org unit ID is.
326     // TODO: add an org_unit param to the .init API for future use?
327     
328     jsonObject *params = jsonParse("{\"parent_ou\":null}");
329     jsonObject *org_unit_id = oilsUtilsCStoreReq(
330         "open-ils.cstore.direct.actor.org_unit.id_list", params);
331     jsonObjectFree(params);
332
333     char* bc_regex = oilsUtilsFetchOrgSetting(
334         (int) jsonObjectGetNumber(org_unit_id), "opac.barcode_regex");
335     jsonObjectFree(org_unit_id);
336
337     if (!bc_regex) {
338         // if no regex is set, assume any identifier starting
339         // with a number is a barcode.
340         bc_regex = strdup("^\\d"); // dupe for later free'ing
341     }
342
343     const char *err_str;
344     int err_offset, match_ret;
345
346     pcre *compiled = pcre_compile(
347         bc_regex, 0, &err_str, &err_offset, NULL);
348
349     if (compiled == NULL) {
350         osrfLogError(OSRF_LOG_MARK,
351             "Could not compile '%s': %s", bc_regex, err_str);
352         free(bc_regex);
353         pcre_free(compiled);
354         return 0;
355     }
356
357     pcre_extra *extra = pcre_study(compiled, 0, &err_str);
358
359     if(err_str != NULL) {
360         osrfLogError(OSRF_LOG_MARK,
361             "Could not study regex '%s': %s", bc_regex, err_str);
362         free(bc_regex);
363         pcre_free(compiled);
364         return 0;
365     } 
366
367     match_ret = pcre_exec(
368         compiled, extra, identifier, strlen(identifier), 0, 0, NULL, 0);       
369
370     free(bc_regex);
371     pcre_free(compiled);
372     if (extra) pcre_free(extra);
373
374     if (match_ret >= 0) return 1; // regex matched
375
376     if (match_ret != PCRE_ERROR_NOMATCH) 
377         osrfLogError(OSRF_LOG_MARK, "Unknown error processing barcode regex");
378
379     return 0; // regex did not match
380 }
381
382
383 /**
384         @brief Implement the "init" method.
385         @param ctx The method context.
386         @return Zero if successful, or -1 if not.
387
388         Method parameters:
389         - username
390         - nonce : optional login seed (string) provided by the caller which
391                 is added to the auth init cache to differentiate between logins
392                 using the same username and thus avoiding cache collisions for
393                 near-simultaneous logins.
394
395         Return to client: Intermediate authentication seed.
396 */
397 int oilsAuthInit(osrfMethodContext* ctx) {
398     OSRF_METHOD_VERIFY_CONTEXT(ctx);
399     int resp = 0;
400
401     char* identifier = // free
402         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
403     const char* nonce = 
404         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
405
406     if (!nonce) nonce = "";
407     if (!identifier) return -1;  // we need an identifier
408
409     if (oilsAuthIdentIsBarcode(identifier)) {
410         resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce);
411     } else {
412         resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce);
413     }
414
415     free(identifier);
416     return resp;
417 }
418
419 /**
420         Returns 1 if the password provided matches the user's real password
421         Returns 0 otherwise
422         Returns -1 on error
423 */
424 /**
425         @brief Verify the password received from the client.
426         @param ctx The method context.
427         @param userObj An object from the database, representing the user.
428         @param password An obfuscated password received from the client.
429         @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
430
431         (None of the so-called "passwords" used here are in plaintext.  All have been passed
432         through at least one layer of hashing to obfuscate them.)
433
434         Take the password from the user object.  Append it to the username seed from memcache,
435         as stored previously by a call to the init method.  Take an md5 hash of the result.
436         Then compare this hash to the password received from the client.
437
438         In order for the two to match, other than by dumb luck, the client had to construct
439         the password it passed in the same way.  That means it neded to know not only the
440         original password (either hashed or plaintext), but also the seed.  The latter requirement
441         means that the client process needs either to be the same process that called the init
442         method or to receive the seed from the process that did so.
443 */
444 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, 
445         const char* identifier, const char* password, const char* nonce) {
446
447     int verified = 0;
448
449     // We won't be needing the seed again, remove it
450     osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
451
452     // Ask the DB to verify the user's password.
453     // Here, the password is md5(md5(password) + salt)
454
455     jsonObject* params = jsonParseFmt( // free
456         "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}", 
457         user_id, password);
458
459     jsonObject* verify_obj = // free 
460         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
461
462     jsonObjectFree(params);
463
464     if (verify_obj) {
465         verified = oilsUtilsIsDBTrue(
466             jsonObjectGetString(
467                 jsonObjectGetKeyConst(
468                     verify_obj, "actor.verify_passwd")));
469
470         jsonObjectFree(verify_obj);
471     }
472
473     char* countkey = va_list_to_string("%s%s%s", 
474         OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
475     jsonObject* countobject = osrfCacheGetObject( countkey );
476     if(countobject) {
477         long failcount = (long) jsonObjectGetNumber( countobject );
478         if(failcount >= _oilsAuthBlockCount) {
479             verified = 0;
480             osrfLogInfo(OSRF_LOG_MARK, 
481                 "oilsAuth found too many recent failures for '%s' : %i, "
482                 "forcing failure state.", identifier, failcount);
483         }
484         if(verified == 0) {
485             failcount += 1;
486         }
487         jsonObjectSetNumber( countobject, failcount );
488         osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
489         jsonObjectFree(countobject);
490     }
491     free(countkey);
492
493     return verified;
494 }
495
496 /*
497         Adds the authentication token to the user cache.  The timeout for the
498         auth token is based on the type of login as well as (if type=='opac')
499         the org location id.
500         Returns the event that should be returned to the user.
501         Event must be freed
502 */
503 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
504                 const char* type, int orgloc, const char* workstation ) {
505
506         oilsEvent* response = NULL;
507
508     jsonObject* params = jsonNewObject(NULL);
509     jsonObjectSetKey(params, "user_id", 
510         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
511     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
512     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
513     if (workstation) 
514         jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
515
516     jsonObject* authEvt = oilsUtilsQuickReq(
517         "open-ils.auth_internal",
518         "open-ils.auth_internal.session.create", params);
519     jsonObjectFree(params);
520
521     if (authEvt) {
522
523         response = oilsNewEvent2(
524             OSRF_LOG_MARK, 
525             jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
526             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
527         );
528
529         jsonObjectFree(authEvt);
530
531     } else {
532         osrfLogError(OSRF_LOG_MARK, 
533             "Error caching auth session in open-ils.auth_internal");
534     }
535
536     return response;
537 }
538
539
540 /**
541         @brief Implement the "complete" method.
542         @param ctx The method context.
543         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
544         client to indicate completion; a positive integer if successful but no such STATUS
545         message has been sent.
546
547         Method parameters:
548         - a hash with some combination of the following elements:
549                 - "username"
550                 - "barcode"
551                 - "password" (hashed with the cached seed; not plaintext)
552                 - "type"
553                 - "org"
554                 - "workstation"
555                 - "agent" (what software/interface/3rd-party is making the request)
556                 - "nonce" optional login seed to differentiate logins using the same username.
557
558         The password is required.  Either a username or a barcode must also be present.
559
560         Return to client: Intermediate authentication seed.
561
562         Validate the password, using the username if available, or the barcode if not.  The
563         user must be active, and not barred from logging on.  The barcode, if used for
564         authentication, must be active as well.  The workstation, if specified, must be valid.
565
566         Upon deciding whether to allow the logon, return a corresponding event to the client.
567 */
568 int oilsAuthComplete( osrfMethodContext* ctx ) {
569     OSRF_METHOD_VERIFY_CONTEXT(ctx);
570
571     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
572
573     const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
574     const char* identifier  = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
575     const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
576     const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
577     int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
578     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
579     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
580     const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
581     const char* nonce       = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
582
583     const char* ws = (workstation) ? workstation : "";
584     if (!nonce) nonce = "";
585
586     // we no longer care how the identifier reaches us, 
587     // as long as we have one.
588     if (!identifier) {
589         if (uname) {
590             identifier = uname;
591         } else if (barcode) {
592             identifier = barcode;
593         }
594     }
595
596     if (!identifier) {
597         return osrfAppRequestRespondException(ctx->session, ctx->request,
598             "username/barcode and password required for method: %s", 
599             ctx->method->name);
600     }
601
602     osrfLogInfo(OSRF_LOG_MARK, 
603         "Patron completing authentication with identifer %s", identifier);
604
605     /* Use __FILE__, harmless_line_number for creating
606      * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
607      * giving away information about why an authentication attempt failed.
608      */
609     int harmless_line_number = __LINE__;
610
611     if( !type )
612          type = OILS_AUTH_STAFF;
613
614     oilsEvent* response = NULL; // free
615     jsonObject* userObj = NULL; // free
616     int card_active = 1; // boolean; assume active until proven otherwise
617
618     char* cache_key = va_list_to_string(
619         "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
620     jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
621
622     if (!cacheObj) {
623         return osrfAppRequestRespondException(ctx->session,
624             ctx->request, "No authentication seed found. "
625             "open-ils.auth.authenticate.init must be called first "
626             " (check that memcached is running and can be connected to) "
627         );
628     }
629
630     int user_id = jsonObjectGetNumber(
631         jsonObjectGetKeyConst(cacheObj, "user_id"));
632
633     jsonObject* param = jsonNewNumberObject(user_id); // free
634     userObj = oilsUtilsCStoreReq(
635         "open-ils.cstore.direct.actor.user.retrieve", param);
636     jsonObjectFree(param);
637
638     char* freeable_uname = NULL;
639     if (!uname) {
640         uname = freeable_uname = oilsFMGetString(userObj, "usrname");
641     }
642
643     // See if the user is allowed to login.
644
645     jsonObject* params = jsonNewObject(NULL);
646     jsonObjectSetKey(params, "user_id", 
647         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
648     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
649     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
650     if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
651
652     jsonObject* authEvt = oilsUtilsQuickReq( // freed after password test
653         "open-ils.auth_internal",
654         "open-ils.auth_internal.user.validate", params);
655     jsonObjectFree(params);
656
657     if (!authEvt) {
658         // Something went seriously wrong.  Get outta here before 
659         // we start segfaulting.
660         jsonObjectFree(userObj);
661         if(freeable_uname) free(freeable_uname);
662         return -1;
663     }
664
665     const char* authEvtCode = 
666         jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
667
668     if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
669         // Received the generic login failure event.
670
671         osrfLogInfo(OSRF_LOG_MARK,  
672             "failed login: username=%s, barcode=%s, workstation=%s",
673             uname, (barcode ? barcode : "(none)"), ws);
674
675         response = oilsNewEvent(
676             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
677     }
678
679     int passOK = 0;
680     
681     if (!response) {
682         // User exists and is not barred, etc.  Test the password.
683
684         passOK = oilsAuthVerifyPassword(
685             ctx, user_id, identifier, password, nonce);
686
687         if (!passOK) {
688             // Password check failed. Return generic login failure.
689
690             response = oilsNewEvent(
691                 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
692
693             osrfLogInfo(OSRF_LOG_MARK,  
694                 "failed login: username=%s, barcode=%s, workstation=%s",
695                     uname, (barcode ? barcode : "(none)"), ws );
696         }
697     }
698
699
700     // Below here, we know the password check succeeded if no response
701     // object is present.
702
703     if (!response && (
704         !strcmp(authEvtCode, "PATRON_INACTIVE") ||
705         !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
706         // Patron and/or card is inactive but the correct password 
707         // was provided.  Alert the caller to the inactive-ness.
708         response = oilsNewEvent2(
709             OSRF_LOG_MARK, authEvtCode,
710             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
711         );
712     }
713
714     if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
715         // Validate API returned an unexpected non-success event.
716         // To be safe, treat this as a generic login failure.
717
718         response = oilsNewEvent(
719             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
720     }
721
722     if (!response) {
723         // password OK and no other events have prevented login completion.
724
725         char* ewhat = "login";
726
727         if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
728             response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
729             ewhat = "verify";
730
731         } else {
732             response = oilsAuthHandleLoginOK(
733                 userObj, uname, type, orgloc, workstation);
734         }
735
736         oilsUtilsTrackUserActivity(
737             oilsFMGetObjectId(userObj), 
738             ewho, ewhat, 
739             osrfAppSessionGetIngress()
740         );
741     }
742
743     // reply
744     osrfAppRespondComplete(ctx, oilsEventToJSON(response));
745
746     // clean up
747     oilsEventFree(response);
748     jsonObjectFree(userObj);
749     jsonObjectFree(authEvt);
750     if(freeable_uname)
751         free(freeable_uname);
752
753         return 0;
754 }
755
756
757
758 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
759         OSRF_METHOD_VERIFY_CONTEXT(ctx);
760
761         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
762         jsonObject* resp = NULL;
763
764         if( authToken ) {
765                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
766                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
767                 osrfCacheRemove(key);
768                 resp = jsonNewObject(authToken); /**/
769                 free(key);
770         }
771
772         osrfAppRespondComplete( ctx, resp );
773         jsonObjectFree(resp);
774         return 0;
775 }
776
777 /**
778  * Fetches the user object from the database and updates the user object in 
779  * the cache object, which then has to be re-inserted into the cache.
780  * User object is retrieved inside a transaction to avoid replication issues.
781  */
782 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
783     int reqid, userId;
784     osrfAppSession* session;
785         osrfMessage* omsg;
786     jsonObject *param, *userObj, *newUserObj = NULL;
787
788     userObj = jsonObjectGetKey( cacheObj, "userobj" );
789     userId = oilsFMGetObjectId( userObj );
790
791     session = osrfAppSessionClientInit( "open-ils.cstore" );
792     osrfAppSessionConnect(session);
793
794     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
795         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
796
797     if(omsg) {
798
799         osrfMessageFree(omsg);
800         param = jsonNewNumberObject(userId);
801         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
802             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
803         jsonObjectFree(param);
804
805         if(omsg) {
806             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
807             osrfMessageFree(omsg);
808             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
809                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
810             osrfMessageFree(omsg);
811         }
812     }
813
814     osrfAppSessionFree(session); // calls disconnect internally
815
816     if(newUserObj) {
817
818         // ws_ou and wsid are ephemeral and need to be manually propagated
819         // oilsFMSetString dupe()'s internally, no need to clone the string
820         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
821         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
822
823         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
824         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
825         return 1;
826     } 
827
828     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
829     return 0;
830 }
831
832 /**
833         Resets the auth login timeout
834         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
835 */
836 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
837         if(!authToken) return NULL;
838
839         oilsEvent* evt = NULL;
840         time_t timeout;
841
842         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
843         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
844         jsonObject* cacheObj = osrfCacheGetObject( key );
845
846         if(!cacheObj) {
847                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
848                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
849
850         } else {
851
852         if(reloadUser) {
853             _oilsAuthReloadUser(cacheObj);
854         }
855
856                 // Determine a new timeout value
857                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
858                 if( endtime_obj ) {
859                         // Extend the current endtime by a fixed amount
860                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
861                         int reset_interval = DEFAULT_RESET_INTERVAL;
862                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
863                                 cacheObj, "reset_interval" );
864                         if( reset_interval_obj ) {
865                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
866                                 if( reset_interval <= 0 )
867                                         reset_interval = DEFAULT_RESET_INTERVAL;
868                         }
869
870                         time_t now = time( NULL );
871                         time_t new_endtime = now + reset_interval;
872                         if( new_endtime > endtime ) {
873                                 // Keep the session alive a little longer
874                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
875                                 timeout = reset_interval;
876                                 osrfCachePutObject( key, cacheObj, timeout );
877                         } else {
878                                 // The session isn't close to expiring, so don't reset anything.
879                                 // Just report the time remaining.
880                                 timeout = endtime - now;
881                         }
882                 } else {
883                         // Reapply the existing timeout from the current time
884                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
885                         osrfCachePutObject( key, cacheObj, timeout );
886                 }
887
888                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
889                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
890                 jsonObjectFree(payload);
891                 jsonObjectFree(cacheObj);
892         }
893
894         free(key);
895         return evt;
896 }
897
898 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
899         OSRF_METHOD_VERIFY_CONTEXT(ctx);
900         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
901     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
902         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
903         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
904         oilsEventFree(evt);
905         return 0;
906 }
907
908
909 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
910         OSRF_METHOD_VERIFY_CONTEXT(ctx);
911     bool returnFull = false;
912
913         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
914
915     if(ctx->params->size > 1) {
916         // caller wants full cached object, with authtime, etc.
917         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
918         if(rt && strcmp(rt, "0") != 0) 
919             returnFull = true;
920     }
921
922         jsonObject* cacheObj = NULL;
923         oilsEvent* evt = NULL;
924
925         if( authToken ){
926
927                 // Reset the timeout to keep the session alive
928                 evt = _oilsAuthResetTimeout(authToken, 0);
929
930                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
931                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
932
933                 } else {
934
935                         // Retrieve the cached session object
936                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
937                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
938                         cacheObj = osrfCacheGetObject( key );
939                         if(cacheObj) {
940                                 // Return a copy of the cached user object
941                 if(returnFull)
942                                     osrfAppRespondComplete( ctx, cacheObj);
943                 else
944                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
945                                 jsonObjectFree(cacheObj);
946                         } else {
947                                 // Auth token is invalid or expired
948                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
949                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
950                                 oilsEventFree(evt2);
951                         }
952                         free(key);
953                 }
954
955         } else {
956
957                 // No session
958                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
959                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
960         }
961
962         if(evt)
963                 oilsEventFree(evt);
964
965         return 0;
966 }