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.
196 lines
6.0 KiB
Go
196 lines
6.0 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`)
|
|
)
|
|
|
|
type User[IdType any] interface {
|
|
UID() IdType
|
|
}
|
|
|
|
// Authenticator should be used by clients to provide authentication functions and mapping of session tokens to users
|
|
type Authenticator[UserID any, U User[UserID]] interface {
|
|
// CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser"
|
|
CheckUserPassword(user UserID, password string) error
|
|
|
|
// GetUserPermissions gets the list of permissions for this user
|
|
UserPermissions(user UserID) ([]string, error)
|
|
|
|
// SessionTokenFromUser returns a (new) session token that represents this user
|
|
SessionTokenFromUser(user UserID) (string, error)
|
|
// UserFromSessionToken returns the corresponding user for this session token or "service.ErrNoUserForSession"
|
|
UserFromSessionToken(session string) (*U, 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[UserID any, U User[UserID]] struct {
|
|
SessionCookieName string
|
|
SessionCookiePath string
|
|
SessionCookieDuration time.Duration
|
|
|
|
Authenticator Authenticator[UserID, U]
|
|
}
|
|
|
|
// NewAuthSessionService creates a new "*AuthSessionService" with a default session cookie name and path
|
|
func NewAuthSessionService[UserID any, U User[UserID]](auth Authenticator[UserID, U]) *AuthSessionService[UserID, U] {
|
|
oneWeek := 7 * 24 * time.Hour
|
|
|
|
return &AuthSessionService[UserID, U]{
|
|
"session",
|
|
"/",
|
|
oneWeek,
|
|
auth,
|
|
}
|
|
}
|
|
|
|
// Login tries to login a user given its id and password
|
|
func (service *AuthSessionService[UserID, U]) Login(w http.ResponseWriter, userID 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[UserID, U]) 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[UserID, U]) 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
|
|
}
|
|
|
|
user, 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((*user).UID())
|
|
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[UserID, U]) LoggedMiddleware() func(http.Handler) http.Handler {
|
|
return service.Middleware(&MiddlewareConfig{
|
|
RequireLogged: true,
|
|
NeedPermissions: []string{},
|
|
})
|
|
}
|
|
|
|
// RequestUser retrieves the "userID" from the given request based on the cookie session token.
|
|
func (service *AuthSessionService[UserID, U]) RequestUser(r *http.Request) (*U, error) {
|
|
cookie, err := r.Cookie(service.SessionCookieName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := service.Authenticator.UserFromSessionToken(cookie.Value)
|
|
if err == ErrNoUserForSession {
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|