Skip to content

Commit e83110e

Browse files
committed
Add UpdatePassword.
- Fix aarondl#50
1 parent 04d2716 commit e83110e

3 files changed

Lines changed: 180 additions & 2 deletions

File tree

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Once you've got this code set up, it's time to implement the use cases you care
6868
<a name="use_cases"></a>Use Cases
6969
=================================
7070
- Get the logged in user ([goto](#current_user))
71+
- Reset a User's password ([goto](#reset_password))
7172
- User authentication via password ([goto](#auth))
7273
- User authentication via OAuth2 ([goto](#oauth2))
7374
- User registration ([goto](#register))
@@ -103,6 +104,34 @@ nil, ErrUserNotFound | Session had user ID, but user not found in database.
103104
nil, err | Some horrible error has occurred.
104105
user struct, nil | The user is logged in.
105106

107+
## <a name="reset_password"></a>Reset a User's password
108+
109+
Because on password reset various cleanings need to happen (for example Remember Me tokens
110+
should all be deleted) setting the password yourself is not a good idea.
111+
112+
Authboss has the UpdatePassword method for you to use. Please consult it's documentation
113+
for a thorough explanation of each parameter.
114+
115+
```go
116+
func UpdatePassword(w http.ResponseWriter, r *http.Request, ptPassword string, user interface{}, updater func() error) error {
117+
```
118+
119+
Please read it's documentation as it's quite thorough, and example usage might be:
120+
121+
```go
122+
myUserSave := func() error {
123+
_, err := db.Exec(`update user set name = $1, password = $2 where id = $3`, user.Name, user.Password, user.ID)
124+
return err
125+
}
126+
127+
// HINT: Never pass the form value directly into the database as you see here :D
128+
err := UpdatePassword(w, r, r.FormValue("password"), &user1, myUserSave)
129+
if err != nil {
130+
// Handle error here, in most cases this will be the error from myUserSave
131+
}
132+
133+
```
134+
106135
## <a name="auth"></a>User Authentication via Password
107136
**Requirements:**
108137
- Auth module ([gopkg.in/authboss.v0/auth](https://github.com/go-authboss/authboss/tree/master/auth))

authboss.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ races without having to think about how to store passwords or remember tokens.
77
package authboss // import "gopkg.in/authboss.v0"
88

99
import (
10+
"database/sql"
11+
"errors"
1012
"fmt"
1113
"net/http"
14+
"reflect"
1215
"strings"
16+
17+
"golang.org/x/crypto/bcrypt"
1318
)
1419

1520
// Init authboss and it's loaded modules.
@@ -70,3 +75,66 @@ func CurrentUserP(w http.ResponseWriter, r *http.Request) interface{} {
7075
}
7176
return i
7277
}
78+
79+
/*
80+
UpdatePassword should be called to recalculate hashes and do any cleanup
81+
that should occur on password resets. Updater should return an error if the
82+
update to the user failed (for reasons say like validation, duplicate
83+
primary key, etc...). In that case the cleanup will not be performed.
84+
85+
The w and r parameters are for establishing session and cookie storers.
86+
87+
The ptPassword parameter is for the password to update to, if it is empty then
88+
nothing is updated and the cleanup routines are not called.
89+
90+
The user parameter is the user struct which will have it's
91+
Password string/sql.NullString value set to the new bcrypted password. Therefore
92+
it must be passed in as a pointer with the Password field exported or an error
93+
will be returned.
94+
95+
The error returned is returned either from the updater if that produced an error
96+
or from the cleanup routines.
97+
*/
98+
func UpdatePassword(w http.ResponseWriter, r *http.Request,
99+
ptPassword string, user interface{}, updater func() error) error {
100+
101+
updatePwd := len(ptPassword) > 0
102+
103+
if updatePwd {
104+
pass, err := bcrypt.GenerateFromPassword([]byte(ptPassword), Cfg.BCryptCost)
105+
if err != nil {
106+
return err
107+
}
108+
109+
val := reflect.ValueOf(user).Elem()
110+
field := val.FieldByName("Password")
111+
if !field.CanSet() {
112+
return errors.New("authboss: UpdatePassword called without a modifyable user struct")
113+
}
114+
fieldPtr := field.Addr()
115+
116+
if scanner, ok := fieldPtr.Interface().(sql.Scanner); ok {
117+
if err := scanner.Scan(string(pass)); err != nil {
118+
return err
119+
}
120+
} else {
121+
field.SetString(string(pass))
122+
}
123+
}
124+
125+
if err := updater(); err != nil {
126+
return err
127+
}
128+
129+
if !updatePwd {
130+
return nil
131+
}
132+
133+
ctx, err := ContextFromRequest(r)
134+
if err != nil {
135+
return err
136+
}
137+
ctx.SessionStorer = clientStoreWrapper{Cfg.SessionStoreMaker(w, r)}
138+
ctx.CookieStorer = clientStoreWrapper{Cfg.CookieStoreMaker(w, r)}
139+
return Cfg.Callbacks.FireAfter(EventPasswordReset, ctx)
140+
}

authboss_test.go

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package authboss
22

33
import (
4+
"database/sql"
5+
"errors"
46
"net/http"
57
"net/http/httptest"
68
"os"
@@ -15,15 +17,15 @@ func TestMain(main *testing.M) {
1517
}
1618

1719
func TestAuthBossInit(t *testing.T) {
18-
NewConfig()
20+
Cfg = NewConfig()
1921
err := Init()
2022
if err != nil {
2123
t.Error("Unexpected error:", err)
2224
}
2325
}
2426

2527
func TestAuthBossCurrentUser(t *testing.T) {
26-
NewConfig()
28+
Cfg = NewConfig()
2729
Cfg.Storer = mockStorer{"joe": Attributes{"email": "john@john.com", "password": "lies"}}
2830
Cfg.SessionStoreMaker = func(_ http.ResponseWriter, _ *http.Request) ClientStorer {
2931
return mockClientStore{SessionKey: "joe"}
@@ -46,3 +48,82 @@ func TestAuthBossCurrentUser(t *testing.T) {
4648
t.Error("Wrong user found!")
4749
}
4850
}
51+
52+
func TestAuthbossUpdatePassword(t *testing.T) {
53+
Cfg = NewConfig()
54+
session := mockClientStore{}
55+
cookies := mockClientStore{}
56+
Cfg.SessionStoreMaker = func(_ http.ResponseWriter, _ *http.Request) ClientStorer {
57+
return session
58+
}
59+
Cfg.CookieStoreMaker = func(_ http.ResponseWriter, _ *http.Request) ClientStorer {
60+
return cookies
61+
}
62+
63+
called := false
64+
Cfg.Callbacks.After(EventPasswordReset, func(ctx *Context) error {
65+
called = true
66+
return nil
67+
})
68+
69+
user1 := struct {
70+
Password string
71+
}{}
72+
user2 := struct {
73+
Password sql.NullString
74+
}{}
75+
76+
r, _ := http.NewRequest("GET", "http://localhost", nil)
77+
78+
called = false
79+
err := UpdatePassword(nil, r, "newpassword", &user1, func() error { return nil })
80+
if err != nil {
81+
t.Error(err)
82+
}
83+
84+
if len(user1.Password) == 0 {
85+
t.Error("Password not updated")
86+
}
87+
if !called {
88+
t.Error("Callbacks should have been called.")
89+
}
90+
91+
called = false
92+
err = UpdatePassword(nil, r, "newpassword", &user2, func() error { return nil })
93+
if err != nil {
94+
t.Error(err)
95+
}
96+
97+
if !user2.Password.Valid || len(user2.Password.String) == 0 {
98+
t.Error("Password not updated")
99+
}
100+
if !called {
101+
t.Error("Callbacks should have been called.")
102+
}
103+
104+
called = false
105+
oldPassword := user1.Password
106+
err = UpdatePassword(nil, r, "", &user1, func() error { return nil })
107+
if err != nil {
108+
t.Error(err)
109+
}
110+
111+
if user1.Password != oldPassword {
112+
t.Error("Password not updated")
113+
}
114+
if called {
115+
t.Error("Callbacks should not have been called")
116+
}
117+
}
118+
119+
func TestAuthbossUpdatePasswordFail(t *testing.T) {
120+
user1 := struct {
121+
Password string
122+
}{}
123+
124+
anErr := errors.New("AnError")
125+
err := UpdatePassword(nil, nil, "update", &user1, func() error { return anErr })
126+
if err != anErr {
127+
t.Error("Expected an specific error:", err)
128+
}
129+
}

0 commit comments

Comments
 (0)