Alexander Belanger vor 5 Jahren
Ursprung
Commit
8767f38d31

BIN
app


+ 5 - 1
cmd/app/main.go

@@ -10,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/server/api"
 
 	adapter "github.com/porter-dev/porter/internal/adapter"
+	sessionstore "github.com/porter-dev/porter/internal/auth/"
 	"github.com/porter-dev/porter/internal/config"
 	lr "github.com/porter-dev/porter/internal/logger"
 	vr "github.com/porter-dev/porter/internal/validator"
@@ -27,10 +28,13 @@ func main() {
 		return
 	}
 
+	key = []byte("secret") // TODO: change to os.Getenv("SESSION_KEY")
+	store, _ = sessionstore.NewStore(db, key)
+
 	validator := vr.New()
 	repo := gorm.NewRepository(db)
 
-	a := api.New(logger, repo, validator)
+	a := api.New(logger, repo, validator, store)
 
 	appRouter := router.New(a)
 

+ 8 - 1
go.mod

@@ -3,6 +3,7 @@ module github.com/porter-dev/porter
 go 1.14
 
 require (
+	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/cosmtrek/air v1.21.2 // indirect
 	github.com/creack/pty v1.1.11 // indirect
 	github.com/fatih/color v1.9.0 // indirect
@@ -10,13 +11,19 @@ require (
 	github.com/go-playground/locales v0.13.0
 	github.com/go-playground/universal-translator v0.17.0
 	github.com/go-playground/validator/v10 v10.3.0
+	github.com/go-test/deep v1.0.7
+	github.com/gorilla/securecookie v1.1.1
+	github.com/gorilla/sessions v1.2.1
 	github.com/imdario/mergo v0.3.11 // indirect
+	github.com/jinzhu/gorm v1.9.16
 	github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
 	github.com/leodido/go-urn v1.2.0 // indirect
 	github.com/mattn/go-colorable v0.1.7 // indirect
 	github.com/pelletier/go-toml v1.8.1 // indirect
+	github.com/pkg/errors v0.9.1
 	github.com/rs/zerolog v1.20.0
-	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
+	github.com/stretchr/testify v1.5.1
+	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
 	golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
 	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
 	golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d // indirect

+ 14 - 0
go.sum

@@ -41,6 +41,8 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
+github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
 github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -108,6 +110,8 @@ github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+
 github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
+github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
@@ -164,6 +168,10 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
 github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
+github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -232,6 +240,8 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
 github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
+github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@@ -296,7 +306,9 @@ github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNC
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -316,10 +328,12 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

+ 62 - 0
internal/auth/example/authExample.go

@@ -0,0 +1,62 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/internal/config"
+
+	dbConn "github.com/porter-dev/porter/internal/adapter"
+	sessionstore "github.com/porter-dev/porter/internal/auth"
+)
+
+var appConf = config.AppConfig()
+
+var db, dbErr = dbConn.New(&appConf.Db)
+
+var (
+	key      = []byte("secret") // change to os.Getenv("SESSION_KEY")
+	store, _ = sessionstore.NewStore(db, key)
+)
+
+func secret(w http.ResponseWriter, r *http.Request) {
+
+	session, _ := store.Get(r, "cookie-name")
+	fmt.Println(session.Values["authenticated"])
+
+	// Check if user is authenticated
+	if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
+		http.Error(w, "Forbidden", http.StatusForbidden)
+		return
+	}
+
+	// Print secret message
+	fmt.Fprintln(w, "The cake is a lie!")
+}
+
+func login(w http.ResponseWriter, r *http.Request) {
+	session, _ := store.Get(r, "cookie-name")
+
+	// Authentication goes here
+	// ...
+
+	// Set user as authenticated
+	session.Values["authenticated"] = true
+	session.Save(r, w)
+}
+
+func logout(w http.ResponseWriter, r *http.Request) {
+	session, _ := store.Get(r, "cookie-name")
+
+	// Revoke users authentication
+	session.Values["authenticated"] = false
+	session.Save(r, w)
+}
+
+func main() {
+	http.HandleFunc("/secret", secret)
+	http.HandleFunc("/login", login)
+	http.HandleFunc("/logout", logout)
+
+	http.ListenAndServe(":8080", nil)
+}

