[open-ils-commits] [GIT] Evergreen ILS branch master updated. 5f9da3a7a4dfbe5ccc67e9d009050b76a34390cc

Evergreen Git git at git.evergreen-ils.org
Fri Feb 26 10:38:52 EST 2016


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  5f9da3a7a4dfbe5ccc67e9d009050b76a34390cc (commit)
       via  f9ca07d7e1b2b5dae919d844562318dbadd8cce8 (commit)
       via  aaffaf89d2216b6db90377e8ca0675aaab78ccab (commit)
       via  807dd95dffab3e8a25f167973a8d1c05ebf84b11 (commit)
       via  b55de021d866d2b08c1cccf3f14de57c8f70db2e (commit)
       via  c2680931f1a10a69df18a5d2bbd5719bf6ab5a0a (commit)
       via  fb350446a74434f1ea0d73524fe971dbfee7194e (commit)
       via  2f6429f258377c7cc74f634af6bfeff0803de70f (commit)
       via  79490cefc2a93154ce3835d23257d2f60c1534bc (commit)
       via  bc1de44899000f87905f68e5928f74dc5267ce2b (commit)
       via  c427559c208c173700d622610fe440225550eb54 (commit)
       via  493302324c114e2a84ce6f11b14c3247334a2d7a (commit)
       via  732b14aa25d7ba09763daf2734bdb18962aae15d (commit)
       via  be3e4e339a8de50edc112a267bff2f11d35de2ba (commit)
       via  bd14977ee5087c59ba720dba809d2c3bbb71b049 (commit)
       via  c29a4c0f703c705241982ddfff18c2e8ffb907a9 (commit)
       via  fb856f49257e21ab2c3190c0fd9b70db676b2f11 (commit)
       via  62de23eeca6f27c2deaacb28358b411444b7dbbc (commit)
       via  2e9b6b3f2126a0ba136388bf5cc2689428f78d63 (commit)
       via  5ddb3f61b5cf9c97cf5b45c42209a1083e8efff8 (commit)
       via  3cb50795de44054049611cc570bf87634583079e (commit)
       via  2e3689583d21f320d69c5f33049047e5c91e627b (commit)
       via  2c0c522c1380c797f597c2ca482e360fba8a5f8c (commit)
       via  722338616034d1be24603ef193cc775fbbe08307 (commit)
       via  08b06012de32a612a33fb2f73aecbf439c155035 (commit)
       via  5deb393c01d400a24f38a6e6324cc2181be3480a (commit)
       via  f47a980e1e19c3e90ebe3189be803a6841807e5f (commit)
      from  72a8c6a0004602e7eae6c1b3cfa4704df8e8618a (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 5f9da3a7a4dfbe5ccc67e9d009050b76a34390cc
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Feb 26 10:11:56 2016 -0500

    LP#1468422 Stamping DB upgrade for actor.passwd
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 12d93f9..eef5e65 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -91,7 +91,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0960', :eg_version); -- berick/kmlussier
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0961', :eg_version); -- berick/dbwells
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql b/Open-ILS/src/sql/Pg/upgrade/0961.schema.password-storage.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
rename to Open-ILS/src/sql/Pg/upgrade/0961.schema.password-storage.sql
index 309823b..0d7bb40 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0961.schema.password-storage.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('0961', :eg_version);
 
 CREATE EXTENSION IF NOT EXISTS pgcrypto;
 

commit f9ca07d7e1b2b5dae919d844562318dbadd8cce8
Author: Dan Wells <dbw2 at calvin.edu>
Date:   Tue Jan 26 14:22:21 2016 -0500

    LP#1468422 Use auth_internal.validate to shore up AuthProxy
    
    Even if a user has valid credentials in the external system, we should
    block them from logging in if their Evergreen account is out of sorts.
    Use the API designed for this.
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
index 94bb2d1..66a1b71 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
@@ -235,7 +235,38 @@ sub login {
             if (exists $event->{'payload'}) { # we have a complete native login
                 return $event;
             } else { # create an EG session for the successful external login
-                return &_create_session($args);
+                #
+                # before we actually create the session, let's first check if
+                # Evergreen thinks this user is allowed to login
+                #
+                # (we do this *after* authentication to avoid any personal data
+                # leakage)
+
+                # get the user id
+                my $user = $U->cstorereq(
+                    "open-ils.cstore.direct.actor.user.search.atomic",
+                    { usrname => $args->{'username'} }
+                );
+                if (!$user->[0]) {
+                    $logger->debug("Authenticated username '" . $args->{'username'} . "' has no Evergreen account, aborting");
+                    return OpenILS::Event->new( 'LOGIN_FAILED' );
+                } else {
+                    $args->{user_id} = $user->[0]->id;
+                }
+
+                # validate the account
+                my $trimmed_args = {
+                    user_id => $args->{user_id},
+                    login_type => $args->{type},
+                    org_unit => $args->{org}
+                };
+                $event = &_auth_internal('user.validate', $trimmed_args);
+                if ($U->event_code($event)) { # non-zero = we didn't succeed
+                    # can't recover from invalid user, return right away
+                    return $event;
+                } else { # it's all good
+                    return &_auth_internal('session.create', $trimmed_args);
+                }
             }
         }
     }
@@ -249,27 +280,12 @@ sub login {
     return OpenILS::Event->new( 'LOGIN_FAILED' );
 }
 
