Repository: prest/prest
Pull request #451 (2020-10-09 11:49:45)

Basic AUTH with JWT
sharing how I imagine endpoint auth token generation
cc @tiaguinho

fixed: #276


Patch:
diff --git a/cmd/auth.go b/cmd/auth.go
new file mode 100644
index 0000000..0b453bf
--- /dev/null
+++ b/cmd/auth.go
@@ -0,0 +1,56 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/prest/prest/adapters/postgres"
+	"github.com/prest/prest/config"
+	"github.com/spf13/cobra"
+)
+
+var authUpCmd = &cobra.Command{
+	Use:   "auth",
+	Short: "Create auth table",
+	Long:  "Create basic table to use on auth endpoint",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if config.PrestConf.Adapter == nil {
+			postgres.Load()
+		}
+
+		db, err := postgres.Get()
+		if err != nil {
+			fmt.Fprintf(os.Stdout, err.Error())
+			return err
+		}
+		_, err = db.Exec("CREATE TABLE IF NOT EXISTS public.prest_users (id serial, name text, username text unique, password text, metadata jsonb)")
+		if err != nil {
+			fmt.Fprintf(os.Stdout, err.Error())
+			return err
+		}
+		return nil
+	},
+}
+
+var authDownCmd = &cobra.Command{
+	Use:   "auth",
+	Short: "Drop auth table",
+	Long:  "Drop basic table used on auth endpoint",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if config.PrestConf.Adapter == nil {
+			postgres.Load()
+		}
+
+		db, err := postgres.Get()
+		if err != nil {
+			fmt.Fprintf(os.Stdout, err.Error())
+			return err
+		}
+		_, err = db.Exec("DROP TABLE IF EXISTS public.prest_users")
+		if err != nil {
+			fmt.Fprintf(os.Stdout, err.Error())
+			return err
+		}
+		return nil
+	},
+}
diff --git a/cmd/root.go b/cmd/root.go
index 126a51a..da7884b 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -21,7 +21,7 @@ import (
 var RootCmd = &cobra.Command{
 	Use:   "prest",
 	Short: "Serve a RESTful API from any PostgreSQL database",
-	Long:  `Serve a RESTful API from any PostgreSQL database, start HTTP server`,
+	Long:  `pREST (PostgreSQL REST), simplify and accelerate development, ⚡ instant, realtime, high-performance on any Postgres application, existing or new`,
 	Run: func(cmd *cobra.Command, args []string) {
 		if config.PrestConf.Adapter == nil {
 			nlog.Warningln("adapter is not set. Using the default (postgres)")
@@ -34,6 +34,8 @@ var RootCmd = &cobra.Command{
 // Execute adds all child commands to the root command sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 func Execute() {
+	upCmd.AddCommand(authUpCmd)
+	downCmd.AddCommand(authDownCmd)
 	migrateCmd.AddCommand(downCmd)
 	migrateCmd.AddCommand(mversionCmd)
 	migrateCmd.AddCommand(nextCmd)
@@ -55,6 +57,10 @@ func Execute() {
 func MakeHandler() http.Handler {
 	n := middlewares.GetApp()
 	r := router.Get()
+	// if auth is enabled
+	if config.PrestConf.AuthEnabled {
+		r.HandleFunc("/auth", controllers.Auth).Methods("POST")
+	}
 	r.HandleFunc("/databases", controllers.GetDatabases).Methods("GET")
 	r.HandleFunc("/schemas", controllers.GetSchemas).Methods("GET")
 	r.HandleFunc("/tables", controllers.GetTables).Methods("GET")
@@ -69,6 +75,7 @@ func MakeHandler() http.Handler {
 	crudRoutes.HandleFunc("/{database}/{schema}/{table}", controllers.UpdateTable).Methods("PUT", "PATCH")
 	r.PathPrefix("/").Handler(negroni.New(
 		middlewares.AccessControl(),
+		middlewares.AuthMiddleware(),
 		negroni.Wrap(crudRoutes),
 	))
 	n.UseHandler(r)
diff --git a/config/config.go b/config/config.go
index c565395..98c9769 100644
--- a/config/config.go
+++ b/config/config.go
@@ -29,6 +29,13 @@ type AccessConf struct {
 
 // Prest basic config
 type Prest struct {
+	AuthEnabled      bool
+	AuthTable        string
+	AuthUsername     string
+	AuthPassword     string
+	AuthEncrypt      string
+	AuthMetadata     []string
+	AuthType         string
 	HTTPHost         string // HTTPHost Declare which http address the PREST used
 	HTTPPort         int    // HTTPPort Declare which http port the PREST used
 	PGHost           string
@@ -47,6 +54,7 @@ type Prest struct {
 	PGConnTimeout    int
 	JWTKey           string
 	JWTAlgo          string
+	JWTWhiteList     string
 	MigrationsPath   string
 	QueriesPath      string
 	AccessConf       AccessConf
@@ -82,6 +90,12 @@ func viperCfg() {
 	viper.AddConfigPath(dir)
 	viper.SetConfigName(file)
 	viper.SetConfigType("toml")
+	viper.SetDefault("auth.enabled", false)
+	viper.SetDefault("auth.username", "username")
+	viper.SetDefault("auth.password", "password")
+	viper.SetDefault("auth.table", "prest_users")
+	viper.SetDefault("auth.encrypt", "MD5")
+	viper.SetDefault("auth.type", "body")
 	viper.SetDefault("http.host", "0.0.0.0")
 	viper.SetDefault("http.port", 3000)
 	viper.SetDefault("pg.host", "127.0.0.1")
@@ -93,6 +107,7 @@ func viperCfg() {
 	viper.SetDefault("debug", false)
 	viper.SetDefault("jwt.default", true)
 	viper.SetDefault("jwt.algo", "HS256")
+	viper.SetDefault("jwt.whitelist", "/auth")
 	viper.SetDefault("cors.allowheaders", []string{"*"})
 	viper.SetDefault("cache.enable", true)
 	viper.SetDefault("context", "/")
@@ -132,6 +147,13 @@ func Parse(cfg *Prest) (err error) {
 			return
 		}
 	}
+	cfg.AuthEnabled = viper.GetBool("auth.enabled")
+	cfg.AuthTable = viper.GetString("auth.table")
+	cfg.AuthUsername = viper.GetString("auth.username")
+	cfg.AuthPassword = viper.GetString("auth.password")
+	cfg.AuthEncrypt = viper.GetString("auth.encrypt")
+	cfg.AuthMetadata = viper.GetStringSlice("auth.metadata")
+	cfg.AuthType = viper.GetString("auth.type")
 	cfg.HTTPHost = viper.GetString("http.host")
 	cfg.HTTPPort = viper.GetInt("http.port")
 	cfg.PGURL = viper.GetString("pg.url")
@@ -161,6 +183,7 @@ func Parse(cfg *Prest) (err error) {
 	cfg.PGConnTimeout = viper.GetInt("pg.conntimeout")
 	cfg.JWTKey = viper.GetString("jwt.key")
 	cfg.JWTAlgo = viper.GetString("jwt.algo")
+	cfg.JWTWhiteList = viper.GetString("jwt.whitelist")
 	cfg.MigrationsPath = viper.GetString("migrations")
 	cfg.AccessConf.Restrict = viper.GetBool("access.restrict")
 	cfg.QueriesPath = viper.GetString("queries.location")
diff --git a/controllers/auth.go b/controllers/auth.go
new file mode 100644
index 0000000..5cc5571
--- /dev/null
+++ b/controllers/auth.go
@@ -0,0 +1,143 @@
+package controllers
+
+import (
+	"crypto/md5"
+	"crypto/sha1"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	jwt "github.com/dgrijalva/jwt-go"
+	"github.com/prest/prest/config"
+	"github.com/prest/prest/controllers/auth"
+)
+
+// Response representation
+type Response struct {
+	LoggedUser interface{} `json:"user_info"`
+	Token      string      `json:"token"`
+}
+
+// RavensRequest representation
+type RavensRequest struct {
+	Type       string   `json:"type_of"`
+	Subject    string   `json:"subject"`
+	Recipients []string `json:"recipients"`
+	Sender     string   `json:"sender"`
+	SenderName string   `json:"sender_name"`
+	Content    string   `json:"content"`
+}
+
+// Login representation of data received in authentication
+type Login struct {
+	Username string `json:"username"`
+	Password string `json:"password"`
+}
+
+// Token for user
+func Token(u auth.User) (t string, err error) {
+	// add expiry time in configuration (in minute format, so we support the maximum need)
+	expireToken := time.Now().Add(time.Hour * 6).Unix()
+	claims := auth.Claims{
+		UserInfo: u,
+		StandardClaims: jwt.StandardClaims{
+			ExpiresAt: expireToken,
+			Id:        strconv.Itoa(u.ID),
+			IssuedAt:  time.Now().Unix(),
+			Issuer:    strconv.Itoa(u.ID),
+		},
+	}
+	tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	return tok.SignedString([]byte(config.PrestConf.JWTKey))
+}
+
+const unf = "user not found"
+
+// Auth controller
+func Auth(w http.ResponseWriter, r *http.Request) {
+	login := Login{}
+	switch config.PrestConf.AuthType {
+	// TODO: form support
+	case "body":
+		// to use body field authentication
+		dec := json.NewDecoder(r.Body)
+		dec.DisallowUnknownFields()
+		dec.Decode(&login)
+		break
+	case "basic":
+		// to use http basic authentication
+		var ok bool
+		login.Username, login.Password, ok = r.BasicAuth()
+		if !ok {
+			http.Error(w, unf, http.StatusBadRequest)
+			return
+		}
+		break
+	}
+
+	loggedUser, err := basicPasswordCheck(strings.ToLower(login.Username), login.Password)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusUnauthorized)
+		return
+	}
+	token, err := Token(loggedUser)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	resp := Response{
+		LoggedUser: loggedUser,
+		Token:      token,
+	}
+	err = json.NewEncoder(w).Encode(resp)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+// basicPasswordCheck
+func basicPasswordCheck(user, password string) (obj auth.User, err error) {
+	/**
+	table name, fields (user and password) and encryption must be defined in
+	the configuration file (toml)
+	by default this endpoint will not be available, it is necessary to activate
+	in the configuration file
+	*/
+	sc := config.PrestConf.Adapter.Query(getSelectQuery(), user, encrypt(password))
+	if sc.Err() != nil {
+		err = sc.Err()
+		return
+	}
+	n, err := sc.Scan(&obj)
+	if err != nil {
+		return
+	}
+	if n != 1 {
+		err = fmt.Errorf(unf)
+	}
+
+	return
+}
+
+// getSelectQuery create the query to authenticate the user
+func getSelectQuery() (query string) {
+	query = fmt.Sprintf(`SELECT * FROM %s WHERE %s=$1 AND %s=$2 LIMIT 1`, config.PrestConf.AuthTable, config.PrestConf.AuthUsername, config.PrestConf.AuthPassword)
+	return
+}
+
+// encrypt will apply the encryption algorithm to the password
+func encrypt(password string) (encrypted string) {
+	switch config.PrestConf.AuthEncrypt {
+	case "MD5":
+		encrypted = fmt.Sprintf("%x", md5.Sum([]byte(password)))
+		break
+	case "SHA1":
+		encrypted = fmt.Sprintf("%x", sha1.Sum([]byte(password)))
+		break
+	}
+	return
+}
diff --git a/controllers/auth/models.go b/controllers/auth/models.go
new file mode 100644
index 0000000..6ddcdb6
--- /dev/null
+++ b/controllers/auth/models.go
@@ -0,0 +1,17 @@
+package auth
+
+import "github.com/dgrijalva/jwt-go"
+
+// User logged in user representation
+type User struct {
+	ID       int         `json:"id"`
+	Name     string      `json:"name"`
+	Username string      `json:"username"`
+	Metadata interface{} `json:"metadata"`
+}
+
+// Claims JWT
+type Claims struct {
+	UserInfo User
+	jwt.StandardClaims
+}
diff --git a/docs/configurations/_index.en.md b/docs/configurations/_index.en.md
index d9f5f29..c755fb3 100644
--- a/docs/configurations/_index.en.md
+++ b/docs/configurations/_index.en.md
@@ -11,15 +11,21 @@ Via environment variables or via toml file.
 
 - PREST\_CONF
 - PREST\_HTTP_HOST (*default 0.0.0.0*)
-- PREST\_HTTP_PORT or PORT (PORT is cloud factor, **when declaring this variable overwritten PREST\_HTTP_PORT**, *default 3000*)
+- PREST\_HTTP_PORT or **PORT** (PORT is cloud factor, _when declaring this variable overwritten PREST\_HTTP_PORT, default 3000_)
 - PREST\_PG_HOST (*default 127.0.0.1*)
 - PREST\_PG_USER
 - PREST\_PG_PASS
 - PREST\_PG_DATABASE
 - PREST\_PG_PORT (*default 5432*)
-- PREST\_PG_URL or DATABASE\_URL (cloud factor, **when declaring this variable all the previous connection fields are overwritten**)
+- PREST\_PG_URL or **DATABASE\_URL** (cloud factor, _when declaring this variable all the previous connection fields are overwritten_)
 - PREST\_JWT_KEY
 - PREST\_JWT_ALGO
+- PREST\_AUTH_ENABLED (*default false*)
+- PREST\_AUTH_ENCRYPT (*default MD5*)
+- PREST\_AUTH_TYPE (*default body*)
+- PREST\_AUTH_TABLE (*default prest_users*)
+- PREST\_AUTH_USERNAME (*default username*)
+- PREST\_AUTH_PASSWORD (*default password*)
 
 
 ## TOML
@@ -41,6 +47,14 @@ port = 6000
 key = "secret"
 algo = "HS256"
 
+[auth]
+enabled = true
+type = "body"
+encrypt = "MD5"
+table = "prest_users"
+username = "username"
+password = "password"
+
 [pg]
 host = "127.0.0.1"
 user = "postgres"
@@ -59,7 +73,9 @@ sslrootcert = "./PATH"
 
 ## Authorization
 
-- JWT middleware is enabled by default. To disable JWT need to set default to false. Enabling debug mode will also disable it.
+### JWT
+
+JWT middleware is enabled by default. To disable JWT need to set default to false. Enabling debug mode will also disable it.
 
 ```toml
 [jwt]
@@ -81,7 +97,34 @@ The supported signing algorithms are:
 * The [ECDSA signing method](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm): `ES256`,`ES384`,`ES512`
 
 
-### SSL
+### Auth
+
+pREST has support in jwt token generation based on two fields (example user and password), being possible to use an existing table from your database to login configuring some parameters in the configuration file (or environment variable), _by default this feature is_ **disabled**.
+
+```toml
+[auth]
+enabled = true
+type = "body"
+encrypt = "MD5"
+table = "prest_users"
+username = "username"
+password = "password"
+```
+
+| Name | Description |
+| ---- | ----------- |
+| enabled | **Boolean** field that activates or deactivates token generation endpoint support |
+| type | Type that will receive the login, support for **body and http basic authentication** |
+| encrypt | Type of encryption used in password field, support for **MD5 and SHA1** |
+| table | Table name we will consult (query) |
+| username | User **field** that will be consulted - if your software uses email just abstract name username (at pREST code level it was necessary to define an internal standard) |
+| password | Password **field** that will be consulted |
+
+
+> to validate all endpoints with generated jwt token must be activated jwt option
+
+
+## SSL
 
 - There is 4 options to set on ssl mode:
 
@@ -92,7 +135,7 @@ The supported signing algorithms are:
 "verify-full" - # Always SSL (verify that the certification presented by the server was signed by a trusted CA and the server host name matches the one in the certificate)
 ```
 
-### Debug Mode
+## Debug Mode
 
 - Set environment variable `PREST_DEBUG` or `debug=true` on top of prest.toml file.
 
@@ -109,28 +152,31 @@ PREST_DEBUG=true
 PREST_MIGRATIONS
 
 # apply all available migrations
-prest migrate --url driver://url --path ./migrations up
+prestd migrate --url driver://url --path ./migrations up
 
 # roll back all migrations
-prest migrate --url driver://url --path ./migrations down
+prestd migrate --url driver://url --path ./migrations down
 
 # roll back the most recently applied migration, then run it again.
-prest migrate --url driver://url --path ./migrations redo
+prestd migrate --url driver://url --path ./migrations redo
 
 # run down and then up command
-prest migrate --url driver://url --path ./migrations reset
+prestd migrate --url driver://url --path ./migrations reset
 
 # show the current migration version
-prest migrate --url driver://url --path ./migrations version
+prestd migrate --url driver://url --path ./migrations version
 
 # apply the next n migrations
-prest migrate --url driver://url --path ./migrations next +1
-prest migrate --url driver://url --path ./migrations next +2
-prest migrate --url driver://url --path ./migrations next +n
+prestd migrate --url driver://url --path ./migrations next +1
+prestd migrate --url driver://url --path ./migrations next +2
+prestd migrate --url driver://url --path ./migrations next +n
 
 # roll back the previous n migrations
-prest migrate --url driver://url --path ./migrations next -1
-prest migrate --url driver://url --path ./migrations next -2
-prest migrate --url driver://url --path ./migrations next -n
+prestd migrate --url driver://url --path ./migrations next -1
+prestd migrate --url driver://url --path ./migrations next -2
+prestd migrate --url driver://url --path ./migrations next -n
 
+# create or remove default pREST authentication table
+prestd migrate up auth
+prestd migrate down auth
 ```
diff --git a/docs/query-statements/_index.en.md b/docs/query-statements/_index.en.md
index 9ea96f3..97a2aa3 100644
--- a/docs/query-statements/_index.en.md
+++ b/docs/query-statements/_index.en.md
@@ -5,6 +5,20 @@ weight: 12
 menu: main
 ---
 
+### Auth /auth - POST
+
+pREST has support in jwt token generation based on two fields (example user and password), being possible to use an existing table from your database to login configuring some parameters in the configuration file (or environment variable), _by default this feature is_ **disabled**.
+
+#### Basic Authentication
+```sh
+curl -i -X POST http://127.0.0.1:8000/auth -H "Content-Type: application/json" -d '{"username": "<username>", "password": "<password>"}'
+```
+
+#### Basic Authentication
+```sh
+curl -i -X POST http://127.0.0.1:8000/auth --user "<username>:<password>"
+```
+
 ### Filter (WHERE)
 
 ```
diff --git a/middlewares/middlewares.go b/middlewares/middlewares.go
index cbf619a..637127f 100644
--- a/middlewares/middlewares.go
+++ b/middlewares/middlewares.go
@@ -1,15 +1,18 @@
 package middlewares
 
 import (
+	"context"
 	"fmt"
+	"log"
 	"net/http"
 	"net/http/httptest"
 	"strconv"
 	"strings"
 
-	"github.com/auth0/go-jwt-middleware"
+	jwtmiddleware "github.com/auth0/go-jwt-middleware"
 	"github.com/dgrijalva/jwt-go"
 	"github.com/prest/prest/config"
+	"github.com/prest/prest/controllers/auth"
 	"github.com/urfave/negroni"
 )
 
@@ -24,6 +27,51 @@ func HandlerSet() negroni.Handler {
 	})
 }
 
+// AuthMiddleware handle request token validation
+func AuthMiddleware() negroni.Handler {
+	return negroni.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+		if config.PrestConf.AuthEnabled {
+			// extract authorization token
+			ts := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1)
+			if ts == "" {
+				err := fmt.Errorf("authorization token is empty")
+				http.Error(rw, err.Error(), http.StatusForbidden)
+				return
+			}
+
+			_, err := jwt.ParseWithClaims(ts, &auth.Claims{}, func(token *jwt.Token) (interface{}, error) {
+				// verify token sign method
+				if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+					return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+				}
+
+				// parse token claims
+				var claims *auth.Claims
+				if v, ok := token.Claims.(*auth.Claims); ok {
+					claims = v
+				} else {
+					return nil, fmt.Errorf("token invalid")
+				}
+
+				// pass user_info to the next handler
+				ctx := r.Context()
+				ctx = context.WithValue(ctx, "user_info", claims.UserInfo)
+				r = r.WithContext(ctx)
+
+				return []byte(config.PrestConf.JWTKey), nil
+			})
+
+			if err != nil {
+				http.Error(rw, err.Error(), http.StatusBadRequest)
+				return
+			}
+		}
+
+		// if auth isn't enabled
+		next(rw, r)
+	})
+}
+
 // AccessControl is a middleware to handle permissions on tables in pREST
 func AccessControl() negroni.Handler {
 	return negroni.HandlerFunc(func(rw http.ResponseWriter, rq *http.Request, next http.HandlerFunc) {
@@ -57,7 +105,25 @@ func JwtMiddleware(key string, algo string) negroni.Handler {
 		},
 		SigningMethod: jwt.GetSigningMethod(algo),
 	})
-	return negroni.HandlerFunc(jwtMiddleware.HandlerWithNext)
+
+	return negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+		match, err := MatchURL(r.URL.String())
+		if err != nil {
+			http.Error(w, fmt.Sprintf(`{"error": "%v"}`, err), http.StatusInternalServerError)
+			return
+		}
+		if match {
+			next(w, r)
+			return
+		}
+		err = jwtMiddleware.CheckJWT(w, r)
+		if err != nil {
+			log.Println("check jwt error", err.Error())
+			w.Write([]byte(fmt.Sprintf(`{"error": "%v"}`, err.Error())))
+			return
+		}
+		next(w, r)
+	})
 }
 
 // Cors middleware
diff --git a/middlewares/utils.go b/middlewares/utils.go
index 1cfeb7e..c84bcbf 100644
--- a/middlewares/utils.go
+++ b/middlewares/utils.go
@@ -6,9 +6,11 @@ import (
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"regexp"
 	"strings"
 
 	"github.com/clbanning/mxj/j2x"
+	"github.com/prest/prest/config"
 	"github.com/prest/prest/middlewares/statements"
 )
 
@@ -112,3 +114,14 @@ func checkCors(r *http.Request, origin []string) (allowed bool) {
 	}
 	return
 }
+
+// MatchURL matches the given url with a whitelist from config.core
+func MatchURL(url string) (match bool, err error) {
+	for _, exp := range strings.Fields(config.PrestConf.JWTWhiteList) {
+		match, err = regexp.Match(exp, []byte(url))
+		if match || err != nil {
+			return
+		}
+	}
+	return
+}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index aec5892..805f76a 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -295,3 +295,45 @@ func Test_portFromEnv(t *testing.T) {
 
 	os.Unsetenv("PORT")
 }
+
+func Test_Auth(t *testing.T) {
+	os.Setenv("PREST_CONF", "testdata/prest.toml")
+
+	viperCfg()
+	cfg := &Prest{}
+	err := Parse(cfg)
+	if err != nil {
+		t.Errorf("expected no errors, but got %v", err)
+	}
+
+	if cfg.AuthEnabled != false {
+		t.Errorf("expected auth.enabled to be: false, got: %v", cfg.AuthEnabled)
+	}
+
+	if cfg.AuthTable != "prest_users" {
+		t.Errorf("expected auth.table to be: prest_users, got: %s", cfg.AuthTable)
+	}
+
+	if cfg.AuthUsername != "username" {
+		t.Errorf("expected auth.username to be: username, got: %s", cfg.AuthUsername)
+	}
+
+	if cfg.AuthPassword != "password" {
+		t.Errorf("expected auth.password to be: password, got: %s", cfg.AuthPassword)
+	}
+
+	if cfg.AuthEncrypt != "MD5" {
+		t.Errorf("expected auth.encrypt to be: MD5, got: %s", cfg.AuthEncrypt)
+	}
+
+	metadata := []string{"first_name", "last_name", "last_login"}
+	if len(cfg.AuthMetadata) != len(metadata) {
+		t.Errorf("expected auth.metadata to be: %d, got: %d", len(cfg.AuthMetadata), len(metadata))
+	}
+
+	for i, v := range cfg.AuthMetadata {
+		if v != metadata[i] {
+			t.Errorf("expected auth.metadata field %d to be: %s, got: %s", i, v, metadata[i])
+		}
+	}
+}
diff --git a/config/testdata/prest.toml b/config/testdata/prest.toml
index 4188a68..a54dcca 100644
--- a/config/testdata/prest.toml
+++ b/config/testdata/prest.toml
@@ -3,6 +3,12 @@
 # [jwt]
 # key = "mysecretkey"
 
+[auth]
+table = "prest_users"
+username = "username"
+password = "password"
+metadata = ["first_name", "last_name", "last_login"]
+
 [http]
 port = 6000
 
@@ -71,4 +77,4 @@ restrict = true  # can access only the tables listed below
     [[access.tables]]
     name = "test_group_by_table"
     permissions = ["read"]
-    fields = ["id", "name", "age", "salary"]
\ No newline at end of file
+    fields = ["id", "name", "age", "salary"]
diff --git a/controllers/auth_test.go b/controllers/auth_test.go
new file mode 100644
index 0000000..934deab
--- /dev/null
+++ b/controllers/auth_test.go
@@ -0,0 +1,53 @@
+package controllers
+
+import (
+	"crypto/md5"
+	"crypto/sha1"
+	"fmt"
+	"testing"
+
+	"github.com/prest/prest/adapters/postgres"
+	"github.com/prest/prest/config"
+)
+
+func Test_basicPasswordCheck(t *testing.T) {
+	config.Load()
+	postgres.Load()
+
+	_, err := basicPasswordCheck("test@postgres.rest", "123456")
+	if err != nil {
+		t.Errorf("expected authenticated user, got: %s", err)
+	}
+}
+
+func Test_getSelectQuery(t *testing.T) {
+	config.Load()
+
+	expected := "SELECT * FROM prest_users WHERE username=$1 AND password=$2 LIMIT 1"
+	query := getSelectQuery()
+
+	if query != expected {
+		t.Errorf("expected query: %s, got: %s", expected, query)
+	}
+}
+
+func Test_encrypt(t *testing.T) {
+	config.Load()
+
+	pwd := "123456"
+	enc := encrypt(pwd)
+
+	md5Enc := fmt.Sprintf("%x", md5.Sum([]byte(pwd)))
+	if enc != md5Enc {
+		t.Errorf("expected encrypted password to be: %s, got: %s", enc, md5Enc)
+	}
+
+	config.PrestConf.AuthEncrypt = "SHA1"
+
+	enc = encrypt(pwd)
+
+	sha1Enc := fmt.Sprintf("%x", sha1.Sum([]byte(pwd)))
+	if enc != sha1Enc {
+		t.Errorf("expected encrypted password to be: %s, got: %s", enc, sha1Enc)
+	}
+}
diff --git a/controllers/testdata/prest.toml b/controllers/testdata/prest.toml
index b0695e2..9d21914 100644
--- a/controllers/testdata/prest.toml
+++ b/controllers/testdata/prest.toml
@@ -3,6 +3,12 @@
 # [jwt]
 # key = "mysecretkey"
 
+[auth]
+table = "prest_users"
+username = "username"
+password = "password"
+metadata = ["first_name", "last_name", "last_login"]
+
 [http]
 port = 6000
 
@@ -75,4 +81,4 @@ restrict = true  # can access only the tables listed below
     [[access.tables]]
     name = "test_group_by_table"
     permissions = ["read"]
-    fields = ["id", "name", "age", "salary"]
\ No newline at end of file
+    fields = ["id", "name", "age", "salary"]
diff --git a/controllers/testdata/schema.sql b/controllers/testdata/schema.sql
index b39e019..318e133 100644
--- a/controllers/testdata/schema.sql
+++ b/controllers/testdata/schema.sql
@@ -17,6 +17,7 @@ create table testjson(name text, data jsonb);
 create table testarray(id serial, data character varying(250)[]);
 create table test_empty_table(id serial, data character varying(250)[]);
 create table test_group_by_table(id serial, name text, age integer, salary int);
+CREATE TABLE prest_users(id serial, username text, password text);
 
 -- Inserts
 insert into test (name) values ('prest tester');
@@ -39,8 +40,9 @@ insert into test_group_by_table(name, age, salary) values ('joao', 20, 1250);
 insert into test_group_by_table(name, age, salary) values ('maria', 19, 3999);
 insert into test_group_by_table(name, age, salary) values ('gopher', 20, 100);
 insert into test_group_by_table(name, age, salary) values ('guitarra humana', 19, 3998);
+INSERT INTO prest_users(username, password) VALUES('test@postgres.rest', 'e10adc3949ba59abbe56e057f20f883e');
 
 -- Views
 create table table_to_view(id serial, name text, celphone text);
 insert into table_to_view (name, celphone) values ('gopher', '8888888');
-create view view_test as select name as player from table_to_view;
\ No newline at end of file
+create view view_test as select name as player from table_to_view;
diff --git a/middlewares/utils_test.go b/middlewares/utils_test.go
index 04a1b2d..b32165a 100644
--- a/middlewares/utils_test.go
+++ b/middlewares/utils_test.go
@@ -1,9 +1,11 @@
 package middlewares
 
 import (
-	"github.com/prest/prest/middlewares/statements"
 	"net/http"
 	"testing"
+
+	"github.com/prest/prest/config"
+	"github.com/prest/prest/middlewares/statements"
 )
 
 func Test_getVars(t *testing.T) {
@@ -56,3 +58,50 @@ func Test_checkCors(t *testing.T) {
 		t.Error("expected false, got true")
 	}
 }
+
+func TestMatchURL(t *testing.T) {
+	test := []struct {
+		Label        string
+		URL          string
+		JWTWhiteList string
+		match        bool
+	}{
+		{
+			Label:        "auth",
+			URL:          "/auth",
+			JWTWhiteList: `\/auth`,
+			match:        true,
+		},
+		{
+			Label:        "auth regex",
+			URL:          "/auth/any",
+			JWTWhiteList: `\/auth\/.*`,
+			match:        true,
+		},
+		{
+			Label:        "auth2 lock",
+			URL:          "/auth2",
+			JWTWhiteList: `\/auth`,
+			match:        true,
+		},
+		{
+			Label:        "multi allow",
+			URL:          "/auth",
+			JWTWhiteList: `\/auth \/databases`,
+			match:        true,
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.Label, func(t *testing.T) {
+			config.PrestConf.JWTWhiteList = tt.JWTWhiteList
+			match, err := MatchURL(tt.URL)
+			if err != nil {
+				t.Error(err)
+			}
+			if match != tt.match {
+				t.Errorf("expected %v, but got %v\n", tt.match, match)
+			}
+		})
+	}
+}
diff --git a/testdata/migrations_test.sh b/testdata/migrations_test.sh
index 6fd3d70..f63e47c 100644
--- a/testdata/migrations_test.sh
+++ b/testdata/migrations_test.sh
@@ -4,6 +4,6 @@ export PREST_MIGRATIONS=./testdata/migrations
 DB_HOST=${PREST_PG_HOST:-localhost}
 DB_USER=${PREST_PG_USER:-postgres}
 DB_PORT=${PREST_PG_PORT:-5438}
-DB_NAME=${PREST_PG_DATABASE:-prest} 
+DB_NAME=${PREST_PG_DATABASE:-prest}
 DB_URL="postgres://${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable"
 env go run ./cmd/prestd/main.go migrate --url="$DB_URL" up
diff --git a/testdata/prest.toml b/testdata/prest.toml
index fddf2c2..b0b815d 100644
--- a/testdata/prest.toml
+++ b/testdata/prest.toml
@@ -3,6 +3,12 @@
 # [jwt]
 # key = "mysecretkey"
 
+[auth]
+table = "prest_users"
+username = "username"
+password = "password"
+metadata = ["first_name", "last_name", "last_login"]
+
 [http]
 port = 6000
 
diff --git a/testdata/schema.sh b/testdata/schema.sh
index 0931b7d..84651bd 100644
--- a/testdata/schema.sh
+++ b/testdata/schema.sh
@@ -2,9 +2,9 @@
 DB_HOST=${PREST_PG_HOST:-localhost}
 DB_USER=${PREST_PG_USER:-postgres}
 DB_PORT=${PREST_PG_PORT:-5432}
-DB_NAME=${PREST_PG_DATABASE:-prest} 
+DB_NAME=${PREST_PG_DATABASE:-prest}
 
 psql -h $DB_HOST -p $DB_PORT -U $DB_USER -c "DROP DATABASE IF EXISTS $DB_NAME"
 psql -h $DB_HOST -p $DB_PORT -U $DB_USER -c "create database $DB_NAME;"
 
-psql -d $DB_NAME -h $DB_HOST -p $DB_PORT -U $DB_USER -f testdata/schema.sql
\ No newline at end of file
+psql -d $DB_NAME -h $DB_HOST -p $DB_PORT -U $DB_USER -f testdata/schema.sql
diff --git a/testdata/schema.sql b/testdata/schema.sql
index 93f118b..1e14c74 100644
--- a/testdata/schema.sql
+++ b/testdata/schema.sql
@@ -17,6 +17,7 @@ CREATE TABLE testjson(name text, data jsonb);
 CREATE TABLE testarray(id serial, data character varying(250)[]);
 CREATE TABLE test_empty_table(id serial, data character varying(250)[]);
 CREATE TABLE test_group_by_table(id serial, name text, age integer, salary int);
+CREATE TABLE prest_users(id serial, username text, password text);
 
 -- Inserts
 INSERT INTO test (name) VALUES ('prest tester');
@@ -38,6 +39,7 @@ INSERT INTO test_group_by_table(name, age, salary) VALUES('joao', 20, 1250);
 INSERT INTO test_group_by_table(name, age, salary) VALUES('maria', 19, 3999);
 INSERT INTO test_group_by_table(name, age, salary) VALUES('gopher', 20, 100);
 INSERT INTO test_group_by_table(name, age, salary) VALUES('guitarra humana', 19, 3998);
+INSERT INTO prest_users(username, password) VALUES('test@postgres.rest', 'e10adc3949ba59abbe56e057f20f883e');
 
 -- Views
 CREATE TABLE table_to_view(id serial, name text, celphone text);