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/memberships/main.go b/core/auth/memberships/main.go
index e72a163..955e783 100644
--- a/core/auth/memberships/main.go
+++ b/core/auth/memberships/main.go
@@ -654,7 +654,7 @@
}
func getLoggedInUser(r *http.Request) (string, error) {
- if user := r.Header.Get("X-User"); user != "" {
+ if user := r.Header.Get("X-Forwarded-User"); user != "" {
return user, nil
} else {
return "", fmt.Errorf("unauthenticated")
diff --git a/core/auth/memberships/store_test.go b/core/auth/memberships/store_test.go
index bc2e9c6..aba707c 100644
--- a/core/auth/memberships/store_test.go
+++ b/core/auth/memberships/store_test.go
@@ -214,7 +214,7 @@
router := mux.NewRouter()
router.HandleFunc("/group/{parent-group}/remove-child-group/{child-group}", server.removeChildGroupHandler).Methods(http.MethodPost)
req, err := http.NewRequest("POST", "/group/bb/remove-child-group/aa", nil)
- req.Header.Set("X-User", "testuser")
+ req.Header.Set("X-Forwarded-User", "testuser")
if err != nil {
t.Fatal(err)
}
@@ -328,7 +328,7 @@
// case when group present or exist
router.HandleFunc("/api/users", server.apiGetAllUsers).Methods(http.MethodGet)
req, err := http.NewRequest("GET", "/api/users?groups=b,e,t", nil)
- req.Header.Set("X-User", "testuser1")
+ req.Header.Set("X-Forwarded-User", "testuser1")
if err != nil {
t.Fatal(err)
}
@@ -353,7 +353,7 @@
// case when no group present
req, err = http.NewRequest("GET", "/api/users?groups=", nil)
- req.Header.Set("X-User", "testuser1")
+ req.Header.Set("X-Forwarded-User", "testuser1")
if err != nil {
t.Fatal(err)
}
@@ -380,7 +380,7 @@
// case when wrong groups
req, err = http.NewRequest("GET", "/api/users?groups=x,y", nil)
- req.Header.Set("X-User", "testuser1")
+ req.Header.Set("X-Forwarded-User", "testuser1")
if err != nil {
t.Fatal(err)
}
diff --git a/core/auth/proxy/main.go b/core/auth/proxy/main.go
index 2c10258..8c98d20 100644
--- a/core/auth/proxy/main.go
+++ b/core/auth/proxy/main.go
@@ -44,6 +44,7 @@
type user struct {
Identity struct {
+ Id string `json:"id"`
Traits struct {
Username string `json:"username"`
} `json:"traits"`
@@ -95,7 +96,8 @@
func handle(w http.ResponseWriter, r *http.Request) {
reqAuth := true
for _, p := range strings.Split(*noAuthPathPrefixes, ",") {
- if strings.HasPrefix(r.URL.Path, p) {
+ t := strings.TrimSpace(p)
+ if len(t) > 0 && strings.HasPrefix(r.URL.Path, t) {
reqAuth = false
break
}
@@ -104,6 +106,7 @@
if reqAuth {
var err error
user, err = queryWhoAmI(r.Cookies())
+ fmt.Printf("--- %+v\n", user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -142,11 +145,10 @@
}
}
}
+ fmt.Printf("%+v\n", user)
rc := r.Clone(context.Background())
- if user != nil {
- // TODO(gio): Rename to X-Forwarded-User
- rc.Header.Add("X-User", user.Identity.Traits.Username)
- }
+ rc.Header.Add("X-Forwarded-User", user.Identity.Traits.Username)
+ rc.Header.Add("X-Forwarded-UserId", user.Identity.Id)
ru, err := url.Parse(fmt.Sprintf("http://%s%s", *upstream, r.URL.RequestURI()))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
diff --git a/core/auth/ui/api.go b/core/auth/ui/api.go
index 6fb7426..b8c5bad 100644
--- a/core/auth/ui/api.go
+++ b/core/auth/ui/api.go
@@ -4,7 +4,9 @@
"bytes"
"encoding/json"
"fmt"
+ "io"
"net/http"
+ "net/url"
"strings"
"unicode"
@@ -111,7 +113,7 @@
}
}
if !digit || !lowerCase || !upperCase || !special {
- errors = append(errors, ValidationError{"password", "Password must contain at least one ditig, lower/upper and special character"})
+ errors = append(errors, ValidationError{"password", "Password must contain at least one digit, lower&upper case and special characters"})
}
// TODO other validations
return errors
@@ -167,6 +169,74 @@
}
}
+type changePasswordReq struct {
+ Id string `json:"id,omitempty"`
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+}
+
+func (s *APIServer) apiPasswordChange(id, username, password string) ([]ValidationError, error) {
+ var usernameErrors []ValidationError
+ passwordErrors := validatePassword(password)
+ allErrors := append(usernameErrors, passwordErrors...)
+ if len(allErrors) > 0 {
+ return allErrors, nil
+ }
+ var kreq kratosIdentityCreateReq
+ kreq.Credentials.Password.Config.Password = password
+ kreq.SchemaID = "user"
+ kreq.State = "active"
+ kreq.Traits.Username = username
+ var buf bytes.Buffer
+ if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
+ return nil, err
+ }
+ c := http.Client{}
+ addr, err := url.Parse(s.identityEndpoint(id))
+ if err != nil {
+ return nil, err
+ }
+ hreq := &http.Request{
+ Method: http.MethodPut,
+ URL: addr,
+ Header: http.Header{"Content-Type": []string{"application/json"}},
+ Body: io.NopCloser(&buf),
+ }
+ resp, err := c.Do(hreq)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode != http.StatusOK {
+ var buf bytes.Buffer
+ io.Copy(&buf, resp.Body)
+ respS := buf.String()
+ fmt.Printf("PASSWORD CHANGE ERROR: %s\n", respS)
+ var e ErrorResponse
+ if err := json.NewDecoder(bytes.NewReader([]byte(respS))).Decode(&e); err != nil {
+ return nil, err
+ }
+ return extractKratosErrorMessage(e), nil
+ }
+ return nil, nil
+}
+
+func (s *APIServer) passwordChange(w http.ResponseWriter, r *http.Request) {
+ var req changePasswordReq
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if verr, err := s.apiPasswordChange(req.Id, req.Username, req.Password); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ } else if len(verr) > 0 {
+ replyWithErrors(w, verr)
+ }
+}
+
func (s *APIServer) identitiesEndpoint() string {
return fmt.Sprintf("%s/admin/identities", s.kratosAddr)
}
+
+func (s *APIServer) identityEndpoint(id string) string {
+ return fmt.Sprintf("%s/admin/identities/%s", s.kratosAddr, id)
+}
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())
}()
diff --git a/core/auth/ui/static/main.css b/core/auth/ui/static/main.css
index 454eb2d..a261094 100644
--- a/core/auth/ui/static/main.css
+++ b/core/auth/ui/static/main.css
@@ -83,3 +83,7 @@
label {
color: white;
}
+
+.error-message {
+ color: var(--pico-primary-hover);
+}
diff --git a/core/auth/ui/templates/base.html b/core/auth/ui/templates/base.html
index 26817d3..624f908 100644
--- a/core/auth/ui/templates/base.html
+++ b/core/auth/ui/templates/base.html
@@ -3,7 +3,7 @@
<head>
<link rel="stylesheet" href="/static/pico.2.0.6.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
- <link rel="stylesheet" href="/static/main.css?v=0.0.1">
+ <link rel="stylesheet" href="/static/main.css?v=0.0.2">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ block "title" . }}Title{{ end }}</title>
diff --git a/core/auth/ui/templates/change-password-success.html b/core/auth/ui/templates/change-password-success.html
new file mode 100644
index 0000000..61c8e74
--- /dev/null
+++ b/core/auth/ui/templates/change-password-success.html
@@ -0,0 +1,6 @@
+{{ define "title" }}dodo: password changed{{ end }}
+{{ define "main" }}
+<div>
+ <p>Password changed successfully.</p>
+</div>
+{{ end }}
diff --git a/core/auth/ui/templates/change-password.html b/core/auth/ui/templates/change-password.html
new file mode 100644
index 0000000..4d0b487
--- /dev/null
+++ b/core/auth/ui/templates/change-password.html
@@ -0,0 +1,23 @@
+{{ define "title" }}dodo: change password{{ end }}
+{{ define "main" }}
+<div class="form-container">
+ <div class="logo">
+ <span>do</span><span>do:</span>
+ </div>
+ <form action="" method="POST">
+ <label>
+ new password
+ <input type="password" name="password" aria-label="Password" value="{{ .Password }}" aria-invalid="{{ if .PasswordErrors }}true{{ else }}undefined{{ end }}" required/>
+ </label>
+ {{ if .PasswordErrors }}
+ {{ range .PasswordErrors }}
+ <small class="error-message" aria-live="assertive">
+ {{ .Message }}
+ </small>
+ {{ end }}
+ {{ end }}
+ <button type="submit">change password</button>
+ <input type="hidden" name="username" value="{{ .Username }}" />
+ </form>
+</div>
+{{ end }}
diff --git a/core/auth/ui/templates/consent.html b/core/auth/ui/templates/consent.html
index 3a504a5..703434b 100644
--- a/core/auth/ui/templates/consent.html
+++ b/core/auth/ui/templates/consent.html
@@ -1,4 +1,4 @@
-{{ define "title" }}Consent{{ end }}
+{{ define "title" }}dodo: consent{{ end }}
{{ define "main" }}
<form action="" method="POST">
{{ range . }}
diff --git a/core/auth/ui/templates/login.html b/core/auth/ui/templates/login.html
index a80ecc7..0d961b5 100644
--- a/core/auth/ui/templates/login.html
+++ b/core/auth/ui/templates/login.html
@@ -1,4 +1,4 @@
-{{ define "title" }}Sign in{{ end }}
+{{ define "title" }}dodo: sign in{{ end }}
{{ define "main" }}
<div>
<div class="logo">
diff --git a/core/auth/ui/templates/register.html b/core/auth/ui/templates/register.html
index 22d69a5..626dc52 100644
--- a/core/auth/ui/templates/register.html
+++ b/core/auth/ui/templates/register.html
@@ -1,4 +1,4 @@
-{{ define "title" }}Create Account{{ end }}
+{{ define "title" }}dodo: create account{{ end }}
{{ define "main" }}
<form action="" method="POST">
<input type="text" name="username" placeholder="Username" autofocus required />
diff --git a/core/auth/ui/templates/whoami.html b/core/auth/ui/templates/whoami.html
index 2001c9f..944ed28 100644
--- a/core/auth/ui/templates/whoami.html
+++ b/core/auth/ui/templates/whoami.html
@@ -1,4 +1,4 @@
-{{ define "title" }}Who Am I{{ end }}
+{{ define "title" }}dodo: who am i{{ end }}
{{ define "main" }}
Hello {{.}}!
<a href="/logout" role="button">logout</a>