forked from mirrors/homebox
feat: change password (#35)
* refactor: implement factories for testing * add additional factories * change protection for dropFields * prevent timed attacks on login * use switch instead of else-if * API implementation for changing password * add change-password dialog
This commit is contained in:
parent
a6e3989aee
commit
a6d2fd45df
19 changed files with 458 additions and 149 deletions
|
@ -822,6 +822,35 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/change-password": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Updates the users password",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Password Payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ChangePassword"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/login": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
|
@ -1579,6 +1608,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.ChangePassword": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"current": {
|
||||
"type": "string"
|
||||
},
|
||||
"new": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.GroupInvitation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -814,6 +814,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/change-password": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Updates the users password",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Password Payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ChangePassword"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/login": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
|
@ -1571,6 +1600,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.ChangePassword": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"current": {
|
||||
"type": "string"
|
||||
},
|
||||
"new": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.GroupInvitation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -353,6 +353,13 @@ definitions:
|
|||
version:
|
||||
type: string
|
||||
type: object
|
||||
v1.ChangePassword:
|
||||
properties:
|
||||
current:
|
||||
type: string
|
||||
new:
|
||||
type: string
|
||||
type: object
|
||||
v1.GroupInvitation:
|
||||
properties:
|
||||
expiresAt:
|
||||
|
@ -877,6 +884,23 @@ paths:
|
|||
summary: Retrieves the basic information about the API
|
||||
tags:
|
||||
- Base
|
||||
/v1/users/change-password:
|
||||
put:
|
||||
parameters:
|
||||
- description: Password Payload
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/v1.ChangePassword'
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Updates the users password
|
||||
tags:
|
||||
- User
|
||||
/v1/users/login:
|
||||
post:
|
||||
consumes:
|
||||
|
|
|
@ -65,6 +65,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
|||
r.Put(v1Base("/users/self/password"), v1Ctrl.HandleUserUpdatePassword())
|
||||
r.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout())
|
||||
r.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh())
|
||||
r.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword())
|
||||
|
||||
r.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate())
|
||||
|
||||
|
|
|
@ -36,7 +36,8 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
loginForm := &LoginForm{}
|
||||
|
||||
if r.Header.Get("Content-Type") == server.ContentFormUrlEncoded {
|
||||
switch r.Header.Get("Content-Type") {
|
||||
case server.ContentFormUrlEncoded:
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
server.Respond(w, http.StatusBadRequest, server.Wrap(err))
|
||||
|
@ -46,7 +47,7 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
|||
|
||||
loginForm.Username = r.PostFormValue("username")
|
||||
loginForm.Password = r.PostFormValue("password")
|
||||
} else if r.Header.Get("Content-Type") == server.ContentJSON {
|
||||
case server.ContentJSON:
|
||||
err := server.Decode(r, loginForm)
|
||||
|
||||
if err != nil {
|
||||
|
@ -54,7 +55,7 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
|||
server.Respond(w, http.StatusBadRequest, server.Wrap(err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
server.Respond(w, http.StatusBadRequest, errors.New("invalid content type"))
|
||||
return
|
||||
}
|
||||
|
@ -67,7 +68,7 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
|||
newToken, err := ctrl.svc.User.Login(r.Context(), loginForm.Username, loginForm.Password)
|
||||
|
||||
if err != nil {
|
||||
server.RespondError(w, http.StatusUnauthorized, err)
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -119,3 +119,37 @@ func (ctrl *V1Controller) HandleUserSelfDelete() http.HandlerFunc {
|
|||
server.Respond(w, http.StatusNoContent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
ChangePassword struct {
|
||||
Current string `json:"current,omitempty"`
|
||||
New string `json:"new,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// HandleUserSelfChangePassword godoc
|
||||
// @Summary Updates the users password
|
||||
// @Tags User
|
||||
// @Success 204
|
||||
// @Param payload body ChangePassword true "Password Payload"
|
||||
// @Router /v1/users/change-password [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfChangePassword() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var cp ChangePassword
|
||||
err := server.Decode(r, &cp)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("user failed to change password")
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
ok := ctrl.svc.User.ChangePassword(ctx, cp.Current, cp.New)
|
||||
if !ok {
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
server.Respond(w, http.StatusNoContent, nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,3 +121,7 @@ func (e *UserRepository) GetSuperusers(ctx context.Context) ([]*ent.User, error)
|
|||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) ChangePassword(ctx context.Context, UID uuid.UUID, pw string) error {
|
||||
return r.db.User.UpdateOneID(UID).SetPassword(pw).Exec(ctx)
|
||||
}
|
||||
|
|
|
@ -142,7 +142,13 @@ func (svc *UserService) createToken(ctx context.Context, userId uuid.UUID) (User
|
|||
func (svc *UserService) Login(ctx context.Context, username, password string) (UserAuthTokenDetail, error) {
|
||||
usr, err := svc.repos.Users.GetOneEmail(ctx, username)
|
||||
|
||||
if err != nil || !hasher.CheckPasswordHash(password, usr.PasswordHash) {
|
||||
if err != nil {
|
||||
// SECURITY: Perform hash to ensure response times are the same
|
||||
hasher.CheckPasswordHash("not-a-real-password", "not-a-real-password")
|
||||
return UserAuthTokenDetail{}, ErrorInvalidLogin
|
||||
}
|
||||
|
||||
if !hasher.CheckPasswordHash(password, usr.PasswordHash) {
|
||||
return UserAuthTokenDetail{}, ErrorInvalidLogin
|
||||
}
|
||||
|
||||
|
@ -190,3 +196,29 @@ func (svc *UserService) NewInvitation(ctx Context, uses int, expiresAt time.Time
|
|||
|
||||
return token.Raw, nil
|
||||
}
|
||||
|
||||
func (svc *UserService) ChangePassword(ctx Context, current string, new string) (ok bool) {
|
||||
usr, err := svc.repos.Users.GetOneId(ctx, ctx.UID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !hasher.CheckPasswordHash(current, usr.PasswordHash) {
|
||||
log.Err(errors.New("current password is incorrect")).Msg("Failed to change password")
|
||||
return false
|
||||
}
|
||||
|
||||
hashed, err := hasher.HashPassword(new)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to hash password")
|
||||
return false
|
||||
}
|
||||
|
||||
err = svc.repos.Users.ChangePassword(ctx.Context, ctx.UID, hashed)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to change password")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue