Auth: Enhance password validation.

Increase minimum length to 20.
Check for digits, lower/upper case and special characters.

Change-Id: I7837780716487843f01ed2af97fcf30505d27ef7
diff --git a/core/auth/ui/api.go b/core/auth/ui/api.go
index ab0ea76..6fb7426 100644
--- a/core/auth/ui/api.go
+++ b/core/auth/ui/api.go
@@ -5,6 +5,8 @@
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
+	"unicode"
 
 	"github.com/gorilla/mux"
 )
@@ -37,22 +39,20 @@
 	return s.serv.ListenAndServe()
 }
 
-const identityCreateTmpl = `
-{
-  "credentials": {
-    "password": {
-      "config": {
-        "password": "%s"
-      }
-    }
-  },
-  "schema_id": "user",
-  "state": "active",
-  "traits": {
-    "username": "%s"
-  }
+type kratosIdentityCreateReq struct {
+	Credentials struct {
+		Password struct {
+			Config struct {
+				Password string `json:"password"`
+			} `json:"config"`
+		} `json:"password"`
+	} `json:"credentials"`
+	SchemaID string `json:"schema_id"`
+	State    string `json:"state"`
+	Traits   struct {
+		Username string `json:"username"`
+	} `json:"traits"`
 }
-`
 
 type identityCreateReq struct {
 	Username string `json:"username,omitempty"`
@@ -92,8 +92,26 @@
 
 func validatePassword(password string) []ValidationError {
 	var errors []ValidationError
-	if len(password) < 6 {
-		errors = append(errors, ValidationError{"password", "Password must be at least 6 characters long."})
+	if len(password) < 20 {
+		errors = append(errors, ValidationError{"password", "Password must be at least 20 characters long."})
+	}
+	digit := false
+	lowerCase := false
+	upperCase := false
+	special := false
+	for _, c := range password {
+		if unicode.IsDigit(c) {
+			digit = true
+		} else if unicode.IsLower(c) {
+			lowerCase = true
+		} else if unicode.IsUpper(c) {
+			upperCase = true
+		} else if strings.Contains(" !\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~", string(c)) {
+			special = true
+		}
+	}
+	if !digit || !lowerCase || !upperCase || !special {
+		errors = append(errors, ValidationError{"password", "Password must contain at least one ditig, lower/upper and special character"})
 	}
 	// TODO other validations
 	return errors
@@ -122,8 +140,16 @@
 		replyWithErrors(w, allErrors)
 		return
 	}
+	var kreq kratosIdentityCreateReq
+	kreq.Credentials.Password.Config.Password = req.Password
+	kreq.SchemaID = "user"
+	kreq.State = "active"
+	kreq.Traits.Username = req.Username
 	var buf bytes.Buffer
-	fmt.Fprintf(&buf, identityCreateTmpl, req.Password, req.Username)
+	if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	resp, err := http.Post(s.identitiesEndpoint(), "application/json", &buf)
 	if err != nil {
 		http.Error(w, "failed", http.StatusInternalServerError)
diff --git a/core/auth/ui/api_test.go b/core/auth/ui/api_test.go
new file mode 100644
index 0000000..90e64be
--- /dev/null
+++ b/core/auth/ui/api_test.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"testing"
+)
+
+func TestPasswordInvalid(t *testing.T) {
+	errs := validatePassword("foobar")
+	if len(errs) != 2 {
+		t.Fatal(errs)
+	}
+}
+
+func TestPasswordValid(t *testing.T) {
+	errs := validatePassword("foBa2r-gdkjS1-SA0120")
+	if len(errs) != 0 {
+		t.Fatal(errs)
+	}
+}
diff --git a/core/auth/ui/go.mod b/core/auth/ui/go.mod
index 49c33b1..0930937 100644
--- a/core/auth/ui/go.mod
+++ b/core/auth/ui/go.mod
@@ -1,8 +1,19 @@
 module github.com/giolekva/pcloud/core/auth/ui
 
-go 1.16
+go 1.20
 
 require (
 	github.com/gorilla/mux v1.8.0
 	github.com/itaysk/regogo v0.0.0-20200423164851-e9433c1fe5a7
 )
+
+require (
+	github.com/OneOfOne/xxhash v1.2.7 // indirect
+	github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4 // indirect
+	github.com/gobwas/glob v0.2.3 // indirect
+	github.com/open-policy-agent/opa v0.18.0 // indirect
+	github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0 // indirect
+	github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect
+	github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b // indirect
+	gopkg.in/yaml.v2 v2.2.1 // indirect
+)