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 char* key = va_list_to_string("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce ); /**/
456 osrfCacheRemove(key);
459 // Ask the DB to verify the user's password.
460 // Here, the password is md5(md5(password) + salt)
462 jsonObject* params = jsonParseFmt( // free
463 "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}",
466 jsonObject* verify_obj = // free
467 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
469 jsonObjectFree(params);
472 verified = oilsUtilsIsDBTrue(
474 jsonObjectGetKeyConst(
475 verify_obj, "actor.verify_passwd")));
477 jsonObjectFree(verify_obj);
480 char* countkey = va_list_to_string("%s%s%s",
481 OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
482 jsonObject* countobject = osrfCacheGetObject( countkey );
484 long failcount = (long) jsonObjectGetNumber( countobject );
485 if(failcount >= _oilsAuthBlockCount) {
487 osrfLogInfo(OSRF_LOG_MARK,
488 "oilsAuth found too many recent failures for '%s' : %i, "
489 "forcing failure state.", identifier, failcount);
494 jsonObjectSetNumber( countobject, failcount );
495 osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
496 jsonObjectFree(countobject);
504 * Returns true if the provided password is correct.
505 * Turn the password into the nested md5 hash required of migrated
506 * passwords, then check the password in the DB.
508 static int oilsAuthLoginCheckPassword(int user_id, const char* password) {
510 growing_buffer* gb = buffer_init(33); // free me 1
511 char* salt = oilsAuthGetSalt(user_id); // free me 2
512 char* passhash = md5sum(password); // free me 3
514 buffer_add(gb, salt); // gb strdup's internally
515 buffer_add(gb, passhash);
517 free(salt); // free 2
518 free(passhash); // free 3
520 // salt + md5(password)
521 passhash = buffer_release(gb); // free 1 ; free me 4
522 char* finalpass = md5sum(passhash); // free me 5
524 free(passhash); // free 4
526 jsonObject *arr = jsonNewObjectType(JSON_ARRAY);
527 jsonObjectPush(arr, jsonNewObject("actor.verify_passwd"));
528 jsonObjectPush(arr, jsonNewNumberObject((long) user_id));
529 jsonObjectPush(arr, jsonNewObject("main"));
530 jsonObjectPush(arr, jsonNewObject(finalpass));
531 jsonObject *params = jsonNewObjectType(JSON_HASH); // free me 6
532 jsonObjectSetKey(params, "from", arr);
534 free(finalpass); // free 5
536 jsonObject* verify_obj = // free
537 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
539 jsonObjectFree(params); // free 6
541 if (!verify_obj) return 0; // error
543 int verified = oilsUtilsIsDBTrue(
545 jsonObjectGetKeyConst(verify_obj, "actor.verify_passwd")
549 jsonObjectFree(verify_obj);
554 static int oilsAuthLoginVerifyPassword(const osrfMethodContext* ctx,
555 int user_id, const char* username, const char* password) {
557 // build the cache key
558 growing_buffer* gb = buffer_init(64); // free me
559 buffer_add(gb, OILS_AUTH_CACHE_PRFX);
560 buffer_add(gb, username);
561 buffer_add(gb, OILS_AUTH_COUNT_SFFX);
562 char* countkey = buffer_release(gb); // free me
564 jsonObject* countobject = osrfCacheGetObject(countkey); // free me
568 failcount = (long) jsonObjectGetNumber(countobject);
570 if (failcount >= _oilsAuthBlockCount) {
571 // User is blocked. Don't waste any more CPU cycles on them.
573 osrfLogInfo(OSRF_LOG_MARK,
574 "oilsAuth found too many recent failures for '%s' : %i, "
575 "forcing failure state.", username, failcount);
577 jsonObjectFree(countobject);
583 int verified = oilsAuthLoginCheckPassword(user_id, password);
585 if (!verified) { // login failed. increment failure counter.
589 // append to existing counter
590 jsonObjectSetNumber(countobject, failcount);
593 // first failure, create a new counter
594 countobject = jsonNewNumberObject((double) failcount);
597 osrfCachePutObject(countkey, countobject, _oilsAuthBlockTimeout);
600 jsonObjectFree(countobject); // NULL OK
608 Adds the authentication token to the user cache. The timeout for the
609 auth token is based on the type of login as well as (if type=='opac')
611 Returns the event that should be returned to the user.
614 static oilsEvent* oilsAuthHandleLoginOK( osrfMethodContext* ctx, jsonObject* userObj, const char* uname,
615 const char* type, int orgloc, const char* workstation ) {
617 oilsEvent* response = NULL;
619 jsonObject* params = jsonNewObject(NULL);
620 jsonObjectSetKey(params, "user_id",
621 jsonNewNumberObject(oilsFMGetObjectId(userObj)));
622 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
623 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
625 jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
627 jsonObject* authEvt = oilsUtilsQuickReqCtx(
629 "open-ils.auth_internal",
630 "open-ils.auth_internal.session.create", params);
631 jsonObjectFree(params);
635 response = oilsNewEvent2(
637 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
638 jsonObjectGetKey(authEvt, "payload") // cloned within Event
641 osrfLogActivity(OSRF_LOG_MARK,
642 "successful login: username=%s, authtoken=%s, workstation=%s",
645 jsonObjectGetKeyConst(
646 jsonObjectGetKeyConst(authEvt, "payload"),
650 workstation ? workstation : ""
653 jsonObjectFree(authEvt);
656 osrfLogError(OSRF_LOG_MARK,
657 "Error caching auth session in open-ils.auth_internal");
665 @brief Implement the "complete" method.
666 @param ctx The method context.
667 @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
668 client to indicate completion; a positive integer if successful but no such STATUS
669 message has been sent.
672 - a hash with some combination of the following elements:
675 - "password" (hashed with the cached seed; not plaintext)
679 - "agent" (what software/interface/3rd-party is making the request)
680 - "nonce" optional login seed to differentiate logins using the same username.
682 The password is required. Either a username or a barcode must also be present.
684 Return to client: Intermediate authentication seed.
686 Validate the password, using the username if available, or the barcode if not. The
687 user must be active, and not barred from logging on. The barcode, if used for
688 authentication, must be active as well. The workstation, if specified, must be valid.
690 Upon deciding whether to allow the logon, return a corresponding event to the client.
692 int oilsAuthComplete( osrfMethodContext* ctx ) {
693 OSRF_METHOD_VERIFY_CONTEXT(ctx);
695 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
697 const char* uname = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
698 const char* identifier = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
699 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
700 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
701 int orgloc = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
702 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
703 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
704 const char* ewho = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
705 const char* nonce = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
707 const char* ws = (workstation) ? workstation : "";
708 if (!nonce) nonce = "";
710 // we no longer care how the identifier reaches us,
711 // as long as we have one.
715 } else if (barcode) {
716 identifier = barcode;
721 return osrfAppRequestRespondException(ctx->session, ctx->request,
722 "username/barcode and password required for method: %s",
726 osrfLogInfo(OSRF_LOG_MARK,
727 "Patron completing authentication with identifer %s", identifier);
729 /* Use __FILE__, harmless_line_number for creating
730 * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
731 * giving away information about why an authentication attempt failed.
733 int harmless_line_number = __LINE__;
736 type = OILS_AUTH_STAFF;
738 oilsEvent* response = NULL; // free
739 jsonObject* userObj = NULL; // free
741 char* cache_key = va_list_to_string(
742 "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
743 jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
746 return osrfAppRequestRespondException(ctx->session,
747 ctx->request, "No authentication seed found. "
748 "open-ils.auth.authenticate.init must be called first "
749 " (check that memcached is running and can be connected to) "
753 int user_id = jsonObjectGetNumber(
754 jsonObjectGetKeyConst(cacheObj, "user_id"));
757 // User was not found during init. Clean up and exit early.
758 response = oilsNewEvent(
759 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
760 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
761 oilsEventFree(response); // frees event JSON
762 osrfCacheRemove(cache_key);
763 jsonObjectFree(cacheObj);
767 jsonObject* param = jsonNewNumberObject(user_id); // free
768 userObj = oilsUtilsCStoreReqCtx(
769 ctx, "open-ils.cstore.direct.actor.user.retrieve", param);
770 jsonObjectFree(param);
772 char* freeable_uname = NULL;
774 uname = freeable_uname = oilsFMGetString(userObj, "usrname");
777 // See if the user is allowed to login.
779 jsonObject* params = jsonNewObject(NULL);
780 jsonObjectSetKey(params, "user_id",
781 jsonNewNumberObject(oilsFMGetObjectId(userObj)));
782 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
783 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
784 if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
786 jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
788 "open-ils.auth_internal",
789 "open-ils.auth_internal.user.validate", params);
790 jsonObjectFree(params);
793 // Something went seriously wrong. Get outta here before
794 // we start segfaulting.
795 jsonObjectFree(userObj);
796 if(freeable_uname) free(freeable_uname);
800 const char* authEvtCode =
801 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
803 if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
804 // Received the generic login failure event.
806 osrfLogInfo(OSRF_LOG_MARK,
807 "failed login: username=%s, barcode=%s, workstation=%s",
808 uname, (barcode ? barcode : "(none)"), ws);
810 response = oilsNewEvent(
811 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
817 // User exists and is not barred, etc. Test the password.
819 passOK = oilsAuthVerifyPassword(
820 ctx, user_id, identifier, password, nonce);
823 // Password check failed. Return generic login failure.
825 response = oilsNewEvent(
826 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
828 osrfLogInfo(OSRF_LOG_MARK,
829 "failed login: username=%s, barcode=%s, workstation=%s",
830 uname, (barcode ? barcode : "(none)"), ws );
835 // Below here, we know the password check succeeded if no response
836 // object is present.
839 !strcmp(authEvtCode, "PATRON_INACTIVE") ||
840 !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
841 // Patron and/or card is inactive but the correct password
842 // was provided. Alert the caller to the inactive-ness.
843 response = oilsNewEvent2(
844 OSRF_LOG_MARK, authEvtCode,
845 jsonObjectGetKey(authEvt, "payload") // cloned within Event
849 if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
850 // Validate API returned an unexpected non-success event.
851 // To be safe, treat this as a generic login failure.
853 response = oilsNewEvent(
854 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
858 // password OK and no other events have prevented login completion.
860 char* ewhat = "login";
862 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
863 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
867 response = oilsAuthHandleLoginOK(
868 ctx, userObj, uname, type, orgloc, workstation);
871 oilsUtilsTrackUserActivity(
873 oilsFMGetObjectId(userObj),
875 osrfAppSessionGetIngress()
880 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
883 oilsEventFree(response);
884 jsonObjectFree(userObj);
885 jsonObjectFree(authEvt);
886 jsonObjectFree(cacheObj);
888 free(freeable_uname);
894 int oilsAuthLogin(osrfMethodContext* ctx) {
895 OSRF_METHOD_VERIFY_CONTEXT(ctx);
897 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
899 const char* username = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
900 const char* identifier = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
901 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
902 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
903 int orgloc = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
904 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
905 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
906 const char* ewho = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
908 const char* ws = (workstation) ? workstation : "";
909 if (!type) type = OILS_AUTH_STAFF;
911 jsonObject* userObj = NULL; // free me
912 oilsEvent* response = NULL; // free me
914 /* Use __FILE__, harmless_line_number for creating
915 * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
916 * giving away information about why an authentication attempt failed.
918 int harmless_line_number = __LINE__;
920 // translate a generic identifier into a username or barcode if necessary.
921 if (identifier && !username && !barcode) {
922 if (oilsAuthIdentIsBarcode(identifier, orgloc)) {
923 barcode = identifier;
925 username = identifier;
930 barcode = NULL; // avoid superfluous identifiers
931 userObj = oilsUtilsFetchUserByUsername(ctx, username);
933 } else if (barcode) {
934 userObj = oilsUtilsFetchUserByBarcode(ctx, barcode);
938 return osrfAppRequestRespondException(ctx->session, ctx->request,
939 "username/barcode and password required for method: %s",
943 if (!userObj) { // user not found.
944 response = oilsNewEvent(
945 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
946 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
947 oilsEventFree(response); // frees event JSON
951 long user_id = oilsFMGetObjectId(userObj);
953 // username is freed when userObj is freed.
954 // From here we can use the username as the generic identifier
955 // since it's guaranteed to have a value.
956 if (!username) username = oilsFMGetStringConst(userObj, "usrname");
958 // See if the user is allowed to login.
959 jsonObject* params = jsonNewObject(NULL);
960 jsonObjectSetKey(params, "user_id", jsonNewNumberObject(user_id));
961 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
962 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
963 if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
965 jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
967 "open-ils.auth_internal",
968 "open-ils.auth_internal.user.validate", params);
969 jsonObjectFree(params);
971 if (!authEvt) { // unknown error
972 jsonObjectFree(userObj);
976 const char* authEvtCode =
977 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
979 if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
980 // Received the generic login failure event.
982 osrfLogInfo(OSRF_LOG_MARK,
983 "failed login: username=%s, barcode=%s, workstation=%s",
984 username, (barcode ? barcode : "(none)"), ws);
986 response = oilsNewEvent(
987 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
990 if (!response && // user exists and is not barred, etc.
991 !oilsAuthLoginVerifyPassword(ctx, user_id, username, password)) {
992 // User provided the wrong password or is blocked from too
993 // many previous login failures.
995 response = oilsNewEvent(
996 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
998 osrfLogInfo(OSRF_LOG_MARK,
999 "failed login: username=%s, barcode=%s, workstation=%s",
1000 username, (barcode ? barcode : "(none)"), ws );
1003 // Below here, we know the password check succeeded if no response
1004 // object is present.
1007 !strcmp(authEvtCode, "PATRON_INACTIVE") ||
1008 !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
1009 // Patron and/or card is inactive but the correct password
1010 // was provided. Alert the caller to the inactive-ness.
1011 response = oilsNewEvent2(
1012 OSRF_LOG_MARK, authEvtCode,
1013 jsonObjectGetKey(authEvt, "payload") // cloned within Event
1017 if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
1018 // Validate API returned an unexpected non-success event.
1019 // To be safe, treat this as a generic login failure.
1021 response = oilsNewEvent(
1022 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
1026 // password OK and no other events have prevented login completion.
1028 char* ewhat = "login";
1030 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
1031 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
1035 response = oilsAuthHandleLoginOK(
1036 ctx, userObj, username, type, orgloc, workstation);
1039 oilsUtilsTrackUserActivity(
1041 oilsFMGetObjectId(userObj),
1043 osrfAppSessionGetIngress()
1048 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
1051 oilsEventFree(response);
1052 jsonObjectFree(userObj);
1053 jsonObjectFree(authEvt);
1060 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
1061 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1063 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
1064 jsonObject* resp = NULL;
1067 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
1068 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
1069 osrfCacheRemove(key);
1070 resp = jsonNewObject(authToken); /**/
1074 osrfAppRespondComplete( ctx, resp );
1075 jsonObjectFree(resp);
1080 * Fetches the user object from the database and updates the user object in
1081 * the cache object, which then has to be re-inserted into the cache.
1082 * User object is retrieved inside a transaction to avoid replication issues.
1084 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
1086 osrfAppSession* session;
1088 jsonObject *param, *userObj, *newUserObj = NULL;
1090 userObj = jsonObjectGetKey( cacheObj, "userobj" );
1091 userId = oilsFMGetObjectId( userObj );
1093 session = osrfAppSessionClientInit( "open-ils.cstore" );
1094 osrfAppSessionConnect(session);
1096 reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
1097 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1101 osrfMessageFree(omsg);
1102 param = jsonNewNumberObject(userId);
1103 reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
1104 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1105 jsonObjectFree(param);
1108 newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
1109 osrfMessageFree(omsg);
1110 reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
1111 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1112 osrfMessageFree(omsg);
1116 osrfAppSessionFree(session); // calls disconnect internally
1120 // ws_ou and wsid are ephemeral and need to be manually propagated
1121 // oilsFMSetString dupe()'s internally, no need to clone the string
1122 oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
1123 oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
1125 jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
1126 jsonObjectSetKey(cacheObj, "userobj", newUserObj);
1130 osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
1135 Resets the auth login timeout
1136 @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
1138 static oilsEvent* _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
1139 if(!authToken) return NULL;
1141 oilsEvent* evt = NULL;
1144 osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
1145 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1146 jsonObject* cacheObj = osrfCacheGetObject( key );
1149 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
1150 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1155 _oilsAuthReloadUser(cacheObj);
1158 // Determine a new timeout value
1159 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
1161 // Extend the current endtime by a fixed amount
1162 time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
1163 int reset_interval = DEFAULT_RESET_INTERVAL;
1164 const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
1165 cacheObj, "reset_interval" );
1166 if( reset_interval_obj ) {
1167 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
1168 if( reset_interval <= 0 )
1169 reset_interval = DEFAULT_RESET_INTERVAL;
1172 time_t now = time( NULL );
1173 time_t new_endtime = now + reset_interval;
1174 if( new_endtime > endtime ) {
1175 // Keep the session alive a little longer
1176 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
1177 timeout = reset_interval;
1178 osrfCachePutObject( key, cacheObj, timeout );
1180 // The session isn't close to expiring, so don't reset anything.
1181 // Just report the time remaining.
1182 timeout = endtime - now;
1185 // Reapply the existing timeout from the current time
1186 timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
1187 osrfCachePutObject( key, cacheObj, timeout );
1190 jsonObject* payload = jsonNewNumberObject( (double) timeout );
1191 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
1192 jsonObjectFree(payload);
1193 jsonObjectFree(cacheObj);
1200 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
1201 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1202 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1203 double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
1204 oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
1205 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
1211 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
1212 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1213 bool returnFull = false;
1214 bool noTimeoutReset = false;
1216 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1218 if(ctx->params->size > 1) {
1219 // caller wants full cached object, with authtime, etc.
1220 const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
1221 if(rt && strcmp(rt, "0") != 0)
1224 if (ctx->params->size > 2) {
1225 // Avoid resetting the auth session timeout.
1226 const char* noReset =
1227 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 2));
1228 if (noReset && strcmp(noReset, "0") != 0)
1229 noTimeoutReset = true;
1233 jsonObject* cacheObj = NULL;
1234 oilsEvent* evt = NULL;
1238 // Reset the timeout to keep the session alive
1239 if (!noTimeoutReset)
1240 evt = _oilsAuthResetTimeout(authToken, 0);
1242 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
1243 osrfAppRespondComplete( ctx, oilsEventToJSON( evt )); // can't reset timeout
1247 // Retrieve the cached session object
1248 osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
1249 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1250 cacheObj = osrfCacheGetObject( key );
1252 // Return a copy of the cached user object
1254 osrfAppRespondComplete( ctx, cacheObj);
1256 osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
1257 jsonObjectFree(cacheObj);
1259 // Auth token is invalid or expired
1260 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1261 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
1262 oilsEventFree(evt2);
1270 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1271 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );