Auth: Add page to change password.

Configure launcher as a default return to address.

Use standard X-Forwarded-User instead of custom X-User header.
Add X-Forwarded-UserId header holding user unique identificator.

Change-Id: Ib2e6329ba9fb91d2cc9a86b0c5fc78898769e3b8
diff --git a/core/auth/ui/main.go b/core/auth/ui/main.go
index 3f77bc8..73561e7 100644
--- a/core/auth/ui/main.go
+++ b/core/auth/ui/main.go
@@ -24,11 +24,10 @@
 var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
 var hydra = flag.String("hydra", "hydra.pcloud", "Hydra admin server address")
 var emailDomain = flag.String("email-domain", "lekva.me", "Email domain")
-
 var apiPort = flag.Int("api-port", 8081, "API Port to listen on")
 var kratosAPI = flag.String("kratos-api", "", "Kratos API address")
-
 var enableRegistration = flag.Bool("enable-registration", false, "If true account registration will be enabled")
+var defaultReturnTo = flag.String("default-return-to", "", "Default redirect address after login")
 
 var ErrNotLoggedIn = errors.New("Not logged in")
 
@@ -39,10 +38,12 @@
 var static embed.FS
 
 type Templates struct {
-	WhoAmI   *template.Template
-	Register *template.Template
-	Login    *template.Template
-	Consent  *template.Template
+	WhoAmI                *template.Template
+	Register              *template.Template
+	Login                 *template.Template
+	Consent               *template.Template
+	ChangePassword        *template.Template
+	ChangePasswordSuccess *template.Template
 }
 
 func ParseTemplates(fs embed.FS) (*Templates, error) {
@@ -73,7 +74,15 @@
 	if err != nil {
 		return nil, err
 	}
-	return &Templates{whoami, register, login, consent}, nil
+	changePassword, err := parse("templates/change-password.html")
+	if err != nil {
+		return nil, err
+	}
+	changePasswordSuccess, err := parse("templates/change-password-success.html")
+	if err != nil {
+		return nil, err
+	}
+	return &Templates{whoami, register, login, consent, changePassword, changePasswordSuccess}, nil
 }
 
 type Server struct {
@@ -83,15 +92,25 @@
 	hydra              *HydraClient
 	tmpls              *Templates
 	enableRegistration bool
+	api                *APIServer
+	defaultReturnTo    string
 }
 
-func NewServer(port int, kratos string, hydra *HydraClient, tmpls *Templates, enableRegistration bool) *Server {
+func NewServer(
+	port int,
+	kratos string,
+	hydra *HydraClient,
+	tmpls *Templates,
+	enableRegistration bool,
+	api *APIServer,
+	defaultReturnTo string,
+) *Server {
 	r := mux.NewRouter()
 	serv := &http.Server{
 		Addr:    fmt.Sprintf(":%d", port),
 		Handler: r,
 	}
-	return &Server{r, serv, kratos, hydra, tmpls, enableRegistration}
+	return &Server{r, serv, kratos, hydra, tmpls, enableRegistration, api, defaultReturnTo}
 }
 
 func cacheControlWrapper(h http.Handler) http.Handler {
@@ -115,6 +134,8 @@
 	s.r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
 	s.r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
 	s.r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
+	s.r.Path("/change-password").Methods("POST").HandlerFunc(s.changePassword)
+	s.r.Path("/change-password").Methods("GET").HandlerFunc(s.changePasswordForm)
 	s.r.Path("/").HandlerFunc(s.whoami)
 	return s.serv.ListenAndServe()
 }
@@ -225,7 +246,7 @@
 		return
 	}
 	if challenge, ok := r.Form["login_challenge"]; ok {
-		username, err := getWhoAmIFromKratos(r.Cookies())
+		_, username, err := getWhoAmIFromKratos(r.Cookies())
 		if err != nil && err != ErrNotLoggedIn {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -246,7 +267,10 @@
 			HttpOnly: true,
 		})
 	}
-	returnTo := r.Form.Get("return_to")
+	returnTo := r.FormValue("return_to")
+	if returnTo == "" && s.defaultReturnTo != "" {
+		returnTo = s.defaultReturnTo
+	}
 	flow, ok := r.Form["flow"]
 	if !ok {
 		addr := s.kratos + "/self-service/login/browser"
@@ -358,10 +382,10 @@
 	return lr.LogoutURL, nil
 }
 
