Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
1package auth
2
3import (
4 "context"
5 "crypto/rand"
6 "crypto/sha256"
7 "encoding/base64"
8 "encoding/hex"
9 "fmt"
10 "net/http"
11
12 "github.com/gorilla/sessions"
13 "golang.org/x/crypto/bcrypt"
14)
15
16type contextKey string
17
18const UserIDKey contextKey = "user_id"
19
20var store *sessions.CookieStore
21
22func InitStore(secret string) {
23 store = sessions.NewCookieStore([]byte(secret))
24 store.Options = &sessions.Options{
25 Path: "/",
26 MaxAge: 86400 * 30, // 30 days
27 HttpOnly: true,
28 SameSite: http.SameSiteLaxMode,
29 }
30}
31
32func GetSession(r *http.Request) *sessions.Session {
33 sess, _ := store.Get(r, "mdhub")
34 return sess
35}
36
37func SetUserID(w http.ResponseWriter, r *http.Request, userID string) error {
38 sess := GetSession(r)
39 sess.Values["user_id"] = userID
40 return sess.Save(r, w)
41}
42
43func GetUserID(r *http.Request) string {
44 sess := GetSession(r)
45 if v, ok := sess.Values["user_id"].(string); ok {
46 return v
47 }
48 return ""
49}
50
51func ClearSession(w http.ResponseWriter, r *http.Request) error {
52 sess := GetSession(r)
53 sess.Options.MaxAge = -1
54 return sess.Save(r, w)
55}
56
57func UserIDFromContext(ctx context.Context) string {
58 if v, ok := ctx.Value(UserIDKey).(string); ok {
59 return v
60 }
61 return ""
62}
63
64// Password helpers
65
66func HashPassword(password string) (string, error) {
67 hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
68 if err != nil {
69 return "", err
70 }
71 return string(hash), nil
72}
73
74func CheckPassword(hash, password string) bool {
75 return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
76}
77
78func GenerateToken() string {
79 b := make([]byte, 32)
80 rand.Read(b)
81 return hex.EncodeToString(b)
82}
83
84// State parameter for CSRF protection
85func SetOAuthState(w http.ResponseWriter, r *http.Request) string {
86 state := GenerateToken()
87 sess := GetSession(r)
88 sess.Values["oauth_state"] = state
89 sess.Save(r, w)
90 return state
91}
92
93func ValidateOAuthState(r *http.Request, state string) error {
94 sess := GetSession(r)
95 expected, ok := sess.Values["oauth_state"].(string)
96 if !ok || expected != state {
97 return fmt.Errorf("invalid oauth state")
98 }
99 delete(sess.Values, "oauth_state")
100 return nil
101}
102
103// PKCEVerifier generates a cryptographically random PKCE code verifier (43-128 chars, URL-safe base64).
104func PKCEVerifier() string {
105 b := make([]byte, 32)
106 rand.Read(b)
107 return base64.RawURLEncoding.EncodeToString(b)
108}
109
110// PKCEChallenge derives the S256 code challenge from a verifier.
111func PKCEChallenge(verifier string) string {
112 h := sha256.Sum256([]byte(verifier))
113 return base64.RawURLEncoding.EncodeToString(h[:])
114}