# Laravel Passport Authentication for Contact Model

Complete guide for implementing Passport-based authentication for Contacts in a multi-tenant Laravel application.

---

## 📋 Table of Contents

- [Overview](#overview)
- [Prerequisites](#prerequisites)
- [Configuration](#configuration)
- [Database Setup](#database-setup)
- [Contact Model Setup](#contact-model-setup)
- [Create Personal Access Client](#create-personal-access-client)
- [API Endpoints](#api-endpoints)
- [Testing with cURL](#testing-with-curl)
- [Testing with Postman](#testing-with-postman)
- [Troubleshooting](#troubleshooting)

---

## Overview

This implementation provides:

- ✅ **Email + Password** authentication
- ✅ **Phone-only** authentication (no password required)
- ✅ Multi-tenant support (Stancl Tenancy)
- ✅ Laravel Passport v13+ integration
- ✅ Secure token-based API authentication

---

## Prerequisites

- Laravel 12+
- Laravel Passport v13.7+
- Stancl Tenancy package
- PostgreSQL database
- PHP 8.2+

---

## Configuration

### 1. Passport Config (`config/passport.php`)

```php
<?php

return [
    'guard' => 'api',
    'middleware' => [],
    
    'private_key' => env('PASSPORT_PRIVATE_KEY', storage_path('app/../oauth-private.key')),
    'public_key' => env('PASSPORT_PUBLIC_KEY', storage_path('app/../oauth-public.key')),
    
    'connection' => env('PASSPORT_CONNECTION', null),
];
```

### 2. Auth Config (`config/auth.php`)

```php
'guards' => [
    'api' => [
        'driver' => 'passport',
        'provider' => 'contacts',
    ],
],

'providers' => [
    'contacts' => [
        'driver' => 'eloquent',
        'model' => App\Models\Contact::class,
    ],
],
```

---

## Database Setup

### Generate Passport Keys

```bash
php artisan passport:keys --force
```

### Run Migrations

```bash
# OAuth tables on tenant databases
php artisan tenants:migrate --path=database/migrations/tenant

# Passport columns on contacts table
php artisan tenants:migrate --path=database/migrations/2026_04_02_065927_add_passport_columns_to_contacts_table.php
```

---

## Contact Model Setup

**File:** `app/Models/Contact.php`

```php
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class Contact extends Authenticatable
{
    use HasApiTokens, Notifiable;

    protected $fillable = [
        'uid', 'category', 'type', 'name', 'contact_code',
        'primary_email', 'primary_phone', 'is_login', 'app_name',
        'source', 'status', 'created_by', 'updated_by',
        'password', 'remember_token',
    ];

    protected $hidden = ['password', 'remember_token'];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    /**
     * Find contact by email or phone for authentication.
     * Phone login: no password required
     * Email login: password required
     */
    public function findForPassport($identifier): ?self
    {
        // Check if identifier is a phone number
        $isPhone = preg_match('/^[+\d]/', $identifier);
        
        if ($isPhone) {
            // Phone login - no password required
            return $this->where('primary_phone', $identifier)->first();
        }
        
        // Email login - must have password set
        return $this->where('primary_email', $identifier)
            ->whereNotNull('password')
            ->first();
    }

    public function getAuthIdentifierName(): string
    {
        return 'id';
    }

    public function getAuthIdentifier()
    {
        return $this->getKey();
    }

    public function getAuthPassword(): string
    {
        return $this->password;
    }

    public function getRememberToken(): ?string
    {
        return $this->remember_token;
    }

    public function setRememberToken($value): void
    {
        $this->remember_token = $value;
    }

    public function getRememberTokenName(): string
    {
        return 'remember_token';
    }
}
```

---

## Create Personal Access Client

### Option 1: Using Custom Command

```bash
# For specific tenant (replace 1 with tenant ID)
php artisan passport:personal-access-client --tenant=1

# For all tenants
php artisan passport:personal-access-client --tenant=all
```

### Option 2: Via Seeder (Automatic)

When seeding a tenant, the personal access client is created automatically:

```bash
php artisan tenants:seed --tenant=1
```

### Option 3: Manual via Tinker

```bash
php artisan tinker
```

```php
$tenant = AdminApp\Models\Tenant::find(1);
tenancy()->initialize($tenant);

$client = new Laravel\Passport\Client();
$client->id = (string) Illuminate\Support\Str::orderedUuid();
$client->name = 'Contact Personal Access';
$client->secret = Illuminate\Support\Str::random(40);
$client->provider = 'contacts';
$client->redirect_uris = [];
$client->grant_types = ['personal_access'];
$client->revoked = false;
$client->save();
```

---

## API Endpoints

| Method | Endpoint | Auth Required | Description |
|--------|----------|---------------|-------------|
| POST | `/contact/register` | No | Register new contact |
| POST | `/contact/login` | No | Login (email+password or phone-only) |
| GET | `/contact/user` | Yes | Get authenticated user |
| POST | `/contact/logout` | Yes | Logout and revoke token |

---

## Testing with cURL

### 1. Register Contact

```bash
curl -X POST http://demo.localhost:8000/contact/register \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "name": "John Doe",
    "primary_email": "john@example.com",
    "primary_phone": "+1234567890",
    "password": "password123",
    "password_confirmation": "password123"
  }'
```

**Response:**
```json
{
  "message": "Contact registered successfully",
  "contact": {
    "id": 1,
    "uid": "abc123xyz",
    "name": "John Doe",
    "primary_email": "john@example.com",
    "primary_phone": "+1234567890",
    ...
  },
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
  "token_type": "Bearer"
}
```

---

### 2. Login with Email + Password

```bash
curl -X POST http://demo.localhost:8000/contact/login \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "primary_email": "john@example.com",
    "password": "password123"
  }'
```

---

### 3. Login with Phone Only (No Password)

```bash
curl -X POST http://demo.localhost:8000/contact/login \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "primary_phone": "+1234567890"
  }'
```

> **Note:** Phone login does not require a password. Anyone with the phone number can login.

---

### 4. Get Authenticated User

```bash
curl -X GET http://demo.localhost:8000/contact/user \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Accept: application/json"
```

Replace `YOUR_ACCESS_TOKEN` with the token from login/register response.

---

### 5. Logout

```bash
curl -X POST http://demo.localhost:8000/contact/logout \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Accept: application/json"
```

---

## Testing with Postman

### Step 1: Register or Login

1. Create a new request
2. Method: `POST`
3. URL: `http://demo.localhost:8000/contact/register` (or `/contact/login`)
4. Headers tab:
   - `Content-Type: application/json`
   - `Accept: application/json`
5. Body tab → raw → JSON:
   ```json
   {
     "name": "John Doe",
     "primary_email": "john@example.com",
     "primary_phone": "+1234567890",
     "password": "password123",
     "password_confirmation": "password123"
   }
   ```
6. Click **Send**
7. Copy the `access_token` from response

---

### Step 2: Authenticated Request

1. Create a new request
2. Method: `GET`
3. URL: `http://demo.localhost:8000/contact/user`
4. Authorization tab:
   - Type: `Bearer Token`
   - Token: `{paste_access_token_here}`
5. Click **Send**

---

## Troubleshooting

### "Invalid key supplied"

```bash
# Regenerate Passport keys
php artisan passport:keys --force

# Verify keys exist
ls -la storage/oauth-*.key
```

---

### "Personal access client not found"

```bash
# Create client for tenant
php artisan passport:personal-access-client --tenant=1
```

---

### "user_id invalid input syntax"

Ensure:
- `getAuthIdentifierName()` returns `'id'`
- `oauth_access_tokens` migration uses `unsignedBigInteger('user_id')`

---

### Clear Caches

```bash
php artisan config:clear
php artisan cache:clear
php artisan route:clear
```

---

### Check OAuth Clients in Tenant

```bash
php artisan tinker --execute="
    \$tenant = AdminApp\Models\Tenant::find(1);
    if(\$tenant) {
        tenancy()->initialize(\$tenant);
        \$clients = Laravel\Passport\Client::where('provider', 'contacts')->get();
        foreach(\$clients as \$client) {
            echo 'ID: ' . \$client->id . ', Name: ' . \$client->name . PHP_EOL;
        }
    }
"
```

---

## Security Notes

⚠️ **Phone Login Security:**
- Phone-only login does **not** require a password
- Anyone with the phone number can authenticate
- Consider adding OTP verification for production use

✅ **Email Login Security:**
- Requires valid password
- Password is hashed using bcrypt
- Token expires after configured time (default: 1 year)

---

## File Structure

```
app/
├── Console/Commands/
│   └── CreatePassportPersonalAccessClient.php
├── Http/Controllers/Contact/
│   └── ContactAuthController.php
├── Models/
│   └── Contact.php
config/
├── auth.php
└── passport.php
database/
├── migrations/
│   └── 2026_04_02_065927_add_passport_columns_to_contacts_table.php
├── migrations/tenant/
│   ├── 2026_04_02_063135_create_oauth_auth_codes_table.php
│   ├── 2026_04_02_063136_create_oauth_access_tokens_table.php
│   ├── 2026_04_02_063137_create_oauth_refresh_tokens_table.php
│   ├── 2026_04_02_063138_create_oauth_clients_table.php
│   └── 2026_04_02_063139_create_oauth_device_codes_table.php
└── seeders/
    └── DatabaseSeeder.php
routes/
└── contact.php
```

---

## License

This implementation is part of the TaskCo Starter Kit.
