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.

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

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) :

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 :

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 :

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