From c9da46ea981ae4d97e9573e17832fa402ed7ce07 Mon Sep 17 00:00:00 2001 From: William Petit Date: Sat, 24 Jun 2023 12:29:03 -0600 Subject: [PATCH] feat: add basic auth tutorial implementation as exemple --- .../director/layer/basicauth/basicauth.go | 112 ++++++++++++++++++ .../layer/basicauth/layer-options.json | 14 +++ .../director/layer/basicauth/layer_options.go | 8 ++ internal/setup/basicauth_layer.go | 15 +++ 4 files changed, 149 insertions(+) create mode 100644 internal/proxy/director/layer/basicauth/basicauth.go create mode 100644 internal/proxy/director/layer/basicauth/layer-options.json create mode 100644 internal/proxy/director/layer/basicauth/layer_options.go create mode 100644 internal/setup/basicauth_layer.go diff --git a/internal/proxy/director/layer/basicauth/basicauth.go b/internal/proxy/director/layer/basicauth/basicauth.go new file mode 100644 index 0000000..9ac3c52 --- /dev/null +++ b/internal/proxy/director/layer/basicauth/basicauth.go @@ -0,0 +1,112 @@ +package basicauth + +import ( + "crypto/sha256" + "crypto/subtle" + "net/http" + + proxy "forge.cadoles.com/Cadoles/go-proxy" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" + "forge.cadoles.com/cadoles/bouncer/internal/store" + "gitlab.com/wpetit/goweb/logger" +) + +const LayerType store.LayerType = "basicauth" + +type BasicAuth struct{} + +// LayerType implements director.MiddlewareLayer. +func (*BasicAuth) LayerType() store.LayerType { + return LayerType +} + +// Middleware implements director.MiddlewareLayer. +func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware { + // La méthode doit retourner un "Middleware" qui est un alias + // pour les fonctions généralement utilisées + // dans les librairies http en Go pour créer + // une fonction d'interception/transformation de requête. + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + // On récupère les identifiants "basic auth" transmis (ou non) + // avec la requête + username, password, ok := r.BasicAuth() + + // On créait une méthode locale pour gérer le cas d'une erreur d'authentification. + unauthorized := func() { + // On ajoute cette entête HTTP à la réponse pour déclencher l'affichage + // de la popup d'authentification dans le navigateur web de l'utilisateur. + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + + // On retoure un code d'erreur HTTP 401 (Unauthorized) + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } + + if !ok { + // L'entête Authorization est absente ou ne correspondant + // pas à du Basic Auth, on retourne une erreur HTTP 401 et + // on interrompt le traitement de la requête ici + unauthorized() + + return + } + + // On extrait les identifiants des options associées + // à notre layer + expectedUsername, usernameExists := layer.Options["username"].(string) + expectedPassword, passwordExists := layer.Options["password"].(string) + + // Si le nom d'utilisateur ou le mot de passe attendu n'existe pas + // alors on retourne une erreur HTTP 500 à l'utilisateur. + if !usernameExists || !passwordExists { + logger.Error(r.Context(), "basicauth layer missing password or username option") + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + // On vérifie les identifiants associés à la requête + isAuthenticated := authenticate(username, password, expectedUsername, expectedPassword) + + // Si les identifiants sont non reconnus alors + // on interrompt le traitement de la requête + if !isAuthenticated { + unauthorized() + + return + } + + // L'authentification a réussie ! On passe la main au handler HTTP suivant + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} + +func authenticate(username, password string, expectedUsername, expectedPassword string) bool { + // On génère une empreinte au format sha256 pour nos identifiants + usernameHash := sha256.Sum256([]byte(username)) + passwordHash := sha256.Sum256([]byte(password)) + + // On effectue de même avec les identifiants attendus. + // Pour l'instant, on utilise un couple d'identifiants en "dur". + expectedUsernameHash := sha256.Sum256([]byte(expectedUsername)) + expectedPasswordHash := sha256.Sum256([]byte(expectedPassword)) + + // On utilise la méthode subtle.ConstantTimeCompare() + // pour faire la comparaison des identifiants en temps constant + // et ainsi éviter les attaques par timing. + usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) + passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) + + // L'utilisateur est authentifié si son nom et son mot de passe + // correspondent avec ceux attendus. + return usernameMatch && passwordMatch +} + +func New() *BasicAuth { + return &BasicAuth{} +} + +var _ director.MiddlewareLayer = &BasicAuth{} diff --git a/internal/proxy/director/layer/basicauth/layer-options.json b/internal/proxy/director/layer/basicauth/layer-options.json new file mode 100644 index 0000000..b037cd0 --- /dev/null +++ b/internal/proxy/director/layer/basicauth/layer-options.json @@ -0,0 +1,14 @@ +{ + "$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options", + "title": "BasicAuth layer options", + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/internal/proxy/director/layer/basicauth/layer_options.go b/internal/proxy/director/layer/basicauth/layer_options.go new file mode 100644 index 0000000..7785ae7 --- /dev/null +++ b/internal/proxy/director/layer/basicauth/layer_options.go @@ -0,0 +1,8 @@ +package basicauth + +import ( + _ "embed" +) + +//go:embed layer-options.json +var RawLayerOptionsSchema []byte diff --git a/internal/setup/basicauth_layer.go b/internal/setup/basicauth_layer.go new file mode 100644 index 0000000..94eedca --- /dev/null +++ b/internal/setup/basicauth_layer.go @@ -0,0 +1,15 @@ +package setup + +import ( + "forge.cadoles.com/cadoles/bouncer/internal/config" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/basicauth" +) + +func init() { + RegisterLayer(basicauth.LayerType, setupBasicAuthLayer, basicauth.RawLayerOptionsSchema) +} + +func setupBasicAuthLayer(conf *config.Config) (director.Layer, error) { + return &basicauth.BasicAuth{}, nil +}