Auth: implement consent logic
diff --git a/core/auth/ui/hydra.go b/core/auth/ui/hydra.go
new file mode 100644
index 0000000..8a0dea0
--- /dev/null
+++ b/core/auth/ui/hydra.go
@@ -0,0 +1,162 @@
+package main
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+type HydraClient struct {
+	httpClient *http.Client
+	host       string
+}
+
+func NewHydraClient(host string) *HydraClient {
+	return &HydraClient{
+		// TODO(giolekva): trust selfsigned-root-ca automatically on pods
+		&http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+			},
+		},
+		host,
+	}
+}
+
+type loginResp struct {
+	RedirectTo       string `json:"redirect_to"`
+	Error            string `json:"error"`
+	ErrorDebug       string `json:"error_debug"`
+	ErrorDescription string `json:"error_description"`
+	StatusCode       int    `json:"status_code"`
+}
+
+func (c *HydraClient) LoginAcceptChallenge(challenge, subject string) (string, error) {
+	req := &http.Request{
+		Method: http.MethodPut,
+		URL: &url.URL{
+			Scheme:   "https",
+			Host:     c.host,
+			Path:     "/oauth2/auth/requests/login/accept",
+			RawQuery: fmt.Sprintf("login_challenge=%s", challenge),
+		},
+		Header: map[string][]string{
+			"Content-Type": []string{"application/json"},
+		},
+		// TODO(giolekva): user stable userid instead
+		Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`
+{
+    "subject": "%s",
+    "remember": true,
+    "remember_for": 3600
+}`, subject))),
+	}
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return "", err
+	}
+	var r loginResp
+	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
+		return "", err
+	}
+	if r.Error != "" {
+		return "", errors.New(r.Error)
+	}
+	return r.RedirectTo, nil
+}
+
+func (c *HydraClient) LoginRejectChallenge(challenge, message string) (string, error) {
+	req := &http.Request{
+		Method: http.MethodPut,
+		URL: &url.URL{
+			Scheme:   "https",
+			Host:     c.host,
+			Path:     "/oauth2/auth/requests/login/reject",
+			RawQuery: fmt.Sprintf("login_challenge=%s", challenge),
+		},
+		Header: map[string][]string{
+			"Content-Type": []string{"application/json"},
+		},
+		Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`
+{
+    "error": "login_required %s"
+}`, message))),
+	}
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return "", err
+	}
+	var r loginResp
+	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
+		return "", err
+	}
+	if r.Error != "" {
+		return "", errors.New(r.Error)
+	}
+	return r.RedirectTo, nil
+}
+
+type RequestedConsent struct {
+	Challenge       string   `json:"challenge"`
+	Subject         string   `json:"subject"`
+	RequestedScopes []string `json:"requested_scope"`
+}
+
+func (c *HydraClient) GetConsentChallenge(challenge string) (RequestedConsent, error) {
+	var consent RequestedConsent
+	resp, err := c.httpClient.Get(fmt.Sprintf("https://%s/oauth2/auth/requests/consent?consent_challenge=%s", c.host, challenge))
+	if err != nil {
+		return consent, err
+	}
+	err = json.NewDecoder(resp.Body).Decode(&consent)
+	return consent, err
+}
+
+type consentAcceptReq struct {
+	GrantScope []string `json:"grant_scope"`
+	Session    session  `json:"session"`
+}
+
+type session struct {
+	IDToken map[string]string `json:"id_token"`
+}
+
+type consentAcceptResp struct {
+	RedirectTo string `json:"redirect_to"`
+}
+
+func (c *HydraClient) ConsentAccept(challenge string, scopes []string, idToken map[string]string) (string, error) {
+	accept := consentAcceptReq{scopes, session{idToken}}
+	var data bytes.Buffer
+	if err := json.NewEncoder(&data).Encode(accept); err != nil {
+		return "", err
+	}
+	req := &http.Request{
+		Method: http.MethodPut,
+		URL: &url.URL{
+			Scheme:   "https",
+			Host:     c.host,
+			Path:     "/oauth2/auth/requests/consent/accept",
+			RawQuery: fmt.Sprintf("challenge=%s", challenge),
+		},
+		Header: map[string][]string{
+			"Content-Type": []string{"application/json"},
+		},
+		Body: io.NopCloser(&data),
+	}
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return "", err
+	}
+	var r consentAcceptResp
+	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
+		return "", err
+	}
+	return r.RedirectTo, err
+}
diff --git a/core/auth/ui/main.go b/core/auth/ui/main.go
index 92ca885..a9b349d 100644
--- a/core/auth/ui/main.go
+++ b/core/auth/ui/main.go
@@ -2,7 +2,6 @@
 
 import (
 	"bytes"
-	"crypto/tls"
 	"embed"
 	"encoding/json"
 	"errors"
@@ -15,7 +14,6 @@
 	"net/http"
 	"net/http/cookiejar"
 	"net/url"
-	"strings"
 
 	"github.com/gorilla/mux"
 	"github.com/itaysk/regogo"
@@ -23,6 +21,7 @@
 
 var port = flag.Int("port", 8080, "Port to listen on")
 var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
+var hydra = flag.String("hydra", "hydra.pcloud", "Hydra admin server address")
 
 var ErrNotLoggedIn = errors.New("Not logged in")
 
@@ -33,9 +32,14 @@
 	WhoAmI       *template.Template
 	Registration *template.Template
 	Login        *template.Template
+	Consent      *template.Template
 }
 
 func ParseTemplates(fs embed.FS) (*Templates, error) {
+	whoami, err := template.ParseFS(fs, "templates/whoami.html")
+	if err != nil {
+		return nil, err
+	}
 	registration, err := template.ParseFS(fs, "templates/registration.html")
 	if err != nil {
 		return nil, err
@@ -44,15 +48,16 @@
 	if err != nil {
 		return nil, err
 	}
-	whoami, err := template.ParseFS(fs, "templates/whoami.html")
+	consent, err := template.ParseFS(fs, "templates/consent.html")
 	if err != nil {
 		return nil, err
 	}
-	return &Templates{whoami, registration, login}, nil
+	return &Templates{whoami, registration, login, consent}, nil
 }
 
 type Server struct {
 	kratos string
+	hydra  *HydraClient
 	tmpls  *Templates
 }
 
@@ -63,6 +68,8 @@
 	r.Path("/registration").Methods(http.MethodPost).HandlerFunc(s.registration)
 	r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
 	r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
+	r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
+	r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
 	r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
 	r.Path("/").HandlerFunc(s.whoami)
 	fmt.Printf("Starting HTTP server on port: %d\n", port)
@@ -112,7 +119,6 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	log.Println(csrfToken)
 	w.Header().Set("Content-Type", "text/html")
 	if err := s.tmpls.Registration.Execute(w, csrfToken); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -172,17 +178,23 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	if challenge, ok := r.Form["login_challenge"]; ok {
+		// TODO(giolekva): encrypt
+		http.SetCookie(w, &http.Cookie{
+			Name:     "login_challenge",
+			Value:    challenge[0],
+			HttpOnly: true,
+		})
+	} else {
+		// http.SetCookie(w, &http.Cookie{
+		// 	Name:     "login_challenge",
+		// 	Value:    "",
+		// 	Expires:  time.Unix(0, 0),
+		// 	HttpOnly: true,
+		// })
+	}
 	flow, ok := r.Form["flow"]
 	if !ok {
-		challenge, ok := r.Form["login_challenge"]
-		if ok {
-			// TODO(giolekva): encrypt
-			http.SetCookie(w, &http.Cookie{
-				Name:     "login_challenge",
-				Value:    challenge[0],
-				HttpOnly: true,
-			})
-		}
 		http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
 		return
 	}
@@ -191,7 +203,6 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	log.Println(csrfToken)
 	w.Header().Set("Content-Type", "text/html")
 	if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -286,6 +297,25 @@
 
 }
 