+ 195 - 0
internal/auth/sessionstore.go

@@ -0,0 +1,195 @@
+// Package sessionstore is a postgresql backend implementation of gorilla/sessions Session interface, based on
+// antonlindstrom/pgstore. Key change is to use GORM instead of typical sql driver using queries.
+package sessionstore
+
+import (
+	"database/sql"
+	"encoding/base32"
+	"net/http"
+	"strings"
+	"time"
+
+	"gorm.io/gorm"
+
+	"github.com/gorilla/securecookie"
+	"github.com/gorilla/sessions"
+	"github.com/pkg/errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	rp "github.com/porter-dev/porter/internal/repository/gorm"
+)
+
+// structs
+
+// PGStore is a wrapper around gorilla/sessions store.
+type PGStore struct {
+	Codecs  []securecookie.Codec
+	Options *sessions.Options
+	Path    string
+	DbPool  *gorm.DB
+}
+
+// Helpers
+
+// MaxLength restricts the maximum length of new sessions to l.
+// If l is 0 there is no limit to the size of a session, use with caution.
+// The default for a new PGStore is 4096. PostgreSQL allows for max
+// value sizes of up to 1GB (http://www.postgresql.org/docs/current/interactive/datatype-character.html)
+func (db *PGStore) MaxLength(l int) {
+	for _, c := range db.Codecs {
+		if codec, ok := c.(*securecookie.SecureCookie); ok {
+			codec.MaxLength(l)
+		}
+	}
+}
+
+// MaxAge sets the maximum age for the store and the underlying cookie
+// implementation. Individual sessions can be deleted by setting Options.MaxAge
+// = -1 for that session.
+func (db *PGStore) MaxAge(age int) {
+	db.Options.MaxAge = age
+
+	// Set the maxAge for each securecookie instance.
+	for _, codec := range db.Codecs {
+		if sc, ok := codec.(*securecookie.SecureCookie); ok {
+			sc.MaxAge(age)
+		}
+	}
+}
+
+// load fetches a session by ID from the database and decodes its content
+// into session.Values.
+func (db *PGStore) load(session *sessions.Session) error {
+	repo := rp.NewRepository(db.DbPool)
+	res, err := repo.Session.SelectSession(&models.Session{Key: session.ID})
+
+	if err != nil {
+		return err
+	}
+
+	return securecookie.DecodeMulti(session.Name(), string(res.Data), &session.Values, db.Codecs...)
+}
+
+// save writes encoded session.Values to a database record.
+// writes to http_sessions table by default.
+func (db *PGStore) save(session *sessions.Session) error {
+	encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, db.Codecs...)
+	if err != nil {
+		return err
+	}
+
+	exOn := session.Values["expires_on"]
+
+	var expiresOn time.Time
+
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+	} else {
+		expiresOn = exOn.(time.Time)
+		if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
+			expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		}
+	}
+
+	s := models.Session{
+		Key:       session.ID,
+		Data:      []byte(encoded),
+		ExpiresAt: expiresOn,
+	}
+
+	repo := rp.NewRepository(db.DbPool)
+
+	if session.IsNew {
+		_, createErr := repo.Session.CreateSession(&s)
+		return createErr
+	}
+
+	_, updateErr := repo.Session.UpdateSession(&s)
+	return updateErr
+}
+
+// Implementation of the interface (Get, New, Save)
+
+// NewStore takes an initialized db and session key pairs to create a session-store in postgres db.
+func NewStore(db *gorm.DB, keyPairs ...[]byte) (*PGStore, error) {
+	dbStore := &PGStore{
+		Codecs: securecookie.CodecsFromPairs(keyPairs...),
+		Options: &sessions.Options{
+			Path:   "/",
+			MaxAge: 86400 * 30,
+		},
+		DbPool: db,
+	}
+
+	return dbStore, nil
+}
+
+// Get Fetches a session for a given name after it has been added to the
+// registry.
+func (db *PGStore) Get(r *http.Request, name string) (*sessions.Session, error) {
+	return sessions.GetRegistry(r).Get(db, name)
+}
+
+// New returns a new session for the given name without adding it to the registry.
+func (db *PGStore) New(r *http.Request, name string) (*sessions.Session, error) {
+	session := sessions.NewSession(db, name)
+	if session == nil {
+		return nil, nil
+	}
+
+	opts := *db.Options
+	session.Options = &(opts)
+	session.IsNew = true
+
+	var err error
+	if c, errCookie := r.Cookie(name); errCookie == nil {
+		err = securecookie.DecodeMulti(name, c.Value, &session.ID, db.Codecs...)
+		if err == nil {
+			err = db.load(session)
+			if err == nil {
+				session.IsNew = false
+			} else if errors.Cause(err) == sql.ErrNoRows {
+				err = nil
+			}
+		}
+	}
+
+	db.MaxAge(db.Options.MaxAge)
+
+	return session, err
+}
+
+// Save saves the given session into the database and deletes cookies if needed
+func (db *PGStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+	repo := rp.NewRepository(db.DbPool)
+
+	// Set delete if max-age is < 0
+	if session.Options.MaxAge < 0 {
+		if _, err := repo.Session.DeleteSession(&models.Session{Key: session.ID}); err != nil {
+			return err
+		}
+		http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
+		return nil
+	}
+
+	if session.ID == "" {
+		// Generate a random session ID key suitable for storage in the DB
+		session.ID = strings.TrimRight(
+			base32.StdEncoding.EncodeToString(
+				securecookie.GenerateRandomKey(32),
+			), "=")
+	}
+
+	if err := db.save(session); err != nil {
+		return err
+	}
+
+	// Keep the session ID key in a cookie so it can be looked up in DB later.
+	encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, db.Codecs...)
+	if err != nil {
+		return err
+	}
+
+	http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
+	return nil
+}

