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 );
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 );
482 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
483 osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s, barcode=%s, workstation=%s",
484 uname, (barcode ? barcode : "(none)"), ws );
485 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
486 oilsEventFree(response);
487 return 0; // No such user
490 // Such a user exists. Now see if he or she has the right credentials.
493 passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
495 passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password );
498 jsonObjectFree(userObj);
502 // See if the account is active
503 char* active = oilsFMGetString(userObj, "active");
504 if( !oilsUtilsIsDBTrue(active) ) {
506 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
508 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
510 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
511 oilsEventFree(response);
512 jsonObjectFree(userObj);
518 osrfLogInfo( OSRF_LOG_MARK, "Fetching card by barcode %s", barcode );
521 osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
522 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
523 osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
524 oilsEventFree( response );
525 jsonObjectFree( userObj );
530 // See if the user is even allowed to log in
531 if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
532 jsonObjectFree(userObj);
536 // If a workstation is defined, add the workstation info
537 if( workstation != NULL ) {
538 osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
539 response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
541 jsonObjectFree(userObj);
542 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
543 oilsEventFree(response);
548 // Otherwise, use the home org as the workstation org on the user
549 char* orgid = oilsFMGetString(userObj, "home_ou");
550 oilsFMSetString(userObj, "ws_ou", orgid);
554 char* freeable_uname = NULL;
556 uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
560 response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
563 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
564 osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s, barcode=%s, workstation=%s",
565 uname, (barcode ? barcode : "(none)"), ws );
568 jsonObjectFree(userObj);
569 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
570 oilsEventFree(response);
573 free(freeable_uname);
580 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
581 OSRF_METHOD_VERIFY_CONTEXT(ctx);
583 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
584 jsonObject* resp = NULL;
587 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
588 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
589 osrfCacheRemove(key);
590 resp = jsonNewObject(authToken); /**/
594 osrfAppRespondComplete( ctx, resp );
595 jsonObjectFree(resp);
600 Resets the auth login timeout
601 @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
603 static oilsEvent* _oilsAuthResetTimeout( const char* authToken ) {
604 if(!authToken) return NULL;
606 oilsEvent* evt = NULL;
609 osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
610 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
611 jsonObject* cacheObj = osrfCacheGetObject( key );
614 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
615 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
619 timeout = jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
620 osrfCacheSetExpire( timeout, key );
621 jsonObject* payload = jsonNewNumberObject(timeout);
622 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
623 jsonObjectFree(payload);
624 jsonObjectFree(cacheObj);
631 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
632 OSRF_METHOD_VERIFY_CONTEXT(ctx);
633 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
634 oilsEvent* evt = _oilsAuthResetTimeout(authToken);
635 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
641 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
642 OSRF_METHOD_VERIFY_CONTEXT(ctx);
644 const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
645 jsonObject* cacheObj = NULL;
646 oilsEvent* evt = NULL;
650 evt = _oilsAuthResetTimeout(authToken);
652 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
653 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
657 osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
658 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
659 cacheObj = osrfCacheGetObject( key );
661 osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
662 jsonObjectFree(cacheObj);
664 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
665 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
673 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
674 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );