Brute Force protection for authentication
[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
10 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
11 #define OILS_AUTH_COUNT_SFFX "_count"
12
13 #define MODULENAME "open-ils.auth"
14
15 #define OILS_AUTH_OPAC "opac"
16 #define OILS_AUTH_STAFF "staff"
17 #define OILS_AUTH_TEMP "temp"
18 #define OILS_AUTH_PERSIST "persist"
19
20 // Default time for extending a persistent session: ten minutes
21 #define DEFAULT_RESET_INTERVAL 10 * 60
22
23 #define OILS_AUTH_COUNT_MAX 10
24 #define OILS_AUTH_COUNT_INTERVAL 90
25
26 int osrfAppInitialize();
27 int osrfAppChildInit();
28
29 static long _oilsAuthOPACTimeout = 0;
30 static long _oilsAuthStaffTimeout = 0;
31 static long _oilsAuthOverrideTimeout = 0;
32 static long _oilsAuthPersistTimeout = 0;
33
34
35 /**
36         @brief Initialize the application by registering functions for method calls.
37         @return Zero in all cases.
38 */
39 int osrfAppInitialize() {
40
41         osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
42
43         /* load and parse the IDL */
44         if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
45
46         osrfAppRegisterMethod(
47                 MODULENAME,
48                 "open-ils.auth.authenticate.init",
49                 "oilsAuthInit",
50                 "Start the authentication process and returns the intermediate authentication seed"
51                 " PARAMS( username )", 1, 0 );
52
53         osrfAppRegisterMethod(
54                 MODULENAME,
55                 "open-ils.auth.authenticate.complete",
56                 "oilsAuthComplete",
57                 "Completes the authentication process.  Returns an object like so: "
58                 "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
59                 "token and authtime is the number of seconds the session will be active"
60                 "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
61                 "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
62                 "org_id is the location at which the login should be considered "
63                 "active for login timeout purposes", 1, 0 );
64
65         osrfAppRegisterMethod(
66                 MODULENAME,
67                 "open-ils.auth.session.retrieve",
68                 "oilsAuthSessionRetrieve",
69                 "Pass in the auth token and this retrieves the user object.  The auth "
70                 "timeout is reset when this call is made "
71                 "Returns the user object (password blanked) for the given login session "
72                 "PARAMS( authToken )", 1, 0 );
73
74         osrfAppRegisterMethod(
75                 MODULENAME,
76                 "open-ils.auth.session.delete",
77                 "oilsAuthSessionDelete",
78                 "Destroys the given login session "
79                 "PARAMS( authToken )",  1, 0 );
80
81         osrfAppRegisterMethod(
82                 MODULENAME,
83                 "open-ils.auth.session.reset_timeout",
84                 "oilsAuthResetTimeout",
85                 "Resets the login timeout for the given session "
86                 "Returns an ILS Event with payload = session_timeout of session "
87                 "if found, otherwise returns the NO_SESSION event"
88                 "PARAMS( authToken )", 1, 0 );
89
90         return 0;
91 }
92
93 /**
94         @brief Dummy placeholder for initializing a server drone.
95
96         There is nothing to do, so do nothing.
97 */
98 int osrfAppChildInit() {
99         return 0;
100 }
101
102 /**
103         @brief Implement the "init" method.
104         @param ctx The method context.
105         @return Zero if successful, or -1 if not.
106
107         Method parameters:
108         - username
109
110         Return to client: Intermediate authentication seed.
111
112         Combine the username with a timestamp and process ID, and take an md5 hash of the result.
113         Store the hash in memcache, with a key based on the username.  Then return the hash to
114         the client.
115
116         However: if the username includes one or more embedded blank spaces, return a dummy
117         hash without storing anything in memcache.  The dummy will never match a stored hash, so
118         any attempt to authenticate with it will fail.
119 */
120 int oilsAuthInit( osrfMethodContext* ctx ) {
121         OSRF_METHOD_VERIFY_CONTEXT(ctx);
122
123         char* username  = jsonObjectToSimpleString( jsonObjectGetIndex(ctx->params, 0) );
124         if( username ) {
125
126                 jsonObject* resp;
127
128                 if( strchr( username, ' ' ) ) {
129
130                         // Embedded spaces are not allowed in a username.  Use "x" as a dummy
131                         // seed.  It will never be a valid seed because 'x' is not a hex digit.
132                         resp = jsonNewObject( "x" );
133
134                 } else {
135
136                         // Build a key and a seed; store them in memcache.
137                         char* key  = va_list_to_string( "%s%s", OILS_AUTH_CACHE_PRFX, username );
138                         char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, username, OILS_AUTH_COUNT_SFFX );
139                         char* seed = md5sum( "%d.%ld.%s", (int) time(NULL), (long) getpid(), username );
140                         jsonObject* countobject = osrfCacheGetObject( countkey );
141                         if(!countobject) {
142                                 countobject = jsonNewNumberObject( (double) 0 );
143                         }
144                         osrfCachePutString( key, seed, 30 );
145                         osrfCachePutObject( countkey, countobject, OILS_AUTH_COUNT_INTERVAL );
146
147                         osrfLogDebug( OSRF_LOG_MARK, "oilsAuthInit(): has seed %s and key %s", seed, key );
148
149                         // Build a returnable object containing the seed.
150                         resp = jsonNewObject( seed );
151
152                         free( seed );
153                         free( key );
154                         free( countkey );
155                         jsonObjectFree( countobject );
156                 }
157
158                 // Return the seed to the client.
159                 osrfAppRespondComplete( ctx, resp );
160
161                 jsonObjectFree(resp);
162                 free(username);
163                 return 0;
164         }
165
166         return -1;  // Error: no username parameter
167 }
168
169 /**
170         Verifies that the user has permission to login with the
171         given type.  If the permission fails, an oilsEvent is returned
172         to the caller.
173         @return -1 if the permission check failed, 0 if the permission
174         is granted
175 */
176 static int oilsAuthCheckLoginPerm(
177                 osrfMethodContext* ctx, const jsonObject* userObj, const char* type ) {
178
179         if(!(userObj && type)) return -1;
180         oilsEvent* perm = NULL;
181
182         if(!strcasecmp(type, OILS_AUTH_OPAC)) {
183                 char* permissions[] = { "OPAC_LOGIN" };
184                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
185
186         } else if(!strcasecmp(type, OILS_AUTH_STAFF)) {
187                 char* permissions[] = { "STAFF_LOGIN" };
188                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
189
190         } else if(!strcasecmp(type, OILS_AUTH_TEMP)) {
191                 char* permissions[] = { "STAFF_LOGIN" };
192                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
193         } else if(!strcasecmp(type, OILS_AUTH_PERSIST)) {
194                 char* permissions[] = { "PERSISTENT_LOGIN" };
195                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
196         }
197
198         if(perm) {
199                 osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
200                 oilsEventFree(perm);
201                 return -1;
202         }
203
204         return 0;
205 }
206
207 /**
208         Returns 1 if the password provided matches the user's real password
209         Returns 0 otherwise
210         Returns -1 on error
211 */
212 /**
213         @brief Verify the password received from the client.
214         @param ctx The method context.
215         @param userObj An object from the database, representing the user.
216         @param password An obfuscated password received from the client.
217         @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
218
219         (None of the so-called "passwords" used here are in plaintext.  All have been passed
220         through at least one layer of hashing to obfuscate them.)
221
222         Take the password from the user object.  Append it to the username seed from memcache,
223         as stored previously by a call to the init method.  Take an md5 hash of the result.
224         Then compare this hash to the password received from the client.
225
226         In order for the two to match, other than by dumb luck, the client had to construct
227         the password it passed in the same way.  That means it neded to know not only the
228         original password (either hashed or plaintext), but also the seed.  The latter requirement
229         means that the client process needs either to be the same process that called the init
230         method or to receive the seed from the process that did so.
231 */
232 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
233                 const jsonObject* userObj, const char* uname, const char* password ) {
234
235         // Get the username seed, as stored previously in memcache by the init method
236         char* seed = osrfCacheGetString( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
237         if(!seed) {
238                 return osrfAppRequestRespondException( ctx->session,
239                         ctx->request, "No authentication seed found. "
240                         "open-ils.auth.authenticate.init must be called first "
241                         " (check that memcached is running and can be connected to) "
242                 );
243         }
244
245         // Get the hashed password from the user object
246         char* realPassword = oilsFMGetString( userObj, "passwd" );
247
248         osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
249         osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );
250
251         // Concatenate them and take an MD5 hash of the result
252         char* maskedPw = md5sum( "%s%s", seed, realPassword );
253
254         free(realPassword);
255         free(seed);
256
257         if( !maskedPw ) {
258                 // This happens only if md5sum() runs out of memory
259                 free( maskedPw );
260                 return -1;  // md5sum() ran out of memory
261         }
262
263         osrfLogDebug(OSRF_LOG_MARK,  "oilsAuth generated masked password %s. "
264                         "Testing against provided password %s", maskedPw, password );
265
266         int ret = 0;
267         if( !strcmp( maskedPw, password ) )
268                 ret = 1;
269
270         free(maskedPw);
271
272         char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, OILS_AUTH_COUNT_SFFX );
273         jsonObject* countobject = osrfCacheGetObject( countkey );
274         if(countobject) {
275                 double failcount = jsonObjectGetNumber( countobject );
276                 if(failcount >= OILS_AUTH_COUNT_MAX) {
277                         ret = 0;
278             osrfLogInternal(OSRF_LOG_MARK, "oilsAuth found too many recent failures: %d, forcing failure state.", failcount);
279                 }
280                 if(ret == 0) {
281                         failcount += 1;
282                 }
283                 jsonObjectSetNumber( countobject, failcount );
284                 osrfCachePutObject( countkey, countobject, OILS_AUTH_COUNT_INTERVAL );
285                 jsonObjectFree(countobject);
286         }
287         free(countkey);
288
289         return ret;
290 }
291
292 /**
293         @brief Determine the login timeout.
294         @param userObj Pointer to an object describing the user.
295         @param type Pointer to one of four possible character strings identifying the login type.
296         @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
297         @return The length of the timeout, in seconds.
298
299         The default timeout value comes from the configuration file, and depends on the
300         login type.
301
302         The default may be overridden by a corresponding org unit setting.  The @a orgloc
303         parameter says what org unit to use for the lookup.  If @a orgloc <= 0, or if the
304         lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
305         instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).
306
307         Whether defined in the config file or in an org unit setting, a timeout value may be
308         expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
309         space) or as an interval string to be translated into seconds by PostgreSQL.
310 */
311 static long oilsAuthGetTimeout( const jsonObject* userObj, const char* type, int orgloc ) {
312
313         if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
314
315                 jsonObject* value_obj;
316
317                 value_obj = osrf_settings_host_value_object(
318                         "/apps/open-ils.auth/app_settings/default_timeout/opac" );
319                 _oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
320                 jsonObjectFree(value_obj);
321                 if( -1 == _oilsAuthOPACTimeout ) {
322                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
323                         _oilsAuthOPACTimeout = 0;
324                 }
325
326                 value_obj = osrf_settings_host_value_object(
327                         "/apps/open-ils.auth/app_settings/default_timeout/staff" );
328                 _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
329                 jsonObjectFree(value_obj);
330                 if( -1 == _oilsAuthStaffTimeout ) {
331                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
332                         _oilsAuthStaffTimeout = 0;
333                 }
334
335                 value_obj = osrf_settings_host_value_object(
336                         "/apps/open-ils.auth/app_settings/default_timeout/temp" );
337                 _oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
338                 jsonObjectFree(value_obj);
339                 if( -1 == _oilsAuthOverrideTimeout ) {
340                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
341                         _oilsAuthOverrideTimeout = 0;
342                 }
343
344                 value_obj = osrf_settings_host_value_object(
345                         "/apps/open-ils.auth/app_settings/default_timeout/persist" );
346                 _oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
347                 jsonObjectFree(value_obj);
348                 if( -1 == _oilsAuthPersistTimeout ) {
349                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
350                         _oilsAuthPersistTimeout = 0;
351                 }
352
353                 osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
354                         "opac => %ld : staff => %ld : temp => %ld : persist => %ld",
355                         _oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
356                         _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
357         }
358
359         int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
360         if(orgloc < 1)
361                 orgloc = home_ou;
362
363         char* setting = NULL;
364         long default_timeout = 0;
365
366         if( !strcmp( type, OILS_AUTH_OPAC )) {
367                 setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
368                 default_timeout = _oilsAuthOPACTimeout;
369         } else if( !strcmp( type, OILS_AUTH_STAFF )) {
370                 setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
371                 default_timeout = _oilsAuthStaffTimeout;
372         } else if( !strcmp( type, OILS_AUTH_TEMP )) {
373                 setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
374                 default_timeout = _oilsAuthOverrideTimeout;
375         } else if( !strcmp( type, OILS_AUTH_PERSIST )) {
376                 setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
377                 default_timeout = _oilsAuthPersistTimeout;
378         }
379
380         // Get the org unit setting, if there is one.
381         char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
382         if(!timeout) {
383                 if( orgloc != home_ou ) {
384                         osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
385                                 "trying home_ou %d", orgloc, home_ou );
386                         timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
387                 }
388         }
389
390         if(!timeout)
391                 return default_timeout;   // No override from org unit setting
392
393         // Translate the org unit setting to a number
394         long t;
395         if( !*timeout ) {
396                 osrfLogWarning( OSRF_LOG_MARK,
397                         "Timeout org unit setting is an empty string for %s login; using default",
398                         timeout, type );
399                 t = default_timeout;
400         } else {
401                 // Treat timeout string as an interval, and convert it to seconds
402                 t = oilsUtilsIntervalToSeconds( timeout );
403                 if( -1 == t ) {
404                         // Unable to convert; possibly an invalid interval string
405                         osrfLogError( OSRF_LOG_MARK,
406                                 "Unable to convert timeout interval \"%s\" for %s login; using default",
407                                 timeout, type );
408                         t = default_timeout;
409                 }
410         }
411
412         free(timeout);
413         return t;
414 }
415
416 /*
417         Adds the authentication token to the user cache.  The timeout for the
418         auth token is based on the type of login as well as (if type=='opac')
419         the org location id.
420         Returns the event that should be returned to the user.
421         Event must be freed
422 */
423 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
424                 const char* type, int orgloc, const char* workstation ) {
425
426         oilsEvent* response;
427
428         long timeout;
429         char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "ws_ou"));
430         if(wsorg) { /* if there is a workstation, use it for the timeout */
431                 osrfLogDebug( OSRF_LOG_MARK,
432                                 "Auth session trying workstation id %d for auth timeout", atoi(wsorg));
433                 timeout = oilsAuthGetTimeout( userObj, type, atoi(wsorg) );
434                 free(wsorg);
435         } else {
436                 osrfLogDebug( OSRF_LOG_MARK,
437                                 "Auth session trying org from param [%d] for auth timeout", orgloc );
438                 timeout = oilsAuthGetTimeout( userObj, type, orgloc );
439         }
440         osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %ld", uname, timeout );
441
442         char* string = va_list_to_string(
443                         "%d.%ld.%s", (long) getpid(), time(NULL), uname );
444         char* authToken = md5sum(string);
445         char* authKey = va_list_to_string(
446                         "%s%s", OILS_AUTH_CACHE_PRFX, authToken );
447
448         const char* ws = (workstation) ? workstation : "";
449         osrfLogActivity(OSRF_LOG_MARK,
450                 "successful login: username=%s, authtoken=%s, workstation=%s", uname, authToken, ws );
451
452         oilsFMSetString( userObj, "passwd", "" );
453         jsonObject* cacheObj = jsonParseFmt( "{\"authtime\": %ld}", timeout );
454         jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
455
456         if( !strcmp( type, OILS_AUTH_PERSIST )) {
457                 // Add entries for endtime and reset_interval, so that we can gracefully
458                 // extend the session a bit if the user is active toward the end of the 
459                 // timeout originally specified.
460                 time_t endtime = time( NULL ) + timeout;
461                 jsonObjectSetKey( cacheObj, "endtime", jsonNewNumberObject( (double) endtime ) );
462
463                 // Reset interval is hard-coded for now, but if we ever want to make it
464                 // configurable, this is the place to do it:
465                 jsonObjectSetKey( cacheObj, "reset_interval",
466                         jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL ));
467         }
468
469         osrfCachePutObject( authKey, cacheObj, (time_t) timeout );
470         jsonObjectFree(cacheObj);
471         osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
472         jsonObject* payload = jsonParseFmt(
473                 "{ \"authtoken\": \"%s\", \"authtime\": %ld }", authToken, timeout );
474
475         response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
476         free(string); free(authToken); free(authKey);
477         jsonObjectFree(payload);
478
479         return response;
480 }
481
482 static oilsEvent* oilsAuthVerifyWorkstation(
483                 const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
484         osrfLogInfo(OSRF_LOG_MARK, "Attaching workstation to user at login: %s", ws);
485         jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
486         if(!workstation || workstation->type == JSON_NULL) {
487                 jsonObjectFree(workstation);
488                 return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
489         }
490         long wsid = oilsFMGetObjectId(workstation);
491         LONG_TO_STRING(wsid);
492         char* orgid = oilsFMGetString(workstation, "owning_lib");
493         oilsFMSetString(userObj, "wsid", LONGSTR);
494         oilsFMSetString(userObj, "ws_ou", orgid);
495         free(orgid);
496         jsonObjectFree(workstation);
497         return NULL;
498 }
499
500
501
502 /**
503         @brief Implement the "complete" method.
504         @param ctx The method context.
505         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
506         client to indicate completion; a positive integer if successful but no such STATUS
507         message has been sent.
508
509         Method parameters:
510         - a hash with some combination of the following elements:
511                 - "username"
512                 - "barcode"
513                 - "password" (hashed with the cached seed; not plaintext)
514                 - "type"
515                 - "org"
516                 - "workstation"
517
518         The password is required.  Either a username or a barcode must also be present.
519
520         Return to client: Intermediate authentication seed.
521
522         Validate the password, using the username if available, or the barcode if not.  The
523         user must be active, and not barred from logging on.  The barcode, if used for
524         authentication, must be active as well.  The workstation, if specified, must be valid.
525
526         Upon deciding whether to allow the logon, return a corresponding event to the client.
527 */
528 int oilsAuthComplete( osrfMethodContext* ctx ) {
529         OSRF_METHOD_VERIFY_CONTEXT(ctx);
530
531         const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
532
533         const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
534         const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
535         const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
536         int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
537         const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
538         const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
539
540         const char* ws = (workstation) ? workstation : "";
541
542         if( !type )
543                  type = OILS_AUTH_STAFF;
544
545         if( !( (uname || barcode) && password) ) {
546                 return osrfAppRequestRespondException( ctx->session, ctx->request,
547                         "username/barcode and password required for method: %s", ctx->method->name );
548         }
549
550         oilsEvent* response = NULL;
551         jsonObject* userObj = NULL;
552         int card_active     = 1;      // boolean; assume active until proven otherwise
553
554         // Fetch a row from the actor.usr table, by username if available,
555         // or by barcode if not.
556         if(uname) {
557                 userObj = oilsUtilsFetchUserByUsername( uname );
558                 if( userObj && JSON_NULL == userObj->type ) {
559                         jsonObjectFree( userObj );
560                         userObj = NULL;         // username not found
561                 }
562         }
563         else if(barcode) {
564                 // Read from actor.card by barcode
565
566                 osrfLogInfo( OSRF_LOG_MARK, "Fetching user by barcode %s", barcode );
567
568                 jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
569                 jsonObject* card = oilsUtilsQuickReq(
570                         "open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
571                 jsonObjectFree( params );
572
573                 if( card && card->type != JSON_NULL ) {
574                         // Determine whether the card is active
575                         char* card_active_str = oilsFMGetString( card, "active" );
576                         card_active = oilsUtilsIsDBTrue( card_active_str );
577                         free( card_active_str );
578
579                         // Look up the user who owns the card
580                         char* userid = oilsFMGetString( card, "usr" );
581                         jsonObjectFree( card );
582                         params = jsonParseFmt( "[%s]", userid );
583                         free( userid );
584                         userObj = oilsUtilsQuickReq(
585                                         "open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params );
586                         jsonObjectFree( params );
587                         if( userObj && JSON_NULL == userObj->type ) {
588                                 // user not found (shouldn't happen, due to foreign key)
589                                 jsonObjectFree( userObj );
590                                 userObj = NULL;
591                         }
592                 }
593         }
594
595         if(!userObj) {
596                 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
597                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
598                                 uname, (barcode ? barcode : "(none)"), ws );
599                 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
600                 oilsEventFree(response);
601                 return 0;           // No such user
602         }
603
604         // Such a user exists.  Now see if he or she has the right credentials.
605         int passOK = -1;
606         if(uname)
607                 passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
608         else if (barcode)
609                 passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password );
610
611         if( passOK < 0 ) {
612                 jsonObjectFree(userObj);
613                 return passOK;
614         }
615
616         // See if the account is active
617         char* active = oilsFMGetString(userObj, "active");
618         if( !oilsUtilsIsDBTrue(active) ) {
619                 if( passOK )
620                         response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
621                 else
622                         response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
623
624                 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
625                 oilsEventFree(response);
626                 jsonObjectFree(userObj);
627                 free(active);
628                 return 0;
629         }
630         free(active);
631
632         osrfLogInfo( OSRF_LOG_MARK, "Fetching card by barcode %s", barcode );
633
634         if( !card_active ) {
635                 osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
636                 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
637                 osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
638                 oilsEventFree( response );
639                 jsonObjectFree( userObj );
640                 return 0;
641         }
642
643
644         // See if the user is even allowed to log in
645         if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
646                 jsonObjectFree(userObj);
647                 return 0;
648         }
649
650         // If a workstation is defined, add the workstation info
651         if( workstation != NULL ) {
652                 osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
653                 response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
654                 if(response) {
655                         jsonObjectFree(userObj);
656                         osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
657                         oilsEventFree(response);
658                         return 0;
659                 }
660
661         } else {
662                 // Otherwise, use the home org as the workstation org on the user
663                 char* orgid = oilsFMGetString(userObj, "home_ou");
664                 oilsFMSetString(userObj, "ws_ou", orgid);
665                 free(orgid);
666         }
667
668         char* freeable_uname = NULL;
669         if(!uname) {
670                 uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
671         }
672
673         if( passOK ) {
674                 response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
675
676         } else {
677                 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
678                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
679                                 uname, (barcode ? barcode : "(none)"), ws );
680         }
681
682         jsonObjectFree(userObj);
683         osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
684         oilsEventFree(response);
685
686         if(freeable_uname)
687                 free(freeable_uname);
688
689         return 0;
690 }
691
692
693
694 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
695         OSRF_METHOD_VERIFY_CONTEXT(ctx);
696
697         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
698         jsonObject* resp = NULL;
699
700         if( authToken ) {
701                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
702                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
703                 osrfCacheRemove(key);
704                 resp = jsonNewObject(authToken); /**/
705                 free(key);
706         }
707
708         osrfAppRespondComplete( ctx, resp );
709         jsonObjectFree(resp);
710         return 0;
711 }
712
713 /**
714  * Fetches the user object from the database and updates the user object in 
715  * the cache object, which then has to be re-inserted into the cache.
716  * User object is retrieved inside a transaction to avoid replication issues.
717  */
718 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
719     int reqid, userId;
720     osrfAppSession* session;
721         osrfMessage* omsg;
722     jsonObject *param, *userObj, *newUserObj;
723
724     userObj = jsonObjectGetKey( cacheObj, "userobj" );
725     userId = oilsFMGetObjectId( userObj );
726
727     session = osrfAppSessionClientInit( "open-ils.cstore" );
728     osrfAppSessionConnect(session);
729
730     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
731         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
732
733     if(omsg) {
734
735         osrfMessageFree(omsg);
736         param = jsonNewNumberObject(userId);
737         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
738             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
739         jsonObjectFree(param);
740
741         if(omsg) {
742             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
743             osrfMessageFree(omsg);
744             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
745                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
746             osrfMessageFree(omsg);
747         }
748     }
749
750     osrfAppSessionFree(session); // calls disconnect internally
751
752     if(newUserObj) {
753
754         // ws_ou and wsid are ephemeral and need to be manually propagated
755         // oilsFMSetString dupe()'s internally, no need to clone the string
756         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
757         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
758
759         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
760         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
761         return 1;
762     } 
763
764     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
765     return 0;
766 }
767
768 /**
769         Resets the auth login timeout
770         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
771 */
772 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
773         if(!authToken) return NULL;
774
775         oilsEvent* evt = NULL;
776         time_t timeout;
777
778         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
779         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
780         jsonObject* cacheObj = osrfCacheGetObject( key );
781
782         if(!cacheObj) {
783                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
784                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
785
786         } else {
787
788         if(reloadUser) {
789             _oilsAuthReloadUser(cacheObj);
790         }
791
792                 // Determine a new timeout value
793                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
794                 if( endtime_obj ) {
795                         // Extend the current endtime by a fixed amount
796                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
797                         int reset_interval = DEFAULT_RESET_INTERVAL;
798                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
799                                 cacheObj, "reset_interval" );
800                         if( reset_interval_obj ) {
801                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
802                                 if( reset_interval <= 0 )
803                                         reset_interval = DEFAULT_RESET_INTERVAL;
804                         }
805
806                         time_t now = time( NULL );
807                         time_t new_endtime = now + reset_interval;
808                         if( new_endtime > endtime ) {
809                                 // Keep the session alive a little longer
810                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
811                                 timeout = reset_interval;
812                                 osrfCachePutObject( key, cacheObj, timeout );
813                         } else {
814                                 // The session isn't close to expiring, so don't reset anything.
815                                 // Just report the time remaining.
816                                 timeout = endtime - now;
817                         }
818                 } else {
819                         // Reapply the existing timeout from the current time
820                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
821                         osrfCachePutObject( key, cacheObj, timeout );
822                 }
823
824                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
825                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
826                 jsonObjectFree(payload);
827                 jsonObjectFree(cacheObj);
828         }
829
830         free(key);
831         return evt;
832 }
833
834 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
835         OSRF_METHOD_VERIFY_CONTEXT(ctx);
836         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
837     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
838         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
839         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
840         oilsEventFree(evt);
841         return 0;
842 }
843
844
845 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
846         OSRF_METHOD_VERIFY_CONTEXT(ctx);
847     bool returnFull = false;
848
849         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
850
851     if(ctx->params->size > 1) {
852         // caller wants full cached object, with authtime, etc.
853         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
854         if(rt && strcmp(rt, "0") != 0) 
855             returnFull = true;
856     }
857
858         jsonObject* cacheObj = NULL;
859         oilsEvent* evt = NULL;
860
861         if( authToken ){
862
863                 // Reset the timeout to keep the session alive
864                 evt = _oilsAuthResetTimeout(authToken, 0);
865
866                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
867                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
868
869                 } else {
870
871                         // Retrieve the cached session object
872                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
873                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
874                         cacheObj = osrfCacheGetObject( key );
875                         if(cacheObj) {
876                                 // Return a copy of the cached user object
877                 if(returnFull)
878                                     osrfAppRespondComplete( ctx, cacheObj);
879                 else
880                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
881                                 jsonObjectFree(cacheObj);
882                         } else {
883                                 // Auth token is invalid or expired
884                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
885                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
886                                 oilsEventFree(evt2);
887                         }
888                         free(key);
889                 }
890
891         } else {
892
893                 // No session
894                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
895                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
896         }
897
898         if(evt)
899                 oilsEventFree(evt);
900
901         return 0;
902 }