+ 152 - 0
internal/auth/sessionstore_test.go

@@ -0,0 +1,152 @@
+package sessionstore
+
+import (
+	"encoding/base64"
+	"net/http"
+	"testing"
+
+	"github.com/gorilla/securecookie"
+	"github.com/gorilla/sessions"
+
+	dbConn "github.com/porter-dev/porter/internal/adapter"
+)
+
+type headerOnlyResponseWriter http.Header
+
+func (ho headerOnlyResponseWriter) Header() http.Header {
+	return http.Header(ho)
+}
+
+func (ho headerOnlyResponseWriter) Write([]byte) (int, error) {
+	panic("NOIMPL")
+}
+
+func (ho headerOnlyResponseWriter) WriteHeader(int) {
+	panic("NOIMPL")
+}
+
+var secret = "secret"
+
+func TestPGStore(t *testing.T) {
+	db, _ := dbConn.New()
+
+	ss, err := NewStore(db, []byte(secret))
+	if err != nil {
+		t.Fatal("Failed to get store", err)
+	}
+
+	// ROUND 1 - Check that the cookie is being saved
+	req, err := http.NewRequest("GET", "http://www.example.com", nil)
+	if err != nil {
+		t.Fatal("failed to create request", err)
+	}
+
+	session, err := ss.Get(req, "mysess")
+	if err != nil {
+		t.Fatal("failed to get session", err.Error())
+	}
+
+	session.Values["counter"] = 1
+
+	m := make(http.Header)
+	if err = ss.Save(req, headerOnlyResponseWriter(m), session); err != nil {
+		t.Fatal("Failed to save session:", err.Error())
+	}
+
+	if m["Set-Cookie"][0][0:6] != "mysess" {
+		t.Fatal("Cookie wasn't set!")
+	}
+
+	// ROUND 2 - check that the cookie can be retrieved
+	req, err = http.NewRequest("GET", "http://www.example.com", nil)
+	if err != nil {
+		t.Fatal("failed to create round 2 request", err)
+	}
+
+	encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, ss.Codecs...)
+	if err != nil {
+		t.Fatal("Failed to make cookie value", err)
+	}
+
+	req.AddCookie(sessions.NewCookie(session.Name(), encoded, session.Options))
+
+	session, err = ss.Get(req, "mysess")
+	if err != nil {
+		t.Fatal("failed to get round 2 session", err.Error())
+	}
+
+	if session.Values["counter"] != 1 {
+		t.Fatal("Retrieved session had wrong value:", session.Values["counter"])
+	}
+
+	session.Values["counter"] = 9 // set new value for round 3
+	if err = ss.Save(req, headerOnlyResponseWriter(m), session); err != nil {
+		t.Fatal("Failed to save session:", err.Error())
+	}
+
+	// ROUND 2 - check that the cookie has been updated
+	req, err = http.NewRequest("GET", "http://www.example.com", nil)
+	if err != nil {
+		t.Fatal("failed to create round 3 request", err)
+	}
+	req.AddCookie(sessions.NewCookie(session.Name(), encoded, session.Options))
+
+	session, err = ss.Get(req, "mysess")
+	if err != nil {
+		t.Fatal("failed to get session round 3", err.Error())
+	}
+
+	if session.Values["counter"] != 9 {
+		t.Fatal("Retrieved session had wrong value in round 3:", session.Values["counter"])
+	}
+
+	// ROUND 3 - Increase max length
+	req, err = http.NewRequest("GET", "http://www.example.com", nil)
+	if err != nil {
+		t.Fatal("failed to create round 3 request", err)
+	}
+
+	req.AddCookie(sessions.NewCookie(session.Name(), encoded, session.Options))
+	session, err = ss.New(req, "my session")
+	if err != nil {
+		t.Fatal("failed to create session", err)
+	}
+
+	session.Values["big"] = make([]byte, base64.StdEncoding.DecodedLen(4096*2))
+
+	if err = ss.Save(req, headerOnlyResponseWriter(m), session); err == nil {
+		t.Fatal("expected an error, got nil")
+	}
+
+	ss.MaxLength(4096 * 3) // A bit more than the value size to account for encoding overhead.
+	if err = ss.Save(req, headerOnlyResponseWriter(m), session); err != nil {
+		t.Fatal("Failed to save session:", err.Error())
+	}
+}
+
+func TestSessionOptionsAreUniquePerSession(t *testing.T) {
+	db, _ := dbConn.New()
+
+	ss, err := NewStore(db, []byte(secret))
+	if err != nil {
+		t.Fatal("Failed to get store", err)
+	}
+
+	ss.Options.MaxAge = 900
+
+	req, err := http.NewRequest("GET", "http://www.example.com", nil)
+	if err != nil {
+		t.Fatal("Failed to create request", err)
+	}
+
+	session, err := ss.Get(req, "newsess")
+	if err != nil {
+		t.Fatal("Failed to create session", err)
+	}
+
+	session.Options.MaxAge = -1
+
+	if ss.Options.MaxAge != 900 {
+		t.Fatalf("PGStore.Options.MaxAge: expected %d, got %d", 900, ss.Options.MaxAge)
+	}
+}

