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 _oilsAuthOPACTimeout = 0;
28 static long _oilsAuthStaffTimeout = 0;
29 static long _oilsAuthOverrideTimeout = 0;
30 static long _oilsAuthPersistTimeout = 0;
31 static long _oilsAuthSeedTimeout = 0;
32 static long _oilsAuthBlockTimeout = 0;
33 static long _oilsAuthBlockCount = 0;
37 @brief Initialize the application by registering functions for method calls.
38 @return Zero in all cases.
40 int osrfAppInitialize() {
42 osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
44 /* load and parse the IDL */
45 if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
47 osrfAppRegisterMethod(
49 "open-ils.auth.authenticate.init",
51 "Start the authentication process and returns the intermediate authentication seed"
52 " PARAMS( username )", 1, 0 );
54 osrfAppRegisterMethod(
56 "open-ils.auth.authenticate.init.barcode",
57 "oilsAuthInitBarcode",
58 "Start the authentication process using a patron barcode and return "
59 "the intermediate authentication seed. PARAMS(barcode)", 1, 0);
61 osrfAppRegisterMethod(
63 "open-ils.auth.authenticate.init.username",
64 "oilsAuthInitUsername",
65 "Start the authentication process using a patron username and return "
66 "the intermediate authentication seed. PARAMS(username)", 1, 0);
68 osrfAppRegisterMethod(
70 "open-ils.auth.authenticate.complete",
72 "Completes the authentication process. Returns an object like so: "
73 "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
74 "token and authtime is the number of seconds the session will be active"
75 "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
76 "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
77 "org_id is the location at which the login should be considered "
78 "active for login timeout purposes", 1, 0 );
80 osrfAppRegisterMethod(
82 "open-ils.auth.authenticate.verify",
84 "Verifies the user provided a valid username and password."
85 "Params and are the same as open-ils.auth.authenticate.complete."
86 "Returns SUCCESS event on success, failure event on failure", 1, 0);
89 osrfAppRegisterMethod(
91 "open-ils.auth.session.retrieve",
92 "oilsAuthSessionRetrieve",
93 "Pass in the auth token and this retrieves the user object. The auth "
94 "timeout is reset when this call is made "
95 "Returns the user object (password blanked) for the given login session "
96 "PARAMS( authToken )", 1, 0 );
98 osrfAppRegisterMethod(
100 "open-ils.auth.session.delete",
101 "oilsAuthSessionDelete",
102 "Destroys the given login session "
103 "PARAMS( authToken )", 1, 0 );
105 osrfAppRegisterMethod(
107 "open-ils.auth.session.reset_timeout",
108 "oilsAuthResetTimeout",
109 "Resets the login timeout for the given session "
110 "Returns an ILS Event with payload = session_timeout of session "
111 "if found, otherwise returns the NO_SESSION event"
112 "PARAMS( authToken )", 1, 0 );
114 if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
116 jsonObject* value_obj;
118 value_obj = osrf_settings_host_value_object(
119 "/apps/open-ils.auth/app_settings/auth_limits/seed" );
120 _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
121 jsonObjectFree(value_obj);
122 if( -1 == _oilsAuthSeedTimeout ) {
123 osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
124 _oilsAuthSeedTimeout = 30;
127 value_obj = osrf_settings_host_value_object(
128 "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
129 _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
130 jsonObjectFree(value_obj);
131 if( -1 == _oilsAuthBlockTimeout ) {
132 osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
133 _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
136 value_obj = osrf_settings_host_value_object(
137 "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
138 _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
139 jsonObjectFree(value_obj);
140 if( -1 == _oilsAuthBlockCount ) {
141 osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
142 _oilsAuthBlockCount = 10;
145 osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
146 "seed => %ld : block_timeout => %ld : block_count => %ld",
147 _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
154 @brief Dummy placeholder for initializing a server drone.
156 There is nothing to do, so do nothing.
158 int osrfAppChildInit() {
163 static char* oilsAuthGetSalt(int user_id) {
164 char* salt_str = NULL;
166 jsonObject* params = jsonParseFmt( // free
167 "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, "main");
169 jsonObject* salt_obj = // free
170 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
172 jsonObjectFree(params);
176 if (salt_obj->type != JSON_NULL) {
178 const char* salt_val = jsonObjectGetString(
179 jsonObjectGetKeyConst(salt_obj, "actor.get_salt"));
181 // caller expects a free-able string, could be NULL.
182 if (salt_val) { salt_str = strdup(salt_val); }
185 jsonObjectFree(salt_obj);
191 // ident is either a username or barcode
192 // Returns the init seed -> requires free();
193 static char* oilsAuthBuildInitCache(
194 int user_id, const char* ident, const char* ident_type, const char* nonce) {
196 char* cache_key = va_list_to_string(
197 "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce);
199 char* count_key = va_list_to_string(
200 "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX);
202 char* auth_seed = oilsAuthGetSalt(user_id);
204 jsonObject* seed_object = jsonParseFmt(
205 "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
206 ident_type, ident, user_id, auth_seed);
208 jsonObject* count_object = osrfCacheGetObject(count_key);
210 count_object = jsonNewNumberObject((double) 0);
213 osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
214 osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
216 osrfLogDebug(OSRF_LOG_MARK,
217 "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
221 jsonObjectFree(count_object);
222 jsonObjectFree(seed_object);
227 static int oilsAuthInitUsernameHandler(
228 osrfMethodContext* ctx, const char* username, const char* nonce) {
230 osrfLogInfo(OSRF_LOG_MARK,
231 "User logging in with username %s", username);
233 jsonObject* resp = NULL; // free
234 jsonObject* user_obj = oilsUtilsFetchUserByUsername(username); // free
238 if (JSON_NULL == user_obj->type) { // user not found
239 resp = jsonNewObject("x");
242 char* seed = oilsAuthBuildInitCache(
243 oilsFMGetObjectId(user_obj), username, "username", nonce);
244 resp = jsonNewObject(seed);
248 jsonObjectFree(user_obj);
251 resp = jsonNewObject("x");
254 osrfAppRespondComplete(ctx, resp);
255 jsonObjectFree(resp);
259 // open-ils.auth.authenticate.init.username
260 int oilsAuthInitUsername(osrfMethodContext* ctx) {
261 OSRF_METHOD_VERIFY_CONTEXT(ctx);
263 char* username = // free
264 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
266 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
268 if (!nonce) nonce = "";
269 if (!username) return -1;
271 int resp = oilsAuthInitUsernameHandler(ctx, username, nonce);
277 static int oilsAuthInitBarcodeHandler(
278 osrfMethodContext* ctx, const char* barcode, const char* nonce) {
280 osrfLogInfo(OSRF_LOG_MARK,
281 "User logging in with barcode %s", barcode);
283 jsonObject* resp = NULL; // free
284 jsonObject* user_obj = oilsUtilsFetchUserByBarcode(barcode); // free
287 if (JSON_NULL == user_obj->type) { // not found
288 resp = jsonNewObject("x");
290 char* seed = oilsAuthBuildInitCache(
291 oilsFMGetObjectId(user_obj), barcode, "barcode", nonce);
292 resp = jsonNewObject(seed);
296 jsonObjectFree(user_obj);
298 resp = jsonNewObject("x");
301 osrfAppRespondComplete(ctx, resp);
302 jsonObjectFree(resp);
307 // open-ils.auth.authenticate.init.barcode
308 int oilsAuthInitBarcode(osrfMethodContext* ctx) {
309 OSRF_METHOD_VERIFY_CONTEXT(ctx);
311 char* barcode = // free
312 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
314 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
316 if (!nonce) nonce = "";
317 if (!barcode) return -1;
319 int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce);
325 // returns true if the provided identifier matches the barcode regex.
326 static int oilsAuthIdentIsBarcode(const char* identifier) {
328 // before we can fetch the barcode regex unit setting,
329 // first determine what the root org unit ID is.
330 // TODO: add an org_unit param to the .init API for future use?
332 jsonObject *params = jsonParse("{\"parent_ou\":null}");
333 jsonObject *org_unit_id = oilsUtilsCStoreReq(
334 "open-ils.cstore.direct.actor.org_unit.id_list", params);
335 jsonObjectFree(params);
337 char* bc_regex = oilsUtilsFetchOrgSetting(
338 (int) jsonObjectGetNumber(org_unit_id), "opac.barcode_regex");
339 jsonObjectFree(org_unit_id);
342 // if no regex is set, assume any identifier starting
343 // with a number is a barcode.
344 bc_regex = strdup("^\\d"); // dupe for later free'ing
348 int err_offset, match_ret;
350 pcre *compiled = pcre_compile(
351 bc_regex, 0, &err_str, &err_offset, NULL);
353 if (compiled == NULL) {
354 osrfLogError(OSRF_LOG_MARK,
355 "Could not compile '%s': %s", bc_regex, err_str);
361 pcre_extra *extra = pcre_study(compiled, 0, &err_str);
363 if(err_str != NULL) {
364 osrfLogError(OSRF_LOG_MARK,
365 "Could not study regex '%s': %s", bc_regex, err_str);
371 match_ret = pcre_exec(
372 compiled, extra, identifier, strlen(identifier), 0, 0, NULL, 0);
376 if (extra) pcre_free(extra);
378 if (match_ret >= 0) return 1; // regex matched
380 if (match_ret != PCRE_ERROR_NOMATCH)
381 osrfLogError(OSRF_LOG_MARK, "Unknown error processing barcode regex");
383 return 0; // regex did not match
388 @brief Implement the "init" method.
389 @param ctx The method context.
390 @return Zero if successful, or -1 if not.
394 - nonce : optional login seed (string) provided by the caller which
395 is added to the auth init cache to differentiate between logins
396 using the same username and thus avoiding cache collisions for
397 near-simultaneous logins.
399 Return to client: Intermediate authentication seed.
401 int oilsAuthInit(osrfMethodContext* ctx) {
402 OSRF_METHOD_VERIFY_CONTEXT(ctx);
405 char* identifier = // free
406 jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
408 jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
410 if (!nonce) nonce = "";
411 if (!identifier) return -1; // we need an identifier
413 if (oilsAuthIdentIsBarcode(identifier)) {
414 resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce);
416 resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce);
424 Verifies that the user has permission to login with the
425 given type. If the permission fails, an oilsEvent is returned
427 @return -1 if the permission check failed, 0 if the permission
430 static int oilsAuthCheckLoginPerm(
431 osrfMethodContext* ctx, const jsonObject* userObj, const char* type ) {
433 if(!(userObj && type)) return -1;
434 oilsEvent* perm = NULL;
436 if(!strcasecmp(type, OILS_AUTH_OPAC)) {
437 char* permissions[] = { "OPAC_LOGIN" };
438 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
440 } else if(!strcasecmp(type, OILS_AUTH_STAFF)) {
441 char* permissions[] = { "STAFF_LOGIN" };
442 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
444 } else if(!strcasecmp(type, OILS_AUTH_TEMP)) {
445 char* permissions[] = { "STAFF_LOGIN" };
446 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
447 } else if(!strcasecmp(type, OILS_AUTH_PERSIST)) {
448 char* permissions[] = { "PERSISTENT_LOGIN" };
449 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
453 osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
462 Returns 1 if the password provided matches the user's real password
467 @brief Verify the password received from the client.
468 @param ctx The method context.
469 @param userObj An object from the database, representing the user.
470 @param password An obfuscated password received from the client.
471 @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
473 (None of the so-called "passwords" used here are in plaintext. All have been passed
474 through at least one layer of hashing to obfuscate them.)
476 Take the password from the user object. Append it to the username seed from memcache,
477 as stored previously by a call to the init method. Take an md5 hash of the result.
478 Then compare this hash to the password received from the client.
480 In order for the two to match, other than by dumb luck, the client had to construct
481 the password it passed in the same way. That means it neded to know not only the
482 original password (either hashed or plaintext), but also the seed. The latter requirement
483 means that the client process needs either to be the same process that called the init
484 method or to receive the seed from the process that did so.
486 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id,
487 const char* identifier, const char* password, const char* nonce) {
491 // We won't be needing the seed again, remove it
492 osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
494 // Ask the DB to verify the user's password.
495 // Here, the password is md5(md5(password) + salt)
497 jsonObject* params = jsonParseFmt( // free
498 "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}",
501 jsonObject* verify_obj = // free
502 oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
504 jsonObjectFree(params);
507 verified = oilsUtilsIsDBTrue(
509 jsonObjectGetKeyConst(
510 verify_obj, "actor.verify_passwd")));
512 jsonObjectFree(verify_obj);
515 char* countkey = va_list_to_string("%s%s%s",
516 OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
517 jsonObject* countobject = osrfCacheGetObject( countkey );
519 long failcount = (long) jsonObjectGetNumber( countobject );
520 if(failcount >= _oilsAuthBlockCount) {
522 osrfLogInfo(OSRF_LOG_MARK,
523 "oilsAuth found too many recent failures for '%s' : %i, "
524 "forcing failure state.", identifier, failcount);
529 jsonObjectSetNumber( countobject, failcount );
530 osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
531 jsonObjectFree(countobject);
539 @brief Determine the login timeout.
540 @param userObj Pointer to an object describing the user.
541 @param type Pointer to one of four possible character strings identifying the login type.
542 @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
543 @return The length of the timeout, in seconds.
545 The default timeout value comes from the configuration file, and depends on the
548 The default may be overridden by a corresponding org unit setting. The @a orgloc
549 parameter says what org unit to use for the lookup. If @a orgloc <= 0, or if the
550 lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
551 instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).
553 Whether defined in the config file or in an org unit setting, a timeout value may be
554 expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
555 space) or as an interval string to be translated into seconds by PostgreSQL.
557 static long oilsAuthGetTimeout( const jsonObject* userObj, const char* type, int orgloc ) {
559 if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
561 jsonObject* value_obj;
563 value_obj = osrf_settings_host_value_object(
564 "/apps/open-ils.auth/app_settings/default_timeout/opac" );
565 _oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
566 jsonObjectFree(value_obj);
567 if( -1 == _oilsAuthOPACTimeout ) {
568 osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
569 _oilsAuthOPACTimeout = 0;
572 value_obj = osrf_settings_host_value_object(
573 "/apps/open-ils.auth/app_settings/default_timeout/staff" );
574 _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
575 jsonObjectFree(value_obj);
576 if( -1 == _oilsAuthStaffTimeout ) {
577 osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
578 _oilsAuthStaffTimeout = 0;
581 value_obj = osrf_settings_host_value_object(
582 "/apps/open-ils.auth/app_settings/default_timeout/temp" );
583 _oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
584 jsonObjectFree(value_obj);
585 if( -1 == _oilsAuthOverrideTimeout ) {
586 osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
587 _oilsAuthOverrideTimeout = 0;
590 value_obj = osrf_settings_host_value_object(
591 "/apps/open-ils.auth/app_settings/default_timeout/persist" );
592 _oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
593 jsonObjectFree(value_obj);
594 if( -1 == _oilsAuthPersistTimeout ) {
595 osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
596 _oilsAuthPersistTimeout = 0;
599 osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
600 "opac => %ld : staff => %ld : temp => %ld : persist => %ld",
601 _oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
602 _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
605 int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
609 char* setting = NULL;
610 long default_timeout = 0;
612 if( !strcmp( type, OILS_AUTH_OPAC )) {
613 setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
614 default_timeout = _oilsAuthOPACTimeout;
615 } else if( !strcmp( type, OILS_AUTH_STAFF )) {
616 setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
617 default_timeout = _oilsAuthStaffTimeout;
618 } else if( !strcmp( type, OILS_AUTH_TEMP )) {
619 setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
620 default_timeout = _oilsAuthOverrideTimeout;
621 } else if( !strcmp( type, OILS_AUTH_PERSIST )) {
622 setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
623 default_timeout = _oilsAuthPersistTimeout;
626 // Get the org unit setting, if there is one.
627 char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
629 if( orgloc != home_ou ) {
630 osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
631 "trying home_ou %d", orgloc, home_ou );
632 timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
637 return default_timeout; // No override from org unit setting
639 // Translate the org unit setting to a number
642 osrfLogWarning( OSRF_LOG_MARK,
643 "Timeout org unit setting is an empty string for %s login; using default",
647 // Treat timeout string as an interval, and convert it to seconds
648 t = oilsUtilsIntervalToSeconds( timeout );
650 // Unable to convert; possibly an invalid interval string
651 osrfLogError( OSRF_LOG_MARK,
652 "Unable to convert timeout interval \"%s\" for %s login; using default",
663 Adds the authentication token to the user cache. The timeout for the
664 auth token is based on the type of login as well as (if type=='opac')
666 Returns the event that should be returned to the user.
669 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
670 const char* type, int orgloc, const char* workstation ) {
675 char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "ws_ou"));
676 if(wsorg) { /* if there is a workstation, use it for the timeout */
677 osrfLogDebug( OSRF_LOG_MARK,
678 "Auth session trying workstation id %d for auth timeout", atoi(wsorg));
679 timeout = oilsAuthGetTimeout( userObj, type, atoi(wsorg) );
682 osrfLogDebug( OSRF_LOG_MARK,
683 "Auth session trying org from param [%d] for auth timeout", orgloc );
684 timeout = oilsAuthGetTimeout( userObj, type, orgloc );
686 osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %ld", uname, timeout );
688 char* string = va_list_to_string(
689 "%d.%ld.%s", (long) getpid(), time(NULL), uname );
690 char* authToken = md5sum(string);
691 char* authKey = va_list_to_string(
692 "%s%s", OILS_AUTH_CACHE_PRFX, authToken );
694 const char* ws = (workstation) ? workstation : "";
695 osrfLogActivity(OSRF_LOG_MARK,
696 "successful login: username=%s, authtoken=%s, workstation=%s", uname, authToken, ws );
698 oilsFMSetString( userObj, "passwd", "" );
699 jsonObject* cacheObj = jsonParseFmt( "{\"authtime\": %ld}", timeout );
700 jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
702 if( !strcmp( type, OILS_AUTH_PERSIST )) {
703 // Add entries for endtime and reset_interval, so that we can gracefully
704 // extend the session a bit if the user is active toward the end of the
705 // timeout originally specified.
706 time_t endtime = time( NULL ) + timeout;
707 jsonObjectSetKey( cacheObj, "endtime", jsonNewNumberObject( (double) endtime ) );
709 // Reset interval is hard-coded for now, but if we ever want to make it
710 // configurable, this is the place to do it:
711 jsonObjectSetKey( cacheObj, "reset_interval",
712 jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL ));
715 osrfCachePutObject( authKey, cacheObj, (time_t) timeout );
716 jsonObjectFree(cacheObj);
717 osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
718 jsonObject* payload = jsonParseFmt(
719 "{ \"authtoken\": \"%s\", \"authtime\": %ld }", authToken, timeout );
721 response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
722 free(string); free(authToken); free(authKey);
723 jsonObjectFree(payload);
728 static oilsEvent* oilsAuthVerifyWorkstation(
729 const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
730 osrfLogInfo(OSRF_LOG_MARK, "Attaching workstation to user at login: %s", ws);
731 jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
732 if(!workstation || workstation->type == JSON_NULL) {
733 jsonObjectFree(workstation);
734 return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
736 long wsid = oilsFMGetObjectId(workstation);
737 LONG_TO_STRING(wsid);
738 char* orgid = oilsFMGetString(workstation, "owning_lib");
739 oilsFMSetString(userObj, "wsid", LONGSTR);
740 oilsFMSetString(userObj, "ws_ou", orgid);
742 jsonObjectFree(workstation);
749 @brief Implement the "complete" method.
750 @param ctx The method context.
751 @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
752 client to indicate completion; a positive integer if successful but no such STATUS
753 message has been sent.
756 - a hash with some combination of the following elements:
759 - "password" (hashed with the cached seed; not plaintext)
763 - "agent" (what software/interface/3rd-party is making the request)
764 - "nonce" optional login seed to differentiate logins using the same username.
766 The password is required. Either a username or a barcode must also be present.
768 Return to client: Intermediate authentication seed.
770 Validate the password, using the username if available, or the barcode if not. The
771 user must be active, and not barred from logging on. The barcode, if used for
772 authentication, must be active as well. The workstation, if specified, must be valid.
774 Upon deciding whether to allow the logon, return a corresponding event to the client.
776 int oilsAuthComplete( osrfMethodContext* ctx ) {
777 OSRF_METHOD_VERIFY_CONTEXT(ctx);
779 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
781 const char* uname = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
782 const char* identifier = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
783 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
784 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
785 int orgloc = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
786 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
787 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
788 const char* ewho = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
789 const char* nonce = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
791 const char* ws = (workstation) ? workstation : "";
792 if (!nonce) nonce = "";
794 // we no longer care how the identifier reaches us,
795 // as long as we have one.
799 } else if (barcode) {
800 identifier = barcode;
805 return osrfAppRequestRespondException(ctx->session, ctx->request,
806 "username/barcode and password required for method: %s",
810 osrfLogInfo(OSRF_LOG_MARK,
811 "Patron completing authentication with identifer %s", identifier);
813 /* Use __FILE__, harmless_line_number for creating
814 * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
815 * giving away information about why an authentication attempt failed.
817 int harmless_line_number = __LINE__;
820 type = OILS_AUTH_STAFF;
822 oilsEvent* response = NULL; // free
823 jsonObject* userObj = NULL; // free
824 int card_active = 1; // boolean; assume active until proven otherwise
825 int using_card = 0; // true if this is a barcode login
827 char* cache_key = va_list_to_string(
828 "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
829 jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
832 return osrfAppRequestRespondException(ctx->session,
833 ctx->request, "No authentication seed found. "
834 "open-ils.auth.authenticate.init must be called first "
835 " (check that memcached is running and can be connected to) "
839 int user_id = jsonObjectGetNumber(
840 jsonObjectGetKeyConst(cacheObj, "user_id"));
842 jsonObject* param = jsonNewNumberObject(user_id); // free
843 userObj = oilsUtilsCStoreReq(
844 "open-ils.cstore.direct.actor.user.retrieve", param);
845 jsonObjectFree(param);
847 using_card = (jsonObjectGetKeyConst(cacheObj, "barcode") != NULL);
850 // see if the card is inactive
852 jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", identifier);
853 jsonObject* card = oilsUtilsCStoreReq(
854 "open-ils.cstore.direct.actor.card.search", params);
855 jsonObjectFree(params);
858 if (card->type != JSON_NULL) {
859 char* card_active_str = oilsFMGetString(card, "active");
860 card_active = oilsUtilsIsDBTrue(card_active_str);
861 free(card_active_str);
863 jsonObjectFree(card);
867 int barred = 0, deleted = 0;
868 char *barred_str, *deleted_str;
871 barred_str = oilsFMGetString(userObj, "barred");
872 barred = oilsUtilsIsDBTrue(barred_str);
875 deleted_str = oilsFMGetString(userObj, "deleted");
876 deleted = oilsUtilsIsDBTrue(deleted_str);
880 if(!userObj || barred || deleted) {
881 response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
882 osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s, barcode=%s, workstation=%s",
883 uname, (barcode ? barcode : "(none)"), ws );
884 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
885 oilsEventFree(response);
886 return 0; // No such user
889 // Such a user exists and isn't barred or deleted.
890 // Now see if he or she has the right credentials.
891 int passOK = oilsAuthVerifyPassword(
892 ctx, user_id, identifier, password, nonce);
895 jsonObjectFree(userObj);
899 // See if the account is active
900 char* active = oilsFMGetString(userObj, "active");
901 if( !oilsUtilsIsDBTrue(active) ) {
903 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
905 response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
907 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
908 oilsEventFree(response);
909 jsonObjectFree(userObj);
916 osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
917 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
918 osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
919 oilsEventFree( response );
920 jsonObjectFree( userObj );
925 // See if the user is even allowed to log in
926 if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
927 jsonObjectFree(userObj);
931 // If a workstation is defined, add the workstation info
932 if( workstation != NULL ) {
933 osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
934 response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
936 jsonObjectFree(userObj);
937 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
938 oilsEventFree(response);
943 // Otherwise, use the home org as the workstation org on the user
944 char* orgid = oilsFMGetString(userObj, "home_ou");
945 oilsFMSetString(userObj, "ws_ou", orgid);
949 char* freeable_uname = NULL;
951 uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
954 if( passOK ) { // login successful
956 char* ewhat = "login";
958 if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
959 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
963 response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
966 oilsUtilsTrackUserActivity(
967 oilsFMGetObjectId(userObj),
969 osrfAppSessionGetIngress()
973 response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
974 osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s, barcode=%s, workstation=%s",
975 uname, (barcode ? barcode : "(none)"), ws );
978 jsonObjectFree(userObj);
979 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
980 oilsEventFree(response);
983 free(freeable_uname);
990 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
991 OSRF_METHOD_VERIFY_CONTEXT(ctx);
993 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
994 jsonObject* resp = NULL;
997 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
998 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
999 osrfCacheRemove(key);
1000 resp = jsonNewObject(authToken); /**/
1004 osrfAppRespondComplete( ctx, resp );
1005 jsonObjectFree(resp);
1010 * Fetches the user object from the database and updates the user object in
1011 * the cache object, which then has to be re-inserted into the cache.
1012 * User object is retrieved inside a transaction to avoid replication issues.
1014 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
1016 osrfAppSession* session;
1018 jsonObject *param, *userObj, *newUserObj = NULL;
1020 userObj = jsonObjectGetKey( cacheObj, "userobj" );
1021 userId = oilsFMGetObjectId( userObj );
1023 session = osrfAppSessionClientInit( "open-ils.cstore" );
1024 osrfAppSessionConnect(session);
1026 reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
1027 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1031 osrfMessageFree(omsg);
1032 param = jsonNewNumberObject(userId);
1033 reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
1034 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1035 jsonObjectFree(param);
1038 newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
1039 osrfMessageFree(omsg);
1040 reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
1041 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1042 osrfMessageFree(omsg);
1046 osrfAppSessionFree(session); // calls disconnect internally
1050 // ws_ou and wsid are ephemeral and need to be manually propagated
1051 // oilsFMSetString dupe()'s internally, no need to clone the string
1052 oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
1053 oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
1055 jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
1056 jsonObjectSetKey(cacheObj, "userobj", newUserObj);
1060 osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
1065 Resets the auth login timeout
1066 @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
1068 static oilsEvent* _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
1069 if(!authToken) return NULL;
1071 oilsEvent* evt = NULL;
1074 osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
1075 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1076 jsonObject* cacheObj = osrfCacheGetObject( key );
1079 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
1080 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1085 _oilsAuthReloadUser(cacheObj);
1088 // Determine a new timeout value
1089 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
1091 // Extend the current endtime by a fixed amount
1092 time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
1093 int reset_interval = DEFAULT_RESET_INTERVAL;
1094 const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
1095 cacheObj, "reset_interval" );
1096 if( reset_interval_obj ) {
1097 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
1098 if( reset_interval <= 0 )
1099 reset_interval = DEFAULT_RESET_INTERVAL;
1102 time_t now = time( NULL );
1103 time_t new_endtime = now + reset_interval;
1104 if( new_endtime > endtime ) {
1105 // Keep the session alive a little longer
1106 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
1107 timeout = reset_interval;
1108 osrfCachePutObject( key, cacheObj, timeout );
1110 // The session isn't close to expiring, so don't reset anything.
1111 // Just report the time remaining.
1112 timeout = endtime - now;
1115 // Reapply the existing timeout from the current time
1116 timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
1117 osrfCachePutObject( key, cacheObj, timeout );
1120 jsonObject* payload = jsonNewNumberObject( (double) timeout );
1121 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
1122 jsonObjectFree(payload);
1123 jsonObjectFree(cacheObj);
1130 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
1131 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1132 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1133 double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
1134 oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
1135 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
1141 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
1142 OSRF_METHOD_VERIFY_CONTEXT(ctx);
1143 bool returnFull = false;
1145 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1147 if(ctx->params->size > 1) {
1148 // caller wants full cached object, with authtime, etc.
1149 const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
1150 if(rt && strcmp(rt, "0") != 0)
1154 jsonObject* cacheObj = NULL;
1155 oilsEvent* evt = NULL;
1159 // Reset the timeout to keep the session alive
1160 evt = _oilsAuthResetTimeout(authToken, 0);
1162 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
1163 osrfAppRespondComplete( ctx, oilsEventToJSON( evt )); // can't reset timeout
1167 // Retrieve the cached session object
1168 osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
1169 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1170 cacheObj = osrfCacheGetObject( key );
1172 // Return a copy of the cached user object
1174 osrfAppRespondComplete( ctx, cacheObj);
1176 osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
1177 jsonObjectFree(cacheObj);
1179 // Auth token is invalid or expired
1180 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1181 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
1182 oilsEventFree(evt2);
1190 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1191 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );