Sessioni Go net/http

Il pacchetto github.com/keenauth/keenauth-go/bff aggiunge l'accesso a un'applicazione Go net/http. Il tuo server è il client OAuth e conserva tutti i token lato server. Il browser riceve solo un cookie di sessione cifrato e HttpOnly e non vede mai un access token, un refresh token o un ID token. Questo è il pattern backend-for-frontend (BFF). Il pacchetto usa solo la libreria standard. Non è ancora pubblicato.

Per proteggere invece un'API che riceve bearer token, consulta Proteggere la tua API.

Prima di iniziare

  1. Nella dashboard, crea un'applicazione web nel tuo ambiente.
  2. Scegli l'autenticazione del client: un client secret (bff.ClientSecretBasic) oppure private_key_jwt (bff.PrivateKeyJWT). Per private_key_jwt, registra la chiave pubblica dell'applicazione e conserva la chiave privata sul tuo server.
  3. Aggiungi il redirect URI https://app.acme.example/auth/callback e il post-logout redirect URI https://app.acme.example/.
  4. Genera un cookie secret di almeno 32 caratteri, ad esempio con openssl rand -base64 32. Conservalo nel tuo secret store, non nel repository.

Configurazione

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

Per usare private_key_jwt invece di un client secret, passa la chiave privata. Il pacchetto firma una nuova client assertion per ogni richiesta di token e di PAR (iss e sub sono il tuo client ID, aud è l'issuer, durata 2 minuti, un nuovo jti ogni volta):

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")},
})

Sono supportati ES256 (P-256), EdDSA (Ed25519), RS256 e PS256 (RSA, 2048 bit o più); l'algoritmo segue la chiave, e Algorithm: "PS256" seleziona PSS per una chiave RSA. Key può essere qualsiasi crypto.Signer, quindi funziona anche una chiave conservata in un KMS o HSM. Se firmi altrove, imposta invece bff.PrivateKeyJWT{Assertion: f}, dove f restituisce una nuova assertion firmata a ogni chiamata. Imposta uno solo tra Key e Assertion, non entrambi.

AppBaseURL è l'indirizzo pubblico della tua app. Il pacchetto costruisce da lì il redirect URI e ogni redirect, mai dagli header Host o X-Forwarded-* della richiesta. Il protocollo http è accettato solo per localhost.

Route

Middleware espone tre route e legge la sessione su ogni altra richiesta. Puoi anche montare le sole route con mux.Handle("/auth/", auth).

Route Cosa fa
GET /auth/login Avvia l'accesso. Parametri query facoltativi: returnTo (dove arriva l'utente) e organization (accedi a un'organizzazione).
GET /auth/callback Completa l'accesso e imposta il cookie di sessione.
GET o POST /auth/logout Disconnette in KeenAuth e riporta alla tua app.

L'accesso usa il flusso authorization code con PKCE (S256). La richiesta viene inviata al server (PAR) quando il tuo ambiente la supporta. Nel callback il pacchetto verifica state, iss e l'ID token, incluso il suo nonce. Ogni accesso riuscito crea una nuova sessione; quella precedente viene eliminata.

returnTo deve essere un percorso della tua app, come /orders?tab=open. Qualsiasi altro valore, come //evil.example o un URL completo, viene ignorato e l'utente arriva su /.

Proteggere gli handler

auth.RequireAuth(handler) lascia passare una richiesta solo con una sessione:

Per POST, PUT, PATCH, DELETE e gli altri metodi non sicuri, il browser deve indicare che la richiesta proviene dalla tua app: Sec-Fetch-Site: same-origin, oppure un header Origin uguale all'origine della tua app. Le altre richieste ricevono 403. Questo protegge dal cross-site request forgery in aggiunta a SameSite=Lax.

Usare la sessione e chiamare la tua API

bff.FromContext(r.Context()) restituisce:

Quando l'access token scade entro 60 secondi, il pacchetto lo rinnova prima che il tuo handler venga eseguito. Ogni rinnovo ruota il refresh token e imposta il nuovo cookie di sessione nella risposta. Se il server rifiuta il refresh token, la sessione termina. Un errore temporaneo del server lascia la sessione invariata. Le richieste concorrenti in un processo condividono un unico rinnovo.

Cambiare organizzazione

auth.SwitchOrganizationPath("org_01j9…", "/dashboard") restituisce un link a /auth/login che fa accedere di nuovo l'utente in quell'organizzazione. KeenAuth verifica l'appartenenza. Dopo l'accesso, Session.User.OrgID contiene l'organizzazione.

Disconnettersi

Invia una richiesta POST a /auth/logout da un form. Il pacchetto elimina il cookie di sessione (e la sessione memorizzata, se usi uno store), revoca il refresh token e manda il browser all'endpoint di logout del tuo ambiente, che lo riporta alla tua app. Una richiesta che il browser segnala come proveniente da un altro sito viene rifiutata. I browser che non inviano Sec-Fetch-Site possono disconnettersi solo con un POST dalle tue pagine.

Archiviazione della sessione

Per impostazione predefinita l'intera sessione è nel cookie, cifrata e autenticata con AES-256-GCM usando una chiave derivata dal secret. Un cookie modificato o copiato viene rifiutato. Il cookie si chiama __Host-keenauth_session.0 e viene impostato con Secure, HttpOnly, SameSite=Lax e Path=/. Su http://localhost il prefisso __Host- e Secure vengono omessi. Le sessioni grandi vengono divise in massimo tre cookie.

Per conservare i token sul server, imposta Options.Store su un bff.SessionStore (Get, Set, Delete), ad esempio basato su Redis. Il cookie contiene allora solo un ID di sessione casuale cifrato. Ogni accesso crea un nuovo ID ed elimina il record precedente. Lo store contiene token, quindi limitane l'accesso. bff.MemoryStore serve solo per sviluppo e test.

Una sessione dura al massimo 7 giorni (SessionMaxAge); il rinnovo non la prolunga. Per cambiare il secret, inserisci prima il nuovo: Secrets: []string{newSecret, oldSecret}. Rimuovi il vecchio dopo SessionMaxAge.

Limiti attuali