+ 8 - 3
internal/forms/user.go

@@ -3,6 +3,7 @@ package forms
 import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
+	"golang.org/x/crypto/bcrypt"
 	"gorm.io/gorm"
 )
 
@@ -19,12 +20,16 @@ type CreateUserForm struct {
 }
 
 // ToUser converts a CreateUserForm to models.User
-//
-// TODO -- PASSWORD HASHING HERE
 func (cuf *CreateUserForm) ToUser() (*models.User, error) {
+	hashed, err := bcrypt.GenerateFromPassword([]byte(cuf.Password), 8)
+
+	if err != nil {
+		return nil, err
+	}
+
 	return &models.User{
 		Email:    cuf.Email,
-		Password: cuf.Password,
+		Password: string(hashed),
 	}, nil
 }
 

+ 18 - 0
internal/models/session.go

@@ -0,0 +1,18 @@
+package models
+
+import (
+	"time"
+
+	"github.com/jinzhu/gorm"
+)
+
+// Session type that extends gorm.Model.
+type Session struct {
+	gorm.Model
+	// Session ID
+	Key string
+	// encrypted cookie
+	Data []byte
+	// Time the session will expire
+	ExpiresAt time.Time
+}

+ 2 - 1
internal/repository/gorm/repository.go

@@ -9,6 +9,7 @@ import (
 // gorm.DB for querying the database
 func NewRepository(db *gorm.DB) *repository.Repository {
 	return &repository.Repository{
-		User: NewUserRepository(db),
+		User:    NewUserRepository(db),
+		Session: NewSessionRepository(db),
 	}
 }

+ 55 - 0
internal/repository/gorm/session.go

@@ -0,0 +1,55 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+type sessionrepo struct {
+	db *gorm.DB
+}
+
+// NewSessionRepository returns pointer to repo along with the db
+func NewSessionRepository(db *gorm.DB) repository.SessionRepository {
+	return &sessionrepo{
+		db: db,
+	}
+}
+
+// CreateSession must take in Key, Data, and ExpiresAt as arguments.
+func (s *sessionrepo) CreateSession(session *models.Session) (*models.Session, error) {
+	// TODO: check for duplicate and return error
+	if err := s.db.Create(session).Error; err != nil {
+		return nil, err
+	}
+	return session, nil
+}
+
+// UpdateSession updates only the Data field using Key as selector.
+func (s *sessionrepo) UpdateSession(session *models.Session) (*models.Session, error) {
+	if err := s.db.Model(session).Where("Key = ?", session.Key).Updates(session).Error; err != nil {
+		return nil, err
+	}
+	return session, nil
+}
+
+// DeleteSession deletes a session by Key
+func (s *sessionrepo) DeleteSession(session *models.Session) (*models.Session, error) {
+
+	if err := s.db.Where("Key = ?", session.Key).Delete(session).Error; err != nil {
+		return nil, err
+	}
+
+	return session, nil
+}
+
+// SelectSession returns a session with matching key
+func (s *sessionrepo) SelectSession(session *models.Session) (*models.Session, error) {
+
+	if err := s.db.Where("Key = ?", session.Key).First(session).Error; err != nil {
+		return nil, err
+	}
+
+	return session, nil
+}

+ 150 - 0
internal/repository/gorm/session_test.go

@@ -0,0 +1,150 @@
+package gorm
+
+import (
+	"database/sql"
+	"regexp"
+	"testing"
+	"time"
+
+	"gorm.io/driver/postgres"
+
+	"github.com/DATA-DOG/go-sqlmock"
+	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	"gorm.io/gorm"
+)
+
+type Suite struct {
+	suite.Suite
+	db   *gorm.DB
+	mock sqlmock.Sqlmock
+
+	repo    repository.SessionRepository
+	session *models.Session
+}
+
+func (s *Suite) SetupSuite() {
+	var (
+		db  *sql.DB
+		err error
+	)
+
+	// TODO: make it work with gorm.io/gorm, currently only works with jinzhu/gorm (gorm V1)
+	db, s.mock, err = sqlmock.New()
+	// db, s.mock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
+
+	require.NoError(s.T(), err)
+
+	s.db, err = gorm.Open(postgres.New(postgres.Config{
+		Conn: db,
+	}), &gorm.Config{})
+
+	require.NoError(s.T(), err)
+
+	s.repo = NewSessionRepository(s.db)
+}
+
+func (s *Suite) AfterTest(_, _ string) {
+	require.NoError(s.T(), s.mock.ExpectationsWereMet())
+}
+
+func TestInit(t *testing.T) {
+	suite.Run(t, new(Suite))
+}
+
+func (s *Suite) TestShouldCreateNewSession() {
+	var (
+		key       = "onekey"
+		data      = []byte("onedata")
+		expiresAt = time.Now()
+	)
+
+	rows := sqlmock.NewRows([]string{"id"}).AddRow("111")
+
+	s.mock.ExpectBegin()
+	s.mock.ExpectQuery(regexp.QuoteMeta(
+		`INSERT INTO "sessions" ("created_at","updated_at","deleted_at","key","data","expires_at")
+		VALUES ($1,$2,$3,$4,$5,$6) RETURNING "sessions"."id"`)).
+		WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), key, data, expiresAt).
+		WillReturnRows(rows)
+	s.mock.ExpectCommit()
+
+	// test function
+	_, err := s.repo.CreateSession(&models.Session{
+		Key:       key,
+		Data:      data,
+		ExpiresAt: expiresAt,
+	})
+
+	require.NoError(s.T(), err)
+}
+
+func (s *Suite) TestShoudSelectSessionByKey() {
+	var (
+		key = "onekey"
+	)
+
+	rows := sqlmock.NewRows([]string{"Key"}).AddRow(key)
+
+	s.mock.ExpectQuery(`.*`). // do proper regex labor later as meditative exercise
+					WithArgs(key).
+					WillReturnRows(rows)
+
+	// test function
+	res, err := s.repo.SelectSession(&models.Session{
+		Key: key,
+	})
+
+	require.NoError(s.T(), err)
+	require.Nil(s.T(), deep.Equal(&models.Session{Key: key}, res))
+}
+
+func (s *Suite) TestShouldUpdateSessionByKey() {
+	var (
+		key       = "onekey"
+		data      = []byte("chobanilime")
+		expiresAt = time.Now()
+	)
+
+	// rows := sqlmock.NewRows([]string{"Key"}).AddRow(key)
+
+	s.mock.ExpectBegin()
+	s.mock.ExpectExec(`.*`). // do proper regex labor later as meditative exercise
+					WithArgs(data, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), key).
+					WillReturnResult(sqlmock.NewResult(1, 1))
+	s.mock.ExpectCommit()
+
+	// test function
+	_, err := s.repo.UpdateSession(&models.Session{
+		Key:       key,
+		Data:      data,
+		ExpiresAt: expiresAt,
+	})
+
+	require.NoError(s.T(), err)
+	// require.Nil(s.T(), deep.Equal(&models.Session{Data: data}, res))
+}
+
+func (s *Suite) TestShouldDeleteSession() {
+	var (
+		key = "onekey"
+	)
+
+	// rows := sqlmock.NewRows([]string{"id"}).AddRow("111")
+
+	s.mock.ExpectBegin()
+	s.mock.ExpectExec(`.*`).
+		WithArgs(sqlmock.AnyArg(), key).
+		WillReturnResult(sqlmock.NewResult(1, 1))
+	s.mock.ExpectCommit()
+
+	// test function
+	_, err := s.repo.DeleteSession(&models.Session{
+		Key: key,
+	})
+
+	require.NoError(s.T(), err)
+}

