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\Controller\AuthController. 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 $user object 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.

Leaving ghost accounts lying around is bad security practice. The SDNY Interpreters formerly had a cron job that would send weekly emails to all users in the submitter role to show them what interpreters they had scheduled for the coming week and remind them to update as needed. A side effect was that when user's email accounts were removed, the messages would bounce with an error message to the effect that there was no such user -- a reliable indication that the user had probably left the organization and the IT department had removed the email account. We set up a second cron job, scheduled to run a few minutes after the email-reminder task, to log into the Interpreters collective email account and search for these bounced emails in the inbox, identify the user account, disable it, and then delete the bounced email message. It was elegant and convenient, and also good house-keeping. Unfortunately this cron job broke when the organization shifted its email to a new platform. The point of the story is that a mechanism that somehow monitors and cleans up unused accounts is a good idea.

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.