-func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
+func getWhoAmIFromKratos(cookies []*http.Cookie) (string, string, error) {
 	jar, err := cookiejar.New(nil)
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	client := &http.Client{
 		Jar: jar,
@@ -371,25 +395,32 @@
 	}
 	b, err := url.Parse(*kratos + "/sessions/whoami")
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	client.Jar.SetCookies(b, cookies)
 	resp, err := client.Get(*kratos + "/sessions/whoami")
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	respBody, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	username, err := regogo.Get(string(respBody), "input.identity.traits.username")
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	if username.String() == "" {
-		return "", ErrNotLoggedIn
+		return "", "", ErrNotLoggedIn
 	}
-	return username.String(), nil
+	id, err := regogo.Get(string(respBody), "input.identity.id")
+	if err != nil {
+		return "", "", err
+	}
+	if id.String() == "" {
+		return "", "", ErrNotLoggedIn
+	}
+	return id.String(), username.String(), nil
 
 }
 
@@ -430,7 +461,6 @@
 		"identifier": []string{r.FormValue("username")},
 	}
 	resp, err := postFormToKratos("login", flow[0], r.Cookies(), req)
-	fmt.Printf("--- %d\n", resp.StatusCode)
 	var vv bytes.Buffer
 	io.Copy(&vv, resp.Body)
 	fmt.Println(vv.String())
@@ -451,7 +481,7 @@
 		http.SetCookie(w, c)
 	}
 	if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
-		username, err := getWhoAmIFromKratos(resp.Cookies())
+		_, username, err := getWhoAmIFromKratos(resp.Cookies())
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -481,7 +511,7 @@
 }
 
 func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
-	if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
+	if _, username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
 		if errors.Is(err, ErrNotLoggedIn) {
 			http.Redirect(w, r, "/login", http.StatusSeeOther)
 			return
@@ -510,7 +540,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	username, err := getWhoAmIFromKratos(r.Cookies())
+	_, username, err := getWhoAmIFromKratos(r.Cookies())
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -538,7 +568,7 @@
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	username, err := getWhoAmIFromKratos(r.Cookies())
+	_, username, err := getWhoAmIFromKratos(r.Cookies())
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -560,15 +590,67 @@
 	}
 }
 
+type changePasswordData struct {
+	Username       string
+	Password       string
+	PasswordErrors []ValidationError
+}
+
+func (s *Server) changePasswordForm(w http.ResponseWriter, r *http.Request) {
+	_, username, err := getWhoAmIFromKratos(r.Cookies())
+	if err != nil {
+		if errors.Is(err, ErrNotLoggedIn) {
+			http.Redirect(w, r, "/", http.StatusSeeOther)
+		} else {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+		return
+	}
+	if err := s.tmpls.ChangePassword.Execute(w, changePasswordData{Username: username}); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	password := r.FormValue("password")
+	id, username, err := getWhoAmIFromKratos(r.Cookies())
+	if err != nil {
+		if errors.Is(err, ErrNotLoggedIn) {
+			http.Redirect(w, r, "/", http.StatusSeeOther)
+		} else {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+		return
+	}
+	if verr, err := s.api.apiPasswordChange(id, username, password); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	} else if len(verr) > 0 {
+		if err := s.tmpls.ChangePassword.Execute(w, changePasswordData{username, password, verr}); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	} else {
+		if err := s.tmpls.ChangePasswordSuccess.Execute(w, nil); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	}
+}
+
 func main() {
 	flag.Parse()
 	t, err := ParseTemplates(tmpls)
 	if err != nil {
 		log.Fatal(err)
 	}
+	api := NewAPIServer(*apiPort, *kratosAPI)
 	go func() {
-		s := NewAPIServer(*apiPort, *kratosAPI)
-		log.Fatal(s.Start())
+		log.Fatal(api.Start())
 	}()
 	func() {
 		s := NewServer(
@@ -577,6 +659,8 @@
 			NewHydraClient(*hydra),
 			t,
 			*enableRegistration,
+			api,
+			*defaultReturnTo,
 		)
 		log.Fatal(s.Start())
 	}()