owpengram-server/cmd/telesrv-admin/session.go
Egor Egorov fff8de783a
feat: add NFT usernames and bot verification (#22)
Implements collectible usernames, official verification workflows, and third-party bot verification after maintainer protocol and migration review.

The composite activity/moderation rating remains an admin-only read model; Telegram Stars Rating wire fields stay unset pending a dedicated official-semantics implementation.

Reviewed-Head: 2796345775ea0f908fb7734601e5e1dee4b653b9
Original-Head: fa082b892fd5180c9c9bc53c81c21cf5d250a75b

Co-authored-by: Egor Egorov <business.egor.sg@gmail.com>
2026-07-28 01:18:00 +08:00

118 lines
3.5 KiB
Go

package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"time"
)
const sessionCookieName = "telesrv_admin_session"
// csrfCookieName is the double-submit cookie. It is deliberately NOT HttpOnly:
// the panel's own JavaScript has to read it back to echo it in the X-CSRF-Token
// header, which is the whole mechanism.
const csrfCookieName = "telesrv_admin_csrf"
// csrfHeaderName is the header the panel echoes the cookie in.
const csrfHeaderName = "X-CSRF-Token"
type sessionClaims struct {
Actor string `json:"actor"`
Exp int64 `json:"exp"`
Nonce string `json:"nonce"`
// Permissions is the right set granted to this session, taken from
// TELESRV_ADMIN_UI_PERMISSIONS at login. It travels inside the signed cookie
// rather than being re-read per request, so a session keeps the rights it was
// issued with, and it cannot be edited by the browser: the HMAC covers it.
Permissions []string `json:"permissions,omitempty"`
// CSRF is the double-submit token bound to this session. Binding it into the
// signed claims is what makes the cookie/header pair unforgeable by a sibling
// origin that can only *write* cookies (a subdomain, say): such an attacker
// can set both the cookie and the header to a value they know, but they cannot
// produce a session cookie that agrees with it.
CSRF string `json:"csrf,omitempty"`
}
func signSession(key []byte, claims sessionClaims) (string, error) {
payload, err := json.Marshal(claims)
if err != nil {
return "", err
}
encPayload := base64.RawURLEncoding.EncodeToString(payload)
mac := hmac.New(sha256.New, key)
mac.Write([]byte(encPayload))
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
return encPayload + "." + sig, nil
}
func verifySession(key []byte, value string, now time.Time) (sessionClaims, bool) {
parts := strings.Split(value, ".")
if len(parts) != 2 {
return sessionClaims{}, false
}
mac := hmac.New(sha256.New, key)
mac.Write([]byte(parts[0]))
want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
if subtle.ConstantTimeCompare([]byte(parts[1]), []byte(want)) != 1 {
return sessionClaims{}, false
}
payload, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return sessionClaims{}, false
}
var claims sessionClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return sessionClaims{}, false
}
if claims.Actor == "" || claims.Exp <= now.Unix() {
return sessionClaims{}, false
}
return claims, true
}
// newCSRFToken mints a fresh double-submit token.
func newCSRFToken() (string, error) {
var raw [32]byte
if _, err := rand.Read(raw[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(raw[:]), nil
}
// setCSRFCookie publishes the token to the browser.
func setCSRFCookie(w http.ResponseWriter, token string, ttl time.Duration) {
http.SetCookie(w, &http.Cookie{
Name: csrfCookieName,
Value: token,
Path: "/",
MaxAge: int(ttl.Seconds()),
// Readable by the panel's script on purpose; see csrfCookieName.
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
})
}
func clearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.SetCookie(w, &http.Cookie{
Name: csrfCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
})
}