package auth import ( "context" "errors" "net/http" "strings" "github.com/go-kit/kit/log/level" "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 } func (u User) String() string { readOnly := " (ro)" if u.CanWrite { readOnly = "" } return "{" + u.Name + readOnly + "}" } 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") 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") authFailed.ServeHTTP(w, r) return } level.Info(log.Get(r)). Log("msg", "no session, using anon auth") user := &User{ Name: "anon", } ctx := context.WithValue(r.Context(), key, *user) next.ServeHTTP(w, r.WithContext(ctx)) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var token string cookie, err := r.Cookie("session") if err == nil { token = cookie.Value } user, err := provider.Valid(r.Context(), token) if errors.Is(err, ErrAuthFailed) { anonAuth(w, r) 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?) level.Error(log.Get(r)). Log("error", err, "msg", "can't authenticate user") http.Error(w, err.Error(), http.StatusInternalServerError) return } user.CanWrite = true level.Info(log.Get(r)). Log("user", *user, "msg", "user authenticated") 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 { return User{} } return val.(User) }