From 18b313d5c933a43dc02ced2ab035197797ea36b3 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 15 Aug 2017 13:13:03 -0400 Subject: [PATCH] LP#1710949 open-ils.auth.login API Adds a new open-ils.auth API call 'open-ils.auth.login' which performs the combined steps of open-ils.auth.authenticate.init and open-ils.auth.authenticate.complete so the caller only need call one API to login. API params are consistent with open-ils.auth.authenticate.complete with 2 notable excpetions. The API uses the bare password instead of the hashed password, so the caller also need not perform the extra hashing steps. Also, no 'nonce' parameter is used as it's no longer needed, because there is no intermediate authentication cache object as with .init. Response data is consistent with open-ils.auth.authenticate.complete. Example: srfsh# request open-ils.auth open-ils.auth.login {"username":"admin","password":"fakepassword"} Other changes in the new code: 1. Using the generic "identifier" parameter in combination with the "org" parameter allows the API to reliably determine if a value is a username or barcode. 2. Once a caller has reached the configured maximum number of login failures, no further attempts to track failures occurs, based on the idea that no additional cpu/network cycles should be used on a lost cause. 3. A failure count object is only added to memcache when failures occur, unlike open-ils.auth.authenticate.init which creates a failure tracking object for every login. 4. The code avoids use of the jsonParseFmt() and va_list_to_string() functions as these functions require extra data cleansing. Signed-off-by: Bill Erickson Signed-off-by: Galen Charlton Signed-off-by: Mike Rylander --- Open-ILS/src/c-apps/oils_auth.c | 294 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 287 insertions(+), 7 deletions(-) diff --git a/Open-ILS/src/c-apps/oils_auth.c b/Open-ILS/src/c-apps/oils_auth.c index 0fd924fa21..4afb36cf50 100644 --- a/Open-ILS/src/c-apps/oils_auth.c +++ b/Open-ILS/src/c-apps/oils_auth.c @@ -75,6 +75,18 @@ int osrfAppInitialize() { osrfAppRegisterMethod( MODULENAME, + "open-ils.auth.login", + "oilsAuthLogin", + "Request an authentication token logging in with username or " + "barcode. Parameter is a keyword arguments hash with keys " + "username, barcode, identifier, password, type, org, workstation, " + "agent. The 'identifier' option is used when the caller wants the " + "API to determine if an identifier string is a username or barcode " + "using the barcode format configuration.", + 1, 0); + + osrfAppRegisterMethod( + MODULENAME, "open-ils.auth.authenticate.verify", "oilsAuthComplete", "Verifies the user provided a valid username and password." @@ -317,12 +329,12 @@ int oilsAuthInitBarcode(osrfMethodContext* ctx) { } // returns true if the provided identifier matches the barcode regex. -static int oilsAuthIdentIsBarcode(const char* identifier) { +static int oilsAuthIdentIsBarcode(const char* identifier, int org_id) { - // Assumes barcode regex is a global setting. - // TODO: add an org_unit param to the .init API for future use? - char* bc_regex = oilsUtilsFetchOrgSetting( - oilsUtilsGetRootOrgId(), "opac.barcode_regex"); + if (org_id < 1) + org_id = oilsUtilsGetRootOrgId(); + + char* bc_regex = oilsUtilsFetchOrgSetting(org_id, "opac.barcode_regex"); if (!bc_regex) { // if no regex is set, assume any identifier starting @@ -396,7 +408,7 @@ int oilsAuthInit(osrfMethodContext* ctx) { if (!nonce) nonce = ""; if (!identifier) return -1; // we need an identifier - if (oilsAuthIdentIsBarcode(identifier)) { + if (oilsAuthIdentIsBarcode(identifier, 0)) { resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce); } else { resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce); @@ -483,6 +495,110 @@ static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, return verified; } +/** + * Returns true if the provided password is correct. + * Turn the password into the nested md5 hash required of migrated + * passwords, then check the password in the DB. + */ +static int oilsAuthLoginCheckPassword(int user_id, const char* password) { + + growing_buffer* gb = buffer_init(33); // free me 1 + char* salt = oilsAuthGetSalt(user_id); // free me 2 + char* passhash = md5sum(password); // free me 3 + + buffer_add(gb, salt); // gb strdup's internally + buffer_add(gb, passhash); + + free(salt); // free 2 + free(passhash); // free 3 + + // salt + md5(password) + passhash = buffer_release(gb); // free 1 ; free me 4 + char* finalpass = md5sum(passhash); // free me 5 + + free(passhash); // free 4 + + jsonObject *arr = jsonNewObjectType(JSON_ARRAY); + jsonObjectPush(arr, jsonNewObject("actor.verify_passwd")); + jsonObjectPush(arr, jsonNewNumberObject((long) user_id)); + jsonObjectPush(arr, jsonNewObject("main")); + jsonObjectPush(arr, jsonNewObject(finalpass)); + jsonObject *params = jsonNewObjectType(JSON_HASH); // free me 6 + jsonObjectSetKey(params, "from", arr); + + free(finalpass); // free 5 + + jsonObject* verify_obj = // free + oilsUtilsCStoreReq("open-ils.cstore.json_query", params); + + jsonObjectFree(params); // free 6 + + if (!verify_obj) return 0; // error + + int verified = oilsUtilsIsDBTrue( + jsonObjectGetString( + jsonObjectGetKeyConst(verify_obj, "actor.verify_passwd") + ) + ); + + jsonObjectFree(verify_obj); + + return verified; +} + +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; +} + + /* 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') @@ -616,7 +732,6 @@ int oilsAuthComplete( osrfMethodContext* ctx ) { oilsEvent* response = NULL; // free jsonObject* userObj = NULL; // free - int card_active = 1; // boolean; assume active until proven otherwise char* cache_key = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce); @@ -767,6 +882,171 @@ int oilsAuthComplete( osrfMethodContext* ctx ) { if(freeable_uname) free(freeable_uname); + return 0; +} + + +int oilsAuthLogin(osrfMethodContext* ctx) { + OSRF_METHOD_VERIFY_CONTEXT(ctx); + + const jsonObject* args = jsonObjectGetIndex(ctx->params, 0); + + const char* username = jsonObjectGetString(jsonObjectGetKeyConst(args, "username")); + const char* identifier = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier")); + 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* ws = (workstation) ? workstation : ""; + if (!type) type = OILS_AUTH_STAFF; + + jsonObject* userObj = NULL; // free me + oilsEvent* response = NULL; // free me + + /* 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__; + + // translate a generic identifier into a username or barcode if necessary. + if (identifier && !username && !barcode) { + if (oilsAuthIdentIsBarcode(identifier, orgloc)) { + barcode = identifier; + } else { + username = identifier; + } + } + + if (username) { + barcode = NULL; // avoid superfluous identifiers + userObj = oilsUtilsFetchUserByUsername(ctx, username); + + } else if (barcode) { + userObj = oilsUtilsFetchUserByBarcode(ctx, barcode); + + } else { + // not enough params + return osrfAppRequestRespondException(ctx->session, ctx->request, + "username/barcode and password required for method: %s", + ctx->method->name); + } + + if (!userObj) { // user not found. + response = oilsNewEvent( + __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED); + osrfAppRespondComplete(ctx, oilsEventToJSON(response)); + oilsEventFree(response); // frees event JSON + return 0; + } + + long user_id = oilsFMGetObjectId(userObj); + + // username is freed when userObj is freed. + // From here we can use the username as the generic identifier + // since it's guaranteed to have a value. + if (!username) username = oilsFMGetStringConst(userObj, "usrname"); + + // See if the user is allowed to login. + jsonObject* params = jsonNewObject(NULL); + jsonObjectSetKey(params, "user_id", jsonNewNumberObject(user_id)); + jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc)); + jsonObjectSetKey(params, "login_type", jsonNewObject(type)); + if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode)); + + jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test + ctx, + "open-ils.auth_internal", + "open-ils.auth_internal.user.validate", params); + jsonObjectFree(params); + + if (!authEvt) { // unknown error + jsonObjectFree(userObj); + return -1; + } + + const char* authEvtCode = + jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")); + + if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) { + // Received the generic login failure event. + + osrfLogInfo(OSRF_LOG_MARK, + "failed login: username=%s, barcode=%s, workstation=%s", + username, (barcode ? barcode : "(none)"), ws); + + response = oilsNewEvent( + __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED); + } + + if (!response && // user exists and is not barred, etc. + !oilsAuthLoginVerifyPassword(ctx, user_id, username, password)) { + // User provided the wrong password or is blocked from too + // many previous login failures. + + response = oilsNewEvent( + __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED); + + osrfLogInfo(OSRF_LOG_MARK, + "failed login: username=%s, barcode=%s, workstation=%s", + username, (barcode ? barcode : "(none)"), ws ); + } + + // Below here, we know the password check succeeded if no response + // object is present. + + if (!response && ( + !strcmp(authEvtCode, "PATRON_INACTIVE") || + !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) { + // Patron and/or card is inactive but the correct password + // was provided. Alert the caller to the inactive-ness. + response = oilsNewEvent2( + OSRF_LOG_MARK, authEvtCode, + jsonObjectGetKey(authEvt, "payload") // cloned within Event + ); + } + + if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) { + // Validate API returned an unexpected non-success event. + // To be safe, treat this as a generic login failure. + + response = oilsNewEvent( + __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED); + } + + if (!response) { + // password OK and no other events have prevented login completion. + + 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( + ctx, userObj, username, type, orgloc, workstation); + } + + oilsUtilsTrackUserActivity( + ctx, + oilsFMGetObjectId(userObj), + ewho, ewhat, + osrfAppSessionGetIngress() + ); + } + + // reply + osrfAppRespondComplete(ctx, oilsEventToJSON(response)); + + // clean up + oilsEventFree(response); + jsonObjectFree(userObj); + jsonObjectFree(authEvt); + return 0; } -- 2.11.0