]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_auth.c
LP#1468422 New open-ils.auth_internal service
[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         Verifies that the user has permission to login with the
421         given type.  If the permission fails, an oilsEvent is returned
422         to the caller.
423         @return -1 if the permission check failed, 0 if the permission
424         is granted
425 */
426 static int oilsAuthCheckLoginPerm(
427                 osrfMethodContext* ctx, const jsonObject* userObj, const char* type ) {
428
429         if(!(userObj && type)) return -1;
430         oilsEvent* perm = NULL;
431
432         if(!strcasecmp(type, OILS_AUTH_OPAC)) {
433                 char* permissions[] = { "OPAC_LOGIN" };
434                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
435
436         } else if(!strcasecmp(type, OILS_AUTH_STAFF)) {
437                 char* permissions[] = { "STAFF_LOGIN" };
438                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
439
440         } else if(!strcasecmp(type, OILS_AUTH_TEMP)) {
441                 char* permissions[] = { "STAFF_LOGIN" };
442                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
443         } else if(!strcasecmp(type, OILS_AUTH_PERSIST)) {
444                 char* permissions[] = { "PERSISTENT_LOGIN" };
445                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
446         }
447
448         if(perm) {
449                 osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
450                 oilsEventFree(perm);
451                 return -1;
452         }
453
454         return 0;
455 }
456
457 /**
458         Returns 1 if the password provided matches the user's real password
459         Returns 0 otherwise
460         Returns -1 on error
461 */
462 /**
463         @brief Verify the password received from the client.
464         @param ctx The method context.
465         @param userObj An object from the database, representing the user.
466         @param password An obfuscated password received from the client.
467         @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
468
469         (None of the so-called "passwords" used here are in plaintext.  All have been passed
470         through at least one layer of hashing to obfuscate them.)
471
472         Take the password from the user object.  Append it to the username seed from memcache,
473         as stored previously by a call to the init method.  Take an md5 hash of the result.
474         Then compare this hash to the password received from the client.
475
476         In order for the two to match, other than by dumb luck, the client had to construct
477         the password it passed in the same way.  That means it neded to know not only the
478         original password (either hashed or plaintext), but also the seed.  The latter requirement
479         means that the client process needs either to be the same process that called the init
480         method or to receive the seed from the process that did so.
481 */
482 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, 
483         const char* identifier, const char* password, const char* nonce) {
484
485     int verified = 0;
486
487     // We won't be needing the seed again, remove it
488     osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
489
490     // Ask the DB to verify the user's password.
491     // Here, the password is md5(md5(password) + salt)
492
493     jsonObject* params = jsonParseFmt( // free
494         "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}", 
495         user_id, password);
496
497     jsonObject* verify_obj = // free 
498         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
499
500     jsonObjectFree(params);
501
502     if (verify_obj) {
503         verified = oilsUtilsIsDBTrue(
504             jsonObjectGetString(
505                 jsonObjectGetKeyConst(
506                     verify_obj, "actor.verify_passwd")));
507
508         jsonObjectFree(verify_obj);
509     }
510
511     char* countkey = va_list_to_string("%s%s%s", 
512         OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
513     jsonObject* countobject = osrfCacheGetObject( countkey );
514     if(countobject) {
515         long failcount = (long) jsonObjectGetNumber( countobject );
516         if(failcount >= _oilsAuthBlockCount) {
517             verified = 0;
518             osrfLogInfo(OSRF_LOG_MARK, 
519                 "oilsAuth found too many recent failures for '%s' : %i, "
520                 "forcing failure state.", identifier, failcount);
521         }
522         if(verified == 0) {
523             failcount += 1;
524         }
525         jsonObjectSetNumber( countobject, failcount );
526         osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
527         jsonObjectFree(countobject);
528     }
529     free(countkey);
530
531     return verified;
532 }
533
534 /*
535         Adds the authentication token to the user cache.  The timeout for the
536         auth token is based on the type of login as well as (if type=='opac')
537         the org location id.
538         Returns the event that should be returned to the user.
539         Event must be freed
540 */
541 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
542                 const char* type, int orgloc, const char* workstation ) {
543
544         oilsEvent* response = NULL;
545
546     jsonObject* params = jsonNewObject(NULL);
547     jsonObjectSetKey(params, "user_id", 
548         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
549     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
550     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
551     if (workstation) 
552         jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
553
554     jsonObject* authEvt = oilsUtilsQuickReq(
555         "open-ils.auth_internal",
556         "open-ils.auth_internal.session.create", params);
557     jsonObjectFree(params);
558
559     if (authEvt) {
560
561         response = oilsNewEvent2(
562             OSRF_LOG_MARK, 
563             jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
564             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
565         );
566
567         jsonObjectFree(authEvt);
568
569     } else {
570         osrfLogError(OSRF_LOG_MARK, 
571             "Error caching auth session in open-ils.auth_internal");
572     }
573
574     return response;
575 }
576
577
578 /**
579         @brief Implement the "complete" method.
580         @param ctx The method context.
581         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
582         client to indicate completion; a positive integer if successful but no such STATUS
583         message has been sent.
584
585         Method parameters:
586         - a hash with some combination of the following elements:
587                 - "username"
588                 - "barcode"
589                 - "password" (hashed with the cached seed; not plaintext)
590                 - "type"
591                 - "org"
592                 - "workstation"
593                 - "agent" (what software/interface/3rd-party is making the request)
594                 - "nonce" optional login seed to differentiate logins using the same username.
595
596         The password is required.  Either a username or a barcode must also be present.
597
598         Return to client: Intermediate authentication seed.
599
600         Validate the password, using the username if available, or the barcode if not.  The
601         user must be active, and not barred from logging on.  The barcode, if used for
602         authentication, must be active as well.  The workstation, if specified, must be valid.
603
604         Upon deciding whether to allow the logon, return a corresponding event to the client.
605 */
606 int oilsAuthComplete( osrfMethodContext* ctx ) {
607     OSRF_METHOD_VERIFY_CONTEXT(ctx);
608
609     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
610
611     const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
612     const char* identifier  = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
613     const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
614     const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
615     int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
616     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
617     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
618     const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
619     const char* nonce       = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
620
621     const char* ws = (workstation) ? workstation : "";
622     if (!nonce) nonce = "";
623
624     // we no longer care how the identifier reaches us, 
625     // as long as we have one.
626     if (!identifier) {
627         if (uname) {
628             identifier = uname;
629         } else if (barcode) {
630             identifier = barcode;
631         }
632     }
633
634     if (!identifier) {
635         return osrfAppRequestRespondException(ctx->session, ctx->request,
636             "username/barcode and password required for method: %s", 
637             ctx->method->name);
638     }
639
640     osrfLogInfo(OSRF_LOG_MARK, 
641         "Patron completing authentication with identifer %s", identifier);
642
643     /* Use __FILE__, harmless_line_number for creating
644      * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
645      * giving away information about why an authentication attempt failed.
646      */
647     int harmless_line_number = __LINE__;
648
649     if( !type )
650          type = OILS_AUTH_STAFF;
651
652     oilsEvent* response = NULL; // free
653     jsonObject* userObj = NULL; // free
654     int card_active = 1; // boolean; assume active until proven otherwise
655     int using_card  = 0; // true if this is a barcode login
656
657     char* cache_key = va_list_to_string(
658         "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
659     jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
660
661     if (!cacheObj) {
662         return osrfAppRequestRespondException(ctx->session,
663             ctx->request, "No authentication seed found. "
664             "open-ils.auth.authenticate.init must be called first "
665             " (check that memcached is running and can be connected to) "
666         );
667     }
668
669     int user_id = jsonObjectGetNumber(
670         jsonObjectGetKeyConst(cacheObj, "user_id"));
671
672     jsonObject* param = jsonNewNumberObject(user_id); // free
673     userObj = oilsUtilsCStoreReq(
674         "open-ils.cstore.direct.actor.user.retrieve", param);
675     jsonObjectFree(param);
676
677     using_card = (jsonObjectGetKeyConst(cacheObj, "barcode") != NULL);
678
679     if (using_card) {
680         // see if the card is inactive
681
682                 jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", identifier);
683                 jsonObject* card = oilsUtilsCStoreReq(
684                         "open-ils.cstore.direct.actor.card.search", params);
685                 jsonObjectFree(params);
686
687         if (card) {
688             if (card->type != JSON_NULL) {
689                             char* card_active_str = oilsFMGetString(card, "active");
690                             card_active = oilsUtilsIsDBTrue(card_active_str);
691                             free(card_active_str);
692                         }
693             jsonObjectFree(card);
694                 }
695         }
696
697         int     barred = 0, deleted = 0;
698         char   *barred_str, *deleted_str;
699
700         if (userObj) {
701                 barred_str = oilsFMGetString(userObj, "barred");
702                 barred = oilsUtilsIsDBTrue(barred_str);
703                 free(barred_str);
704
705                 deleted_str = oilsFMGetString(userObj, "deleted");
706                 deleted = oilsUtilsIsDBTrue(deleted_str);
707                 free(deleted_str);
708         }
709
710         if(!userObj || barred || deleted) {
711                 response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
712                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
713                                 uname, (barcode ? barcode : "(none)"), ws );
714                 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
715                 oilsEventFree(response);
716                 return 0;           // No such user
717         }
718
719         // Such a user exists and isn't barred or deleted.
720         // Now see if he or she has the right credentials.
721         int passOK = oilsAuthVerifyPassword(
722         ctx, user_id, identifier, password, nonce);
723
724         if( passOK < 0 ) {
725                 jsonObjectFree(userObj);
726                 return passOK;
727         }
728
729         // See if the account is active
730         char* active = oilsFMGetString(userObj, "active");
731         if( !oilsUtilsIsDBTrue(active) ) {
732                 if( passOK )
733                         response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
734                 else
735                         response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
736
737                 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
738                 oilsEventFree(response);
739                 jsonObjectFree(userObj);
740                 free(active);
741                 return 0;
742         }
743         free(active);
744
745         if( !card_active ) {
746                 osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
747                 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
748                 osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
749                 oilsEventFree( response );
750                 jsonObjectFree( userObj );
751                 return 0;
752         }
753
754
755         // See if the user is even allowed to log in
756         if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
757                 jsonObjectFree(userObj);
758                 return 0;
759         }
760
761         char* freeable_uname = NULL;
762         if(!uname) {
763                 uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
764         }
765
766         if( passOK ) { // login successful  
767         
768                 char* ewhat = "login";
769
770                 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
771                         response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
772                         ewhat = "verify";
773
774                 } else {
775                         response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
776                 }
777
778                 oilsUtilsTrackUserActivity(
779                         oilsFMGetObjectId(userObj), 
780                         ewho, ewhat, 
781                         osrfAppSessionGetIngress()
782                 );
783
784         } else {
785                 response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
786                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
787                                 uname, (barcode ? barcode : "(none)"), ws );
788         }
789
790         jsonObjectFree(userObj);
791         osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
792         oilsEventFree(response);
793
794         if(freeable_uname)
795                 free(freeable_uname);
796
797         return 0;
798 }
799
800
801
802 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
803         OSRF_METHOD_VERIFY_CONTEXT(ctx);
804
805         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
806         jsonObject* resp = NULL;
807
808         if( authToken ) {
809                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
810                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
811                 osrfCacheRemove(key);
812                 resp = jsonNewObject(authToken); /**/
813                 free(key);
814         }
815
816         osrfAppRespondComplete( ctx, resp );
817         jsonObjectFree(resp);
818         return 0;
819 }
820
821 /**
822  * Fetches the user object from the database and updates the user object in 
823  * the cache object, which then has to be re-inserted into the cache.
824  * User object is retrieved inside a transaction to avoid replication issues.
825  */
826 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
827     int reqid, userId;
828     osrfAppSession* session;
829         osrfMessage* omsg;
830     jsonObject *param, *userObj, *newUserObj = NULL;
831
832     userObj = jsonObjectGetKey( cacheObj, "userobj" );
833     userId = oilsFMGetObjectId( userObj );
834
835     session = osrfAppSessionClientInit( "open-ils.cstore" );
836     osrfAppSessionConnect(session);
837
838     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
839         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
840
841     if(omsg) {
842
843         osrfMessageFree(omsg);
844         param = jsonNewNumberObject(userId);
845         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
846             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
847         jsonObjectFree(param);
848
849         if(omsg) {
850             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
851             osrfMessageFree(omsg);
852             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
853                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
854             osrfMessageFree(omsg);
855         }
856     }
857
858     osrfAppSessionFree(session); // calls disconnect internally
859
860     if(newUserObj) {
861
862         // ws_ou and wsid are ephemeral and need to be manually propagated
863         // oilsFMSetString dupe()'s internally, no need to clone the string
864         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
865         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
866
867         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
868         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
869         return 1;
870     } 
871
872     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
873     return 0;
874 }
875
876 /**
877         Resets the auth login timeout
878         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
879 */
880 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
881         if(!authToken) return NULL;
882
883         oilsEvent* evt = NULL;
884         time_t timeout;
885
886         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
887         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
888         jsonObject* cacheObj = osrfCacheGetObject( key );
889
890         if(!cacheObj) {
891                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
892                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
893
894         } else {
895
896         if(reloadUser) {
897             _oilsAuthReloadUser(cacheObj);
898         }
899
900                 // Determine a new timeout value
901                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
902                 if( endtime_obj ) {
903                         // Extend the current endtime by a fixed amount
904                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
905                         int reset_interval = DEFAULT_RESET_INTERVAL;
906                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
907                                 cacheObj, "reset_interval" );
908                         if( reset_interval_obj ) {
909                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
910                                 if( reset_interval <= 0 )
911                                         reset_interval = DEFAULT_RESET_INTERVAL;
912                         }
913
914                         time_t now = time( NULL );
915                         time_t new_endtime = now + reset_interval;
916                         if( new_endtime > endtime ) {
917                                 // Keep the session alive a little longer
918                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
919                                 timeout = reset_interval;
920                                 osrfCachePutObject( key, cacheObj, timeout );
921                         } else {
922                                 // The session isn't close to expiring, so don't reset anything.
923                                 // Just report the time remaining.
924                                 timeout = endtime - now;
925                         }
926                 } else {
927                         // Reapply the existing timeout from the current time
928                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
929                         osrfCachePutObject( key, cacheObj, timeout );
930                 }
931
932                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
933                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
934                 jsonObjectFree(payload);
935                 jsonObjectFree(cacheObj);
936         }
937
938         free(key);
939         return evt;
940 }
941
942 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
943         OSRF_METHOD_VERIFY_CONTEXT(ctx);
944         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
945     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
946         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
947         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
948         oilsEventFree(evt);
949         return 0;
950 }
951
952
953 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
954         OSRF_METHOD_VERIFY_CONTEXT(ctx);
955     bool returnFull = false;
956
957         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
958
959     if(ctx->params->size > 1) {
960         // caller wants full cached object, with authtime, etc.
961         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
962         if(rt && strcmp(rt, "0") != 0) 
963             returnFull = true;
964     }
965
966         jsonObject* cacheObj = NULL;
967         oilsEvent* evt = NULL;
968
969         if( authToken ){
970
971                 // Reset the timeout to keep the session alive
972                 evt = _oilsAuthResetTimeout(authToken, 0);
973
974                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
975                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
976
977                 } else {
978
979                         // Retrieve the cached session object
980                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
981                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
982                         cacheObj = osrfCacheGetObject( key );
983                         if(cacheObj) {
984                                 // Return a copy of the cached user object
985                 if(returnFull)
986                                     osrfAppRespondComplete( ctx, cacheObj);
987                 else
988                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
989                                 jsonObjectFree(cacheObj);
990                         } else {
991                                 // Auth token is invalid or expired
992                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
993                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
994                                 oilsEventFree(evt2);
995                         }
996                         free(key);
997                 }
998
999         } else {
1000
1001                 // No session
1002                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1003                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
1004         }
1005
1006         if(evt)
1007                 oilsEventFree(evt);
1008
1009         return 0;
1010 }