go – Basic authentication with session ID


I’m quite new to web development. I want to implement basic session ID authentication. Here’s the implementation

package httphandler

import (
    "context"
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
    "time"

    "github.com/satori/go.uuid"
    log "github.com/sirupsen/logrus"
    "github.com/yura-under-review/auth-01-session-id/user"
)

const UserIdTag = "userId"

type (
    Handler struct {
        host           string
        port           int
        server         *http.Server
        userStorage    UserStorage
        sessionStorage SessionStorage
        contentPage    ()byte
        loginPage      ()byte
    }

    Config struct {
        Host string
        Port int
    }

    UserStorage interface {
        Close() error
        Create(*user.User, string) error
        Upsert(*user.User, string) error
        Get(string) (*user.User, error)
        Exists(string) (bool, error)
        CheckPassword(string, string) (bool, error)
    }

    SessionStorage interface {
        Set(userID string)
        Get(userID string) bool
    }

    Credentials struct {
        Login    string `json:"login"`
        Password string `json:"password"`
    }
)

func New(config Config, userStorage UserStorage, sessionStorage SessionStorage) (*Handler, error) {

    handler := Handler{
        host:           config.Host,
        port:           config.Port,
        userStorage:    userStorage,
        sessionStorage: sessionStorage,
    }

    mux := http.NewServeMux()
    mux.Handle("/", handler.Auth(http.HandlerFunc(handler.handleContent)))
    mux.HandleFunc("/login", handler.handleLogin)
    mux.HandleFunc("/auth", handler.handleAuth)

    handler.server = &http.Server{
        Addr:    fmt.Sprintf("%s:%d", config.Host, config.Port),
        Handler: mux,
    }

    if err := handler.initPages(); err != nil {
        return nil, err
    }

    return &handler, nil
}

func (h *Handler) initPages() error {
    contentPage, err := ioutil.ReadFile("html-pages/content.html")
    if err != nil {
        return fmt.Errorf("failed to read content page: %v", err)
    }

    h.contentPage = contentPage

    loginPage, err := ioutil.ReadFile("html-pages/login.html")
    if err != nil {
        return fmt.Errorf("failed to read content page: %v", err)
    }

    h.loginPage = loginPage

    return nil
}

func (h *Handler) Run(ctx context.Context, wg *sync.WaitGroup) {

    go func() {
        <-ctx.Done()

        ctxClose, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()

        _ = h.server.Shutdown(ctxClose)
    }()

    wg.Add(1)
    go func() {
        defer wg.Done()

        if err := h.server.ListenAndServe(); err != nil {
            log.Errorf("server exited with error: %vn", err)
        }
    }()
}

func (h *Handler) handleContent(w http.ResponseWriter, r *http.Request) {
    _, err := w.Write(h.contentPage)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
    }
}

func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
    _, err := w.Write(h.loginPage)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
    }
}

func (h *Handler) handleAuth(w http.ResponseWriter, r *http.Request) {

    err := r.ParseForm()
    if err != nil {
        return
    }

    login := r.Form.Get("login")
    password := r.Form.Get("password")

    ok, err := h.userStorage.CheckPassword(login, password)
    if err != nil {
        log.Errorf("failed to check password: %v", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    if !ok {
        http.Redirect(w, r, "/login", 301)
        return
    }

    newID := uuid.NewV4().String()

    h.sessionStorage.Set(newID)

    c := http.Cookie{
        Name:  UserIdTag,
        Value: newID,
        Path:  "/",
    }

    http.SetCookie(w, &c)

    http.RedirectHandler("/", 307).ServeHTTP(w, r)
    return
}

func (h *Handler) Auth(destHandler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        var userID string

        for _, cookie := range r.Cookies() {
            if cookie.Name == UserIdTag {
                userID = cookie.Value
                break
            }
        }

        if len(userID) != 0 {
            ok := h.sessionStorage.Get(userID)
            if ok {
                h.sessionStorage.Set(userID)
                destHandler.ServeHTTP(w, r)
                return
            }
        }

        http.RedirectHandler("/login", 307).ServeHTTP(w, r)
        return

    })
}

Here’s the project on github
https://github.com/yura-under-review/auth-01-session-id

I will appreciate for the comments. Could this approach be implemented in production? What must be improved (i.e. passwords shouldn’t be stored opened, service must be configurable etc.).

Any kind of ideas and critics is welcome.