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