auth-ui: rewrite ui
Change-Id: I6f00867015ec77aa7e336e89da4dc1b081e330c6
diff --git a/core/auth/ui/main.go b/core/auth/ui/main.go
index 55dc963..244a9b0 100644
--- a/core/auth/ui/main.go
+++ b/core/auth/ui/main.go
@@ -15,6 +15,7 @@
"net/http"
"net/http/cookiejar"
"net/url"
+ "time"
"github.com/gorilla/mux"
"github.com/itaysk/regogo"
@@ -41,9 +42,9 @@
WhoAmI *template.Template
Register *template.Template
Login *template.Template
- Consent *template.Template
ChangePassword *template.Template
ChangePasswordSuccess *template.Template
+ Error *template.Template
}
func ParseTemplates(fs embed.FS) (*Templates, error) {
@@ -70,10 +71,6 @@
if err != nil {
return nil, err
}
- consent, err := parse("templates/consent.html")
- if err != nil {
- return nil, err
- }
changePassword, err := parse("templates/change-password.html")
if err != nil {
return nil, err
@@ -82,7 +79,83 @@
if err != nil {
return nil, err
}
- return &Templates{whoami, register, login, consent, changePassword, changePasswordSuccess}, nil
+ errorPage, err := parse("templates/error.html")
+ if err != nil {
+ return nil, err
+ }
+ return &Templates{whoami, register, login, changePassword, changePasswordSuccess, errorPage}, nil
+}
+
+const (
+ invalidLoginMessage = "Username or password is incorrect."
+ duplicateRegistrationMessage = "Username is not available."
+ expiredFlowMessage = "This form expired. Please try again."
+ registrationRejectedMessage = "Registration could not be completed. Please review your details and try again."
+ passwordChangeRejectedMessage = "Password could not be changed. Please choose a different password and try again."
+ authenticationUnavailableMessage = "Authentication service is temporarily unavailable. Please try again."
+
+ authNoticeCookieName = "auth_ui_notice"
+ authNoticeLoginInvalid = "login_invalid"
+ authNoticeFlowExpired = "flow_expired"
+)
+
+var errFlowExpired = errors.New("self-service flow expired")
+
+type LoginPageData struct {
+ FormAction string
+ CSRFToken string
+ EnableRegistration bool
+ GeneralNotice string
+}
+
+type RegisterPageData struct {
+ FormAction string
+ CSRFToken string
+ Username string
+ UsernameErrors []ValidationError
+ PasswordErrors []ValidationError
+ GeneralError string
+}
+
+type ChangePasswordPageData struct {
+ Username string
+ CSRFToken string
+ FormAction string
+ PasswordErrors []ValidationError
+ GeneralError string
+}
+
+type AccountPageData struct {
+ Username string
+}
+
+type ErrorPageData struct {
+ Title string
+ Message string
+ Status int
+ RecoveryHref string
+ RecoveryText string
+}
+
+type oryFlowResponse struct {
+ ID string `json:"id"`
+ UI struct {
+ Nodes []struct {
+ Attributes struct {
+ Name string `json:"name"`
+ Value json.RawMessage `json:"value"`
+ } `json:"attributes"`
+ } `json:"nodes"`
+ Messages []struct {
+ ID int64 `json:"id"`
+ } `json:"messages"`
+ } `json:"ui"`
+}
+
+type oryErrorResponse struct {
+ Error struct {
+ ID string `json:"id"`
+ } `json:"error"`
}
type Server struct {
@@ -113,6 +186,153 @@
return &Server{r, serv, kratos, hydra, tmpls, enableRegistration, api, defaultReturnTo}
}
+func executeTemplate(tmpl *template.Template, data any) ([]byte, error) {
+ var page bytes.Buffer
+ if err := tmpl.Execute(&page, data); err != nil {
+ return nil, err
+ }
+ return page.Bytes(), nil
+}
+
+func writeTemplate(w http.ResponseWriter, status int, page []byte) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(status)
+ _, _ = w.Write(page)
+}
+
+func renderTemplate(w http.ResponseWriter, tmpl *template.Template, status int, data any) {
+ page, err := executeTemplate(tmpl, data)
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ writeTemplate(w, status, page)
+}
+
+func (s *Server) renderDependencyError(w http.ResponseWriter, context string) {
+ data := ErrorPageData{Message: authenticationUnavailableMessage, Status: http.StatusBadGateway}
+ switch context {
+ case "login":
+ data.Title = "Authentication unavailable"
+ data.RecoveryHref = "/login"
+ data.RecoveryText = "Try signing in again"
+ case "registration":
+ data.Title = "Registration unavailable"
+ if s.enableRegistration {
+ data.RecoveryHref = "/register"
+ data.RecoveryText = "Try registration again"
+ } else {
+ data.RecoveryHref = "/login"
+ data.RecoveryText = "Go to sign in"
+ }
+ default:
+ data.Title = "Account unavailable"
+ data.RecoveryHref = "/"
+ data.RecoveryText = "Back to account"
+ }
+ renderTemplate(w, s.tmpls.Error, data.Status, data)
+}
+
+func setAuthNotice(w http.ResponseWriter, code string) {
+ if code != authNoticeLoginInvalid && code != authNoticeFlowExpired {
+ return
+ }
+ http.SetCookie(w, &http.Cookie{
+ Name: authNoticeCookieName,
+ Value: code,
+ Path: "/",
+ MaxAge: 120,
+ Expires: time.Now().Add(2 * time.Minute),
+ HttpOnly: true,
+ SameSite: http.SameSiteLaxMode,
+ })
+}
+
+func clearAuthNotice(w http.ResponseWriter) {
+ http.SetCookie(w, &http.Cookie{
+ Name: authNoticeCookieName,
+ Value: "",
+ Path: "/",
+ MaxAge: -1,
+ HttpOnly: true,
+ SameSite: http.SameSiteLaxMode,
+ })
+}
+
+func pendingAuthNotice(r *http.Request, accepted ...string) (code string, clear bool) {
+ cookie, err := r.Cookie(authNoticeCookieName)
+ if err != nil {
+ return "", false
+ }
+ for _, acceptedCode := range accepted {
+ if cookie.Value == acceptedCode {
+ return cookie.Value, true
+ }
+ }
+ if cookie.Value != authNoticeLoginInvalid && cookie.Value != authNoticeFlowExpired {
+ return "", true
+ }
+ return "", false
+}
+
+func parseOryErrorID(body []byte) string {
+ var response oryErrorResponse
+ if json.Unmarshal(body, &response) != nil {
+ return ""
+ }
+ return response.Error.ID
+}
+
+func parseRetryFlow(body []byte) (flowID, csrfToken string, duplicate bool, err error) {
+ var response oryFlowResponse
+ if json.Unmarshal(body, &response) != nil || response.ID == "" {
+ return "", "", false, errors.New("invalid retry flow")
+ }
+ csrfCount := 0
+ for _, node := range response.UI.Nodes {
+ if node.Attributes.Name != "csrf_token" {
+ continue
+ }
+ csrfCount++
+ if json.Unmarshal(node.Attributes.Value, &csrfToken) != nil || csrfToken == "" {
+ return "", "", false, errors.New("invalid retry csrf token")
+ }
+ }
+ if csrfCount != 1 {
+ return "", "", false, errors.New("invalid retry csrf token count")
+ }
+ for _, message := range response.UI.Messages {
+ if message.ID == 4000007 {
+ duplicate = true
+ }
+ }
+ return response.ID, csrfToken, duplicate, nil
+}
+
+func readResponseBody(resp *http.Response) ([]byte, error) {
+ defer resp.Body.Close()
+ return ioutil.ReadAll(resp.Body)
+}
+
+func localFlowAction(path, flow string) string {
+ return path + "?flow=" + url.QueryEscape(flow)
+}
+
+func (s *Server) restartFlow(w http.ResponseWriter, r *http.Request, flowType string) {
+ setAuthNotice(w, authNoticeFlowExpired)
+ addr := s.kratos + "/self-service/" + flowType + "/browser"
+ if flowType == "login" {
+ returnTo := r.FormValue("return_to")
+ if returnTo == "" && s.defaultReturnTo != "" {
+ returnTo = s.defaultReturnTo
+ }
+ if returnTo != "" {
+ addr += fmt.Sprintf("?return_to=%s", returnTo)
+ }
+ }
+ http.Redirect(w, r, addr, http.StatusSeeOther)
+}
+
func cacheControlWrapper(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO(giolekva): enable caching
@@ -160,37 +380,60 @@
if err != nil {
return "", err
}
- respBody, err := ioutil.ReadAll(resp.Body)
+ respBody, err := readResponseBody(resp)
if err != nil {
return "", err
}
- token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
+ if resp.StatusCode == http.StatusGone && parseOryErrorID(respBody) == "self_service_flow_expired" {
+ return "", errFlowExpired
+ }
+ if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
+ return "", errors.New("flow fetch failed")
+ }
+ _, token, _, err := parseRetryFlow(respBody)
if err != nil {
return "", err
}
- return token.String(), nil
+ return token, nil
}
func (s *Server) registerInitiate(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
- flow, ok := r.Form["flow"]
- if !ok {
+ flow := r.FormValue("flow")
+ if flow == "" {
http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
return
}
- csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
+ csrfToken, err := getCSRFToken("registration", flow, r.Cookies())
+ if errors.Is(err, errFlowExpired) {
+ s.restartFlow(w, r, "registration")
+ return
+ }
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "registration")
return
}
- w.Header().Set("Content-Type", "text/html")
- if err := s.tmpls.Register.Execute(w, csrfToken); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ notice := ""
+ noticeCode, clearNotice := pendingAuthNotice(r, authNoticeFlowExpired)
+ if noticeCode == authNoticeFlowExpired {
+ notice = expiredFlowMessage
+ }
+ page, err := executeTemplate(s.tmpls.Register, RegisterPageData{
+ FormAction: localFlowAction(r.URL.Path, flow),
+ CSRFToken: csrfToken,
+ GeneralError: notice,
+ })
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+ if clearNotice {
+ clearAuthNotice(w)
+ }
+ writeTemplate(w, http.StatusOK, page)
}
type regReq struct {
@@ -206,36 +449,85 @@
func (s *Server) register(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
- flow, ok := r.Form["flow"]
- if !ok {
+ flow := r.FormValue("flow")
+ if flow == "" {
http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
return
}
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+ usernameErrors := validateUsername(username)
+ passwordErrors := validatePassword(password)
+ if len(usernameErrors)+len(passwordErrors) > 0 {
+ renderTemplate(w, s.tmpls.Register, http.StatusUnprocessableEntity, RegisterPageData{
+ FormAction: localFlowAction(r.URL.Path, flow),
+ CSRFToken: r.FormValue("csrf_token"),
+ Username: username,
+ UsernameErrors: usernameErrors,
+ PasswordErrors: passwordErrors,
+ })
+ return
+ }
req := regReq{
CSRFToken: r.FormValue("csrf_token"),
Method: "password",
- Password: r.FormValue("password"),
+ Password: password,
Traits: regReqTraits{
- Username: r.FormValue("username"),
+ Username: username,
},
}
var reqBody bytes.Buffer
if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "registration")
return
}
- if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ resp, err := postToKratos("registration", flow, r.Cookies(), &reqBody)
+ if err != nil {
+ s.renderDependencyError(w, "registration")
return
- } else {
- for _, c := range resp.Cookies() {
- http.SetCookie(w, c)
- }
+ }
+ for _, cookie := range resp.Cookies() {
+ http.SetCookie(w, cookie)
+ }
+ if resp.StatusCode < http.StatusBadRequest {
+ resp.Body.Close()
http.Redirect(w, r, "/", http.StatusSeeOther)
+ return
}
+ body, err := readResponseBody(resp)
+ if err != nil {
+ s.renderDependencyError(w, "registration")
+ return
+ }
+ if (resp.StatusCode == http.StatusGone && parseOryErrorID(body) == "self_service_flow_expired") ||
+ (resp.StatusCode == http.StatusForbidden && parseOryErrorID(body) == "security_csrf_violation") {
+ s.restartFlow(w, r, "registration")
+ return
+ }
+ if resp.StatusCode != http.StatusBadRequest {
+ s.renderDependencyError(w, "registration")
+ return
+ }
+ retryFlow, retryCSRF, duplicate, err := parseRetryFlow(body)
+ if err != nil {
+ s.renderDependencyError(w, "registration")
+ return
+ }
+ status := http.StatusUnprocessableEntity
+ generalError := registrationRejectedMessage
+ if duplicate {
+ status = http.StatusConflict
+ generalError = duplicateRegistrationMessage
+ }
+ renderTemplate(w, s.tmpls.Register, status, RegisterPageData{
+ FormAction: localFlowAction(r.URL.Path, retryFlow),
+ CSRFToken: retryCSRF,
+ Username: username,
+ GeneralError: generalError,
+ })
}
// Login flow
@@ -253,7 +545,7 @@
func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
challenge, hasChallenge := r.Form["login_challenge"]
@@ -264,13 +556,13 @@
if hasChallenge {
_, username, err := getWhoAmIFromKratos(r.Cookies())
if err != nil && err != ErrNotLoggedIn {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "login")
return
}
if err == nil {
redirectTo, err := s.hydra.LoginAcceptChallenge(challenge[0], username)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "login")
return
}
clearLoginChallengeCookie(w)
@@ -299,18 +591,36 @@
return
}
csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
+ if errors.Is(err, errFlowExpired) {
+ s.restartFlow(w, r, "login")
+ return
+ }
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "login")
return
}
- w.Header().Set("Content-Type", "text/html")
- if err := s.tmpls.Login.Execute(w, map[string]any{
- "csrfToken": csrfToken,
- "enableRegistration": s.enableRegistration,
- }); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ notice := ""
+ noticeCode, clearNotice := pendingAuthNotice(r, authNoticeLoginInvalid, authNoticeFlowExpired)
+ switch noticeCode {
+ case authNoticeLoginInvalid:
+ notice = invalidLoginMessage
+ case authNoticeFlowExpired:
+ notice = expiredFlowMessage
+ }
+ page, err := executeTemplate(s.tmpls.Login, LoginPageData{
+ FormAction: localFlowAction(r.URL.Path, flow[0]),
+ CSRFToken: csrfToken,
+ EnableRegistration: s.enableRegistration,
+ GeneralNotice: notice,
+ })
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+ if clearNotice {
+ clearAuthNotice(w)
+ }
+ writeTemplate(w, http.StatusOK, page)
}
type loginReq struct {
@@ -442,72 +752,65 @@
}
-func extractError(r io.Reader) error {
- respBody, err := ioutil.ReadAll(r)
- if err != nil {
- return err
+func isRejectedLoginRedirect(resp *http.Response) bool {
+ if resp.StatusCode != http.StatusSeeOther {
+ return false
}
- fmt.Printf("++ %s\n", respBody)
- 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
+ location, err := url.Parse(resp.Header.Get("Location"))
+ return err == nil && location.Path == "/login" && location.Query().Get("flow") != ""
}
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
- flow, ok := r.Form["flow"]
- if !ok {
+ flow := r.FormValue("flow")
+ if flow == "" {
http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
return
}
req := url.Values{
- "csrf_token": []string{r.FormValue("csrf_token")},
- "method": []string{"password"},
- "password": []string{r.FormValue("password")},
- "identifier": []string{r.FormValue("username")},
+ "csrf_token": {r.FormValue("csrf_token")},
+ "method": {"password"},
+ "password": {r.FormValue("password")},
+ "identifier": {r.FormValue("username")},
}
- resp, err := postFormToKratos("login", flow[0], r.Cookies(), req)
- var vv bytes.Buffer
- io.Copy(&vv, resp.Body)
- fmt.Println(vv.String())
+ resp, err := postFormToKratos("login", flow, r.Cookies(), req)
if err != nil {
- if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
- redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- clearLoginChallengeCookie(w)
- http.Redirect(w, r, redirectTo, http.StatusSeeOther)
- return
- }
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "login")
return
}
- for _, c := range resp.Cookies() {
- http.SetCookie(w, c)
+ defer resp.Body.Close()
+ var responseBody bytes.Buffer
+ _, _ = io.Copy(&responseBody, resp.Body)
+ fmt.Println(responseBody.String())
+ for _, cookie := range resp.Cookies() {
+ http.SetCookie(w, cookie)
+ }
+ if (resp.StatusCode == http.StatusGone && parseOryErrorID(responseBody.Bytes()) == "self_service_flow_expired") ||
+ (resp.StatusCode == http.StatusForbidden && parseOryErrorID(responseBody.Bytes()) == "security_csrf_violation") {
+ s.restartFlow(w, r, "login")
+ return
+ }
+ if isRejectedLoginRedirect(resp) {
+ setAuthNotice(w, authNoticeLoginInvalid)
+ http.Redirect(w, r, resp.Header.Get("Location"), http.StatusSeeOther)
+ return
+ }
+ if resp.StatusCode != http.StatusSeeOther {
+ s.renderDependencyError(w, "login")
+ return
}
if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
_, username, err := getWhoAmIFromKratos(resp.Cookies())
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "login")
return
}
redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "login")
return
}
clearLoginChallengeCookie(w)
@@ -523,7 +826,7 @@
func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "account")
return
} else {
http.Redirect(w, r, logoutURL, http.StatusSeeOther)
@@ -536,11 +839,9 @@
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "account")
} else {
- if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
+ renderTemplate(w, s.tmpls.WhoAmI, http.StatusOK, AccountPageData{Username: username})
}
}
@@ -576,11 +877,6 @@
} else {
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
}
- // 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) {
@@ -610,14 +906,6 @@
}
}
-type changePasswordData struct {
- Username string
- Password string
- CSRFToken string
- FormAction string
- PasswordErrors []ValidationError
-}
-
func (s *Server) changePasswordForm(w http.ResponseWriter, r *http.Request) {
flow := r.FormValue("flow")
if flow == "" {
@@ -626,23 +914,42 @@
}
_, username, err := getWhoAmIFromKratos(r.Cookies())
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "account")
return
}
csrfToken, err := getCSRFToken("settings", flow, r.Cookies())
+ if errors.Is(err, errFlowExpired) {
+ s.restartFlow(w, r, "settings")
+ return
+ }
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "account")
return
}
- if err := s.tmpls.ChangePassword.Execute(w, changePasswordData{Username: username, CSRFToken: csrfToken, FormAction: r.URL.Path + "?flow=" + url.QueryEscape(flow)}); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ notice := ""
+ noticeCode, clearNotice := pendingAuthNotice(r, authNoticeFlowExpired)
+ if noticeCode == authNoticeFlowExpired {
+ notice = expiredFlowMessage
+ }
+ page, err := executeTemplate(s.tmpls.ChangePassword, ChangePasswordPageData{
+ Username: username,
+ CSRFToken: csrfToken,
+ FormAction: localFlowAction(r.URL.Path, flow),
+ GeneralError: notice,
+ })
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+ if clearNotice {
+ clearAuthNotice(w)
+ }
+ writeTemplate(w, http.StatusOK, page)
}
func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
flow := r.FormValue("flow")
@@ -653,13 +960,16 @@
password := r.FormValue("password")
_, username, err := getWhoAmIFromKratos(r.Cookies())
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "account")
return
}
- if verr := validatePassword(password); len(verr) > 0 {
- if err := s.tmpls.ChangePassword.Execute(w, changePasswordData{Username: username, Password: password, CSRFToken: r.FormValue("csrf_token"), FormAction: r.URL.Path + "?flow=" + url.QueryEscape(flow), PasswordErrors: verr}); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
+ if passwordErrors := validatePassword(password); len(passwordErrors) > 0 {
+ renderTemplate(w, s.tmpls.ChangePassword, http.StatusUnprocessableEntity, ChangePasswordPageData{
+ Username: username,
+ CSRFToken: r.FormValue("csrf_token"),
+ FormAction: localFlowAction(r.URL.Path, flow),
+ PasswordErrors: passwordErrors,
+ })
return
}
resp, err := postFormToKratos("settings", flow, r.Cookies(), url.Values{
@@ -668,24 +978,39 @@
"password": {password},
})
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ s.renderDependencyError(w, "account")
return
}
- defer resp.Body.Close()
- if resp.StatusCode >= http.StatusBadRequest {
- if err := extractError(resp.Body); err != nil {
- if renderErr := s.tmpls.ChangePassword.Execute(w, changePasswordData{Username: username, Password: password, CSRFToken: r.FormValue("csrf_token"), FormAction: r.URL.Path + "?flow=" + url.QueryEscape(flow), PasswordErrors: []ValidationError{{Field: "password", Message: err.Error()}}}); renderErr != nil {
- http.Error(w, renderErr.Error(), http.StatusInternalServerError)
- }
- return
- }
- http.Error(w, "password change failed", resp.StatusCode)
+ if resp.StatusCode < http.StatusBadRequest {
+ resp.Body.Close()
+ renderTemplate(w, s.tmpls.ChangePasswordSuccess, http.StatusOK, nil)
return
}
- if err := s.tmpls.ChangePasswordSuccess.Execute(w, nil); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ body, err := readResponseBody(resp)
+ if err != nil {
+ s.renderDependencyError(w, "account")
return
}
+ if (resp.StatusCode == http.StatusGone && parseOryErrorID(body) == "self_service_flow_expired") ||
+ (resp.StatusCode == http.StatusForbidden && parseOryErrorID(body) == "security_csrf_violation") {
+ s.restartFlow(w, r, "settings")
+ return
+ }
+ if resp.StatusCode != http.StatusBadRequest {
+ s.renderDependencyError(w, "account")
+ return
+ }
+ retryFlow, retryCSRF, _, err := parseRetryFlow(body)
+ if err != nil {
+ s.renderDependencyError(w, "account")
+ return
+ }
+ renderTemplate(w, s.tmpls.ChangePassword, http.StatusUnprocessableEntity, ChangePasswordPageData{
+ Username: username,
+ CSRFToken: retryCSRF,
+ FormAction: localFlowAction(r.URL.Path, retryFlow),
+ GeneralError: passwordChangeRejectedMessage,
+ })
}
func main() {