GameOver1337 / GitClone Public
К репозиторию
3.8 KB
internal/auth/session.go
3.8 KB
1package auth
2
3import (
4 "crypto/rand"
5 "crypto/subtle"
6 "encoding/base64"
7 "net/http"
8 "strings"
9 "sync"
10 "time"
11)
12
13const sessionCookie = "clancore_session"
14const csrfCookie = "clancore_csrf"
15const sessionTTL = 72 * time.Hour
16const csrfTTL = 7 * 24 * time.Hour
17
18type session struct {
19 userID int
20 expiry time.Time
21}
22
23var (
24 mu sync.RWMutex
25 sessions = map[string]*session{}
26)
27
28func newToken() string {
29 b := make([]byte, 32)
30 if _, err := rand.Read(b); err != nil {
31 return ""
32 }
33 return base64.RawURLEncoding.EncodeToString(b)
34}
35
36// StartSessionCleanup периодически удаляет просроченные сессии.
37func StartSessionCleanup() {
38 go func() {
39 ticker := time.NewTicker(30 * time.Minute)
40 defer ticker.Stop()
41 for range ticker.C {
42 now := time.Now()
43 mu.Lock()
44 for token, s := range sessions {
45 if now.After(s.expiry) {
46 delete(sessions, token)
47 }
48 }
49 mu.Unlock()
50 }
51 }()
52}
53
54// CreateSession регистрирует новую сессию для пользователя и ставит cookie.
55func CreateSession(w http.ResponseWriter, userID int) {
56 token := newToken()
57 if token == "" {
58 return
59 }
60 mu.Lock()
61 sessions[token] = &session{userID: userID, expiry: time.Now().Add(sessionTTL)}
62 mu.Unlock()
63
64 http.SetCookie(w, &http.Cookie{
65 Name: sessionCookie,
66 Value: token,
67 Path: "/",
68 Expires: time.Now().Add(sessionTTL),
69 HttpOnly: true,
70 SameSite: http.SameSiteLaxMode,
71 MaxAge: int(sessionTTL.Seconds()),
72 })
73}
74
75// DestroySession удаляет сессию и чистит cookie.
76func DestroySession(r *http.Request, w http.ResponseWriter) {
77 if c, err := r.Cookie(sessionCookie); err == nil {
78 mu.Lock()
79 delete(sessions, c.Value)
80 mu.Unlock()
81 }
82 http.SetCookie(w, &http.Cookie{
83 Name: sessionCookie,
84 Value: "",
85 Path: "/",
86 HttpOnly: true,
87 SameSite: http.SameSiteLaxMode,
88 MaxAge: -1,
89 })
90}
91
92// SessionUserID возвращает id пользователя по активной сессии, либо 0.
93func SessionUserID(r *http.Request) int {
94 c, err := r.Cookie(sessionCookie)
95 if err != nil || c.Value == "" {
96 return 0
97 }
98 mu.RLock()
99 s, ok := sessions[c.Value]
100 mu.RUnlock()
101 if !ok {
102 return 0
103 }
104 if time.Now().After(s.expiry) {
105 mu.Lock()
106 delete(sessions, c.Value)
107 mu.Unlock()
108 return 0
109 }
110 return s.userID
111}
112
113// CSRFToken возвращает CSRF-токен из cookie, создавая его при необходимости.
114func CSRFToken(w http.ResponseWriter, r *http.Request) string {
115 if c, err := r.Cookie(csrfCookie); err == nil && c.Value != "" {
116 return c.Value
117 }
118 token := newToken()
119 if token == "" {
120 return ""
121 }
122 http.SetCookie(w, &http.Cookie{
123 Name: csrfCookie,
124 Value: token,
125 Path: "/",
126 Expires: time.Now().Add(csrfTTL),
127 HttpOnly: true,
128 SameSite: http.SameSiteLaxMode,
129 MaxAge: int(csrfTTL.Seconds()),
130 })
131 return token
132}
133
134// CheckCSRF сверяет токен из формы/заголовка с cookie (double-submit).
135func CheckCSRF(r *http.Request) bool {
136 c, err := r.Cookie(csrfCookie)
137 if err != nil || c.Value == "" {
138 return false
139 }
140 token := r.FormValue("csrf_token")
141 if token == "" {
142 token = r.Header.Get("X-CSRF-Token")
143 }
144 if token == "" {
145 return false
146 }
147 return subtle.ConstantTimeCompare([]byte(token), []byte(c.Value)) == 1
148}
149
150// CheckBasicAuth разбирает заголовок Authorization: Basic ...
151func CheckBasicAuth(r *http.Request) (user, pass string, ok bool) {
152 h := r.Header.Get("Authorization")
153 if h == "" || !strings.HasPrefix(h, "Basic ") {
154 return "", "", false
155 }
156 decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(h, "Basic "))
157 if err != nil {
158 return "", "", false
159 }
160 pair := strings.SplitN(string(decoded), ":", 2)
161 if len(pair) != 2 {
162 return "", "", false
163 }
164 return pair[0], pair[1], true
165}