Pour suivre ce tutoriel, il faut au préalable connaître les handlers. Je vous conseille en premier lieu de lire l’article Ajoutez des fonctionnalités à votre serveur web HTTP grâce aux Handlers.

Introduction

Ce tutoriel à pour objectif de vous montrez de façon plus avancé, en utilisant un cas pratique, comment créer un Handler. Nous allons créer un handler d’authentification Basic HTTP, pour sécuriser l’accès à nos fichiers par un identifiant et un mot de passe.

Attention ! je tiens à prévenir sur le fait qu’une simple authentification HTTP Basic n’est pas la meilleur des sécurités. Il est recommandé d’utiliser ce type d’authentification avec une connexion chiffrée (HTTPS)

Si vous ne connaissez pas le protocol d’authentification Basic HTTP je vous recommande de vous documentez sur Wikipédia.

Nous utiliserons pour cet article, deux handlers natif du package net/http :

Mise en place

Pour commencer, qu’avons nous besoin comme type d’information ?

  • Un identifiant
  • Un mot de passe
  • Un message pour la demande d’authentification

Pour que notre code soit réutilisable, il est préférable de séparer la gestion de l’authentification du Handler lui même. Pour cela nous allons découper notre code en plusieurs parties.

La première partie consistera à coder la gestion de l’authentification et la deuxième à l’implémenter dans un Handler.

Une authentification simple se résume souvent par un identifiant et mot de passe. L’utilisateur entre les informations et l’application permet ou pas d’accéder à une zone privée.

Définissons cela en créant un nouveau type qui représentera une fonction :

package main

import "net/http"

type BasicAuthFunc func(username, password string) bool

Comme vous pouvez le voir le type BasicAuthFunc attend un identifiant/mot de passe et retourne un simple boolean.

Attachons maintenant à ce type, deux méthodes pour forcer la demande d’authentification grâce aux entêtes HTTP :

  1. RequireAuth – Requiert les informations de connexion
  2. Authenticate – Vérifie les informations de connexion
func (f BasicAuthFunc) RequireAuth(w http.ResponseWriter) {
    w.Header().Set("WWW-Authenticate", `Basic realm="Authorization Required"`)
    w.WriteHeader(401)
}

func (f BasicAuthFunc) Authenticate(r *http.Request) bool {
    username, password, ok := r.BasicAuth()
    return ok && f(username, password)
}

Note de version 1.4

Depuis la version 1.4, Go fourni nativement deux méthodes liées à l’authentification Basic HTTP. Elles sont disponibles via l’objet Request du package net/http :

  1. BasicAuth Permet de récupérer les informations de l’entête Authorization
  2. SetBasicAuth Permet d’initialiser une entête de type Authorization

Testons ce code et vérifions qu’il soit fonctionnel :

package main

import (
    "fmt"
    "net/http"
)

type BasicAuthFunc func(username, password string) bool

func (f BasicAuthFunc) RequireAuth(w http.ResponseWriter) {
    w.Header().Set("WWW-Authenticate", `Basic realm="Authorization Required"`)
    w.WriteHeader(401)
}

func (f BasicAuthFunc) Authenticate(r *http.Request) bool {
    username, password, ok := r.BasicAuth()
    return ok && f(username, password)
}

func home(w http.ResponseWriter, r *http.Request) {
    // Identifiant et mot de passe attendu
    username, password := "foo", "bar"

    // Création d'une fonction de test de type BasicAuthFunc
    f := BasicAuthFunc(func(user, pass string) bool {
        return username == user && password == pass
    })

    if !f.Authenticate(r) {
        f.RequireAuth(w)
    } else {
        fmt.Fprintf(w, "<h1>%s</h1>", "Welcome to the private zone")
    }
}

func main() {
    fmt.Println("Server is running...")

    http.Handle("/", http.HandlerFunc(home))
    http.ListenAndServe(":3000", nil)
}

Lancez la commande suivante dans vôtre terminal :

go run main.go

Avec une authentification Basic HTTP, la session durera jusqu’à la fermeture de vôtre navigateur. Pour faire des tests il est plus confortable de passer en navigation privé : http://localhost:3000

Ce code est fonctionnel mais pas très réutilisable si nous souhaitons l’appliquer à une autre page autre que l’accueil. L’idéal est d’encapsuler la portion de code suivant dans une fonction :

    // Identifiant et mot de passe attendu
    username, password := "foo", "bar"

    // Création d'une fonction de test de type BasicAuthFunc
    f := BasicAuthFunc(func(user, pass string) bool {
        return username == user && password == pass
    })

par

func SimpleBasicAuth(username, password string) BasicAuthFunc {
    return BasicAuthFunc(func(user, pass string) bool {
        return username == user && password == pass
    })
}

Observez le résultat après remplacement :

package main

import (
    "fmt"
    "net/http"
)

type BasicAuthFunc func(username, password string) bool

func (f BasicAuthFunc) RequireAuth(w http.ResponseWriter) {
    w.Header().Set("WWW-Authenticate", `Basic realm="Authorization Required"`)
    w.WriteHeader(401)
}

func (f BasicAuthFunc) Authenticate(r *http.Request) bool {
    username, password, ok := r.BasicAuth()
    return ok && f(username, password)
}

