Protéger une API Go
Ce guide de démarrage rapide protège une API Go net/http avec des jetons d'accès KeenAuth. Vous ajoutez le middleware protect du SDK Go, exigez une autorisation sur une route et testez l'API hors ligne avec le kit de test du SDK. Le middleware n'utilise que la bibliothèque standard et fonctionne avec tout routeur qui accepte un http.Handler.
L'API comporte trois points de terminaison :
| Point de terminaison | Exige |
|---|---|
GET /api/public |
aucun |
GET /api/private |
un jeton d'accès valide pour votre API |
GET /api/messages |
un jeton d'accès valide avec l'autorisation messages:read |
Avant de commencer
- Un environnement KeenAuth. Son émetteur ressemble à
https://your-environment.eu.keenauth.app. - Une API enregistrée dans l'environnement (Créer une API). Son
identifier, par exemplehttps://api.example.com, correspond à l'audience des jetons acceptés par votre API. - L'autorisation
messages:readsur cette API, accordée à l'application qui l'appelle. - Go 1.26 ou ultérieur.
1. Installer le SDK
go get github.com/keenauth/keenauth-go
Le SDK n'est pas encore publié. En attendant, l'exemple se compile avec les sources du SDK grâce à une directive replace dans go.mod.
2. Configurer
L'API lit sa configuration uniquement à partir de variables d'environnement :
| Variable | Obligatoire | Signification |
|---|---|---|
KEENAUTH_ISSUER |
oui | L'émetteur de votre environnement |
KEENAUTH_AUDIENCE |
oui | L'identifiant de votre API |
PUBLIC_BASE_URL |
oui | L'URL appelée par les clients (voir Derrière un proxy) |
KEENAUTH_WEBHOOK_SECRET |
recommandé | Le secret de signature idp_whsec_ du point de terminaison webhook qui livre token.revoked (deux valeurs séparées par des virgules pendant la rotation). Sans lui, l'API n'est jamais informée des jetons révoqués |
ADDR |
non | Adresse d'écoute, par défaut :8080 |
3. Protéger vos routes
NewHandler crée le middleware et les routes. m.Handler exige un jeton valide ; m.Require exige en plus des autorisations. Le gestionnaire lit les revendications vérifiées avec protect.ClaimsFrom.
// NewHandler builds the API. The verifier is a parameter so that tests can pass the offline test kit's verifier;
// main passes a real one. The revocation cache is fed from the token.revoked webhook.
func NewHandler(v protect.AccessTokenVerifier, publicBaseURL string, revocation protect.RevocationChecker) (http.Handler, error) {
m, err := protect.New(v, protect.Options{
PublicBaseURL: publicBaseURL,
Revocation: revocation,
OnError: func(r *http.Request, err error) {
// The error never contains the token or claim values; never log the Authorization or DPoP header.
log.Printf("rejected %s %s: %v", r.Method, r.URL.Path, err)
},
})
if err != nil {
return nil, err
}
mux := http.NewServeMux()
// No token needed.
mux.HandleFunc("GET /api/public", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, map[string]string{"message": "Hello from a public endpoint."})
})
// Any valid access token for this API.
mux.Handle("GET /api/private", m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, _ := protect.ClaimsFrom(r.Context())
writeJSON(w, map[string]string{"message": "Hello from a private endpoint.", "sub": claims.Subject})
})))
// A valid access token that also carries the messages:read scope.
mux.Handle("GET /api/messages", m.Require(protect.Requirement{Scopes: []string{"messages:read"}})(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, map[string]any{"messages": []string{"Hello from a scoped endpoint."}})
})))
return mux, nil
}
main crée le vérificateur réel à partir de la configuration. Le vérificateur récupère les clés de signature de votre environnement la première fois qu'il voit un jeton.
func main() {
cfg, err := ConfigFromEnv(os.Getenv)
if err != nil {
log.Fatal(err)
}
// verify.New refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
v, err := verify.New(verify.Config{Issuer: cfg.Issuer, Audience: cfg.Audience})
if err != nil {
log.Fatal(err)
}
revocation := protect.NewMemoryRevocationCache(0, nil)
api, err := NewHandler(v, cfg.PublicBaseURL, revocation)
if err != nil {
log.Fatal(err)
}
h := api
if len(cfg.WebhookSecrets) > 0 {
wv, err := webhook.NewVerifier(cfg.WebhookSecrets) // the error never contains the secret
if err != nil {
log.Fatalf("KEENAUTH_WEBHOOK_SECRET: %v", err)
}
h = WithWebhook(api, NewWebhookHandler(wv, revocation, nil))
} else {
log.Print("KEENAUTH_WEBHOOK_SECRET is not set: token.revoked events are not received, so revoked tokens stay valid until they expire")
}
srv := &http.Server{Addr: cfg.Addr, Handler: h, ReadHeaderTimeout: 10 * time.Second}
log.Printf("listening on %s", cfg.Addr)
log.Fatal(srv.ListenAndServe())
}
4. Exécuter et appeler l'API
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
go run .
Pour un premier appel, obtenez un jeton avec le flux d'identifiants client d'une application machine à machine qui dispose de l'autorisation messages:read sur votre API :
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials \
-d resource=https://api.example.com \
-d scope=messages:read \
https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)
curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
5. Tester sans réseau
Le paquet du kit de test keenauthtest émet des jetons signés avec une clé de test publique et crée un vérificateur qui n'accepte que ces jetons. Les tests transmettent ce vérificateur à NewHandler ; ils n'ont donc besoin ni de réseau ni de compte KeenAuth. Voir Kit de test hors ligne.
func newTestAPI(t *testing.T) (http.Handler, *protect.MemoryRevocationCache) {
t.Helper()
v, err := keenauthtest.NewVerifier(verify.Config{Audience: audience})
if err != nil {
t.Fatal(err)
}
cache := protect.NewMemoryRevocationCache(0, nil)
h, err := NewHandler(v, baseURL, cache)
if err != nil {
t.Fatal(err)
}
return h, cache
}
go test ./...
Les tests de l'exemple vérifient qu'un jeton valide est accepté et qu'un jeton absent, expiré ou destiné à une autre audience, une autorisation manquante, un jeton dans la requête de l'URL et un jeton révoqué sont rejetés avec les statuts ci-dessous. Ils envoient aussi un jeton lié à DPoP avec une preuve pour PUBLIC_BASE_URL. Les tests envoient également des livraisons token.revoked signées : une livraison valide révoque le jeton, une livraison falsifiée, altérée ou périmée reçoit 400 et ne révoque rien, et une livraison répétée n'a qu'un seul effet.
Réponses
| Requête | Statut | code dans le corps |
|---|---|---|
En-tête Authorization absent |
401 |
missing_token |
| Jeton expiré, jeton destiné à une autre API, signature invalide, jeton révoqué | 401 |
invalid_token |
| Jeton sans l'autorisation requise | 403 |
insufficient_scope |
Jeton dans la requête de l'URL (?access_token=) |
400 |
invalid_request |
Le corps est application/problem+json et l'en-tête WWW-Authenticate transporte le défi standard. Le tableau complet se trouve dans Protéger votre API avec un middleware.
Derrière un proxy
Si les clients utilisent DPoP, le middleware vérifie que chaque preuve a été créée pour l'URL appelée par le client. Définissez PUBLIC_BASE_URL sur l'URL externe appelée par les clients, par exemple https://api.example.com, y compris derrière un proxy ou un répartiteur de charge. Cette variable est obligatoire et doit utiliser https (http uniquement pour localhost, 127.0.0.1 ou [::1]) ; l'application refuse de démarrer sans elle. Le middleware ne fait jamais confiance aux en-têtes X-Forwarded-*.
Jetons révoqués
Un jeton d'accès reste valide jusqu'à son expiration ; KeenAuth informe donc votre API lorsque des jetons sont révoqués. L'exemple transmet au middleware un cache de révocation en mémoire et expose un webhook token.revoked à POST /webhooks/keenauth (main.go, webhook.go) qui l'alimente.
- Dans le tableau de bord, créez un point de terminaison webhook avec l'URL
https://<your host>/webhooks/keenauthqui s'abonne àtoken.revoked, puis exportez son secret de signature sous le nomKEENAUTH_WEBHOOK_SECRET. - Le gestionnaire vérifie la signature avec le SDK sur le corps brut et répond
400lorsqu'elle est invalide ou que l'horodatage s'écarte de plus de cinq minutes. Il associe le sélecteur de l'événement (access_token,session,user,client,org,org_user,client_user) à un marqueur de révocation et répond204; le middleware rejette alors tout jeton correspondant émis avantnot_before. - KeenAuth livre au moins une fois, donc le gestionnaire applique chaque
webhook-idune seule fois. Si le marqueur ne peut pas être enregistré, il répond503et KeenAuth livre à nouveau l'événement. Il ignore les autres types d'événements et les sélecteurs qu'aucun jeton d'accès ne peut correspondre (refresh_family,api).
Le gestionnaire journalise l'identifiant de l'événement, le type de sélecteur et la raison, jamais le corps, une signature ni le secret. Chaque processus possède son propre cache ; avec plusieurs instances, placez les marqueurs dans un stockage partagé derrière la même méthode Revoke. Voir Cache de révocation.
Prochaines étapes
- Protéger votre API avec un middleware : exigences d'élévation, DPoP et toutes les options.
- Vérifier les jetons : le vérificateur sous le middleware.
- Kit de test hors ligne.