Skip to content

Directory Search

The Directory Search integration looks up Northwestern users by NetID, email, or employee ID via the LDAP-backed Directory Search API. It is the primary mechanism for user provisioning during SSO login and for keeping user profile data synchronized with Northwestern’s identity system.

The underlying API client is provided by the northwestern-sysdev/laravel-soa package.

  1. Search type detection

    DirectorySearchType::fromSearchValue() auto-detects whether the input is an email address, employee ID (numeric), or NetID (fallback).

  2. API lookup

    The DirectorySearch class from laravel-soa calls Northwestern’s Directory Search API with a basic detail level.

  3. Validation

    The response is validated to ensure required fields are present. eduPersonPrimaryAffiliation and mail must exist for the entry to be considered valid.

  4. Field mapping

    SyncUserFromDirectory maps raw LDAP attributes onto the User model, handling multi-value arrays and student-specific field priority.

  5. Persistence

    PersistUserWithUniqueUsername saves the user record and assigns the default Northwestern User role.


ClassPurpose
FindOrUpdateUserFromDirectoryOrchestrates the full lookup-validate-sync-persist flow
SyncUserFromDirectoryMaps LDAP attributes to User model fields
DirectorySearchTypeAuto-detects search input type (email, employee ID, NetID)
PersistUserWithUniqueUsernameSaves/updates user with unique username generation

After a user is synced and persisted, FindOrUpdateUserFromDirectory dispatches a configurable list of post-retrieval jobs. This is the extension point for running custom logic when a user is fetched from the directory, whether during SSO login, manual creation in Filament, or a scheduled directory sync.

Add your jobs to the postRetrievalJobs() method in FindOrUpdateUserFromDirectory:

app/Domains/User/Actions/Directory/FindOrUpdateUserFromDirectory.php
private function postRetrievalJobs(): array
{
$jobs = [
// Add your custom post-retrieval jobs here — see docs for the full job contract.
];
if (config('platform.wildcard_photo_sync')) {
$jobs[] = DownloadWildcardPhotoJob::class;
}
return $jobs;
}

Each job in the array must:

  • Implement Illuminate\Contracts\Queue\ShouldQueue
  • Accept a User $user as its constructor argument
app/Domains/User/Jobs/AssignDepartmentJob.php
use App\Domains\User\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class AssignDepartmentJob implements ShouldQueue
{
use Queueable;
public function __construct(public User $user)
{
//
}
public function handle(): void
{
// Your custom logic here
}
}

The caller controls whether jobs run synchronously or are dispatched to the queue via the $immediate flag:

Caller$immediateBehavior
SSO loginfalseJobs are queued, login completes immediately
Filament user creationtrueJobs run synchronously, the admin sees the result before the page redirects
Scheduled directory syncfalseJobs are queued, sync processes users in bulk

The starter ships with DownloadWildcardPhotoJob as a built-in post-retrieval job (enabled when platform.wildcard_photo_sync is true).


The SyncUserFromDirectory::syncDemographics() method defines how LDAP directory attributes map onto User model fields. Customize this when your project needs different directory fields, additional attributes, or different priority logic.

User AttributeLDAP Keys (priority order)Notes
first_namegivenNameStudents prepend nuStudentGivenName
last_namesnStudents prepend nuStudentSn
emailmailStudents prepend nuStudentEmail
employee_idnuStudentNumber, employeeNumberIDs shorter than 7 characters are discarded
phonetelephoneNumberStudents prepend nuAllStudentCurrentPhone
departmentsnuAllDepartmentNameMulti-value (stored as JSON array)
job_titlesnuAllTitleSuppressed for students
hr_employee_idemployeeNumberSet when both employeeNumber and nuStudentNumber exist and differ
timezoneSet from config('platform.default_user_timezone') (default: America/Chicago)

For students, student-specific LDAP fields are prepended to the key arrays so they take priority. The findValue() helper returns the first non-empty match, so earlier keys win.

To sync an additional directory attribute (e.g., building/office location):

  1. Add a migration

    Terminal window
    php artisan make:migration add_building_to_users_table
    Schema::table('users', function (Blueprint $table) {
    $table->string('building')->nullable();
    });
  2. Update the field mapping

    In SyncUserFromDirectory::syncDemographics(), add the new mapping alongside the existing ones:

    app/Domains/User/Actions/Directory/SyncUserFromDirectory.php
    $user->building = $this->findValue($directoryData, ['nuPosition1Building', 'postalAddress']);
  3. Update the User model (if needed)

    Add a cast if the field requires one (e.g., array, datetime). No $fillable changes are needed because the starter runs Model::unguard() globally.


When a directory lookup returns invalid data (missing required fields), the behavior depends on whether the user already exists:

  • Existing user - The account is marked with netid_inactive = true and directory_sync_last_failed_at is recorded. The user is not deleted.
  • New user - A BadDirectoryEntry exception is thrown, preventing account creation with incomplete data.

This ensures that transient directory issues don’t destroy existing accounts while still preventing invalid new accounts.


The DirectorySearchCheck class provides a Spatie Health check for the Directory Search API. It performs a test lookup against a configured NetID and validates the response structure.


#DIRECTORY_SEARCH_URL https://northwestern-prod.apigee.net/directory-search

Directory Search API base URL

#DIRECTORY_SEARCH_API_KEY Required

Apigee API key for Directory Search

#DIRECTORY_SEARCH_HEALTH_CHECK_NETID swd2981

NetID used for health check lookups