feat(crypto): HMAC signed session tokens
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// token = base64(user) "." expiryUnix "." base64(hmac)
|
||||||
|
func SignSession(secret []byte, user string, expiry time.Time) string {
|
||||||
|
payload := base64.RawURLEncoding.EncodeToString([]byte(user)) + "." +
|
||||||
|
strconv.FormatInt(expiry.Unix(), 10)
|
||||||
|
return payload + "." + sign(secret, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifySession(secret []byte, token string, now time.Time) (string, bool) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
payload := parts[0] + "." + parts[1]
|
||||||
|
if !hmac.Equal([]byte(parts[2]), []byte(sign(secret, payload))) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
exp, err := strconv.ParseInt(parts[1], 10, 64)
|
||||||
|
if err != nil || now.Unix() > exp {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
user, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return string(user), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func sign(secret []byte, payload string) string {
|
||||||
|
m := hmac.New(sha256.New, secret)
|
||||||
|
fmt.Fprint(m, payload)
|
||||||
|
return base64.RawURLEncoding.EncodeToString(m.Sum(nil))
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSessionRoundTrip(t *testing.T) {
|
||||||
|
secret := []byte("s3cr3t")
|
||||||
|
now := time.Unix(1_700_000_000, 0)
|
||||||
|
tok := SignSession(secret, "admin", now.Add(time.Hour))
|
||||||
|
user, ok := VerifySession(secret, tok, now)
|
||||||
|
if !ok || user != "admin" {
|
||||||
|
t.Fatalf("verify = %q,%v want admin,true", user, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionRejectsExpired(t *testing.T) {
|
||||||
|
secret := []byte("s3cr3t")
|
||||||
|
now := time.Unix(1_700_000_000, 0)
|
||||||
|
tok := SignSession(secret, "admin", now.Add(-time.Second))
|
||||||
|
if _, ok := VerifySession(secret, tok, now); ok {
|
||||||
|
t.Fatal("expired token must be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionRejectsTampered(t *testing.T) {
|
||||||
|
secret := []byte("s3cr3t")
|
||||||
|
now := time.Unix(1_700_000_000, 0)
|
||||||
|
tok := SignSession(secret, "admin", now.Add(time.Hour))
|
||||||
|
if _, ok := VerifySession([]byte("other"), tok, now); ok {
|
||||||
|
t.Fatal("wrong secret must be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user