# Laravel 11 Nafath Authentication

A readable Laravel 11 web and REST authentication implementation for Nafath 3.0.1. It includes a Blade login experience, verified local users, Laravel sessions, bearer API tokens, a Postman collection, external-call monitoring, PostgreSQL, pgAdmin, Nginx, and PHP 8.3.

For architecture diagrams, a file-by-file explanation, security decisions, REST examples, troubleshooting, reuse instructions, and the production checklist, read the [complete Nafath developer guide](docs/NAFATH_DEVELOPER_GUIDE.md).

## Authentication flows

### Mobile approval without redirect

1. The user enters a National ID or Iqama on the Blade page or through the REST API.
2. The application calls Nafath MFA `request` and receives `transId` plus a two-digit `random` number.
3. The same page displays the number while polling the MFA `status` endpoint.
4. The user opens the Nafath mobile app, matches the number, and approves or rejects the request.
5. `COMPLETED` marks the mobile step approved, but does not create a user by itself.
6. ELM sends the shared callback a one-time `state`. Laravel retrieves the OIDC JWT, validates it with ELM, verifies its JWK signature locally, and correlates its signed `requestId` to the MFA attempt.
7. Laravel requires the JWT `nationalId` to match the encrypted National ID submitted at the start. Only then does it create/update the rich user, log the browser in, or allow REST token exchange.

This hybrid design has no browser redirect: MFA provides the mobile approval experience, while the OIDC callback/JWT APIs provide signed identity claims. The callback URL must be publicly reachable and registered with ELM even though the user's browser never leaves the local page.

### Browser OIDC redirect

1. The application requests a signed Nafath URL with a client-generated UUID `requestId`.
2. The user opens that URL and authenticates on Nafath.
3. ELM posts a single-use `state` to the application callback.
4. The application exchanges `state` for a JWT and asks ELM to validate it.
5. The JWT signature is also verified locally against ELM's JWK.
6. The verified `nationalId` and documented identity claims create or update the local user.
7. The original browser receives a Laravel session, or an API client exchanges its completed attempt for a bearer token.

Passwords are never collected by this application.

## Start with Docker

```bash
docker compose up --build -d
```

Services:

| Service | Address |
| --- | --- |
| Web login | http://localhost:8000/login |
| REST API | http://localhost:8000/api/v1 |
| Health | http://localhost:8000/api/v1/health |
| pgAdmin | http://localhost:5050 |
| PostgreSQL | localhost:5432 |

The application container waits for PostgreSQL and runs migrations automatically.

### Live development without rebuilding Nginx

The Compose development setup bind-mounts the project into the PHP container. Changes to PHP classes, routes, configuration files, Blade views, JavaScript, CSS, and files under `public/` are therefore visible on the next request. Nginx and PostgreSQL remain running.

Run the initial build once:

```bash
docker compose up --build -d
```

After that, edit files normally. You do **not** need to run `docker compose down`, rebuild the image, or recreate Nginx for application-code changes.

Some changes still require an explicit command:

```bash
# Run a newly added migration without restarting containers
docker compose exec app php artisan migrate

# Install packages after composer.json or composer.lock changes
docker compose exec app composer install

# Clear Laravel caches if you manually cached configuration or routes
docker compose exec app php artisan optimize:clear

# Reload Nginx only after changing docker/nginx/default.conf
docker compose exec nginx nginx -s reload
```

The Linux `vendor/` directory is stored in the `composer_vendor` Docker volume. This prevents Windows-host packages from replacing the packages used by PHP inside the container.

The development image includes `require-dev` packages such as Laravel Pail. On container startup, Composer checks the Linux vendor volume and regenerates Laravel's package manifests. This prevents stale host caches from referencing service providers that are absent inside the container.

If the application container previously failed with `Laravel\Pail\PailServiceProvider not found`, apply the corrected image and startup script with:

```bash
docker compose up -d --build --force-recreate app
```

This is a one-time repair. It does not recreate Nginx, PostgreSQL, or pgAdmin.

## Nafath configuration

Set these values in `.env`:

```dotenv
NAFATH_BASE_URL=https://rabet-nafath.api.elm.sa/nafath-sandbox
# Set this only when ELM gives MFA a different sandbox/production base URL.
# NAFATH_MFA_BASE_URL=https://provider-confirmed-mfa-base.example/
NAFATH_APP_ID=your-app-id
NAFATH_APP_KEY=your-app-key
NAFATH_CLIENT_PUBLIC_IP=your-registered-egress-ip
NAFATH_SERVICE=Login
NAFATH_TIMEOUT=30
NAFATH_ATTEMPT_TTL_MINUTES=10
NAFATH_LOG_LEVEL=info
```

`NAFATH_CLIENT_PUBLIC_IP` must be the public outbound IP registered with ELM. Do not use a Docker `172.x.x.x` or private `192.168.x.x` address.

The application reads the bind-mounted `.env` file instead of receiving a frozen copy through Compose. An `.env` change is therefore used by subsequent Laravel requests without rebuilding Nginx. If configuration was cached manually, run `docker compose exec app php artisan optimize:clear`.

ELM must configure this callback, hosted in Saudi Arabia as required by the supplied documentation:

```text
https://your-public-domain.example/auth/nafath/callback
```

Localhost cannot receive ELM's internet callback. Use the real staging domain for an end-to-end sandbox login.

## Web routes

| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/login` | Blade login page |
| POST | `/auth/nafath/start` | Create the signed Nafath session |
| GET | `/auth/nafath/status/{attempt}` | Browser polling endpoint |
| POST | `/auth/nafath/mfa/start` | Start mobile approval using National ID |
| GET | `/auth/nafath/mfa/status/{attempt}` | Poll Nafath mobile approval and log in on completion |
| POST | `/auth/nafath/callback` | ELM callback receiving `state` |
| GET | `/dashboard` | Authenticated identity profile |
| POST | `/logout` | End the Laravel session |
| GET | `/monitoring/external-calls` | Public development monitoring page |

## REST API

| Method | Route | Authentication |
| --- | --- | --- |
| POST | `/api/v1/auth/nafath/start` | Public, throttled |
| GET | `/api/v1/auth/nafath/status/{attempt}` | `X-Nafath-Attempt-Token` |
| POST | `/api/v1/auth/nafath/mfa/start` | Public, National ID in JSON body |
| GET | `/api/v1/auth/nafath/mfa/status/{attempt}` | `X-Nafath-Attempt-Token`; returns user on completion |
| POST | `/api/v1/auth/nafath/token/{attempt}` | Attempt token in JSON body |
| GET | `/api/v1/auth/me` | Bearer token |
| POST | `/api/v1/auth/logout` | Bearer token |

Import [postman/Nafath-Authentication.postman_collection.json](postman/Nafath-Authentication.postman_collection.json) into Postman. Its test scripts save the attempt ID, attempt token, authorization URL, and bearer token automatically.

## Monitoring

Every external Nafath call records:

- endpoint and method;
- request ID;
- response status;
- duration;
- redacted request/response diagnostics;
- connection and provider errors.

View records publicly at `/monitoring/external-calls`; signing in is not required. The page shows the full provider URL, request IDs, status codes, duration, errors, and expandable query/body/response diagnostics. A dedicated daily file is also written to `storage/logs/nafath-*.log`. Tokens, callback state, national IDs, transaction IDs, random values, and credentials are redacted from monitoring payloads.

The public route is intended for integration testing. Protect it with authentication, IP allow-listing, or administrator authorization before exposing a production deployment to the internet.

## Tests

Build the disposable test image and run PHPUnit:

```bash
docker build --build-arg INSTALL_DEV=true -t laravel-api-test .
docker run --rm --entrypoint php \
  -e APP_ENV=testing \
  -e APP_KEY=base64:khKlThaPgddOLAxireqRPaoL/oEswdylttqxMnsjO8Q= \
  -e CACHE_STORE=array \
  -e SESSION_DRIVER=array \
  laravel-api-test vendor/bin/phpunit
```

The tests fake ELM network responses. They never create a real Nafath authentication request.

## pgAdmin

Open http://localhost:5050 and register PostgreSQL using host `db`, port `5432`, and the credentials from `.env`.
