package authserver import ( "database/sql" "encoding/gob" "errors" "flag" "net/http" "os" "time" "github.com/gin-contrib/cors" "github.com/gin-contrib/requestid" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/postgres" "github.com/gin-gonic/gin" "github.com/go-logr/logr" "github.com/peterbourgon/ff/v3" "edge-infra.dev/pkg/edge/api/middleware" authproxy "edge-infra.dev/pkg/edge/auth-proxy/types" ) var ( // sessionIdentifier identifier string for the session. sessionIdentifier = "edge-session" bannerHeaderName = "Banner" // sessionDurationMinutes is the session max time duration. This wouldn't be used here because the authserver does not create or update sessions. sessionDurationMinutes = 15 // cookie security config // https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/06-Session_Management_Testing/02-Testing_for_Cookies_Attributes#:~:text=Strong%20Practices sessionOptions = sessions.Options{ Secure: true, HttpOnly: true, MaxAge: sessionDurationMinutes * 60, SameSite: http.SameSiteStrictMode, Path: "/", } // ErrSessionExpired is an error returned when the session exists but has expired. ErrSessionExpired = errors.New("access denied, session expired") // ErrSessionHasNoExpiration is an error returned when session exists but the expiration is empty (probably would never happen). ErrSessionHasNoExpiration = errors.New("an error occurred, session has no expiration") // ErrInvalidCredentials is an error returned when no session is present. ErrInvalidCredentials = errors.New("an error occurred, invalid credentials provided") ) type AuthServer struct { GinEngine *gin.Engine Log logr.Logger GinMode string databaseHost string databaseConnectionName string databaseName string databaseUsername string databasePassword string databasePort string sessionSecret string db *sql.DB checks []check } func init() { // register other types for cookie codec gob.Register(time.Time{}) } func NewAuthServer(args []string, router *gin.Engine) (*AuthServer, error) { authServer := AuthServer{ GinEngine: router, checks: allChecks, } fs := flag.NewFlagSet("authserver", flag.ExitOnError) fs.StringVar( &authServer.GinMode, "gin-mode", gin.ReleaseMode, "gin release mode (debug or release)", ) fs.StringVar(&authServer.databaseHost, "database-host", "", "Database Host", ) fs.StringVar(&authServer.databaseConnectionName, "database-connection-name", "", "Database Connection Name", ) fs.StringVar(&authServer.databaseName, "database-name", "", "Database Name", ) fs.StringVar(&authServer.databaseUsername, "database-username", "", "Database User Name", ) fs.StringVar(&authServer.databasePassword, "database-password", "", "Database Password", ) fs.StringVar(&authServer.databasePort, "database-port", "", "Database Port", ) fs.StringVar(&authServer.sessionSecret, "session-secret", "", "Session Secret", ) if err := ff.Parse(fs, args[1:], ff.WithEnvVarNoPrefix()); err != nil { return nil, err } gin.SetMode(authServer.GinMode) if err := authServer.newGinServer(); err != nil { return nil, err } // set logger newLogger(&authServer) return &authServer, nil } func (as *AuthServer) newGinServer() error { router := as.GinEngine useMetrics(router) // Create contextual id at beginning of request router.Use(middleware.SetRequestContext()) // Http logging router.Use(requestid.New()) router.Use(gin.Recovery()) router.Any("/health", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) router.Any("/ready", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) router.NoRoute(as.handleAuthRequest()) if err := as.createSessionStore(router); err != nil { return err } corsConfig := cors.DefaultConfig() corsConfig.AllowAllOrigins = true corsConfig.AllowHeaders = []string{"*"} router.Use(cors.New(corsConfig)) return nil } func Run() error { ginEngine := gin.New() authServer, err := NewAuthServer(os.Args, ginEngine) if err != nil { return err } authServer.Log.Info("starting auth server") return authServer.GinEngine.Run() } func (as *AuthServer) createSessionStore(ginEngine *gin.Engine) error { db, err := as.connectDatabase() if err != nil { return err } as.db = db store, err := postgres.NewStore(db, []byte(as.sessionSecret)) if err != nil { return err } store.Options(sessionOptions) session := sessions.Sessions(sessionIdentifier, store) ginEngine.Use(session) return nil } func (as *AuthServer) handleAuthRequest() gin.HandlerFunc { fn := func(ctx *gin.Context) { startTime := time.Now().UTC() session := sessions.Default(ctx) auth := session.Get(authproxy.SessionIDField) switch auth { case nil: as.handleNotAuthenticated(ctx, startTime) default: as.handleAuthenticated(ctx, startTime, session) } } return fn }