// 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`) ) var SessionCookieName = "session" // TODO: Make configurable // AuthMiddlewareConfig configures the middleware to only accept logged users (if "RequireLogged" is true) and with certain permissions (user must have all permissions inside "WithPermissions") type AuthMiddlewareConfig struct { // RequireLogged rejects not logged users if true RequireLogged bool // WithPermissions is the list of permissions the user should have to pass the middleware WithPermissions []string } // // Authenticator is the spec of this library // type Authenticator interface { // // Login checks user credentials and adds a session cookie to the user if successfull // Login(w http.ResponseWriter, r *http.Request, userID, password string) // // Logout clears the user session cookies (by setting the session cookie timeout to 0) // Logout(w http.ResponseWriter) // // Middleware is a configurable middleware to authenticate http routes based on logged status and permissions // Middleware(*AuthMiddlewareConfig) func(http.Handler) http.Handler // // LoggedMiddleware accepts all logged users without checking for specific permissions // LoggedMiddleware() func(http.Handler) http.Handler // // RequestUser returns the userID for this cookie session token // RequestUser(r *http.Request) (string, error) // } // var _ Authenticator = &AuthService{} // AuthService handles cookies, authentication and authorization of http routes by providing middlewares, logint/logout methods, user sessions and retriving the userID of an authenticated request. type AuthService struct { // CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser" CheckUserPassword func(userID string, password string) error // GetUserPermissions gets the list of permissions for this user UserPermissions func(userID string) ([]string, error) // SessionTokenFromUser returns a (new) session token that represents this user SessionTokenFromUser func(userID string) (string, error) // UserFromSessionToken returns the corresponing user for this session token or "auth.ErrNoUserForSession" UserFromSessionToken func(session string) (string, error) // AuthenticationFailed handles failed authentications AuthenticationFailed func(error) http.Handler // OtherError handles other errors OtherError func(error) http.Handler } // Login tries to login a user given its id and password func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, password string) error { if err := auth.CheckUserPassword(userID, password); err != nil { return err } token, err := auth.SessionTokenFromUser(userID) if err != nil { return err } http.SetCookie(w, &http.Cookie{ Name: SessionCookieName, Path: "/", // TODO: Make configurable Value: token, Expires: time.Now().Add(7 * 24 * time.Hour), // TODO: Make configurable }) return nil } // Logout clears the session cookie from a request effectivly logging out the user for future requests func (auth *AuthService) Logout(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: SessionCookieName, Path: "/", Value: "", Expires: time.Now(), }) } // Middleware checks if the user is logged or not and if the user has all the permissions set in "config.WithPermissions" func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) 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(SessionCookieName) if err == http.ErrNoCookie { if !config.RequireLogged { // Login not required next.ServeHTTP(w, r) return } auth.AuthenticationFailed(err).ServeHTTP(w, r) return } if err != nil { auth.OtherError(err).ServeHTTP(w, r) return } userID, err := auth.UserFromSessionToken(cookie.Value) if err == ErrNoUserForSession { auth.Logout(w) w.WriteHeader(http.StatusUnauthorized) return } if err != nil { auth.OtherError(err).ServeHTTP(w, r) return } if config.RequireLogged { userPerms, err := auth.UserPermissions(userID) if err != nil { auth.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.WithPermissions { if _, present := userPermsMap[perm]; !present { hasAll = false break } } if !hasAll { auth.AuthenticationFailed(err).ServeHTTP(w, r) return } } // Refresh session cookie expiration http.SetCookie(w, &http.Cookie{ Name: 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 checks if a user is logged, no extra permissions are checked func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler { return auth.Middleware(&AuthMiddlewareConfig{ RequireLogged: true, WithPermissions: []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 (auth *AuthService) RequestUser(r *http.Request) (string, error) { cookie, err := r.Cookie(SessionCookieName) if err != nil { return "", err } userID, err := auth.UserFromSessionToken(cookie.Value) if err == ErrNoUserForSession { return "", err } return userID, nil }