LP#1468422 Internal auth service WIP
authorBill Erickson <berickxx@gmail.com>
Thu, 19 Nov 2015 20:00:20 +0000 (15:00 -0500)
committerBill Erickson <berickxx@gmail.com>
Thu, 19 Nov 2015 20:00:20 +0000 (15:00 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/examples/opensrf.xml.example
Open-ILS/src/c-apps/oils_auth_internal.c [new file with mode: 0644]

index 3b47481..241f104 100644 (file)
@@ -424,6 +424,29 @@ vim:et:ts=4:sw=4:
                 </unix_config>
                 <app_settings>
                     <!-- defined app-specific settings here -->
+                    <auth_limits>
+                        <seed>30</seed> <!-- amount of time a seed request is valid for -->
+                        <block_time>90</block_time> <!-- amount of time since last auth or seed request to save failure counts -->
+                        <block_count>10</block_count> <!-- number of failures before blocking access -->
+                    </auth_limits>
+                </app_settings>
+            </open-ils.auth>
+
+            <!-- Internal authentication server -->
+            <open-ils.auth-internal>
+                <keepalive>5</keepalive>
+                <stateless>1</stateless>
+                <language>c</language>
+                <implementation>oils_auth_internal.so</implementation>
+                <unix_config>
+                    <max_requests>1000</max_requests>
+                    <min_children>1</min_children>
+                    <max_children>15</max_children>
+                    <min_spare_children>1</min_spare_children>
+                    <max_spare_children>5</max_spare_children>
+                </unix_config>
+                <app_settings>
+                    <!-- defined app-specific settings here -->
                     <default_timeout>
                         <!-- default login timeouts based on login type -->
                         <opac>420</opac>
@@ -431,13 +454,10 @@ vim:et:ts=4:sw=4:
                         <temp>300</temp>
                         <persist>2 weeks</persist>
                     </default_timeout>
-                    <auth_limits>
-                        <seed>30</seed> <!-- amount of time a seed request is valid for -->
-                        <block_time>90</block_time> <!-- amount of time since last auth or seed request to save failure counts -->
-                        <block_count>10</block_count> <!-- number of failures before blocking access -->
-                    </auth_limits>
                 </app_settings>
-            </open-ils.auth>
+            </open-ils.auth-internal>
+
+
 
             <!-- Authentication proxy server -->
             <open-ils.auth_proxy>
diff --git a/Open-ILS/src/c-apps/oils_auth_internal.c b/Open-ILS/src/c-apps/oils_auth_internal.c
new file mode 100644 (file)
index 0000000..c00df20
--- /dev/null
@@ -0,0 +1,998 @@
+#include "opensrf/osrf_app_session.h"
+#include "opensrf/osrf_application.h"
+#include "opensrf/osrf_settings.h"
+#include "opensrf/osrf_json.h"
+#include "opensrf/log.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_constants.h"
+#include "openils/oils_event.h"
+
+#define OILS_AUTH_CACHE_PRFX "oils_auth_"
+#define OILS_AUTH_COUNT_SFFX "_count"
+
+#define MODULENAME "open-ils.auth-internal"
+
+#define OILS_AUTH_OPAC "opac"
+#define OILS_AUTH_STAFF "staff"
+#define OILS_AUTH_TEMP "temp"
+#define OILS_AUTH_PERSIST "persist"
+
+// Default time for extending a persistent session: ten minutes
+#define DEFAULT_RESET_INTERVAL 10 * 60
+
+int osrfAppInitialize();
+int osrfAppChildInit();
+
+static long _oilsAuthOPACTimeout = 0;
+static long _oilsAuthStaffTimeout = 0;
+static long _oilsAuthOverrideTimeout = 0;
+static long _oilsAuthPersistTimeout = 0;
+static long _oilsAuthSeedTimeout = 0;
+static long _oilsAuthBlockTimeout = 0;
+static long _oilsAuthBlockCount = 0;
+
+
+/**
+       @brief Initialize the application by registering functions for method calls.
+       @return Zero in all cases.
+*/
+int osrfAppInitialize() {
+
+       osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Internal Server...");
+
+       /* load and parse the IDL */
+       if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth-internal.session.create",
+               "oilsAutInternalCreateSession",
+        "Adds a user to the authentication cache to indicate the user is authenticated"
+               " PARAMS( username )", 1, 0 );
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.authenticate.complete",
+               "oilsAuthComplete",
+               "Completes the authentication process.  Returns an object like so: "
+               "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
+               "token and authtime is the number of seconds the session will be active"
+               "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
+               "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
+               "org_id is the location at which the login should be considered "
+               "active for login timeout purposes", 1, 0 );
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.authenticate.verify",
+               "oilsAuthComplete",
+               "Verifies the user provided a valid username and password."
+               "Params and are the same as open-ils.auth.authenticate.complete."
+               "Returns SUCCESS event on success, failure event on failure", 1, 0);
+
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.session.retrieve",
+               "oilsAuthSessionRetrieve",
+               "Pass in the auth token and this retrieves the user object.  The auth "
+               "timeout is reset when this call is made "
+               "Returns the user object (password blanked) for the given login session "
+               "PARAMS( authToken )", 1, 0 );
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.session.delete",
+               "oilsAuthSessionDelete",
+               "Destroys the given login session "
+               "PARAMS( authToken )",  1, 0 );
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.session.reset_timeout",
+               "oilsAuthResetTimeout",
+               "Resets the login timeout for the given session "
+               "Returns an ILS Event with payload = session_timeout of session "
+               "if found, otherwise returns the NO_SESSION event"
+               "PARAMS( authToken )", 1, 0 );
+
+       if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
+
+               jsonObject* value_obj;
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/auth_limits/seed" );
+               _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthSeedTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
+                       _oilsAuthSeedTimeout = 30;
+               }
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
+               _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthBlockTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
+                       _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
+               }
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
+               _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthBlockCount ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
+                       _oilsAuthBlockCount = 10;
+               }
+
+               osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
+                       "seed => %ld : block_timeout => %ld : block_count => %ld",
+                       _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
+       }
+
+       return 0;
+}
+
+/**
+       @brief Dummy placeholder for initializing a server drone.
+
+       There is nothing to do, so do nothing.
+*/
+int osrfAppChildInit() {
+       return 0;
+}
+
+/**
+       @brief Implement the "init" method.
+       @param ctx The method context.
+       @return Zero if successful, or -1 if not.
+
+       Method parameters:
+       - username
+       - nonce : optional login seed (string) provided by the caller which
+               is added to the auth init cache to differentiate between logins
+               using the same username and thus avoiding cache collisions for
+               near-simultaneous logins.
+
+       Return to client: Intermediate authentication seed.
+
+       Combine the username with a timestamp and process ID, and take an md5 hash of the result.
+       Store the hash in memcache, with a key based on the username.  Then return the hash to
+       the client.
+
+       However: if the username includes one or more embedded blank spaces, return a dummy
+       hash without storing anything in memcache.  The dummy will never match a stored hash, so
+       any attempt to authenticate with it will fail.
+*/
+int oilsAuthInit( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+       char* username  = jsonObjectToSimpleString( jsonObjectGetIndex(ctx->params, 0) );
+       const char* nonce = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
+       if (!nonce) nonce = "";
+
+       if( username ) {
+
+               jsonObject* resp;
+
+               if( strchr( username, ' ' ) ) {
+
+                       // Embedded spaces are not allowed in a username.  Use "x" as a dummy
+                       // seed.  It will never be a valid seed because 'x' is not a hex digit.
+                       resp = jsonNewObject( "x" );
+
+               } else {
+
+                       // Build a key and a seed; store them in memcache.
+                       char* key  = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, username, nonce );
+                       char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, username, OILS_AUTH_COUNT_SFFX );
+                       char* seed = md5sum( "%d.%ld.%s.%s", (int) time(NULL), (long) getpid(), username, nonce );
+                       jsonObject* countobject = osrfCacheGetObject( countkey );
+                       if(!countobject) {
+                               countobject = jsonNewNumberObject( (double) 0 );
+                       }
+                       osrfCachePutString( key, seed, _oilsAuthSeedTimeout );
+                       osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
+
+                       osrfLogDebug( OSRF_LOG_MARK, "oilsAuthInit(): has seed %s and key %s", seed, key );
+
+                       // Build a returnable object containing the seed.
+                       resp = jsonNewObject( seed );
+
+                       free( seed );
+                       free( key );
+                       free( countkey );
+                       jsonObjectFree( countobject );
+               }
+
+               // Return the seed to the client.
+               osrfAppRespondComplete( ctx, resp );
+
+               jsonObjectFree(resp);
+               free(username);
+               return 0;
+       }
+
+       return -1;  // Error: no username parameter
+}
+
+/**
+       Verifies that the user has permission to login with the
+       given type.  If the permission fails, an oilsEvent is returned
+       to the caller.
+       @return -1 if the permission check failed, 0 if the permission
+       is granted
+*/
+static int oilsAuthCheckLoginPerm(
+               osrfMethodContext* ctx, const jsonObject* userObj, const char* type ) {
+
+       if(!(userObj && type)) return -1;
+       oilsEvent* perm = NULL;
+
+       if(!strcasecmp(type, OILS_AUTH_OPAC)) {
+               char* permissions[] = { "OPAC_LOGIN" };
+               perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
+
+       } else if(!strcasecmp(type, OILS_AUTH_STAFF)) {
+               char* permissions[] = { "STAFF_LOGIN" };
+               perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
+
+       } else if(!strcasecmp(type, OILS_AUTH_TEMP)) {
+               char* permissions[] = { "STAFF_LOGIN" };
+               perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
+       } else if(!strcasecmp(type, OILS_AUTH_PERSIST)) {
+               char* permissions[] = { "PERSISTENT_LOGIN" };
+               perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
+       }
+
+       if(perm) {
+               osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
+               oilsEventFree(perm);
+               return -1;
+       }
+
+       return 0;
+}
+
+/**
+       Returns 1 if the password provided matches the user's real password
+       Returns 0 otherwise
+       Returns -1 on error
+*/
+/**
+       @brief Verify the password received from the client.
+       @param ctx The method context.
+       @param userObj An object from the database, representing the user.
+       @param password An obfuscated password received from the client.
+       @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
+
+       (None of the so-called "passwords" used here are in plaintext.  All have been passed
+       through at least one layer of hashing to obfuscate them.)
+
+       Take the password from the user object.  Append it to the username seed from memcache,
+       as stored previously by a call to the init method.  Take an md5 hash of the result.
+       Then compare this hash to the password received from the client.
+
+       In order for the two to match, other than by dumb luck, the client had to construct
+       the password it passed in the same way.  That means it neded to know not only the
+       original password (either hashed or plaintext), but also the seed.  The latter requirement
+       means that the client process needs either to be the same process that called the init
+       method or to receive the seed from the process that did so.
+*/
+static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
+               const jsonObject* userObj, const char* uname,
+               const char* password, const char* nonce ) {
+
+       // Get the username seed, as stored previously in memcache by the init method
+       char* seed = osrfCacheGetString( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, nonce );
+       if(!seed) {
+               return osrfAppRequestRespondException( ctx->session,
+                       ctx->request, "No authentication seed found. "
+                       "open-ils.auth.authenticate.init must be called first "
+                       " (check that memcached is running and can be connected to) "
+               );
+       }
+    
+       // We won't be needing the seed again, remove it
+       osrfCacheRemove( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, nonce );
+
+       // Get the hashed password from the user object
+       char* realPassword = oilsFMGetString( userObj, "passwd" );
+
+       osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
+       osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );
+
+       // Concatenate them and take an MD5 hash of the result
+       char* maskedPw = md5sum( "%s%s", seed, realPassword );
+
+       free(realPassword);
+       free(seed);
+
+       if( !maskedPw ) {
+               // This happens only if md5sum() runs out of memory
+               free( maskedPw );
+               return -1;  // md5sum() ran out of memory
+       }
+
+       osrfLogDebug(OSRF_LOG_MARK,  "oilsAuth generated masked password %s. "
+                       "Testing against provided password %s", maskedPw, password );
+
+       int ret = 0;
+       if( !strcmp( maskedPw, password ) )
+               ret = 1;
+
+       free(maskedPw);
+
+       char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, OILS_AUTH_COUNT_SFFX );
+       jsonObject* countobject = osrfCacheGetObject( countkey );
+       if(countobject) {
+               long failcount = (long) jsonObjectGetNumber( countobject );
+               if(failcount >= _oilsAuthBlockCount) {
+                       ret = 0;
+                   osrfLogInfo(OSRF_LOG_MARK, "oilsAuth found too many recent failures for '%s' : %i, forcing failure state.", uname, failcount);
+               }
+               if(ret == 0) {
+                       failcount += 1;
+               }
+               jsonObjectSetNumber( countobject, failcount );
+               osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
+               jsonObjectFree(countobject);
+       }
+       free(countkey);
+
+       return ret;
+}
+
+/**
+       @brief Determine the login timeout.
+       @param userObj Pointer to an object describing the user.
+       @param type Pointer to one of four possible character strings identifying the login type.
+       @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
+       @return The length of the timeout, in seconds.
+
+       The default timeout value comes from the configuration file, and depends on the
+       login type.
+
+       The default may be overridden by a corresponding org unit setting.  The @a orgloc
+       parameter says what org unit to use for the lookup.  If @a orgloc <= 0, or if the
+       lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
+       instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).
+
+       Whether defined in the config file or in an org unit setting, a timeout value may be
+       expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
+       space) or as an interval string to be translated into seconds by PostgreSQL.
+*/
+static long oilsAuthGetTimeout( const jsonObject* userObj, const char* type, int orgloc ) {
+
+       if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
+
+               jsonObject* value_obj;
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/default_timeout/opac" );
+               _oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthOPACTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
+                       _oilsAuthOPACTimeout = 0;
+               }
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/default_timeout/staff" );
+               _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthStaffTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
+                       _oilsAuthStaffTimeout = 0;
+               }
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/default_timeout/temp" );
+               _oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthOverrideTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
+                       _oilsAuthOverrideTimeout = 0;
+               }
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/default_timeout/persist" );
+               _oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthPersistTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
+                       _oilsAuthPersistTimeout = 0;
+               }
+
+               osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
+                       "opac => %ld : staff => %ld : temp => %ld : persist => %ld",
+                       _oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
+                       _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
+       }
+
+       int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
+       if(orgloc < 1)
+               orgloc = home_ou;
+
+       char* setting = NULL;
+       long default_timeout = 0;
+
+       if( !strcmp( type, OILS_AUTH_OPAC )) {
+               setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
+               default_timeout = _oilsAuthOPACTimeout;
+       } else if( !strcmp( type, OILS_AUTH_STAFF )) {
+               setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
+               default_timeout = _oilsAuthStaffTimeout;
+       } else if( !strcmp( type, OILS_AUTH_TEMP )) {
+               setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
+               default_timeout = _oilsAuthOverrideTimeout;
+       } else if( !strcmp( type, OILS_AUTH_PERSIST )) {
+               setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
+               default_timeout = _oilsAuthPersistTimeout;
+       }
+
+       // Get the org unit setting, if there is one.
+       char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
+       if(!timeout) {
+               if( orgloc != home_ou ) {
+                       osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
+                               "trying home_ou %d", orgloc, home_ou );
+                       timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
+               }
+       }
+
+       if(!timeout)
+               return default_timeout;   // No override from org unit setting
+
+       // Translate the org unit setting to a number
+       long t;
+       if( !*timeout ) {
+               osrfLogWarning( OSRF_LOG_MARK,
+                       "Timeout org unit setting is an empty string for %s login; using default",
+                       timeout, type );
+               t = default_timeout;
+       } else {
+               // Treat timeout string as an interval, and convert it to seconds
+               t = oilsUtilsIntervalToSeconds( timeout );
+               if( -1 == t ) {
+                       // Unable to convert; possibly an invalid interval string
+                       osrfLogError( OSRF_LOG_MARK,
+                               "Unable to convert timeout interval \"%s\" for %s login; using default",
+                               timeout, type );
+                       t = default_timeout;
+               }
+       }
+
+       free(timeout);
+       return t;
+}
+
+/*
+       Adds the authentication token to the user cache.  The timeout for the
+       auth token is based on the type of login as well as (if type=='opac')
+       the org location id.
+       Returns the event that should be returned to the user.
+       Event must be freed
+*/
+static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
+               const char* type, int orgloc, const char* workstation ) {
+
+       oilsEvent* response;
+
+       long timeout;
+       char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "ws_ou"));
+       if(wsorg) { /* if there is a workstation, use it for the timeout */
+               osrfLogDebug( OSRF_LOG_MARK,
+                               "Auth session trying workstation id %d for auth timeout", atoi(wsorg));
+               timeout = oilsAuthGetTimeout( userObj, type, atoi(wsorg) );
+               free(wsorg);
+       } else {
+               osrfLogDebug( OSRF_LOG_MARK,
+                               "Auth session trying org from param [%d] for auth timeout", orgloc );
+               timeout = oilsAuthGetTimeout( userObj, type, orgloc );
+       }
+       osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %ld", uname, timeout );
+
+       char* string = va_list_to_string(
+                       "%d.%ld.%s", (long) getpid(), time(NULL), uname );
+       char* authToken = md5sum(string);
+       char* authKey = va_list_to_string(
+                       "%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+
+       const char* ws = (workstation) ? workstation : "";
+       osrfLogActivity(OSRF_LOG_MARK,
+               "successful login: username=%s, authtoken=%s, workstation=%s", uname, authToken, ws );
+
+       oilsFMSetString( userObj, "passwd", "" );
+       jsonObject* cacheObj = jsonParseFmt( "{\"authtime\": %ld}", timeout );
+       jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
+
+       if( !strcmp( type, OILS_AUTH_PERSIST )) {
+               // Add entries for endtime and reset_interval, so that we can gracefully
+               // extend the session a bit if the user is active toward the end of the 
+               // timeout originally specified.
+               time_t endtime = time( NULL ) + timeout;
+               jsonObjectSetKey( cacheObj, "endtime", jsonNewNumberObject( (double) endtime ) );
+
+               // Reset interval is hard-coded for now, but if we ever want to make it
+               // configurable, this is the place to do it:
+               jsonObjectSetKey( cacheObj, "reset_interval",
+                       jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL ));
+       }
+
+       osrfCachePutObject( authKey, cacheObj, (time_t) timeout );
+       jsonObjectFree(cacheObj);
+       osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
+       jsonObject* payload = jsonParseFmt(
+               "{ \"authtoken\": \"%s\", \"authtime\": %ld }", authToken, timeout );
+
+       response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
+       free(string); free(authToken); free(authKey);
+       jsonObjectFree(payload);
+
+       return response;
+}
+
+static oilsEvent* oilsAuthVerifyWorkstation(
+               const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
+       osrfLogInfo(OSRF_LOG_MARK, "Attaching workstation to user at login: %s", ws);
+       jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
+       if(!workstation || workstation->type == JSON_NULL) {
+               jsonObjectFree(workstation);
+               return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
+       }
+       long wsid = oilsFMGetObjectId(workstation);
+       LONG_TO_STRING(wsid);
+       char* orgid = oilsFMGetString(workstation, "owning_lib");
+       oilsFMSetString(userObj, "wsid", LONGSTR);
+       oilsFMSetString(userObj, "ws_ou", orgid);
+       free(orgid);
+       jsonObjectFree(workstation);
+       return NULL;
+}
+
+
+
+/**
+       @brief Implement the "complete" method.
+       @param ctx The method context.
+       @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
+       client to indicate completion; a positive integer if successful but no such STATUS
+       message has been sent.
+
+       Method parameters:
+       - a hash with some combination of the following elements:
+               - "username"
+               - "barcode"
+               - "password" (hashed with the cached seed; not plaintext)
+               - "type"
+               - "org"
+               - "workstation"
+               - "agent" (what software/interface/3rd-party is making the request)
+               - "nonce" optional login seed to differentiate logins using the same username.
+
+       The password is required.  Either a username or a barcode must also be present.
+
+       Return to client: Intermediate authentication seed.
+
+       Validate the password, using the username if available, or the barcode if not.  The
+       user must be active, and not barred from logging on.  The barcode, if used for
+       authentication, must be active as well.  The workstation, if specified, must be valid.
+
+       Upon deciding whether to allow the logon, return a corresponding event to the client.
+*/
+int oilsAuthComplete( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+       const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
+
+       const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
+       const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
+       const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
+       int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
+       const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
+       const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
+       const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
+       const char* nonce       = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
+
+       const char* ws = (workstation) ? workstation : "";
+       if (!nonce) nonce = "";
+
+       /* Use __FILE__, harmless_line_number for creating
+        * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
+        * giving away information about why an authentication attempt failed.
+        */
+       int harmless_line_number = __LINE__;
+
+       if( !type )
+                type = OILS_AUTH_STAFF;
+
+       if( !( (uname || barcode) && password) ) {
+               return osrfAppRequestRespondException( ctx->session, ctx->request,
+                       "username/barcode and password required for method: %s", ctx->method->name );
+       }
+
+       oilsEvent* response = NULL;
+       jsonObject* userObj = NULL;
+       int card_active     = 1;      // boolean; assume active until proven otherwise
+
+       // Fetch a row from the actor.usr table, by username if available,
+       // or by barcode if not.
+       if(uname) {
+               userObj = oilsUtilsFetchUserByUsername( uname );
+               if( userObj && JSON_NULL == userObj->type ) {
+                       jsonObjectFree( userObj );
+                       userObj = NULL;         // username not found
+               }
+       }
+       else if(barcode) {
+               // Read from actor.card by barcode
+
+               osrfLogInfo( OSRF_LOG_MARK, "Fetching user by barcode %s", barcode );
+
+               jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
+               jsonObject* card = oilsUtilsQuickReq(
+                       "open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
+               jsonObjectFree( params );
+
+               if( card && card->type != JSON_NULL ) {
+                       // Determine whether the card is active
+                       char* card_active_str = oilsFMGetString( card, "active" );
+                       card_active = oilsUtilsIsDBTrue( card_active_str );
+                       free( card_active_str );
+
+                       // Look up the user who owns the card
+                       char* userid = oilsFMGetString( card, "usr" );
+                       jsonObjectFree( card );
+                       params = jsonParseFmt( "[%s]", userid );
+                       free( userid );
+                       userObj = oilsUtilsQuickReq(
+                                       "open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params );
+                       jsonObjectFree( params );
+                       if( userObj && JSON_NULL == userObj->type ) {
+                               // user not found (shouldn't happen, due to foreign key)
+                               jsonObjectFree( userObj );
+                               userObj = NULL;
+                       }
+               }
+       }
+
+       int     barred = 0, deleted = 0;
+       char   *barred_str, *deleted_str;
+
+       if(userObj) {
+               barred_str = oilsFMGetString( userObj, "barred" );
+               barred = oilsUtilsIsDBTrue( barred_str );
+               free( barred_str );
+
+               deleted_str = oilsFMGetString( userObj, "deleted" );
+               deleted = oilsUtilsIsDBTrue( deleted_str );
+               free( deleted_str );
+       }
+
+       if(!userObj || barred || deleted) {
+               response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+               osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
+                               uname, (barcode ? barcode : "(none)"), ws );
+               osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+               oilsEventFree(response);
+               return 0;           // No such user
+       }
+
+       // Such a user exists and isn't barred or deleted.
+       // Now see if he or she has the right credentials.
+       int passOK = -1;
+       if(uname)
+               passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password, nonce );
+       else if (barcode)
+               passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password, nonce );
+
+       if( passOK < 0 ) {
+               jsonObjectFree(userObj);
+               return passOK;
+       }
+
+       // See if the account is active
+       char* active = oilsFMGetString(userObj, "active");
+       if( !oilsUtilsIsDBTrue(active) ) {
+               if( passOK )
+                       response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
+               else
+                       response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+
+               osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+               oilsEventFree(response);
+               jsonObjectFree(userObj);
+               free(active);
+               return 0;
+       }
+       free(active);
+
+       osrfLogInfo( OSRF_LOG_MARK, "Fetching card by barcode %s", barcode );
+
+       if( !card_active ) {
+               osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
+               response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
+               osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
+               oilsEventFree( response );
+               jsonObjectFree( userObj );
+               return 0;
+       }
+
+
+       // See if the user is even allowed to log in
+       if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
+               jsonObjectFree(userObj);
+               return 0;
+       }
+
+       // If a workstation is defined, add the workstation info
+       if( workstation != NULL ) {
+               osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
+               response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
+               if(response) {
+                       jsonObjectFree(userObj);
+                       osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+                       oilsEventFree(response);
+                       return 0;
+               }
+
+       } else {
+               // Otherwise, use the home org as the workstation org on the user
+               char* orgid = oilsFMGetString(userObj, "home_ou");
+               oilsFMSetString(userObj, "ws_ou", orgid);
+               free(orgid);
+       }
+
+       char* freeable_uname = NULL;
+       if(!uname) {
+               uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
+       }
+
+       if( passOK ) { // login successful  
+        
+               char* ewhat = "login";
+
+               if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
+                       response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
+                       ewhat = "verify";
+
+               } else {
+                       response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
+               }
+
+               oilsUtilsTrackUserActivity(
+                       oilsFMGetObjectId(userObj), 
+                       ewho, ewhat, 
+                       osrfAppSessionGetIngress()
+               );
+
+       } else {
+               response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+               osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
+                               uname, (barcode ? barcode : "(none)"), ws );
+       }
+
+       jsonObjectFree(userObj);
+       osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+       oilsEventFree(response);
+
+       if(freeable_uname)
+               free(freeable_uname);
+
+       return 0;
+}
+
+
+
+int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+       const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
+       jsonObject* resp = NULL;
+
+       if( authToken ) {
+               osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
+               char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
+               osrfCacheRemove(key);
+               resp = jsonNewObject(authToken); /**/
+               free(key);
+       }
+
+       osrfAppRespondComplete( ctx, resp );
+       jsonObjectFree(resp);
+       return 0;
+}
+
+/**
+ * Fetches the user object from the database and updates the user object in 
+ * the cache object, which then has to be re-inserted into the cache.
+ * User object is retrieved inside a transaction to avoid replication issues.
+ */
+static int _oilsAuthReloadUser(jsonObject* cacheObj) {
+    int reqid, userId;
+    osrfAppSession* session;
+       osrfMessage* omsg;
+    jsonObject *param, *userObj, *newUserObj;
+
+    userObj = jsonObjectGetKey( cacheObj, "userobj" );
+    userId = oilsFMGetObjectId( userObj );
+
+    session = osrfAppSessionClientInit( "open-ils.cstore" );
+    osrfAppSessionConnect(session);
+
+    reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
+       omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+
+    if(omsg) {
+
+        osrfMessageFree(omsg);
+        param = jsonNewNumberObject(userId);
+        reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
+           omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+        jsonObjectFree(param);
+
+        if(omsg) {
+            newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
+            osrfMessageFree(omsg);
+            reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
+               omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+            osrfMessageFree(omsg);
+        }
+    }
+
+    osrfAppSessionFree(session); // calls disconnect internally
+
+    if(newUserObj) {
+
+        // ws_ou and wsid are ephemeral and need to be manually propagated
+        // oilsFMSetString dupe()'s internally, no need to clone the string
+        oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
+        oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
+
+        jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
+        jsonObjectSetKey(cacheObj, "userobj", newUserObj);
+        return 1;
+    } 
+
+    osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
+    return 0;
+}
+
+/**
+       Resets the auth login timeout
+       @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
+*/
+static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
+       if(!authToken) return NULL;
+
+       oilsEvent* evt = NULL;
+       time_t timeout;
+
+       osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
+       char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+       jsonObject* cacheObj = osrfCacheGetObject( key );
+
+       if(!cacheObj) {
+               osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
+               evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+
+       } else {
+
+        if(reloadUser) {
+            _oilsAuthReloadUser(cacheObj);
+        }
+
+               // Determine a new timeout value
+               jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
+               if( endtime_obj ) {
+                       // Extend the current endtime by a fixed amount
+                       time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
+                       int reset_interval = DEFAULT_RESET_INTERVAL;
+                       const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
+                               cacheObj, "reset_interval" );
+                       if( reset_interval_obj ) {
+                               reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
+                               if( reset_interval <= 0 )
+                                       reset_interval = DEFAULT_RESET_INTERVAL;
+                       }
+
+                       time_t now = time( NULL );
+                       time_t new_endtime = now + reset_interval;
+                       if( new_endtime > endtime ) {
+                               // Keep the session alive a little longer
+                               jsonObjectSetNumber( endtime_obj, (double) new_endtime );
+                               timeout = reset_interval;
+                               osrfCachePutObject( key, cacheObj, timeout );
+                       } else {
+                               // The session isn't close to expiring, so don't reset anything.
+                               // Just report the time remaining.
+                               timeout = endtime - now;
+                       }
+               } else {
+                       // Reapply the existing timeout from the current time
+                       timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
+                       osrfCachePutObject( key, cacheObj, timeout );
+               }
+
+               jsonObject* payload = jsonNewNumberObject( (double) timeout );
+               evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
+               jsonObjectFree(payload);
+               jsonObjectFree(cacheObj);
+       }
+
+       free(key);
+       return evt;
+}
+
+int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+       const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
+    double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
+       oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
+       osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
+       oilsEventFree(evt);
+       return 0;
+}
+
+
+int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+    bool returnFull = false;
+
+       const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
+
+    if(ctx->params->size > 1) {
+        // caller wants full cached object, with authtime, etc.
+        const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
+        if(rt && strcmp(rt, "0") != 0) 
+            returnFull = true;
+    }
+
+       jsonObject* cacheObj = NULL;
+       oilsEvent* evt = NULL;
+
+       if( authToken ){
+
+               // Reset the timeout to keep the session alive
+               evt = _oilsAuthResetTimeout(authToken, 0);
+
+               if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
+                       osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
+
+               } else {
+
+                       // Retrieve the cached session object
+                       osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
+                       char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+                       cacheObj = osrfCacheGetObject( key );
+                       if(cacheObj) {
+                               // Return a copy of the cached user object
+                if(returnFull)
+                                   osrfAppRespondComplete( ctx, cacheObj);
+                else
+                                   osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
+                               jsonObjectFree(cacheObj);
+                       } else {
+                               // Auth token is invalid or expired
+                               oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+                               osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
+                               oilsEventFree(evt2);
+                       }
+                       free(key);
+               }
+
+       } else {
+
+               // No session
+               evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+               osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
+       }
+
+       if(evt)
+               oilsEventFree(evt);
+
+       return 0;
+}