# build-auth-audience — multi-audience API auth + ownership authorization

> **Builds:** the decision + wiring for **how a token for one audience differs from another** (user vs
> provider vs delegate vs company) and how every endpoint is scoped to the **right owner** (no IDOR).
> **Consumed by:** `analysis.md` §1 (the auth-model decision is recorded there, BEFORE the schema GATE) and
> every `api/<audience>/<flow>/<endpoint>.md` plan (its `guard/ability` field).
> **Source of truth (no duplication):** endpoint mechanics → [`build-api.md`](build-api.md); model/columns →
> [`build-database.md`](build-database.md). This file owns **only** the audience-auth + authorization decision.

`auth:sanctum` proves a request is **authenticated** — it says nothing about **which audience** the caller is
or whether they may touch **this row**. A multi-audience project (one product split into apps sharing one
backend+DB) must decide both. Do it here, once, and record it in `analysis.md`.

---

## 0) What the base already gives you (mirror it — don't reinvent)

- **API auth = Sanctum tokens** (not a guard). Tokens are minted in the service:
  `$user->createToken('authToken')->plainTextToken`, revoked with
  `$request->user()->currentAccessToken()->delete()` (`AuthService`, `LogoutController`).
- **Abilities precedent already exists:** the activation flow mints a scoped, expiring token —
  `$user->createToken('activation', ['activation'], now()->addMinutes(5))` (`UserStateResponseTrait`).
  The `personal_access_tokens.abilities` column is there; use the same mechanism for audiences.
- **RBAC is ADMIN-ONLY:** `Role`/`Permission` (pivot `permission_role`) + `AdminType` (`SUPER_ADMIN` bypasses,
  `ADMIN` is permission-checked) + `CheckRolePermission` (route-name vs `admin.*` permissions) +
  `PermissionSeeder` (auto-discovers `admin.*` routes). **End-users have NO roles** — don't assume they do.
- **Middleware aliases** are registered in `bootstrap/app.php` (`admin.api`, `api.lang`, `complete.info`,
  `throttle:request-code`, …). New aliases go in the same place — **never** hand-edit route registration elsewhere.

---

## 1) Decide the audience-auth model (record in `analysis.md` §1 — BLOCKER before the GATE)

`match` the question: are the audiences **genuinely different entities** (different data/fields/flows — a
provider isn't a customer), or the **same kind of account** differing only in capabilities/screens?

### Model A (DEFAULT) — a model + Sanctum guard per audience (multi-auth, mirrors `admins`)
Use for the common case: the audiences are **distinct entities** (user vs provider vs delegate vs company),
each with its own table, fields, and flows. This is the base's own pattern for `admins` (separate table +
guard + model) — extend it to the API audiences instead of cramming everyone into one `users` table.

