Mardi 1er octobre 2019

Utilisation du module de demande d’authentification NGINX auth_request

Le client dispose d’une application Web existante hébergée dans un centre de données dédié ainsi que de toute l’infrastructure matérielle, y compris Citrix NetScaler , une appliance d’équilibrage de la charge et de proxy inverse dotée de quelques fonctionnalités supplémentaires.
Une telle fonctionnalité est une passerelle d’authentification, c’est-à-dire que NetScaler autorise uniquement l’accès aux applications principales pour les utilisateurs authentifiés. L’application Web du client n’est toutefois qu’une des nombreuses applications qui, ensemble, forment un système complexe. Toutes les applications sont hébergées dans le même centre de données et partagent les mêmes utilisateurs du domaine. En d’autres termes, l’utilisateur du domaine peut accéder à chaque application avec une seule paire nom d’utilisateur / mot de passe. Dans chaque application, chaque utilisateur de domaine est mappé à un utilisateur d’application.
La situation est illustrée schématiquement dans la figure suivante.

Schéma d’authentification NetScaler

Le processus d’authentification utilisateur simplifié comprend les étapes suivantes:

  1. HTTP GET https://protected-resource.example.redbyte.eu
  2. NetScaler détecte que l’utilisateur n’est pas authentifié et redirige (HTTP 302) vers la page de connexion
  3. POST HTTP demande de connexion à la page
  4. Authentification d’utilisateur sur Active Directory
  5. Rediriger (HTTP 302) vers la destination d’origine (https://protected-resource.example.redbyte.eu)
  6. HTTP GET https://protected-resource.example.redbyte.eu
  7. Proxy vers un serveur principal. Le serveur d’arrière-plan lit le nom d’utilisateur du domaine à partir de l’en-tête HTTP et identifie l’utilisateur de l’application correspondante.

Le problème avec une telle configuration est sa testabilité. Le client dispose de plusieurs environnements de transfert et introduire NetScaler dans ces environnements serait excessif (sans compter la gestion de domaine pour tous les environnements). La demande du client était de “contourner” en quelque sorte NetScaler et toute la complexité de la configuration et de la gestion des utilisateurs sans modifier le code ou la configuration de l’application. Il devait ressembler et se comporter comme si NetScaler était là.

Utiliser ngx_http_auth_request_module

Une solution consiste à utiliser le serveur HTTP NGINX avec le module ngx_http_auth_request_module
La documentation de ce module indique qu’elle implémente l’autorisation du client en fonction du résultat d’une sous-demande.
Qu’est-ce que cela veut dire exactement?
Le principe est assez simple, lorsque vous envoyez une requête HTTP à une URL protégée, NGINX effectue une sous-demande interne sur une URL d’autorisation définie.

  • Si le résultat de la sous-demande est HTTP 2xx, NGINX envoie la requête HTTP d’origine au serveur dorsal.
  • Si le résultat de la sous-demande est HTTP 401 ou 403, l’accès au serveur principal est refusé.

En configurant NGINX, vous pouvez rediriger ces 401 ou 403 vers une page de connexion où l’utilisateur est authentifié puis redirigé vers la destination d’origine. L’ensemble du processus de sous-demande d’autorisation est ensuite répété, mais comme l’utilisateur est maintenant authentifié, la sous-demande renvoie HTTP 200 et la requête HTTP d’origine est envoyée par proxy au serveur principal.

Naturellement, NGINX fournit seulement un mécanisme pour y parvenir - le serveur d’autorisation doit être une construction personnalisée pour un cas d’utilisation spécifique. Dans notre cas, FakeNetscaler est le serveur d’autorisation - j’y reviendrai plus tard.
Voyons maintenant comment fonctionne ngx_http_auth_request_module:

Système d’authentification utilisant NGINX et ngx_http_auth_request_module

  1. HTTP GET https://protected-resource.example.redbyte.eu
  2. NGINX envoie une demande d’autorisation à FakeNetScaler
  3. L’utilisateur n’est pas encore authentifié, donc FakeNetScaler renvoie le code HTTP 401
  4. NGINX redirige le navigateur (HTTP 302) vers la page de connexion
  5. L’utilisateur entre les informations de connexion et soumet le formulaire de connexion.
  6. Les identifiants de connexion sont valides, FakeNetScaler renvoie un cookie contenant «l’utilisateur dont l’identifiant est XXX est authentifié» et redirige le navigateur (HTTP 302) vers la destination d’origine.
  7. HTTP GET la destination d’origine
  8. NGINX envoie une demande d’autorisation à FakeNetScaler
  9. FakeNetscaler lit le contenu du cookie et s’aperçoit que l’utilisateur est authentifié. Renvoie donc HTTP 200 comme résultat de la sous-demande.
  10. NGINX envoie la requête à un serveur principal, ainsi qu’un en-tête HTTP avec le nom d’utilisateur du domaine. Le serveur backend lit l’en-tête HTTP du nom d’utilisateur du domaine et identifie l’utilisateur de l’application correspondante.

À première vue, cela semble être encore plus complexe que le processus d’authentification NetScaler original, mais le fait est que je viens de le décrire en utilisant la méthode de la boîte blanche, où NetScaler était décrit comme une boîte noire (en particulier les points 3. , 4. et 5.).

Il devrait maintenant être clair comment fonctionne le ngx_http_auth_request_module
Regardons le fichier de configuration NGINX du domaine protected-resource.example.redbyte.eu :

 1server {
 2  listen   443 ssl;
 3  server_name protected-resource.example.redbyte.eu;
 4
 5  # ssl and server configuration left out
 6
 7  location / {
 8    auth_request /auth;
 9    error_page 401 = @error401;
10
11    auth_request_set $user $upstream_http_x_forwarded_user;
12    proxy_set_header X-Forwarded-User $user;
13    proxy_pass http://protected-resource:8080;
14  }
15
16  location /auth {
17    internal;
18    proxy_set_header Host $host;
19    proxy_pass_request_body off;
20    proxy_set_header Content-Length "";
21    proxy_pass http://fakenetscaler:8888;
22  }
23
24  location @error401 {
25    add_header Set-Cookie "NSREDIRECT=$scheme://$http_host$request_uri;Domain=.example.redbyte.eu;Path=/";
26    return 302 https://fakenetscaler.example.redbyte.eu;
27  }
28
29}

Les lignes les plus importantes sont:

  • 8 - nous disons ici que pour toutes les URL commençant par / NGINX, la sous-demande d’autorisation sera exécutée sur l’URL /auth
  • 9 - que le code HTTP 401 sera redirigé vers la page de connexion
  • 11 - Ici, nous définissons $ user variable sur la valeur envoyée par le serveur d’autorisations via l’en-tête HTTP X-Forwarded-User
  • 12 - L’en-tête HTTP X-Forwarded-User est défini par NGINX sur la valeur de la variable $ user
  • 16 - nous définissons ici la sous-demande d’autorisation. La sous-requête est transmise par proxy à http://fakenetscaler:8888 , qui est un hôte du réseau interne.
  • 25 - Ici, nous définissons un cookie avec l’URL de destination d’origine
  • 26 - une redirection HTTP 302 vers la page de connexion servie par le serveur d’autorisation. Dans ce cas, nous devons utiliser un nom de domaine complet car le navigateur n’est pas en mesure de résoudre les noms d’hôte internes.

Fichier de configuration fakenetscaler.example.redbyte.eu domaine du serveur d’autorisation fakenetscaler.example.redbyte.eu :

1server {
 2  listen   443 ssl;
 3  server_name fakenetscaler.example.redbyte.eu;
 4
 5  # ssl and server configuration left out
 6
 7  location / {
 8        proxy_set_header Host $http_host;
 9        proxy_pass http://fakenetscaler:8888;
10  }
11}

Comme vous pouvez le constater, il s’agit d’un proxy inverse envoyé à un serveur principal exécutant le serveur d’autorisation HTTP d’ http://fakenetscaler:8888 .

FakeNetScaler - serveur d’autorisation

Jusqu’ici, nous n’avons joué qu’avec la configuration du serveur NGINX.
Regardons le serveur d’autorisation FakeNetscaler. Comme je l’ai mentionné précédemment, NGINX ne fournit qu’un cadre d’autorisation, le serveur d’autorisation doit être personnalisé et adapté aux exigences du client:

  • doit pouvoir répondre à la demande HTTP GET /auth et décider si l’utilisateur est authentifié ou non sur un cookie. Si c’est le cas, il répondra avec HTTP 200 s’il ne le fait pas, HTTP 401
  • HTTP GET / affiche la page de connexion
  • HTTP POST / soumettre le formulaire de connexion. Si un utilisateur a saisi le nom d’utilisateur et le mot de passe corrects, le cookie établit son authentification et le redirige vers la destination d’origine en fonction des informations stockées dans le cookie. Si l’utilisateur n’a pas saisi les informations de connexion correctes, la page de connexion avec la description de l’erreur s’affiche à nouveau.
  • doit pouvoir répondre à la demande HTTP GET /auth et, en fonction de la valeur du cookie, décider si l’utilisateur est connecté ou non. Si l’utilisateur est connecté, le code de réponse HTTP est 200, sinon 401.
  • HTTP GET to / URL affiche la page de connexion.
  • HTTP POST to / URL soumet le formulaire de connexion. Si l’utilisateur a entré un nom d’utilisateur et un mot de passe valides, un cookie de connexion est créé et le navigateur est redirigé vers la destination d’origine. Si l’utilisateur n’a pas saisi de nom d’utilisateur ou de mot de passe valide, la page de connexion avec le message d’erreur s’affiche.

Cela devrait être un service très simple et nous allons l’implémenter en utilisant le langage de programmation Go . Go possède une bibliothèque standard riche comprenant un serveur HTTP très performant. Un serveur tiers n’est pas nécessaire (par exemple, comme dans la plupart des déploiements Java).

Jugez par vous-même, ceci est un code source complet du serveur FakeNetScaler:

1package main
  2
  3import (
  4	"flag"
  5	"fmt"
  6	"github.com/BurntSushi/toml"
  7	"github.com/codegangsta/negroni"
  8	"github.com/gorilla/securecookie"
  9	"github.com/julienschmidt/httprouter"
 10	"gopkg.in/unrolled/render.v1"
 11	"log"
 12	"net/http"
 13	"time"
 14)
 15
 16var (
 17	nsCookieName         = "NSLOGIN"
 18	nsCookieHashKey      = []byte("SECURE_COOKIE_HASH_KEY")
 19	nsRedirectCookieName = "NSREDIRECT"
 20	cfg                  config
 21)
 22
 23type config struct {
 24	// e.g. https://protected-resource.example.redbyte.eu
 25	DefaultRedirectUrl string
 26	// shared password
 27	Password string
 28	// shared domain prefix between protected resource and auth server
 29	// e.g. .example.redbyte.eu (note the leading dot)
 30	Domain string
 31}
 32
 33func main() {
 34
 35	// configuration
 36	port := flag.Int("port", 8888, "listen port")
 37	flag.Parse()
 38	var err error
 39	if cfg, err = loadConfig("config.toml"); err != nil {
 40		log.Fatal(err)
 41	}
 42
 43	// template renderer
 44	rndr := render.New(render.Options{
 45		Directory:     "templates",
 46		IsDevelopment: false,
 47	})
 48
 49	// router
 50	router := httprouter.New()
 51	router.GET("/", indexHandler(rndr))
 52	router.POST("/", loginHandler(rndr))
 53	router.GET("/auth", authHandler)
 54
 55	// middleware and static content file server
 56	n := negroni.New(negroni.NewRecovery(), negroni.NewLogger(),
 57		&negroni.Static{
 58			Dir:    http.Dir("public"),
 59			Prefix: ""})
 60	n.UseHandler(router)
 61
 62	n.Run(fmt.Sprintf(":%d", *port))
 63}
 64
 65func authHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 66	var s = securecookie.New(nsCookieHashKey, nil)
 67	// get the cookie from the request
 68	if cookie, err := r.Cookie(nsCookieName); err == nil {
 69		value := make(map[string]string)
 70		// try to decode it
 71		if err = s.Decode(nsCookieName, cookie.Value, &value); err == nil {
 72			// if if succeeds set X-Forwarded-User header and return HTTP 200 status code
 73			w.Header().Add("X-Forwarded-User", value["user"])
 74			w.WriteHeader(http.StatusOK)
 75			return
 76		}
 77	}
 78
 79	// otherwise return HTTP 401 status code
 80	http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
 81}
 82
 83func indexHandler(render *render.Render) httprouter.Handle {
 84	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
 85		// just render the login page
 86		render.HTML(w, http.StatusOK, "index", nil)
 87	}
 88}
 89
 90func loginHandler(render *render.Render) httprouter.Handle {
 91	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
 92		login := r.PostFormValue("login")
 93		passwd := r.PostFormValue("passwd")
 94
 95		var errorMessage = false
 96
 97		// nothing fancy here, it is just a demo so every user has the same password
 98		// and if it doesn't match render the login page and present user with error message
 99		if login == "" || passwd != cfg.Password {
100			errorMessage = true
101			render.HTML(w, http.StatusOK, "index", errorMessage)
102		} else {
103			var s = securecookie.New(nsCookieHashKey, nil)
104			value := map[string]string{
105				"user": login,
106			}
107
108			// encode username to secure cookie
109			if encoded, err := s.Encode(nsCookieName, value); err == nil {
110				cookie := &http.Cookie{
111					Name:    nsCookieName,
112					Value:   encoded,
113					Domain:  cfg.Domain,
114					Expires: time.Now().AddDate(1, 0, 0),
115					Path:    "/",
116				}
117				http.SetCookie(w, cookie)
118			}
119
120			// after successful login redirect to original destination (if it exists)
121			var redirectUrl = cfg.DefaultRedirectUrl
122			if cookie, err := r.Cookie(nsRedirectCookieName); err == nil {
123				redirectUrl = cookie.Value
124			}
125			// ... and delete the original destination holder cookie
126			http.SetCookie(w, &http.Cookie{
127				Name:    nsRedirectCookieName,
128				Value:   "deleted",
129				Domain:  cfg.Domain,
130				Expires: time.Now().Add(time.Hour * -24),
131				Path:    "/",
132			})
133
134			http.Redirect(w, r, redirectUrl, http.StatusFound)
135		}
136
137	}
138}
139
140// loads the config file from filename
141// Example config file content:
142/*
143defaultRedirectUrl = "https://protected-resource.example.redbyte.eu"
144password = "shared_password"
145domain = ".example.redbyte.eu"
146*/
147func loadConfig(filename string) (config, error) {
148	var cfg config
149	if _, err := toml.DecodeFile(filename, &cfg); err != nil {
150		return config{}, err
151	}
152	return cfg, nil
153}

Après avoir compilé le code Go, un binaire lié de manière statique, sans aucune autre dépendance d’exécution, est créé. Lorsque vous l’exécutez, vous obtenez un serveur HTTP à l’écoute sur le port 8888.

Conclusion

Dans ce blog, nous avons montré comment utiliser NGINX et son ngx_http_auth_request_module , qui fournit un cadre de base pour la création d’une autorisation client personnalisée à l’aide de principes simples.
En utilisant le langage de programmation Go, nous avons implémenté notre propre serveur d’autorisation, que nous utilisions avec NGINX.