/** Resets the auth login timeout @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION */ static oilsEvent* _oilsAuthResetTimeout( const char* authToken ) { 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 { // 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; }
/** * Cache the transaction with the JID of the backend process we are talking to */ static void osrfHttpTranslatorCacheSession(osrfHttpTranslator* trans, const char* jid) { jsonObject* cacheObj = jsonNewObject(NULL); jsonObjectSetKey(cacheObj, "ip", jsonNewObject(trans->remoteHost)); jsonObjectSetKey(cacheObj, "jid", jsonNewObject(jid)); jsonObjectSetKey(cacheObj, "service", jsonNewObject(trans->service)); osrfCachePutObject(trans->thread, cacheObj, CACHE_TIME); }
/* 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; }
// ident is either a username or barcode // Returns the init seed -> requires free(); static char* oilsAuthBuildInitCache( int user_id, const char* ident, const char* ident_type, const char* nonce) { char* cache_key = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce); char* count_key = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX); char* auth_seed; if (user_id == -1) { // user does not exist. Use a dummy seed auth_seed = strdup("x"); } else { auth_seed = oilsAuthGetSalt(user_id); } jsonObject* seed_object = jsonParseFmt( "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}", ident_type, ident, user_id, auth_seed); jsonObject* count_object = osrfCacheGetObject(count_key); if(!count_object) { count_object = jsonNewNumberObject((double) 0); } osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout); if (user_id != -1) { // Only track login counts for existing users, since a // login for a nonexistent user will never succeed anyway. osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout); } osrfLogDebug(OSRF_LOG_MARK, "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key); free(cache_key); free(count_key); jsonObjectFree(count_object); jsonObjectFree(seed_object); return auth_seed; }
static int oilsAuthLoginVerifyPassword(const osrfMethodContext* ctx, int user_id, const char* username, const char* password) { // build the cache key growing_buffer* gb = buffer_init(64); // free me buffer_add(gb, OILS_AUTH_CACHE_PRFX); buffer_add(gb, username); buffer_add(gb, OILS_AUTH_COUNT_SFFX); char* countkey = buffer_release(gb); // free me jsonObject* countobject = osrfCacheGetObject(countkey); // free me long failcount = 0; if (countobject) { failcount = (long) jsonObjectGetNumber(countobject); if (failcount >= _oilsAuthBlockCount) { // User is blocked. Don't waste any more CPU cycles on them. osrfLogInfo(OSRF_LOG_MARK, "oilsAuth found too many recent failures for '%s' : %i, " "forcing failure state.", username, failcount); jsonObjectFree(countobject); free(countkey); return 0; } } int verified = oilsAuthLoginCheckPassword(user_id, password); if (!verified) { // login failed. increment failure counter. failcount++; if (countobject) { // append to existing counter jsonObjectSetNumber(countobject, failcount); } else { // first failure, create a new counter countobject = jsonNewNumberObject((double) failcount); } osrfCachePutObject(countkey, countobject, _oilsAuthBlockTimeout); } jsonObjectFree(countobject); // NULL OK free(countkey); return verified; }
/** @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, int user_id, const char* identifier, const char* password, const char* nonce) { int verified = 0; // We won't be needing the seed again, remove it osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce); // Ask the DB to verify the user's password. // Here, the password is md5(md5(password) + salt) jsonObject* params = jsonParseFmt( // free "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}", user_id, password); jsonObject* verify_obj = // free oilsUtilsCStoreReq("open-ils.cstore.json_query", params); jsonObjectFree(params); if (verify_obj) { verified = oilsUtilsIsDBTrue( jsonObjectGetString( jsonObjectGetKeyConst( verify_obj, "actor.verify_passwd"))); jsonObjectFree(verify_obj); } char* countkey = va_list_to_string("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX ); jsonObject* countobject = osrfCacheGetObject( countkey ); if(countobject) { long failcount = (long) jsonObjectGetNumber( countobject ); if(failcount >= _oilsAuthBlockCount) { verified = 0; osrfLogInfo(OSRF_LOG_MARK, "oilsAuth found too many recent failures for '%s' : %i, " "forcing failure state.", identifier, failcount); } if(verified == 0) { failcount += 1; } jsonObjectSetNumber( countobject, failcount ); osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout ); jsonObjectFree(countobject); } free(countkey); return verified; }
/** @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 }
/** @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 Implement the session create 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: - "user_id" -- actor.usr (au) ID for the user to cache. - "org_unit" -- actor.org_unit (aou) ID representing the physical location / context used for timeout, etc. settings. - "login_type" -- login type (opac, staff, temp, persist) - "workstation" -- workstation name */ int oilsAuthInternalCreateSession(osrfMethodContext* ctx) { OSRF_METHOD_VERIFY_CONTEXT(ctx); const jsonObject* args = jsonObjectGetIndex(ctx->params, 0); const char* user_id = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id")); const char* login_type = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type")); const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation")); int org_unit = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org_unit")); if ( !(user_id && login_type) ) { return osrfAppRequestRespondException( ctx->session, ctx->request, "Missing parameters for method: %s", ctx->method->name ); } oilsEvent* response = NULL; // fetch the user object jsonObject* idParam = jsonNewNumberStringObject(user_id); jsonObject* userObj = oilsUtilsCStoreReqCtx( ctx, "open-ils.cstore.direct.actor.user.retrieve", idParam); jsonObjectFree(idParam); if (!userObj) { return osrfAppRequestRespondException(ctx->session, ctx->request, "No user found with ID %s", user_id); } // If a workstation is defined, add the workstation info if (workstation) { response = oilsAuthVerifyWorkstation(ctx, userObj, workstation); if (response) { // invalid workstation. jsonObjectFree(userObj); osrfAppRespondComplete(ctx, oilsEventToJSON(response)); oilsEventFree(response); return 0; } else { // workstation OK. // The worksation org unit supersedes any org unit value // provided via the API. oilsAuthVerifyWorkstation() sets the // ws_ou value to the WS owning lib. A value is guaranteed. org_unit = atoi(oilsFMGetStringConst(userObj, "ws_ou")); } } else { // no workstation // For backwards compatibility, when no workstation is provided, use // the users's home org as its workstation org unit, regardless of // any API-level org unit value provided. const char* orgid = oilsFMGetStringConst(userObj, "home_ou"); oilsFMSetString(userObj, "ws_ou", orgid); // The context org unit defaults to the user's home library when // no workstation is used and no API-level value is provided. if (org_unit < 1) org_unit = atoi(orgid); } // determine the auth/cache timeout long timeout = oilsAuthGetTimeout(userObj, login_type, org_unit); char* string = va_list_to_string("%d.%ld.%ld", (long) getpid(), time(NULL), oilsFMGetObjectId(userObj)); char* authToken = md5sum(string); char* authKey = va_list_to_string( "%s%s", OILS_AUTH_CACHE_PRFX, authToken); oilsFMSetString(userObj, "passwd", ""); jsonObject* cacheObj = jsonParseFmt("{\"authtime\": %ld}", timeout); jsonObjectSetKey(cacheObj, "userobj", jsonObjectClone(userObj)); if( !strcmp(login_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); 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); jsonObjectFree(userObj); osrfAppRespondComplete(ctx, oilsEventToJSON(response)); oilsEventFree(response); return 0; }