func SimpleBasicAuth(username, password string) BasicAuthFunc {
    return BasicAuthFunc(func(user, pass string) bool {
        return username == user && password == pass
    })
}

func home(w http.ResponseWriter, r *http.Request) {
    f := SimpleBasicAuth("foo", "bar")

    if !f.Authenticate(r) {
        f.RequireAuth(w)
    } else {
        fmt.Fprintf(w, "<h1>%s</h1>", "Welcome to the private zone")
    }
}

func main() {
    fmt.Println("Server is running...")

    http.Handle("/", http.HandlerFunc(home))
    http.ListenAndServe(":3000", nil)
}

Passons maintenant à la création de notre Handler (Middleware).

Notre Handler sera une struct qui aura comme champ une fonction de type BasicAuthFunc et une fonction de type Handler qui servira de page final si l’authentification s’est bien déroulé :

type BasicAuthHandler struct {
    Auth BasicAuthFunc
    Next http.Handler
}

// Permet de récupérer une instance de notre struct BasicAuthHandler
func NewBasicAuthHandler(auth BasicAuthFunc, next http.Handler) http.Handler {
    return &BasicAuthHandler{Auth: auth, Next: next}
}

Afin de satisfaire l’interface du type Handler, il faut implémenter la méthode ServeHTTP à la struct BasicAuthHandler :

func (h *BasicAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if !h.Auth.Authenticate(r) {
        h.Auth.RequireAuth(w)
    } else {
        h.Next.ServeHTTP(w, r)
    }
}

Modifions un peu le code :

package main

import (
    "fmt"
    "net/http"
)

type BasicAuthFunc func(username, password string) bool

func (f BasicAuthFunc) RequireAuth(w http.ResponseWriter) {
    w.Header().Set("WWW-Authenticate", `Basic realm="Authorization Required"`)
    w.WriteHeader(401)
}

func (f BasicAuthFunc) Authenticate(r *http.Request) bool {
    username, password, ok := r.BasicAuth()
    return ok && f(username, password)
}

func SimpleBasicAuth(username, password string) BasicAuthFunc {
    return BasicAuthFunc(func(user, pass string) bool {
        return username == user && password == pass
    })
}

type BasicAuthHandler struct {
    Auth BasicAuthFunc
    Next http.Handler
}

func NewBasicAuthHandler(auth BasicAuthFunc, next http.Handler) http.Handler {
    return &BasicAuthHandler{Auth: auth, Next: next}
}

func (h *BasicAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if !h.Auth.Authenticate(r) {
        h.Auth.RequireAuth(w)
    } else {
        h.Next.ServeHTTP(w, r)
    }
}

func home(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "<h1>Home Page</h1>")
}

func main() {
    fmt.Println("Server is running...")

    homeHandler := http.HandlerFunc(home)
    authHandler := NewBasicAuthHandler(SimpleBasicAuth("foo", "bar"), homeHandler)

    http.Handle("/", authHandler)
    http.ListenAndServe(":3000", nil)
}

Pour terminer, mettons en place un accès sécurisé à nos fichiers avec les handlers FileServer et StripPrefix. Le principe reste le même que dans l’exemple précédent (Exemple avec un répertoire nommé private) :

func main() {
    fmt.Println("Server is running...")

    auth := SimpleBasicAuth("foo", "bar")
    files := http.StripPrefix("/", http.FileServer(http.Dir("./private")))
    handler := NewBasicAuthHandler(auth, files)

    http.Handle("/", handler)
    http.ListenAndServe(":3000", nil)
}

Maintenant vous pouvez accéder à vos fichiers se trouvant dans le répertoire « private » depuis l’url http://localhost:3000/

Code complet :

package main

import (
    "fmt"
    "net/http"
)

type BasicAuthFunc func(username, password string) bool

func (f BasicAuthFunc) RequireAuth(w http.ResponseWriter) {
    w.Header().Set("WWW-Authenticate", `Basic realm="Authorization Required"`)
    w.WriteHeader(401)
}

func (f BasicAuthFunc) Authenticate(r *http.Request) bool {
    username, password, ok := r.BasicAuth()
    return ok && f(username, password)
}

func SimpleBasicAuth(username, password string) BasicAuthFunc {
    return BasicAuthFunc(func(user, pass string) bool {
        return username == user && password == pass
    })
}

type BasicAuthHandler struct {
    Auth BasicAuthFunc
    Next http.Handler
}

func NewBasicAuthHandler(auth BasicAuthFunc, next http.Handler) http.Handler {
    return &BasicAuthHandler{Auth: auth, Next: next}
}

func (h *BasicAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if !h.Auth.Authenticate(r) {
        h.Auth.RequireAuth(w)
    } else {
        h.Next.ServeHTTP(w, r)
    }
}

func main() {
    fmt.Println("Server is running...")

    auth := SimpleBasicAuth("foo", "bar")
    files := http.StripPrefix("/", http.FileServer(http.Dir("./private")))
    handler := NewBasicAuthHandler(auth, files)

    http.Handle("/", handler)
    http.ListenAndServe(":3000", nil)
}

Pour allez plus loin sur l’authentification basic avec Go vous pouvez utiliser et consulter le code source du middleware Bulma sur Github.

Il n'y a pas de commentaires.