diff --git a/addy.go b/addy.go
index c2878e1..7a01e9a 100644
--- a/addy.go
+++ b/addy.go
@@ -12,6 +12,7 @@ import (
"log"
"net/http"
"os"
+ "slices"
"strings"
)
@@ -134,14 +135,7 @@ func addyAliasAPI(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Alias creation is not configured", http.StatusServiceUnavailable)
return
}
- domainAllowed := false
- for _, d := range allowedAliasDomains {
- if body.Domain == d {
- domainAllowed = true
- break
- }
- }
- if !domainAllowed {
+ if !slices.Contains(allowedAliasDomains, body.Domain) {
http.Error(w, "Domain not allowed", http.StatusBadRequest)
return
}
diff --git a/altcha.go b/altcha.go
new file mode 100644
index 0000000..ea5e50b
--- /dev/null
+++ b/altcha.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "net/http"
+
+ goaltcha "github.com/k42-software/go-altcha"
+ altchahttp "github.com/k42-software/go-altcha/http"
+)
+
+func serveAltchaJS(w http.ResponseWriter, r *http.Request) {
+ altchahttp.ServeJavascript(w, r)
+}
+
+func serveAltchaChallenge(w http.ResponseWriter, r *http.Request) {
+ challenge := goaltcha.NewChallenge()
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "private, no-cache, no-store, must-revalidate")
+ _, _ = w.Write([]byte(challenge.Encode()))
+}
+
+func validateAltcha(r *http.Request) bool {
+ encoded := r.PostFormValue("altcha")
+ if encoded == "" {
+ return false
+ }
+ return goaltcha.ValidateResponse(encoded, true)
+}
diff --git a/change.go b/change.go
index 8d1b32f..d915aba 100644
--- a/change.go
+++ b/change.go
@@ -33,7 +33,7 @@ func checkPasswdConstraint(password string) error {
func changePassword(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && !changeLimiter.Allow(remoteIP(r)) {
csrfToken, _ := setCSRFToken(w)
- displayTmplError(w, http.StatusTooManyRequests, "change.html", map[string]interface{}{"error": "Too many requests. Please try again later.", "csrf_token": csrfToken})
+ displayTmplError(w, http.StatusTooManyRequests, "change.html", map[string]any{"error": "Too many requests. Please try again later.", "csrf_token": csrfToken})
return
}
@@ -43,19 +43,25 @@ func changePassword(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
- displayTmpl(w, "change.html", map[string]interface{}{"csrf_token": csrfToken})
+ displayTmpl(w, "change.html", map[string]any{"csrf_token": csrfToken})
return
}
if !validateCSRF(r) {
csrfToken, _ := setCSRFToken(w)
- displayTmplError(w, http.StatusForbidden, "change.html", map[string]interface{}{"error": "Invalid or missing CSRF token. Please try again.", "csrf_token": csrfToken})
+ displayTmplError(w, http.StatusForbidden, "change.html", map[string]any{"error": "Invalid or missing CSRF token. Please try again.", "csrf_token": csrfToken})
+ return
+ }
+
+ if !validateAltcha(r) {
+ csrfToken, _ := setCSRFToken(w)
+ displayTmplError(w, http.StatusForbidden, "change.html", map[string]any{"error": "Invalid or missing altcha response. Please try again.", "csrf_token": csrfToken})
return
}
renderError := func(status int, msg string) {
csrfToken, _ := setCSRFToken(w)
- displayTmplError(w, status, "change.html", map[string]interface{}{"error": msg, "csrf_token": csrfToken})
+ displayTmplError(w, status, "change.html", map[string]any{"error": msg, "csrf_token": csrfToken})
}
// Check the two new passwords are identical
diff --git a/csrf.go b/csrf.go
index a94be83..42f46d6 100644
--- a/csrf.go
+++ b/csrf.go
@@ -25,6 +25,7 @@ func setCSRFToken(w http.ResponseWriter) (string, error) {
Path: "/",
HttpOnly: false, // must be readable via form hidden field comparison
SameSite: http.SameSiteStrictMode,
+ Secure: !devMode,
})
return token, nil
}
diff --git a/go.mod b/go.mod
index 17e467e..eb358eb 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,8 @@ require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/k42-software/go-altcha v0.1.1
+ github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.36.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
)
diff --git a/go.sum b/go.sum
index ea772ab..318fb83 100644
--- a/go.sum
+++ b/go.sum
@@ -41,6 +41,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/k42-software/go-altcha v0.1.1 h1:vfA+0+0gr7jK4vp21Q7xvEpIjDsx8PqzxS0obgIToQs=
+github.com/k42-software/go-altcha v0.1.1/go.mod h1:2aX+0PkUSI0YPDVfjapZeuGELWt8ugEXkg8gr6QejMU=
+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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/login.go b/login.go
index cd72b3c..63af618 100644
--- a/login.go
+++ b/login.go
@@ -2,8 +2,6 @@ package main
import (
"fmt"
- "html"
- "html/template"
"log"
"net/http"
"net/url"
@@ -12,6 +10,69 @@ import (
"github.com/go-ldap/ldap/v3"
)
+type profileField struct {
+ Name string
+ Label string
+ Value string
+}
+
+type profileAlias struct {
+ Value string
+ URLSafe string
+ ElemID string
+ Token string
+}
+
+var ldapLabels = map[string]string{
+ "cn": "Full name",
+ "uid": "Username",
+ "givenName": "First name",
+ "sn": "Last name",
+ "displayName": "Display name",
+ "telephoneNumber": "Phone",
+ "mobile": "Mobile",
+ "employeeNumber": "Employee ID",
+ "o": "Organization",
+ "ou": "Department",
+ "title": "Title",
+ "description": "Description",
+ "labeledURI": "Website",
+}
+
+// ldapSkip lists attributes that should never be shown to the user.
+var ldapSkip = map[string]bool{
+ "userPassword": true,
+ "krbPrincipalKey": true,
+ "objectClass": true,
+ "entryUUID": true,
+ "entryDN": true,
+ "structuralObjectClass": true,
+ "hasSubordinates": true,
+ "krbExtraData": true,
+}
+
+// isGeneratedAlias returns true for auto-generated alias local parts:
+// exactly 10 characters and containing at least one digit or uppercase letter,
+// which distinguishes them from plain words like "postmaster" or "abonnement".
+func isGeneratedAlias(local string) bool {
+ if len(local) != 10 {
+ return false
+ }
+ for _, c := range local {
+ if c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' {
+ return true
+ }
+ }
+ return false
+}
+
+func ldapLabel(name string) string {
+ if l, ok := ldapLabels[name]; ok {
+ return l
+ }
+ return name
+}
+
func login(login string, password string) ([]*ldap.EntryAttribute, error) {
conn, err := myLDAP.Connect()
if err != nil || conn == nil {
@@ -44,42 +105,71 @@ func login(login string, password string) ([]*ldap.EntryAttribute, error) {
func tryLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
- displayTmpl(w, "login.html", map[string]interface{}{})
+ displayTmpl(w, "login.html", map[string]any{})
return
}
if !authLimiter.Allow(remoteIP(r)) {
- displayTmplError(w, http.StatusTooManyRequests, "login.html", map[string]interface{}{"error": "Too many login attempts. Please try again later."})
+ displayTmplError(w, http.StatusTooManyRequests, "login.html", map[string]any{"error": "Too many login attempts. Please try again later."})
return
}
- if entries, err := login(r.PostFormValue("login"), r.PostFormValue("password")); err != nil {
- log.Println(err)
- displayTmplError(w, http.StatusInternalServerError, "login.html", map[string]interface{}{"error": err.Error()})
- } else {
- apiToken := AddyAPIToken(r.PostFormValue("login"))
+ if !validateAltcha(r) {
+ displayTmplError(w, http.StatusForbidden, "login.html", map[string]any{"error": "Invalid or missing altcha response. Please try again."})
+ return
+ }
- cnt := "
"
- for _, e := range entries {
- for i, v := range e.Values {
- safeName := html.EscapeString(e.Name)
- safeVal := html.EscapeString(v)
- elemID := fmt.Sprintf("mailAlias-%d", i)
- if e.Name == "userPassword" || e.Name == "krbPrincipalKey" {
- cnt += "- " + safeName + ": [...]
"
- } else if e.Name == "mailAlias" && len(strings.SplitN(v, "@", 2)[0]) == 10 {
- safeURL := url.PathEscape(v)
- safeToken := html.EscapeString(apiToken)
- safeElemID := html.EscapeString(elemID)
- cnt += `- ` + safeName + `: ` + safeVal +
- `
`
- } else {
- cnt += "- " + safeName + ": " + safeVal + "
"
- }
+ loginName := r.PostFormValue("login")
+ entries, err := login(loginName, r.PostFormValue("password"))
+ if err != nil {
+ log.Println(err)
+ displayTmplError(w, http.StatusInternalServerError, "login.html", map[string]any{"error": err.Error()})
+ return
+ }
+
+ apiToken := AddyAPIToken(loginName)
+ var fields []profileField
+ var emails []string
+ var aliases []profileAlias
+ aliasIdx := 0
+
+ for _, e := range entries {
+ if ldapSkip[e.Name] {
+ continue
+ }
+ for _, v := range e.Values {
+ switch {
+ case e.Name == "mail":
+ emails = append(emails, v)
+ case e.Name == "mailAlias" && isGeneratedAlias(strings.SplitN(v, "@", 2)[0]):
+ elemID := fmt.Sprintf("alias-%d", aliasIdx)
+ aliasIdx++
+ aliases = append(aliases, profileAlias{
+ Value: v,
+ URLSafe: url.PathEscape(v),
+ ElemID: elemID,
+ Token: apiToken,
+ })
+ case e.Name == "mailAlias":
+ emails = append(emails, v)
+ default:
+ fields = append(fields, profileField{
+ Name: e.Name,
+ Label: ldapLabel(e.Name),
+ Value: v,
+ })
}
}
- displayTmpl(w, "message.html", map[string]interface{}{"details": template.HTML(`Login ok
Here are the information we have about you:` + cnt + "
To use our Addy.io compatible API, use the following token: " + html.EscapeString(apiToken) + "
")})
}
+
+ displayTmpl(w, "profile.html", map[string]any{
+ "login": loginName,
+ "fields": fields,
+ "emails": emails,
+ "aliases": aliases,
+ "api_token": apiToken,
+ "card_wide": true,
+ })
}
func httpBasicAuth(w http.ResponseWriter, r *http.Request) {
@@ -102,7 +192,7 @@ func httpBasicAuth(w http.ResponseWriter, r *http.Request) {
for _, e := range entries {
for _, v := range e.Values {
if e.Name != "userPassword" {
- w.Write([]byte(fmt.Sprintf("%s: %s", e.Name, v)))
+ fmt.Fprintf(w, "%s: %s", e.Name, v)
}
}
}
diff --git a/lost.go b/lost.go
index 250ac42..7afe093 100644
--- a/lost.go
+++ b/lost.go
@@ -88,7 +88,7 @@ func lostPasswordToken(conn *LDAPConn, login string) (string, string, error) {
func lostPassword(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && !lostLimiter.Allow(remoteIP(r)) {
- displayTmplError(w, http.StatusTooManyRequests, "lost.html", map[string]interface{}{"error": "Too many requests. Please try again later."})
+ displayTmplError(w, http.StatusTooManyRequests, "lost.html", map[string]any{"error": "Too many requests. Please try again later."})
return
}
@@ -98,12 +98,17 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
- displayTmpl(w, "lost.html", map[string]interface{}{"csrf_token": csrfToken})
+ displayTmpl(w, "lost.html", map[string]any{"csrf_token": csrfToken})
return
}
if !validateCSRF(r) {
- displayTmplError(w, http.StatusForbidden, "lost.html", map[string]interface{}{"error": "Invalid or missing CSRF token. Please try again."})
+ displayTmplError(w, http.StatusForbidden, "lost.html", map[string]any{"error": "Invalid or missing CSRF token. Please try again."})
+ return
+ }
+
+ if !validateAltcha(r) {
+ displayTmplError(w, http.StatusForbidden, "lost.html", map[string]any{"error": "Invalid or missing altcha response. Please try again."})
return
}
@@ -111,7 +116,7 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
conn, err := myLDAP.Connect()
if err != nil || conn == nil {
log.Println(err)
- displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]interface{}{"error": "Unable to process your request. Please try again later."})
+ displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to process your request. Please try again later."})
return
}
@@ -162,7 +167,7 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
s, err = d.Dial()
if err != nil {
log.Println("Unable to connect to email server: " + err.Error())
- displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]interface{}{"error": "Unable to send password recovery email. Please try again later."})
+ displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to send password recovery email. Please try again later."})
return
}
} else {
@@ -197,7 +202,7 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
if err := gomail.Send(s, m); err != nil {
log.Println("Unable to send email: " + err.Error())
- displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]interface{}{"error": "Unable to send password recovery email. Please try again later."})
+ displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to send password recovery email. Please try again later."})
return
}
diff --git a/main.go b/main.go
index 843dadf..32a1124 100644
--- a/main.go
+++ b/main.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"flag"
"fmt"
- "io/ioutil"
+ "io"
"log"
"net/http"
"net/url"
@@ -18,6 +18,9 @@ import (
)
var myPublicURL = "https://ldap.nemunai.re"
+var devMode bool
+var brandName = "chldapasswd"
+var brandLogo = ""
// dockerRegistrySecret is required for X-Special-Auth anonymous access.
// If empty, the feature is disabled.
@@ -80,9 +83,24 @@ func main() {
var baseURL = flag.String("baseurl", "/", "URL prepended to each URL")
var configfile = flag.String("config", "", "path to the configuration file")
var publicURL = flag.String("public-url", myPublicURL, "Public base URL used in password reset emails")
+ var dev = flag.Bool("dev", false, "Development mode: disables HSTS and cookie Secure flag for local HTTP testing")
+ var bname = flag.String("brand-name", "chldapasswd", "Brand name displayed in the UI")
+ var blogo = flag.String("brand-logo", "", "URL of brand logo displayed in the UI (added to CSP img-src)")
flag.Parse()
myPublicURL = *publicURL
+ devMode = *dev
+ brandName = *bname
+ brandLogo = *blogo
+ if val, ok := os.LookupEnv("BRAND_NAME"); ok {
+ brandName = val
+ }
+ if val, ok := os.LookupEnv("BRAND_LOGO"); ok {
+ brandLogo = val
+ }
+ if devMode {
+ log.Println("WARNING: running in development mode — security features relaxed, do not use in production")
+ }
// Sanitize options
log.Println("Checking paths...")
@@ -98,7 +116,7 @@ func main() {
if configfile != nil && *configfile != "" {
if fd, err := os.Open(*configfile); err != nil {
log.Fatal(err)
- } else if cnt, err := ioutil.ReadAll(fd); err != nil {
+ } else if cnt, err := io.ReadAll(fd); err != nil {
log.Fatal(err)
} else if err := json.Unmarshal(cnt, &myLDAP); err != nil {
log.Fatal(err)
@@ -131,7 +149,7 @@ func main() {
if val, ok := os.LookupEnv("LDAP_SERVICE_PASSWORD_FILE"); ok {
if fd, err := os.Open(val); err != nil {
log.Fatal(err)
- } else if cnt, err := ioutil.ReadAll(fd); err != nil {
+ } else if cnt, err := io.ReadAll(fd); err != nil {
log.Fatal(err)
} else {
myLDAP.ServicePassword = string(cnt)
@@ -213,6 +231,9 @@ func main() {
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
// Register handlers
+ http.HandleFunc(fmt.Sprintf("GET %s/altcha.min.js", *baseURL), serveAltchaJS)
+ http.HandleFunc(fmt.Sprintf("GET %s/style.css", *baseURL), serveStyleCSS)
+ http.HandleFunc(fmt.Sprintf("GET %s/altcha-challenge", *baseURL), serveAltchaChallenge)
http.HandleFunc(fmt.Sprintf("%s/{$}", *baseURL), changePassword)
http.HandleFunc(fmt.Sprintf("POST %s/api/v1/aliases", *baseURL), addyAliasAPI)
http.HandleFunc(fmt.Sprintf("DELETE %s/api/v1/aliases/{alias}", *baseURL), addyAliasAPIDelete)
@@ -231,7 +252,8 @@ func main() {
go func() {
log.Fatal(srv.ListenAndServe())
}()
- log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
+ log.Printf("Using LDAP server at %s:%d (baseDN: %s)", myLDAP.Host, myLDAP.Port, myLDAP.BaseDN)
+ log.Printf("Ready, listening on %s", *bind)
// Wait shutdown signal
<-interrupt
diff --git a/reset.go b/reset.go
index c2172b0..a65c27c 100644
--- a/reset.go
+++ b/reset.go
@@ -16,7 +16,7 @@ func resetPassword(w http.ResponseWriter, r *http.Request) {
return
}
- base := map[string]interface{}{
+ base := map[string]any{
"login": r.URL.Query().Get("l"),
"token": r.URL.Query().Get("t"),
}
@@ -44,6 +44,11 @@ func resetPassword(w http.ResponseWriter, r *http.Request) {
return
}
+ if !validateAltcha(r) {
+ renderError(http.StatusForbidden, "Invalid or missing altcha response. Please try again.")
+ return
+ }
+
// Check the two new passwords are identical
if r.PostFormValue("newpassword") != r.PostFormValue("new2password") {
renderError(http.StatusNotAcceptable, "New passwords are not identical. Please retry.")
diff --git a/static.go b/static.go
index e808fe1..6c5f316 100644
--- a/static.go
+++ b/static.go
@@ -5,6 +5,8 @@ import (
"html/template"
"log"
"net/http"
+ "net/url"
+ "strings"
)
func securityHeaders(next http.Handler) http.Handler {
@@ -12,8 +14,19 @@ func securityHeaders(next http.Handler) http.Handler {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
- w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline' https://stackpath.bootstrapcdn.com; style-src https://stackpath.bootstrapcdn.com; img-src 'self'; font-src https://stackpath.bootstrapcdn.com")
- w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+
+ imgSrc := "'self' data:"
+ if strings.HasPrefix(brandLogo, "http://") || strings.HasPrefix(brandLogo, "https://") {
+ if u, err := url.Parse(brandLogo); err == nil {
+ imgSrc += " " + u.Scheme + "://" + u.Host
+ }
+ }
+ csp := "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline'; style-src 'self' 'sha256-W6z8OR2iqpPyNGe72eRXH58H75H3UVJDuwHoKA6pX98='; img-src " + imgSrc + "; worker-src blob:"
+ w.Header().Set("Content-Security-Policy", csp)
+
+ if !devMode {
+ w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+ }
next.ServeHTTP(w, r)
})
}
@@ -21,7 +34,26 @@ func securityHeaders(next http.Handler) http.Handler {
//go:embed all:static
var assets embed.FS
-func displayTmpl(w http.ResponseWriter, page string, vars map[string]interface{}) {
+func serveStyleCSS(w http.ResponseWriter, r *http.Request) {
+ data, err := assets.ReadFile("static/style.css")
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "text/css; charset=utf-8")
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+ w.Write(data)
+}
+
+func displayTmpl(w http.ResponseWriter, page string, vars map[string]any) {
+ if vars == nil {
+ vars = map[string]any{}
+ }
+ vars["brand_name"] = brandName
+ if brandLogo != "" {
+ vars["brand_logo"] = brandLogo
+ }
+
data, err := assets.ReadFile("static/" + page)
if err != nil {
log.Fatalf("Unable to find %q: %s", page, err.Error())
@@ -43,7 +75,7 @@ func displayTmpl(w http.ResponseWriter, page string, vars map[string]interface{}
tpl.ExecuteTemplate(w, "page", vars)
}
-func displayTmplError(w http.ResponseWriter, statusCode int, page string, vars map[string]interface{}) {
+func displayTmplError(w http.ResponseWriter, statusCode int, page string, vars map[string]any) {
w.WriteHeader(statusCode)
displayTmpl(w, page, vars)
}
@@ -56,5 +88,5 @@ func displayMsg(w http.ResponseWriter, msg string, statusCode int) {
label = "message"
}
- displayTmpl(w, "message.html", map[string]interface{}{label: msg})
+ displayTmpl(w, "message.html", map[string]any{label: msg})
}
diff --git a/static/change.html b/static/change.html
index 60ffe7e..5b6fcaf 100644
--- a/static/change.html
+++ b/static/change.html
@@ -1,46 +1,46 @@
-{{template "header"}}
- Change your password Fill the following fields!
+{{template "header" .}}
+ Change your password
+ Fill the following fields!