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. The auth "
102 "timeout is reset when this call is made "
103 "Returns the user object (password blanked) for the given login session "
104 "PARAMS( authToken )", 1, 0 );
106 osrfAppRegisterMethod(
108 "open-ils.auth.session.delete",
109 "oilsAuthSessionDelete",
110 "Destroys the given login session "
111 "PARAMS( authToken )", 1, 0 );
113 osrfAppRegisterMethod(
115 "open-ils.auth.session.reset_timeout",
116 "oilsAuthResetTimeout",
117 "Resets the login timeout for the given session "
118 "Returns an ILS Event with payload = session_timeout of session "
119 "if found, otherwise returns the NO_SESSION event"
120 "PARAMS( authToken )", 1, 0 );
122 if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
124 jsonObject* value_obj;
126 value_obj = osrf_settings_host_value_object(
127 "/apps/open-ils.auth/app_settings/auth_limits/seed" );
128 _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
129 jsonObjectFree(value_obj);
130 if( -1 == _oilsAuthSeedTimeout ) {
131 osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
132 _oilsAuthSeedTimeout = 30;
135 value_obj = osrf_settings_host_value_object(
136 "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
137 _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
138 jsonObjectFree(value_obj);
139 if( -1 == _oilsAuthBlockTimeout ) {
140 osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
141 _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
144 value_obj = osrf_settings_host_value_object(
145 "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
146 _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
147 jsonObjectFree(value_obj);
148 if( -1 == _oilsAuthBlockCount ) {
149 osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
150 _oilsAuthBlockCount = 10;
153 osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
154 "seed => %ld : block_timeout => %ld : block_count => %ld",
155 _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
162 @brief Dummy placeholder for initializing a server drone.
164 There is nothing to do, so do nothing.
166 int osrfAppChildInit() {
171 static char* oilsAuthGetSalt(int user_id) {
172 char* salt_str = NULL;
174 jsonObject* params = jsonParseFmt( // free
175 "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, "main");
177 jsonObject* salt_obj = // free
178 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
180 jsonObjectFree(params);
184 if (salt_obj->type != JSON_NULL) {
186 const char* salt_val = jsonObjectGetString(
187 jsonObjectGetKeyConst(salt_obj, "actor.get_salt"));
189 // caller expects a free-able string, could be NULL.
190 if (salt_val) { salt_str = strdup(salt_val); }
193 jsonObjectFree(salt_obj);
199 // ident is either a username or barcode
200 // Returns the init seed -> requires free();
201 static char* oilsAuthBuildInitCache(
202 int user_id, const char* ident, const char* ident_type, const char* nonce) {
204 char* cache_key = va_list_to_string(
205 "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce);
207 char* count_key = va_list_to_string(
208 "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX);
212 // user does not exist. Use a dummy seed
213 auth_seed = strdup("x");
215 auth_seed = oilsAuthGetSalt(user_id);
218 jsonObject* seed_object = jsonParseFmt(
219 "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
220 ident_type, ident, user_id, auth_seed);
222 jsonObject* count_object = osrfCacheGetObject(count_key);
224 count_object = jsonNewNumberObject((double) 0);
227 osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
230 // Only track login counts for existing users, since a
231 // login for a nonexistent user will never succeed anyway.
232 osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
235 osrfLogDebug(OSRF_LOG_MARK,
236 "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
240 jsonObjectFree(count_object);
241 jsonObjectFree(seed_object);
246 static int oilsAuthInitUsernameHandler(
247 osrfMethodContext* ctx, const char* username, const char* nonce) {
249 osrfLogInfo(OSRF_LOG_MARK,
250 "User logging in with username %s", username);
253 jsonObject* resp = NULL; // free
254 jsonObject* user_obj = oilsUtilsFetchUserByUsername(ctx, username); // free
256 if (user_obj && user_obj->type != JSON_NULL)
257 user_id = oilsFMGetObjectId(user_obj);
259 jsonObjectFree(user_obj); // NULL OK
261 char* seed = oilsAuthBuildInitCache(user_id, username, "username", nonce);
262 resp = jsonNewObject(seed);
265 osrfAppRespondComplete(ctx, resp);
266 jsonObjectFree(resp);
270 // open-ils.auth.authenticate.init.username
271 int oilsAuthInitUsername(osrfMethodContext* ctx) {
272 OSRF_METHOD_VERIFY_CONTEXT(ctx);
274 char* username = // free
275 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
277 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
279 if (!nonce) nonce = "";
280 if (!username) return -1;
282 int resp = oilsAuthInitUsernameHandler(ctx, username, nonce);
288 static int oilsAuthInitBarcodeHandler(
289 osrfMethodContext* ctx, const char* barcode, const char* nonce) {
291 osrfLogInfo(OSRF_LOG_MARK,
292 "User logging in with barcode %s", barcode);
295 jsonObject* resp = NULL; // free
296 jsonObject* user_obj = oilsUtilsFetchUserByBarcode(ctx, barcode); // free
298 if (user_obj && user_obj->type != JSON_NULL)
299 user_id = oilsFMGetObjectId(user_obj);
301 jsonObjectFree(user_obj); // NULL OK
303 char* seed = oilsAuthBuildInitCache(user_id, barcode, "barcode", nonce);
304 resp = jsonNewObject(seed);
307 osrfAppRespondComplete(ctx, resp);
308 jsonObjectFree(resp);
313 // open-ils.auth.authenticate.init.barcode
314 int oilsAuthInitBarcode(osrfMethodContext* ctx) {
315 OSRF_METHOD_VERIFY_CONTEXT(ctx);
317 char* barcode = // free
318 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
320 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
322 if (!nonce) nonce = "";
323 if (!barcode) return -1;
325 int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce);
331 // returns true if the provided identifier matches the barcode regex.
332 static int oilsAuthIdentIsBarcode(const char* identifier, int org_id) {
335 org_id = oilsUtilsGetRootOrgId();
337 char* bc_regex = oilsUtilsFetchOrgSetting(org_id, "opac.barcode_regex");
340 // if no regex is set, assume any identifier starting
341 // with a number is a barcode.
342 bc_regex = strdup("^\\d"); // dupe for later free'ing
346 int err_offset, match_ret;
348 pcre *compiled = pcre_compile(
349 bc_regex, 0, &err_str, &err_offset, NULL);
351 if (compiled == NULL) {
352 osrfLogError(OSRF_LOG_MARK,
353 "Could not compile '%s': %s", bc_regex, err_str);
359 pcre_extra *extra = pcre_study(compiled, 0, &err_str);
361 if(err_str != NULL) {
362 osrfLogError(OSRF_LOG_MARK,
363 "Could not study regex '%s': %s", bc_regex, err_str);
369 match_ret = pcre_exec(
370 compiled, extra, identifier, strlen(identifier), 0, 0, NULL, 0);
374 if (extra) pcre_free(extra);
376 if (match_ret >= 0) return 1; // regex matched
378 if (match_ret != PCRE_ERROR_NOMATCH)
379 osrfLogError(OSRF_LOG_MARK, "Unknown error processing barcode regex");
381 return 0; // regex did not match
386 @brief Implement the "init" method.
387 @param ctx The method context.
388 @return Zero if successful, or -1 if not.
392 - nonce : optional login seed (string) provided by the caller which
393 is added to the auth init cache to differentiate between logins
394 using the same username and thus avoiding cache collisions for
395 near-simultaneous logins.
397 Return to client: Intermediate authentication seed.
399 int oilsAuthInit(osrfMethodContext* ctx) {
400 OSRF_METHOD_VERIFY_CONTEXT(ctx);
403 char* identifier = // free
404 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
406 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
408 if (!nonce) nonce = "";
409 if (!identifier) return -1; // we need an identifier
411 if (oilsAuthIdentIsBarcode(identifier, 0)) {
412 resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce);
414 resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce);
422 Returns 1 if the password provided matches the user's real password
427 @brief Verify the password received from the client.
428 @param ctx The method context.
429 @param userObj An object from the database, representing the user.
430 @param password An obfuscated password received from the client.
431 @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
433 (None of the so-called "passwords" used here are in plaintext. All have been passed
434 through at least one layer of hashing to obfuscate them.)
436 Take the password from the user object. Append it to the username seed from memcache,
437 as stored previously by a call to the init method. Take an md5 hash of the result.
438 Then compare this hash to the password received from the client.
440 In order for the two to match, other than by dumb luck, the client had to construct
441 the password it passed in the same way. That means it neded to know not only the
442 original password (either hashed or plaintext), but also the seed. The latter requirement
443 means that the client process needs either to be the same process that called the init
444 method or to receive the seed from the process that did so.
446 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id,
447 const char* identifier, const char* password, const char* nonce) {
451 // We won't be needing the seed again, remove it
452 osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
454 // Ask the DB to verify the user's password.
455 // Here, the password is md5(md5(password) + salt)
457 jsonObject* params = jsonParseFmt( // free
458 "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}",
461 jsonObject* verify_obj = // free
462 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
464 jsonObjectFree(params);
467 verified = oilsUtilsIsDBTrue(
469 jsonObjectGetKeyConst(
470 verify_obj, "actor.verify_passwd")));
472 jsonObjectFree(verify_obj);
475 char* countkey = va_list_to_string("%s%s%s",
476 OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
477 jsonObject* countobject = osrfCacheGetObject( countkey );
479 long failcount = (long) jsonObjectGetNumber( countobject );
480 if(failcount >= _oilsAuthBlockCount) {
482 osrfLogInfo(OSRF_LOG_MARK,
483 "oilsAuth found too many recent failures for '%s' : %i, "
484 "forcing failure state.", identifier, failcount);
489 jsonObjectSetNumber( countobject, failcount );
490 osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
491 jsonObjectFree(countobject);
499 * Returns true if the provided password is correct.
500 * Turn the password into the nested md5 hash required of migrated
501 * passwords, then check the password in the DB.
503 static int oilsAuthLoginCheckPassword(int user_id, const char* password) {
505 growing_buffer* gb = buffer_init(33); // free me 1
506 char* salt = oilsAuthGetSalt(user_id); // free me 2
507 char* passhash = md5sum(password); // free me 3
509 buffer_add(gb, salt); // gb strdup's internally
510 buffer_add(gb, passhash);
512 free(salt); // free 2
513 free(passhash); // free 3
515 // salt + md5(password)
516 passhash = buffer_release(gb); // free 1 ; free me 4
517 char* finalpass = md5sum(passhash); // free me 5
519 free(passhash); // free 4
521 jsonObject *arr = jsonNewObjectType(JSON_ARRAY);
522 jsonObjectPush(arr, jsonNewObject("actor.verify_passwd"));
523 jsonObjectPush(arr, jsonNewNumberObject((long) user_id));
524 jsonObjectPush(arr, jsonNewObject("main"));
525 jsonObjectPush(arr, jsonNewObject(finalpass));
526 jsonObject *params = jsonNewObjectType(JSON_HASH); // free me 6
527 jsonObjectSetKey(params, "from", arr);
529 free(finalpass); // free 5
531 jsonObject* verify_obj = // free
532 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
534 jsonObjectFree(params); // free 6
536 if (!verify_obj) return 0; // error
538 int verified = oilsUtilsIsDBTrue(
540 jsonObjectGetKeyConst(verify_obj, "actor.verify_passwd")
544 jsonObjectFree(verify_obj);
549 static int oilsAuthLoginVerifyPassword(const osrfMethodContext* ctx,
550 int user_id, const char* username, const char* password) {
552 // build the cache key
553 growing_buffer* gb = buffer_init(64); // free me
554 buffer_add(gb, OILS_AUTH_CACHE_PRFX);
555 buffer_add(gb, username);
556 buffer_add(gb, OILS_AUTH_COUNT_SFFX);
557 char* countkey = buffer_release(gb); // free me
559 jsonObject* countobject = osrfCacheGetObject(countkey); // free me
563 failcount = (long) jsonObjectGetNumber(countobject);
565 if (failcount >= _oilsAuthBlockCount) {
566 // User is blocked. Don't waste any more CPU cycles on them.
568 osrfLogInfo(OSRF_LOG_MARK,
569 "oilsAuth found too many recent failures for '%s' : %i, "
570 "forcing failure state.", username, failcount);
572 jsonObjectFree(countobject);
578 int verified = oilsAuthLoginCheckPassword(user_id, password);
580 if (!verified) { // login failed. increment failure counter.
584 // append to existing counter
585 jsonObjectSetNumber(countobject, failcount);
588 // first failure, create a new counter
589 countobject = jsonNewNumberObject((double) failcount);
592 osrfCachePutObject(countkey, countobject, _oilsAuthBlockTimeout);
595 jsonObjectFree(countobject); // NULL OK
603 Adds the authentication token to the user cache. The timeout for the
604 auth token is based on the type of login as well as (if type=='opac')
606 Returns the event that should be returned to the user.
609 static oilsEvent* oilsAuthHandleLoginOK( osrfMethodContext* ctx, jsonObject* userObj, const char* uname,
610 const char* type, int orgloc, const char* workstation ) {
612 oilsEvent* response = NULL;
614 jsonObject* params = jsonNewObject(NULL);
615 jsonObjectSetKey(params, "user_id",
616 jsonNewNumberObject(oilsFMGetObjectId(userObj)));
617 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
618 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
620 jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
622 jsonObject* authEvt = oilsUtilsQuickReqCtx(
624 "open-ils.auth_internal",
625 "open-ils.auth_internal.session.create", params);
626 jsonObjectFree(params);
630 response = oilsNewEvent2(
632 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
633 jsonObjectGetKey(authEvt, "payload") // cloned within Event
636 osrfLogActivity(OSRF_LOG_MARK,
637 "successful login: username=%s, authtoken=%s, workstation=%s",
640 jsonObjectGetKeyConst(
641 jsonObjectGetKeyConst(authEvt, "payload"),
645 workstation ? workstation : ""
648 jsonObjectFree(authEvt);
651 osrfLogError(OSRF_LOG_MARK,
652 "Error caching auth session in open-ils.auth_internal");
660 @brief Implement the "complete" method.
661 @param ctx The method context.
662 @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
663 client to indicate completion; a positive integer if successful but no such STATUS
664 message has been sent.
667 - a hash with some combination of the following elements:
670 - "password" (hashed with the cached seed; not plaintext)
674 - "agent" (what software/interface/3rd-party is making the request)
675 - "nonce" optional login seed to differentiate logins using the same username.
677 The password is required. Either a username or a barcode must also be present.
679 Return to client: Intermediate authentication seed.
681 Validate the password, using the username if available, or the barcode if not. The
682 user must be active, and not barred from logging on. The barcode, if used for
683 authentication, must be active as well. The workstation, if specified, must be valid.
685 Upon deciding whether to allow the logon, return a corresponding event to the client.
687 int oilsAuthComplete( osrfMethodContext* ctx ) {
688 OSRF_METHOD_VERIFY_CONTEXT(ctx);
690 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
692 const char* uname = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
693 const char* identifier = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
694 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
695 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
696 int orgloc = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
697 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
698 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
699 const char* ewho = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
700 const char* nonce = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
702 const char* ws = (workstation) ? workstation : "";
703 if (!nonce) nonce = "";
705 // we no longer care how the identifier reaches us,
706 // as long as we have one.
710 } else if (barcode) {
711 identifier = barcode;
716 return osrfAppRequestRespondException(ctx->session, ctx->request,
717 "username/barcode and password required for method: %s",
721 osrfLogInfo(OSRF_LOG_MARK,
722 "Patron completing authentication with identifer %s", identifier);
724 /* Use __FILE__, harmless_line_number for creating
725 * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
726 * giving away information about why an authentication attempt failed.
728 int harmless_line_number = __LINE__;
731 type = OILS_AUTH_STAFF;
733 oilsEvent* response = NULL; // free
734 jsonObject* userObj = NULL; // free
736 char* cache_key = va_list_to_string(
737 "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
738 jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
741 return osrfAppRequestRespondException(ctx->session,
742 ctx->request, "No authentication seed found. "
743 "open-ils.auth.authenticate.init must be called first "
744 " (check that memcached is running and can be connected to) "
748 int user_id = jsonObjectGetNumber(
749 jsonObjectGetKeyConst(cacheObj, "user_id"));
752 // User was not found during init. Clean up and exit early.
753 response = oilsNewEvent(
754 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
755 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
756 oilsEventFree(response); // frees event JSON
757 osrfCacheRemove(cache_key);
758 jsonObjectFree(cacheObj);
762 jsonObject* param = jsonNewNumberObject(user_id); // free
763 userObj = oilsUtilsCStoreReqCtx(
764 ctx, "open-ils.cstore.direct.actor.user.retrieve", param);
765 jsonObjectFree(param);
767 char* freeable_uname = NULL;
769 uname = freeable_uname = oilsFMGetString(userObj, "usrname");
772 // See if the user is allowed to login.
774 jsonObject* params = jsonNewObject(NULL);
775 jsonObjectSetKey(params, "user_id",
776 jsonNewNumberObject(oilsFMGetObjectId(userObj)));
777 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
778 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
779 if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
781 jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
783 "open-ils.auth_internal",
784 "open-ils.auth_internal.user.validate", params);
785 jsonObjectFree(params);
788 // Something went seriously wrong. Get outta here before
789 // we start segfaulting.
790 jsonObjectFree(userObj);
791 if(freeable_uname) free(freeable_uname);
795 const char* authEvtCode =
796 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
798 if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
799 // Received the generic login failure event.
801 osrfLogInfo(OSRF_LOG_MARK,
802 "failed login: username=%s, barcode=%s, workstation=%s",
803 uname, (barcode ? barcode : "(none)"), ws);
805 response = oilsNewEvent(
806 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
812 // User exists and is not barred, etc. Test the password.
814 passOK = oilsAuthVerifyPassword(
815 ctx, user_id, identifier, password, nonce);
818 // Password check failed. Return generic login failure.
820 response = oilsNewEvent(
821 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
823 osrfLogInfo(OSRF_LOG_MARK,
824 "failed login: username=%s, barcode=%s, workstation=%s",
825 uname, (barcode ? barcode : "(none)"), ws );
830 // Below here, we know the password check succeeded if no response
831 // object is present.
834 !strcmp(authEvtCode, "PATRON_INACTIVE") ||
835 !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
836 // Patron and/or card is inactive but the correct password
837 // was provided. Alert the caller to the inactive-ness.
838 response = oilsNewEvent2(
839 OSRF_LOG_MARK, authEvtCode,
840 jsonObjectGetKey(authEvt, "payload") // cloned within Event
844 if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
845 // Validate API returned an unexpected non-success event.
846 // To be safe, treat this as a generic login failure.
848 response = oilsNewEvent(
849 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
853 // password OK and no other events have prevented login completion.
855 char* ewhat = "login";
857 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
858 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
862 response = oilsAuthHandleLoginOK(
863 ctx, userObj, uname, type, orgloc, workstation);
866 oilsUtilsTrackUserActivity(
868 oilsFMGetObjectId(userObj),
870 osrfAppSessionGetIngress()
875 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
878 oilsEventFree(response);
879 jsonObjectFree(userObj);
880 jsonObjectFree(authEvt);
881 jsonObjectFree(cacheObj);
883 free(freeable_uname);
889 int oilsAuthLogin(osrfMethodContext* ctx) {
890 OSRF_METHOD_VERIFY_CONTEXT(ctx);
892 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
894 const char* username = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
895 const char* identifier = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
896 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
897 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
898 int orgloc = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
899 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
900 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
901 const char* ewho = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
903 const char* ws = (workstation) ? workstation : "";
904 if (!type) type = OILS_AUTH_STAFF;
906 jsonObject* userObj = NULL; // free me
907 oilsEvent* response = NULL; // free me
909 /* Use __FILE__, harmless_line_number for creating
910 * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
911 * giving away information about why an authentication attempt failed.
913 int harmless_line_number = __LINE__;
915 // translate a generic identifier into a username or barcode if necessary.
916 if (identifier && !username && !barcode) {
917 if (oilsAuthIdentIsBarcode(identifier, orgloc)) {
918 barcode = identifier;
920 username = identifier;
925 barcode = NULL; // avoid superfluous identifiers
926 userObj = oilsUtilsFetchUserByUsername(ctx, username);
928 } else if (barcode) {
929 userObj = oilsUtilsFetchUserByBarcode(ctx, barcode);
933 return osrfAppRequestRespondException(ctx->session, ctx->request,
934 "username/barcode and password required for method: %s",
938 if (!userObj) { // user not found.
939 response = oilsNewEvent(
940 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
941 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
942 oilsEventFree(response); // frees event JSON
946 long user_id = oilsFMGetObjectId(userObj);
948 // username is freed when userObj is freed.
949 // From here we can use the username as the generic identifier
950 // since it's guaranteed to have a value.
951 if (!username) username = oilsFMGetStringConst(userObj, "usrname");
953 // See if the user is allowed to login.
954 jsonObject* params = jsonNewObject(NULL);
955 jsonObjectSetKey(params, "user_id", jsonNewNumberObject(user_id));
956 jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
957 jsonObjectSetKey(params, "login_type", jsonNewObject(type));
958 if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
960 jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
962 "open-ils.auth_internal",
963 "open-ils.auth_internal.user.validate", params);
964 jsonObjectFree(params);
966 if (!authEvt) { // unknown error
967 jsonObjectFree(userObj);
971 const char* authEvtCode =
972 jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
974 if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
975 // Received the generic login failure event.
977 osrfLogInfo(OSRF_LOG_MARK,
978 "failed login: username=%s, barcode=%s, workstation=%s",
979 username, (barcode ? barcode : "(none)"), ws);
981 response = oilsNewEvent(
982 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
985 if (!response && // user exists and is not barred, etc.
986 !oilsAuthLoginVerifyPassword(ctx, user_id, username, password)) {
987 // User provided the wrong password or is blocked from too
988 // many previous login failures.
990 response = oilsNewEvent(
991 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
993 osrfLogInfo(OSRF_LOG_MARK,
994 "failed login: username=%s, barcode=%s, workstation=%s",
995 username, (barcode ? barcode : "(none)"), ws );
998 // Below here, we know the password check succeeded if no response
999 // object is present.
1002 !strcmp(authEvtCode, "PATRON_INACTIVE") ||
1003 !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
1004 // Patron and/or card is inactive but the correct password
1005 // was provided. Alert the caller to the inactive-ness.
1006 response = oilsNewEvent2(
1007 OSRF_LOG_MARK, authEvtCode,
1008 jsonObjectGetKey(authEvt, "payload") // cloned within Event
1012 if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
1013 // Validate API returned an unexpected non-success event.
1014 // To be safe, treat this as a generic login failure.
1016 response = oilsNewEvent(
1017 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
1021 // password OK and no other events have prevented login completion.
1023 char* ewhat = "login";
1025 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
1026 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
1030 response = oilsAuthHandleLoginOK(
1031 ctx, userObj, username, type, orgloc, workstation);
1034 oilsUtilsTrackUserActivity(
1036 oilsFMGetObjectId(userObj),
1038 osrfAppSessionGetIngress()
1043 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
1046 oilsEventFree(response);
1047 jsonObjectFree(userObj);
1048 jsonObjectFree(authEvt);
1055 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
1056 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1058 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
1059 jsonObject* resp = NULL;
1062 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
1063 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
1064 osrfCacheRemove(key);
1065 resp = jsonNewObject(authToken); /**/
1069 osrfAppRespondComplete( ctx, resp );
1070 jsonObjectFree(resp);
1075 * Fetches the user object from the database and updates the user object in
1076 * the cache object, which then has to be re-inserted into the cache.
1077 * User object is retrieved inside a transaction to avoid replication issues.
1079 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
1081 osrfAppSession* session;
1083 jsonObject *param, *userObj, *newUserObj = NULL;
1085 userObj = jsonObjectGetKey( cacheObj, "userobj" );
1086 userId = oilsFMGetObjectId( userObj );
1088 session = osrfAppSessionClientInit( "open-ils.cstore" );
1089 osrfAppSessionConnect(session);
1091 reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
1092 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1096 osrfMessageFree(omsg);
1097 param = jsonNewNumberObject(userId);
1098 reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
1099 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1100 jsonObjectFree(param);
1103 newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
1104 osrfMessageFree(omsg);
1105 reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
1106 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1107 osrfMessageFree(omsg);
1111 osrfAppSessionFree(session); // calls disconnect internally
1115 // ws_ou and wsid are ephemeral and need to be manually propagated
1116 // oilsFMSetString dupe()'s internally, no need to clone the string
1117 oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
1118 oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
1120 jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
1121 jsonObjectSetKey(cacheObj, "userobj", newUserObj);
1125 osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
1130 Resets the auth login timeout
1131 @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
1133 static oilsEvent* _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
1134 if(!authToken) return NULL;
1136 oilsEvent* evt = NULL;
1139 osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
1140 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1141 jsonObject* cacheObj = osrfCacheGetObject( key );
1144 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
1145 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1150 _oilsAuthReloadUser(cacheObj);
1153 // Determine a new timeout value
1154 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
1156 // Extend the current endtime by a fixed amount
1157 time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
1158 int reset_interval = DEFAULT_RESET_INTERVAL;
1159 const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
1160 cacheObj, "reset_interval" );
1161 if( reset_interval_obj ) {
1162 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
1163 if( reset_interval <= 0 )
1164 reset_interval = DEFAULT_RESET_INTERVAL;
1167 time_t now = time( NULL );
1168 time_t new_endtime = now + reset_interval;
1169 if( new_endtime > endtime ) {
1170 // Keep the session alive a little longer
1171 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
1172 timeout = reset_interval;
1173 osrfCachePutObject( key, cacheObj, timeout );
1175 // The session isn't close to expiring, so don't reset anything.
1176 // Just report the time remaining.
1177 timeout = endtime - now;
1180 // Reapply the existing timeout from the current time
1181 timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
1182 osrfCachePutObject( key, cacheObj, timeout );
1185 jsonObject* payload = jsonNewNumberObject( (double) timeout );
1186 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
1187 jsonObjectFree(payload);
1188 jsonObjectFree(cacheObj);
1195 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
1196 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1197 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1198 double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
1199 oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
1200 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
1206 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
1207 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1208 bool returnFull = false;
1210 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1212 if(ctx->params->size > 1) {
1213 // caller wants full cached object, with authtime, etc.
1214 const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
1215 if(rt && strcmp(rt, "0") != 0)
1219 jsonObject* cacheObj = NULL;
1220 oilsEvent* evt = NULL;
1224 // Reset the timeout to keep the session alive
1225 evt = _oilsAuthResetTimeout(authToken, 0);
1227 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
1228 osrfAppRespondComplete( ctx, oilsEventToJSON( evt )); // can't reset timeout
1232 // Retrieve the cached session object
1233 osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
1234 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1235 cacheObj = osrfCacheGetObject( key );
1237 // Return a copy of the cached user object
1239 osrfAppRespondComplete( ctx, cacheObj);
1241 osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
1242 jsonObjectFree(cacheObj);
1244 // Auth token is invalid or expired
1245 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1246 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
1247 oilsEventFree(evt2);
1255 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1256 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );