Integrating an existing app
Integrating an existing app
You do not have to start greenfield, and Cbox ID never forces its own users
table on you. The whole platform references a person by an opaque string id
through one contract — Cbox\Id\Identity\Contracts\Subjects. Bind your own
implementation and every part of Cbox ID (sessions, MFA, passkeys, SSO, OAuth) runs
against your existing user store.
Cbox ID ──asks──► Subjects (contract) ──you implement──► your users table
(never owns users) find / create / verifyPassword … (Passport, Sanctum, homegrown…)
1. Point Cbox ID at your existing users
Implement the contract over your model, and bind it in config. That's the whole integration.
use Cbox\Id\Identity\Contracts\Subjects;
use Cbox\Id\Identity\ValueObjects\Subject;
use App\Models\User;
final class AppSubjects implements Subjects
{
public function find(string $id): ?Subject
{
return ($u = User::find($id)) ? new Subject($u->id, $u->email, $u->name) : null;
}
public function findByEmail(string $email): ?Subject
{
return ($u = User::whereEmail($email)->first()) ? new Subject($u->id, $u->email, $u->name) : null;
}
public function create(string $email, ?string $name = null, ?string $password = null): Subject
{
$u = User::create(['email' => $email, 'name' => $name, 'password' => $password ? Hash::make($password) : null]);
return new Subject($u->id, $u->email, $u->name);
}
public function verifyPassword(string $subjectId, string $password): bool
{
$u = User::find($subjectId);
return $u !== null && Hash::check($password, (string) $u->password);
}
public function setPassword(string $subjectId, string $password): void
{
User::whereKey($subjectId)->update(['password' => Hash::make($password)]);
}
// provisionFederated(), link(), linkedIdentities(), unlink() — implement these
// when you enable SSO/social login (they map an external identity to a user).
}
// config/cbox-id.php
'subject' => ['resolver' => App\Identity\AppSubjects::class],
Ids are opaque, so anything works: an auto-increment id, a ULID, even a namespaced
id ("reseller:42") if you have several authenticatable models. Cbox ID stores no
PII it can't delete through your resolver — good for GDPR erasure.
The package ships a default
DatabaseSubjectsover an optional users table for greenfield installs. Binding your own resolver replaces it entirely.
2. Taking over from Laravel Passport
Passport turns your app into an OAuth2 server issuing tokens to its own clients. Cbox ID is also an OAuth2/OIDC server — but a dedicated identity provider with MFA, passkeys, SSO, SCIM, and a hosted login. The migration is incremental; you never need a big-bang cutover.
Recommended path — run Cbox ID as the IdP, keep your users:
- Keep your
userstable. Bind it viaSubjects(step 1). No user migration. - Register your existing OAuth clients in Cbox ID — one
oauth_clientsrow per Passport client (sameredirect_uris), or let them self-register via Dynamic Client Registration (cbox-id.oauth.dynamic_registration). - Repoint your apps from Passport's
/oauth/authorize+/oauth/tokento Cbox ID's — the endpoints are standard OAuth2/OIDC, so most clients only need the base URL changed. Cbox ID adds PKCE,at+jwtaccess tokens, refresh-token rotation with reuse detection, and optional DPoP (RFC 9449) sender-constrained tokens you didn't have before. - Drain, don't cut. Passport access tokens are short-lived; let them expire. Refresh tokens re-issue against Cbox ID on next refresh (users re-consent once).
- Retire Passport once traffic has moved. Remove
Passport::routes()and thepassporttables when the dashboards show zero issuance.
Verifying tokens during the overlap: Cbox ID publishes a JWKS at
/.well-known/jwks.json and metadata at /.well-known/openid-configuration (+ the
RFC 8414 /.well-known/oauth-authorization-server). Resource servers validate
Cbox ID tokens against that JWKS while still accepting Passport tokens, then drop
Passport verification when it's drained.
3. Unified auth across two (or more) products
The classic "we have two products and want one login" setup. Make Cbox ID the central IdP; each product is an OpenID Connect client.
┌──────────────┐
Product A ◄──► │ │
(OIDC RP) │ Cbox ID │ one identity, one MFA/passkey enrollment,
│ (IdP) │ one session, one SCIM/SSO surface
Product B ◄──► │ │
(OIDC RP) └──────────────┘
- Each product runs a standard OIDC client (Laravel Socialite's
genericdriver, orleague/oauth2-client), pointed at Cbox ID's discovery document. - A user signs in once at Cbox ID; each product receives an
id_tokenand gets the same canonicalsub— so "who is this person" is identical across products. - MFA, passkeys, recovery codes, session revocation, and step-up live once at the IdP, not re-implemented per product.
Sharing the actual user records: if both products already have their own user
tables, bind a Subjects resolver that maps a Cbox ID subject to the canonical
record (e.g. a shared identity service, or product A's users as the source of
truth). New products then read identity from Cbox ID instead of maintaining their
own login.
4. Unifying tenancy
Cbox ID ships an Organization + Membership model (users belong to orgs with roles), with deny-by-default tenant isolation. Two ways to unify:
- Adopt it: model your customers as Cbox ID organizations; memberships carry the
roles, and SCIM/SSO provisioning maps groups onto them. Products read org context
from the token (
orgclaim) and the membership API. - Bridge it: keep your existing tenant model and map Cbox ID orgs to it in your
resolver/claims — the
orgclaim and membership checks still gate access, backed by your own tenant ids.
Either way the tenancy decision is one integration point, not a rewrite.