LP#1348731: Optional Auth login nonce to differentiate same-username logins
authorBill Erickson <berick@esilibrary.com>
Tue, 8 Oct 2013 19:52:12 +0000 (15:52 -0400)
committerMike Rylander <mrylander@gmail.com>
Thu, 31 Jul 2014 12:15:16 +0000 (08:15 -0400)
If multiple login attempts are made using the same username within a
very short period of time, a race condition exists where, upon
completion of the first login, the auth init cache data for any pending
logins are removed, since there can only be one instance of cached init
data per username.

This adds support for allowing the caller to pass in a random string
which is added to the cache key as a way to differentiate between logins
using the same username.

The seed is passed into auth init as an optional secondary parameter
and passed again (via the "nonce" argument) to auth complete to ensure
consistent cache keys across both interactions.

Example:

my $nonce = rand($$);

my $seed = request(
    'open-ils.auth',
    'open-ils.auth.authenticate.init', $username, $nonce );

my $response = request(
    'open-ils.auth',
    'open-ils.auth.authenticate.complete',
    {
        username => $username,
        password => md5_hex($seed . md5_hex($password)),
        type     => 'opac',
        nonce    => $nonce
    }
);

The race condition has been observed with the SIP2 gateway when
multiple devices have been configured to use the same account.

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/src/c-apps/oils_auth.c

index 2c7086a..bd79770 100644 (file)
@@ -151,6 +151,10 @@ int osrfAppChildInit() {
 
        Method parameters:
        - username
+       - nonce : optional login seed (string) provided by the caller which
+               is added to the auth init cache to differentiate between logins
+               using the same username and thus avoiding cache collisions for
+               near-simultaneous logins.
 
        Return to client: Intermediate authentication seed.
 
@@ -166,6 +170,9 @@ int oilsAuthInit( osrfMethodContext* ctx ) {
        OSRF_METHOD_VERIFY_CONTEXT(ctx);
 
        char* username  = jsonObjectToSimpleString( jsonObjectGetIndex(ctx->params, 0) );
+       const char* nonce = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
+       if (!nonce) nonce = "";
+
        if( username ) {
 
                jsonObject* resp;
@@ -179,9 +186,9 @@ int oilsAuthInit( osrfMethodContext* ctx ) {
                } else {
 
                        // Build a key and a seed; store them in memcache.
-                       char* key  = va_list_to_string( "%s%s", OILS_AUTH_CACHE_PRFX, username );
+                       char* key  = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, username, nonce );
                        char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, username, OILS_AUTH_COUNT_SFFX );
-                       char* seed = md5sum( "%d.%ld.%s", (int) time(NULL), (long) getpid(), username );
+                       char* seed = md5sum( "%d.%ld.%s.%s", (int) time(NULL), (long) getpid(), username, nonce );
                        jsonObject* countobject = osrfCacheGetObject( countkey );
                        if(!countobject) {
                                countobject = jsonNewNumberObject( (double) 0 );
@@ -275,10 +282,11 @@ static int oilsAuthCheckLoginPerm(
        method or to receive the seed from the process that did so.
 */
 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
-               const jsonObject* userObj, const char* uname, const char* password ) {
+               const jsonObject* userObj, const char* uname,
+               const char* password, const char* nonce ) {
 
        // Get the username seed, as stored previously in memcache by the init method
-       char* seed = osrfCacheGetString( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
+       char* seed = osrfCacheGetString( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, nonce );
        if(!seed) {
                return osrfAppRequestRespondException( ctx->session,
                        ctx->request, "No authentication seed found. "
@@ -288,7 +296,7 @@ static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
        }
     
        // We won't be needing the seed again, remove it
-       osrfCacheRemove( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
+       osrfCacheRemove( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, nonce );
 
        // Get the hashed password from the user object
        char* realPassword = oilsFMGetString( userObj, "passwd" );
@@ -563,6 +571,7 @@ static oilsEvent* oilsAuthVerifyWorkstation(
                - "org"
                - "workstation"
                - "agent" (what software/interface/3rd-party is making the request)
+               - "nonce" optional login seed to differentiate logins using the same username.
 
        The password is required.  Either a username or a barcode must also be present.
 
@@ -586,8 +595,10 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
        const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
        const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
        const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
+       const char* nonce       = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
 
        const char* ws = (workstation) ? workstation : "";
+       if (!nonce) nonce = "";
 
        /* Use __FILE__, harmless_line_number for creating
         * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
@@ -674,9 +685,9 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
        // Now see if he or she has the right credentials.
        int passOK = -1;
        if(uname)
-               passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
+               passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password, nonce );
        else if (barcode)
-               passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password );
+               passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password, nonce );
 
        if( passOK < 0 ) {
                jsonObjectFree(userObj);