Skip to content
Snippets Groups Projects
auth.go 2.99 KiB
Newer Older
package auth

import (
	"context"
	"errors"
	"net/http"
	"strings"
gbe's avatar
gbe committed

gbe's avatar
gbe committed
	"github.com/go-kit/kit/log/level"

gbe's avatar
gbe committed
	"git.c3pb.de/gbe/invinoveritas/log"
)

type contextKey string

var ErrAuthFailed = errors.New("authentication failed")

// User is an application user.
type User struct {
	Name     string
	IsAdmin  bool
	CanWrite bool // Set for non-anonymous users
gbe's avatar
gbe committed
func (u User) String() string {
	readOnly := " (ro)"
	if u.CanWrite {
		readOnly = ""
	}

	return "{" + u.Name + readOnly + "}"
gbe's avatar
gbe committed
}

type Provider interface {
	// Valid checks whether the given token represents a valid user session. If that is the case, the user
	// is returned. Otherwise, an error is returned.
	Valid(ctx context.Context, token string) (*User, error)
}

// Require wraps hdlr so that it requires authentication to use. Requests handled by hdlr will have the user
// name attached to their context. Use the User function to retrieve it.
//
// If authentication fails, for example because the users' session is no longer valid or doesn't exist, the
// request will be handled by the authFailed handler. It will receive the original request and response writer.
// This is triggered by provider returning an error wrapping ErrAuthFailed from its `Valid` method.
func Require(next http.Handler, authFailed http.Handler, provider Provider) http.Handler {
	key := contextKey("user")

gbe's avatar
gbe committed
	anonAuth := func(w http.ResponseWriter, r *http.Request) {
		// No session: allow GET and friends, deny POST by requesting authentication
		if r.Method == "POST" || strings.HasPrefix(r.URL.Path, "/user/") {
			level.Error(log.Get(r)).
				Log("msg", "denying POST access for unknown user")
gbe's avatar
gbe committed
			authFailed.ServeHTTP(w, r)
gbe's avatar
gbe committed
			return
		}
gbe's avatar
gbe committed
		level.Info(log.Get(r)).
			Log("msg", "no session, using anon auth")
gbe's avatar
gbe committed

gbe's avatar
gbe committed
		user := &User{
			Name: "anon",
		}

		ctx := context.WithValue(r.Context(), key, *user)
gbe's avatar
gbe committed
		next.ServeHTTP(w, r.WithContext(ctx))
	}
gbe's avatar
gbe committed
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gbe's avatar
gbe committed
		var token string

gbe's avatar
gbe committed
		cookie, err := r.Cookie("session")
gbe's avatar
gbe committed
		if err == nil {
			token = cookie.Value
gbe's avatar
gbe committed

gbe's avatar
gbe committed
		user, err := provider.Valid(r.Context(), token)
		if errors.Is(err, ErrAuthFailed) {
gbe's avatar
gbe committed
			anonAuth(w, r)
gbe's avatar
gbe committed
			return
		}

		if err != nil || user == nil {
			if user == nil && err == nil {
				// The auth provider did something weird: no error, but no user either
				err = errors.New("no user")
			}

			// Some error that isn't because the session is expired or doesn't exist (DB?)
gbe's avatar
gbe committed
			level.Error(log.Get(r)).
				Log("error", err, "msg", "can't authenticate user")
			http.Error(w, err.Error(), http.StatusInternalServerError)
		user.CanWrite = true

gbe's avatar
gbe committed
		level.Info(log.Get(r)).
			Log("user", *user, "msg", "user authenticated")
gbe's avatar
gbe committed

		ctx := context.WithValue(r.Context(), key, *user)
		next.ServeHTTP(w, r.WithContext(ctx))
// Get returns the user from r's context. If there is no user in r's context, returns an empty user.
func Get(r *http.Request) User {
	val := r.Context().Value(contextKey("user"))
	if val == nil {