├── .gitignore ├── LICENSE ├── passwordreset_test.go ├── README.md └── passwordreset.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Dmitry Chestnykh 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /passwordreset_test.go: -------------------------------------------------------------------------------- 1 | package passwordreset 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var ( 10 | testLogin = "test user" 11 | testPwdVal = []byte("test password value") 12 | testSecret = []byte("secret key") 13 | testLoginError = errors.New("test unknown login error") 14 | ) 15 | 16 | func getPwdVal(login string) ([]byte, error) { 17 | if login == testLogin { 18 | return testPwdVal, nil 19 | } 20 | return testPwdVal, testLoginError 21 | // ^ return it anyway to test that it's not begin used 22 | } 23 | 24 | func TestNew(t *testing.T) { 25 | pwdVal, _ := getPwdVal(testLogin) 26 | token := NewToken(testLogin, 100*time.Second, pwdVal, testSecret) 27 | login, err := VerifyToken(token, getPwdVal, testSecret) 28 | if err != nil { 29 | t.Errorf("unexpected error %q", err) 30 | } 31 | if login != testLogin { 32 | t.Errorf("login: expected %q, got %q", testLogin, login) 33 | } 34 | } 35 | 36 | func TestNewNoPadding(t *testing.T) { 37 | pwdVal, _ := getPwdVal(testLogin) 38 | token := NewTokenNoPadding(testLogin, 100*time.Second, pwdVal, testSecret) 39 | login, err := VerifyToken(token, getPwdVal, testSecret) 40 | if err != nil { 41 | t.Errorf("unexpected error %q", err) 42 | } 43 | if login != testLogin { 44 | t.Errorf("login: expected %q, got %q", testLogin, login) 45 | } 46 | } 47 | 48 | func TestVerify(t *testing.T) { 49 | bad := []string{ 50 | "", 51 | "bad token", 52 | "Talo3mRjaGVzdITUAGOXYZwCMq7EtHfYH4ILcBgKaoWXDHTJOIlBUfcr", 53 | "Talo3mRjaGVzdITUAGOXYZwCMq7EtHfYH4ILcBgKaoWXDHTJOIlBUfcr=", 54 | } 55 | for i, token := range bad { 56 | login, err := VerifyToken(token, getPwdVal, testSecret) 57 | if login != "" { 58 | t.Errorf(`%d: login for bad token: expected "", got %q`, i, login) 59 | } 60 | if err == nil { 61 | t.Errorf("%d: expected error", i) 62 | } 63 | } 64 | // Test expiration 65 | pwdVal, _ := getPwdVal(testLogin) 66 | token := NewToken(testLogin, -1, pwdVal, testSecret) 67 | if _, err := VerifyToken(token, getPwdVal, testSecret); err == nil { 68 | t.Errorf("verified expired token") 69 | } 70 | // Test wrong password value 71 | pwdVal = []byte("wrong value") 72 | token = NewToken(testLogin, -1, pwdVal, testSecret) 73 | if _, err := VerifyToken(token, getPwdVal, testSecret); err == nil { 74 | t.Errorf("verified with wrong password value") 75 | } 76 | // Test password value error return 77 | login := "unknown login" 78 | _, errVal := getPwdVal(login) 79 | token = NewToken(login, 100*time.Second, testPwdVal, testSecret) 80 | if _, err := VerifyToken(token, getPwdVal, testSecret); err != errVal { 81 | t.Errorf("err: expected %q, got %q", errVal, err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Package passwordreset 2 | ===================== 3 | 4 | import "github.com/dchest/passwordreset" 5 | 6 | Package passwordreset implements creation and verification of secure tokens 7 | useful for implementation of "reset forgotten password" feature in web 8 | applications. 9 | 10 | This package generates and verifies signed one-time tokens that can be 11 | embedded in a link sent to users when they initiate the password reset 12 | procedure. When a user changes their password, or when the expiry time 13 | passes, the token becomes invalid. 14 | 15 | Secure token format: 16 | 17 | expiration time || login || signature 18 | 19 | where expiration time is the number of seconds since Unix epoch UTC 20 | indicating when this token must expire (4 bytes, big-endian, uint32), login 21 | is a byte string of arbitrary length (at least 1 byte, not null-terminated), 22 | and signature is 32 bytes of HMAC-SHA256(expiration_time || login, k), where 23 | k = HMAC-SHA256(expiration_time || login, userkey), where userkey = 24 | HMAC-SHA256(password value, secret key), where password value is any piece 25 | of information derived from user's password, which will change once the user 26 | changes their password (for example, a hash of the password), and secret key 27 | is an application-specific secret key. 28 | 29 | Password value is used to make tokens one-time, that is, once a user changes 30 | their password, the token which they used to do a reset, becomes invalid. 31 | 32 | Usage example: 33 | 34 | Your application must have a strong secret key for password reset purposes. 35 | This key will be used to generate and verify password reset tokens. (If you 36 | already have a secret key, for example, for authcookie package, it's better 37 | not to reuse it, just use a different one.) 38 | 39 | secret := []byte("assume we have a long randomly generated secret key here") 40 | 41 | Create a function that will query your users database and return some 42 | password-related value for the given login. A password-related value means 43 | some value that will change once a user changes their password, for example: 44 | a password hash, a random salt used to generate it, or time of password 45 | creation. This value, mixed with app-specific secret key, will be used as a 46 | key for password reset token, thus it will be kept secret. 47 | 48 | func getPasswordHash(login string) ([]byte, error) { 49 | // return password hash for the login, 50 | // or an error if there's no such user 51 | } 52 | 53 | When a user initiates password reset (by entering their login, and maybe 54 | answering a secret question), generate a reset token: 55 | 56 | pwdval, err := getPasswordHash(login) 57 | if err != nil { 58 | // user doesn't exists, abort 59 | return 60 | } 61 | // Generate reset token that expires in 12 hours 62 | token := passwordreset.NewToken(login, 12 * time.Hour, pwdval, secret) 63 | 64 | Send a link with this token to the user by email, for example: 65 | https://www.example.com/reset?token=Talo3mRjaGVzdITUAGOXYZwCMq7EtHfYH4ILcBgKaoWXDHTJOIlBUfcr 66 | 67 | Once a user clicks this link, read a token from it, then verify this token 68 | by passing it to VerifyToken function along with the getPasswordHash 69 | function, and an app-specific secret key: 70 | 71 | login, err := passwordreset.VerifyToken(token, getPasswordHash, secret) 72 | if err != nil { 73 | // verification failed, don't allow password reset 74 | return 75 | } 76 | // OK, reset password for login (e.g. allow to change it) 77 | 78 | If verification succeeded, allow to change password for the returned login. 79 | 80 | 81 | Variables 82 | --------- 83 | 84 | var ( 85 | ErrMalformedToken = errors.New("malformed token") 86 | ErrExpiredToken = errors.New("token expired") 87 | ErrWrongSignature = errors.New("wrong token signature") 88 | ) 89 | 90 | 91 | var MinTokenLength = authcookie.MinLength 92 | 93 | MinTokenLength is the minimum allowed length of token string. 94 | 95 | It is useful for avoiding DoS attacks with very long tokens: before passing 96 | a token to VerifyToken function, check that it has length less than [the 97 | maximum login length allowed in your application] + MinTokenLength. 98 | 99 | 100 | Functions 101 | --------- 102 | 103 | ### func NewToken 104 | 105 | func NewToken(login string, dur time.Duration, pwdval, secret []byte) string 106 | 107 | NewToken returns a new password reset token for the given login, which expires 108 | after the given time duration since now, signed by the key generated from the 109 | given password value (which can be any value that will be changed once a user 110 | resets their password, such as password hash or salt used to generate it), and 111 | the given secret key. 112 | 113 | ### func VerifyToken 114 | 115 | func VerifyToken(token string, pwdvalFn func(string) ([]byte, error), secret []byte) (login string, err os.Error) 116 | 117 | VerifyToken verifies the given token with the password value returned by the 118 | given function and the given secret key, and returns login extracted from 119 | the valid token. If the token is not valid, the function returns an error. 120 | 121 | Function pwdvalFn must return the current password value for the login it 122 | receives in arguments, or an error. If it returns an error, VerifyToken 123 | returns the same error. 124 | 125 | -------------------------------------------------------------------------------- /passwordreset.go: -------------------------------------------------------------------------------- 1 | // Package passwordreset implements creation and verification of secure tokens 2 | // useful for implementation of "reset forgotten password" feature in web 3 | // applications. 4 | // 5 | // This package generates and verifies signed one-time tokens that can be 6 | // embedded in a link sent to users when they initiate the password reset 7 | // procedure. When a user changes their password, or when the expiry time 8 | // passes, the token becomes invalid. 9 | // 10 | // Secure token format: 11 | // 12 | // expiration time || login || signature 13 | // 14 | // where expiration time is the number of seconds since Unix epoch UTC 15 | // indicating when this token must expire (4 bytes, big-endian, uint32), login 16 | // is a byte string of arbitrary length (at least 1 byte, not null-terminated), 17 | // and signature is 32 bytes of HMAC-SHA256(expiration_time || login, k), where 18 | // k = HMAC-SHA256(expiration_time || login, userkey), where userkey = 19 | // HMAC-SHA256(password value, secret key), where password value is any piece 20 | // of information derived from user's password, which will change once the user 21 | // changes their password (for example, a hash of the password), and secret key 22 | // is an application-specific secret key. 23 | // 24 | // Password value is used to make tokens one-time, that is, once a user changes 25 | // their password, the token which they used to do a reset, becomes invalid. 26 | // 27 | // 28 | // 29 | // Usage example: 30 | // 31 | // Your application must have a strong secret key for password reset purposes. 32 | // This key will be used to generate and verify password reset tokens. (If you 33 | // already have a secret key, for example, for authcookie package, it's better 34 | // not to reuse it, just use a different one.) 35 | // 36 | // secret := []byte("assume we have a long randomly generated secret key here") 37 | // 38 | // Create a function that will query your users database and return some 39 | // password-related value for the given login. A password-related value means 40 | // some value that will change once a user changes their password, for example: 41 | // a password hash, a random salt used to generate it, or time of password 42 | // creation. This value, mixed with app-specific secret key, will be used as a 43 | // key for password reset token, thus it will be kept secret. 44 | // 45 | // func getPasswordHash(login string) ([]byte, error) { 46 | // // return password hash for the login, 47 | // // or an error if there's no such user 48 | // } 49 | // 50 | // When a user initiates password reset (by entering their login, and maybe 51 | // answering a secret question), generate a reset token: 52 | // 53 | // pwdval, err := getPasswordHash(login) 54 | // if err != nil { 55 | // // user doesn't exists, abort 56 | // return 57 | // } 58 | // // Generate reset token that expires in 12 hours 59 | // token := passwordreset.NewToken(login, 12 * time.Hour, pwdval, secret) 60 | // 61 | // Send a link with this token to the user by email, for example: 62 | // https://www.example.com/reset?token=Talo3mRjaGVzdITUAGOXYZwCMq7EtHfYH4ILcBgKaoWXDHTJOIlBUfcr 63 | // 64 | // Once a user clicks this link, read a token from it, then verify this token 65 | // by passing it to VerifyToken function along with the getPasswordHash 66 | // function, and an app-specific secret key: 67 | // 68 | // login, err := passwordreset.VerifyToken(token, getPasswordHash, secret) 69 | // if err != nil { 70 | // // verification failed, don't allow password reset 71 | // return 72 | // } 73 | // // OK, reset password for login (e.g. allow to change it) 74 | // 75 | // If verification succeeded, allow to change password for the returned login. 76 | // 77 | package passwordreset 78 | 79 | import ( 80 | "crypto/hmac" 81 | "crypto/sha256" 82 | "crypto/subtle" 83 | "encoding/base64" 84 | "encoding/binary" 85 | "errors" 86 | "github.com/dchest/authcookie" 87 | "strings" 88 | "time" 89 | ) 90 | 91 | // MinTokenLength is the minimum allowed length of token string. 92 | // 93 | // It is useful for avoiding DoS attacks with very long tokens: before passing 94 | // a token to VerifyToken function, check that it has length less than [the 95 | // maximum login length allowed in your application] + MinTokenLength. 96 | var MinTokenLength = authcookie.MinLength 97 | 98 | var ( 99 | ErrMalformedToken = errors.New("malformed token") 100 | ErrExpiredToken = errors.New("token expired") 101 | ErrWrongSignature = errors.New("wrong token signature") 102 | ) 103 | 104 | func getUserSecretKey(pwdval, secret []byte) []byte { 105 | m := hmac.New(sha256.New, secret) 106 | m.Write(pwdval) 107 | return m.Sum(nil) 108 | } 109 | 110 | func getSignature(b []byte, secret []byte) []byte { 111 | keym := hmac.New(sha256.New, secret) 112 | keym.Write(b) 113 | m := hmac.New(sha256.New, keym.Sum(nil)) 114 | m.Write(b) 115 | return m.Sum(nil) 116 | } 117 | 118 | // NewToken returns a new password reset token for the given login, which 119 | // expires after the given time duration since now, signed by the key generated 120 | // from the given password value (which can be any value that will be changed 121 | // once a user resets their password, such as password hash or salt used to 122 | // generate it), and the given secret key. 123 | func NewToken(login string, dur time.Duration, pwdval, secret []byte) string { 124 | sk := getUserSecretKey(pwdval, secret) 125 | return authcookie.NewSinceNow(login, dur, sk) 126 | } 127 | 128 | func NewTokenNoPadding(login string, dur time.Duration, pwdval, secret []byte) string { 129 | sk := getUserSecretKey(pwdval, secret) 130 | return authcookie.NewSinceNowNoPadding(login, dur, sk) 131 | } 132 | 133 | // VerifyToken verifies the given token with the password value returned by the 134 | // given function and the given secret key, and returns login extracted from 135 | // the valid token. If the token is not valid, the function returns an error. 136 | // 137 | // Function pwdvalFn must return the current password value for the login it 138 | // receives in arguments, or an error. If it returns an error, VerifyToken 139 | // returns the same error. 140 | func VerifyToken(token string, pwdvalFn func(string) ([]byte, error), secret []byte) (login string, err error) { 141 | encoding := base64.RawURLEncoding 142 | // If we have padding, use URLEncoding instead of RawURLEncoding. 143 | if strings.LastIndexByte(token, '=') != -1 { 144 | encoding = base64.URLEncoding 145 | } 146 | blen := encoding.DecodedLen(len(token)) 147 | // Avoid allocation if the token is too short 148 | if blen <= 4+32 { 149 | err = ErrMalformedToken 150 | return 151 | } 152 | b := make([]byte, blen) 153 | blen, err = encoding.Decode(b, []byte(token)) 154 | if err != nil { 155 | return 156 | } 157 | // Decoded length may be bifferent from max length, which 158 | // we allocated, so check it, and set new length for b 159 | if blen <= 4+32 { 160 | err = ErrMalformedToken 161 | return 162 | } 163 | b = b[:blen] 164 | 165 | data := b[:blen-32] 166 | exp := time.Unix(int64(binary.BigEndian.Uint32(data[:4])), 0) 167 | if exp.Before(time.Now()) { 168 | err = ErrExpiredToken 169 | return 170 | } 171 | login = string(data[4:]) 172 | pwdval, err := pwdvalFn(login) 173 | if err != nil { 174 | login = "" 175 | return 176 | } 177 | sig := b[blen-32:] 178 | sk := getUserSecretKey(pwdval, secret) 179 | realSig := getSignature(data, sk) 180 | if subtle.ConstantTimeCompare(realSig, sig) != 1 { 181 | err = ErrWrongSignature 182 | return 183 | } 184 | return 185 | } 186 | --------------------------------------------------------------------------------