Skip to content

Commit 3af7950

Browse files
committed
require user hash
1 parent 41bc1fd commit 3af7950

File tree

6 files changed

+95
-3
lines changed

6 files changed

+95
-3
lines changed

config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ type Config struct {
1313
ShutdownTimeout uint
1414
// MySQL database DSN.
1515
MySQLDSN string
16+
// Secret for signing user IDs.
17+
Secret string
1618
}
1719

1820
func NewConfig() (*Config, error) {

internal/pas/event.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
type Event struct {
1111
UserID UserID `json:"user_id"`
12+
UserHash string `json:"user_hash"`
1213
Timestamp *time.Time `json:"timestamp"`
1314
Name EventName `json:"name"`
1415
Properties []Property `json:"properties"`

internal/pas/handler.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package pas
22

33
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/hex"
47
"encoding/json"
58
"net/http"
69
)
710

811
type Handler struct {
912
http.Handler
1013
analytics *Analytics
14+
secret []byte
1115
}
1216

13-
func NewHandler(analytics *Analytics) *Handler {
17+
func NewHandler(analytics *Analytics, secret string) *Handler {
1418
h := &Handler{
1519
analytics: analytics,
20+
secret: []byte(secret),
1621
}
1722
mux := http.NewServeMux()
1823
mux.HandleFunc("/api/events", h.handleEvents)
@@ -34,6 +39,17 @@ func (s *Handler) handleEvents(w http.ResponseWriter, r *http.Request) {
3439
http.Error(w, err.Error(), http.StatusBadRequest)
3540
return
3641
}
42+
if len(s.secret) > 0 {
43+
hash := hmac.New(sha256.New, s.secret)
44+
for _, e := range events.Events {
45+
hash.Write([]byte(e.UserID))
46+
if hex.EncodeToString(hash.Sum(nil)) != e.UserHash {
47+
http.Error(w, "invalid user_hash", http.StatusBadRequest)
48+
return
49+
}
50+
hash.Reset()
51+
}
52+
}
3753
_, err = s.analytics.InsertEvents(events.Events)
3854
if err != nil {
3955
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -53,6 +69,17 @@ func (s *Handler) handleUsers(w http.ResponseWriter, r *http.Request) {
5369
http.Error(w, err.Error(), http.StatusBadRequest)
5470
return
5571
}
72+
if len(s.secret) > 0 {
73+
hash := hmac.New(sha256.New, s.secret)
74+
for _, u := range users.Users {
75+
hash.Write([]byte(u.ID))
76+
if hex.EncodeToString(hash.Sum(nil)) != u.Hash {
77+
http.Error(w, "invalid user_hash", http.StatusBadRequest)
78+
return
79+
}
80+
hash.Reset()
81+
}
82+
}
5683
_, err = s.analytics.UpdateUsers(users.Users)
5784
if err != nil {
5885
http.Error(w, err.Error(), http.StatusInternalServerError)

internal/pas/handler_test.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package pas_test
22

33
import (
44
"bytes"
5+
"crypto/hmac"
6+
"crypto/sha256"
57
"database/sql"
8+
"encoding/hex"
9+
"fmt"
610
"log"
711
"net/http"
812
"net/http/httptest"
@@ -23,7 +27,7 @@ func init() {
2327

2428
analytics := pas.NewAnalytics(db)
2529

26-
handler = pas.NewHandler(analytics)
30+
handler = pas.NewHandler(analytics, "")
2731
}
2832

2933
func TestPostEvents(t *testing.T) {
@@ -67,3 +71,60 @@ func TestPostUsers(t *testing.T) {
6771
status, http.StatusOK)
6872
}
6973
}
74+
75+
func TestUserHash(t *testing.T) {
76+
const secret = "foobar"
77+
78+
db, err := sql.Open("mysql", localDSN)
79+
if err != nil {
80+
log.Fatal(err)
81+
}
82+
defer db.Close()
83+
84+
analytics := pas.NewAnalytics(db)
85+
86+
handler := pas.NewHandler(analytics, secret)
87+
88+
s0 := `{
89+
"events": [
90+
{"name": "test_done", "user_id": "1234", "user_hash": "%s", "timestamp": "2000-01-01T01:02:03Z", "properties": [
91+
{"name": "foo", "value": "bar", "type": "string"}
92+
]}]}
93+
`
94+
95+
// Test invalid secret
96+
s := fmt.Sprintf(s0, generateUserHash("1234", "invalid"))
97+
var postBody = bytes.NewBufferString(s)
98+
req, err := http.NewRequest("POST", "/api/events", postBody)
99+
if err != nil {
100+
t.Fatal(err)
101+
}
102+
rr := httptest.NewRecorder()
103+
handler.ServeHTTP(rr, req)
104+
if status := rr.Code; status != http.StatusBadRequest {
105+
t.Log(rr.Body.String())
106+
t.Errorf("handler returned wrong status code: got %v want %v",
107+
status, http.StatusOK)
108+
}
109+
110+
// Test correct secret
111+
s = fmt.Sprintf(s0, generateUserHash("1234", secret))
112+
postBody = bytes.NewBufferString(s)
113+
req, err = http.NewRequest("POST", "/api/events", postBody)
114+
if err != nil {
115+
t.Fatal(err)
116+
}
117+
rr = httptest.NewRecorder()
118+
handler.ServeHTTP(rr, req)
119+
if status := rr.Code; status != http.StatusOK {
120+
t.Log(rr.Body.String())
121+
t.Errorf("handler returned wrong status code: got %v want %v",
122+
status, http.StatusOK)
123+
}
124+
}
125+
126+
func generateUserHash(userID, secret string) string {
127+
hash := hmac.New(sha256.New, []byte(secret))
128+
hash.Write([]byte(userID))
129+
return hex.EncodeToString(hash.Sum(nil))
130+
}

internal/pas/user.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
type User struct {
99
ID UserID `json:"id"`
10+
Hash string `json:"hash"`
1011
Properties []Property `json:"properties"`
1112
}
1213

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func main() {
5252
}()
5353

5454
analytics := pas.NewAnalytics(db)
55-
handler := pas.NewHandler(analytics)
55+
handler := pas.NewHandler(analytics, config.Secret)
5656
server := pas.NewServer(config.ListenAddress, handler)
5757

5858
go server.ListenAndServe()

0 commit comments

Comments
 (0)