Created
November 8, 2018 04:20
-
-
Save freman/edd439a65504a72dea692fb32cd63ec9 to your computer and use it in GitHub Desktop.
Just a password reset proof of concept no-one will use.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
It's commonly the case that users will hit "reset password" then get impatient | |
waiting for an email and hit it again thinking that some form of magic will increase | |
the priority of their request and make them get the email faster. | |
In a lot of circumstances that results in the original token in the database being | |
overwritten which makes it invalid. This frustrates the user when a token finally | |
turns up and they click on it only to have it fail. | |
The code below is a proof of concept that makes it possible to have any reset | |
token work as long as it was generated *after* the last time the password was | |
reset. It does this by remembering the last time the pasword was reset vs every | |
single token that was generated, and generating a signed token containing their | |
userid and a timestamp. | |
$ go run pwreset.go | |
$ curl http://localhost:9090/reset/34948938-19c5-4bef-bdcf-c2b38ab90d86 | |
2Rb9bpa6uExy7abg2KbtZh4pMzKcNDVPvALpdZGNUseetjKZToh5ocNPj4KH4DctRvij1vkiVxEbY | |
$ curl http://localhost:9090/reset/34948938-19c5-4bef-bdcf-c2b38ab90d86 | |
2Rb9bpa6uExy7abg2KbtZh4v8Ha1UNZYNFqfahxU3GYPqbhrtySjWXdNMaXip282HnshXHhBuguMA | |
$ curl http://localhost:9090/reset/34948938-19c5-4bef-bdcf-c2b38ab90d86 | |
2Rb9bpa6uExy7abg2KbtZh4v8Ha1UNZYNFqfahxU3GYPqbhrtySjWXdNMaXip282HnshXHhBuguMA | |
$ curl http://localhost:9090/reset/34948938-19c5-4bef-bdcf-c2b38ab90d86 | |
2Rb9bpa6uExy7abg2KbtZh4y1SCD2T6cbAG3Jfw3mbBXXsL8D6runJtzgCYo4CqFjByPvf9DwZ1qg | |
$ curl http://localhost:9090/reset/34948938-19c5-4bef-bdcf-c2b38ab90d86 | |
2Rb9bpa6uExy7abg2KbtZh4y1SCD2T6cbAG3Jfw3mbBXXsL8D6runJtzgCYo4CqFjByPvf9DwZ1qg | |
# Try the token from the middle of that pack | |
$ curl http://localhost:9090/verify/2Rb9bpa6uExy7abg2KbtZh4v8Ha1UNZYNFqfahxU3GYPqbhrtySjWXdNMaXip282HnshXHhBuguMA | |
ok | |
# Try the token from the end of that pack | |
curl http://localhost:9090/verify/2Rb9bpa6uExy7abg2KbtZh4y1SCD2T6cbAG3Jfw3mbBXXsL8D6runJtzgCYo4CqFjByPvf9DwZ1qg | |
Token is no longer valid | |
*/ | |
package main | |
import ( | |
"crypto" | |
"crypto/hmac" | |
"encoding/binary" | |
"fmt" | |
"net/http" | |
"sync" | |
"time" | |
"github.com/google/uuid" | |
"github.com/gorilla/mux" | |
"github.com/mr-tron/base58/base58" | |
) | |
const ( | |
uuidSize = 16 | |
timestampSize = 8 | |
sha256Size = 32 | |
dataSize = uuidSize + timestampSize | |
tokenSize = dataSize + sha256Size | |
tokenLifetime = 1 * time.Hour | |
) | |
var privateKey = []byte("some block of bytes egh") | |
func main() { | |
// Serves to take the place of an actual data store for the purpose of this | |
// little demonstration. | |
var m sync.Map | |
r := mux.NewRouter() | |
r.HandleFunc("/reset/{id}", func(w http.ResponseWriter, r *http.Request) { | |
v := mux.Vars(r) | |
id, err := uuid.Parse(v["id"]) | |
if err != nil { | |
http.Error(w, err.Error(), http.StatusInternalServerError) | |
return | |
} | |
buf := make([]byte, tokenSize) | |
copy(buf[0:uuidSize], id[:]) | |
binary.LittleEndian.PutUint64(buf[uuidSize:dataSize], uint64(time.Now().Unix())) | |
hash := hmac.New(crypto.SHA256.New, privateKey) | |
hash.Write(buf[0:dataSize]) | |
copy(buf[dataSize:tokenSize], hash.Sum(nil)) | |
fmt.Fprintln(w, base58.Encode(buf)) | |
}) | |
r.HandleFunc("/verify/{hash}", func(w http.ResponseWriter, r *http.Request) { | |
v := mux.Vars(r) | |
buf, err := base58.Decode(v["hash"]) | |
if err != nil { | |
http.Error(w, err.Error(), http.StatusInternalServerError) | |
return | |
} | |
if len(buf) < tokenSize { | |
http.Error(w, "Token is not valid", http.StatusBadRequest) | |
return | |
} | |
hash := hmac.New(crypto.SHA256.New, []byte("hello world")) | |
hash.Write(buf[0:dataSize]) | |
cmp := hash.Sum(nil) | |
if !hmac.Equal(cmp, buf[dataSize:tokenSize]) { | |
http.Error(w, "Invalid", http.StatusBadRequest) | |
return | |
} | |
id, err := uuid.FromBytes(buf[0:uuidSize]) | |
if err != nil { | |
http.Error(w, err.Error(), http.StatusInternalServerError) | |
return | |
} | |
ts := time.Unix(int64(binary.LittleEndian.Uint64(buf[uuidSize:dataSize])), 0) | |
if time.Now().Sub(ts) > tokenLifetime { | |
http.Error(w, "Token has expired", http.StatusBadRequest) | |
return | |
} | |
iface, found := m.Load(id) | |
if found && iface.(time.Time).After(ts) { | |
http.Error(w, "Token is no longer valid", http.StatusBadRequest) | |
return | |
} | |
m.Store(id, time.Now()) | |
fmt.Fprintln(w, "ok") | |
}) | |
if err := http.ListenAndServe(":9090", r); err != nil { | |
panic(err) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment