All projects

LDAP authentication integration

2018

I built an authentication system that provides secure access to a suite of web apps and authenticates against the organization's Active Directory service. I used hardened PHP sessions to manage authentication state and to store application state in place of cookies. For users outside the organization, I extended the framework with passwordless guest accounts that use magic link authentication.

Implementation highlights

Automatic username correction

Each user has multiple identifiers, each of which has historically been used for signing in to at least one platform at the organization. It can be difficult for users to remember which identifier they need to use for each platform, so I implemented username correction to facilitate successful authentication whenever possible, even with an "incorrect" identifier.

Active Directory uses the username for authentication, which is a string of 1-8 characters. For users with long last names, their last name is truncated in the username. Since users are in the habit of typing their entire last name, they might type their entire last name by mistake.

Each user also has an email address based on the username, an email alias with their name spelled out, and an ID number. So, a user named Alex Anderson might try to use any of the following values as their username:

aanderso
aanderso@example.com
aanderson
aanderson@example.com
alex.anderson@example.com
alex.anderson
T01234567
1234567

...or versions of any of the above with different casing.

We can try to fix a badly formed username with the following steps:

  • Convert the username to lowercase
  • Ignore @ and anything after, so that it doesn't matter whether they enter the username or the full email address
  • Ignore any characters after the 8th character in the username

These steps result in a successful sign-in for the first four values in the above example. Once the user signs in once, we can save their email alias and ID number in a database. We can use those values in future sign-ins to ensure a successful sign-in for the final four values.

Storing user data locally

Active Directory fields, such as name, email address, and ID number, are saved in a local users table so that they can be referenced by production applications. These fields are updated each time a user signs in, so production applications are able to store only the ID number in their own tables, and reference the users table to display the most up-to-date information.

The users table is also used to load the fields of the currently signed-in user and inject it into production applications via a static method. As a result, the table is queried only once per request lifecycle, and the same object is returned for any application classes or pages that need it.

Migrating from cookies to sessions

I decided to start using sessions instead of cookies to store all user-facing application state. Authentication sessions are the standout use case, but applications were also using cookies to store application settings, such as sort order preferences on tables. Storing everything in sessions means that only one session cookie needs to be set in the browser, minimizing the risk of vulnerabilities introduced by production applications.

I built a simple session library on top of hardened PHP sessions, inspired by Robert Hafner's excellent article How to Create Bulletproof Sessions. The library I created exposes static methods for CRUD operations, allowing applications to easily save and retrieve variables from the session. It supports options such as lifetime and delete_on_logout for each variable, giving applications easy flexibility without having to implement these features each time.

Protecting user sessions

Although the session library regularly regenerates the overall session token automatically, I applied extra protection to signed-in users by having the authentication system call the public regenerate_session function in the session library at the time that a user signs in.

By forcing the token to regenerate right away, the user is protected from the outset even if an attacker took the current session token with them a few minutes prior to the new user signing in.

Authentication service status check

If the Active Directory service is unavailable, the ldap_connect function hangs for over a minute before failing, which means that the user will be staring at the sign-in form with a loading icon in the browser tab. They are likely to click the "sign in" button again before our server responds.

To avoid this, I used the fsockopen function to test if any of the IPs resolved for the AD hostname are responsive. If not, we can display an error message that tells the user that the sign-in service is currently unavailable. This also helps them feel confident that the problem is not that their credentials are invalid.

This results in a smooth and clear experience for the user even if the sign-in is unsuccessful.

I implemented a straightforward magic link authentication system for guest users outside the organization. Using magic links allows the process to be substantially simplified:

  • No passwords - Users do not need to create and remember passwords, and there is no risk of unsafe password storage. We do not need to implement password storage.
  • Implicit email verification - The use cases of the production applications supported by this authentication system require email verification to ensure that guest users are who they say they are. Email verification is accomplished implicitly, since magic links are delivered via email. This avoids a complex and disruptive email verification process.
  • No password resets - There is no need to implement a password reset workflow.

Guest users are signed in with these basic steps:

  • Randomly generate an auth ID and a cryptographically secure auth token
  • Encode both elements in the magic link emailed to the user
  • Store the auth ID in a session variable so that the magic link can only be used on the current browser
  • Hash the auth token with PHP's secure password_hash function and store it in database along with the unhashed auth ID, representing the current sign-in

When the user follows the magic link, we can verify it and complete the sign-in process:

  • Extract the auth ID, use it to identify the current sign-in in the database, and match it against the auth ID stored in the session
  • Extract the token from the magic link, and use PHP's password_verify function to test it against the hashed token in the current sign-in (from the database)
  • If the token matches, save the user object as a session variable