+func extractError(r io.Reader) error {
+	respBody, err := ioutil.ReadAll(r)
+	if err != nil {
+		return err
+	}
+	t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
+	if err != nil {
+		return err
+	}
+	if t.String() == "error" {
+		message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
+		if err != nil {
+			return err
+		}
+		return errors.New(message.String())
+	}
+	return nil
+}
+
 func (s *Server) login(w http.ResponseWriter, r *http.Request) {
 	if err := r.ParseForm(); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -307,52 +337,41 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	} else {
-		for _, c := range resp.Cookies() {
-			http.SetCookie(w, c)
-		}
+	resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody)
+	if err == nil {
+		err = extractError(resp.Body)
+	}
+	if err != nil {
 		if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
-			username, err := getWhoAmIFromKratos(resp.Cookies())
+			redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
 			if err != nil {
 				http.Error(w, err.Error(), http.StatusInternalServerError)
 				return
 			}
-			req := &http.Request{
-				Method: http.MethodPut,
-				URL: &url.URL{
-					Scheme:   "https",
-					Host:     "hydra.pcloud",
-					Path:     "/oauth2/auth/requests/login/accept",
-					RawQuery: fmt.Sprintf("login_challenge=%s", challenge.Value),
-				},
-				Header: map[string][]string{
-					"Content-Type": []string{"text/html"},
-				},
-				// TODO(giolekva): user stable userid instead
-				Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`
-{
-    "subject": "%s",
-    "remember": true,
-    "remember_for": 3600
-}`, username))),
-			}
-			client := &http.Client{
-				Transport: &http.Transport{
-					TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-				},
-			}
-			resp, err := client.Do(req)
-			if err != nil {
-				http.Error(w, err.Error(), http.StatusInternalServerError)
-			} else {
-				io.Copy(w, resp.Body)
-			}
+			http.Redirect(w, r, redirectTo, http.StatusSeeOther)
+			return
 		}