+ 16 - 0
internal/repository/gorm/user.go

@@ -3,6 +3,7 @@ package gorm
 import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/crypto/bcrypt"
 	"gorm.io/gorm"
 )
 
@@ -59,3 +60,18 @@ func (repo *UserRepository) DeleteUser(user *models.User) (*models.User, error)
 	}
 	return user, nil
 }
+
+// CheckPassword checks the input password is correct for the provided user id.
+func (repo *UserRepository) CheckPassword(id int, pwd string) (bool, error) {
+	u := &models.User{}
+
+	if err := repo.db.First(u, id).Error; err != nil {
+		return false, err
+	}
+
+	if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(pwd)); err != nil {
+		return false, err
+	}
+
+	return true, nil
+}

+ 2 - 1
internal/repository/repository.go

@@ -2,5 +2,6 @@ package repository
 
 // Repository collects the repositories for each model
 type Repository struct {
-	User UserRepository
+	User    UserRepository
+	Session SessionRepository
 }

+ 13 - 0
internal/repository/session.go

@@ -0,0 +1,13 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// SessionRepository represents the set of queries on the Session model
+type SessionRepository interface {
+	CreateSession(session *models.Session) (*models.Session, error)
+	UpdateSession(session *models.Session) (*models.Session, error)
+	DeleteSession(session *models.Session) (*models.Session, error)
+	SelectSession(session *models.Session) (*models.Session, error)
+}

