Skip to content

Commit

Permalink
Add Password Reset
Browse files Browse the repository at this point in the history
  • Loading branch information
kvizdos committed Oct 25, 2023
1 parent 38ed6ef commit 4f3c060
Show file tree
Hide file tree
Showing 11 changed files with 590 additions and 14 deletions.
4 changes: 4 additions & 0 deletions authentication/magic/validate_magic.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ func Validate(db database.DatabaseAccessor, tokenIdentifer string) (MagicAuthent
return MagicAuthentication{}, map[string]interface{}{}, fmt.Errorf("uid not found")
}

if rawUser.(map[string]interface{})["magic"] == nil {
return MagicAuthentication{}, map[string]interface{}{}, fmt.Errorf("no magics found")
}

magics := MagicsFromMap(rawUser.(map[string]interface{})["magic"].([]interface{}))

if len(magics) == 0 {
Expand Down
87 changes: 87 additions & 0 deletions authentication/reset/do_reset_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package reset

import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"

"github.com/kvizdos/locksmith/authentication"
"github.com/kvizdos/locksmith/authentication/magic"
"github.com/kvizdos/locksmith/database"
"github.com/kvizdos/locksmith/logger"
"github.com/kvizdos/locksmith/users"
)

type ResetPasswordAPIHandler struct{}

type resetPasswordRequest struct {
Password string `json:"password"`
}

func (r resetPasswordRequest) HasRequiredFields() bool {
return r.Password != ""
}

func (h ResetPasswordAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
authUser := r.Context().Value("authUser").(users.LocksmithUser)
db := r.Context().Value("database").(database.DatabaseAccessor)

body, err := io.ReadAll(r.Body)
if err != nil {
// handle the error
fmt.Println("Error reading request body:", err)
w.WriteHeader(http.StatusBadRequest)
return
}

var resetReq resetPasswordRequest
err = json.Unmarshal(body, &resetReq)

if err != nil || (err == nil && !resetReq.HasRequiredFields()) {
logger.LOGGER.Log(logger.BAD_REQUEST, logger.GetIPFromRequest(*r), r.URL.Path)
w.WriteHeader(http.StatusBadRequest)
return
}

password, err := authentication.CompileLocksmithPassword(resetReq.Password)

if err != nil {
fmt.Println("Error compiling password:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

_, err = db.UpdateOne("users", map[string]interface{}{
"id": authUser.ID,
}, map[database.DatabaseUpdateActions]map[string]interface{}{
database.SET: {
"password": password.ToMap(),
"sessions": []interface{}{},
},
})

cookie, err := r.Cookie("magic")

if err == nil {
magic.ExpireOld(db, authUser.ID, cookie.Value)
c := &http.Cookie{
Name: "magic",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),

HttpOnly: true,
}
http.SetCookie(w, c)
}

if err != nil {
fmt.Println("Password Reset Error:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
}
87 changes: 87 additions & 0 deletions authentication/reset/reset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package reset

import (
"fmt"
"net/http"
"time"

"github.com/kvizdos/locksmith/authentication/endpoints"
"github.com/kvizdos/locksmith/authentication/magic"
"github.com/kvizdos/locksmith/database"
"github.com/kvizdos/locksmith/users"
)

type ResetRouterAPIHandler struct {
Database database.DatabaseAccessor
SendResetToken func(token string, user users.LocksmithUserInterface)
}

/*
Flow:
- [ ] User clicks "Forgot password" on login page
- [x] User enters their email
- [x] Show a screen to the user that "if the account exists, we've sent a link to your email address."
- [x] Sends POST request to create & dispatch the reset token
- POST /api/reset-password?email=email
- [x] Create a MAC for the user to access the PUT /api/reset-password endpoint
- [x] Send them a Notification with the MAC (we should make the SendMessage a variable on ResetRouterAPIHandler)
- URL Format: /reset-password/reset?magic=<MAC>
- [x] User will enter their new password
- [x] Password will be changed
- [x] MAC is expired
- PUT /api/reset-password { email: email }
- [x] Redirect to /login
*/
func (h ResetRouterAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPatch:
// This page is only accessible through the
// Magic Access Code.
// Updates the actual password
endpoints.SecureEndpointHTTPMiddleware(ResetPasswordAPIHandler{}, h.Database, endpoints.EndpointSecurityOptions{
MinimalPermissions: []string{"magic.reset.password"},
PrioritizeMagic: true,
}).ServeHTTP(w, r)
return
case http.MethodPost:
// Creates the MAC if the user exists.
username := r.URL.Query().Get("username")

if username == "" {
w.WriteHeader(http.StatusBadRequest)
return
}

user, found := h.Database.FindOne("users", map[string]interface{}{
"username": username,
})

if !found {
w.WriteHeader(http.StatusOK)
return
}

var lsUser users.LocksmithUserInterface
users.LocksmithUser{}.ReadFromMap(&lsUser, user.(map[string]interface{}))
token, err := lsUser.CreateMagicAuthenticationCode(h.Database, magic.MagicAuthenticationVariables{
ForUserID: lsUser.GetID(),
AllowedPermissions: []string{"magic.reset.password"},
TTL: 30 * time.Minute,
})

if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

h.SendResetToken(token, lsUser)

w.WriteHeader(http.StatusOK)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
62 changes: 62 additions & 0 deletions authentication/reset/reset_pages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package reset

import (
"fmt"
"net/http"
"text/template"

"github.com/kvizdos/locksmith/pages"
)

type ResetPasswordPageHandler struct {
AppName string
Styling pages.LocksmithPageStyling
EmailAsUsername bool
ShowResetStage bool
}

func (h ResetPasswordPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")

tmpl, err := template.New("reset-password.html").Parse(string(pages.ResetPasswordPageHTML))

if err != nil {
fmt.Println(err)
}

type TemplateData struct {
Title string
Styling pages.LocksmithPageStyling
EmailAsUsername bool
ShowResetStage bool
}
inv := TemplateData{
Title: h.AppName,
Styling: h.Styling,
EmailAsUsername: h.EmailAsUsername,
ShowResetStage: h.ShowResetStage,
}

if inv.Styling.SubmitColor == "" {
inv.Styling.SubmitColor = "#476ade"
}

if inv.Styling.StartGradient == "" {
inv.Styling.StartGradient = "#476ade"
}

if inv.Styling.EndGradient == "" {
inv.Styling.EndGradient = "#2744a3"
}

if inv.Title == "" {
inv.Title = "Locksmith"
}

err = tmpl.Execute(w, inv)

if err != nil {
fmt.Println("Error executing template :", err)
return
}
}
5 changes: 5 additions & 0 deletions components/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
//go:embed register.component.js
var RegistrationComponentJS []byte

//go:embed reset-password.component.js
var ResetPasswordComponentJS []byte

//go:embed signin.component.js
var SigninComponentJS []byte

Expand All @@ -33,6 +36,8 @@ func ServeComponents(w http.ResponseWriter, r *http.Request) {
serveJSComponent(w, UserListComponentJS)
case "user-tab.component.js":
serveJSComponent(w, UserTabComponentJS)
case "reset-password.component.js":
serveJSComponent(w, ResetPasswordComponentJS)
case "persona-switcher.component.js":
if launchpad.IS_ENABLED {
serveJSComponent(w, PersonaSwitcherJS)
Expand Down

0 comments on commit 4f3c060

Please sign in to comment.