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);
393 /* see if the card used to login is marked as barred */
394 static oilsEvent* oilsAuthCheckCard( const char* barcode ) {
395 if(!barcode) return NULL;
396 osrfLogDebug(OSRF_LOG_MARK, "Checking to see if barcode %s is active", barcode);
398 jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
399 jsonObject* card = oilsUtilsQuickReq(
400 "open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
401 jsonObjectFree(params);
403 char* active = oilsFMGetString(card, "active");
404 jsonObjectFree(card);
406 oilsEvent* return_event = NULL;
407 if( ! oilsUtilsIsDBTrue(active) ) {
408 osrfLogInfo(OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode);
409 return_event = oilsNewEvent(OSRF_LOG_MARK, "PATRON_CARD_INACTIVE");
418 int oilsAuthComplete( osrfMethodContext* ctx ) {
419 OSRF_METHOD_VERIFY_CONTEXT(ctx);
421 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
423 const char* uname = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
424 const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
425 const char* type = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
426 double orgloc = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
427 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
428 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
430 const char* ws = (workstation) ? workstation : "";
433 type = OILS_AUTH_STAFF;
435 if( !( (uname || barcode) && password) ) {
436 return osrfAppRequestRespondException( ctx->session, ctx->request,
437 "username/barcode and password required for method: %s", ctx->method->name );
440 oilsEvent* response = NULL;
441 jsonObject* userObj = NULL;
443 // Fetch a row from the actor.usr table, by username if available,
444 // or by barcode if not.
446 userObj = oilsUtilsFetchUserByUsername( uname );
447 if( userObj && JSON_NULL == userObj->type ) {
448 jsonObjectFree( userObj );
449 userObj = NULL; // username not found
453 userObj = oilsUtilsFetchUserByBarcode( barcode );
456 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
457 osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s, barcode=%s, workstation=%s",
458 uname, (barcode ? barcode : "(none)"), ws );
459 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
460 oilsEventFree(response);
461 return 0; // No such user
464 // Such a user exists. Now see if he or she has the right credentials.
467 passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
469 passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password );
472 jsonObjectFree(userObj);
476 // See if the account is active
477 char* active = oilsFMGetString(userObj, "active");
478 if( !oilsUtilsIsDBTrue(active) ) {
480 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
482 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
484 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
485 oilsEventFree(response);
486 jsonObjectFree(userObj);
492 /* then see if the barcode they used is active */
493 if( barcode && ctx && userObj && (response = oilsAuthCheckCard( barcode )) ) {
494 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
495 oilsEventFree(response);
496 jsonObjectFree(userObj);
501 /* check to see if the user is even allowed to login */
502 if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
503 jsonObjectFree(userObj);
508 /* if a workstation is defined, flesh the user with the workstation info */
509 if( workstation != NULL ) {
510 osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
511 response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
513 jsonObjectFree(userObj);
514 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
515 oilsEventFree(response);
520 /* otherwise, use the home org as the workstation org on the user */
521 char* orgid = oilsFMGetString(userObj, "home_ou");
522 oilsFMSetString(userObj, "ws_ou", orgid);
526 char* freeable_uname = NULL;
528 uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
532 response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
535 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
536 osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s, barcode=%s, workstation=%s",
537 uname, (barcode ? barcode : "(none)"), ws );
540 jsonObjectFree(userObj);
541 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
542 oilsEventFree(response);
545 free(freeable_uname);
552 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
553 OSRF_METHOD_VERIFY_CONTEXT(ctx);
555 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
556 jsonObject* resp = NULL;
559 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
560 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
561 osrfCacheRemove(key);
562 resp = jsonNewObject(authToken); /**/
566 osrfAppRespondComplete( ctx, resp );
567 jsonObjectFree(resp);
572 Resets the auth login timeout
573 @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
575 static oilsEvent* _oilsAuthResetTimeout( const char* authToken ) {
576 if(!authToken) return NULL;
578 oilsEvent* evt = NULL;
581 osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
582 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
583 jsonObject* cacheObj = osrfCacheGetObject( key );
586 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
587 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
591 timeout = jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
592 osrfCacheSetExpire( timeout, key );
593 jsonObject* payload = jsonNewNumberObject(timeout);
594 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
595 jsonObjectFree(payload);
596 jsonObjectFree(cacheObj);
603 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
604 OSRF_METHOD_VERIFY_CONTEXT(ctx);
605 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
606 oilsEvent* evt = _oilsAuthResetTimeout(authToken);
607 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
613 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
614 OSRF_METHOD_VERIFY_CONTEXT(ctx);
616 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
617 jsonObject* cacheObj = NULL;
618 oilsEvent* evt = NULL;
622 evt = _oilsAuthResetTimeout(authToken);
624 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
625 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
629 osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
630 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
631 cacheObj = osrfCacheGetObject( key );
633 osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
634 jsonObjectFree(cacheObj);
636 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
637 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
645 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
646 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );