]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_auth.c
LP#1468422 auth-internal validate API
[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
651     jsonObject* authEvt = oilsUtilsQuickReq( // freed after password test
652         "open-ils.auth_internal",
653         "open-ils.auth_internal.user.validate", params);
654     jsonObjectFree(params);
655
656     if (!authEvt) {
657         // Something went seriously wrong.  Get outta here before 
658         // we start segfaulting.
659         jsonObjectFree(userObj);
660         if(freeable_uname) free(freeable_uname);
661         return -1;
662     }
663
664     const char* evtCode = 
665         jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
666
667     // For security/privacy sake, only report that a patron is 
668     // inactive if the correct password is provided below.
669     int user_inactive = !strcmp(evtCode, "PATRON_INACTIVE");
670
671     if (strcmp(evtCode, "SUCCESS") && !user_inactive) { // validate failed
672         osrfLogInfo(OSRF_LOG_MARK,  
673             "failed login: username=%s, barcode=%s, workstation=%s",
674             uname, (barcode ? barcode : "(none)"), ws);
675         osrfAppRespondComplete(ctx, authEvt);
676         jsonObjectFree(authEvt);
677         jsonObjectFree(userObj);
678         if(freeable_uname) free(freeable_uname);
679         return 0;           // No such user
680     }
681
682         // Such a user exists and isn't barred or deleted.
683         // Now see if he or she has the right credentials.
684         int passOK = oilsAuthVerifyPassword(
685         ctx, user_id, identifier, password, nonce);
686
687         if( passOK < 0 ) {
688         jsonObjectFree(authEvt);
689                 jsonObjectFree(userObj);
690         if(freeable_uname) free(freeable_uname);
691                 return passOK;
692         }
693
694     if (passOK && user_inactive) {
695         // Patron is inactive but provided the correct password.
696         // Return the original PATRON_INACTIVE event.
697         osrfAppRespondComplete(ctx, authEvt);
698         jsonObjectFree(authEvt);
699         jsonObjectFree(userObj);
700         if(freeable_uname) free(freeable_uname);
701         return 0;
702     }
703
704     jsonObjectFree(authEvt); // we're all done with this now.
705
706         if( passOK ) { // login successful  
707         
708                 char* ewhat = "login";
709
710                 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
711                         response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
712                         ewhat = "verify";
713
714                 } else {
715                         response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
716                 }
717
718                 oilsUtilsTrackUserActivity(
719                         oilsFMGetObjectId(userObj), 
720                         ewho, ewhat, 
721                         osrfAppSessionGetIngress()
722                 );
723
724         } else {
725                 response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
726                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
727                                 uname, (barcode ? barcode : "(none)"), ws );
728         }
729
730         jsonObjectFree(userObj);
731         osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
732         oilsEventFree(response);
733
734         if(freeable_uname)
735                 free(freeable_uname);
736
737         return 0;
738 }
739
740
741
742 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
743         OSRF_METHOD_VERIFY_CONTEXT(ctx);
744
745         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
746         jsonObject* resp = NULL;
747
748         if( authToken ) {
749                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
750                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
751                 osrfCacheRemove(key);
752                 resp = jsonNewObject(authToken); /**/
753                 free(key);
754         }
755
756         osrfAppRespondComplete( ctx, resp );
757         jsonObjectFree(resp);
758         return 0;
759 }
760
761 /**
762  * Fetches the user object from the database and updates the user object in 
763  * the cache object, which then has to be re-inserted into the cache.
764  * User object is retrieved inside a transaction to avoid replication issues.
765  */
766 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
767     int reqid, userId;
768     osrfAppSession* session;
769         osrfMessage* omsg;
770     jsonObject *param, *userObj, *newUserObj = NULL;
771
772     userObj = jsonObjectGetKey( cacheObj, "userobj" );
773     userId = oilsFMGetObjectId( userObj );
774
775     session = osrfAppSessionClientInit( "open-ils.cstore" );
776     osrfAppSessionConnect(session);
777
778     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
779         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
780
781     if(omsg) {
782
783         osrfMessageFree(omsg);
784         param = jsonNewNumberObject(userId);
785         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
786             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
787         jsonObjectFree(param);
788
789         if(omsg) {
790             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
791             osrfMessageFree(omsg);
792             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
793                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
794             osrfMessageFree(omsg);
795         }
796     }
797
798     osrfAppSessionFree(session); // calls disconnect internally
799
800     if(newUserObj) {
801
802         // ws_ou and wsid are ephemeral and need to be manually propagated
803         // oilsFMSetString dupe()'s internally, no need to clone the string
804         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
805         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
806
807         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
808         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
809         return 1;
810     } 
811
812     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
813     return 0;
814 }
815
816 /**
817         Resets the auth login timeout
818         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
819 */
820 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
821         if(!authToken) return NULL;
822
823         oilsEvent* evt = NULL;
824         time_t timeout;
825
826         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
827         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
828         jsonObject* cacheObj = osrfCacheGetObject( key );
829
830         if(!cacheObj) {
831                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
832                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
833
834         } else {
835
836         if(reloadUser) {
837             _oilsAuthReloadUser(cacheObj);
838         }
839
840                 // Determine a new timeout value
841                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
842                 if( endtime_obj ) {
843                         // Extend the current endtime by a fixed amount
844                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
845                         int reset_interval = DEFAULT_RESET_INTERVAL;
846                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
847                                 cacheObj, "reset_interval" );
848                         if( reset_interval_obj ) {
849                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
850                                 if( reset_interval <= 0 )
851                                         reset_interval = DEFAULT_RESET_INTERVAL;
852                         }
853
854                         time_t now = time( NULL );
855                         time_t new_endtime = now + reset_interval;
856                         if( new_endtime > endtime ) {
857                                 // Keep the session alive a little longer
858                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
859                                 timeout = reset_interval;
860                                 osrfCachePutObject( key, cacheObj, timeout );
861                         } else {
862                                 // The session isn't close to expiring, so don't reset anything.
863                                 // Just report the time remaining.
864                                 timeout = endtime - now;
865                         }
866                 } else {
867                         // Reapply the existing timeout from the current time
868                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
869                         osrfCachePutObject( key, cacheObj, timeout );
870                 }
871
872                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
873                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
874                 jsonObjectFree(payload);
875                 jsonObjectFree(cacheObj);
876         }
877
878         free(key);
879         return evt;
880 }
881
882 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
883         OSRF_METHOD_VERIFY_CONTEXT(ctx);
884         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
885     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
886         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
887         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
888         oilsEventFree(evt);
889         return 0;
890 }
891
892
893 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
894         OSRF_METHOD_VERIFY_CONTEXT(ctx);
895     bool returnFull = false;
896
897         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
898
899     if(ctx->params->size > 1) {
900         // caller wants full cached object, with authtime, etc.
901         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
902         if(rt && strcmp(rt, "0") != 0) 
903             returnFull = true;
904     }
905
906         jsonObject* cacheObj = NULL;
907         oilsEvent* evt = NULL;
908
909         if( authToken ){
910
911                 // Reset the timeout to keep the session alive
912                 evt = _oilsAuthResetTimeout(authToken, 0);
913
914                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
915                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
916
917                 } else {
918
919                         // Retrieve the cached session object
920                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
921                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
922                         cacheObj = osrfCacheGetObject( key );
923                         if(cacheObj) {
924                                 // Return a copy of the cached user object
925                 if(returnFull)
926                                     osrfAppRespondComplete( ctx, cacheObj);
927                 else
928                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
929                                 jsonObjectFree(cacheObj);
930                         } else {
931                                 // Auth token is invalid or expired
932                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
933                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
934                                 oilsEventFree(evt2);
935                         }
936                         free(key);
937                 }
938
939         } else {
940
941                 // No session
942                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
943                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
944         }
945
946         if(evt)
947                 oilsEventFree(evt);
948
949         return 0;
950 }