Authentication and authorization
There is a general description of how InterpretersOffice handles authentication and authorization in the section on the Laminas MVC request cycle, and in the section on the entity relationship model there is a discussion of the entities Person, User and Role. I suggest reading those before reading this.
InterpretersOffice currently uses single-factor user/password authentication, although this may change in a future version. As an additional security measure, consecutive failed login attempts are counted, and the user account is disabled if a certain number of consecutive failures is reached. This number can be configured (as explained in the setup guide); the default is six. A disabled account can only be re-enabled by a user having the role manager or administrator.
The application also enforces a moderate password strength requirement. (I recognize that password strength policies are not a settled issue, with many sites refusing passwords that are in fact stronger than ones they accept. A truly good password strength policy is one that uses an intelligent tool to check complexity rather than enforcing a set of published rules that arguably help a potential attacker.)
The route to the login page is /login, which is mapped to the
loginAction() of the
InterpretersOffice’s authentication system uses the Laminas\Authentication\AuthenticationService
with a custom adapter.
That adapter uses the Doctrine entity manager to query the database for a user matching the supplied identity and password. Passwords at rest are of course
hashed, using PHP’s native hashing functions. If authentication is successful, the adapter creates a simple PHP stdClass
which the authentication service ultimately stores in the session.
Elsewhere in the application, we determine whether the user is logged in by examining the authentication service, which is accessed by way of the service manager (container). Wherever we need a controller or service class to be authentication-aware, we inject the authentication service instance either via the constructor or a setter.
Upon successful login, the loginAction event is triggered. An event listener has been attached earlier in the request cycle, specifically in our AuthControllerFactory, where an AuthenticationListener is wired to observe the login event. This listener’s job is to update the last_login property of the user object and reset the failed login count to zero. It also logs the user authentication event.
user account management
The InterpretersOffice\Controller\AccountController has action methods for handling new user registration, email verification, and resetting forgotten passwords.
Note that the only type of users who can register their own user accounts are those in the submitter role: users who submit requests for interpreting services by way of the Requests module. This module can be thought of as a front end for users to manage their requests, while the Admin module is the administrative back end.
On the administrative side, users with administrative roles administrator and manager can only be created manually by users with the same or higher access levels. The apparent chicken-and-egg problem is solved with an interactive command line script for creating an initial administrative user when the application is set up (from the app root directory: bin/admin-cli setup:create-admin-user). The reasoning behind this design is that (1) administrative access should be tightly controlled, and (2) the number of admin users required is small, hence manageable by hand (in the Southern District of New York’s Interpreters Office there are nine admin users, and we’re among the larger federal court interpreter offices in the US).
When a user leaves the organization, if the user has a data history then the record cannot be deleted outright because of foreign key constraints – and because we don’t want to obliterate history. Instead, the account should disabled. Currently this has to be done manually by an administrator/manager. If a user – or for that matter, any entity – has no data history, the user management interface displays a Delete button for deleting the underlying database record.
By design, the admin user-management interface provides no means of setting or getting user password (and “getting” them would in any case be pointless because they are hashed). The only way users can reset their passwords is through the actions provided by the AccountController.
You can set any user’s password from the command line with: bin/admin-cli admin:user-password <email_address> <password>
Please see the section dealing with authentication authorization in the discussion of the request cycle, which gives a fair overview. An additional point worth noting is that InterpretersOffice does most of its authorization checking via the event listener attached in the Admin module’s onBootstrap() method. If authorization is denied, a message is logged to that effect. If the authorization is denied by this event listener, it means a user actually tried to navigate to a URL that is not authorized, and was turned away. In some other cases, we simply query the ACL service to determine whether a particular button or link should be displayed to the user, but if the result is negative, the log message is likewise generated. Thus the presence of “access denied” messages in the log does not necessarily mean anyone was trying to do something nefarious. In future versions we might tweak this behavior to make it more nuanced.
The authorization system confers only a few privileges on administrator that manager does not have. Among these is write-access to the configuration settings for the Requests module, available at /admin/configuration/requests which allows the user to control what event listeners will be triggered on various actions on the part of the submitters.
The main configuration file for access control is module/Admin/config/acl.php; other ACL configuration may be found in other modules’ config/module.config.php files. Familiarity with Laminas ACL is required in order to make any sense out of them.
At this writing, ACL configuration is hard-coded in the application. You can change it, but when you update the application you’ll run into merge conflicts or clobber your local changes unless precautions are taken. I may address this in a future version.