
# Añadir inicio de sesión a una aplicación Laravel

Esta guía rápida añade el inicio de sesión de KeenAuth a una aplicación Laravel 13 con el cliente estándar de OpenID Connect `facile-it/php-openid-client`. KeenAuth todavía no tiene un SDK para PHP, y no lo necesita: KeenAuth es un proveedor estándar de OpenID Connect, así que funciona cualquier biblioteca cliente estándar de OpenID Connect. Su aplicación es el cliente de OAuth y guarda todos los tokens en la sesión del servidor; el navegador solo recibe una cookie cifrada con el identificador de sesión. Añadirá las rutas de inicio de sesión, protegerá una página, llamará a una API protegida con el token de acceso del usuario, cerrará la sesión y probará la aplicación sin conexión.

La aplicación tiene estas rutas:

| Ruta | Qué hace |
|---|---|
| `GET /` | Página de inicio pública: un botón para iniciar sesión, o el nombre del usuario y un botón para cerrar sesión |
| `GET /dashboard` | Requiere una sesión. Muestra el usuario y los mensajes que devuelve su API |
| `POST /login` | Inicia el inicio de sesión. Laravel comprueba el token CSRF |
| `GET /auth/callback` | Comprueba la respuesta, intercambia el código e inicia la sesión |
| `POST /logout` | Termina la sesión aquí y en KeenAuth |

## Antes de empezar

- Un entorno de KeenAuth. Su emisor tiene un aspecto como `https://your-environment.eu.keenauth.app`.
- Una aplicación web en el entorno ([Crear una aplicación](/docs/api/management/applications/create)) con un secreto de cliente (`client_secret_basic`). En un entorno `development`, registre el URI de redirección `http://localhost/auth/callback` y el URI de redirección tras cerrar sesión `http://localhost/`; un URI de bucle local se registra sin puerto y coincide con cualquier puerto, como `8000`.
- Una API registrada en el entorno ([Crear una API](/docs/api/management/apis/create)) con el ámbito `messages:read`. Su `identifier`, por ejemplo `https://api.example.com`, es la audiencia de sus tokens de acceso. La [guía rápida de Express](/docs/quickstarts/express) crea una API que acepta estos tokens.
- PHP 8.3 o posterior y Composer 2.

## 1. Instalar

```sh
composer require facile-it/php-openid-client:1.0.0 guzzlehttp/guzzle:8.3.1
```

La biblioteca necesita un cliente HTTP PSR-18; Guzzle es uno de ellos. El ejemplo fija cada paquete exactamente en `composer.json`, y `composer.lock` fija cada paquete a un commit. Se instala con `composer install --no-plugins --no-scripts`, así que no se ejecuta código de paquetes durante la instalación.

## 2. Configurar

La aplicación lee su configuración únicamente de variables de entorno, a través de `config/keenauth.php`. Colóquelas en `.env`, que Laravel carga, y no suba nunca ese archivo al control de versiones.

| Variable | Obligatoria | Significado |
|---|---|---|
| `KEENAUTH_ISSUER` | sí | El emisor de su entorno, con `https` |
| `KEENAUTH_CLIENT_ID` | sí | El `client_id` de la aplicación web |
| `KEENAUTH_CLIENT_SECRET` | sí | Su secreto de cliente |
| `APP_BASE_URL` | sí | La URL pública de la aplicación. El URI de redirección se construye a partir de ella. `http` solo para `localhost`, `127.0.0.1` o `[::1]` |
| `API_URL` | sí | La API que llama la aplicación, por ejemplo `http://localhost:8080` |
| `KEENAUTH_API_AUDIENCE` | sí | El identificador de esa API en KeenAuth |
| `KEENAUTH_ID_TOKEN_ALG` | no | El algoritmo con el que su entorno firma los tokens de ID: `RS256` (predeterminado), `PS256`, `ES256` o `EdDSA` |
| `APP_KEY` | sí | La clave propia de Laravel; cifra la cookie de sesión. Genere una con `php artisan key:generate` |

`Config::fromArray` comprueba estos valores. Un valor que falta o no es utilizable detiene la primera solicitud que lo necesita, y ningún mensaje de error contiene un valor. Se rechaza un algoritmo HMAC, para que el secreto de cliente nunca pueda firmar un token de ID:

