~bigbes/sr-ht-dolt

ref: 2c8903fd8ef6f2e3843f9373be268cf9136ba9e2 sr-ht-dolt/authn/cookie.go -rw-r--r-- 3.2 KiB
2c8903fd — Eugene Blikh browse: report an unparseable start hash as a missing ref 5 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
73
74
package authn

import (
	"log/slog"
	"net/http"

	"go.bigb.es/auxilia/scribe"

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

	"sourcecraft.dev/bigbes/sr-ht-ecore/login"
)

// OptionalCookieMiddleware reads the unified-login cookie and, when it names a
// user this service can resolve, attaches the resolved caller to the request
// context (retrievable with CallerFromContext). It NEVER rejects a request: a
// missing, malformed, undecryptable, or unresolvable cookie leaves the request
// anonymous. This is what allows public browsing and public clones to work
// without credentials — unlike core-go's auth.Middleware, which 401s any
// request lacking a cookie or Authorization header.
//
// The two halves of that sentence are two packages, and the split is the point.
// Decoding the cookie is instance-wide — one session, one seal, one grammar for
// the name inside it — and lives in sr-ht-ecore's login. Turning the name into a
// row is dolt.sr.ht's alone: our user table, our mirror-on-first-sight, our
// answer for a user meta has but we have never seen. Only the second half is
// here, which is also why this is not simply login.Optional: what the rest of
// the service reads out of the context is an *auth.AuthContext with a UserID,
// not a username.
//
// Requires crypto.InitCrypto to have run (server.New does this at startup) and,
// for the user lookup, config.Middleware + database.Middleware installed
// upstream so the context carries the config and database.
func OptionalCookieMiddleware() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if ac := resolveCookie(r); ac != nil {
				r = r.WithContext(WithCaller(r.Context(), ac))
			}
			next.ServeHTTP(w, r)
		})
	}
}

// resolveCookie returns the caller authenticated by the request's unified-login
// cookie, or nil if there is no cookie or it cannot be resolved for any reason.
// Every failure path returns nil (anonymous) — none is fatal.
//
// Suspended users are resolved normally; the suspension flag is carried on the
// caller (via AsCoreCaller) and gates writes at the access-control layer rather
// than being rejected here.
func resolveCookie(r *http.Request) *auth.AuthContext {
	// No cookie, a forged one, a payload that is not core-go's JSON, or a name
	// that could not be an account name: all "" and all anonymous. Nothing is
	// logged, because the cookie value is attacker-supplied and arrives on every
	// request — a warning per bad decode is a log flood anyone can turn on.
	username := login.UsernameFromRequest(r)
	if username == "" {
		return nil
	}

	var ac auth.AuthContext
	if err := meta.LookupUser(r.Context(), username, &ac); err != nil {
		// meta/database unreachable or unknown user: degrade to anonymous
		// rather than failing the request (browsing must keep working). This
		// one *is* logged: the name has already passed login's grammar, so it
		// is bounded text, and an unreachable meta is an operator's problem.
		slog.WarnContext(r.Context(), "resolving the login cookie's user failed",
			"component", "authn", "username", username, scribe.Err(err))
		return nil
	}
	ac.AuthMethod = auth.AUTH_COOKIE
	return &ac
}