-		// http.Redirect(w, r, "/", http.StatusSeeOther)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
+	for _, c := range resp.Cookies() {
+		http.SetCookie(w, c)
+	}
+	if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
+		username, err := getWhoAmIFromKratos(resp.Cookies())
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		http.Redirect(w, r, redirectTo, http.StatusSeeOther)
+		return
+	}
+	http.Redirect(w, r, "/", http.StatusSeeOther)
 }
 
 func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
@@ -378,6 +397,56 @@
 	}
 }
 
+// TODO(giolekva): verify if logged in
+func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	challenge, ok := r.Form["consent_challenge"]
+	if !ok {
+		http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
+		return
+	}
+	consent, err := s.hydra.GetConsentChallenge(challenge[0])
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.Header().Set("Content-Type", "text/html")
+	if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	username, err := getWhoAmIFromKratos(r.Cookies())
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if _, accepted := r.Form["allow"]; accepted {
+		acceptedScopes, _ := r.Form["scope"]
+		idToken := map[string]string{
+			"username": username,
+			"email":    username + "@lekva.me",
+		}
+		if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		} else {
+			http.Redirect(w, r, redirectTo, http.StatusSeeOther)
+		}
+		return
+	} else {
+		// TODO(giolekva): implement rejection logic
+	}
+}
+
 func main() {
 	flag.Parse()
 	t, err := ParseTemplates(tmpls)
@@ -386,6 +455,7 @@
 	}
 	s := &Server{
 		kratos: *kratos,
+		hydra:  NewHydraClient(*hydra),
 		tmpls:  t,
 	}
 	log.Fatal(s.Start(*port))
diff --git a/core/auth/ui/templates/consent.html b/core/auth/ui/templates/consent.html
new file mode 100644
index 0000000..9131e99
--- /dev/null
+++ b/core/auth/ui/templates/consent.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <title>Consent</title>
+</head>
+<body>
+    <a href="/">whoami</a>
+    <a href="/login">login</a>
+    <a href="/logout">logout</a>
+    <a href="/registration">registration</a><br/>
+    <form action="" method="POST">
+	{{range .}}
+	<input type="checkbox" name="scope" value="{{.}}" />{{.}}<br />
+	{{end}}
+	<input type="submit" name="allow" value="Allow" />
+	<input type="submit" name="reject" value="Reject" />
+    </form>
+</body>
+</html>
+
diff --git a/core/auth/ui/templates/login.html b/core/auth/ui/templates/login.html
index 04d0435..b5f73db 100644
--- a/core/auth/ui/templates/login.html
+++ b/core/auth/ui/templates/login.html
@@ -11,7 +11,7 @@
     <a href="/registration">registration</a><br/>
     <form action="" method="POST">
 	<label for="username">Username:</label><br />
-	<input type="text" name="username" /><br />
+	<input type="text" name="username" autofocus /><br />
 	<label for="password">Password:</label><br />
 	<input type="password" name="password" /><br />
 	<input type="hidden" name="csrf_token" value="{{.}}" /><br />
diff --git a/core/auth/ui/templates/registration.html b/core/auth/ui/templates/registration.html
index 6a10af8..9d3c0fd 100644
--- a/core/auth/ui/templates/registration.html
+++ b/core/auth/ui/templates/registration.html
@@ -11,7 +11,7 @@
     <a href="/registration">registration</a><br/>
     <form action="" method="POST">
 	<label for="username">Username:</label><br />
-	<input type="text" name="username" /><br />
+	<input type="text" name="username" autofocus /><br />
 	<label for="password">Password:</label><br />
 	<input type="password" name="password" /><br />
 	<input type="hidden" name="csrf_token" value="{{.}}" /><br />