+ 1 - 0
internal/repository/user.go

@@ -10,6 +10,7 @@ type WriteUser func(user *models.User) (*models.User, error)
 // UserRepository represents the set of queries on the User model
 type UserRepository interface {
 	CreateUser(user *models.User) (*models.User, error)
+	CheckPassword(id int, pwd string) (bool, error)
 	ReadUser(id uint) (*models.User, error)
 	ReadUserByEmail(email string) (*models.User, error)
 	UpdateUser(user *models.User) (*models.User, error)

+ 4 - 0
server/api/api.go

@@ -4,6 +4,7 @@ import (
 	"github.com/go-playground/locales/en"
 	ut "github.com/go-playground/universal-translator"
 	"github.com/go-playground/validator/v10"
+	sessionstore "github.com/porter-dev/porter/internal/auth"
 	lr "github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/repository"
 )
@@ -14,6 +15,7 @@ type App struct {
 	logger     *lr.Logger
 	repo       *repository.Repository
 	validator  *validator.Validate
+	store      *sessionstore.PGStore
 	translator *ut.Translator
 }
 
@@ -22,6 +24,7 @@ func New(
 	logger *lr.Logger,
 	repo *repository.Repository,
 	validator *validator.Validate,
+	store *sessionstore.PGStore,
 ) *App {
 	// for now, will just support the english translator from the
 	// validator/translations package
@@ -33,6 +36,7 @@ func New(
 		logger:     logger,
 		repo:       repo,
 		validator:  validator,
+		store:      store,
 		translator: &trans,
 	}
 }

+ 31 - 0
server/api/user_handler.go

@@ -15,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/crypto/bcrypt"
 )
 
 // Enumeration of user API error codes, represented as int64
@@ -43,6 +44,36 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// HandleLoginUser checks the request header for cookie and validates the user.
+func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
+	session, _ := app.store.Get(r, "cookie-name")
+
+	// read in email and password from request
+	email := chi.URLParam(r, "email")
+	password := chi.URLParam(r, "password")
+
+	// Authentication goes here
+	// Select User by Username (app.repo.User.ReadUserByUsername) and return storedCreds object that has Password.
+	storedUser, readErr := app.repo.User.ReadUserByEmail(email)
+
+	if readErr != nil {
+		// You're not registered error
+		app.logger.Warn().Err(readErr)
+		w.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
+	if err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(password)); err != nil {
+		// If the two passwords don't match, return a 401 status
+		w.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
+	// Set user as authenticated
+	session.Values["authenticated"] = true
+	session.Save(r, w)
+}
+
 // HandleReadUser returns an externalized User (models.UserExternal)
 // based on an ID
 func (app *App) HandleReadUser(w http.ResponseWriter, r *http.Request) {

+ 37 - 0
server/router/middleware/auth.go

@@ -0,0 +1,37 @@
+package middleware
+
+import (
+	"net/http"
+
+	"github.com/gorilla/sessions"
+)
+
+var (
+	key   = []byte("secret")             // change to os.Getenv("SESSION_KEY")
+	store = sessions.NewCookieStore(key) // Swap out with custom store
+)
+
+// Authenticate is middleware for authentication
+func Authenticate(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if isLoggedIn(r) {
+			next.ServeHTTP(w, r)
+		} else {
+			http.Error(w, http.StatusText(403), 403)
+			return
+		}
+
+		return
+	})
+}
+
+// Helpers
+
+func isLoggedIn(r *http.Request) bool {
+	session, _ := store.Get(r, "session-id")
+
+	if auth, ok := session.Values["authenticated"].(bool); !auth || !ok {
+		return false
+	}
+	return true
+}

+ 1 - 0
server/router/router.go

@@ -22,6 +22,7 @@ func New(a *api.App) *chi.Mux {
 		r.Method("POST", "/users", requestlog.NewHandler(a.HandleCreateUser, l))
 		r.Method("PUT", "/users/{id}", requestlog.NewHandler(a.HandleUpdateUser, l))
 		r.Method("DELETE", "/users/{id}", requestlog.NewHandler(a.HandleDeleteUser, l))
+		r.Method("POST", "/login", requestlog.NewHandler(a.HandleLoginUser, l))
 	})
 
 	return r