~bigbes/sr-ht-spec

ref: 4ad8525764dbfb0f7f03eae106b4436cc44040cc sr-ht-spec/authn/cookie.go -rw-r--r-- 2.6 KiB
4ad85257 — bigbes feat(cmd): reindex a space when a push lands 27 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package authn

import (
	"encoding/json"
	"net/http"
	"strings"

	"sourcecraft.dev/bigbes/sr-ht-core/auth"
	"sourcecraft.dev/bigbes/sr-ht-core/crypto"

	"sourcecraft.dev/bigbes/sr-ht-spec/core"
)

// CookieName is the unified-login cookie shared by every service on the
// instance. Its value is a Fernet token sealed by meta.sr.ht with the
// [sr.ht] network-key, which is why spec.srht.bigb.es must live under the
// shared *.srht.bigb.es cookie domain — otherwise the cookie is never sent to
// us and every viewer looks anonymous.
const CookieName = "sr.ht.unified-login.v1"

// UsernameFromRequest returns the username carried by the unified-login cookie,
// or "" for an anonymous viewer.
//
// Every failure path — no cookie, forged or truncated ciphertext, a payload
// that is not JSON, a payload with no name — returns "" rather than an error.
// This service never serves an error page on identity grounds; it decides what
// an anonymous viewer may see instead. Returning an error here would turn a
// stale cookie from a browser tab left open over a key rotation into a broken
// site rather than a logged-out one.
//
// Requires crypto.InitCrypto to have run (server.New does it at startup).
func UsernameFromRequest(r *http.Request) string {
	c, err := r.Cookie(CookieName)
	if err != nil {
		return "" // no cookie: anonymous
	}
	return UsernameFromCookie(c.Value)
}

// UsernameFromCookie is UsernameFromRequest for a cookie value already in hand
// — the form the hooks' RPC path and tests need, where there is no
// *http.Request to read from.
//
// Note the deliberate use of DecryptWithoutExpiration, matching core-go's own
// cookieAuth: the unified-login cookie carries no service-side TTL, and its
// lifetime is the browser cookie's Expires plus meta.sr.ht's ability to rotate
// the network key. Adding a TTL here would log the owner out of this one
// service on a schedule no other service shares.
func UsernameFromCookie(value string) string {
	if value == "" {
		return ""
	}
	payload := crypto.DecryptWithoutExpiration([]byte(value))
	if payload == nil {
		return "" // forged, tampered, or sealed with a key we no longer hold
	}

	var claims auth.AuthCookie
	if err := json.Unmarshal(payload, &claims); err != nil {
		return "" // well-sealed but malformed payload
	}

	// Cookies carry the bare username; strip a leading '~' defensively in case
	// something upstream stored the canonical "~user" form.
	name := strings.TrimPrefix(claims.Name, "~")
	if err := core.ValidateOwner(name); err != nil {
		// An unusable username is not an identity. Rejecting here keeps a
		// hostile cookie payload out of path construction and log lines.
		return ""
	}
	return name
}