```php
        $alg = $values['id_token_alg'] ?? 'RS256';
        if (!in_array($alg, ['RS256', 'PS256', 'ES256', 'EdDSA'], true)) {
            // Never an HMAC algorithm: the client secret must not be able to sign an ID token.
            throw new InvalidArgumentException('KEENAUTH_ID_TOKEN_ALG must be RS256, PS256, ES256 or EdDSA');
        }
```

## 3. Crear el cliente de OpenID Connect

`App\KeenAuth\KeenAuth` construye el cliente a partir del descubrimiento (`<issuer>/.well-known/openid-configuration`) y guarda en caché los metadatos y el conjunto de claves durante una hora:

```php
        $issuer = (new IssuerBuilder())
            ->setMetadataProviderBuilder((new MetadataProviderBuilder())->setHttpClient($this->http)
                ->setCache($this->cache)->setCacheTtl(3600))
            ->setJwksProviderBuilder((new JwksProviderBuilder())->withHttpClient($this->http)
                ->withCache($this->cache)->withCacheTtl(3600))
            ->build($this->config->issuer);
```

```php
            ->setClientMetadata(ClientMetadata::fromArray([
                'client_id' => $this->config->clientId,
                'client_secret' => $this->config->clientSecret,
                'token_endpoint_auth_method' => 'client_secret_basic',
                'redirect_uris' => [$this->config->redirectUri()],
                // The verifier accepts only this algorithm, never an HMAC one.
                'id_token_signed_response_alg' => $this->config->idTokenAlg,
            ]))
```

El proveedor de servicios lo registra como singleton, y el cliente HTTP como un enlace aparte, para que las pruebas puedan reemplazarlo.

## 4. Iniciar el inicio de sesión

Cada inicio de sesión genera un nuevo `state`, `nonce` y verificador PKCE, y los guarda en la sesión:

```php
    public static function newTransaction(): array
    {
        return [
            'state' => self::base64url(random_bytes(32)),
            'nonce' => self::base64url(random_bytes(32)),
            'code_verifier' => self::base64url(random_bytes(32)),
        ];
    }
```

La solicitud de autorización envía el desafío S256 del verificador y el identificador de su API como `resource` (RFC 8707). Sin `resource`, el token de acceso no está destinado a su API y la API lo rechaza.

```php
        return $this->service()->getAuthorizationUri($this->client(), [
            // messages:read is a scope of your API; the example API requires it on /api/messages.
            'scope' => 'openid profile email messages:read',
            'state' => $transaction['state'],
            'nonce' => $transaction['nonce'],
            // PKCE (RFC 7636): S256 of the verifier that only this session knows.
            'code_challenge' => self::base64url(hash('sha256', $transaction['code_verifier'], true)),
            'code_challenge_method' => 'S256',
            // RFC 8707: an access token for your API.
            'resource' => $this->config->apiAudience,
        ]);
```

El inicio de sesión es un formulario que se envía a `/login` con el token CSRF, para que otro sitio no pueda iniciar un inicio de sesión en nombre del usuario:

```php
    <form action="/login" method="post">
      @csrf
      <button type="submit">Sign in</button>
    </form>
```

## 5. Gestionar la llamada de retorno

La biblioteca deja la comprobación de `state` en manos de su aplicación. La llamada de retorno saca la transacción de la sesión, de modo que un `state` solo funciona una vez, y lo compara en tiempo constante. Su entorno se identifica en cada respuesta de autorización (RFC 9207), así que la llamada de retorno también rechaza una respuesta cuyo `iss` falta o nombra otro emisor:

```php
        $transaction = $request->session()->pull('keenauth.transaction');
        $state = $request->query('state');
        if (!is_array($transaction) || !is_string($state) || !hash_equals($transaction['state'], $state)) {
            return $this->failure('invalid_state');
        }
        // RFC 9207: the response names its issuer, so a response from another provider is refused.
        $iss = $request->query('iss');
        if ($keenauth->issuerInResponse() && $iss !== $keenauth->config->issuer) {
            return $this->failure('invalid_issuer');
        }
```

De la llamada de retorno solo se toma el código. La biblioteca lo intercambia con el verificador PKCE y comprueba el token de ID: su firma con las claves de su entorno, `iss`, `aud`, `exp`, `iat`, el algoritmo y el `nonce` de este inicio de sesión.

```php
        return $this->service()->callback($this->client(), ['code' => $code], $this->config->redirectUri(),
            AuthSession::fromArray($transaction));
```

Después, la aplicación inicia un nuevo identificador de sesión y guarda el usuario y los tokens en la sesión del servidor:

```php
        // A new session id after sign-in prevents session fixation.
        $request->session()->regenerate(destroy: true);
```

Los ajustes de sesión están en `config/session.php`: el controlador `file`, una cookie cifrada, HttpOnly y `SameSite=Lax`, que es `Secure` salvo que establezca `SESSION_SECURE_COOKIE=false` para `http://localhost`. Con varios servidores, use el controlador `redis` o `database`.

## 6. Proteger páginas y llamar a su API

Las rutas que necesitan un usuario usan el middleware `keenauth`. El ejemplo no tiene token de actualización: cuando caduca el token de acceso, el usuario vuelve a iniciar sesión.

```php
Route::get('/dashboard', [DashboardController::class, 'show'])->middleware('keenauth');
```

El panel llama a su API con el token de acceso como token de portador. El token permanece en el servidor; el navegador recibe solo el HTML.

```php
        $response = $http->sendRequest(new HttpRequest('GET', $keenauth->config->apiUrl . '/api/messages', [
            'Authorization' => 'Bearer ' . $request->session()->get('keenauth.access_token'),
            'Accept' => 'application/json',
        ]));
```

## 7. Cerrar sesión

Cerrar sesión es un `POST` con el token CSRF. La aplicación termina la sesión local y envía el navegador al endpoint de cierre de sesión de su entorno (OpenID Connect RP-Initiated Logout) con `id_token_hint`, `client_id` y `post_logout_redirect_uri`, que devuelve el navegador a su aplicación:

```php
        $idToken = $request->session()->get('keenauth.id_token');
        $request->session()->invalidate();
        $request->session()->regenerateToken();
```

## 8. Ejecutar la aplicación

Inicie la API de la guía rápida de Express (o cualquier API que acepte los tokens) y ejecute la aplicación:

```sh
php artisan serve
```

Abra `http://localhost:8000`, elija **Iniciar sesión**, inicie sesión en KeenAuth y abra el panel.

## 9. Probar sin red

Las pruebas no necesitan red ni una cuenta de KeenAuth. Un cliente PSR-18 simulado ocupa el lugar del cliente HTTP y hace de su entorno (descubrimiento, un conjunto de claves creado para la ejecución de las pruebas, el endpoint de tokens) y de la API. Las pruebas se ejecutan con `APP_ENV=ci`, porque Laravel omite la comprobación CSRF en `testing` y estas pruebas la verifican.

```php
    public function test_callback_with_an_unknown_state_is_refused_before_any_token_request(): void
    {
        $this->startSignIn();
        $this->returnFromKeenAuth(['state' => 'not-the-state'])->assertStatus(401)->assertSee('invalid_state');
```

```sh
vendor/bin/phpunit
php ../scripts/php_deps.php    # exact pins, commits and licences
```

Las pruebas comprueban que la configuración se valida (y que ningún error contiene un secreto); que el inicio de sesión necesita el token CSRF y envía PKCE S256, `state`, `nonce` y `resource`; que el código se intercambia con el verificador correspondiente y la autenticación del cliente; que el token de acceso va a la API en la cabecera `Authorization` y nunca a una cookie; que un `state` desconocido o reutilizado, un `iss` ausente o incorrecto, un token de ID para otro cliente o con otro nonce, y un error de autorización se rechazan sin crear una sesión; que una página sin sesión redirige a la página de inicio; y que cerrar sesión necesita el token CSRF y va al endpoint de cierre de sesión con `id_token_hint`.

El inicio de sesión completo con un navegador real necesita un entorno de KeenAuth. No forma parte de las pruebas sin conexión.

## Detrás de un proxy o balanceador de carga

Establezca `APP_BASE_URL` en la URL pública de la aplicación; el URI de redirección se construye a partir de ella, nunca a partir de la cabecera `Host`. Si un proxy termina TLS, configure los proxies de confianza de Laravel solo para ese proxy.

## Límites actuales

- No hay token de actualización ni inicio de sesión silencioso: el usuario vuelve a iniciar sesión cuando caduca el token de acceso.
- El cliente se autentica con un secreto de cliente. La biblioteca también admite `private_key_jwt`; el ejemplo no lo muestra.

## Próximos pasos

- [Proteger una API de Express](/docs/quickstarts/express): la API que llama esta aplicación.
- [Verificación de tokens](/docs/sdk/token-verification): el verificador bajo el middleware de su API.
