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"
11 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
12 #define OILS_AUTH_COUNT_SFFX "_count"
14 #define MODULENAME "open-ils.auth"
16 #define OILS_AUTH_OPAC "opac"
17 #define OILS_AUTH_STAFF "staff"
18 #define OILS_AUTH_TEMP "temp"
19 #define OILS_AUTH_PERSIST "persist"
21 // Default time for extending a persistent session: ten minutes
22 #define DEFAULT_RESET_INTERVAL 10 * 60
24 int osrfAppInitialize();
25 int osrfAppChildInit();
27 static long _oilsAuthSeedTimeout = 0;
28 static long _oilsAuthBlockTimeout = 0;
29 static long _oilsAuthBlockCount = 0;
33 @brief Initialize the application by registering functions for method calls.
34 @return Zero in all cases.
36 int osrfAppInitialize() {
38 osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
40 /* load and parse the IDL */
41 if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
43 osrfAppRegisterMethod(
45 "open-ils.auth.authenticate.init",
47 "Start the authentication process and returns the intermediate authentication seed"
48 " PARAMS( username )", 1, 0 );
50 osrfAppRegisterMethod(
52 "open-ils.auth.authenticate.init.barcode",
53 "oilsAuthInitBarcode",
54 "Start the authentication process using a patron barcode and return "
55 "the intermediate authentication seed. PARAMS(barcode)", 1, 0);
57 osrfAppRegisterMethod(
59 "open-ils.auth.authenticate.init.username",
60 "oilsAuthInitUsername",
61 "Start the authentication process using a patron username and return "
62 "the intermediate authentication seed. PARAMS(username)", 1, 0);
64 osrfAppRegisterMethod(
66 "open-ils.auth.authenticate.complete",
68 "Completes the authentication process. Returns an object like so: "
69 "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
70 "token and authtime is the number of seconds the session will be active"
71 "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
72 "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
73 "org_id is the location at which the login should be considered "
74 "active for login timeout purposes", 1, 0 );
76 osrfAppRegisterMethod(
78 "open-ils.auth.login",
80 "Request an authentication token logging in with username or "
81 "barcode. Parameter is a keyword arguments hash with keys "
82 "username, barcode, identifier, password, type, org, workstation, "
83 "agent. The 'identifier' option is used when the caller wants the "
84 "API to determine if an identifier string is a username or barcode "
85 "using the barcode format configuration.",
88 osrfAppRegisterMethod(
90 "open-ils.auth.authenticate.verify",
92 "Verifies the user provided a valid username and password."
93 "Params and are the same as open-ils.auth.authenticate.complete."
94 "Returns SUCCESS event on success, failure event on failure", 1, 0);
97 osrfAppRegisterMethod(
99 "open-ils.auth.session.retrieve",
100 "oilsAuthSessionRetrieve",
101 "Pass in the auth token and this retrieves the user object. By "
102 "default, the auth timeout is reset when this call is made. If "
103 "a second non-zero parameter is passed, the auth timeout info is "
104 "returned to the caller along with the user object. If a 3rd "
105 "non-zero parameter is passed, the auth timeout will not be reset."
106 "Returns the user object (password blanked) for the given login session "
107 "PARAMS( authToken[, returnTime[, doNotResetSession]] )", 1, 0 );
109 osrfAppRegisterMethod(
111 "open-ils.auth.session.delete",
112 "oilsAuthSessionDelete",
113 "Destroys the given login session "
114 "PARAMS( authToken )", 1, 0 );
116 osrfAppRegisterMethod(
118 "open-ils.auth.session.reset_timeout",
119 "oilsAuthResetTimeout",
120 "Resets the login timeout for the given session "
121 "Returns an ILS Event with payload = session_timeout of session "
122 "if found, otherwise returns the NO_SESSION event"
123 "PARAMS( authToken )", 1, 0 );
125 if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
127 jsonObject* value_obj;
129 value_obj = osrf_settings_host_value_object(
130 "/apps/open-ils.auth/app_settings/auth_limits/seed" );
131 _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
132 jsonObjectFree(value_obj);
133 if( -1 == _oilsAuthSeedTimeout ) {
134 osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
135 _oilsAuthSeedTimeout = 30;
138 value_obj = osrf_settings_host_value_object(
139 "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
140 _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
141 jsonObjectFree(value_obj);
142 if( -1 == _oilsAuthBlockTimeout ) {
143 osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
144 _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
147 value_obj = osrf_settings_host_value_object(
148 "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
149 _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
150 jsonObjectFree(value_obj);
151 if( -1 == _oilsAuthBlockCount ) {
152 osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
153 _oilsAuthBlockCount = 10;
156 osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
157 "seed => %ld : block_timeout => %ld : block_count => %ld",
158 _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
165 @brief Dummy placeholder for initializing a server drone.
167 There is nothing to do, so do nothing.
169 int osrfAppChildInit() {
174 static char* oilsAuthGetSalt(int user_id) {
175 char* salt_str = NULL;
177 jsonObject* params = jsonParseFmt( // free
178 "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, "main");
180 jsonObject* salt_obj = // free
181 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
183 jsonObjectFree(params);
187 if (salt_obj->type != JSON_NULL) {
189 const char* salt_val = jsonObjectGetString(
190 jsonObjectGetKeyConst(salt_obj, "actor.get_salt"));
192 // caller expects a free-able string, could be NULL.
193 if (salt_val) { salt_str = strdup(salt_val); }
196 jsonObjectFree(salt_obj);
202 // ident is either a username or barcode
203 // Returns the init seed -> requires free();
204 static char* oilsAuthBuildInitCache(
205 int user_id, const char* ident, const char* ident_type, const char* nonce) {
207 char* cache_key = va_list_to_string(
208 "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce);
210 char* count_key = va_list_to_string(
211 "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX);
215 // user does not exist. Use a dummy seed
216 auth_seed = strdup("x");
218 auth_seed = oilsAuthGetSalt(user_id);
221 jsonObject* seed_object = jsonParseFmt(
222 "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
223 ident_type, ident, user_id, auth_seed);
225 jsonObject* count_object = osrfCacheGetObject(count_key);
227 count_object = jsonNewNumberObject((double) 0);
230 osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
233 // Only track login counts for existing users, since a
234 // login for a nonexistent user will never succeed anyway.
235 osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
238 osrfLogDebug(OSRF_LOG_MARK,
239 "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
243 jsonObjectFree(count_object);
244 jsonObjectFree(seed_object);
249 static int oilsAuthInitUsernameHandler(
250 osrfMethodContext* ctx, const char* username, const char* nonce) {
252 osrfLogInfo(OSRF_LOG_MARK,
253 "User logging in with username %s", username);
256 jsonObject* resp = NULL; // free
257 jsonObject* user_obj = oilsUtilsFetchUserByUsername(ctx, username); // free
259 if (user_obj && user_obj->type != JSON_NULL)
260 user_id = oilsFMGetObjectId(user_obj);
262 jsonObjectFree(user_obj); // NULL OK
264 char* seed = oilsAuthBuildInitCache(user_id, username, "username", nonce);
265 resp = jsonNewObject(seed);
268 osrfAppRespondComplete(ctx, resp);
269 jsonObjectFree(resp);
273 // open-ils.auth.authenticate.init.username
274 int oilsAuthInitUsername(osrfMethodContext* ctx) {
275 OSRF_METHOD_VERIFY_CONTEXT(ctx);
277 char* username = // free
278 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
280 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
282 if (!nonce) nonce = "";
283 if (!username) return -1;
285 int resp = oilsAuthInitUsernameHandler(ctx, username, nonce);
291 static int oilsAuthInitBarcodeHandler(
292 osrfMethodContext* ctx, const char* barcode, const char* nonce) {
294 osrfLogInfo(OSRF_LOG_MARK,
295 "User logging in with barcode %s", barcode);
298 jsonObject* resp = NULL; // free
299 jsonObject* user_obj = oilsUtilsFetchUserByBarcode(ctx, barcode); // free
301 if (user_obj && user_obj->type != JSON_NULL)
302 user_id = oilsFMGetObjectId(user_obj);
304 jsonObjectFree(user_obj); // NULL OK
306 char* seed = oilsAuthBuildInitCache(user_id, barcode, "barcode", nonce);
307 resp = jsonNewObject(seed);
310 osrfAppRespondComplete(ctx, resp);
311 jsonObjectFree(resp);
316 // open-ils.auth.authenticate.init.barcode
317 int oilsAuthInitBarcode(osrfMethodContext* ctx) {
318 OSRF_METHOD_VERIFY_CONTEXT(ctx);
320 char* barcode = // free
321 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
323 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
325 if (!nonce) nonce = "";
326 if (!barcode) return -1;
328 int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce);
334 // returns true if the provided identifier matches the barcode regex.
335 static int oilsAuthIdentIsBarcode(const char* identifier, int org_id) {
338 org_id = oilsUtilsGetRootOrgId();
340 char* bc_regex = oilsUtilsFetchOrgSetting(org_id, "opac.barcode_regex");
343 // if no regex is set, assume any identifier starting
344 // with a number is a barcode.
345 bc_regex = strdup("^\\d"); // dupe for later free'ing
349 int err_offset, match_ret;
351 pcre *compiled = pcre_compile(
352 bc_regex, 0, &err_str, &err_offset, NULL);
354 if (compiled == NULL) {
355 osrfLogError(OSRF_LOG_MARK,
356 "Could not compile '%s': %s", bc_regex, err_str);
362 pcre_extra *extra = pcre_study(compiled, 0, &err_str);
364 if(err_str != NULL) {
365 osrfLogError(OSRF_LOG_MARK,
366 "Could not study regex '%s': %s", bc_regex, err_str);
372 match_ret = pcre_exec(
373 compiled, extra, identifier, strlen(identifier), 0, 0, NULL, 0);
377 if (extra) pcre_free(extra);
379 if (match_ret >= 0) return 1; // regex matched
381 if (match_ret != PCRE_ERROR_NOMATCH)
382 osrfLogError(OSRF_LOG_MARK, "Unknown error processing barcode regex");
384 return 0; // regex did not match
389 @brief Implement the "init" method.
390 @param ctx The method context.
391 @return Zero if successful, or -1 if not.
395 - nonce : optional login seed (string) provided by the caller which
396 is added to the auth init cache to differentiate between logins
397 using the same username and thus avoiding cache collisions for
398 near-simultaneous logins.
400 Return to client: Intermediate authentication seed.
402 int oilsAuthInit(osrfMethodContext* ctx) {
403 OSRF_METHOD_VERIFY_CONTEXT(ctx);
406 char* identifier = // free
407 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
409 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
411 if (!nonce) nonce = "";
412 if (!identifier) return -1; // we need an identifier
414 if (oilsAuthIdentIsBarcode(identifier, 0)) {
415 resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce);
417 resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce);
425 Returns 1 if the password provided matches the user's real password
430 @brief Verify the password received from the client.
431 @param ctx The method context.
432 @param userObj An object from the database, representing the user.
433 @param password An obfuscated password received from the client.
434 @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
436 (None of the so-called "passwords" used here are in plaintext. All have been passed
437 through at least one layer of hashing to obfuscate them.)
439 Take the password from the user object. Append it to the username seed from memcache,
440 as stored previously by a call to the init method. Take an md5 hash of the result.
441 Then compare this hash to the password received from the client.
443 In order for the two to match, other than by dumb luck, the client had to construct
444 the password it passed in the same way. That means it neded to know not only the
445 original password (either hashed or plaintext), but also the seed. The latter requirement
446 means that the client process needs either to be the same process that called the init
447 method or to receive the seed from the process that did so.
449 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id,
450 const char* identifier, const char* password, const char* nonce) {
454 // We won't be needing the seed again, remove it
455 osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
457 // Ask the DB to verify the user's password.
458 // Here, the password is md5(md5(password) + salt)
460 jsonObject* params = jsonParseFmt( // free
461 "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}",
464 jsonObject* verify_obj = // free
465 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
467 jsonObjectFree(params);
470 verified = oilsUtilsIsDBTrue(
472 jsonObjectGetKeyConst(
473 verify_obj, "actor.verify_passwd")));
475 jsonObjectFree(verify_obj);
478 char* countkey = va_list_to_string("%s%s%s",
479 OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
480 jsonObject* countobject = osrfCacheGetObject( countkey );
482 long failcount = (long) jsonObjectGetNumber( countobject );
483 if(failcount >= _oilsAuthBlockCount) {
485 osrfLogInfo(OSRF_LOG_MARK,
486 "oilsAuth found too many recent failures for '%s' : %i, "
487 "forcing failure state.", identifier, failcount);
492 jsonObjectSetNumber( countobject, failcount );
493 osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
494 jsonObjectFree(countobject);
502 * Returns true if the provided password is correct.
503 * Turn the password into the nested md5 hash required of migrated
504 * passwords, then check the password in the DB.
506 static int oilsAuthLoginCheckPassword(int user_id, const char* password) {
508 growing_buffer* gb = buffer_init(33); // free me 1
509 char* salt = oilsAuthGetSalt(user_id); // free me 2
510 char* passhash = md5sum(password); // free me 3
512 buffer_add(gb, salt); // gb strdup's internally
513 buffer_add(gb, passhash);
515 free(salt); // free 2
516 free(passhash); // free 3
518 // salt + md5(password)
519 passhash = buffer_release(gb); // free 1 ; free me 4
520 char* finalpass = md5sum(passhash); // free me 5
522 free(passhash); // free 4
524 jsonObject *arr = jsonNewObjectType(JSON_ARRAY);
525 jsonObjectPush(arr, jsonNewObject("actor.verify_passwd"));
526 jsonObjectPush(arr, jsonNewNumberObject((long) user_id));
527 jsonObjectPush(arr, jsonNewObject("main"));
528 jsonObjectPush(arr, jsonNewObject(finalpass));
529 jsonObject *params = jsonNewObjectType(JSON_HASH); // free me 6
530 jsonObjectSetKey(params, "from", arr);
532 free(finalpass); // free 5
534 jsonObject* verify_obj = // free
535 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
537 jsonObjectFree(params); // free 6
539 if (!verify_obj) return 0; // error
541 int verified = oilsUtilsIsDBTrue(
543 jsonObjectGetKeyConst(verify_obj, "actor.verify_passwd")
547 jsonObjectFree(verify_obj);
552 static int oilsAuthLoginVerifyPassword(const osrfMethodContext* ctx,
553 int user_id, const char* username, const char* password) {
555 // build the cache key
556 growing_buffer* gb = buffer_init(64); // free me
557 buffer_add(gb, OILS_AUTH_CACHE_PRFX);
558 buffer_add(gb, username);
559 buffer_add(gb, OILS_AUTH_COUNT_SFFX);
560 char* countkey = buffer_release(gb); // free me
562 jsonObject* countobject = osrfCacheGetObject(countkey); // free me
566 failcount = (long) jsonObjectGetNumber(countobject);
568 if (failcount >= _oilsAuthBlockCount) {
569 // User is blocked. Don't waste any more CPU cycles on them.
571 osrfLogInfo(OSRF_LOG_MARK,
572 "oilsAuth found too many recent failures for '%s' : %i, "
573 "forcing failure state.", username, failcount);
575 jsonObjectFree(countobject);
581 int verified = oilsAuthLoginCheckPassword(user_id, password);
583 if (!verified) { // login failed. increment failure counter.
587 // append to existing counter
588 jsonObjectSetNumber(countobject, failcount);
591 // first failure, create a new counter
592 countobject = jsonNewNumberObject((double) failcount);
595 osrfCachePutObject(countkey, countobject, _oilsAuthBlockTimeout);
598 jsonObjectFree(countobject); // NULL OK
606 Adds the authentication token to the user cache. The timeout for the
607 auth token is based on the type of login as well as (if type=='opac')
609 Returns the event that should be returned to the user.
612 static oilsEvent* oilsAuthHandleLoginOK( osrfMethodContext* ctx, jsonObject* userObj, const char* uname,
613 const char* type, int orgloc, const char* workstation ) {
615 oilsEvent* response = NULL;
617 jsonObject* params = jsonNewObject(NULL);
618 jsonObjectSetKey(params, "user_id",
619 jsonNewNumberObject(oilsFMGetObjectId(userObj)));
620 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
621 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
623 jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
625 jsonObject* authEvt = oilsUtilsQuickReqCtx(
627 "open-ils.auth_internal",
628 "open-ils.auth_internal.session.create", params);
629 jsonObjectFree(params);
633 response = oilsNewEvent2(
635 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
636 jsonObjectGetKey(authEvt, "payload") // cloned within Event
639 osrfLogActivity(OSRF_LOG_MARK,
640 "successful login: username=%s, authtoken=%s, workstation=%s",
643 jsonObjectGetKeyConst(
644 jsonObjectGetKeyConst(authEvt, "payload"),
648 workstation ? workstation : ""
651 jsonObjectFree(authEvt);
654 osrfLogError(OSRF_LOG_MARK,
655 "Error caching auth session in open-ils.auth_internal");
663 @brief Implement the "complete" method.
664 @param ctx The method context.
665 @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
666 client to indicate completion; a positive integer if successful but no such STATUS
667 message has been sent.
670 - a hash with some combination of the following elements:
673 - "password" (hashed with the cached seed; not plaintext)
677 - "agent" (what software/interface/3rd-party is making the request)
678 - "nonce" optional login seed to differentiate logins using the same username.
680 The password is required. Either a username or a barcode must also be present.
682 Return to client: Intermediate authentication seed.
684 Validate the password, using the username if available, or the barcode if not. The
685 user must be active, and not barred from logging on. The barcode, if used for
686 authentication, must be active as well. The workstation, if specified, must be valid.
688 Upon deciding whether to allow the logon, return a corresponding event to the client.
690 int oilsAuthComplete( osrfMethodContext* ctx ) {
691 OSRF_METHOD_VERIFY_CONTEXT(ctx);
693 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
695 const char* uname = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
696 const char* identifier = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
697 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
698 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
699 int orgloc = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
700 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
701 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
702 const char* ewho = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
703 const char* nonce = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
705 const char* ws = (workstation) ? workstation : "";
706 if (!nonce) nonce = "";
708 // we no longer care how the identifier reaches us,
709 // as long as we have one.
713 } else if (barcode) {
714 identifier = barcode;
719 return osrfAppRequestRespondException(ctx->session, ctx->request,
720 "username/barcode and password required for method: %s",
724 osrfLogInfo(OSRF_LOG_MARK,
725 "Patron completing authentication with identifer %s", identifier);
727 /* Use __FILE__, harmless_line_number for creating
728 * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
729 * giving away information about why an authentication attempt failed.
731 int harmless_line_number = __LINE__;
734 type = OILS_AUTH_STAFF;
736 oilsEvent* response = NULL; // free
737 jsonObject* userObj = NULL; // free
739 char* cache_key = va_list_to_string(
740 "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
741 jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
744 return osrfAppRequestRespondException(ctx->session,
745 ctx->request, "No authentication seed found. "
746 "open-ils.auth.authenticate.init must be called first "
747 " (check that memcached is running and can be connected to) "
751 int user_id = jsonObjectGetNumber(
752 jsonObjectGetKeyConst(cacheObj, "user_id"));
755 // User was not found during init. Clean up and exit early.
756 response = oilsNewEvent(
757 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
758 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
759 oilsEventFree(response); // frees event JSON
760 osrfCacheRemove(cache_key);
761 jsonObjectFree(cacheObj);
765 jsonObject* param = jsonNewNumberObject(user_id); // free
766 userObj = oilsUtilsCStoreReqCtx(
767 ctx, "open-ils.cstore.direct.actor.user.retrieve", param);
768 jsonObjectFree(param);
770 char* freeable_uname = NULL;
772 uname = freeable_uname = oilsFMGetString(userObj, "usrname");
775 // See if the user is allowed to login.
777 jsonObject* params = jsonNewObject(NULL);
778 jsonObjectSetKey(params, "user_id",
779 jsonNewNumberObject(oilsFMGetObjectId(userObj)));
780 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
781 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
782 if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
784 jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
786 "open-ils.auth_internal",
787 "open-ils.auth_internal.user.validate", params);
788 jsonObjectFree(params);
791 // Something went seriously wrong. Get outta here before
792 // we start segfaulting.
793 jsonObjectFree(userObj);
794 if(freeable_uname) free(freeable_uname);
798 const char* authEvtCode =
799 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
801 if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
802 // Received the generic login failure event.
804 osrfLogInfo(OSRF_LOG_MARK,
805 "failed login: username=%s, barcode=%s, workstation=%s",
806 uname, (barcode ? barcode : "(none)"), ws);
808 response = oilsNewEvent(
809 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
815 // User exists and is not barred, etc. Test the password.
817 passOK = oilsAuthVerifyPassword(
818 ctx, user_id, identifier, password, nonce);
821 // Password check failed. Return generic login failure.
823 response = oilsNewEvent(
824 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
826 osrfLogInfo(OSRF_LOG_MARK,
827 "failed login: username=%s, barcode=%s, workstation=%s",
828 uname, (barcode ? barcode : "(none)"), ws );
833 // Below here, we know the password check succeeded if no response
834 // object is present.
837 !strcmp(authEvtCode, "PATRON_INACTIVE") ||
838 !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
839 // Patron and/or card is inactive but the correct password
840 // was provided. Alert the caller to the inactive-ness.
841 response = oilsNewEvent2(
842 OSRF_LOG_MARK, authEvtCode,
843 jsonObjectGetKey(authEvt, "payload") // cloned within Event
847 if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
848 // Validate API returned an unexpected non-success event.
849 // To be safe, treat this as a generic login failure.
851 response = oilsNewEvent(
852 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
856 // password OK and no other events have prevented login completion.
858 char* ewhat = "login";
860 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
861 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
865 response = oilsAuthHandleLoginOK(
866 ctx, userObj, uname, type, orgloc, workstation);
869 oilsUtilsTrackUserActivity(
871 oilsFMGetObjectId(userObj),
873 osrfAppSessionGetIngress()
878 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
881 oilsEventFree(response);
882 jsonObjectFree(userObj);
883 jsonObjectFree(authEvt);
884 jsonObjectFree(cacheObj);
886 free(freeable_uname);
892 int oilsAuthLogin(osrfMethodContext* ctx) {
893 OSRF_METHOD_VERIFY_CONTEXT(ctx);
895 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
897 const char* username = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
898 const char* identifier = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
899 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
900 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
901 int orgloc = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
902 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
903 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
904 const char* ewho = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
906 const char* ws = (workstation) ? workstation : "";
907 if (!type) type = OILS_AUTH_STAFF;
909 jsonObject* userObj = NULL; // free me
910 oilsEvent* response = NULL; // free me
912 /* Use __FILE__, harmless_line_number for creating
913 * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
914 * giving away information about why an authentication attempt failed.
916 int harmless_line_number = __LINE__;
918 // translate a generic identifier into a username or barcode if necessary.
919 if (identifier && !username && !barcode) {
920 if (oilsAuthIdentIsBarcode(identifier, orgloc)) {
921 barcode = identifier;
923 username = identifier;
928 barcode = NULL; // avoid superfluous identifiers
929 userObj = oilsUtilsFetchUserByUsername(ctx, username);
931 } else if (barcode) {
932 userObj = oilsUtilsFetchUserByBarcode(ctx, barcode);
936 return osrfAppRequestRespondException(ctx->session, ctx->request,
937 "username/barcode and password required for method: %s",
941 if (!userObj) { // user not found.
942 response = oilsNewEvent(
943 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
944 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
945 oilsEventFree(response); // frees event JSON
949 long user_id = oilsFMGetObjectId(userObj);
951 // username is freed when userObj is freed.
952 // From here we can use the username as the generic identifier
953 // since it's guaranteed to have a value.
954 if (!username) username = oilsFMGetStringConst(userObj, "usrname");
956 // See if the user is allowed to login.
957 jsonObject* params = jsonNewObject(NULL);
958 jsonObjectSetKey(params, "user_id", jsonNewNumberObject(user_id));
959 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
960 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
961 if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
963 jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
965 "open-ils.auth_internal",
966 "open-ils.auth_internal.user.validate", params);
967 jsonObjectFree(params);
969 if (!authEvt) { // unknown error
970 jsonObjectFree(userObj);
974 const char* authEvtCode =
975 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
977 if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
978 // Received the generic login failure event.
980 osrfLogInfo(OSRF_LOG_MARK,
981 "failed login: username=%s, barcode=%s, workstation=%s",
982 username, (barcode ? barcode : "(none)"), ws);
984 response = oilsNewEvent(
985 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
988 if (!response && // user exists and is not barred, etc.
989 !oilsAuthLoginVerifyPassword(ctx, user_id, username, password)) {
990 // User provided the wrong password or is blocked from too
991 // many previous login failures.
993 response = oilsNewEvent(
994 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
996 osrfLogInfo(OSRF_LOG_MARK,
997 "failed login: username=%s, barcode=%s, workstation=%s",
998 username, (barcode ? barcode : "(none)"), ws );
1001 // Below here, we know the password check succeeded if no response
1002 // object is present.
1005 !strcmp(authEvtCode, "PATRON_INACTIVE") ||
1006 !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
1007 // Patron and/or card is inactive but the correct password
1008 // was provided. Alert the caller to the inactive-ness.
1009 response = oilsNewEvent2(
1010 OSRF_LOG_MARK, authEvtCode,
1011 jsonObjectGetKey(authEvt, "payload") // cloned within Event
1015 if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
1016 // Validate API returned an unexpected non-success event.
1017 // To be safe, treat this as a generic login failure.
1019 response = oilsNewEvent(
1020 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
1024 // password OK and no other events have prevented login completion.
1026 char* ewhat = "login";
1028 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
1029 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
1033 response = oilsAuthHandleLoginOK(
1034 ctx, userObj, username, type, orgloc, workstation);
1037 oilsUtilsTrackUserActivity(
1039 oilsFMGetObjectId(userObj),
1041 osrfAppSessionGetIngress()
1046 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
1049 oilsEventFree(response);
1050 jsonObjectFree(userObj);
1051 jsonObjectFree(authEvt);
1058 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
1059 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1061 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
1062 jsonObject* resp = NULL;
1065 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
1066 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
1067 osrfCacheRemove(key);
1068 resp = jsonNewObject(authToken); /**/
1072 osrfAppRespondComplete( ctx, resp );
1073 jsonObjectFree(resp);
1078 * Fetches the user object from the database and updates the user object in
1079 * the cache object, which then has to be re-inserted into the cache.
1080 * User object is retrieved inside a transaction to avoid replication issues.
1082 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
1084 osrfAppSession* session;
1086 jsonObject *param, *userObj, *newUserObj = NULL;
1088 userObj = jsonObjectGetKey( cacheObj, "userobj" );
1089 userId = oilsFMGetObjectId( userObj );
1091 session = osrfAppSessionClientInit( "open-ils.cstore" );
1092 osrfAppSessionConnect(session);
1094 reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
1095 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1099 osrfMessageFree(omsg);
1100 param = jsonNewNumberObject(userId);
1101 reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
1102 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1103 jsonObjectFree(param);
1106 newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
1107 osrfMessageFree(omsg);
1108 reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
1109 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1110 osrfMessageFree(omsg);
1114 osrfAppSessionFree(session); // calls disconnect internally
1118 // ws_ou and wsid are ephemeral and need to be manually propagated
1119 // oilsFMSetString dupe()'s internally, no need to clone the string
1120 oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
1121 oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
1123 jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
1124 jsonObjectSetKey(cacheObj, "userobj", newUserObj);
1128 osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
1133 Resets the auth login timeout
1134 @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
1136 static oilsEvent* _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
1137 if(!authToken) return NULL;
1139 oilsEvent* evt = NULL;
1142 osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
1143 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1144 jsonObject* cacheObj = osrfCacheGetObject( key );
1147 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
1148 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1153 _oilsAuthReloadUser(cacheObj);
1156 // Determine a new timeout value
1157 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
1159 // Extend the current endtime by a fixed amount
1160 time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
1161 int reset_interval = DEFAULT_RESET_INTERVAL;
1162 const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
1163 cacheObj, "reset_interval" );
1164 if( reset_interval_obj ) {
1165 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
1166 if( reset_interval <= 0 )
1167 reset_interval = DEFAULT_RESET_INTERVAL;
1170 time_t now = time( NULL );
1171 time_t new_endtime = now + reset_interval;
1172 if( new_endtime > endtime ) {
1173 // Keep the session alive a little longer
1174 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
1175 timeout = reset_interval;
1176 osrfCachePutObject( key, cacheObj, timeout );
1178 // The session isn't close to expiring, so don't reset anything.
1179 // Just report the time remaining.
1180 timeout = endtime - now;
1183 // Reapply the existing timeout from the current time
1184 timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
1185 osrfCachePutObject( key, cacheObj, timeout );
1188 jsonObject* payload = jsonNewNumberObject( (double) timeout );
1189 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
1190 jsonObjectFree(payload);
1191 jsonObjectFree(cacheObj);
1198 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
1199 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1200 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1201 double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
1202 oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
1203 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
1209 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
1210 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1211 bool returnFull = false;
1212 bool noTimeoutReset = false;
1214 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1216 if(ctx->params->size > 1) {
1217 // caller wants full cached object, with authtime, etc.
1218 const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
1219 if(rt && strcmp(rt, "0") != 0)
1222 if (ctx->params->size > 2) {
1223 // Avoid resetting the auth session timeout.
1224 const char* noReset =
1225 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 2));
1226 if (noReset && strcmp(noReset, "0") != 0)
1227 noTimeoutReset = true;
1231 jsonObject* cacheObj = NULL;
1232 oilsEvent* evt = NULL;
1236 // Reset the timeout to keep the session alive
1237 if (!noTimeoutReset)
1238 evt = _oilsAuthResetTimeout(authToken, 0);
1240 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
1241 osrfAppRespondComplete( ctx, oilsEventToJSON( evt )); // can't reset timeout
1245 // Retrieve the cached session object
1246 osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
1247 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1248 cacheObj = osrfCacheGetObject( key );
1250 // Return a copy of the cached user object
1252 osrfAppRespondComplete( ctx, cacheObj);
1254 osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
1255 jsonObjectFree(cacheObj);
1257 // Auth token is invalid or expired
1258 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1259 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
1260 oilsEventFree(evt2);
1268 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1269 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );