You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

197 lines
5.9 KiB
Go

// This package provides a utility for managing session cookies.
//
// The main struct is "AuthService" that provides a pluggable system for login and logout of users, creating and storing their session tokens and middlewares for accepting only logged users or ones with some specified permissions.
package auth
import (
"errors"
"net/http"
"time"
)
var (
ErrNoUserForSession = errors.New(`no user for session token`)
)
// Authenticator should be used by clients to provide authentication functions and mapping of session tokens to users
type Authenticator interface {
// CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser"
CheckUserPassword(userID string, password string) error
// GetUserPermissions gets the list of permissions for this user
UserPermissions(userID string) ([]string, error)
// SessionTokenFromUser returns a (new) session token that represents this user
SessionTokenFromUser(userID string) (string, error)
// UserFromSessionToken returns the corresponding user for this session token or "service.ErrNoUserForSession"
UserFromSessionToken(session string) (string, error)
// AuthenticationFailed handles failed authentications
AuthenticationFailed(error) http.Handler
// OtherError handles other errors
OtherError(error) http.Handler
}
// MiddlewareConfig configures the middleware to only accept logged users (if "RequireLogged" is true) and with certain permissions (user must have all permissions inside "NeedPermissions")
type MiddlewareConfig struct {
// RequireLogged rejects not logged users if true
RequireLogged bool
// NeedPermissions is the list of permissions the user should have to pass the middleware
NeedPermissions []string
}
// AuthSessionService given an Authenticator provides functions to login and logout users and an http.Handler middleware that accept users based on permissions and login status
type AuthSessionService struct {
SessionCookieName string
SessionCookiePath string
SessionCookieDuration time.Duration
Authenticator
}
// NewAuthSessionService creates a new "*AuthSessionService" with a default session cookie name and path
func NewAuthSessionService(auth Authenticator) *AuthSessionService {
oneWeek := 7 * 24 * time.Hour
return &AuthSessionService{
"session",
"/",
oneWeek,
auth,
}
}
// Login tries to login a user given its id and password
func (service *AuthSessionService) Login(w http.ResponseWriter, userID, password string) error {
if err := service.Authenticator.CheckUserPassword(userID, password); err != nil {
return err
}
token, err := service.Authenticator.SessionTokenFromUser(userID)
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: service.SessionCookieName,
Path: service.SessionCookiePath,
Value: token,
Expires: time.Now().Add(service.SessionCookieDuration),
})
return nil
}
// Logout clears the session cookie from a request effectively logging out the user for future requests
func (service *AuthSessionService) Logout(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: service.SessionCookieName,
Path: service.SessionCookiePath,
Value: "",
Expires: time.Now(),
})
}
// Middleware returns an http middleware that accepts users based on login status and permissions
func (service *AuthSessionService) Middleware(config *MiddlewareConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(service.SessionCookieName)
if err == http.ErrNoCookie {
if !config.RequireLogged { // Login not required
next.ServeHTTP(w, r)
return
}
service.Authenticator.AuthenticationFailed(err).ServeHTTP(w, r)
return
}
if err != nil {
service.Authenticator.OtherError(err).ServeHTTP(w, r)
return
}
userID, err := service.Authenticator.UserFromSessionToken(cookie.Value)
if err == ErrNoUserForSession {
service.Logout(w)
w.WriteHeader(http.StatusUnauthorized)
return
}
if err != nil {
service.Authenticator.OtherError(err).ServeHTTP(w, r)
return
}
if config.RequireLogged {
userPerms, err := service.Authenticator.UserPermissions(userID)
if err != nil {
service.Authenticator.OtherError(err).ServeHTTP(w, r)
return
}
userPermsMap := map[string]bool{}
for _, perm := range userPerms {
userPermsMap[perm] = true
}
// check the user has all the permissions to access the route
hasAll := true
for _, perm := range config.NeedPermissions {
if _, present := userPermsMap[perm]; !present {
hasAll = false
break
}
}
if !hasAll {
service.Authenticator.AuthenticationFailed(err).ServeHTTP(w, r)
return
}
}
// Refresh session cookie expiration
http.SetCookie(w, &http.Cookie{
Name: service.SessionCookieName,
Path: "/",
Value: cookie.Value,
Expires: time.Now().Add(7 * 24 * time.Hour),
})
next.ServeHTTP(w, r)
})
}
}
// LoggedMiddleware is a shortcut for
//
// Middleware(*AuthMiddlewareConfig)
//
// that only accepts logged in users, no special permissions are checked
func (service *AuthSessionService) LoggedMiddleware() func(http.Handler) http.Handler {
return service.Middleware(&MiddlewareConfig{
RequireLogged: true,
NeedPermissions: []string{},
})
}
// RequestUser retrives the "userID" from the given request based on the cookie session token.
// When generics arrive this will become something like
//
// func (auth *AuthService[U]) RequestUser(r *http.Request) *U
//
// and will just return nil if no user is present.
func (service *AuthSessionService) RequestUser(r *http.Request) (string, error) {
cookie, err := r.Cookie(service.SessionCookieName)
if err != nil {
return "", err
}
userID, err := service.Authenticator.UserFromSessionToken(cookie.Value)
if err == ErrNoUserForSession {
return "", err
}
return userID, nil
}