Per audience (mirror `User` / `Admin` — don't reinvent):
1. **A model + table + migration** — `App\Models\Provider` (etc.) using `BaseAuthModelTrait, HasFactory,
   SoftDeletes, CanRetrieve, HasApiTokens`, `$hidden = ['password','remember_token']`, `password => 'hashed'`
   cast, `FILES`/`UPLOAD_DIRECTORY`, its own `providers` migration in `database/migrations/<domain>/`, plus a
   factory + seeder (see [`build-database.md`](build-database.md) §3b "Auth-style entity").
2. **A Sanctum guard + eloquent provider** in `config/auth.php` (one per audience; keep the existing `web`/`admin`):
   ```php
   'guards' => [
       'user'     => ['driver' => 'sanctum', 'provider' => 'users'],
       'provider' => ['driver' => 'sanctum', 'provider' => 'providers'],
       // 'delegate' => ['driver' => 'sanctum', 'provider' => 'delegates'], …
   ],
   'providers' => [
       'users'     => ['driver' => 'eloquent', 'model' => App\Models\User::class],
       'providers' => ['driver' => 'eloquent', 'model' => App\Models\Provider::class],
   ],
   ```
   (add a `passwords` broker entry too if that audience resets passwords.)
3. **A login pipeline per audience** — focused controllers under `Api/V1/<Audience>/Auth/` + Form Requests + a
   `<Audience>AuthService`, mirroring base's `Api/V1/Auth/*`. Mint the token **on that model**:
   `$provider->createToken('authToken')->plainTextToken`.
4. **Gate each audience's routes with its guard** — in `routes/api/v1/<audience>.php`:
   ```php
   Route::middleware('auth:provider')->group(function () { /* provider endpoints */ });
   ```
   The `provider` guard only admits tokens whose owner is a `Provider` — clean isolation, **no ability checks
   needed**. `$request->user()` returns the `Provider`.
5. **Shared endpoints** used by more than one audience → accept the relevant guards: `auth:user,provider`
   (Sanctum tries each). Usually each audience has its own profile/logout, so `shared/` stays small.

**Trade-off:** more scaffolding per audience (model + guard + login pipeline), but each entity stays clean (its
own table/fields/flows), isolation is enforced by the **guard** (not an ability flag), and it mirrors how the
base already does `admins`. **This is the default** — distinct audiences are distinct entities.

### Model B (alternative) — one `users` table + `user_type` enum + Sanctum **abilities**
Use **only** when the audiences are the **same kind of account** — identical fields and login, differing only in
which screens/capabilities they get (e.g. "buyer"/"seller" that are otherwise the same person-account). Then add
a `user_type` enum on `users` (default in the model), mint the token with the type as an ability —
`$user->createToken('authToken', [$user->user_type->value])` — and gate routes with Sanctum's `ability`
middleware (register `abilities`/`ability` aliases in `bootstrap/app.php`, next to `admin.api`):
`Route::middleware(['auth:sanctum','ability:provider'])`. Less code, one login pipeline — but only honest when
the audiences really are one entity.

> Don't mix for the same audience: either a separate model+guard (A) **or** a `user_type` on `users` (B).

---

## 2) Ownership authorization (every audience) — prevent IDOR

Authentication ≠ authorization. A `provider` token must not read another provider's rows.

- **Scope every read/write to the caller.** List/show/update/delete service methods filter by the owner:
  `->where('user_id', auth()->id())` (or the audience's owning column). Never return a row by `id` alone
  when it belongs to a user. This is the first line of defense and is **mandatory** on owned resources.
- **Add a Policy when an action crosses ownership** (a provider acting on a user's order, accept/reject, etc.).
  The base has **no `app/Policies/`** yet — create it; Laravel auto-discovers `App\Policies\<Model>Policy`:
  ```php
  <?php
  namespace App\Policies;

  use App\Models\{User, Order};

  class OrderPolicy
  {
      public function update(User $user, Order $order): bool
      {
          return $order->provider_id === $user->id;  // owner/relationship check
      }
  }
  ```
  Enforce in the controller/service with `Gate::authorize('update', $order)` or `$this->authorize(...)`; a
  failed check yields 403 (`respondForbidden` on `ResponseTrait` controllers). Keep the policy thin — no queries.
- **Guest-owned data** (guest mode): scope by the device id `mac_address` instead of `auth()->id()`,
  and merge onto the user on login/register — see [`build-api.md`](build-api.md) "Guest mode".

---

## 3) Verify (definition of done)
1. A token for audience X **cannot** hit audience Y's routes — the `auth:<audience>` guard rejects it (401);
   `$request->user()` in each group returns the right model.
2. Requesting another owner's row by `id` returns 403/404 (not the row) — ownership scoping/Policy holds.
3. Each audience model mirrors `User`/`Admin` (`BaseAuthModelTrait`, `HasApiTokens`, `SoftDeletes`, hashed
   password) and its guard + eloquent provider are registered in `config/auth.php`.
4. The single-`users` + `user_type` model (B) was used **only** where `analysis.md` justified it (one account type).

## References (single source — do not duplicate here)
- Endpoint route/controller/service/request/resource → [`build-api.md`](build-api.md)
- Audience model/table (auth-style entity) + factory/seeder → [`build-database.md`](build-database.md) §3b
- Where the decision is recorded → `docs/project/analysis.md` §1
