// 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 }