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