// Package authtoken signs the short tokens hammerhead hands to visitors and blacktip verifies. package authtoken import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "strings" "time" ) const ( KindCallback = "cb" // minted by hammerhead after a successful login, valid for minutes KindSession = "sess" // minted by blacktip for the visitor's cookie ) type Token struct { Mapping string `json:"m"` Sub string `json:"sub"` // who: an email, "password", "pin", "share:name" Kind string `json:"k"` Exp int64 `json:"exp"` // Epoch is the resource's login generation when this was issued. Hammerhead // bumps it to sign every visitor out (a member removed, a password changed); // tokens from an older epoch are then refused without any per-token state. Epoch int64 `json:"ep"` } var enc = base64.RawURLEncoding func mac(key, payload string) string { m := hmac.New(sha256.New, []byte(key)) m.Write([]byte(payload)) return enc.EncodeToString(m.Sum(nil)) } func Sign(key string, t Token) string { b, _ := json.Marshal(t) p := enc.EncodeToString(b) return p + "." + mac(key, p) } // Verify checks the signature, kind, mapping, epoch and expiry. func Verify(key, tok, kind, mapping string, epoch int64) (Token, bool) { var t Token p, sig, found := strings.Cut(tok, ".") if !found || !hmac.Equal([]byte(sig), []byte(mac(key, p))) { return t, false } b, err := enc.DecodeString(p) if err != nil || json.Unmarshal(b, &t) != nil { return t, false } if t.Kind != kind || t.Mapping != mapping || t.Epoch != epoch || time.Now().Unix() > t.Exp { return t, false } return t, true }