Skip to content

API Authentication

Authenticatable model

The API authenticatable is App\Models\SystemConnection, not User. A caller authenticates as its own connection — there is no separate API-client entity. Never use a User model for API auth — it does not exist in this project, and Conflux has no App/Sanctum-style API-client model either; that concept was removed (see ADR-0002 and ADR-0006).

The connection guard

Authentication runs through a custom guard, not a package:

Piece Value
Guard name connection
Guard driver connection-token, registered via Auth::extend() in app/Providers/AppServiceProvider.php
Guard implementation App\Auth\SystemConnectionTokenGuard
Config config/auth.phpguards.connection

The guard reads the bearer token from the request, hashes it with SHA-256, and looks up a SystemConnectionToken where purpose = access (see System Connection Tokens). If the token is found and isValid() (not expired, not used), the guard resolves to the owning SystemConnection (eager-loaded with tenant and system). No personal_access_tokens table or polymorphic tokenable resolution is involved — the token always belongs to a SystemConnection.

Route protection

All API routes must be defined in routes/api.php. Both middleware are applied automatically to every route in that file via appendToGroup('api', ...) in bootstrap/app.php — no per-route or per-group middleware declaration is needed:

// routes/api.php — just bare routes, middleware is applied globally
Route::get('/me', MeController::class)->name('api.me');
  • auth:connection — resolves and authenticates the bearer token via the connection guard.
  • ensure.connection.active — rejects requests from an unhealthy connection or an inactive tenant with a 401 response. Implemented at app/Http/Middleware/EnsureSystemConnectionIsActive.php.

Routes that must run before a caller has a token — such as /system-connections/register — opt out with ->withoutMiddleware(['auth:connection', 'ensure.connection.active']). See Connection Flow for why /register is unauthenticated and /connect is not.

If a future endpoint must be public, define it in a separate route file — do not add it to routes/api.php.

Issuing tokens

Access tokens are not managed through the admin panel — there is no "generate token" UI action. A SystemConnectionToken with purpose = access is issued the same way any connection token is: via App\Services\SystemConnectionService, as part of the connection flow. The plaintext value is returned to the caller once; only its SHA-256 hash is ever persisted.

For local development, use the conflux-local-dev MCP tool to issue a SystemConnection access token directly, without going through the HTTP handshake.

Testing

Authenticate as a SystemConnection in tests by attaching a valid access token as a bearer header — the guard reads the token from the request, not from Laravel's session-based Auth facade:

$connection = SystemConnection::factory()->active()->create(['tenant_id' => $tenant->id]);
$plainToken = 'plain-access-token-'.$connection->id;

SystemConnectionToken::factory()->accessToken()->valid()->create([
    'system_connection_id' => $connection->id,
    'token' => hash('sha256', $plainToken),
]);

$this->withToken($plainToken)->getJson('/api/me');

The shared tests/Pest.php helpers callerConnection(), activeConnectionFor(), and accessTokenFor() wrap exactly this setup — prefer them over rebuilding it inline. See any test under tests/Feature/Api/ (e.g. MeTest.php) for usage.

Do not use actingAs() for API requests — it authenticates against Laravel's session-based guards (web), not the token-based connection guard used by routes/api.php.

API documentation (Scribe)

Scribe is configured to pick up api/* routes and defaults to Bearer token authentication in the generated docs. Annotate controllers with #[Group] and #[Response] attributes to improve the output:

use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;

#[Group('Connections')]
class SystemConnectionsController extends Controller
{
    #[Response(['data' => []], status: 200)]
    public function __invoke(Request $request): JsonResponse { ... }
}

Regenerate docs after controller changes:

docker compose exec php php artisan scribe:generate

Error Responses

Error responses follow RFC 9457 (Problem Details for HTTP APIs) with Content-Type: application/problem+json. See API Errors for the full reference, including the handled exception types and the #[ApiProblem] attribute for documenting errors in Scribe.