Compare commits
1 Commits
v2023.6.24
...
basicauth-
Author | SHA1 | Date | |
---|---|---|---|
c9da46ea98 |
@ -10,10 +10,4 @@
|
|||||||
|
|
||||||
## Tutoriels
|
## Tutoriels
|
||||||
|
|
||||||
### Utilisation
|
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
|
||||||
|
|
||||||
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
|
|
||||||
|
|
||||||
### Développement
|
|
||||||
|
|
||||||
- [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.md)
|
|
@ -1,4 +1,4 @@
|
|||||||
# Ajouter un layer de type "file d'attente"
|
# Ajouter un calque de type "file d'attente"
|
||||||
|
|
||||||
## Étapes
|
## Étapes
|
||||||
|
|
||||||
@ -43,6 +43,6 @@
|
|||||||
|
|
||||||
3. Le proxy `cadoles` a désormais une file d'attente avec une capacité d'un seul utilisateur. Vous pouvez effectuer le test en ouvrant votre navigateur sur l'adresse `http://<ip_serveur>:8080/` puis en ouvrant une fenêtre de navigation privée sur la même adresse:
|
3. Le proxy `cadoles` a désormais une file d'attente avec une capacité d'un seul utilisateur. Vous pouvez effectuer le test en ouvrant votre navigateur sur l'adresse `http://<ip_serveur>:8080/` puis en ouvrant une fenêtre de navigation privée sur la même adresse:
|
||||||
- La première fenêtre devrait afficher le site Cadoles;
|
- La première fenêtre devrait afficher le site Cadoles;
|
||||||
- La seconde fenêtre devrait afficher une page indiquant qu'on est en file d'attente.
|
- La seconde fenêtre devrait afficher le message suivant: `queued (rank: 2, status: 2/1)`.
|
||||||
|
|
||||||
Si vous laissez expirer la "session" de la première fenêtre (environ 1 minute par défaut) et que vous rafraîchissez la seconde, vous devriez avoir une inversion des états.
|
Si vous laissez expirer la "session" de la première fenêtre (environ 1 minute par défaut) et que vous rafraîchissez la seconde, vous devriez avoir une inversion des états.
|
@ -1,499 +0,0 @@
|
|||||||
# Créer son propre layer
|
|
||||||
|
|
||||||
Dans ce tutoriel, nous allons voir comme implémenter un layer personnalisé qui permettra d'ajouter une authentification de type [`Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) à un proxy.
|
|
||||||
|
|
||||||
## Prérequis
|
|
||||||
|
|
||||||
Les éléments suivants doivent être installés sur votre machine:
|
|
||||||
|
|
||||||
- [Golang > 1.20](https://go.dev/)
|
|
||||||
- [Docker](https://www.docker.com/)
|
|
||||||
- [Git](https://git-scm.com/)
|
|
||||||
- [GNU Make](https://www.gnu.org/software/make/)
|
|
||||||
|
|
||||||
## Étapes
|
|
||||||
|
|
||||||
### Préparer son environnement de développement
|
|
||||||
|
|
||||||
1. Cloner le dépôt des sources du projet Bouncer
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://forge.cadoles.com/Cadoles/bouncer
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Se positionner dans le répertoire du projet
|
|
||||||
|
|
||||||
```
|
|
||||||
cd bouncer
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Lancer le projet en mode "développement"
|
|
||||||
|
|
||||||
```
|
|
||||||
make watch
|
|
||||||
```
|
|
||||||
|
|
||||||
Si toutes les dépendances sont correctement installées et configurées sur votre machine, la console devrait afficher une série de messages pour ensuite s'arrêter sur quelque chose ressemblant à:
|
|
||||||
|
|
||||||
```
|
|
||||||
14:47:06: daemon: make run BOUNCER_CMD="--config config.yml server admin run"
|
|
||||||
2023-06-23 20:47:06.095 [INFO] <./internal/command/server/admin/run.go:42> RunCommand.func1 listening {"url": "http://127.0.0.1:8081"}
|
|
||||||
2023-06-23 20:47:06.095 [INFO] <./internal/admin/server.go:126> (*Server).run http server listening
|
|
||||||
14:47:06: daemon: make run-redis
|
|
||||||
bouncer-redis
|
|
||||||
docker run --rm -t \
|
|
||||||
--name bouncer-redis \
|
|
||||||
-v /home/wpetit/workspace/bouncer/data/redis:/data \
|
|
||||||
-p 6379:6379 \
|
|
||||||
redis:alpine3.17 \
|
|
||||||
redis-server --save 60 1 --loglevel warning
|
|
||||||
1:C 23 Jun 2023 20:47:06.754 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
|
|
||||||
1:C 23 Jun 2023 20:47:06.754 # Redis version=7.0.11, bits=64, commit=00000000, modified=0, pid=1, just started
|
|
||||||
1:C 23 Jun 2023 20:47:06.754 # Configuration loaded
|
|
||||||
1:M 23 Jun 2023 20:47:06.759 # Warning: Could not create server TCP listening socket ::*:6379: unable to bind socket, errno: 97
|
|
||||||
1:M 23 Jun 2023 20:47:06.760 # Server initialized
|
|
||||||
1:M 23 Jun 2023 20:47:06.760 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect
|
|
||||||
```
|
|
||||||
|
|
||||||
À ce stade, le serveur `bouncer-admin` écoute sur http://127.0.0.1:8081 et le serveur `bouncer-proxy` sur http://127.0.0.1:8080.
|
|
||||||
|
|
||||||
L'outil [`modd`](https://github.com/cortesi/modd) est utilisé pour surveiller les modifications sur les sources et relancer automatiquement la compilation et les services en cas de changement.
|
|
||||||
|
|
||||||
### Préparer la structure de base du nouveau layer
|
|
||||||
|
|
||||||
Une implémetation d'un layer se compose majoritairement de 3 éléments:
|
|
||||||
|
|
||||||
- Une structure qui implémente une ou plusieurs interfaces (`director.MiddlewareLayer`, `director.RequestTransformerLayer` et/ou `director.ResponseTransformerLayer`);
|
|
||||||
- Un schéma au format [JSON Schema](http://json-schema.org/) qui permettra de valider les "options" de notre layer;
|
|
||||||
- Un fichier d'amorçage qui permettra à Bouncer de référencer notre nouveau layer.
|
|
||||||
|
|
||||||
1. Créer le répertoire du `package` Go qui contiendra le code de votre layer. Celui ci s'appelera `basicauth`:
|
|
||||||
|
|
||||||
```
|
|
||||||
mkdir -p internal/proxy/director/layer/basicauth
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Créer la structure de base du layer:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
|
||||||
|
|
||||||
package basicauth
|
|
||||||
|
|
||||||
import (
|
|
||||||
proxy "forge.cadoles.com/Cadoles/go-proxy"
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
// On définit le "type" (la chaîne de caractères) qui
|
|
||||||
// sera associé à notre layer
|
|
||||||
const LayerType store.LayerType = "basicauth"
|
|
||||||
|
|
||||||
// On déclare la structure qui
|
|
||||||
// servira de socle pour notre layer
|
|
||||||
type BasicAuth struct{}
|
|
||||||
|
|
||||||
// Les deux méthodes suivantes, attachées à
|
|
||||||
// notre structure BasicAuth, permettent de remplir
|
|
||||||
// le contrat définit par l'interface
|
|
||||||
// director.MiddlewareLayer
|
|
||||||
|
|
||||||
// LayerType implements director.MiddlewareLayer.
|
|
||||||
func (*BasicAuth) LayerType() store.LayerType {
|
|
||||||
return LayerType
|
|
||||||
}
|
|
||||||
|
|
||||||
// C'est dans la méthode "Middleware" qu'on pourra implémenter la
|
|
||||||
// logique appliquée par notre layer.
|
|
||||||
// En l'état actuel l'exécution de la méthode provoquera un panic().
|
|
||||||
|
|
||||||
// Middleware implements director.MiddlewareLayer.
|
|
||||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *BasicAuth {
|
|
||||||
return &BasicAuth{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cette déclaration permet de profiter
|
|
||||||
// des capacités du compilateur pour s'assurer
|
|
||||||
// que la structure BasicAuth remplie toujours le
|
|
||||||
// contrat imposé par l'interface director.MiddlewareLayer
|
|
||||||
var _ director.MiddlewareLayer = &BasicAuth{}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Créer le schéma JSON qui sera associé aux options possibles pour notre layer:
|
|
||||||
|
|
||||||
```json
|
|
||||||
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
|
|
||||||
|
|
||||||
{
|
|
||||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
|
|
||||||
"title": "BasicAuth layer options",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Puis créer le fichier Go qui embarquera ces données dans notre binaire via le package [`embed`](https://pkg.go.dev/embed):
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Fichier internal/proxy/director/layer/basicauth/layer_options.go
|
|
||||||
|
|
||||||
package basicauth
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed layer-options.json
|
|
||||||
var RawLayerOptionsSchema []byte
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Enfin, créer le fichier d'amorçage pour référencer notre nouveau layer avec Bouncer:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Fichier internal/setup/basicauth_layer.go
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// On créait une function d'initialisation qui enregistra les éléments suivants auprès de Bouncer:
|
|
||||||
// - Notre nouveau type de layer
|
|
||||||
// - Une fonction capable de créer une instance de notre layer
|
|
||||||
// - Le schéma associé aux options de notre layer
|
|
||||||
func init() {
|
|
||||||
RegisterLayer(basicauth.LayerType, setupBasicAuthLayer, basicauth.RawLayerOptionsSchema)
|
|
||||||
}
|
|
||||||
|
|
||||||
// La fonction de création de notre layer
|
|
||||||
// reçoit automatiquement la configuration actuelle de Bouncer.
|
|
||||||
|
|
||||||
// Une layer plus avancé pourrait être configurable de cette manière
|
|
||||||
// en créant une nouvelle section de configuration dédiée.
|
|
||||||
func setupBasicAuthLayer(conf *config.Config) (director.Layer, error) {
|
|
||||||
return &basicauth.BasicAuth{}, nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tester l'intégration de notre nouveau layer
|
|
||||||
|
|
||||||
À ce stade, notre nouveau layer est normalement référencé et donc "utilisable" dans Bouncer (si on omet le fait qu'il déclenchera une `panic()`).
|
|
||||||
|
|
||||||
1. Vérifier que notre layer est bien référencé en exécutant la commande:
|
|
||||||
|
|
||||||
```
|
|
||||||
./bin/bouncer admin layer create --help
|
|
||||||
```
|
|
||||||
|
|
||||||
La sortie devrait ressembler à:
|
|
||||||
|
|
||||||
```
|
|
||||||
NAME:
|
|
||||||
bouncer admin layer create - Create layer
|
|
||||||
|
|
||||||
USAGE:
|
|
||||||
bouncer admin layer create [command options] [arguments...]
|
|
||||||
|
|
||||||
OPTIONS:
|
|
||||||
--layer-type LAYER_TYPE Set LAYER_TYPE as layer's type (available: [basicauth queue])
|
|
||||||
[...]
|
|
||||||
```
|
|
||||||
|
|
||||||
Comme vous devriez le voir nous pouvons désormais créer des layers de type `basicauth`.
|
|
||||||
|
|
||||||
2. Créer un proxy puis une instance de notre nouveau layer associée à celui ci:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Créer un proxy
|
|
||||||
./bin/bouncer admin proxy create --proxy-name cadoles --proxy-to https://www.cadoles.com
|
|
||||||
|
|
||||||
# Activer celui-ci
|
|
||||||
./bin/bouncer admin proxy update --proxy-name cadoles --proxy-enabled=true
|
|
||||||
|
|
||||||
# Ajouter un layer de notre nouveau type à notre proxy
|
|
||||||
./bin/bouncer admin layer create --proxy-name cadoles --layer-name mybasicauth --layer-type basicauth
|
|
||||||
|
|
||||||
# Activer notre nouveau layer
|
|
||||||
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-enabled=true
|
|
||||||
```
|
|
||||||
|
|
||||||
**Notre layer est actif** ! Cependant il est loin d'être fonctionnel. En effet, si vous faites un:
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -v http://localhost:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
Vous n'aurez guère en retour qu'un:
|
|
||||||
|
|
||||||
```
|
|
||||||
* Trying 127.0.0.1:8080...
|
|
||||||
* Connected to localhost (127.0.0.1) port 8080 (#0)
|
|
||||||
> GET / HTTP/1.1
|
|
||||||
> Host: localhost:8080
|
|
||||||
> User-Agent: curl/8.1.2
|
|
||||||
> Accept: */*
|
|
||||||
>
|
|
||||||
* Empty reply from server
|
|
||||||
* Closing connection 0
|
|
||||||
curl: (52) Empty reply from server
|
|
||||||
```
|
|
||||||
|
|
||||||
Et également dans la console où s'exécute le service `bouncer-proxy`, vous aurez le message:
|
|
||||||
|
|
||||||
```
|
|
||||||
2023/06/23 18:39:59 http: panic serving 127.0.0.1:59868: unimplemented
|
|
||||||
```
|
|
||||||
|
|
||||||
**Il est temps d'implémenter réellement la logique associée à notre layer !**
|
|
||||||
|
|
||||||
> **Note** Vous pouvez désactiver votre layer via le drapeau `--layer-enabled=false` et voir le site Cadoles s'afficher à nouveau !
|
|
||||||
|
|
||||||
## Implémenter l'authentification sur notre nouveau layer
|
|
||||||
|
|
||||||
Nous allons modifier la méthode `Middleware(layer *store.Layer) proxy.Middleware` attachée à notre structure `BasicAuth`.
|
|
||||||
|
|
||||||
1. Modifier le fichier contenant la structure de notre layer de la manière suivante:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
|
||||||
|
|
||||||
// [...]
|
|
||||||
|
|
||||||
// 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 vérifie les identifiants associés à la requête
|
|
||||||
isAuthenticated := authenticate(username, password)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// La méthode authenticate() prend un couple d'identifiants
|
|
||||||
// est vérifie en temps constant si ceux ci correspondent à un couple
|
|
||||||
// d'identifiants attendus.
|
|
||||||
func authenticate(username, password 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("foo"))
|
|
||||||
expectedPasswordHash := sha256.Sum256([]byte("baz"))
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Dans votre navigateur, essayez d'ouvrir l'URL http://127.0.0.1:8080. La popup d'authentification devrait s'afficher et vous devriez pouvoir utiliser les identifiants définis dans la fonction `authenticate()` pour vous authentifier et accéder au site de Cadoles !
|
|
||||||
|
|
||||||
> **Note** Essayez de désactiver le layer. L'authentification est automatiquement désactivée également !
|
|
||||||
|
|
||||||
## Déclarer des options pour pouvoir utiliser des identifiants dynamiques
|
|
||||||
|
|
||||||
En l'état actuel notre layer est fonctionnel. Cependant il souffre d'un problème notable: les identifiants attendus sont statiques et embarqués en dur dans le code. Nous allons utiliser le schéma associé à nos options, jusqu'alors vide, pour pouvoir créer une paire d'identifiants attendus dynamique.
|
|
||||||
|
|
||||||
|
|
||||||
1. Modifier le schéma JSON des options de notre layer:
|
|
||||||
|
|
||||||
```json
|
|
||||||
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
|
|
||||||
|
|
||||||
{
|
|
||||||
"$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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. On modifie notre méthode `Middleware()` et la fonction `authenticate()` pour utiliser ces nouvelles options:
|
|
||||||
|
|
||||||
```go
|
|
||||||
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.
|
|
||||||
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{}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Modifiez votre layer via la commande d'administration pour déclarer une paire d'identifiants:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-options='{"username":"jdoe","password":"notsosecret"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Essayer d'accéder à l'adresse http://127.0.0.1:8080 avec votre navigateur. La popup d'authentification devrait s'afficher et vous devriez pouvoir vous authentifier avec le nouveau couple d'identifiants définis dans les options de votre layer !
|
|
||||||
|
|
||||||
> **Note** Vous pouvez modifier les identifiants plusieurs fois via la commande et vérifier que la fenêtre s'affiche toujours à nouveau, demandant les nouveaux identifiants.
|
|
@ -19,7 +19,7 @@ func CreateCommand() *cli.Command {
|
|||||||
Name: "create",
|
Name: "create",
|
||||||
Usage: "Create proxy",
|
Usage: "Create proxy",
|
||||||
Flags: proxyFlag.WithProxyFlags(
|
Flags: proxyFlag.WithProxyFlags(
|
||||||
flag.ProxyTo(true),
|
flag.ProxyTo(),
|
||||||
flag.ProxyFrom(),
|
flag.ProxyFrom(),
|
||||||
),
|
),
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
|
@ -30,12 +30,12 @@ func ProxyName() cli.Flag {
|
|||||||
|
|
||||||
const KeyProxyTo = "proxy-to"
|
const KeyProxyTo = "proxy-to"
|
||||||
|
|
||||||
func ProxyTo(required bool) cli.Flag {
|
func ProxyTo() cli.Flag {
|
||||||
return &cli.StringFlag{
|
return &cli.StringFlag{
|
||||||
Name: KeyProxyTo,
|
Name: KeyProxyTo,
|
||||||
Usage: "Set `PROXY_TO` as proxy's destination url",
|
Usage: "Set `PROXY_TO` as proxy's destination url",
|
||||||
Value: "",
|
Value: "",
|
||||||
Required: required,
|
Required: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ func UpdateCommand() *cli.Command {
|
|||||||
Name: "update",
|
Name: "update",
|
||||||
Usage: "Update proxy",
|
Usage: "Update proxy",
|
||||||
Flags: proxyFlag.WithProxyFlags(
|
Flags: proxyFlag.WithProxyFlags(
|
||||||
flag.ProxyTo(false),
|
flag.ProxyTo(),
|
||||||
flag.ProxyFrom(),
|
flag.ProxyFrom(),
|
||||||
flag.ProxyEnabled(),
|
flag.ProxyEnabled(),
|
||||||
flag.ProxyWeight(),
|
flag.ProxyWeight(),
|
||||||
|
112
internal/proxy/director/layer/basicauth/basicauth.go
Normal file
112
internal/proxy/director/layer/basicauth/basicauth.go
Normal file
@ -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{}
|
14
internal/proxy/director/layer/basicauth/layer-options.json
Normal file
14
internal/proxy/director/layer/basicauth/layer-options.json
Normal file
@ -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
|
||||||
|
}
|
8
internal/proxy/director/layer/basicauth/layer_options.go
Normal file
8
internal/proxy/director/layer/basicauth/layer_options.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package basicauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed layer-options.json
|
||||||
|
var RawLayerOptionsSchema []byte
|
15
internal/setup/basicauth_layer.go
Normal file
15
internal/setup/basicauth_layer.go
Normal file
@ -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
|
||||||
|
}
|
Reference in New Issue
Block a user