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.
How It Works
Section titled “How It Works”-
Search type detection
DirectorySearchType::fromSearchValue()auto-detects whether the input is an email address, employee ID (numeric), or NetID (fallback). -
API lookup
The
DirectorySearchclass fromlaravel-soacalls Northwestern’s Directory Search API with abasicdetail level. -
Validation
The response is validated to ensure required fields are present.
eduPersonPrimaryAffiliationandmailmust exist for the entry to be considered valid. -
Field mapping
SyncUserFromDirectorymaps raw LDAP attributes onto the User model, handling multi-value arrays and student-specific field priority. -
Persistence
PersistUserWithUniqueUsernamesaves the user record and assigns the defaultNorthwestern Userrole.
Key Classes
Section titled “Key Classes”| Class | Purpose |
|---|---|
FindOrUpdateUserFromDirectory | Orchestrates the full lookup-validate-sync-persist flow |
SyncUserFromDirectory | Maps LDAP attributes to User model fields |
DirectorySearchType | Auto-detects search input type (email, employee ID, NetID) |
PersistUserWithUniqueUsername | Saves/updates user with unique username generation |
Post-Retrieval Jobs
Section titled “Post-Retrieval Jobs”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:
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;}Job Contract
Section titled “Job Contract”Each job in the array must:
- Implement
Illuminate\Contracts\Queue\ShouldQueue - Accept a
User $useras its constructor argument
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 }}Sync vs. Async Dispatch
Section titled “Sync vs. Async Dispatch”The caller controls whether jobs run synchronously or are dispatched to the queue via the $immediate flag:
| Caller | $immediate | Behavior |
|---|---|---|
| SSO login | false | Jobs are queued, login completes immediately |
| Filament user creation | true | Jobs run synchronously, the admin sees the result before the page redirects |
| Scheduled directory sync | false | Jobs 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).
Customizing Field Mappings
Section titled “Customizing Field Mappings”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.
Default Mappings
Section titled “Default Mappings”| User Attribute | LDAP Keys (priority order) | Notes |
|---|---|---|
first_name | givenName | Students prepend nuStudentGivenName |
last_name | sn | Students prepend nuStudentSn |
email | mail | Students prepend nuStudentEmail |
employee_id | nuStudentNumber, employeeNumber | IDs shorter than 7 characters are discarded |
phone | telephoneNumber | Students prepend nuAllStudentCurrentPhone |
departments | nuAllDepartmentName | Multi-value (stored as JSON array) |
job_titles | nuAllTitle | Suppressed for students |
hr_employee_id | employeeNumber | Set when both employeeNumber and nuStudentNumber exist and differ |
timezone | — | Set 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.
Adding a New Mapped Field
Section titled “Adding a New Mapped Field”To sync an additional directory attribute (e.g., building/office location):
-
Add a migration
Terminal window php artisan make:migration add_building_to_users_tableSchema::table('users', function (Blueprint $table) {$table->string('building')->nullable();}); -
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']); -
Update the User model (if needed)
Add a cast if the field requires one (e.g.,
array,datetime). No$fillablechanges are needed because the starter runsModel::unguard()globally.
Invalid Directory Data
Section titled “Invalid Directory Data”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 = trueanddirectory_sync_last_failed_atis recorded. The user is not deleted. - New user - A
BadDirectoryEntryexception 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.
Health Check
Section titled “Health Check”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.