-sub _create_session {
-    my $args = shift;
-
-    my $user = $U->cstorereq(
-        "open-ils.cstore.direct.actor.user.search.atomic",
-        { usrname => $args->{'username'} }
-    );
-    if (!$user->[0]) {
-        $logger->debug("Authenticated username '" . $args->{'username'} . "' has no Evergreen account, aborting");
-        return OpenILS::Event->new( 'LOGIN_FAILED' );
-    } else {
-        $args->{user_id} = $user->[0]->id;
-    }
+sub _auth_internal {
+    my ($method, $args) = @_;
 
     my $response = OpenSRF::AppSession->create("open-ils.auth_internal")->request(
-        'open-ils.auth_internal.session.create',
-        {
-            user_id => $args->{user_id},
-            login_type => $args->{type},
-            org_unit => $args->{org}
-        }
+        'open-ils.auth_internal.'.$method,
+        $args
     )->gather(1);
 
     return OpenILS::Event->new( 'LOGIN_FAILED' )

commit aaffaf89d2216b6db90377e8ca0675aaab78ccab
Author: Dan Wells <dbw2 at calvin.edu>
Date:   Tue Jan 26 13:36:14 2016 -0500

    LP#1468422 "Aut" to "Auth" typo fix
    
    Just cosmetic for now, but potentially for future sanity protection.
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/c-apps/oils_auth_internal.c b/Open-ILS/src/c-apps/oils_auth_internal.c
index c7621b1..8f43e13 100644
--- a/Open-ILS/src/c-apps/oils_auth_internal.c
+++ b/Open-ILS/src/c-apps/oils_auth_internal.c
@@ -46,7 +46,7 @@ int osrfAppInitialize() {
     osrfAppRegisterMethod(
         MODULENAME,
         "open-ils.auth_internal.session.create",
-        "oilsAutInternalCreateSession",
+        "oilsAuthInternalCreateSession",
         "Adds a user to the authentication cache to indicate "
         "the user is authenticated", 1, 0 
     );
@@ -54,7 +54,7 @@ int osrfAppInitialize() {
     osrfAppRegisterMethod(
         MODULENAME,
         "open-ils.auth_internal.user.validate",
-        "oilsAutInternalValidate",
+        "oilsAuthInternalValidate",
         "Determines whether a user should be allowed to login.  " 
         "Returns SUCCESS oilsEvent when the user is valid, otherwise "
         "returns a non-SUCCESS oilsEvent object", 1, 0
@@ -272,7 +272,7 @@ static oilsEvent* oilsAuthCheckLoginPerm(osrfMethodContext* ctx,
         - "workstation" -- workstation name
 
 */
-int oilsAutInternalCreateSession(osrfMethodContext* ctx) {
+int oilsAuthInternalCreateSession(osrfMethodContext* ctx) {
     OSRF_METHOD_VERIFY_CONTEXT(ctx);
 
     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
@@ -365,7 +365,7 @@ int oilsAutInternalCreateSession(osrfMethodContext* ctx) {
 }
 
 
-int oilsAutInternalValidate(osrfMethodContext* ctx) {
+int oilsAuthInternalValidate(osrfMethodContext* ctx) {
     OSRF_METHOD_VERIFY_CONTEXT(ctx);
 
     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);

commit 807dd95dffab3e8a25f167973a8d1c05ebf84b11
Author: Dan Wells <dbw2 at calvin.edu>
Date:   Tue Jan 5 14:24:21 2016 -0500

    LP#1468422 Make AuthProxy.pm work with new auth
    
    Previously, AuthProxy.pm would simply lookup and use the hashed password
    when the external authentication had passed.  This simple method no
    longer works, since even cstore doesn't have access to the hashed
    password.
    
    Instead, take advantage of the new 'auth_internal' service to create the
    user session after the user has been externally authenticated.
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
index 9ca5ea5..94bb2d1 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
@@ -234,8 +234,8 @@ sub login {
         } elsif (defined $code) { # code is '0', i.e. SUCCESS
             if (exists $event->{'payload'}) { # we have a complete native login
                 return $event;
-            } else { # do a 'forced' login
-                return &_do_login($args, 1);
+            } else { # create an EG session for the successful external login
+                return &_create_session($args);
             }
         }
     }
@@ -249,6 +249,35 @@ sub login {
     return OpenILS::Event->new( 'LOGIN_FAILED' );
 }
 
+sub _create_session {
+    my $args = shift;
+
+    my $user = $U->cstorereq(
+        "open-ils.cstore.direct.actor.user.search.atomic",
+        { usrname => $args->{'username'} }
+    );
+    if (!$user->[0]) {
+        $logger->debug("Authenticated username '" . $args->{'username'} . "' has no Evergreen account, aborting");
+        return OpenILS::Event->new( 'LOGIN_FAILED' );
+    } else {
+        $args->{user_id} = $user->[0]->id;
+    }
+
+    my $response = OpenSRF::AppSession->create("open-ils.auth_internal")->request(
+        'open-ils.auth_internal.session.create',
+        {
+            user_id => $args->{user_id},
+            login_type => $args->{type},
+            org_unit => $args->{org}
+        }
+    )->gather(1);
+
+    return OpenILS::Event->new( 'LOGIN_FAILED' )
+      unless $response;
+
+    return $response;
+}
+
 sub _do_login {
     my $args = shift;
     my $authenticated = shift;
@@ -262,22 +291,7 @@ sub _do_login {
       unless $seed;
 
     my $real_password = $args->{'password'};
-    # if we have already authenticated, look up the password needed to finish
-    if ($authenticated) {
-        # username is required
-        return OpenILS::Event->new( 'LOGIN_FAILED' ) if !$args->{'username'};
-        my $user = $U->cstorereq(
-            "open-ils.cstore.direct.actor.user.search.atomic",
-            { usrname => $args->{'username'} }
-        );
-        if (!$user->[0]) {
-            $logger->debug("Authenticated username '" . $args->{'username'} . "' has no Evergreen account, aborting");
-            return OpenILS::Event->new( 'LOGIN_FAILED' );
-        }
-        $args->{'password'} = md5_hex( $seed . $user->[0]->passwd );
-    } else {
-        $args->{'password'} = md5_hex( $seed . md5_hex($real_password) );
-    }
+    $args->{'password'} = md5_hex( $seed . md5_hex($real_password) );
     my $response = OpenSRF::AppSession->create("open-ils.auth")->request(
         'open-ils.auth.authenticate.complete',
         $args

commit b55de021d866d2b08c1cccf3f14de57c8f70db2e
Author: Dan Wells <dbw2 at calvin.edu>
Date:   Tue Jan 5 14:21:11 2016 -0500

    LP#1468422 Tighten AuthProxy argument requirements
    
    Basically, if we aren't given a username, and we can't find a username
    by barcode, give up immediately.  This helps simplify the rest of the
    code a bit.
    
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
index 023dbb9..9ca5ea5 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AuthProxy.pm
@@ -185,18 +185,19 @@ sub login {
             {flesh => 1, flesh_fields => {ac => ['usr']}}
         ])->[0];
 
-        $args->{username} = $card->usr->usrname if $card;
+        if ($card) {
+            $args->{username} = $card->usr->usrname;
+        } else { # must have or resolve to a username
+            return OpenILS::Event->new( 'LOGIN_FAILED' );
+        }
     }
 
     # check for possibility of brute-force attack
-    my $fail_count;
-    if ($args->{'username'}) {
-        $fail_count = $cache->get_cache('oils_auth_' . $args->{'username'} . '_count') || 0;
-        if ($fail_count >= $block_count) {
-            $logger->debug("AuthProxy found too many recent failures for '" . $args->{'username'} . "' : $fail_count, forcing failure state.");
-            $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
-            return OpenILS::Event->new( 'LOGIN_FAILED' );
-        }
+    my $fail_count = $cache->get_cache('oils_auth_' . $args->{'username'} . '_count') || 0;
+    if ($fail_count >= $block_count) {
+        $logger->debug("AuthProxy found too many recent failures for '" . $args->{'username'} . "' : $fail_count, forcing failure state.");
+        $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
+        return OpenILS::Event->new( 'LOGIN_FAILED' );
     }
 
     my @error_events;
@@ -241,7 +242,7 @@ sub login {
 
     # if we got this far, we failed
     # increment the brute force counter if 'native' didn't already
-    if ($args->{'username'} and !exists $authenticators_by_name{'native'}) {
+    if (!exists $authenticators_by_name{'native'}) {
         $cache->put_cache('oils_auth_' . $args->{'username'} . '_count', ++$fail_count, $block_timeout);
     }
     # TODO: send back some form of collected error events

commit c2680931f1a10a69df18a5d2bbd5719bf6ab5a0a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Feb 18 10:42:11 2016 -0500

    LP#1468422 Release notes example avoid re-migrate
    
    Update the batch password migrate example code in the release notes to
    avoid attempts at migrating already migrated passwords.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc b/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc
index 02e6c0a..bb6206c 100644
--- a/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc
+++ b/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc
@@ -16,14 +16,21 @@ password migration for a given user via a database function:
 
 [source,sql]
 ------------------------------------------------------------
-evergreen=# SELECT actor.migrate_passwd(<USER_ID>);
+-- actor.migrate_passwd() will only migrate un-migrated 
+-- accounts, but it's faster to avoid any re-migration attempts.
+SELECT actor.migrate_passwd(au.id)
+FROM actor.usr au
+    LEFT JOIN actor.passwd pw ON (pw.usr = au.id)
+WHERE pw.usr IS NULL; 
 ------------------------------------------------------------
 
 Using this, admins could perform manual batch updates to force all
 users to use the new, more secure passwords, regardless of when or
-whether a patron logs back into the system.  Beware that doing this
-for all users in the a large database will take a long time and
-should proably be performed in batches.
+whether a patron logs back into the system.  
+
+Beware that doing this for all users in the a large database will 
+take some time and should proably be performed in batches.  (On 
+Bill's test VM it took 14 seconds to migrate 233 users).
 
 open-ils.auth_internal
 ++++++++++++++++++++++

commit fb350446a74434f1ea0d73524fe971dbfee7194e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Feb 18 10:28:46 2016 -0500

    LP#1468422 Manual password migrate does not re-migrate
    
    Avoid migrating already-migrated passwords when actor.migrate_passwd()
    is called manually.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql
index 912b2fc..da94af0 100644
--- a/Open-ILS/src/sql/Pg/005.schema.actors.sql
+++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql
@@ -965,6 +965,16 @@ BEGIN
      * hashing is not required of other passwords.
      */
 
+    -- Avoid calling get_salt() here, because it may result in a 
+    -- migrate_passwd() call, creating a loop.
+    SELECT INTO pw_salt salt FROM actor.passwd 
+        WHERE usr = pw_usr AND passwd_type = 'main';
+
+    -- Only migrate passwords that have not already been migrated.
+    IF FOUND THEN
+        RETURN pw_salt;
+    END IF;
+
     SELECT INTO usr_row * FROM actor.usr WHERE id = pw_usr;
 
     pw_salt := actor.create_salt('main');
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
index 343eb39..309823b 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
@@ -157,6 +157,16 @@ BEGIN
      * hashing is not required of other passwords.
      */
 
+    -- Avoid calling get_salt() here, because it may result in a 
+    -- migrate_passwd() call, creating a loop.
+    SELECT INTO pw_salt salt FROM actor.passwd 
+        WHERE usr = pw_usr AND passwd_type = 'main';
+
+    -- Only migrate passwords that have not already been migrated.
+    IF FOUND THEN
+        RETURN pw_salt;
+    END IF;
+
     SELECT INTO usr_row * FROM actor.usr WHERE id = pw_usr;
 
     pw_salt := actor.create_salt('main');

commit 2f6429f258377c7cc74f634af6bfeff0803de70f
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Feb 17 17:33:39 2016 -0500

    LP#1468422 Release notes manual pw migration comments
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc b/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc
index 01b9ae3..02e6c0a 100644
--- a/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc
+++ b/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc
@@ -7,6 +7,24 @@ the application layer.
 All API changes are backwards compatible with existing 3rd-party
 clients.
 
+Migrating Passwords
++++++++++++++++++++
+
+Passwords are migrated for each user automatically the first time a user
+logs in under the new setup.  However, it is also possible to force
+password migration for a given user via a database function:
+
+[source,sql]
+------------------------------------------------------------
+evergreen=# SELECT actor.migrate_passwd(<USER_ID>);
+------------------------------------------------------------
+
+Using this, admins could perform manual batch updates to force all
+users to use the new, more secure passwords, regardless of when or
+whether a patron logs back into the system.  Beware that doing this
+for all users in the a large database will take a long time and
+should proably be performed in batches.
+
 open-ils.auth_internal
 ++++++++++++++++++++++
 To support the new storage mechanism, a new Evergreen service has

commit 79490cefc2a93154ce3835d23257d2f60c1534bc
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Feb 17 17:23:34 2016 -0500

    LP#1468422 Drop auth work factor from 14 to 10
    
    Avoid what may be an unacceptible login delay caused by work factor 14
    by dropping down to 10.  This reduces the CRYPT() time from ~1 second to
    ~.1 seconds.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index dbea260..a6cc668 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -2440,7 +2440,7 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
 -- Admin user account
 INSERT INTO actor.passwd_type 
     (code, name, login, crypt_algo, iter_count) 
-    VALUES ('main', 'Main Login Password', TRUE, 'bf', 14);
+    VALUES ('main', 'Main Login Password', TRUE, 'bf', 10);
 
 INSERT INTO actor.usr ( profile, card, usrname, passwd, first_given_name, family_name, dob, master_account, super_user, ident_type, ident_value, home_ou ) VALUES ( 1, 1, md5(random()::text), md5(random()::text), 'Administrator', 'System Account', '1979-01-22', TRUE, TRUE, 1, 'identification', 1 );
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
index 1c642b6..343eb39 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
@@ -214,6 +214,6 @@ $$ STRICT LANGUAGE PLPGSQL;
 
 INSERT INTO actor.passwd_type 
     (code, name, login, crypt_algo, iter_count) 
-    VALUES ('main', 'Main Login Password', TRUE, 'bf', 14);
+    VALUES ('main', 'Main Login Password', TRUE, 'bf', 10);
 
 COMMIT;

commit bc1de44899000f87905f68e5928f74dc5267ce2b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Feb 1 10:15:23 2016 -0500

    LP#1468422 Always default to root org unit
    
    If no org unit is passed by the caller, always default to the root org
    unit.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/c-apps/oils_auth_internal.c b/Open-ILS/src/c-apps/oils_auth_internal.c
index c64a6c0..c7621b1 100644
--- a/Open-ILS/src/c-apps/oils_auth_internal.c
+++ b/Open-ILS/src/c-apps/oils_auth_internal.c
@@ -278,15 +278,19 @@ int oilsAutInternalCreateSession(osrfMethodContext* ctx) {
     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
 
     const char* user_id     = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
-    const char* org_unit    = jsonObjectGetString(jsonObjectGetKeyConst(args, "org_unit"));
     const char* login_type  = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
+    int org_unit            = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org_unit"));
 
-    if ( !(user_id && login_type && org_unit) ) {
+    if ( !(user_id && login_type) ) {
         return osrfAppRequestRespondException( ctx->session, ctx->request,
             "Missing parameters for method: %s", ctx->method->name );
     }
 
+    // default to the root org unit if none is provided.
+    if (org_unit < 1) 
+        org_unit = oilsUtilsGetRootOrgId();
+
     oilsEvent* response = NULL;
 
     // fetch the user object
@@ -318,7 +322,7 @@ int oilsAutInternalCreateSession(osrfMethodContext* ctx) {
     }
 
     // determine the auth/cache timeout
-    long timeout = oilsAuthGetTimeout(userObj, login_type, atoi(org_unit));
+    long timeout = oilsAuthGetTimeout(userObj, login_type, org_unit);
 
     char* string = va_list_to_string("%d.%ld.%ld", 
         (long) getpid(), time(NULL), oilsFMGetObjectId(userObj));
@@ -368,14 +372,18 @@ int oilsAutInternalValidate(osrfMethodContext* ctx) {
 
     const char* user_id     = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
-    const char* org_unit    = jsonObjectGetString(jsonObjectGetKeyConst(args, "org_unit"));
     const char* login_type  = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
+    int org_unit            = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org_unit"));
 
-    if ( !(user_id && login_type && org_unit) ) {
+    if ( !(user_id && login_type) ) {
         return osrfAppRequestRespondException( ctx->session, ctx->request,
             "Missing parameters for method: %s", ctx->method->name );
     }
 
+    // default to the root org unit if none is provided.
+    if (org_unit < 1) 
+        org_unit = oilsUtilsGetRootOrgId();
+
     oilsEvent* response = NULL;
     jsonObject *userObj = NULL, *params = NULL;
     char* tmp_str = NULL;
@@ -441,7 +449,7 @@ int oilsAutInternalValidate(osrfMethodContext* ctx) {
     if (!response) { // Still OK
         // Confirm user has permission to login w/ the requested type.
         response = oilsAuthCheckLoginPerm(
-            ctx, atoi(user_id), atoi(org_unit), login_type);
+            ctx, atoi(user_id), org_unit, login_type);
     }
 
 

commit c427559c208c173700d622610fe440225550eb54
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jan 27 12:11:33 2016 -0500

    LP#1468422 Auth efficiency improvements
    
    1. Adds an oils_utils function for retrieving the ID of the root org
    unit.
    
    2. Avoid multiple cstore/db lookups for the root org unit by caching the
    ID at the process level.
    
    3. Move permission checks from open-ils.storage to open-ils.cstore.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/include/openils/oils_utils.h b/Open-ILS/include/openils/oils_utils.h
index 3554d21..6b391f2 100644
--- a/Open-ILS/include/openils/oils_utils.h
+++ b/Open-ILS/include/openils/oils_utils.h
@@ -110,6 +110,11 @@ long oilsUtilsIntervalToSeconds( const char* interval );
  */
 int oilsUtilsTrackUserActivity( long usr, const char* ewho, const char* ewhat, const char* ehow );
 
+/**
+ * Returns the ID of the root org unit (parent_ou = NULL)
+ */
+int oilsUtilsGetRootOrgId();
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/Open-ILS/src/c-apps/oils_auth.c b/Open-ILS/src/c-apps/oils_auth.c
index 22cf96e..fd9b5e7 100644
--- a/Open-ILS/src/c-apps/oils_auth.c
+++ b/Open-ILS/src/c-apps/oils_auth.c
@@ -319,18 +319,10 @@ int oilsAuthInitBarcode(osrfMethodContext* ctx) {
 // returns true if the provided identifier matches the barcode regex.
 static int oilsAuthIdentIsBarcode(const char* identifier) {
 
-    // before we can fetch the barcode regex unit setting,
-    // first determine what the root org unit ID is.
+    // Assumes barcode regex is a global setting.
     // TODO: add an org_unit param to the .init API for future use?
-    
-    jsonObject *params = jsonParse("{\"parent_ou\":null}");
-    jsonObject *org_unit_id = oilsUtilsCStoreReq(
-        "open-ils.cstore.direct.actor.org_unit.id_list", params);
-    jsonObjectFree(params);
-
     char* bc_regex = oilsUtilsFetchOrgSetting(
-        (int) jsonObjectGetNumber(org_unit_id), "opac.barcode_regex");
-    jsonObjectFree(org_unit_id);
+        oilsUtilsGetRootOrgId(), "opac.barcode_regex");
 
     if (!bc_regex) {
         // if no regex is set, assume any identifier starting
diff --git a/Open-ILS/src/c-apps/oils_utils.c b/Open-ILS/src/c-apps/oils_utils.c
index cb334ac..155274f 100644
--- a/Open-ILS/src/c-apps/oils_utils.c
+++ b/Open-ILS/src/c-apps/oils_utils.c
@@ -164,49 +164,65 @@ int oilsUtilsTrackUserActivity(long usr, const char* ewho, const char* ewhat, co
 }
 
 
+static int rootOrgId = 0; // cache the ID of the root org unit.
+int oilsUtilsGetRootOrgId() {
+
+    // return the cached value if we have it.
+    if (rootOrgId > 0) return rootOrgId;
+
+    jsonObject* where_clause = jsonParse("{\"parent_ou\":null}");
+    jsonObject* org = oilsUtilsQuickReq(
+        "open-ils.cstore",
+        "open-ils.cstore.direct.actor.org_unit.search",
+        where_clause
+    );
+
+    rootOrgId = (int) 
+        jsonObjectGetNumber(oilsFMGetObject(org, "id"));
+
+    jsonObjectFree(where_clause);
+    jsonObjectFree(org);
+
+    return rootOrgId;
+}
 
 oilsEvent* oilsUtilsCheckPerms( int userid, int orgid, char* permissions[], int size ) {
-	if (!permissions) return NULL;
-	int i;
-	oilsEvent* evt = NULL;
-
-	// Find the root org unit, i.e. the one with no parent.
-	// Assumption: there is only one org unit with no parent.
-	if (orgid == -1) {
-		jsonObject* where_clause = jsonParse( "{\"parent_ou\":null}" );
-		jsonObject* org = oilsUtilsQuickReq(
-			"open-ils.cstore",
-			"open-ils.cstore.direct.actor.org_unit.search",
-			where_clause
-		);
-		jsonObjectFree( where_clause );
-
-		orgid = (int)jsonObjectGetNumber( oilsFMGetObject( org, "id" ) );
-
-		jsonObjectFree(org);
-	}
+    if (!permissions) return NULL;
+    int i;
 
-	for( i = 0; i < size && permissions[i]; i++ ) {
+    // Check perms against the root org unit if no org unit is provided.
+    if (orgid == -1)
+        orgid = oilsUtilsGetRootOrgId();
 
-		char* perm = permissions[i];
-		jsonObject* params = jsonParseFmt("[%d, \"%s\", %d]", userid, perm, orgid);
-		jsonObject* o = oilsUtilsQuickReq( "open-ils.storage",
-			"open-ils.storage.permission.user_has_perm", params );
+    for( i = 0; i < size && permissions[i]; i++ ) {
+        oilsEvent* evt = NULL;
+        char* perm = permissions[i];
 
-		char* r = jsonObjectToSimpleString(o);
+        jsonObject* params = jsonParseFmt(
+            "{\"from\":[\"permission.usr_has_perm\",\"%d\",\"%s\",\"%d\"]}",
+            userid, perm, orgid
+        );
 
-		if(r && !strcmp(r, "0"))
-			evt = oilsNewEvent3( OSRF_LOG_MARK, OILS_EVENT_PERM_FAILURE, perm, orgid );
+        // Execute the query
+        jsonObject* result = oilsUtilsCStoreReq(
+            "open-ils.cstore.json_query", params);
 
-		jsonObjectFree(params);
-		jsonObjectFree(o);
-		free(r);
+        const jsonObject* hasPermStr = 
+            jsonObjectGetKeyConst(result, "permission.usr_has_perm");
 
-		if(evt)
-			break;
-	}
+        if (!oilsUtilsIsDBTrue(jsonObjectGetString(hasPermStr))) {
+            evt = oilsNewEvent3(
+                OSRF_LOG_MARK, OILS_EVENT_PERM_FAILURE, perm, orgid);
+        }
+
+        jsonObjectFree(params);
+        jsonObjectFree(result);
+
+        // return first failed permission check.
+        if (evt) return evt;
+    }
 
-	return evt;
+    return NULL; // all perm checks succeeded
 }
 
 /**

commit 493302324c114e2a84ce6f11b14c3247334a2d7a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jan 27 11:46:17 2016 -0500

    LP#1468422 Login permission checks are global
    
    For backwards compat, perform all login permission checks using the root
    org unit as the context org unit.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/c-apps/oils_auth_internal.c b/Open-ILS/src/c-apps/oils_auth_internal.c
index a6dba09..c64a6c0 100644
--- a/Open-ILS/src/c-apps/oils_auth_internal.c
+++ b/Open-ILS/src/c-apps/oils_auth_internal.c
@@ -232,6 +232,10 @@ static oilsEvent* oilsAuthVerifyWorkstation(
 static oilsEvent* oilsAuthCheckLoginPerm(osrfMethodContext* ctx, 
     int user_id, int org_id, const char* type ) {
 
+    // For backwards compatibility, check all login permissions 
+    // using the root org unit as the context org unit.
+    org_id = -1;
+
     char* perms[1];
 
     if (!strcasecmp(type, OILS_AUTH_OPAC)) {

commit 732b14aa25d7ba09763daf2734bdb18962aae15d
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Jan 26 14:16:06 2016 -0500

    LP#1468422 Return vanilla login failure on nonexistent username/barcode
    
    For backwards compatibility (and security), return the same login
    failure for nonexistent usernames/barcodes as for bad passwords, etc.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/c-apps/oils_auth.c b/Open-ILS/src/c-apps/oils_auth.c
index cc42f47..22cf96e 100644
--- a/Open-ILS/src/c-apps/oils_auth.c
+++ b/Open-ILS/src/c-apps/oils_auth.c
@@ -195,7 +195,13 @@ static char* oilsAuthBuildInitCache(
     char* count_key = va_list_to_string(
         "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX);
 
-    char* auth_seed = oilsAuthGetSalt(user_id);
+    char* auth_seed;
+    if (user_id == -1) {
+        // user does not exist.  Use a dummy seed
+        auth_seed = strdup("x");
+    } else {
+        auth_seed = oilsAuthGetSalt(user_id);
+    }
 
     jsonObject* seed_object = jsonParseFmt(
         "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
@@ -207,7 +213,12 @@ static char* oilsAuthBuildInitCache(
     }
 
     osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
-    osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
+
+    if (user_id != -1) {
+        // Only track login counts for existing users, since a 
+        // login for a nonexistent user will never succeed anyway.
+        osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
+    }
 
     osrfLogDebug(OSRF_LOG_MARK, 
         "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
@@ -226,26 +237,18 @@ static int oilsAuthInitUsernameHandler(
     osrfLogInfo(OSRF_LOG_MARK, 
         "User logging in with username %s", username);
 
+    int user_id = -1;
     jsonObject* resp = NULL; // free
     jsonObject* user_obj = oilsUtilsFetchUserByUsername(username); // free
 
-    if (user_obj) {
-
-        if (JSON_NULL == user_obj->type) { // user not found
-            resp = jsonNewObject("x");
+    if (user_obj && user_obj->type != JSON_NULL) 
+        user_id = oilsFMGetObjectId(user_obj);
 
-        } else {
-            char* seed = oilsAuthBuildInitCache(
-                oilsFMGetObjectId(user_obj), username, "username", nonce);
-            resp = jsonNewObject(seed);
-            free(seed);
-        }
-
-        jsonObjectFree(user_obj);
+    jsonObjectFree(user_obj); // NULL OK
 
-    } else {
-        resp = jsonNewObject("x");
-    }
+    char* seed = oilsAuthBuildInitCache(user_id, username, "username", nonce);
+    resp = jsonNewObject(seed);
+    free(seed);
 
     osrfAppRespondComplete(ctx, resp);
     jsonObjectFree(resp);
@@ -276,23 +279,18 @@ static int oilsAuthInitBarcodeHandler(
     osrfLogInfo(OSRF_LOG_MARK, 
         "User logging in with barcode %s", barcode);
 
+    int user_id = -1;
     jsonObject* resp = NULL; // free
     jsonObject* user_obj = oilsUtilsFetchUserByBarcode(barcode); // free
 
-    if (user_obj) {
-        if (JSON_NULL == user_obj->type) { // not found
-            resp = jsonNewObject("x");
-        } else {
-            char* seed = oilsAuthBuildInitCache(
-                oilsFMGetObjectId(user_obj), barcode, "barcode", nonce);
-            resp = jsonNewObject(seed);
-            free(seed);
-        }
+    if (user_obj && user_obj->type != JSON_NULL) 
+        user_id = oilsFMGetObjectId(user_obj);
 
-        jsonObjectFree(user_obj);
-    } else {
-        resp = jsonNewObject("x");
-    }
+    jsonObjectFree(user_obj); // NULL OK
+
+    char* seed = oilsAuthBuildInitCache(user_id, barcode, "barcode", nonce);
+    resp = jsonNewObject(seed);
+    free(seed);
 
     osrfAppRespondComplete(ctx, resp);
     jsonObjectFree(resp);
@@ -630,6 +628,17 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
     int user_id = jsonObjectGetNumber(
         jsonObjectGetKeyConst(cacheObj, "user_id"));
 
+    if (user_id == -1) {
+        // User was not found during init.  Clean up and exit early.
+        response = oilsNewEvent(
+            __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
+        osrfAppRespondComplete(ctx, oilsEventToJSON(response));
+        oilsEventFree(response); // frees event JSON
+        osrfCacheRemove(cache_key);
+        jsonObjectFree(cacheObj);
+        return 0;
+    }
+
     jsonObject* param = jsonNewNumberObject(user_id); // free
     userObj = oilsUtilsCStoreReq(
         "open-ils.cstore.direct.actor.user.retrieve", param);
@@ -747,6 +756,7 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
     oilsEventFree(response);
     jsonObjectFree(userObj);
     jsonObjectFree(authEvt);
+    jsonObjectFree(cacheObj);
     if(freeable_uname)
         free(freeable_uname);
 

commit be3e4e339a8de50edc112a267bff2f11d35de2ba
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Jan 11 11:02:23 2016 -0500

    LP#1468422 Report inactive card on password OK
    
    Prevent leaking information from authentication by only reporting that a
    card is inactive if the caller provided the correct credentials.  This
    is consistent with how the code handles inactive patrons.
    
    To avoid a lot of code duplication and to reduce the potential for
    leaking memory (C code, amiright?), this commit includes a number of
    changes to avoid exiting the API function early and saving the memory
    cleanup routines until the end of the API call.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/c-apps/oils_auth.c b/Open-ILS/src/c-apps/oils_auth.c
index 5486f3e..cc42f47 100644
--- a/Open-ILS/src/c-apps/oils_auth.c
+++ b/Open-ILS/src/c-apps/oils_auth.c
@@ -647,6 +647,7 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
+    if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
 
     jsonObject* authEvt = oilsUtilsQuickReq( // freed after password test
         "open-ils.auth_internal",
@@ -661,78 +662,93 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
         return -1;
     }
 
-    const char* evtCode = 
+    const char* authEvtCode = 
         jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
 
-    // For security/privacy sake, only report that a patron is 
-    // inactive if the correct password is provided below.
-    int user_inactive = !strcmp(evtCode, "PATRON_INACTIVE");
+    if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
+        // Received the generic login failure event.
 
-    if (strcmp(evtCode, "SUCCESS") && !user_inactive) { // validate failed
         osrfLogInfo(OSRF_LOG_MARK,  
             "failed login: username=%s, barcode=%s, workstation=%s",
             uname, (barcode ? barcode : "(none)"), ws);
-        osrfAppRespondComplete(ctx, authEvt);
-        jsonObjectFree(authEvt);
-        jsonObjectFree(userObj);
-        if(freeable_uname) free(freeable_uname);
-        return 0;           // No such user
+
+        response = oilsNewEvent(
+            __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
     }
 
-	// Such a user exists and isn't barred or deleted.
-	// Now see if he or she has the right credentials.
-	int passOK = oilsAuthVerifyPassword(
-        ctx, user_id, identifier, password, nonce);
+    int passOK = 0;
+    
+    if (!response) {
+        // User exists and is not barred, etc.  Test the password.
 
-	if( passOK < 0 ) {
-        jsonObjectFree(authEvt);
-		jsonObjectFree(userObj);
-        if(freeable_uname) free(freeable_uname);
-		return passOK;
-	}
+        passOK = oilsAuthVerifyPassword(
+            ctx, user_id, identifier, password, nonce);
 
-    if (passOK && user_inactive) {
-        // Patron is inactive but provided the correct password.
-        // Return the original PATRON_INACTIVE event.
-        osrfAppRespondComplete(ctx, authEvt);
-        jsonObjectFree(authEvt);
-        jsonObjectFree(userObj);
-        if(freeable_uname) free(freeable_uname);
-        return 0;
+        if (!passOK) {
+            // Password check failed. Return generic login failure.
+
+            response = oilsNewEvent(
+                __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
+
+            osrfLogInfo(OSRF_LOG_MARK,  
+                "failed login: username=%s, barcode=%s, workstation=%s",
+                    uname, (barcode ? barcode : "(none)"), ws );
+        }
+    }
+
+
+    // Below here, we know the password check succeeded if no response
+    // object is present.
+
+    if (!response && (
+        !strcmp(authEvtCode, "PATRON_INACTIVE") ||
+        !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
+        // Patron and/or card is inactive but the correct password 
+        // was provided.  Alert the caller to the inactive-ness.
+        response = oilsNewEvent2(
+            OSRF_LOG_MARK, authEvtCode,
+            jsonObjectGetKey(authEvt, "payload")   // cloned within Event
+        );
     }
 
-    jsonObjectFree(authEvt); // we're all done with this now.
+    if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
+        // Validate API returned an unexpected non-success event.
+        // To be safe, treat this as a generic login failure.
 
-	if( passOK ) { // login successful  
-        
-		char* ewhat = "login";
+        response = oilsNewEvent(
+            __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
+    }
 
-		if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
-			response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
-			ewhat = "verify";
+    if (!response) {
+        // password OK and no other events have prevented login completion.
 
-		} else {
-			response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
-		}
+        char* ewhat = "login";
 
-		oilsUtilsTrackUserActivity(
-			oilsFMGetObjectId(userObj), 
-			ewho, ewhat, 
-			osrfAppSessionGetIngress()
-		);
+        if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
+            response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
+            ewhat = "verify";
 
-	} else {
-		response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
-		osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
-				uname, (barcode ? barcode : "(none)"), ws );
-	}
+        } else {
+            response = oilsAuthHandleLoginOK(
+                userObj, uname, type, orgloc, workstation);
+        }
+
+        oilsUtilsTrackUserActivity(
+            oilsFMGetObjectId(userObj), 
+            ewho, ewhat, 
+            osrfAppSessionGetIngress()
+        );
+    }
 
-	jsonObjectFree(userObj);
-	osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
-	oilsEventFree(response);
+    // reply
+    osrfAppRespondComplete(ctx, oilsEventToJSON(response));
 
-	if(freeable_uname)
-		free(freeable_uname);
+    // clean up
+    oilsEventFree(response);
+    jsonObjectFree(userObj);
+    jsonObjectFree(authEvt);
+    if(freeable_uname)
+        free(freeable_uname);
 
 	return 0;
 }

commit bd14977ee5087c59ba720dba809d2c3bbb71b049
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jan 8 15:06:14 2016 -0500

    LP#1468422 auth-internal validate API
    
    Adds a new open-ils.auth_internal API
    open-ils.auth_internal.user.validate for checking whether a user should
    be allowed to login.
    
    It tests user existence, active=true, barred=false, deleted=false.
    
    If a barcode is also provided, it confirms the barcode exists and is
    active.
    
    Modifies open-ils.auth.authenticate.complete to use the new API instead
    of implementing the logic directly.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/c-apps/oils_auth.c b/Open-ILS/src/c-apps/oils_auth.c
index b9998b5..5486f3e 100644
--- a/Open-ILS/src/c-apps/oils_auth.c
+++ b/Open-ILS/src/c-apps/oils_auth.c
@@ -417,44 +417,6 @@ int oilsAuthInit(osrfMethodContext* ctx) {
 }
 
 /**
-	Verifies that the user has permission to login with the
-	given type.  If the permission fails, an oilsEvent is returned
-	to the caller.
-	@return -1 if the permission check failed, 0 if the permission
-	is granted
-*/
-static int oilsAuthCheckLoginPerm(
-		osrfMethodContext* ctx, const jsonObject* userObj, const char* type ) {
-
-	if(!(userObj && type)) return -1;
-	oilsEvent* perm = NULL;
-
-	if(!strcasecmp(type, OILS_AUTH_OPAC)) {
-		char* permissions[] = { "OPAC_LOGIN" };
-		perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
-
-	} else if(!strcasecmp(type, OILS_AUTH_STAFF)) {
-		char* permissions[] = { "STAFF_LOGIN" };
-		perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
-
-	} else if(!strcasecmp(type, OILS_AUTH_TEMP)) {
-		char* permissions[] = { "STAFF_LOGIN" };
-		perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
-	} else if(!strcasecmp(type, OILS_AUTH_PERSIST)) {
-		char* permissions[] = { "PERSISTENT_LOGIN" };
-		perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
-	}
-
-	if(perm) {
-		osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
-		oilsEventFree(perm);
-		return -1;
-	}
-
-	return 0;
-}
-
-/**
 	Returns 1 if the password provided matches the user's real password
 	Returns 0 otherwise
 	Returns -1 on error
@@ -652,7 +614,6 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
     oilsEvent* response = NULL; // free
     jsonObject* userObj = NULL; // free
     int card_active = 1; // boolean; assume active until proven otherwise
-    int using_card  = 0; // true if this is a barcode login
 
     char* cache_key = va_list_to_string(
         "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
@@ -674,47 +635,49 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
         "open-ils.cstore.direct.actor.user.retrieve", param);
     jsonObjectFree(param);
 
-    using_card = (jsonObjectGetKeyConst(cacheObj, "barcode") != NULL);
+    char* freeable_uname = NULL;
+    if (!uname) {
+        uname = freeable_uname = oilsFMGetString(userObj, "usrname");
+    }
 
-    if (using_card) {
-        // see if the card is inactive
+    // See if the user is allowed to login.
 
-		jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", identifier);
-		jsonObject* card = oilsUtilsCStoreReq(
-			"open-ils.cstore.direct.actor.card.search", params);
-		jsonObjectFree(params);
+    jsonObject* params = jsonNewObject(NULL);
+    jsonObjectSetKey(params, "user_id", 
+        jsonNewNumberObject(oilsFMGetObjectId(userObj)));
+    jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
+    jsonObjectSetKey(params, "login_type", jsonNewObject(type));
 
-        if (card) {
-            if (card->type != JSON_NULL) {
-			    char* card_active_str = oilsFMGetString(card, "active");
-			    card_active = oilsUtilsIsDBTrue(card_active_str);
-			    free(card_active_str);
-			}
-            jsonObjectFree(card);
-		}
-	}
+    jsonObject* authEvt = oilsUtilsQuickReq( // freed after password test
+        "open-ils.auth_internal",
+        "open-ils.auth_internal.user.validate", params);
+    jsonObjectFree(params);
 
-	int     barred = 0, deleted = 0;
-	char   *barred_str, *deleted_str;
+    if (!authEvt) {
+        // Something went seriously wrong.  Get outta here before 
+        // we start segfaulting.
+        jsonObjectFree(userObj);
+        if(freeable_uname) free(freeable_uname);
+        return -1;
+    }
 
-	if (userObj) {
-		barred_str = oilsFMGetString(userObj, "barred");
-		barred = oilsUtilsIsDBTrue(barred_str);
-		free(barred_str);
+    const char* evtCode = 
+        jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
 
-		deleted_str = oilsFMGetString(userObj, "deleted");
-		deleted = oilsUtilsIsDBTrue(deleted_str);
-		free(deleted_str);
-	}
+    // For security/privacy sake, only report that a patron is 
+    // inactive if the correct password is provided below.
+    int user_inactive = !strcmp(evtCode, "PATRON_INACTIVE");
 
-	if(!userObj || barred || deleted) {
-		response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
-		osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
-				uname, (barcode ? barcode : "(none)"), ws );
-		osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
-		oilsEventFree(response);
-		return 0;           // No such user
-	}
+    if (strcmp(evtCode, "SUCCESS") && !user_inactive) { // validate failed
+        osrfLogInfo(OSRF_LOG_MARK,  
+            "failed login: username=%s, barcode=%s, workstation=%s",
+            uname, (barcode ? barcode : "(none)"), ws);
+        osrfAppRespondComplete(ctx, authEvt);
+        jsonObjectFree(authEvt);
+        jsonObjectFree(userObj);
+        if(freeable_uname) free(freeable_uname);
+        return 0;           // No such user
+    }
 
 	// Such a user exists and isn't barred or deleted.
 	// Now see if he or she has the right credentials.
@@ -722,46 +685,23 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
         ctx, user_id, identifier, password, nonce);
 
 	if( passOK < 0 ) {
+        jsonObjectFree(authEvt);
 		jsonObjectFree(userObj);
+        if(freeable_uname) free(freeable_uname);
 		return passOK;
 	}
 
-	// See if the account is active
-	char* active = oilsFMGetString(userObj, "active");
-	if( !oilsUtilsIsDBTrue(active) ) {
-		if( passOK )
-			response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
-		else
-			response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
-
-		osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
-		oilsEventFree(response);
-		jsonObjectFree(userObj);
-		free(active);
-		return 0;
-	}
-	free(active);
-
-	if( !card_active ) {
-		osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
-		response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
-		osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
-		oilsEventFree( response );
-		jsonObjectFree( userObj );
-		return 0;
-	}
-
-
-	// See if the user is even allowed to log in
-	if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
-		jsonObjectFree(userObj);
-		return 0;
-	}
+    if (passOK && user_inactive) {
+        // Patron is inactive but provided the correct password.
+        // Return the original PATRON_INACTIVE event.
+        osrfAppRespondComplete(ctx, authEvt);
+        jsonObjectFree(authEvt);
+        jsonObjectFree(userObj);
+        if(freeable_uname) free(freeable_uname);
+        return 0;
+    }
 
-	char* freeable_uname = NULL;
-	if(!uname) {
-		uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
-	}
+    jsonObjectFree(authEvt); // we're all done with this now.
 
 	if( passOK ) { // login successful  
         
diff --git a/Open-ILS/src/c-apps/oils_auth_internal.c b/Open-ILS/src/c-apps/oils_auth_internal.c
index 38c5166..a6dba09 100644
--- a/Open-ILS/src/c-apps/oils_auth_internal.c
+++ b/Open-ILS/src/c-apps/oils_auth_internal.c
@@ -20,6 +20,9 @@
 // Default time for extending a persistent session: ten minutes
 #define DEFAULT_RESET_INTERVAL 10 * 60
 
+int safe_line = __LINE__;
+#define OILS_LOG_MARK_SAFE __FILE__,safe_line
+
 int osrfAppInitialize();
 int osrfAppChildInit();
 
@@ -48,6 +51,15 @@ int osrfAppInitialize() {
         "the user is authenticated", 1, 0 
     );
 
+    osrfAppRegisterMethod(
+        MODULENAME,
+        "open-ils.auth_internal.user.validate",
+        "oilsAutInternalValidate",
+        "Determines whether a user should be allowed to login.  " 
+        "Returns SUCCESS oilsEvent when the user is valid, otherwise "
+        "returns a non-SUCCESS oilsEvent object", 1, 0
+    );
+
     return 0;
 }
 
@@ -212,6 +224,33 @@ static oilsEvent* oilsAuthVerifyWorkstation(
     return NULL;
 }
 
+/**
+    Verifies that the user has permission to login with the given type.  
+    Caller is responsible for freeing returned oilsEvent.
+    @return oilsEvent* if the permission check failed, NULL otherwise.
+*/
+static oilsEvent* oilsAuthCheckLoginPerm(osrfMethodContext* ctx, 
+    int user_id, int org_id, const char* type ) {
+
+    char* perms[1];
+
+    if (!strcasecmp(type, OILS_AUTH_OPAC)) {
+        perms[0] = "OPAC_LOGIN";
+
+    } else if (!strcasecmp(type, OILS_AUTH_STAFF)) {
+        perms[0] = "STAFF_LOGIN";
+
+    } else if (!strcasecmp(type, OILS_AUTH_TEMP)) {
+        perms[0] = "STAFF_LOGIN";
+
+    } else if (!strcasecmp(type, OILS_AUTH_PERSIST)) {
+        perms[0] = "PERSISTENT_LOGIN";
+    }
+
+    return oilsUtilsCheckPerms(user_id, org_id, perms, 1);
+}
+
+
 
 /**
     @brief Implement the session create method
@@ -244,7 +283,7 @@ int oilsAutInternalCreateSession(osrfMethodContext* ctx) {
             "Missing parameters for method: %s", ctx->method->name );
     }
 
-	oilsEvent* response = NULL;
+    oilsEvent* response = NULL;
 
     // fetch the user object
     jsonObject* idParam = jsonNewNumberStringObject(user_id);
@@ -317,3 +356,101 @@ int oilsAutInternalCreateSession(osrfMethodContext* ctx) {
     return 0;
 }
 
+
+int oilsAutInternalValidate(osrfMethodContext* ctx) {
+    OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+    const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
+
+    const char* user_id     = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
+    const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
+    const char* org_unit    = jsonObjectGetString(jsonObjectGetKeyConst(args, "org_unit"));
+    const char* login_type  = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
+
+    if ( !(user_id && login_type && org_unit) ) {
+        return osrfAppRequestRespondException( ctx->session, ctx->request,
+            "Missing parameters for method: %s", ctx->method->name );
+    }
+
+    oilsEvent* response = NULL;
+    jsonObject *userObj = NULL, *params = NULL;
+    char* tmp_str = NULL;
+    int user_exists = 0, user_active = 0, 
+        user_barred = 0, user_deleted = 0;
+
+    // Confirm user exists, active=true, barred=false, deleted=false
+    params = jsonNewNumberStringObject(user_id);
+    userObj = oilsUtilsCStoreReq(
+        "open-ils.cstore.direct.actor.user.retrieve", params);
+    jsonObjectFree(params);
+
+    if (userObj && userObj->type != JSON_NULL) {
+        user_exists = 1;
+
+        tmp_str = oilsFMGetString(userObj, "active");
+        user_active = oilsUtilsIsDBTrue(tmp_str);
+        free(tmp_str);
+
+        tmp_str = oilsFMGetString(userObj, "barred");
+        user_barred = oilsUtilsIsDBTrue(tmp_str);
+        free(tmp_str);
+
+        tmp_str = oilsFMGetString(userObj, "deleted");
+        user_deleted = oilsUtilsIsDBTrue(tmp_str);
+        free(tmp_str);
+    }
+
+    if (!user_exists || user_barred || user_deleted) {
+        response = oilsNewEvent(OILS_LOG_MARK_SAFE, OILS_EVENT_AUTH_FAILED);
+    }
+
+    if (!response && !user_active) {
+        // In some cases, it's useful for the caller to know if the
+        // patron was unable to login becuase the account is inactive.
+        // Return a specific event for this.
+        response = oilsNewEvent(OILS_LOG_MARK_SAFE, "PATRON_INACTIVE");
+    }
+
+    if (!response && barcode) {
+        // Caller provided a barcode.  Ensure it exists and is active.
+
+        int card_ok = 0;
+        params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
+        jsonObject* card = oilsUtilsCStoreReq(
+            "open-ils.cstore.direct.actor.card.search", params);
+        jsonObjectFree(params);
+
+        if (card && card->type != JSON_NULL) {
+            tmp_str = oilsFMGetString(card, "active");
+            card_ok = oilsUtilsIsDBTrue(tmp_str);
+            free(tmp_str);
+        }
+
+        jsonObjectFree(card); // card=NULL OK here.
+
+        if (!card_ok) {
+            response = oilsNewEvent(
+                OILS_LOG_MARK_SAFE, "PATRON_CARD_INACTIVE");
+        }
+    }
+
+    if (!response) { // Still OK
+        // Confirm user has permission to login w/ the requested type.
+        response = oilsAuthCheckLoginPerm(
+            ctx, atoi(user_id), atoi(org_unit), login_type);
+    }
+
+
+    if (!response) {
+        // No tests failed.  Return SUCCESS.
+        response = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_SUCCESS);
+    }
+
+
+    jsonObjectFree(userObj); // userObj=NULL OK here.
+    osrfAppRespondComplete(ctx, oilsEventToJSON(response));
+    oilsEventFree(response);
+
+    return 0;
+}
+

commit c29a4c0f703c705241982ddfff18c2e8ffb907a9
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Nov 24 12:18:16 2015 -0500

    LP#1468422 Remove deprecated open-ils.storage remote_update
    
    User update in Actor.pm was the only remaining code that leveraged
    the open-ils.storage remote_update API.  With that code moving to
    open-ils.cstore, save some RAM by no longer auto-loading/publishing
    remote_update methods.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher.pm
index a10d1d3..db78f30 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher.pm
@@ -332,25 +332,6 @@ sub mass_delete {
     return $success;
 }
 
-sub remote_update_node {
-    my $self = shift;
-    my $client = shift;
-    my $keys = shift;
-    my $vals = shift;
-
-    local $OpenILS::Application::Storage::WRITE = 1;
-
-    my $cdbi = $self->{cdbi};
-
-    my $success = 1;
-    try {
-        $success = $cdbi->remote_update($keys,$vals);
-    } catch Error with {
-        $success = 0;
-    };
-    return $success;
-}
-
 sub merge_node {
     my $self = shift;
     my $client = shift;
@@ -718,29 +699,6 @@ for my $fmclass ( (Fieldmapper->classes) ) {
             );
         }
 
-        # Create the remote_update method
-        unless ( __PACKAGE__->is_registered( $api_prefix.'.remote_update' ) ) {
-            __PACKAGE__->register_method(
-                api_name    => $api_prefix.'.remote_update',
-                method      => 'remote_update_node',
-                api_level   => 1,
-                cdbi        => $cdbi,
-                argc        => 1,
-            );
-        }
-
-        # Create the batch remote_update method
-        unless ( __PACKAGE__->is_registered( $api_prefix.'.batch.remote_update' ) ) {
-            __PACKAGE__->register_method(
-                api_name    => $api_prefix.'.batch.remote_update',
-                method      => 'batch_call',
-                api_level   => 1,
-                unwrap      => 1,
-                cdbi        => $cdbi,
-                argc        => 1,
-            );
-        }
-
         # Create the search-based mass delete method
         unless ( __PACKAGE__->is_registered( $api_prefix.'.mass_delete' ) ) {
             __PACKAGE__->register_method(

commit fb856f49257e21ab2c3190c0fd9b70db676b2f11
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Nov 24 11:46:32 2015 -0500

    LP#1468422 SIP password verification
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm b/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
index 1600db1..16f2563 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
@@ -388,7 +388,8 @@ sub check_password {
     my ($self, $pwd) = @_;
     syslog('LOG_DEBUG', 'OILS: Patron->check_password()');
     return 0 unless (defined $pwd and $self->{user});
-    return md5_hex($pwd) eq $self->{user}->passwd;
+    return $U->verify_migrated_user_password(
+        $self->{editor},$self->{user}->id, $pwd);
 }
 
 sub currency {              # not really implemented

commit 62de23eeca6f27c2deaacb28358b411444b7dbbc
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Nov 24 11:31:06 2015 -0500

    LP#1468422 Implement password reset updates
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
index a0388b5..2d7c5d6 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
@@ -4103,8 +4103,7 @@ sub commit_password_reset {
     }
 
     # All is well; update the password
-    $user->passwd($password);
-    $e->update_actor_user($user);
+    modify_migrated_user_password($e, $user->id, $password);
 
     # And flag that this password reset request has been honoured
     $aupr->[0]->has_been_reset('t');

commit 2e9b6b3f2126a0ba136388bf5cc2689428f78d63
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Nov 24 11:15:06 2015 -0500

    LP#1468422 Port user update to cstore
    
    Migrate the user update code from open-ils.storage to open-ils.cstore.
    This has several benefits:
    
    1. We can re-use the patron password update code
    2. Several actions (bad contacts, invalid address) which previously
       resulted in data modifications outside the main transaction now
       take place with the main patron update transaction.
    3. Bigger, better, faster, stronger.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
index 8e46888..a0388b5 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
@@ -374,20 +374,17 @@ __PACKAGE__->register_method(
 );
 
 sub update_patron {
-    my( $self, $client, $user_session, $patron ) = @_;
+    my( $self, $client, $auth, $patron ) = @_;
 
-    my $session = $apputils->start_db_session();
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->event unless $e->checkauth;
 
-    $logger->info($patron->isnew ? "Creating new patron..." : "Updating Patron: " . $patron->id);
+    $logger->info($patron->isnew ? "Creating new patron..." : 
+        "Updating Patron: " . $patron->id);
 
-    my( $user_obj, $evt ) = $U->checkses($user_session);
+    my $evt = check_group_perm($e, $e->requestor, $patron);
     return $evt if $evt;
 
-    $evt = check_group_perm($session, $user_obj, $patron);
-    return $evt if $evt;
-
-    $apputils->set_audit_info($session, $user_session, $user_obj->id, $user_obj->wsid);
-
     # $new_patron is the patron in progress.  $patron is the original patron
     # passed in with the method.  new_patron will change as the components
     # of patron are added/updated.
@@ -411,74 +408,76 @@ sub update_patron {
     my $barred_hook = '';
 
     if($patron->isnew()) {
-        ( $new_patron, $evt ) = _add_patron($session, _clone_patron($patron), $user_obj);
+        ( $new_patron, $evt ) = _add_patron($e, _clone_patron($patron));
         return $evt if $evt;
         if($U->is_true($patron->barred)) {
-            $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'BAR_PATRON');
-            return $evt if $evt;
+            return $e->die_event unless
+                $e->allowed('BAR_PATRON', $patron->home_ou);
         }
     } else {
         $new_patron = $patron;
 
         # Did auth checking above already.
-        my $e = new_editor;
         $old_patron = $e->retrieve_actor_user($patron->id) or
             return $e->die_event;
-        $e->disconnect;
+
         if($U->is_true($old_patron->barred) != $U->is_true($new_patron->barred)) {
-            $evt = $U->check_perms($user_obj->id, $patron->home_ou, $U->is_true($old_patron->barred) ? 'UNBAR_PATRON' : 'BAR_PATRON');
-            return $evt if $evt;
+            my $perm = $U->is_true($old_patron->barred) ? 'UNBAR_PATRON' : 'BAR_PATRON';
+            return $e->die_event unless $e->allowed($perm, $patron->home_ou);
 
             $barred_hook = $U->is_true($new_patron->barred) ? 
                 'au.barred' : 'au.unbarred';
         }
     }
 
-    ( $new_patron, $evt ) = _add_update_addresses($session, $patron, $new_patron, $user_obj);
+    ( $new_patron, $evt ) = _add_update_addresses($e, $patron, $new_patron);
     return $evt if $evt;
 
-    ( $new_patron, $evt ) = _add_update_cards($session, $patron, $new_patron, $user_obj);
+    ( $new_patron, $evt ) = _add_update_cards($e, $patron, $new_patron);
     return $evt if $evt;
 
-    ( $new_patron, $evt ) = _add_survey_responses($session, $patron, $new_patron, $user_obj);
+    ( $new_patron, $evt ) = _add_survey_responses($e, $patron, $new_patron);
     return $evt if $evt;
 
     # re-update the patron if anything has happened to him during this process
     if($new_patron->ischanged()) {
-        ( $new_patron, $evt ) = _update_patron($session, $new_patron, $user_obj);
+        ( $new_patron, $evt ) = _update_patron($e, $new_patron);
         return $evt if $evt;
     }
 
-    ( $new_patron, $evt ) = _clear_badcontact_penalties($session, $old_patron, $new_patron, $user_obj);
+    ( $new_patron, $evt ) = _clear_badcontact_penalties($e, $old_patron, $new_patron);
     return $evt if $evt;
 
-    ($new_patron, $evt) = _create_stat_maps($session, $user_session, $patron, $new_patron, $user_obj);
+    ($new_patron, $evt) = _create_stat_maps($e, $patron, $new_patron);
     return $evt if $evt;
 
-    ($new_patron, $evt) = _create_perm_maps($session, $user_session, $patron, $new_patron, $user_obj);
+    ($new_patron, $evt) = _create_perm_maps($e, $patron, $new_patron);
     return $evt if $evt;
 
-    $apputils->commit_db_session($session);
-
-    $evt = apply_invalid_addr_penalty($patron);
+    $evt = apply_invalid_addr_penalty($e, $patron);
     return $evt if $evt;
 
+    $e->commit;
+
     my $tses = OpenSRF::AppSession->create('open-ils.trigger');
     if($patron->isnew) {
-        $tses->request('open-ils.trigger.event.autocreate', 'au.create', $new_patron, $new_patron->home_ou);
+        $tses->request('open-ils.trigger.event.autocreate', 
+            'au.create', $new_patron, $new_patron->home_ou);
     } else {
-        $tses->request('open-ils.trigger.event.autocreate', 'au.update', $new_patron, $new_patron->home_ou);
+        $tses->request('open-ils.trigger.event.autocreate', 
+            'au.update', $new_patron, $new_patron->home_ou);
 
         $tses->request('open-ils.trigger.event.autocreate', $barred_hook, 
             $new_patron, $new_patron->home_ou) if $barred_hook;
     }
 
-    return flesh_user($new_patron->id(), new_editor(requestor => $user_obj, xact => 1));
+    $e->xact_begin; # $e->rollback is called in new_flesh_user
+    return flesh_user($new_patron->id(), $e);
 }
 
 sub apply_invalid_addr_penalty {
+    my $e = shift;
     my $patron = shift;
-    my $e = new_editor(xact => 1);
 
     # grab the invalid address penalty if set
     my $penalties = OpenILS::Utils::Penalty->retrieve_usr_penalties($e, $patron->id, $patron->home_ou);
@@ -513,10 +512,6 @@ sub apply_invalid_addr_penalty {
         $penalty->standing_penalty(OILS_PENALTY_INVALID_PATRON_ADDRESS);
 
         $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
-        $e->commit;
-
-    } else {
-        $e->rollback;
     }
 
     return undef;
@@ -573,41 +568,34 @@ sub _clone_patron {
 
 sub _add_patron {
 
-    my $session     = shift;
+    my $e          = shift;
     my $patron      = shift;
-    my $user_obj    = shift;
 
-    my $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'CREATE_USER');
-    return (undef, $evt) if $evt;
+    return (undef, $e->die_event) unless 
+        $e->allowed('CREATE_USER', $patron->home_ou);
 
-    my $ex = $session->request(
-        'open-ils.storage.direct.actor.user.search.usrname', $patron->usrname())->gather(1);
-    if( $ex and @$ex ) {
-        return (undef, OpenILS::Event->new('USERNAME_EXISTS'));
-    }
+    my $ex = $e->search_actor_user(
+        {usrname => $patron->usrname}, {idlist => 1});
+    return (undef, OpenILS::Event->new('USERNAME_EXISTS')) if @$ex;
 
     $logger->info("Creating new user in the DB with username: ".$patron->usrname());
 
-    my $id = $session->request(
-        "open-ils.storage.direct.actor.user.create", $patron)->gather(1);
-    return (undef, $U->DB_UPDATE_FAILED($patron)) unless $id;
+    $e->create_actor_user($patron) or return $e->die_event;
+    my $id = $patron->id; # added by CStoreEditor
 
     $logger->info("Successfully created new user [$id] in DB");
-
-    return ( $session->request( 
-        "open-ils.storage.direct.actor.user.retrieve", $id)->gather(1), undef );
+    return ($e->retrieve_actor_user($id), undef);
 }
 
 
 sub check_group_perm {
-    my( $session, $requestor, $patron ) = @_;
+    my( $e, $requestor, $patron ) = @_;
     my $evt;
 
     # first let's see if the requestor has 
     # priveleges to update this user in any way
     if( ! $patron->isnew ) {
-        my $p = $session->request(
-            'open-ils.storage.direct.actor.user.retrieve', $patron->id )->gather(1);
+        my $p = $e->retrieve_actor_user($patron->id);
 
         # If we are the requestor (trying to update our own account)
         # and we are not trying to change our profile, we're good
@@ -617,20 +605,20 @@ sub check_group_perm {
         }
 
 
-        $evt = group_perm_failed($session, $requestor, $p);
+        $evt = group_perm_failed($e, $requestor, $p);
         return $evt if $evt;
     }
 
     # They are allowed to edit this patron.. can they put the 
     # patron into the group requested?
-    $evt = group_perm_failed($session, $requestor, $patron);
+    $evt = group_perm_failed($e, $requestor, $patron);
     return $evt if $evt;
     return undef;
 }
 
 
 sub group_perm_failed {
-    my( $session, $requestor, $patron ) = @_;
+    my( $e, $requestor, $patron ) = @_;
 
     my $perm;
     my $grp;
@@ -639,40 +627,33 @@ sub group_perm_failed {
     do {
 
         $logger->debug("user update looking for group perm for group $grpid");
-        $grp = $session->request(
-            'open-ils.storage.direct.permission.grp_tree.retrieve', $grpid )->gather(1);
-        return OpenILS::Event->new('PERMISSION_GRP_TREE_NOT_FOUND') unless $grp;
+        $grp = $e->retrieve_permission_grp_tree($grpid);
 
     } while( !($perm = $grp->application_perm) and ($grpid = $grp->parent) );
 
     $logger->info("user update checking perm $perm on user ".
         $requestor->id." for update/create on user username=".$patron->usrname);
 
-    my $evt = $U->check_perms($requestor->id, $patron->home_ou, $perm);
-    return $evt if $evt;
-    return undef;
+    return $e->allowed($perm, $patron->home_ou) ? undef : $e->die_event;
 }
 
 
 
 sub _update_patron {
-    my( $session, $patron, $user_obj, $noperm) = @_;
+    my( $e, $patron, $noperm) = @_;
 
     $logger->info("Updating patron ".$patron->id." in DB");
 
     my $evt;
 
     if(!$noperm) {
-        $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'UPDATE_USER');
-        return (undef, $evt) if $evt;
+        return (undef, $e->die_event)
+            unless $e->allowed('UPDATE_USER', $patron->home_ou);
     }
 
     # update the password by itself to avoid the password protection magic
     if( $patron->passwd ) {
-        my $s = $session->request(
-            'open-ils.storage.direct.actor.user.remote_update',
-            {id => $patron->id}, {passwd => $patron->passwd})->gather(1);
-        return (undef, $U->DB_UPDATE_FAILED($patron)) unless defined($s);
+        modify_migrated_user_password($e, $patron->id, $patron->passwd);
         $patron->clear_passwd;
     }
 
@@ -681,21 +662,18 @@ sub _update_patron {
         $patron->clear_ident_value;
     }
 
-    $evt = verify_last_xact($session, $patron);
+    $evt = verify_last_xact($e, $patron);
     return (undef, $evt) if $evt;
 
-    my $stat = $session->request(
-        "open-ils.storage.direct.actor.user.update",$patron )->gather(1);
-    return (undef, $U->DB_UPDATE_FAILED($patron)) unless defined($stat);
+    $e->update_actor_user($patron) or return (undef, $e->die_event);
 
     return ($patron);
 }
 
 sub verify_last_xact {
-    my( $session, $patron ) = @_;
+    my( $e, $patron ) = @_;
     return undef unless $patron->id and $patron->id > 0;
-    my $p = $session->request(
-        'open-ils.storage.direct.actor.user.retrieve', $patron->id)->gather(1);
+    my $p = $e->retrieve_actor_user($patron->id);
     my $xact = $p->last_xact_id;
     return undef unless $xact;
     $logger->info("user xact = $xact, saving with xact " . $patron->last_xact_id);
@@ -733,7 +711,7 @@ sub _check_dup_ident {
 
 sub _add_update_addresses {
 
-    my $session = shift;
+    my $e = shift;
     my $patron = shift;
     my $new_patron = shift;
 
@@ -767,7 +745,7 @@ sub _add_update_addresses {
 
             $address->usr($new_patron->id());
 
-            ($address, $evt) = _add_address($session,$address);
+            ($address, $evt) = _add_address($e,$address);
             return (undef, $evt) if $evt;
 
             # we need to get the new id
@@ -787,24 +765,24 @@ sub _add_update_addresses {
 
         } elsif($address->ischanged() ) {
 
-            ($address, $evt) = _update_address($session, $address);
+            ($address, $evt) = _update_address($e, $address);
             return (undef, $evt) if $evt;
 
         } elsif($address->isdeleted() ) {
 
             if( $address->id() == $new_patron->mailing_address() ) {
                 $new_patron->clear_mailing_address();
-                ($new_patron, $evt) = _update_patron($session, $new_patron);
+                ($new_patron, $evt) = _update_patron($e, $new_patron);
                 return (undef, $evt) if $evt;
             }
 
             if( $address->id() == $new_patron->billing_address() ) {
                 $new_patron->clear_billing_address();
-                ($new_patron, $evt) = _update_patron($session, $new_patron);
+                ($new_patron, $evt) = _update_patron($e, $new_patron);
                 return (undef, $evt) if $evt;
             }
 
-            $evt = _delete_address($session, $address);
+            $evt = _delete_address($e, $address);
             return (undef, $evt) if $evt;
         } 
     }
@@ -815,30 +793,24 @@ sub _add_update_addresses {
 
 # adds an address to the db and returns the address with new id
 sub _add_address {
-    my($session, $address) = @_;
+    my($e, $address) = @_;
     $address->clear_id();
 
     $logger->info("Creating new address at street ".$address->street1);
 
     # put the address into the database
-    my $id = $session->request(
-        "open-ils.storage.direct.actor.user_address.create", $address )->gather(1);
-    return (undef, $U->DB_UPDATE_FAILED($address)) unless $id;
-
-    $address->id( $id );
+    $e->create_actor_user_address($address) or return (undef, $e->die_event);
     return ($address, undef);
 }
 
 
 sub _update_address {
-    my( $session, $address ) = @_;
+    my( $e, $address ) = @_;
 
     $logger->info("Updating address ".$address->id." in the DB");
 
-    my $stat = $session->request(
-        "open-ils.storage.direct.actor.user_address.update", $address )->gather(1);
+    $e->update_actor_user_address($address) or return (undef, $e->die_event);
 
-    return (undef, $U->DB_UPDATE_FAILED($address)) unless defined($stat);
     return ($address, undef);
 }
 
@@ -846,7 +818,7 @@ sub _update_address {
 
 sub _add_update_cards {
 
-    my $session = shift;
+    my $e = shift;
     my $patron = shift;
     my $new_patron = shift;
 
@@ -862,7 +834,7 @@ sub _add_update_cards {
         if(ref($card) and $card->isnew()) {
 
             $virtual_id = $card->id();
-            ( $card, $evt ) = _add_card($session,$card);
+            ( $card, $evt ) = _add_card($e, $card);
             return (undef, $evt) if $evt;
 
             #if(ref($patron->card)) { $patron->card($patron->card->id); }
@@ -872,7 +844,7 @@ sub _add_update_cards {
             }
 
         } elsif( ref($card) and $card->ischanged() ) {
-            $evt = _update_card($session, $card);
+            $evt = _update_card($e, $card);
             return (undef, $evt) if $evt;
         }
     }
@@ -883,29 +855,23 @@ sub _add_update_cards {
 
 # adds an card to the db and returns the card with new id
 sub _add_card {
-    my( $session, $card ) = @_;
+    my( $e, $card ) = @_;
     $card->clear_id();
 
     $logger->info("Adding new patron card ".$card->barcode);
 
-    my $id = $session->request(
-        "open-ils.storage.direct.actor.card.create", $card )->gather(1);
-    return (undef, $U->DB_UPDATE_FAILED($card)) unless $id;
-    $logger->info("Successfully created patron card $id");
+    $e->create_actor_card($card) or return (undef, $e->die_event);
 
-    $card->id($id);
     return ( $card, undef );
 }
 
 
 # returns event on error.  returns undef otherwise
 sub _update_card {
-    my( $session, $card ) = @_;
+    my( $e, $card ) = @_;
     $logger->info("Updating patron card ".$card->id);
 
-    my $stat = $session->request(
-        "open-ils.storage.direct.actor.card.update", $card )->gather(1);
-    return $U->DB_UPDATE_FAILED($card) unless defined($stat);
+    $e->update_actor_card($card) or return $e->die_event;
     return undef;
 }
 
@@ -914,21 +880,18 @@ sub _update_card {
 
 # returns event on error.  returns undef otherwise
 sub _delete_address {
-    my( $session, $address ) = @_;
+    my( $e, $address ) = @_;
 
     $logger->info("Deleting address ".$address->id." from DB");
 
-    my $stat = $session->request(
-        "open-ils.storage.direct.actor.user_address.delete", $address )->gather(1);
-
-    return $U->DB_UPDATE_FAILED($address) unless defined($stat);
+    $e->delete_actor_user_address($address) or return $e->die_event;
     return undef;
 }
 
 
 
 sub _add_survey_responses {
-    my ($session, $patron, $new_patron) = @_;
+    my ($e, $patron, $new_patron) = @_;
 
     $logger->info( "Updating survey responses for patron ".$new_patron->id );
 
@@ -949,12 +912,11 @@ sub _add_survey_responses {
 }
 
 sub _clear_badcontact_penalties {
-    my ($session, $old_patron, $new_patron, $user_obj) = @_;
+    my ($e, $old_patron, $new_patron) = @_;
 
     return ($new_patron, undef) unless $old_patron;
 
     my $PNM = $OpenILS::Utils::BadContact::PENALTY_NAME_MAP;
-    my $e = new_editor(xact => 1);
 
     # This ignores whether the caller of update_patron has any permission
     # to remove penalties, but these penalties no longer make sense
@@ -998,38 +960,34 @@ sub _clear_badcontact_penalties {
         $e->update_actor_user_standing_penalty($_) or return (undef, $e->die_event);
     }
 
-    $e->commit;
     return ($new_patron, undef);
 }
 
 
 sub _create_stat_maps {
 
-    my($session, $user_session, $patron, $new_patron) = @_;
+    my($e, $patron, $new_patron) = @_;
 
     my $maps = $patron->stat_cat_entries();
 
     for my $map (@$maps) {
 
-        my $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.update";
+        my $method = "update_actor_stat_cat_entry_user_map";
 
         if ($map->isdeleted()) {
-            $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.delete";
+            $method = "delete_actor_stat_cat_entry_user_map";
 
         } elsif ($map->isnew()) {
-            $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.create";
+            $method = "create_actor_stat_cat_entry_user_map";
             $map->clear_id;
         }
 
 
         $map->target_usr($new_patron->id);
 
-        #warn "
         $logger->info("Updating stat entry with method $method and map $map");
 
-        my $stat = $session->request($method, $map)->gather(1);
-        return (undef, $U->DB_UPDATE_FAILED($map)) unless defined($stat);
-
+        $e->$method($map) or return (undef, $e->die_event);
     }
 
     return ($new_patron, undef);
@@ -1037,29 +995,25 @@ sub _create_stat_maps {
 
 sub _create_perm_maps {
 
-    my($session, $user_session, $patron, $new_patron) = @_;
+    my($e, $patron, $new_patron) = @_;
 
     my $maps = $patron->permissions;
 
     for my $map (@$maps) {
 
-        my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
+        my $method = "update_permission_usr_perm_map";
         if ($map->isdeleted()) {
-            $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
+            $method = "delete_permission_usr_perm_map";
         } elsif ($map->isnew()) {
-            $method = "open-ils.storage.direct.permission.usr_perm_map.create";
+            $method = "create_permission_usr_perm_map";
             $map->clear_id;
         }
 
-
         $map->usr($new_patron->id);
 
-        #warn( "Updating permissions with method $method and session $user_session and map $map" );
         $logger->info( "Updating permissions with method $method and map $map" );
 
-        my $stat = $session->request($method, $map)->gather(1);
-        return (undef, $U->DB_UPDATE_FAILED($map)) unless defined($stat);
-
+        $e->$method($map) or return (undef, $e->die_event);
     }
 
     return ($new_patron, undef);
@@ -1424,6 +1378,29 @@ sub patron_adv_search {
 }
 
 
+# A migrated (main) password has the form:
+# CRYPT( MD5( pw_salt || MD5(real_password) ), pw_salt )
+sub modify_migrated_user_password {
+    my ($e, $user_id, $passwd) = @_;
+
+    # new password gets a new salt
+    my $new_salt = $e->json_query({
+        from => ['actor.create_salt', 'main']})->[0];
+    $new_salt = $new_salt->{'actor.create_salt'};
+
+    $e->json_query({
+        from => [
+            'actor.set_passwd',
+            $user_id,
+            'main',
+            md5_hex($new_salt . md5_hex($passwd)),
+            $new_salt
+        ]
+    });
+}
+
+
+
 __PACKAGE__->register_method(
     method    => "update_passwd",
     api_name  => "open-ils.actor.user.password.update",
@@ -1484,22 +1461,7 @@ sub update_passwd {
         # NOTE: with access to the plain text password we could crypt
         # the password without the extra MD5 pre-hashing.  Other changes
         # would be required.  Noting here for future reference.
-
-        # new password gets a new salt
-        my $new_salt = $e->json_query({
-            from => ['actor.create_salt', 'main']})->[0];
-        $new_salt = $new_salt->{'actor.create_salt'};
-
-        $e->json_query({
-            from => [
-                'actor.set_passwd',
-                $db_user->id,
-                'main',
-                md5_hex($new_salt . md5_hex($new_val)),
-                $new_salt
-            ]
-        });
-
+        modify_migrated_user_password($e, $db_user->id, $new_val);
         $db_user->passwd('');
 
     } else {
@@ -3750,8 +3712,7 @@ sub really_delete_user {
     return $e->die_event unless $e->requestor->id != $user->id;
     return $e->die_event unless $e->allowed('DELETE_USER', $user->home_ou);
     # Check if you are allowed to mess with this patron permission group at all
-    my $session = OpenSRF::AppSession->create( "open-ils.storage" );
-    my $evt = group_perm_failed($session, $e->requestor, $user);
+    my $evt = group_perm_failed($e, $e->requestor, $user);
     return $e->die_event($evt) if $evt;
     my $stat = $e->json_query(
         {from => ['actor.usr_delete', $user_id, $dest_user_id]})->[0]

commit 5ddb3f61b5cf9c97cf5b45c42209a1083e8efff8
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Nov 23 17:13:48 2015 -0500

    LP#1468422 Password verify and password update
    
    These API's now support new-style passwords:
    
    open-ils.actor.verify_user_password
    open-ils.actor.user.password.update
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
index 281298a..8e46888 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
@@ -1475,15 +1475,32 @@ sub update_passwd {
         or return $e->die_event;
     my $api = $self->api_name;
 
-    # make sure the original password matches the in-database password
-    if (md5_hex($orig_pw) ne $db_user->passwd) {
+    if (!$U->verify_migrated_user_password($e, $db_user->id, $orig_pw)) {
         $e->rollback;
         return new OpenILS::Event('INCORRECT_PASSWORD');
     }
 
     if( $api =~ /password/o ) {
+        # NOTE: with access to the plain text password we could crypt
+        # the password without the extra MD5 pre-hashing.  Other changes
+        # would be required.  Noting here for future reference.
+
+        # new password gets a new salt
+        my $new_salt = $e->json_query({
+            from => ['actor.create_salt', 'main']})->[0];
+        $new_salt = $new_salt->{'actor.create_salt'};
+
+        $e->json_query({
+            from => [
+                'actor.set_passwd',
+                $db_user->id,
+                'main',
+                md5_hex($new_salt . md5_hex($new_val)),
+                $new_salt
+            ]
+        });
 
-        $db_user->passwd($new_val);
+        $db_user->passwd('');
 
     } else {
 
@@ -3301,8 +3318,8 @@ sub verify_user_password {
     return 0 if (!$user);
     return 0 if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id); 
     return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
-    return 1 if $user->passwd eq $password;
-    return 0;
+    return $U->verify_migrated_user_password(
+        $e, $user_by_username->id, $password, 1);
 }
 
 __PACKAGE__->register_method (
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
index ab89ac4..1378a47 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
@@ -1,5 +1,4 @@
 package OpenILS::Application::AppUtils;
-# vim:noet:ts=4
 use strict; use warnings;
 use OpenILS::Application;
 use base qw/OpenILS::Application/;
@@ -18,6 +17,7 @@ use Encode;
 use DateTime;
 use DateTime::Format::ISO8601;
 use List::MoreUtils qw/uniq/;
+use Digest::MD5 qw(md5_hex);
 
 # ---------------------------------------------------------------------------
 # Pile of utilty methods used accross applications.
@@ -2291,6 +2291,55 @@ sub fpsum {
     return $result / 100;
 }
 
+# Non-migrated passwords can be verified directly in the DB
+# with any extra hashing.
+sub verify_user_password {
+    my ($class, $e, $user_id, $passwd, $pw_type) = @_;
+
+    $pw_type ||= 'main'; # primary login password
+
+    my $verify = $e->json_query({
+        from => [
+            'actor.verify_passwd', 
+            $user_id, $pw_type, $passwd
+        ]
+    })->[0];
+
+    return $class->is_true($verify->{'actor.verify_passwd'});
+}
+
+# Passwords migrated from the original MD5 scheme are passed through 2
+# extra layers of MD5 hashing for backwards compatibility with the
+# MD5 passwords of yore and the MD5-based chap-style authentication.  
+# Passwords are stored in the DB like this:
+# CRYPT( MD5( pw_salt || MD5(real_password) ), pw_salt )
+#
+# If 'as_md5' is true, the password provided has already been
+# MD5 hashed.
+sub verify_migrated_user_password {
+    my ($class, $e, $user_id, $passwd, $as_md5) = @_;
+
+    # 'main' is the primary login password. This is the only password 
+    # type that requires the additional MD5 hashing.
+    my $pw_type = 'main';
+
+    # Sometimes we have the bare password, sometimes the MD5 version.
+    my $md5_pass = $as_md5 ? $passwd : md5_hex($passwd);
+
+    my $salt = $e->json_query({
+        from => [
+            'actor.get_salt', 
+            $user_id, 
+            $pw_type
+        ]
+    })->[0];
+
+    $salt = $salt->{'actor.get_salt'};
+
+    return $class->verify_user_password(
+        $e, $user_id, md5_hex($salt . $md5_pass), $pw_type);
+}
+
 
 1;
 

commit 3cb50795de44054049611cc570bf87634583079e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Nov 23 13:56:36 2015 -0500

    LP#1468422 Admin seed data sets new-style passwd
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/support-scripts/eg_db_config.in b/Open-ILS/src/support-scripts/eg_db_config.in
index 2d0bd3b..b59d044 100755
--- a/Open-ILS/src/support-scripts/eg_db_config.in
+++ b/Open-ILS/src/support-scripts/eg_db_config.in
@@ -240,7 +240,7 @@ sub set_admin_account {
 	# Create a new salt, perform MD5 hashing, set the new password.
 	$stmt = $dbh->prepare("SELECT actor.create_salt('main') AS new_salt");
 	$stmt->execute;
-	my $new_salt = $stmt->selectrow_hashref->{new_salt};
+	my $new_salt = $dbh->selectrow_hashref($stmt)->{new_salt};
 
     $stmt = $dbh->prepare(
 		"SELECT actor.set_passwd(1, 'main', MD5(? || MD5(?)), ?)");

commit 2e3689583d21f320d69c5f33049047e5c91e627b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Nov 23 13:22:37 2015 -0500

    LP#1468422 Password storage release notes
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc b/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc
new file mode 100644
index 0000000..01b9ae3
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc
@@ -0,0 +1,95 @@
+Improved Password Management and Authentication
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Evergreen user passwords are now stored with additional layers of 
+encryption and may only be accessed directly by the database, not
+the application layer.
+
+All API changes are backwards compatible with existing 3rd-party
+clients.
+
+open-ils.auth_internal
+++++++++++++++++++++++
+To support the new storage mechanism, a new Evergreen service has
+been added called "open-ils.auth_internal".  This service runs on
+the private OpenSRF/XMPP domain and is used to store authenticated 
+user data in the authentication cache.  
+
+This is a required service and changes to opensrf.xml (typically 
+/openils/conf/opensrf.xml) are needed to run the new service.
+
+.Modifying opensrf.xml
+* A new <open-ils.auth_internal> app stanza is added to define the 
+  new service
+* Cache timeout settings are moved from the app stanza for open-ils.auth
+  into open-ils.auth_internal
+* open-ils.auth_internal is added to the set of running services for the 
+  domain.
+
+Example diff:
+
+[source,diff]
+---------------------------------------------------------------------
+diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example
+index 3b47481..59f737a 100644
+--- a/Open-ILS/examples/opensrf.xml.example
++++ b/Open-ILS/examples/opensrf.xml.example
+@@ -424,6 +424,29 @@ vim:et:ts=4:sw=4:
+                 </unix_config>
+                 <app_settings>
+                     <!-- defined app-specific settings here -->
++                    <auth_limits>
++                        <seed>30</seed> <!-- amount of time a seed request is valid for -->
++                        <block_time>90</block_time> <!-- amount of time since last auth or seed request to save failure counts -->
++                        <block_count>10</block_count> <!-- number of failures before blocking access -->
++                    </auth_limits>
++                </app_settings>
++            </open-ils.auth>
++
++            <!-- Internal authentication server -->
++            <open-ils.auth_internal>
++                <keepalive>5</keepalive>
++                <stateless>1</stateless>
++                <language>c</language>
++                <implementation>oils_auth_internal.so</implementation>
++                <unix_config>
++                    <max_requests>1000</max_requests>
++                    <min_children>1</min_children>
++                    <max_children>15</max_children>
++                    <min_spare_children>1</min_spare_children>
++                    <max_spare_children>5</max_spare_children>
++                </unix_config>
++                <app_settings>
++                    <!-- defined app-specific settings here -->
+                     <default_timeout>
+                         <!-- default login timeouts based on login type -->
+                         <opac>420</opac>
+@@ -431,13 +454,10 @@ vim:et:ts=4:sw=4:
+                         <temp>300</temp>
+                         <persist>2 weeks</persist>
+                     </default_timeout>
+-                    <auth_limits>
+-                        <seed>30</seed> <!-- amount of time a seed request is valid for -->
+-                        <block_time>90</block_time> <!-- amount of time since last auth or seed request to save failure counts -->
+-                        <block_count>10</block_count> <!-- number of failures before blocking access -->
+-                    </auth_limits>
+                 </app_settings>
+-            </open-ils.auth>
++            </open-ils.auth_internal>
++
++
+ 
+             <!-- Authentication proxy server -->
+             <open-ils.auth_proxy>
+@@ -1177,6 +1197,7 @@ vim:et:ts=4:sw=4:
+                 <appname>open-ils.circ</appname> 
+                 <appname>open-ils.actor</appname> 
+                 <appname>open-ils.auth</appname> 
++                <appname>open-ils.auth_internal</appname>
+                 <appname>open-ils.auth_proxy</appname> 
+                 <appname>open-ils.storage</appname>  
+                 <appname>open-ils.justintime</appname>  
+---------------------------------------------------------------------
+
+
+
+

commit 2c0c522c1380c797f597c2ca482e360fba8a5f8c
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Nov 23 12:51:31 2015 -0500

    LP#1468422 Admin seed data sets new-style passwd
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/support-scripts/eg_db_config.in b/Open-ILS/src/support-scripts/eg_db_config.in
index a30b09e..2d0bd3b 100755
--- a/Open-ILS/src/support-scripts/eg_db_config.in
+++ b/Open-ILS/src/support-scripts/eg_db_config.in
@@ -226,10 +226,27 @@ sub set_admin_account {
 		print STDERR "Error was " . $dbh->errstr . "\n";
 		return;
 	}
-	my $stmt = $dbh->prepare("UPDATE actor.usr SET usrname = ?, passwd = ? WHERE id = 1");
-	$stmt->execute(($admin_user, $admin_pw));
+	my $stmt = $dbh->prepare("UPDATE actor.usr SET usrname = ? WHERE id = 1");
+	$stmt->execute(($admin_user));
 	if ($dbh->err) {
-		print STDERR "Failed to set admin account. ";
+		print STDERR "Failed to set admin username. ";
+		print STDERR "Error was " . $dbh->errstr . "\n";
+		return;
+	}
+
+	# Legacy actor.usr.passwd-style passwords must go through
+	# in intermediate round of hashing before final crypt'ing.
+	# The hashing step requires access to the password salt.
+	# Create a new salt, perform MD5 hashing, set the new password.
+	$stmt = $dbh->prepare("SELECT actor.create_salt('main') AS new_salt");
+	$stmt->execute;
+	my $new_salt = $stmt->selectrow_hashref->{new_salt};
+
+    $stmt = $dbh->prepare(
+		"SELECT actor.set_passwd(1, 'main', MD5(? || MD5(?)), ?)");
+	$stmt->execute(($new_salt, $admin_pw, $new_salt));
+	if ($dbh->err) {
+		print STDERR "Failed to set admin password. ";
 		print STDERR "Error was " . $dbh->errstr . "\n";
 		return;
 	}

commit 722338616034d1be24603ef193cc775fbbe08307
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Nov 19 15:00:20 2015 -0500

    LP#1468422 New open-ils.auth_internal service
    
    Service is responsible for adding user data to the authentication cache.
    Cache times are determined from opensrf.xml/AOUS settings.  No
    authentication checks are performed.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example
index 3b47481..59f737a 100644
--- a/Open-ILS/examples/opensrf.xml.example
+++ b/Open-ILS/examples/opensrf.xml.example
@@ -424,6 +424,29 @@ vim:et:ts=4:sw=4:
                 </unix_config>
                 <app_settings>
                     <!-- defined app-specific settings here -->
+                    <auth_limits>
+                        <seed>30</seed> <!-- amount of time a seed request is valid for -->
+                        <block_time>90</block_time> <!-- amount of time since last auth or seed request to save failure counts -->
+                        <block_count>10</block_count> <!-- number of failures before blocking access -->
+                    </auth_limits>
+                </app_settings>
+            </open-ils.auth>
+
+            <!-- Internal authentication server -->
+            <open-ils.auth_internal>
+                <keepalive>5</keepalive>
+                <stateless>1</stateless>
+                <language>c</language>
+                <implementation>oils_auth_internal.so</implementation>
+                <unix_config>
+                    <max_requests>1000</max_requests>
+                    <min_children>1</min_children>
+                    <max_children>15</max_children>
+                    <min_spare_children>1</min_spare_children>
+                    <max_spare_children>5</max_spare_children>
+                </unix_config>
+                <app_settings>
+                    <!-- defined app-specific settings here -->
                     <default_timeout>
                         <!-- default login timeouts based on login type -->
                         <opac>420</opac>
@@ -431,13 +454,10 @@ vim:et:ts=4:sw=4:
                         <temp>300</temp>
                         <persist>2 weeks</persist>
                     </default_timeout>
-                    <auth_limits>
-                        <seed>30</seed> <!-- amount of time a seed request is valid for -->
-                        <block_time>90</block_time> <!-- amount of time since last auth or seed request to save failure counts -->
-                        <block_count>10</block_count> <!-- number of failures before blocking access -->
-                    </auth_limits>
                 </app_settings>
-            </open-ils.auth>
+            </open-ils.auth_internal>
+
+
 
             <!-- Authentication proxy server -->
             <open-ils.auth_proxy>
@@ -1177,6 +1197,7 @@ vim:et:ts=4:sw=4:
                 <appname>open-ils.circ</appname> 
                 <appname>open-ils.actor</appname> 
                 <appname>open-ils.auth</appname> 
+                <appname>open-ils.auth_internal</appname>
                 <appname>open-ils.auth_proxy</appname> 
                 <appname>open-ils.storage</appname>  
                 <appname>open-ils.justintime</appname>  
diff --git a/Open-ILS/src/c-apps/Makefile.am b/Open-ILS/src/c-apps/Makefile.am
index 06fdf7c..0672a8e 100644
--- a/Open-ILS/src/c-apps/Makefile.am
+++ b/Open-ILS/src/c-apps/Makefile.am
@@ -33,7 +33,7 @@ test_qstore_CFLAGS = $(AM_CFLAGS)
 test_qstore_LDFLAGS = $(AM_LDFLAGS) -loils_idl -loils_utils
 test_qstore_DEPENDENCIES = liboils_idl.la liboils_utils.la
 
-lib_LTLIBRARIES = liboils_idl.la liboils_utils.la oils_cstore.la oils_qstore.la oils_rstore.la oils_pcrud.la oils_auth.la
+lib_LTLIBRARIES = liboils_idl.la liboils_utils.la oils_cstore.la oils_qstore.la oils_rstore.la oils_pcrud.la oils_auth.la oils_auth_internal.la
 
 liboils_idl_la_SOURCES = oils_idl-core.c
 liboils_idl_la_LDFLAGS = -version-info 2:0:0
@@ -61,4 +61,9 @@ oils_auth_la_SOURCES = oils_auth.c
 oils_auth_la_LDFLAGS = -module -loils_utils -lpcre -version-info 2:0:0
 oils_auth_la_DEPENDENCIES = liboils_utils.la
 
+oils_auth_internal_la_SOURCES = oils_auth_internal.c
+oils_auth_internal_la_LDFLAGS = -module -loils_utils -version-info 2:0:0
+oils_auth_internal_la_DEPENDENCIES = liboils_utils.la
+
+
 
diff --git a/Open-ILS/src/c-apps/oils_auth.c b/Open-ILS/src/c-apps/oils_auth.c
index 587fbb4..b9998b5 100644
--- a/Open-ILS/src/c-apps/oils_auth.c
+++ b/Open-ILS/src/c-apps/oils_auth.c
@@ -24,10 +24,6 @@
 int osrfAppInitialize();
 int osrfAppChildInit();
 
-static long _oilsAuthOPACTimeout = 0;
-static long _oilsAuthStaffTimeout = 0;
-static long _oilsAuthOverrideTimeout = 0;
-static long _oilsAuthPersistTimeout = 0;
 static long _oilsAuthSeedTimeout = 0;
 static long _oilsAuthBlockTimeout = 0;
 static long _oilsAuthBlockCount = 0;
@@ -535,130 +531,6 @@ static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id,
     return verified;
 }
 
-/**
-	@brief Determine the login timeout.
-	@param userObj Pointer to an object describing the user.
-	@param type Pointer to one of four possible character strings identifying the login type.
-	@param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
-	@return The length of the timeout, in seconds.
-
-	The default timeout value comes from the configuration file, and depends on the
-	login type.
-
-	The default may be overridden by a corresponding org unit setting.  The @a orgloc
-	parameter says what org unit to use for the lookup.  If @a orgloc <= 0, or if the
-	lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
-	instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).
-
-	Whether defined in the config file or in an org unit setting, a timeout value may be
-	expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
-	space) or as an interval string to be translated into seconds by PostgreSQL.
-*/
-static long oilsAuthGetTimeout( const jsonObject* userObj, const char* type, int orgloc ) {
-
-	if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
-
-		jsonObject* value_obj;
-
-		value_obj = osrf_settings_host_value_object(
-			"/apps/open-ils.auth/app_settings/default_timeout/opac" );
-		_oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
-		jsonObjectFree(value_obj);
-		if( -1 == _oilsAuthOPACTimeout ) {
-			osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
-			_oilsAuthOPACTimeout = 0;
-		}
-
-		value_obj = osrf_settings_host_value_object(
-			"/apps/open-ils.auth/app_settings/default_timeout/staff" );
-		_oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
-		jsonObjectFree(value_obj);
-		if( -1 == _oilsAuthStaffTimeout ) {
-			osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
-			_oilsAuthStaffTimeout = 0;
-		}
-
-		value_obj = osrf_settings_host_value_object(
-			"/apps/open-ils.auth/app_settings/default_timeout/temp" );
-		_oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
-		jsonObjectFree(value_obj);
-		if( -1 == _oilsAuthOverrideTimeout ) {
-			osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
-			_oilsAuthOverrideTimeout = 0;
-		}
-
-		value_obj = osrf_settings_host_value_object(
-			"/apps/open-ils.auth/app_settings/default_timeout/persist" );
-		_oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
-		jsonObjectFree(value_obj);
-		if( -1 == _oilsAuthPersistTimeout ) {
-			osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
-			_oilsAuthPersistTimeout = 0;
-		}
-
-		osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
-			"opac => %ld : staff => %ld : temp => %ld : persist => %ld",
-			_oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
-			_oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
-	}
-
-	int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
-	if(orgloc < 1)
-		orgloc = home_ou;
-
-	char* setting = NULL;
-	long default_timeout = 0;
-
-	if( !strcmp( type, OILS_AUTH_OPAC )) {
-		setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
-		default_timeout = _oilsAuthOPACTimeout;
-	} else if( !strcmp( type, OILS_AUTH_STAFF )) {
-		setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
-		default_timeout = _oilsAuthStaffTimeout;
-	} else if( !strcmp( type, OILS_AUTH_TEMP )) {
-		setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
-		default_timeout = _oilsAuthOverrideTimeout;
-	} else if( !strcmp( type, OILS_AUTH_PERSIST )) {
-		setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
-		default_timeout = _oilsAuthPersistTimeout;
-	}
-
-	// Get the org unit setting, if there is one.
-	char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
-	if(!timeout) {
-		if( orgloc != home_ou ) {
-			osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
-				"trying home_ou %d", orgloc, home_ou );
-			timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
-		}
-	}
-
-	if(!timeout)
-		return default_timeout;   // No override from org unit setting
-
-	// Translate the org unit setting to a number
-	long t;
-	if( !*timeout ) {
-		osrfLogWarning( OSRF_LOG_MARK,
-			"Timeout org unit setting is an empty string for %s login; using default",
-			timeout, type );
-		t = default_timeout;
-	} else {
-		// Treat timeout string as an interval, and convert it to seconds
-		t = oilsUtilsIntervalToSeconds( timeout );
-		if( -1 == t ) {
-			// Unable to convert; possibly an invalid interval string
-			osrfLogError( OSRF_LOG_MARK,
-				"Unable to convert timeout interval \"%s\" for %s login; using default",
-				timeout, type );
-			t = default_timeout;
-		}
-	}
-
-	free(timeout);
-	return t;
-}
-
 /*
 	Adds the authentication token to the user cache.  The timeout for the
 	auth token is based on the type of login as well as (if type=='opac')
@@ -669,80 +541,38 @@ static long oilsAuthGetTimeout( const jsonObject* userObj, const char* type, int
 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
 		const char* type, int orgloc, const char* workstation ) {
 
-	oilsEvent* response;
+	oilsEvent* response = NULL;
 
-	long timeout;
-	char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "ws_ou"));
-	if(wsorg) { /* if there is a workstation, use it for the timeout */
-		osrfLogDebug( OSRF_LOG_MARK,
-				"Auth session trying workstation id %d for auth timeout", atoi(wsorg));
-		timeout = oilsAuthGetTimeout( userObj, type, atoi(wsorg) );
-		free(wsorg);
-	} else {
-		osrfLogDebug( OSRF_LOG_MARK,
-				"Auth session trying org from param [%d] for auth timeout", orgloc );
-		timeout = oilsAuthGetTimeout( userObj, type, orgloc );
-	}
-	osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %ld", uname, timeout );
-
-	char* string = va_list_to_string(
-			"%d.%ld.%s", (long) getpid(), time(NULL), uname );
-	char* authToken = md5sum(string);
-	char* authKey = va_list_to_string(
-			"%s%s", OILS_AUTH_CACHE_PRFX, authToken );
-
-	const char* ws = (workstation) ? workstation : "";
-	osrfLogActivity(OSRF_LOG_MARK,
-		"successful login: username=%s, authtoken=%s, workstation=%s", uname, authToken, ws );
-
-	oilsFMSetString( userObj, "passwd", "" );
-	jsonObject* cacheObj = jsonParseFmt( "{\"authtime\": %ld}", timeout );
-	jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
-
-	if( !strcmp( type, OILS_AUTH_PERSIST )) {
-		// Add entries for endtime and reset_interval, so that we can gracefully
-		// extend the session a bit if the user is active toward the end of the 
-		// timeout originally specified.
-		time_t endtime = time( NULL ) + timeout;
-		jsonObjectSetKey( cacheObj, "endtime", jsonNewNumberObject( (double) endtime ) );
-
-		// Reset interval is hard-coded for now, but if we ever want to make it
-		// configurable, this is the place to do it:
-		jsonObjectSetKey( cacheObj, "reset_interval",
-			jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL ));
-	}
+    jsonObject* params = jsonNewObject(NULL);
+    jsonObjectSetKey(params, "user_id", 
+        jsonNewNumberObject(oilsFMGetObjectId(userObj)));
+    jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
+    jsonObjectSetKey(params, "login_type", jsonNewObject(type));
+    if (workstation) 
+        jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
+
+    jsonObject* authEvt = oilsUtilsQuickReq(
+        "open-ils.auth_internal",
+        "open-ils.auth_internal.session.create", params);
+    jsonObjectFree(params);
 
-	osrfCachePutObject( authKey, cacheObj, (time_t) timeout );
-	jsonObjectFree(cacheObj);
-	osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
-	jsonObject* payload = jsonParseFmt(
-		"{ \"authtoken\": \"%s\", \"authtime\": %ld }", authToken, timeout );
+    if (authEvt) {
 
-	response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
-	free(string); free(authToken); free(authKey);
-	jsonObjectFree(payload);
+        response = oilsNewEvent2(
+            OSRF_LOG_MARK, 
+            jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
+            jsonObjectGetKey(authEvt, "payload")   // cloned within Event
+        );
 
-	return response;
-}
+        jsonObjectFree(authEvt);
 
-static oilsEvent* oilsAuthVerifyWorkstation(
-		const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
-	osrfLogInfo(OSRF_LOG_MARK, "Attaching workstation to user at login: %s", ws);
-	jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
-	if(!workstation || workstation->type == JSON_NULL) {
-		jsonObjectFree(workstation);
-		return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
-	}
-	long wsid = oilsFMGetObjectId(workstation);
-	LONG_TO_STRING(wsid);
-	char* orgid = oilsFMGetString(workstation, "owning_lib");
-	oilsFMSetString(userObj, "wsid", LONGSTR);
-	oilsFMSetString(userObj, "ws_ou", orgid);
-	free(orgid);
-	jsonObjectFree(workstation);
-	return NULL;
-}
+    } else {
+        osrfLogError(OSRF_LOG_MARK, 
+            "Error caching auth session in open-ils.auth_internal");
+    }
 
+    return response;
+}
 
 
 /**
@@ -928,24 +758,6 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
 		return 0;
 	}
 
-	// If a workstation is defined, add the workstation info
-	if( workstation != NULL ) {
-		osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
-		response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
-		if(response) {
-			jsonObjectFree(userObj);
-			osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
-			oilsEventFree(response);
-			return 0;
-		}
-
-	} else {
-		// Otherwise, use the home org as the workstation org on the user
-		char* orgid = oilsFMGetString(userObj, "home_ou");
-		oilsFMSetString(userObj, "ws_ou", orgid);
-		free(orgid);
-	}
-
 	char* freeable_uname = NULL;
 	if(!uname) {
 		uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
diff --git a/Open-ILS/src/c-apps/oils_auth_internal.c b/Open-ILS/src/c-apps/oils_auth_internal.c
new file mode 100644
index 0000000..38c5166
--- /dev/null
+++ b/Open-ILS/src/c-apps/oils_auth_internal.c
@@ -0,0 +1,319 @@
+#include "opensrf/osrf_app_session.h"
+#include "opensrf/osrf_application.h"
+#include "opensrf/osrf_settings.h"
+#include "opensrf/osrf_json.h"
+#include "opensrf/log.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_constants.h"
+#include "openils/oils_event.h"
+
+#define OILS_AUTH_CACHE_PRFX "oils_auth_"
+#define OILS_AUTH_COUNT_SFFX "_count"
+
+#define MODULENAME "open-ils.auth_internal"
+
+#define OILS_AUTH_OPAC "opac"
+#define OILS_AUTH_STAFF "staff"
+#define OILS_AUTH_TEMP "temp"
+#define OILS_AUTH_PERSIST "persist"
+
+// Default time for extending a persistent session: ten minutes
+#define DEFAULT_RESET_INTERVAL 10 * 60
+
+int osrfAppInitialize();
+int osrfAppChildInit();
+
+static long _oilsAuthOPACTimeout = 0;
+static long _oilsAuthStaffTimeout = 0;
+static long _oilsAuthOverrideTimeout = 0;
+static long _oilsAuthPersistTimeout = 0;
+
+/**
+    @brief Initialize the application by registering functions for method calls.
+    @return Zero on success, 1 on error.
+*/
+int osrfAppInitialize() {
+
+    osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Internal Server...");
+
+    /* load and parse the IDL */
+    /* return non-zero to indicate error */
+    if (!oilsInitIDL(NULL)) return 1; 
+
+    osrfAppRegisterMethod(
+        MODULENAME,
+        "open-ils.auth_internal.session.create",
+        "oilsAutInternalCreateSession",
+        "Adds a user to the authentication cache to indicate "
+        "the user is authenticated", 1, 0 
+    );
+
+    return 0;
+}
+
+/**
+    @brief Dummy placeholder for initializing a server drone.
+
+    There is nothing to do, so do nothing.
+*/
+int osrfAppChildInit() {
+    return 0;
+}
+
+
+/**
+    @brief Determine the login timeout.
+    @param userObj Pointer to an object describing the user.
+    @param type Pointer to one of four possible character strings identifying the login type.
+    @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
+    @return The length of the timeout, in seconds.
+
+    The default timeout value comes from the configuration file, and
+    depends on the login type.
+
+    The default may be overridden by a corresponding org unit setting.
+    The @a orgloc parameter says what org unit to use for the lookup.
+    If @a orgloc <= 0, or if the lookup for @a orgloc yields no result,
+    we look up the setting for the user's home org unit instead (except
+    that if it's the same as @a orgloc we don't bother repeating the
+    lookup).
+
+    Whether defined in the config file or in an org unit setting, a
+    timeout value may be expressed as a raw number (i.e. all digits,
+    possibly with leading and/or trailing white space) or as an interval
+    string to be translated into seconds by PostgreSQL.
+*/
+static long oilsAuthGetTimeout(
+    const jsonObject* userObj, const char* type, int orgloc) {
+
+    if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
+
+        jsonObject* value_obj;
+
+        value_obj = osrf_settings_host_value_object(
+            "/apps/open-ils.auth/app_settings/default_timeout/opac" );
+        _oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+        jsonObjectFree(value_obj);
+        if( -1 == _oilsAuthOPACTimeout ) {
+            osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
+            _oilsAuthOPACTimeout = 0;
+        }
+
+        value_obj = osrf_settings_host_value_object(
+            "/apps/open-ils.auth/app_settings/default_timeout/staff" );
+        _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+        jsonObjectFree(value_obj);
+        if( -1 == _oilsAuthStaffTimeout ) {
+            osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
+            _oilsAuthStaffTimeout = 0;
+        }
+
+        value_obj = osrf_settings_host_value_object(
+            "/apps/open-ils.auth/app_settings/default_timeout/temp" );
+        _oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+        jsonObjectFree(value_obj);
+        if( -1 == _oilsAuthOverrideTimeout ) {
+            osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
+            _oilsAuthOverrideTimeout = 0;
+        }
+
+        value_obj = osrf_settings_host_value_object(
+            "/apps/open-ils.auth/app_settings/default_timeout/persist" );
+        _oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+        jsonObjectFree(value_obj);
+        if( -1 == _oilsAuthPersistTimeout ) {
+            osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
+            _oilsAuthPersistTimeout = 0;
+        }
+
+        osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
+            "opac => %ld : staff => %ld : temp => %ld : persist => %ld",
+            _oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
+            _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
+    }
+
+    int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
+    if(orgloc < 1)
+        orgloc = home_ou;
+
+    char* setting = NULL;
+    long default_timeout = 0;
+
+    if( !strcmp( type, OILS_AUTH_OPAC )) {
+        setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
+        default_timeout = _oilsAuthOPACTimeout;
+    } else if( !strcmp( type, OILS_AUTH_STAFF )) {
+        setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
+        default_timeout = _oilsAuthStaffTimeout;
+    } else if( !strcmp( type, OILS_AUTH_TEMP )) {
+        setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
+        default_timeout = _oilsAuthOverrideTimeout;
+    } else if( !strcmp( type, OILS_AUTH_PERSIST )) {
+        setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
+        default_timeout = _oilsAuthPersistTimeout;
+    }
+
+    // Get the org unit setting, if there is one.
+    char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
+    if(!timeout) {
+        if( orgloc != home_ou ) {
+            osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
+                "trying home_ou %d", orgloc, home_ou );
+            timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
+        }
+    }
+
+    if(!timeout)
+        return default_timeout;   // No override from org unit setting
+
+    // Translate the org unit setting to a number
+    long t;
+    if( !*timeout ) {
+        osrfLogWarning( OSRF_LOG_MARK,
+            "Timeout org unit setting is an empty string for %s login; using default",
+            timeout, type );
+        t = default_timeout;
+    } else {
+        // Treat timeout string as an interval, and convert it to seconds
+        t = oilsUtilsIntervalToSeconds( timeout );
+        if( -1 == t ) {
+            // Unable to convert; possibly an invalid interval string
+            osrfLogError( OSRF_LOG_MARK,
+                "Unable to convert timeout interval \"%s\" for %s login; using default",
+                timeout, type );
+            t = default_timeout;
+        }
+    }
+
+    free(timeout);
+    return t;
+}
+
+/**
+ * Verify workstation exists and stuff it into the user object to be cached
+ */
+static oilsEvent* oilsAuthVerifyWorkstation(
+        const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
+
+    jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
+
+    if(!workstation || workstation->type == JSON_NULL) {
+        jsonObjectFree(workstation);
+        return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
+    }
+
+    long wsid = oilsFMGetObjectId(workstation);
+    LONG_TO_STRING(wsid);
+    char* orgid = oilsFMGetString(workstation, "owning_lib");
+    oilsFMSetString(userObj, "wsid", LONGSTR);
+    oilsFMSetString(userObj, "ws_ou", orgid);
+    free(orgid);
+    jsonObjectFree(workstation);
+    return NULL;
+}
+
+
+/**
+    @brief Implement the session create method
+    @param ctx The method context.
+    @return -1 upon error; zero if successful, and if a STATUS message has 
+    been sent to the client to indicate completion; a positive integer if 
+    successful but no such STATUS message has been sent.
+
+    Method parameters:
+    - a hash with some combination of the following elements:
+        - "user_id"     -- actor.usr (au) ID for the user to cache.
+        - "org_unit"    -- actor.org_unit (aou) ID representing the physical 
+                           location / context used for timeout, etc. settings.
+        - "login_type"  -- login type (opac, staff, temp, persist)
+        - "workstation" -- workstation name
+
+*/
+int oilsAutInternalCreateSession(osrfMethodContext* ctx) {
+    OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+    const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
+
+    const char* user_id     = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
+    const char* org_unit    = jsonObjectGetString(jsonObjectGetKeyConst(args, "org_unit"));
+    const char* login_type  = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
+    const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
+
+    if ( !(user_id && login_type && org_unit) ) {
+        return osrfAppRequestRespondException( ctx->session, ctx->request,
+            "Missing parameters for method: %s", ctx->method->name );
+    }
+
+	oilsEvent* response = NULL;
+
+    // fetch the user object
+    jsonObject* idParam = jsonNewNumberStringObject(user_id);
+    jsonObject* userObj = oilsUtilsCStoreReq(
+        "open-ils.cstore.direct.actor.user.retrieve", idParam);
+    jsonObjectFree(idParam);
+
+    if (!userObj) {
+        return osrfAppRequestRespondException(ctx->session, 
+            ctx->request, "No user found with ID %s", user_id);
+    }
+
+    // If a workstation is defined, add the workstation info
+    if (workstation) {
+        response = oilsAuthVerifyWorkstation(ctx, userObj, workstation);
+        if (response) {
+            jsonObjectFree(userObj);
+            osrfAppRespondComplete(ctx, oilsEventToJSON(response));
+            oilsEventFree(response);
+            return 0;
+        }
+
+    } else {
+        // Otherwise, use the home org as the workstation org on the user
+        char* orgid = oilsFMGetString(userObj, "home_ou");
+        oilsFMSetString(userObj, "ws_ou", orgid);
+        free(orgid);
+    }
+
+    // determine the auth/cache timeout
+    long timeout = oilsAuthGetTimeout(userObj, login_type, atoi(org_unit));
+
+    char* string = va_list_to_string("%d.%ld.%ld", 
+        (long) getpid(), time(NULL), oilsFMGetObjectId(userObj));
+    char* authToken = md5sum(string);
+    char* authKey = va_list_to_string(
+        "%s%s", OILS_AUTH_CACHE_PRFX, authToken);
+
+    oilsFMSetString(userObj, "passwd", "");
+    jsonObject* cacheObj = jsonParseFmt("{\"authtime\": %ld}", timeout);
+    jsonObjectSetKey(cacheObj, "userobj", jsonObjectClone(userObj));
+
+    if( !strcmp(login_type, OILS_AUTH_PERSIST)) {
+        // Add entries for endtime and reset_interval, so that we can gracefully
+        // extend the session a bit if the user is active toward the end of the 
+        // timeout originally specified.
+        time_t endtime = time( NULL ) + timeout;
+        jsonObjectSetKey(cacheObj, "endtime", 
+            jsonNewNumberObject( (double) endtime ));
+
+        // Reset interval is hard-coded for now, but if we ever want to make it
+        // configurable, this is the place to do it:
+        jsonObjectSetKey(cacheObj, "reset_interval",
+            jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL));
+    }
+
+    osrfCachePutObject(authKey, cacheObj, (time_t) timeout);
+    jsonObjectFree(cacheObj);
+    jsonObject* payload = jsonParseFmt(
+        "{\"authtoken\": \"%s\", \"authtime\": %ld}", authToken, timeout);
+
+    response = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
+    free(string); free(authToken); free(authKey);
+    jsonObjectFree(payload);
+
+    jsonObjectFree(userObj);
+    osrfAppRespondComplete(ctx, oilsEventToJSON(response));
+    oilsEventFree(response);
+
+    return 0;
+}
+

commit 08b06012de32a612a33fb2f73aecbf439c155035
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jul 24 11:20:06 2015 -0400

    LP#1468422 Add libpcre to build for open-ils.auth
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/c-apps/Makefile.am b/Open-ILS/src/c-apps/Makefile.am
index 77d48e5..06fdf7c 100644
--- a/Open-ILS/src/c-apps/Makefile.am
+++ b/Open-ILS/src/c-apps/Makefile.am
@@ -58,7 +58,7 @@ oils_pcrud_la_LDFLAGS = $(AM_LDFLAGS) -ldbi -ldbdpgsql -loils_utils -module -ver
 oils_pcrud_la_DEPENDENCIES = liboils_utils.la
 
 oils_auth_la_SOURCES = oils_auth.c
-oils_auth_la_LDFLAGS = -module -loils_utils -version-info 2:0:0
+oils_auth_la_LDFLAGS = -module -loils_utils -lpcre -version-info 2:0:0
 oils_auth_la_DEPENDENCIES = liboils_utils.la
 
 
diff --git a/configure.ac b/configure.ac
index 585ab00..74e6b3b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -311,6 +311,8 @@ if test "x$openils_core" = "xtrue"; then
     AC_CHECK_LIB([xml2], [main], [], AC_MSG_ERROR(*** OpenILS requires libxml2))
     AC_CHECK_LIB([xslt], [main], [], AC_MSG_ERROR(*** OpenILS requires libxslt))
     AC_CHECK_LIB([pq], [main], [], AC_MSG_ERROR(*** OpenILS requires libpq))
+    PKG_CHECK_MODULES(pcre, libpcre >= 3.0.0)
+
 
     #------------------------------------
     # Checks for header files.

commit 5deb393c01d400a24f38a6e6324cc2181be3480a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jul 22 18:02:19 2015 -0400

    LP#1468422 open-ils.auth API salted pw changes
    
    Added .init.barcode and .init.username methods.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/c-apps/oils_auth.c b/Open-ILS/src/c-apps/oils_auth.c
index bd79770..587fbb4 100644
--- a/Open-ILS/src/c-apps/oils_auth.c
+++ b/Open-ILS/src/c-apps/oils_auth.c
@@ -6,6 +6,7 @@
 #include "openils/oils_utils.h"
 #include "openils/oils_constants.h"
 #include "openils/oils_event.h"
+#include <pcre.h>
 
 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
 #define OILS_AUTH_COUNT_SFFX "_count"
@@ -50,6 +51,20 @@ int osrfAppInitialize() {
 		"Start the authentication process and returns the intermediate authentication seed"
 		" PARAMS( username )", 1, 0 );
 
+    osrfAppRegisterMethod(
+        MODULENAME,
+        "open-ils.auth.authenticate.init.barcode",
+        "oilsAuthInitBarcode",
+        "Start the authentication process using a patron barcode and return "
+        "the intermediate authentication seed. PARAMS(barcode)", 1, 0);
+
+    osrfAppRegisterMethod(
+        MODULENAME,
+        "open-ils.auth.authenticate.init.username",
+        "oilsAuthInitUsername",
+        "Start the authentication process using a patron username and return "
+        "the intermediate authentication seed. PARAMS(username)", 1, 0);
+
 	osrfAppRegisterMethod(
 		MODULENAME,
 		"open-ils.auth.authenticate.complete",
@@ -144,78 +159,265 @@ int osrfAppChildInit() {
 	return 0;
 }
 
-/**
-	@brief Implement the "init" method.
-	@param ctx The method context.
-	@return Zero if successful, or -1 if not.
+// free() response
+static char* oilsAuthGetSalt(int user_id) {
+    char* salt_str = NULL;
 
-	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.
+    jsonObject* params = jsonParseFmt( // free
+        "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, "main");
 
-	Return to client: Intermediate authentication seed.
+    jsonObject* salt_obj = // free
+        oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
 
-	Combine the username with a timestamp and process ID, and take an md5 hash of the result.
-	Store the hash in memcache, with a key based on the username.  Then return the hash to
-	the client.
+    jsonObjectFree(params);
 
-	However: if the username includes one or more embedded blank spaces, return a dummy
-	hash without storing anything in memcache.  The dummy will never match a stored hash, so
-	any attempt to authenticate with it will fail.
-*/
-int oilsAuthInit( osrfMethodContext* ctx ) {
-	OSRF_METHOD_VERIFY_CONTEXT(ctx);
+    if (salt_obj) {
 
-	char* username  = jsonObjectToSimpleString( jsonObjectGetIndex(ctx->params, 0) );
-	const char* nonce = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
-	if (!nonce) nonce = "";
+        if (salt_obj->type != JSON_NULL) {
 
-	if( username ) {
+            const char* salt_val = jsonObjectGetString(
+                jsonObjectGetKeyConst(salt_obj, "actor.get_salt"));
 
-		jsonObject* resp;
+            // caller expects a free-able string, could be NULL.
+            if (salt_val) { salt_str = strdup(salt_val); } 
+        }
 
-		if( strchr( username, ' ' ) ) {
+        jsonObjectFree(salt_obj);
+    }
 
-			// Embedded spaces are not allowed in a username.  Use "x" as a dummy
-			// seed.  It will never be a valid seed because 'x' is not a hex digit.
-			resp = jsonNewObject( "x" );
+    return salt_str;
+}
 
-		} else {
+// ident is either a username or barcode
+// Returns the init seed -> requires free();
+static char* oilsAuthBuildInitCache(
+    int user_id, const char* ident, const char* ident_type, const char* nonce) {
 
-			// Build a key and a seed; store them in memcache.
-			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.%s", (int) time(NULL), (long) getpid(), username, nonce );
-			jsonObject* countobject = osrfCacheGetObject( countkey );
-			if(!countobject) {
-				countobject = jsonNewNumberObject( (double) 0 );
-			}
-			osrfCachePutString( key, seed, _oilsAuthSeedTimeout );
-			osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
+    char* cache_key  = va_list_to_string(
+        "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce);
 
-			osrfLogDebug( OSRF_LOG_MARK, "oilsAuthInit(): has seed %s and key %s", seed, key );
+    char* count_key = va_list_to_string(
+        "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX);
 
-			// Build a returnable object containing the seed.
-			resp = jsonNewObject( seed );
+    char* auth_seed = oilsAuthGetSalt(user_id);
 
-			free( seed );
-			free( key );
-			free( countkey );
-			jsonObjectFree( countobject );
-		}
+    jsonObject* seed_object = jsonParseFmt(
+        "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
+        ident_type, ident, user_id, auth_seed);
 
-		// Return the seed to the client.
-		osrfAppRespondComplete( ctx, resp );
+    jsonObject* count_object = osrfCacheGetObject(count_key);
+    if(!count_object) {
+        count_object = jsonNewNumberObject((double) 0);
+    }
 
-		jsonObjectFree(resp);
-		free(username);
-		return 0;
-	}
+    osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
+    osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
+
+    osrfLogDebug(OSRF_LOG_MARK, 
+        "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
+
+    free(cache_key);
+    free(count_key);
+    jsonObjectFree(count_object);
+    jsonObjectFree(seed_object);
+
+    return auth_seed;
+}
+
+static int oilsAuthInitUsernameHandler(
+    osrfMethodContext* ctx, const char* username, const char* nonce) {
+
+    osrfLogInfo(OSRF_LOG_MARK, 
+        "User logging in with username %s", username);
+
+    jsonObject* resp = NULL; // free
+    jsonObject* user_obj = oilsUtilsFetchUserByUsername(username); // free
+
+    if (user_obj) {
+
+        if (JSON_NULL == user_obj->type) { // user not found
+            resp = jsonNewObject("x");
+
+        } else {
+            char* seed = oilsAuthBuildInitCache(
+                oilsFMGetObjectId(user_obj), username, "username", nonce);
+            resp = jsonNewObject(seed);
+            free(seed);
+        }
+
+        jsonObjectFree(user_obj);
+
+    } else {
+        resp = jsonNewObject("x");
+    }
+
+    osrfAppRespondComplete(ctx, resp);
+    jsonObjectFree(resp);
+    return 0;
+}
+
+// open-ils.auth.authenticate.init.username
+int oilsAuthInitUsername(osrfMethodContext* ctx) {
+    OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+    char* username =  // free
+        jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
+    const char* nonce = 
+        jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
+
+    if (!nonce) nonce = "";
+    if (!username) return -1;
+
+    int resp = oilsAuthInitUsernameHandler(ctx, username, nonce);
+
+    free(username);
+    return resp;
+}
+
+static int oilsAuthInitBarcodeHandler(
+    osrfMethodContext* ctx, const char* barcode, const char* nonce) {
+
+    osrfLogInfo(OSRF_LOG_MARK, 
+        "User logging in with barcode %s", barcode);
+
+    jsonObject* resp = NULL; // free
+    jsonObject* user_obj = oilsUtilsFetchUserByBarcode(barcode); // free
+
+    if (user_obj) {
+        if (JSON_NULL == user_obj->type) { // not found
+            resp = jsonNewObject("x");
+        } else {
+            char* seed = oilsAuthBuildInitCache(
+                oilsFMGetObjectId(user_obj), barcode, "barcode", nonce);
+            resp = jsonNewObject(seed);
+            free(seed);
+        }
+
+        jsonObjectFree(user_obj);
+    } else {
+        resp = jsonNewObject("x");
+    }
+
+    osrfAppRespondComplete(ctx, resp);
+    jsonObjectFree(resp);
+    return 0;
+}
+
+
+// open-ils.auth.authenticate.init.barcode
+int oilsAuthInitBarcode(osrfMethodContext* ctx) {
+    OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+    char* barcode = // free
+        jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
+    const char* nonce = 
+        jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
 
-	return -1;  // Error: no username parameter
+    if (!nonce) nonce = "";
+    if (!barcode) return -1;
+
+    int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce);
+
+    free(barcode);
+    return resp;
+}
+
+// returns true if the provided identifier matches the barcode regex.
+static int oilsAuthIdentIsBarcode(const char* identifier) {
+
+    // before we can fetch the barcode regex unit setting,
+    // first determine what the root org unit ID is.
+    // TODO: add an org_unit param to the .init API for future use?
+    
+    jsonObject *params = jsonParse("{\"parent_ou\":null}");
+    jsonObject *org_unit_id = oilsUtilsCStoreReq(
+        "open-ils.cstore.direct.actor.org_unit.id_list", params);
+    jsonObjectFree(params);
+
+    char* bc_regex = oilsUtilsFetchOrgSetting(
+        (int) jsonObjectGetNumber(org_unit_id), "opac.barcode_regex");
+    jsonObjectFree(org_unit_id);
+
+    if (!bc_regex) {
+        // if no regex is set, assume any identifier starting
+        // with a number is a barcode.
+        bc_regex = strdup("^\\d"); // dupe for later free'ing
+    }
+
+    const char *err_str;
+    int err_offset, match_ret;
+
+    pcre *compiled = pcre_compile(
+        bc_regex, 0, &err_str, &err_offset, NULL);
+
+    if (compiled == NULL) {
+        osrfLogError(OSRF_LOG_MARK,
+            "Could not compile '%s': %s", bc_regex, err_str);
+        free(bc_regex);
+        pcre_free(compiled);
+        return 0;
+    }
+
+    pcre_extra *extra = pcre_study(compiled, 0, &err_str);
+
+    if(err_str != NULL) {
+        osrfLogError(OSRF_LOG_MARK,
+            "Could not study regex '%s': %s", bc_regex, err_str);
+        free(bc_regex);
+        pcre_free(compiled);
+        return 0;
+    } 
+
+    match_ret = pcre_exec(
+        compiled, extra, identifier, strlen(identifier), 0, 0, NULL, 0);       
+
+    free(bc_regex);
+    pcre_free(compiled);
+    if (extra) pcre_free(extra);
+
+    if (match_ret >= 0) return 1; // regex matched
+
+    if (match_ret != PCRE_ERROR_NOMATCH) 
+        osrfLogError(OSRF_LOG_MARK, "Unknown error processing barcode regex");
+
+    return 0; // regex did not match
+}
+
+
+/**
+	@brief Implement the "init" method.
+	@param ctx The method context.
+	@return Zero if successful, or -1 if not.
+
+	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.
+*/
+int oilsAuthInit(osrfMethodContext* ctx) {
+    OSRF_METHOD_VERIFY_CONTEXT(ctx);
+    int resp = 0;
+
+    char* identifier = // free
+        jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
+    const char* nonce = 
+        jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
+
+    if (!nonce) nonce = "";
+    if (!identifier) return -1;  // we need an identifier
+
+    if (oilsAuthIdentIsBarcode(identifier)) {
+        resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce);
+    } else {
+        resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce);
+    }
+
+    free(identifier);
+    return resp;
 }
 
 /**
@@ -281,68 +483,56 @@ static int oilsAuthCheckLoginPerm(
 	means that the client process needs either to be the same process that called the init
 	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 char* nonce ) {
-
-	// Get the username seed, as stored previously in memcache by the init method
-	char* seed = osrfCacheGetString( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, nonce );
-	if(!seed) {
-		return osrfAppRequestRespondException( ctx->session,
-			ctx->request, "No authentication seed found. "
-			"open-ils.auth.authenticate.init must be called first "
-			" (check that memcached is running and can be connected to) "
-		);
-	}
-    
-	// We won't be needing the seed again, remove it
-	osrfCacheRemove( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, nonce );
+static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, 
+        const char* identifier, const char* password, const char* nonce) {
 
-	// Get the hashed password from the user object
-	char* realPassword = oilsFMGetString( userObj, "passwd" );
+    int verified = 0;
 
-	osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
-	osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );
+    // We won't be needing the seed again, remove it
+    osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
 
-	// Concatenate them and take an MD5 hash of the result
-	char* maskedPw = md5sum( "%s%s", seed, realPassword );
+    // Ask the DB to verify the user's password.
+    // Here, the password is md5(md5(password) + salt)
 
-	free(realPassword);
-	free(seed);
+    jsonObject* params = jsonParseFmt( // free
+        "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}", 
+        user_id, password);
 
-	if( !maskedPw ) {
-		// This happens only if md5sum() runs out of memory
-		free( maskedPw );
-		return -1;  // md5sum() ran out of memory
-	}
+    jsonObject* verify_obj = // free 
+        oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
 
-	osrfLogDebug(OSRF_LOG_MARK,  "oilsAuth generated masked password %s. "
-			"Testing against provided password %s", maskedPw, password );
+    jsonObjectFree(params);
 
-	int ret = 0;
-	if( !strcmp( maskedPw, password ) )
-		ret = 1;
+    if (verify_obj) {
+        verified = oilsUtilsIsDBTrue(
+            jsonObjectGetString(
+                jsonObjectGetKeyConst(
+                    verify_obj, "actor.verify_passwd")));
 
-	free(maskedPw);
+        jsonObjectFree(verify_obj);
+    }
 
-	char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, OILS_AUTH_COUNT_SFFX );
-	jsonObject* countobject = osrfCacheGetObject( countkey );
-	if(countobject) {
-		long failcount = (long) jsonObjectGetNumber( countobject );
-		if(failcount >= _oilsAuthBlockCount) {
-			ret = 0;
-		    osrfLogInfo(OSRF_LOG_MARK, "oilsAuth found too many recent failures for '%s' : %i, forcing failure state.", uname, failcount);
-		}
-		if(ret == 0) {
-			failcount += 1;
-		}
-		jsonObjectSetNumber( countobject, failcount );
-		osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
-		jsonObjectFree(countobject);
-	}
-	free(countkey);
+    char* countkey = va_list_to_string("%s%s%s", 
+        OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
+    jsonObject* countobject = osrfCacheGetObject( countkey );
+    if(countobject) {
+        long failcount = (long) jsonObjectGetNumber( countobject );
+        if(failcount >= _oilsAuthBlockCount) {
+            verified = 0;
+            osrfLogInfo(OSRF_LOG_MARK, 
+                "oilsAuth found too many recent failures for '%s' : %i, "
+                "forcing failure state.", identifier, failcount);
+        }
+        if(verified == 0) {
+            failcount += 1;
+        }
+        jsonObjectSetNumber( countobject, failcount );
+        osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
+        jsonObjectFree(countobject);
+    }
+    free(countkey);
 
-	return ret;
+    return verified;
 }
 
 /**
@@ -584,92 +774,107 @@ static oilsEvent* oilsAuthVerifyWorkstation(
 	Upon deciding whether to allow the logon, return a corresponding event to the client.
 */
 int oilsAuthComplete( osrfMethodContext* ctx ) {
-	OSRF_METHOD_VERIFY_CONTEXT(ctx);
+    OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+    const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
+
+    const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
+    const char* identifier  = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
+    const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
+    const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
+    int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
+    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 = "";
+
+    // we no longer care how the identifier reaches us, 
+    // as long as we have one.
+    if (!identifier) {
+        if (uname) {
+            identifier = uname;
+        } else if (barcode) {
+            identifier = barcode;
+        }
+    }
 
-	const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
+    if (!identifier) {
+        return osrfAppRequestRespondException(ctx->session, ctx->request,
+            "username/barcode and password required for method: %s", 
+            ctx->method->name);
+    }
 
-	const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
-	const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
-	const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
-	int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
-	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"));
+    osrfLogInfo(OSRF_LOG_MARK, 
+        "Patron completing authentication with identifer %s", identifier);
+
+    /* Use __FILE__, harmless_line_number for creating
+     * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
+     * giving away information about why an authentication attempt failed.
+     */
+    int harmless_line_number = __LINE__;
+
+    if( !type )
+         type = OILS_AUTH_STAFF;
+
+    oilsEvent* response = NULL; // free
+    jsonObject* userObj = NULL; // free
+    int card_active = 1; // boolean; assume active until proven otherwise
+    int using_card  = 0; // true if this is a barcode login
+
+    char* cache_key = va_list_to_string(
+        "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
+    jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
+
+    if (!cacheObj) {
+        return osrfAppRequestRespondException(ctx->session,
+            ctx->request, "No authentication seed found. "
+            "open-ils.auth.authenticate.init must be called first "
+            " (check that memcached is running and can be connected to) "
+        );
+    }
 
-	const char* ws = (workstation) ? workstation : "";
-	if (!nonce) nonce = "";
+    int user_id = jsonObjectGetNumber(
+        jsonObjectGetKeyConst(cacheObj, "user_id"));
 
-	/* Use __FILE__, harmless_line_number for creating
-	 * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
-	 * giving away information about why an authentication attempt failed.
-	 */
-	int harmless_line_number = __LINE__;
+    jsonObject* param = jsonNewNumberObject(user_id); // free
+    userObj = oilsUtilsCStoreReq(
+        "open-ils.cstore.direct.actor.user.retrieve", param);
+    jsonObjectFree(param);
 
-	if( !type )
-		 type = OILS_AUTH_STAFF;
+    using_card = (jsonObjectGetKeyConst(cacheObj, "barcode") != NULL);
 
-	if( !( (uname || barcode) && password) ) {
-		return osrfAppRequestRespondException( ctx->session, ctx->request,
-			"username/barcode and password required for method: %s", ctx->method->name );
-	}
+    if (using_card) {
+        // see if the card is inactive
 
-	oilsEvent* response = NULL;
-	jsonObject* userObj = NULL;
-	int card_active     = 1;      // boolean; assume active until proven otherwise
-
-	// Fetch a row from the actor.usr table, by username if available,
-	// or by barcode if not.
-	if(uname) {
-		userObj = oilsUtilsFetchUserByUsername( uname );
-		if( userObj && JSON_NULL == userObj->type ) {
-			jsonObjectFree( userObj );
-			userObj = NULL;         // username not found
-		}
-	}
-	else if(barcode) {
-		// Read from actor.card by barcode
-
-		osrfLogInfo( OSRF_LOG_MARK, "Fetching user by barcode %s", barcode );
-
-		jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
-		jsonObject* card = oilsUtilsQuickReq(
-			"open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
-		jsonObjectFree( params );
-
-		if( card && card->type != JSON_NULL ) {
-			// Determine whether the card is active
-			char* card_active_str = oilsFMGetString( card, "active" );
-			card_active = oilsUtilsIsDBTrue( card_active_str );
-			free( card_active_str );
-
-			// Look up the user who owns the card
-			char* userid = oilsFMGetString( card, "usr" );
-			jsonObjectFree( card );
-			params = jsonParseFmt( "[%s]", userid );
-			free( userid );
-			userObj = oilsUtilsQuickReq(
-					"open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params );
-			jsonObjectFree( params );
-			if( userObj && JSON_NULL == userObj->type ) {
-				// user not found (shouldn't happen, due to foreign key)
-				jsonObjectFree( userObj );
-				userObj = NULL;
+		jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", identifier);
+		jsonObject* card = oilsUtilsCStoreReq(
+			"open-ils.cstore.direct.actor.card.search", params);
+		jsonObjectFree(params);
+
+        if (card) {
+            if (card->type != JSON_NULL) {
+			    char* card_active_str = oilsFMGetString(card, "active");
+			    card_active = oilsUtilsIsDBTrue(card_active_str);
+			    free(card_active_str);
 			}
+            jsonObjectFree(card);
 		}
 	}
 
 	int     barred = 0, deleted = 0;
 	char   *barred_str, *deleted_str;
 
-	if(userObj) {
-		barred_str = oilsFMGetString( userObj, "barred" );
-		barred = oilsUtilsIsDBTrue( barred_str );
-		free( barred_str );
+	if (userObj) {
+		barred_str = oilsFMGetString(userObj, "barred");
+		barred = oilsUtilsIsDBTrue(barred_str);
+		free(barred_str);
 
-		deleted_str = oilsFMGetString( userObj, "deleted" );
-		deleted = oilsUtilsIsDBTrue( deleted_str );
-		free( deleted_str );
+		deleted_str = oilsFMGetString(userObj, "deleted");
+		deleted = oilsUtilsIsDBTrue(deleted_str);
+		free(deleted_str);
 	}
 
 	if(!userObj || barred || deleted) {
@@ -683,11 +888,8 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
 
 	// Such a user exists and isn't barred or deleted.
 	// Now see if he or she has the right credentials.
-	int passOK = -1;
-	if(uname)
-		passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password, nonce );
-	else if (barcode)
-		passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password, nonce );
+	int passOK = oilsAuthVerifyPassword(
+        ctx, user_id, identifier, password, nonce);
 
 	if( passOK < 0 ) {
 		jsonObjectFree(userObj);
@@ -710,8 +912,6 @@ int oilsAuthComplete( osrfMethodContext* ctx ) {
 	}
 	free(active);
 
-	osrfLogInfo( OSRF_LOG_MARK, "Fetching card by barcode %s", barcode );
-
 	if( !card_active ) {
 		osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
 		response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
@@ -815,7 +1015,7 @@ static int _oilsAuthReloadUser(jsonObject* cacheObj) {
     int reqid, userId;
     osrfAppSession* session;
 	osrfMessage* omsg;
-    jsonObject *param, *userObj, *newUserObj;
+    jsonObject *param, *userObj, *newUserObj = NULL;
 
     userObj = jsonObjectGetKey( cacheObj, "userobj" );
     userId = oilsFMGetObjectId( userObj );

commit f47a980e1e19c3e90ebe3189be803a6841807e5f
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jul 17 16:00:17 2015 -0400

    LP#1468422 Password storage/migration SQL getting started
    
    * Backwards compatible salted password storage using pgcrypt
    * Adds actor.passwd and actor.passwd_type tables
    * Includes pgtap tests
    * Includes installation of pgcrypto
    
    Current flow:
    
    1. Application requests a salt to use as the CHAP-style seed
    2. If new-style password exists, salt is returned.
    3. Else, old password is migrated and the new salt is returned.
    4. App finalizes login by checking verify_passwd.
    
    == continued...
    
    Store the iter_count and start using the crypt_algo column.
    
    Make it possible to change the salt, and potentially strengthen
    the salt, when changing passwords.
    
    Make is possible to start salt-less passwords, for pw's that are managed
    outside of the DB.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Dan Wells <dbw2 at calvin.edu>

diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql
index c023977..912b2fc 100644
--- a/Open-ILS/src/sql/Pg/005.schema.actors.sql
+++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql
@@ -812,4 +812,210 @@ CREATE TRIGGER restrict_usr_message_limited_tgr
     INSTEAD OF UPDATE OR INSERT OR DELETE ON actor.usr_message_limited
     FOR EACH ROW EXECUTE PROCEDURE actor.restrict_usr_message_limited();
 
+CREATE TABLE actor.passwd_type (
+    code        TEXT PRIMARY KEY,
+    name        TEXT UNIQUE NOT NULL,
+    login       BOOLEAN NOT NULL DEFAULT FALSE,
+    regex       TEXT,   -- pending
+    crypt_algo  TEXT,   -- e.g. 'bf'
+
+    -- gen_salt() iter count used with each new salt.
+    -- A non-NULL value for iter_count is our indication the 
+    -- password is salted and encrypted via crypt()
+    iter_count  INTEGER CHECK (iter_count IS NULL OR iter_count > 0)
+);
+
+CREATE TABLE actor.passwd (
+    id          SERIAL PRIMARY KEY,
+    usr         INTEGER NOT NULL REFERENCES actor.usr(id)
+                ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    salt        TEXT, -- will be NULL for non-crypt'ed passwords
+    passwd      TEXT NOT NULL,
+    passwd_type TEXT NOT NULL REFERENCES actor.passwd_type(code)
+                DEFERRABLE INITIALLY DEFERRED,
+    create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+    edit_date   TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+    CONSTRAINT  passwd_type_once_per_user UNIQUE (usr, passwd_type)
+);
+
+CREATE OR REPLACE FUNCTION actor.create_salt(pw_type TEXT)
+    RETURNS TEXT AS $$
+DECLARE
+    type_row actor.passwd_type%ROWTYPE;
+BEGIN
+    /* Returns a new salt based on the passwd_type encryption settings.
+     * Returns NULL If the password type is not crypt()'ed.
+     */
+
+    SELECT INTO type_row * FROM actor.passwd_type WHERE code = pw_type;
+
+    IF NOT FOUND THEN
+        RETURN EXCEPTION 'No such password type: %', pw_type;
+    END IF;
+
+    IF type_row.iter_count IS NULL THEN
+        -- This password type is unsalted.  That's OK.
+        RETURN NULL;
+    END IF;
+
+    RETURN gen_salt(type_row.crypt_algo, type_row.iter_count);
+END;
+$$ LANGUAGE PLPGSQL;
+
+
+/* 
+    TODO: when a user changes their password in the application, the
+    app layer has access to the bare password.  At that point, we have
+    the opportunity to store the new password without the MD5(MD5())
+    intermediate hashing.  Do we care?  We would need a way to indicate
+    which passwords have the legacy intermediate hashing and which don't
+    so the app layer would know whether it should perform the intermediate
+    hashing.  In either event, with the exception of migrate_passwd(), the
+    DB functions know or care nothing about intermediate hashing.  Every
+    password is just a value that may or may not be internally crypt'ed. 
+*/
+
+CREATE OR REPLACE FUNCTION actor.set_passwd(
+    pw_usr INTEGER, pw_type TEXT, new_pass TEXT, new_salt TEXT DEFAULT NULL)
+    RETURNS BOOLEAN AS $$
+DECLARE
+    pw_salt TEXT;
+    pw_text TEXT;
+BEGIN
+    /* Sets the password value, creating a new actor.passwd row if needed.
+     * If the password type supports it, the new_pass value is crypt()'ed.
+     * For crypt'ed passwords, the salt comes from one of 3 places in order:
+     * new_salt (if present), existing salt (if present), newly created 
+     * salt.
+     */
+
+    IF new_salt IS NOT NULL THEN
+        pw_salt := new_salt;
+    ELSE 
+        pw_salt := actor.get_salt(pw_usr, pw_type);
+
+        IF pw_salt IS NULL THEN
+            /* We have no salt for this user + type.  Assume they want a 
+             * new salt.  If this type is unsalted, create_salt() will 
+             * return NULL. */
+            pw_salt := actor.create_salt(pw_type);
+        END IF;
+    END IF;
+
+    IF pw_salt IS NULL THEN 
+        pw_text := new_pass; -- unsalted, use as-is.
+    ELSE
+        pw_text := CRYPT(new_pass, pw_salt);
+    END IF;
+
+    UPDATE actor.passwd 
+        SET passwd = pw_text, salt = pw_salt, edit_date = NOW()
+        WHERE usr = pw_usr AND passwd_type = pw_type;
+
+    IF NOT FOUND THEN
+        -- no password row exists for this user + type.  Create one.
+        INSERT INTO actor.passwd (usr, passwd_type, salt, passwd) 
+            VALUES (pw_usr, pw_type, pw_salt, pw_text);
+    END IF;
+
+    RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION actor.get_salt(pw_usr INTEGER, pw_type TEXT)
+    RETURNS TEXT AS $$
+DECLARE
+    pw_salt TEXT;
+    type_row actor.passwd_type%ROWTYPE;
+BEGIN
+    /* Returns the salt for the requested user + type.  If the password 
+     * type of "main" is requested and no password exists in actor.passwd, 
+     * the user's existing password is migrated and the new salt is returned.
+     * Returns NULL if the password type is not crypt'ed (iter_count is NULL).
+     */
+
+    SELECT INTO pw_salt salt FROM actor.passwd 
+        WHERE usr = pw_usr AND passwd_type = pw_type;
+
+    IF FOUND THEN
+        RETURN pw_salt;
+    END IF;
+
+    IF pw_type = 'main' THEN
+        -- Main password has not yet been migrated. 
+        -- Do it now and return the newly created salt.
+        RETURN actor.migrate_passwd(pw_usr);
+    END IF;
+
+    -- We have no salt to return.  actor.create_salt() needed.
+    RETURN NULL;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION 
+    actor.migrate_passwd(pw_usr INTEGER) RETURNS TEXT AS $$
+DECLARE
+    pw_salt TEXT;
+    usr_row actor.usr%ROWTYPE;
+BEGIN
+    /* Migrates legacy actor.usr.passwd value to actor.passwd with 
+     * a password type 'main' and returns the new salt.  For backwards
+     * compatibility with existing CHAP-style API's, we perform a 
+     * layer of intermediate MD5(MD5()) hashing.  This is intermediate
+     * hashing is not required of other passwords.
+     */
+
+    SELECT INTO usr_row * FROM actor.usr WHERE id = pw_usr;
+
+    pw_salt := actor.create_salt('main');
+
+    PERFORM actor.set_passwd(
+        pw_usr, 'main', MD5(pw_salt || usr_row.passwd), pw_salt);
+
+    -- clear the existing password
+    UPDATE actor.usr SET passwd = '' WHERE id = usr_row.id;
+
+    RETURN pw_salt;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION 
+    actor.verify_passwd(pw_usr INTEGER, pw_type TEXT, test_passwd TEXT) 
+    RETURNS BOOLEAN AS $$
+DECLARE
+    pw_salt TEXT;
+BEGIN
+    /* Returns TRUE if the password provided matches the in-db password.  
+     * If the password type is salted, we compare the output of CRYPT().
+     * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy 
+     * 'main' passwords.
+     */
+
+    SELECT INTO pw_salt salt FROM actor.passwd 
+        WHERE usr = pw_usr AND passwd_type = pw_type;
+
+    IF NOT FOUND THEN
+        -- no such password
+        RETURN FALSE;
+    END IF;
+
+    IF pw_salt IS NULL THEN
+        -- Password is unsalted, compare the un-CRYPT'ed values.
+        RETURN EXISTS (
+            SELECT TRUE FROM actor.passwd WHERE 
+                usr = pw_usr AND
+                passwd_type = pw_type AND
+                passwd = test_passwd
+        );
+    END IF;
+
+    RETURN EXISTS (
+        SELECT TRUE FROM actor.passwd WHERE 
+            usr = pw_usr AND
+            passwd_type = pw_type AND
+            passwd = CRYPT(test_passwd, pw_salt)
+    );
+END;
+$$ STRICT LANGUAGE PLPGSQL;
+
 COMMIT;
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index b804d72..dbea260 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -2438,6 +2438,10 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
 
 
 -- Admin user account
+INSERT INTO actor.passwd_type 
+    (code, name, login, crypt_algo, iter_count) 
+    VALUES ('main', 'Main Login Password', TRUE, 'bf', 14);
+
 INSERT INTO actor.usr ( profile, card, usrname, passwd, first_given_name, family_name, dob, master_account, super_user, ident_type, ident_value, home_ou ) VALUES ( 1, 1, md5(random()::text), md5(random()::text), 'Administrator', 'System Account', '1979-01-22', TRUE, TRUE, 1, 'identification', 1 );
 
 -- Admin user barcode
diff --git a/Open-ILS/src/sql/Pg/create_database_extensions.sql b/Open-ILS/src/sql/Pg/create_database_extensions.sql
index b73a871..b61aa5b 100644
--- a/Open-ILS/src/sql/Pg/create_database_extensions.sql
+++ b/Open-ILS/src/sql/Pg/create_database_extensions.sql
@@ -20,3 +20,4 @@ CREATE EXTENSION tablefunc;
 CREATE EXTENSION xml2;
 CREATE EXTENSION hstore;
 CREATE EXTENSION intarray;
+CREATE EXTENSION pgcrypto;
diff --git a/Open-ILS/src/sql/Pg/live_t/lp1468422_passwd_storage.pg b/Open-ILS/src/sql/Pg/live_t/lp1468422_passwd_storage.pg
new file mode 100644
index 0000000..30c8ad1
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/live_t/lp1468422_passwd_storage.pg
@@ -0,0 +1,71 @@
+\set ECHO none
+\set QUIET 1
+-- Turn off echo and keep things quiet.
+
+-- Format the output for nice TAP.
+\pset format unaligned
+\pset tuples_only true
+\pset pager
+
+-- Revert all changes on failure.
+\set ON_ERROR_ROLLBACK 1
+\set ON_ERROR_STOP true
+\set QUIET 1
+
+BEGIN;
+
+-- Plan the tests.
+SELECT plan(6);
+
+SELECT ok(
+    (SELECT TRUE AS verify_old_pw FROM actor.usr 
+        WHERE id = 187 AND passwd = MD5('montyc1234')),
+    'Legacy password should match'
+);
+
+SELECT isnt_empty(
+    'SELECT actor.get_salt(187, ''main'')',
+    'get_salt() returns a new salt'
+);
+
+SELECT isnt_empty(
+    'SELECT * FROM actor.passwd WHERE usr = 187 AND passwd_type = ''main''',
+    'get_salt() should migrate the password'
+);
+
+SELECT ok(
+    (SELECT actor.verify_passwd(187, 'main', 
+        MD5(actor.get_salt(187, 'main') || MD5('montyc1234')))),
+    'verify_passwd should verify migrated password'
+);
+
+SELECT ok(
+    (SELECT NOT (
+        SELECT actor.verify_passwd(187, 'main', 
+            MD5(actor.get_salt(187, 'main') || MD5('BADPASSWORD'))))
+    ),
+    'verify_passwd should fail with wrong password'
+);
+
+-- This code chunk mimics the application changing a user's password
+DO $$
+    DECLARE new_salt TEXT;
+BEGIN
+    -- we have to capture the salt, because subsequent 
+    -- calls will create a new one.
+    SELECT INTO new_salt actor.create_salt('main');
+    PERFORM actor.set_passwd(
+        187, 'main', MD5(new_salt || MD5('bobblehead')), new_salt);
+END $$;
+
+SELECT ok(
+    (SELECT actor.verify_passwd(187, 'main', 
+        MD5(actor.get_salt(187, 'main') || MD5('bobblehead')))),
+    'verify_passwd should verify new password'
+);
+
+-- Finish the tests and clean up.
+SELECT * FROM finish();
+
+ROLLBACK;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
new file mode 100644
index 0000000..1c642b6
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.password-storage.sql
@@ -0,0 +1,219 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+CREATE TABLE actor.passwd_type (
+    code        TEXT PRIMARY KEY,
+    name        TEXT UNIQUE NOT NULL,
+    login       BOOLEAN NOT NULL DEFAULT FALSE,
+    regex       TEXT,   -- pending
+    crypt_algo  TEXT,   -- e.g. 'bf'
+
+    -- gen_salt() iter count used with each new salt.
+    -- A non-NULL value for iter_count is our indication the 
+    -- password is salted and encrypted via crypt()
+    iter_count  INTEGER CHECK (iter_count IS NULL OR iter_count > 0)
+);
+
+CREATE TABLE actor.passwd (
+    id          SERIAL PRIMARY KEY,
+    usr         INTEGER NOT NULL REFERENCES actor.usr(id)
+                ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    salt        TEXT, -- will be NULL for non-crypt'ed passwords
+    passwd      TEXT NOT NULL,
+    passwd_type TEXT NOT NULL REFERENCES actor.passwd_type(code)
+                DEFERRABLE INITIALLY DEFERRED,
+    create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+    edit_date   TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+    CONSTRAINT  passwd_type_once_per_user UNIQUE (usr, passwd_type)
+);
+
+CREATE OR REPLACE FUNCTION actor.create_salt(pw_type TEXT)
+    RETURNS TEXT AS $$
+DECLARE
+    type_row actor.passwd_type%ROWTYPE;
+BEGIN
+    /* Returns a new salt based on the passwd_type encryption settings.
+     * Returns NULL If the password type is not crypt()'ed.
+     */
+
+    SELECT INTO type_row * FROM actor.passwd_type WHERE code = pw_type;
+
+    IF NOT FOUND THEN
+        RETURN EXCEPTION 'No such password type: %', pw_type;
+    END IF;
+
+    IF type_row.iter_count IS NULL THEN
+        -- This password type is unsalted.  That's OK.
+        RETURN NULL;
+    END IF;
+
+    RETURN gen_salt(type_row.crypt_algo, type_row.iter_count);
+END;
+$$ LANGUAGE PLPGSQL;
+
+
+/* 
+    TODO: when a user changes their password in the application, the
+    app layer has access to the bare password.  At that point, we have
+    the opportunity to store the new password without the MD5(MD5())
+    intermediate hashing.  Do we care?  We would need a way to indicate
+    which passwords have the legacy intermediate hashing and which don't
+    so the app layer would know whether it should perform the intermediate
+    hashing.  In either event, with the exception of migrate_passwd(), the
+    DB functions know or care nothing about intermediate hashing.  Every
+    password is just a value that may or may not be internally crypt'ed. 
+*/
+
+CREATE OR REPLACE FUNCTION actor.set_passwd(
+    pw_usr INTEGER, pw_type TEXT, new_pass TEXT, new_salt TEXT DEFAULT NULL)
+    RETURNS BOOLEAN AS $$
+DECLARE
+    pw_salt TEXT;
+    pw_text TEXT;
+BEGIN
+    /* Sets the password value, creating a new actor.passwd row if needed.
+     * If the password type supports it, the new_pass value is crypt()'ed.
+     * For crypt'ed passwords, the salt comes from one of 3 places in order:
+     * new_salt (if present), existing salt (if present), newly created 
+     * salt.
+     */
+
+    IF new_salt IS NOT NULL THEN
+        pw_salt := new_salt;
+    ELSE 
+        pw_salt := actor.get_salt(pw_usr, pw_type);
+
+        IF pw_salt IS NULL THEN
+            /* We have no salt for this user + type.  Assume they want a 
+             * new salt.  If this type is unsalted, create_salt() will 
+             * return NULL. */
+            pw_salt := actor.create_salt(pw_type);
+        END IF;
+    END IF;
+
+    IF pw_salt IS NULL THEN 
+        pw_text := new_pass; -- unsalted, use as-is.
+    ELSE
+        pw_text := CRYPT(new_pass, pw_salt);
+    END IF;
+
+    UPDATE actor.passwd 
+        SET passwd = pw_text, salt = pw_salt, edit_date = NOW()
+        WHERE usr = pw_usr AND passwd_type = pw_type;
+
+    IF NOT FOUND THEN
+        -- no password row exists for this user + type.  Create one.
+        INSERT INTO actor.passwd (usr, passwd_type, salt, passwd) 
+            VALUES (pw_usr, pw_type, pw_salt, pw_text);
+    END IF;
+
+    RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION actor.get_salt(pw_usr INTEGER, pw_type TEXT)
+    RETURNS TEXT AS $$
+DECLARE
+    pw_salt TEXT;
+    type_row actor.passwd_type%ROWTYPE;
+BEGIN
+    /* Returns the salt for the requested user + type.  If the password 
+     * type of "main" is requested and no password exists in actor.passwd, 
+     * the user's existing password is migrated and the new salt is returned.
+     * Returns NULL if the password type is not crypt'ed (iter_count is NULL).
+     */
+
+    SELECT INTO pw_salt salt FROM actor.passwd 
+        WHERE usr = pw_usr AND passwd_type = pw_type;
+
+    IF FOUND THEN
+        RETURN pw_salt;
+    END IF;
+
+    IF pw_type = 'main' THEN
+        -- Main password has not yet been migrated. 
+        -- Do it now and return the newly created salt.
+        RETURN actor.migrate_passwd(pw_usr);
+    END IF;
+
+    -- We have no salt to return.  actor.create_salt() needed.
+    RETURN NULL;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION 
+    actor.migrate_passwd(pw_usr INTEGER) RETURNS TEXT AS $$
+DECLARE
+    pw_salt TEXT;
+    usr_row actor.usr%ROWTYPE;
+BEGIN
+    /* Migrates legacy actor.usr.passwd value to actor.passwd with 
+     * a password type 'main' and returns the new salt.  For backwards
+     * compatibility with existing CHAP-style API's, we perform a 
+     * layer of intermediate MD5(MD5()) hashing.  This is intermediate
+     * hashing is not required of other passwords.
+     */
+
+    SELECT INTO usr_row * FROM actor.usr WHERE id = pw_usr;
+
+    pw_salt := actor.create_salt('main');
+
+    PERFORM actor.set_passwd(
+        pw_usr, 'main', MD5(pw_salt || usr_row.passwd), pw_salt);
+
+    -- clear the existing password
+    UPDATE actor.usr SET passwd = '' WHERE id = usr_row.id;
+
+    RETURN pw_salt;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION 
+    actor.verify_passwd(pw_usr INTEGER, pw_type TEXT, test_passwd TEXT) 
+    RETURNS BOOLEAN AS $$
+DECLARE
+    pw_salt TEXT;
+BEGIN
+    /* Returns TRUE if the password provided matches the in-db password.  
+     * If the password type is salted, we compare the output of CRYPT().
+     * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy 
+     * 'main' passwords.
+     */
+
+    SELECT INTO pw_salt salt FROM actor.passwd 
+        WHERE usr = pw_usr AND passwd_type = pw_type;
+
+    IF NOT FOUND THEN
+        -- no such password
+        RETURN FALSE;
+    END IF;
+
+    IF pw_salt IS NULL THEN
+        -- Password is unsalted, compare the un-CRYPT'ed values.
+        RETURN EXISTS (
+            SELECT TRUE FROM actor.passwd WHERE 
+                usr = pw_usr AND
+                passwd_type = pw_type AND
+                passwd = test_passwd
+        );
+    END IF;
+
+    RETURN EXISTS (
+        SELECT TRUE FROM actor.passwd WHERE 
+            usr = pw_usr AND
+            passwd_type = pw_type AND
+            passwd = CRYPT(test_passwd, pw_salt)
+    );
+END;
+$$ STRICT LANGUAGE PLPGSQL;
+
+--- DATA ----------------------
+
+INSERT INTO actor.passwd_type 
+    (code, name, login, crypt_algo, iter_count) 
+    VALUES ('main', 'Main Login Password', TRUE, 'bf', 14);
+
+COMMIT;

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/opensrf.xml.example              |   33 +-
 Open-ILS/include/openils/oils_utils.h              |    5 +
 Open-ILS/src/c-apps/Makefile.am                    |    9 +-
 Open-ILS/src/c-apps/oils_auth.c                    | 1060 ++++++++++----------
 Open-ILS/src/c-apps/oils_auth_internal.c           |  468 +++++++++
 Open-ILS/src/c-apps/oils_utils.c                   |   84 +-
 .../src/perlmods/lib/OpenILS/Application/Actor.pm  |  257 +++---
 .../perlmods/lib/OpenILS/Application/AppUtils.pm   |   51 +-
 .../perlmods/lib/OpenILS/Application/AuthProxy.pm  |   87 ++-
 .../lib/OpenILS/Application/Storage/Publisher.pm   |   42 -
 Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm    |    3 +-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/005.schema.actors.sql          |  216 ++++
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |    4 +
 Open-ILS/src/sql/Pg/create_database_extensions.sql |    1 +
 .../src/sql/Pg/live_t/lp1468422_passwd_storage.pg  |   71 ++
 .../Pg/upgrade/0961.schema.password-storage.sql    |  229 +++++
 Open-ILS/src/support-scripts/eg_db_config.in       |   23 +-
 configure.ac                                       |    2 +
 .../Administration/password-storage.lp1468422.adoc |  120 +++
 20 files changed, 1964 insertions(+), 803 deletions(-)
 create mode 100644 Open-ILS/src/c-apps/oils_auth_internal.c
 create mode 100644 Open-ILS/src/sql/Pg/live_t/lp1468422_passwd_storage.pg
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0961.schema.password-storage.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Administration/password-storage.lp1468422.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list