Skip to content

Instantly share code, notes, and snippets.

@freman
Created November 8, 2018 04:20
Show Gist options
  • Save freman/edd439a65504a72dea692fb32cd63ec9 to your computer and use it in GitHub Desktop.
Save freman/edd439a65504a72dea692fb32cd63ec9 to your computer and use it in GitHub Desktop.
Just a password reset proof of concept no-one will use.
/*
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