Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Identity Core Laravel Package

liberusoftware/identity-core

Identity Core provides the foundational identity layer for Laravel applications: users, authentication flows, roles and permissions, and account lifecycle helpers. Lightweight, framework-native services designed to sit beneath your app's own auth logic.

View on GitHub
Deep Wiki
Context7

Getting Started

liberusoftware/identity-core sits beside Laravel's auth stack rather than replacing it. It gives you three things you can lean on independently: a normalizer that turns whatever a user typed into a canonical identifier, a policy object that decides whether an authentication attempt is allowed (lockout thresholds, MFA requirements, credential rules), and a stream of security events you can subscribe to for auditing.

Caveat before you commit: this package has 0 stars, 0 dependents, and a release date (2026-10-27) that is ahead of the current date — a metadata anomaly worth confirming against the actual git tags. Treat the API surface as "read the source first" rather than "trust the docs." Budget an afternoon to read src/Contracts, src/Events, and the service provider before wiring it into a login path.

Minimal setup:

composer require liberusoftware/identity-core
php artisan vendor:publish --provider="Liberu\IdentityCore\IdentityCoreServiceProvider"
php artisan migrate

Then open, in this order:

  1. config/identity-core.php — provider/driver selection, normalizer rules, lockout windows.
  2. src/Contracts/ — the interfaces you'll actually type-hint (IdentifierNormalizer, AuthPolicy, maybe IdentityProvider).
  3. src/Events/ — the security event classes; this is the part with the widest day-to-day value.
  4. The migrations — check whether it adds columns to users (normalized identifier, lockout counters) or its own tables.

First use case: normalize an identifier at login and registration, evaluate the policy, and log the outcome. That's a two-hour integration that immediately improves duplicate-account hygiene and gives you an audit trail:

use Liberu\IdentityCore\Contracts\IdentifierNormalizer;
use Liberu\IdentityCore\Contracts\AuthPolicy;
use Illuminate\Support\Facades\Event;

public function store(LoginRequest $request, IdentifierNormalizer $norm, AuthPolicy $policy)
{
    $identifier = $norm->normalize($request->string('email'));

    if (! $policy->allowsAttempt($identifier, $request->ip())) {
        Event::dispatch(new \Liberu\IdentityCore\Events\AuthenticationBlocked($identifier));
        throw ValidationException::withMessages(['email' => __('auth.throttled')]);
    }

    // hand off to your existing guard with the normalized identifier
}

Implementation Patterns

Normalize at every boundary, not just login. The single highest-value pattern is to make normalization a boundary concern so no code path can bypass it. Use prepareForValidation() on the form requests that accept identifiers — login, register, forgot-password, invite, admin user create, CSV import:

protected function prepareForValidation(): void
{
    $this->merge([
        'email' => app(IdentifierNormalizer::class)->normalize($this->input('email', '')),
    ]);
}

For non-HTTP entry points (artisan commands, queued imports, seeders), wrap them in a small NormalizesIdentifiers trait rather than re-implementing the rules. Consistency between the write path and the lookup path is what prevents "user exists but can't log in."

Pair normalization with a database-level guarantee. Add a normalized_identifier column with a unique index, backfill it, and query against it. Normalization in PHP alone is a convention; the unique index is the enforcement. The package's normalizer tells you what canonical means — your schema should encode it.

Bind the policy per guard/tenant, not globally. If the package resolves AuthPolicy from the container, rebind it in a scoped way rather than reading a global config key:

$this->app->when(AdminLoginController::class)
    ->needs(AuthPolicy::class)
    ->give(fn ($app) => $app->make(PolicyFactory::class)->forTenant($tenantId, guard: 'admin'));

This is also the seam for tests — swap in a PermissivePolicy so lockout state doesn't bleed between tests.

Fan out security events: durable locally, queued remotely. The events are the integration point most teams underuse. A pattern that holds up:

// Listener 1 — synchronous, cheap, durable: write to your audit table.
// Listener 2 — queued: push to SIEM / Slack / metrics.
Event::listen(AuthenticationFailed::class, RecordSecurityEvent::class);
Event::listen(AuthenticationFailed::class, ForwardToSiem::class); // implements ShouldQueue

Keep the synchronous listener to a single insert. Anything that touches the network belongs behind ShouldQueue, otherwise you've added 200ms to every failed login — which is also your lockout path.

Step-up enforcement as middleware. If the policy exposes an MFA/assurance requirement, wrap sensitive routes rather than sprinkling checks in controllers:

Route::middleware(['auth', 'identity.assurance:mfa'])->group(function () {
    Route::get('/billing', BillingController::class);
});

Test against events, not internals. Event::fake([AuthenticationFailed::class]) plus assertDispatched gives you stable assertions even if the policy implementation changes underneath. For lockout windows, inject a fixed clock — time-travel tests against real now() calls are the usual source of flaky auth suites.

Keep an escape hatch. Wrap the package behind your own thin Identity service. If the package goes unmaintained (see below), you swap one class instead of grepping 40 controllers.

Gotchas and Tips

Backfilling will create duplicates if you aren't careful. Normalizing new writes while old rows stay raw means [email protected] and [email protected] both exist, and the unique index migration fails. Sequence it: (1) add nullable column, (2) run a backfill command that normalizes and reports collisions, (3) resolve collisions manually, (4) add the unique index, (5) make the column non-nullable.

Unicode and case folding are subtler than strtolower(). Use mb_strtolower — and be aware that case folding (mb_convert_case(..., MB_CASE_FOLD)) differs from lowercasing. The classic bug is the Turkish dotted/dotless i: "İ".lower() is not "i" under a Turkish locale. Also normalize IDN domains to punycode and Unicode to NFC before comparing, or müller@ and the NFD form become two accounts.

Plus-addressing normalization must be opt-in. Stripping +tag from Gmail addresses prevents trivial multi-accounting, but it will silently merge accounts for users who relied on per-tag filters — and it's wrong for providers where + is a legal literal character. Gate it behind the policy config and document it.

Dispatch events after commit. Login flows often write session/last-seen rows inside a transaction. An event dispatched mid-transaction can be observed by a queued listener before the row is visible, or be lost entirely on rollback. Use ShouldDispatchAfterCommit (or DB::afterCommit) on anything that writes.

Don't leak account existence through event-driven responses. If your listener returns different messages or timings for "no such user" vs "wrong password," you've reintroduced enumeration the policy layer was meant to reduce. Keep the response uniform and push the distinction into the audit event only.

Scope the policy to the guard. A lockout counter keyed only by identifier means an attacker can lock a user out of the admin panel by hammering the customer-facing login. Key counters by (identifier, guard) or (identifier, ip).

Config changes need config:cache cleared. Standard Laravel trap, but it bites here because normalizer rules live in config — a stale cache means production normalizes differently from staging, which is exactly the kind of bug that only shows up as duplicate accounts weeks later.

Ecosystem risk is the real caveat. Zero stars, zero dependents, and a future-dated release mean no external validation and a real bus-factor problem. Mitigations: pin an exact version ("liberusoftware/identity-core": "1.2.3"), vendor or re-implement the normalizer (it's the piece most likely to outlive the package), and write contract tests asserting the behaviors you depend on so an upgrade can't silently change normalization. The policy and event layers are thin enough that forking is realistic if the upstream goes quiet.

Extension points to check in the source. Look for a bound interface for the normalizer (so you can add your own rules for phone/E.164 or employee IDs) and a driver/config key for policy resolution. If normalization is a final class with hardcoded rules, that's a signal to wrap rather than extend — and a good reason to keep the facade thin.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource