
# Sessions Go net/http

Le package `github.com/keenauth/keenauth-go/bff` ajoute la connexion à une application Go `net/http`. Votre serveur est le client OAuth et conserve tous les jetons côté serveur. Le navigateur ne reçoit qu'un cookie de session chiffré et HttpOnly, et ne voit jamais de jeton d'accès, de jeton de rafraîchissement ou de jeton d'identité. Il s'agit du modèle backend-for-frontend (BFF). Le package n'utilise que la bibliothèque standard. Il n'est pas encore publié.

Pour protéger une API qui reçoit à la place des jetons porteurs, consultez [Protéger votre API](/docs/sdk/protect-api).

## Avant de commencer

1. Dans le tableau de bord, créez une application web dans votre environnement.
2. Choisissez l'authentification du client : un secret client (`bff.ClientSecretBasic`) ou `private_key_jwt` (`bff.PrivateKeyJWT`). Pour `private_key_jwt`, enregistrez la clé publique de l'application et conservez la clé privée sur votre serveur.
3. Ajoutez l'URI de redirection `https://app.acme.example/auth/callback` et l'URI de redirection après déconnexion `https://app.acme.example/`.
4. Générez un secret de cookie d'au moins 32 caractères, par exemple avec `openssl rand -base64 32`. Conservez-le dans votre gestionnaire de secrets, et non dans le dépôt.

## Configuration

```go
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/keenauth/keenauth-go/bff"
)

func main() {
	auth, err := bff.New(bff.Options{
		Issuer:     "https://acme.eu.keenauth.app", // your environment's issuer, exactly
		ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
		ClientAuth: bff.ClientSecretBasic{Secret: os.Getenv("KEENAUTH_CLIENT_SECRET")},
		AppBaseURL: "https://app.acme.example",
		Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
	})
	if err != nil {
		panic(err) // configuration errors stop the app at start-up
	}

	mux := http.NewServeMux()
	mux.Handle("GET /orders", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s := bff.FromContext(r.Context()).Session
		fmt.Fprintf(w, "Signed in as %s", s.User.Email)
	})))

	http.ListenAndServe(":8080", auth.Middleware(mux))
}
```

Pour utiliser `private_key_jwt` à la place d'un secret client, transmettez la clé privée. Le package signe une nouvelle assertion client pour chaque requête de jeton et chaque requête PAR (`iss` et `sub` correspondent à votre ID client, `aud` correspond à l'émetteur, durée de vie de 2 minutes, un nouveau `jti` à chaque fois) :

```go
pemBytes, err := os.ReadFile("/run/secrets/keenauth-app-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes) // import "github.com/keenauth/keenauth-go/assertion"
if err != nil {
	panic(err)
}
auth, err := bff.New(bff.Options{
	Issuer:     "https://acme.eu.keenauth.app",
	ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
	ClientAuth: bff.PrivateKeyJWT{Key: key, KeyID: "app-key-2026-11"},
	AppBaseURL: "https://app.acme.example",
	Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
})
```

ES256 (P-256), EdDSA (Ed25519), RS256 et PS256 (RSA, 2048 bits ou plus) sont pris en charge ; l'algorithme suit la clé, et `Algorithm: "PS256"` sélectionne PSS pour une clé RSA. `Key` peut être n'importe quel `crypto.Signer`, donc une clé conservée dans un KMS ou un HSM fonctionne aussi. Si vous signez ailleurs, utilisez plutôt `bff.PrivateKeyJWT{Assertion: f}`, où `f` retourne une nouvelle assertion signée à chaque appel. Définissez l'un des deux, `Key` ou `Assertion`, mais pas les deux.

`AppBaseURL` est l'adresse publique de votre application. Le package construit l'URI de redirection et toutes les redirections à partir de cette valeur, jamais à partir des en-têtes `Host` ou `X-Forwarded-*` de la requête. Le protocole `http` n'est accepté que pour `localhost`.

## Routes

`Middleware` expose trois routes et lit la session à chaque autre requête. Vous pouvez aussi monter les routes seules avec `mux.Handle("/auth/", auth)`.

| Route | Rôle |
|---|---|
| `GET /auth/login` | Démarre la connexion. Paramètres de requête facultatifs : `returnTo` (où l'utilisateur atterrit) et `organization` (se connecter à une organisation). |
| `GET /auth/callback` | Termine la connexion et définit le cookie de session. |
| `GET` ou `POST /auth/logout` | Déconnecte l'utilisateur chez KeenAuth et le ramène à votre application. |

La connexion utilise le flux d'autorisation par code avec PKCE (S256). La requête est envoyée au serveur (PAR) lorsque votre environnement le propose. Au retour de l'appel de rappel, le package vérifie `state`, `iss` et le jeton d'identité, y compris son `nonce`. Chaque connexion réussie crée une nouvelle session ; l'ancienne est supprimée.

`returnTo` doit être un chemin de votre propre application, par exemple `/orders?tab=open`. Toute autre valeur, comme `//evil.example` ou une URL complète, est ignorée et l'utilisateur atterrit sur `/`.

## Protéger les gestionnaires

`auth.RequireAuth(handler)` ne laisse passer une requête qu'avec une session :

- une requête de page (`GET` ou `HEAD`) sans session est redirigée vers `/auth/login` avec `returnTo` ;
- une requête vers `/api/…`, ou avec une autre méthode, reçoit `401` avec un document de problème. Transmettez `bff.GuardOptions{API: true}` pour répondre `401` sur n'importe quel chemin.

Pour `POST`, `PUT`, `PATCH`, `DELETE` et les autres méthodes non sûres, le navigateur doit indiquer que la requête provient de votre application : `Sec-Fetch-Site: same-origin`, ou un en-tête `Origin` égal à l'origine de votre application. Les autres requêtes reçoivent `403`. Cela protège contre la falsification de requête intersite, en complément de `SameSite=Lax`.

## Utiliser la session et appeler votre API

`bff.FromContext(r.Context())` retourne :

- `Session` : `nil`, ou l'utilisateur connecté. `Session.User` expose des revendications typées du jeton d'identité, comme `Subject`, `Email`, `Name`, `OrgID` et `OrgRoles` ; toutes les revendications figurent dans `Session.User.Claims`. La session ne contient jamais de jeton.
- `AccessToken` : un jeton d'accès valide pour les appels côté serveur, ou `""`. Ne l'envoyez pas au navigateur.

Lorsque le jeton d'accès expire dans moins de 60 secondes, le package le rafraîchit avant l'exécution de votre gestionnaire. Chaque rafraîchissement remplace le jeton de rafraîchissement et définit le nouveau cookie de session dans la réponse. Si le serveur refuse le jeton de rafraîchissement, la session prend fin. Une erreur serveur temporaire laisse la session inchangée. Les requêtes simultanées d'un même processus partagent un seul rafraîchissement.

## Changer d'organisation

`auth.SwitchOrganizationPath("org_01j9…", "/dashboard")` retourne un lien vers `/auth/login` qui reconnecte l'utilisateur dans cette organisation. KeenAuth vérifie l'appartenance. Après la connexion, `Session.User.OrgID` contient l'organisation.

## Se déconnecter

Envoyez une requête `POST` vers `/auth/logout` depuis un formulaire. Le package supprime le cookie de session (et la session stockée, si vous utilisez un store), révoque le jeton de rafraîchissement et envoie le navigateur vers le point de terminaison de déconnexion de votre environnement, qui le ramène à votre application. Une requête que le navigateur marque comme provenant d'un autre site est refusée. Les navigateurs qui n'envoient pas `Sec-Fetch-Site` ne peuvent se déconnecter qu'avec un `POST` depuis vos propres pages.

## Stockage des sessions

Par défaut, la session entière se trouve dans le cookie, chiffrée et authentifiée avec AES-256-GCM à l'aide d'une clé dérivée du secret. Un cookie modifié ou copié est rejeté. Le cookie se nomme `__Host-keenauth_session.0` et est défini avec `Secure`, `HttpOnly`, `SameSite=Lax` et `Path=/`. Sur `http://localhost`, le préfixe `__Host-` et `Secure` sont omis. Les sessions volumineuses sont réparties sur jusqu'à trois cookies.

Pour conserver les jetons côté serveur, définissez `Options.Store` avec un `bff.SessionStore` (`Get`, `Set`, `Delete`), par exemple adossé à Redis. Le cookie ne contient alors qu'un identifiant de session aléatoire chiffré. Chaque connexion crée un nouvel identifiant et supprime l'enregistrement précédent. Le store contient des jetons : limitez l'accès à celui-ci. `bff.MemoryStore` est réservé au développement et aux tests.

Une session dure au maximum 7 jours (`SessionMaxAge`) ; le rafraîchissement ne la prolonge pas. Pour changer le secret, placez le nouveau en premier dans la liste : `Secrets: []string{newSecret, oldSecret}`. Retirez l'ancien après `SessionMaxAge`.

## Limites actuelles

- Les jetons liés à DPoP et la déconnexion back-channel ne sont pas encore gérés.
- Les rafraîchissements ne sont partagés qu'au sein d'un même processus serveur. Avec plusieurs instances, le paramètre d'application `refresh_reuse_interval_s` (5 secondes par défaut) permet à un rafraîchissement parallèle de réussir ; si vous le définissez à `0`, le second rafraîchissement met fin à la session.
