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"
10 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
12 #define MODULENAME "open-ils.auth"
14 #define OILS_AUTH_OPAC "opac"
15 #define OILS_AUTH_STAFF "staff"
16 #define OILS_AUTH_TEMP "temp"
18 int osrfAppInitialize();
19 int osrfAppChildInit();
21 static int _oilsAuthOPACTimeout = 0;
22 static int _oilsAuthStaffTimeout = 0;
23 static int _oilsAuthOverrideTimeout = 0;
27 @brief Initialize the application by registering functions for method calls.
28 @return Zero in all cases.
30 int osrfAppInitialize() {
32 osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
34 /* load and parse the IDL */
35 if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
37 osrfAppRegisterMethod(
39 "open-ils.auth.authenticate.init",
41 "Start the authentication process and returns the intermediate authentication seed"
42 " PARAMS( username )", 1, 0 );
44 osrfAppRegisterMethod(
46 "open-ils.auth.authenticate.complete",
48 "Completes the authentication process. Returns an object like so: "
49 "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
50 "token and authtime is the number of seconds the session will be active"
51 "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
52 "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
53 "org_id is the location at which the login should be considered "
54 "active for login timeout purposes", 1, 0 );
56 osrfAppRegisterMethod(
58 "open-ils.auth.session.retrieve",
59 "oilsAuthSessionRetrieve",
60 "Pass in the auth token and this retrieves the user object. The auth "
61 "timeout is reset when this call is made "
62 "Returns the user object (password blanked) for the given login session "
63 "PARAMS( authToken )", 1, 0 );
65 osrfAppRegisterMethod(
67 "open-ils.auth.session.delete",
68 "oilsAuthSessionDelete",
69 "Destroys the given login session "
70 "PARAMS( authToken )", 1, 0 );
72 osrfAppRegisterMethod(
74 "open-ils.auth.session.reset_timeout",
75 "oilsAuthResetTimeout",
76 "Resets the login timeout for the given session "
77 "Returns an ILS Event with payload = session_timeout of session "
78 "if found, otherwise returns the NO_SESSION event"
79 "PARAMS( authToken )", 1, 0 );
85 @brief Dummy placeholder for initializing a server drone.
87 There is nothing to do, so do nothing.
89 int osrfAppChildInit() {
94 @brief Implement the "init" method.
95 @param ctx The method context.
96 @return Zero if successful, or -1 if not.
101 Return to client: Intermediate authentication seed.
103 Combine the username with a timestamp and process ID, and take an md5 hash of the result.
104 Store the hash in memcache, with a key based on the username. Then return the hash to
107 However: if the username includes one or more embedded blank spaces, return a dummy
108 hash without storing anything in memcache. The dummy will never match a stored hash, so
109 any attempt to authenticate with it will fail.
111 int oilsAuthInit( osrfMethodContext* ctx ) {
112 OSRF_METHOD_VERIFY_CONTEXT(ctx);
114 char* username = jsonObjectToSimpleString( jsonObjectGetIndex(ctx->params, 0) );
119 if( strchr( username, ' ' ) ) {
121 // Embedded spaces are not allowed in a username. Use "x" as a dummy
122 // seed. It will never be a valid seed because 'x' is not a hex digit.
123 resp = jsonNewObject( "x" );
127 // Build a key and a seed; store them in memcache.
128 char* key = va_list_to_string( "%s%s", OILS_AUTH_CACHE_PRFX, username );
129 char* seed = md5sum( "%d.%ld.%s", (int) time(NULL), (long) getpid(), username );
130 osrfCachePutString( key, seed, 30 );
132 osrfLogDebug( OSRF_LOG_MARK, "oilsAuthInit(): has seed %s and key %s", seed, key );
134 // Build a returnable object containing the seed.
135 resp = jsonNewObject( seed );
141 // Return the seed to the client.
142 osrfAppRespondComplete( ctx, resp );
144 jsonObjectFree(resp);
149 return -1; // Error: no username parameter
153 Verifies that the user has permission to login with the
154 given type. If the permission fails, an oilsEvent is returned
156 @return -1 if the permission check failed, 0 if the permission
159 static int oilsAuthCheckLoginPerm(
160 osrfMethodContext* ctx, const jsonObject* userObj, const char* type ) {
162 if(!(userObj && type)) return -1;
163 oilsEvent* perm = NULL;
165 if(!strcasecmp(type, OILS_AUTH_OPAC)) {
166 char* permissions[] = { "OPAC_LOGIN" };
167 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
169 } else if(!strcasecmp(type, OILS_AUTH_STAFF)) {
170 char* permissions[] = { "STAFF_LOGIN" };
171 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
173 } else if(!strcasecmp(type, OILS_AUTH_TEMP)) {
174 char* permissions[] = { "STAFF_LOGIN" };
175 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
179 osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
188 Returns 1 if the password provided matches the user's real password
193 @brief Verify the password received from the client.
194 @param ctx The method context.
195 @param userObj An object from the database, representing the user.
196 @param password An obfuscated password received from the client.
197 @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
199 (None of the so-called "passwords" used here are in plaintext. All have been passed
200 through at least one layer of hashing to obfuscate them.)
202 Take the password from the user object. Append it to the username seed from memcache,
203 as stored previously by a call to the init method. Take an md5 hash of the result.
204 Then compare this hash to the password received from the client.
206 In order for the two to match, other than by dumb luck, the client had to construct
207 the password it passed in the same way. That means it neded to know not only the
208 original password (either hashed or plaintext), but also the seed. The latter requirement
209 means that the client process needs either to be the same process that called the init
210 method or to receive the seed from the process that did so.
212 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
213 const jsonObject* userObj, const char* uname, const char* password ) {
215 // Get the username seed, as stored previously in memcache by the init method
216 char* seed = osrfCacheGetString( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
218 return osrfAppRequestRespondException( ctx->session,
219 ctx->request, "No authentication seed found. "
220 "open-ils.auth.authenticate.init must be called first");
223 // Get the hashed password from the user object
224 char* realPassword = oilsFMGetString( userObj, "passwd" );
226 osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
227 osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );
229 // Concatenate them and take an MD5 hash of the result
230 char* maskedPw = md5sum( "%s%s", seed, realPassword );
236 // This happens only if md5sum() runs out of memory
238 return -1; // md5sum() ran out of memory
241 osrfLogDebug(OSRF_LOG_MARK, "oilsAuth generated masked password %s. "
242 "Testing against provided password %s", maskedPw, password );
245 if( !strcmp( maskedPw, password ) )
254 Calculates the login timeout
255 1. If orgloc is 1 or greater and has a timeout specified as an
256 org unit setting, it is used
257 2. If orgloc is not valid, we check the org unit auth timeout
258 setting for the home org unit of the user logging in
259 3. If that setting is not defined, we use the configured defaults
261 static double oilsAuthGetTimeout( const jsonObject* userObj, const char* type, double orgloc ) {
263 if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
265 jsonObject* value_obj;
267 value_obj = osrf_settings_host_value_object(
268 "/apps/open-ils.auth/app_settings/default_timeout/opac" );
269 _oilsAuthOPACTimeout = jsonObjectGetNumber(value_obj);
270 jsonObjectFree(value_obj);
272 value_obj = osrf_settings_host_value_object(
273 "/apps/open-ils.auth/app_settings/default_timeout/staff" );
274 _oilsAuthStaffTimeout = jsonObjectGetNumber(value_obj);
275 jsonObjectFree(value_obj);
277 value_obj = osrf_settings_host_value_object(
278 "/apps/open-ils.auth/app_settings/default_timeout/temp" );
279 _oilsAuthOverrideTimeout = jsonObjectGetNumber(value_obj);
280 jsonObjectFree(value_obj);
283 osrfLogInfo(OSRF_LOG_MARK,
284 "Set default auth timeouts: opac => %d : staff => %d : temp => %d",
285 _oilsAuthOPACTimeout, _oilsAuthStaffTimeout, _oilsAuthOverrideTimeout );
288 char* setting = NULL;
290 double home_ou = jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ) );
291 if(orgloc < 1) orgloc = (int) home_ou;
293 if(!strcmp(type, OILS_AUTH_OPAC))
294 setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
295 else if(!strcmp(type, OILS_AUTH_STAFF))
296 setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
297 else if(!strcmp(type, OILS_AUTH_TEMP))
298 setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
300 char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
303 if( orgloc != home_ou ) {
304 osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
305 "trying home_ou %d", orgloc, home_ou );
306 timeout = oilsUtilsFetchOrgSetting( (int) home_ou, setting );
309 if(!strcmp(type, OILS_AUTH_STAFF)) return _oilsAuthStaffTimeout;
310 if(!strcmp(type, OILS_AUTH_TEMP)) return _oilsAuthOverrideTimeout;
311 return _oilsAuthOPACTimeout;
315 double t = atof(timeout);
321 Adds the authentication token to the user cache. The timeout for the
322 auth token is based on the type of login as well as (if type=='opac')
324 Returns the event that should be returned to the user.
327 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
328 const char* type, double orgloc, const char* workstation ) {
333 char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "ws_ou"));
334 if(wsorg) { /* if there is a workstation, use it for the timeout */
335 osrfLogDebug( OSRF_LOG_MARK,
336 "Auth session trying workstation id %d for auth timeout", atoi(wsorg));
337 timeout = oilsAuthGetTimeout( userObj, type, atoi(wsorg) );
340 osrfLogDebug( OSRF_LOG_MARK,
341 "Auth session trying org from param [%d] for auth timeout", orgloc );
342 timeout = oilsAuthGetTimeout( userObj, type, orgloc );
344 osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %f", uname, timeout );
346 char* string = va_list_to_string(
347 "%d.%ld.%s", (long) getpid(), time(NULL), uname );
348 char* authToken = md5sum(string);
349 char* authKey = va_list_to_string(
350 "%s%s", OILS_AUTH_CACHE_PRFX, authToken );
352 const char* ws = (workstation) ? workstation : "";
353 osrfLogActivity(OSRF_LOG_MARK,
354 "successful login: username=%s, authtoken=%s, workstation=%s", uname, authToken, ws );
356 oilsFMSetString( userObj, "passwd", "" );
357 jsonObject* cacheObj = jsonParseFmt("{\"authtime\": %f}", timeout);
358 jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
360 osrfCachePutObject( authKey, cacheObj, timeout );
361 jsonObjectFree(cacheObj);
362 osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
363 jsonObject* payload = jsonParseFmt(
364 "{ \"authtoken\": \"%s\", \"authtime\": %f }", authToken, timeout );
366 response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
367 free(string); free(authToken); free(authKey);
368 jsonObjectFree(payload);
373 static oilsEvent* oilsAuthVerifyWorkstation(
374 const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
375 osrfLogInfo(OSRF_LOG_MARK, "Attaching workstation to user at login: %s", ws);
376 jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
377 if(!workstation || workstation->type == JSON_NULL) {
378 jsonObjectFree(workstation);
379 return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
381 long wsid = oilsFMGetObjectId(workstation);
382 LONG_TO_STRING(wsid);
383 char* orgid = oilsFMGetString(workstation, "owning_lib");
384 oilsFMSetString(userObj, "wsid", LONGSTR);
385 oilsFMSetString(userObj, "ws_ou", orgid);
387 jsonObjectFree(workstation);
394 @brief Implement the "complete" method.
395 @param ctx The method context.
396 @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
397 client to indicate completion; a positive integer if successful but no such STATUS
398 message has been sent.
401 - a hash with some combination of the following elements:
404 - "password" (hashed with the cached seed; not plaintext)
409 The password is required. Either a username or a barcode must also be present.
411 Return to client: Intermediate authentication seed.
413 Validate the password, using the username if available, or the barcode if not. The
414 user must be active, and not barred from logging on. The barcode, if used for
415 authentication, must be active as well. The workstation, if specified, must be valid.
417 Upon deciding whether to allow the logon, return a corresponding event to the client.
419 int oilsAuthComplete( osrfMethodContext* ctx ) {
420 OSRF_METHOD_VERIFY_CONTEXT(ctx);
422 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
424 const char* uname = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
425 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
426 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
427 double orgloc = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
428 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
429 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
431 const char* ws = (workstation) ? workstation : "";
434 type = OILS_AUTH_STAFF;
436 if( !( (uname || barcode) && password) ) {
437 return osrfAppRequestRespondException( ctx->session, ctx->request,
438 "username/barcode and password required for method: %s", ctx->method->name );
441 oilsEvent* response = NULL;
442 jsonObject* userObj = NULL;
443 int card_active = 1; // boolean; assume active until proven otherwise
445 // Fetch a row from the actor.usr table, by username if available,
446 // or by barcode if not.
448 userObj = oilsUtilsFetchUserByUsername( uname );
449 if( userObj && JSON_NULL == userObj->type ) {
450 jsonObjectFree( userObj );
451 userObj = NULL; // username not found
455 // Read from actor.card by barcode
457 osrfLogInfo( OSRF_LOG_MARK, "Fetching user by barcode %s", barcode );
459 jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
460 jsonObject* card = oilsUtilsQuickReq(
461 "open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
462 jsonObjectFree( params );
464 if( card && card->type != JSON_NULL ) {
465 // Determine whether the card is active
466 char* card_active_str = oilsFMGetString( card, "active" );
467 card_active = oilsUtilsIsDBTrue( card_active_str );
468 free( card_active_str );
470 // Look up the user who owns the card
471 char* userid = oilsFMGetString( card, "usr" );
472 jsonObjectFree( card );
473 params = jsonParseFmt( "[%s]", userid );
475 userObj = oilsUtilsQuickReq(
476 "open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params );
477 jsonObjectFree( params );
478 if( userObj && JSON_NULL == userObj->type ) {
479 // user not found (shouldn't happen, due to foreign key)
480 jsonObjectFree( userObj );
487 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
488 osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s, barcode=%s, workstation=%s",
489 uname, (barcode ? barcode : "(none)"), ws );
490 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
491 oilsEventFree(response);
492 return 0; // No such user
495 // Such a user exists. Now see if he or she has the right credentials.
498 passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
500 passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password );
503 jsonObjectFree(userObj);
507 // See if the account is active
508 char* active = oilsFMGetString(userObj, "active");
509 if( !oilsUtilsIsDBTrue(active) ) {
511 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
513 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
515 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
516 oilsEventFree(response);
517 jsonObjectFree(userObj);
523 osrfLogInfo( OSRF_LOG_MARK, "Fetching card by barcode %s", barcode );
526 osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
527 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
528 osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
529 oilsEventFree( response );
530 jsonObjectFree( userObj );
535 // See if the user is even allowed to log in
536 if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
537 jsonObjectFree(userObj);
541 // If a workstation is defined, add the workstation info
542 if( workstation != NULL ) {
543 osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
544 response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
546 jsonObjectFree(userObj);
547 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
548 oilsEventFree(response);
553 // Otherwise, use the home org as the workstation org on the user
554 char* orgid = oilsFMGetString(userObj, "home_ou");
555 oilsFMSetString(userObj, "ws_ou", orgid);
559 char* freeable_uname = NULL;
561 uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
565 response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
568 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
569 osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s, barcode=%s, workstation=%s",
570 uname, (barcode ? barcode : "(none)"), ws );
573 jsonObjectFree(userObj);
574 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
575 oilsEventFree(response);
578 free(freeable_uname);
585 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
586 OSRF_METHOD_VERIFY_CONTEXT(ctx);
588 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
589 jsonObject* resp = NULL;
592 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
593 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
594 osrfCacheRemove(key);
595 resp = jsonNewObject(authToken); /**/
599 osrfAppRespondComplete( ctx, resp );
600 jsonObjectFree(resp);
605 Resets the auth login timeout
606 @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
608 static oilsEvent* _oilsAuthResetTimeout( const char* authToken ) {
609 if(!authToken) return NULL;
611 oilsEvent* evt = NULL;
614 osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
615 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
616 jsonObject* cacheObj = osrfCacheGetObject( key );
619 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
620 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
624 timeout = jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
625 osrfCacheSetExpire( timeout, key );
626 jsonObject* payload = jsonNewNumberObject(timeout);
627 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
628 jsonObjectFree(payload);
629 jsonObjectFree(cacheObj);
636 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
637 OSRF_METHOD_VERIFY_CONTEXT(ctx);
638 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
639 oilsEvent* evt = _oilsAuthResetTimeout(authToken);
640 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
646 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
647 OSRF_METHOD_VERIFY_CONTEXT(ctx);
649 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
650 jsonObject* cacheObj = NULL;
651 oilsEvent* evt = NULL;
655 evt = _oilsAuthResetTimeout(authToken);
657 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
658 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
662 osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
663 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
664 cacheObj = osrfCacheGetObject( key );
666 osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
667 jsonObjectFree(cacheObj);
669 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
670 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
678 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
679 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );