arietimmerman/laravel-scim-server
Add a SCIM 2.0 server to Laravel apps for automated user and group provisioning. Exposes standard REST endpoints so identity providers like Okta, Azure AD, and OneLogin can securely sync users, groups, and attributes.
composer require limosa-io/laravel-scim-server, then publish its config (php artisan vendor:publish and pick the SCIM provider tag). Nearly every decision you'll make lives in config/scim.php: route prefix, middleware stack, auth token/guard, and the resource map.config/scim.php → the README section on resources → a generated resource stub. The stub is the real documentation; it pins down the exact attribute-builder API and method signatures for your version.web middleware — CSRF and session handling will break provisioning.users table. Map userName, name.givenName/familyName, emails, active, externalId in a single resource class, run the IdP's "test connection" + "provision user", and watch storage/logs/laravel.log for the raw payloads.curl -H "Authorization: Bearer $TOKEN" https://app.test/scim/v2/Users?filter=userName eq "[email protected]" before touching the IdP. Getting the filter syntax right by hand once saves hours of IdP-side debugging.schema discovery endpoints IdPs poll at setup time. Getting mutable, required, caseExact, and uniqueness wrong is the usual cause of "the IdP keeps retrying and creating duplicates."externalId for dedupe, and catch unique-constraint violations to return a proper SCIM 409 with scimType: uniqueness instead of a 500 — a 500 makes Okta retry and sometimes mark the integration broken.replace/add/remove with path expressions, including filtered paths (emails[type eq "work"].value) and the no-path "merge object" form. Write feature tests using payloads captured from the real IdP, not hand-authored ones.account_type = 'employee'), express that as a constraint on the resource's base Eloquent query rather than trying to bolt it onto the filter mapping. Then add your own regression tests for filter combinations — see the note on v1.5.3 below.user_name, external_id, and updated_at should be indexed; updated_at also powers meta.lastModified and the ?filter=meta.lastModified gt ... delta queries some IdPs use.schemas, detail, status, scimType) — IdP admin UIs surface detail verbatim to whoever is configuring the app, and it's your only channel to them.eq, co, sw, and, or, not, grouped parens, unsupported attributes) asserting both result sets and status codes.userName eq "a" or userName eq "b" could combine with your base query under SQL precedence and return rows outside your scope — in a multi-tenant setup, that's a cross-tenant read via the SCIM API. If you run anything below 1.5.3 with a narrowed base query, treat it as a leak and pin ^1.5.3. Add a regression test that hits a scoped resource with an or filter and asserts nothing outside the scope comes back.404 as "gone" and happily proceed; returning 200 with an empty body makes them think the user still exists and blocks re-creation of that username.id strategy breaks matching. IdPs cache it. Prefer stable UUIDs on the resource over auto-increment primary keys, decided before you go to production.400 with scimType: invalidFilter. Silently returning everything (or nothing) causes IdPs to conclude a user is absent and re-provision them, producing duplicates.emails array including type and primary, or the IdP will rewrite it on every sync.active: false is not a delete. Map it to your own deactivation (and decide what it means for tokens/sessions), and make sure deactivated users are still returned by GET — IdPs use that to confirm the deactivation landed.How can I help you explore Laravel packages today?