-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Csrf #6
base: db2
Are you sure you want to change the base?
Csrf #6
Changes from 5 commits
3656afc
0554cae
55592ad
82d9305
5b04e82
117dfe4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
db-data | ||
/cmd/server/db-data | ||
.git | ||
.gitignore |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,8 @@ type AuthService interface { | |
Register(ctx context.Context, user models.User) (models.User, error) | ||
CreateSession(ctx context.Context, ID int) (models.Session, error) | ||
DeleteSession(ctx context.Context, token string) | ||
CreateCSRF(ctx context.Context, encryptionKey []byte, s *models.Session) (string, error) | ||
CheckCSRF(ctx context.Context, encryptionKey []byte, s *models.Session, inputToken string) (bool, error) | ||
} | ||
|
||
type RegisterRequest struct { | ||
|
@@ -291,3 +293,10 @@ func userToProfileResponse(user models.User) ProfileResponse { | |
func (h *AuthHandler) CheckSessionMiddleware(ctx context.Context, cookie string) (models.Session, bool) { | ||
return h.service.CheckSession(ctx, cookie) | ||
} | ||
func (h *AuthHandler) CheckCSRFMiddleware(ctx context.Context, encryptionKey []byte, s *models.Session, inputToken string) (bool, error) { | ||
return h.service.CheckCSRF(ctx, encryptionKey, s, inputToken) | ||
} | ||
|
||
func (h *AuthHandler) CreateCSRFMiddleware(ctx context.Context, encryptionKey []byte, s *models.Session) (string, error) { | ||
return h.service.CreateCSRF(ctx, encryptionKey, s) | ||
} | ||
Comment on lines
+296
to
+302
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Странно называть методы миддлварами, если это не миддлвары. Наверное, можно отказаться от этих методов, если передавать сервис вместо хэндлера в AuthWithCSRFMiddleware? |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,10 @@ type sessionKeyType struct{} | |
|
||
var sessionKey sessionKeyType | ||
|
||
type csrfKeyType struct{} | ||
|
||
var csrfKey sessionKeyType | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Кажется, должно быть csrfKeyType |
||
|
||
type requestIDKeyType struct{} | ||
|
||
var requestIDKey requestIDKeyType | ||
|
@@ -43,6 +47,18 @@ func SetSessionInContext(ctx context.Context, session models.Session) context.Co | |
return context.WithValue(ctx, sessionKey, session) | ||
} | ||
|
||
func GetCSRFFromContext(ctx context.Context) (models.TokenData, bool) { | ||
csrfKey, ok := ctx.Value(csrfKey).(models.TokenData) | ||
if !ok { | ||
return csrfKey, false | ||
} | ||
return csrfKey, true | ||
} | ||
|
||
func SetCSRFInContext(ctx context.Context, token models.TokenData) context.Context { | ||
return context.WithValue(ctx, csrfKey, token) | ||
} | ||
|
||
func GetRequestIDFromContext(ctx context.Context) string { | ||
ID, _ := ctx.Value(requestIDKey).(string) | ||
return ID | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,81 @@ | ||
package middleware | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"kudago/internal/http/auth" | ||
"kudago/internal/http/utils" | ||
"kudago/internal/models" | ||
) | ||
|
||
const ( | ||
SessionToken = "session_token" | ||
SessionKey = "session" | ||
) | ||
|
||
func AuthMiddleware(whitelist []string, authHandler *auth.AuthHandler, next http.Handler) http.Handler { | ||
func AuthWithCSRFMiddleware(whitelist []string, authHandler *auth.AuthHandler, encryptionKey []byte, next http.Handler) http.Handler { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Не сразу подумал об этом, а мы не можем передавать сервис в миддлвару вместо хэндлера? На хэндлеры зависимости плохо делать |
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
// Проверяем сессионный токен | ||
cookie, err := r.Cookie(SessionToken) | ||
if err == nil { | ||
session, authenticated := authHandler.CheckSessionMiddleware(r.Context(), cookie.Value) | ||
if authenticated { | ||
ctx := utils.SetSessionInContext(r.Context(), session) | ||
r = r.WithContext(ctx) | ||
|
||
w.Header().Set("X-Test-Header", "test-value") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. К РК убрать |
||
if r.Method != http.MethodGet && r.Method != http.MethodHead && r.Method != http.MethodOptions { | ||
csrfToken := r.Header.Get("X-CSRF-Token") | ||
fmt.Println("CSRF Token got from header:", csrfToken) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Это и другие дебажные принты к РК убрать |
||
if csrfToken == "" { | ||
utils.WriteResponse(w, http.StatusForbidden, map[string]string{"error": "CSRF token missing"}) | ||
return | ||
} | ||
|
||
valid, err := authHandler.CheckCSRFMiddleware(r.Context(), encryptionKey, &session, csrfToken) | ||
if err != nil || !valid { | ||
utils.WriteResponse(w, http.StatusForbidden, map[string]string{"error": "Invalid CSRF token"}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Вместо map[string]string сделать структуру с ошибкой |
||
return | ||
} | ||
} else if r.Method == http.MethodGet { | ||
session, ok := utils.GetSessionFromContext(r.Context()) | ||
if ok { | ||
csrfToken, err := authHandler.CreateCSRFMiddleware(r.Context(), encryptionKey, &session) | ||
if err != nil { | ||
utils.WriteResponse(w, http.StatusInternalServerError, map[string]string{"error": "Failed to generate CSRF token"}) | ||
return | ||
} | ||
fmt.Println("Generated CSRF Token:", csrfToken) | ||
|
||
w.Header().Set("X-CSRF-Token", csrfToken) | ||
fmt.Println("Setting X-CSRF-Token in header:", w.Header().Get("X-CSRF-Token")) | ||
|
||
// Далее проверьте, что токен установлен | ||
for k, v := range w.Header() { | ||
fmt.Printf("Header after setting CSRF: %s: %s\n", k, v) | ||
} | ||
|
||
ctx := utils.SetCSRFInContext(r.Context(), models.TokenData{CSRFtoken: csrfToken}) | ||
r = r.WithContext(ctx) | ||
} | ||
} | ||
|
||
// Переходим к следующему обработчику | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
} | ||
|
||
// Если маршрут в white list, пропускаем проверку | ||
for _, path := range whitelist { | ||
if strings.HasPrefix(r.URL.Path, path) { | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
} | ||
|
||
// Отправляем ошибку, если запрос не авторизован | ||
http.Error(w, "Unauthorized", http.StatusUnauthorized) | ||
return | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package models | ||
|
||
import "time" | ||
|
||
type TokenData struct { | ||
CSRFtoken string | ||
SessionToken string | ||
UserID int | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. А юзер айди тут точно должен быть? Вроде он в сессии лежит |
||
Exp time.Time | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package csrf | ||
|
||
import ( | ||
"context" | ||
"crypto/aes" | ||
"crypto/cipher" | ||
"crypto/rand" | ||
"encoding/base64" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"time" | ||
|
||
"github.com/redis/go-redis/v9" | ||
"kudago/internal/models" | ||
) | ||
|
||
const CSRFTokenExpTime = 15 * time.Minute | ||
|
||
type csrfDB struct { | ||
client *redis.Client | ||
} | ||
|
||
func NewDB(client *redis.Client) *csrfDB { | ||
return &csrfDB{client: client} | ||
} | ||
|
||
func (db *csrfDB) CreateCSRF(ctx context.Context, encryptionKey []byte, s *models.Session) (string, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Токен по-хорошему должен генериться на уровне бизнес-логики. Репозиторий должен только делать CRUD |
||
block, err := aes.NewCipher(encryptionKey) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
aesgcm, err := cipher.NewGCM(block) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
nonce := make([]byte, aesgcm.NonceSize()) | ||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil { | ||
return "", err | ||
} | ||
|
||
tokenExpTime := time.Now().Add(CSRFTokenExpTime) | ||
td := models.TokenData{ | ||
SessionToken: s.Token, | ||
UserID: s.UserID, | ||
Exp: tokenExpTime, | ||
} | ||
data, _ := json.Marshal(td) | ||
ciphertext := aesgcm.Seal(nil, nonce, data, nil) | ||
|
||
res := append(nonce, ciphertext...) | ||
token := base64.StdEncoding.EncodeToString(res) | ||
|
||
err = db.client.Set(ctx, PrefixedKey(s.Token), token, CSRFTokenExpTime).Err() | ||
if err != nil { | ||
return "", fmt.Errorf("failed to store token in Redis: %v", err) | ||
} | ||
|
||
return token, nil | ||
} | ||
|
||
func (db *csrfDB) CheckCSRF(ctx context.Context, encryptionKey []byte, s *models.Session, inputToken string) (bool, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Здесь тоже много логики, явно в сервисный слой надо |
||
storedToken, err := db.client.Get(ctx, PrefixedKey(s.Token)).Result() | ||
if err == redis.Nil { | ||
return false, fmt.Errorf("token not found in Redis") | ||
} else if err != nil { | ||
return false, fmt.Errorf("failed to get token from Redis: %v", err) | ||
} | ||
|
||
if storedToken != inputToken { | ||
return false, fmt.Errorf("invalid token") | ||
} | ||
|
||
block, err := aes.NewCipher(encryptionKey) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
aesgcm, err := cipher.NewGCM(block) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
ciphertext, err := base64.StdEncoding.DecodeString(inputToken) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
nonceSize := aesgcm.NonceSize() | ||
if len(ciphertext) < nonceSize { | ||
return false, fmt.Errorf("short ciphertext") | ||
} | ||
|
||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] | ||
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) | ||
if err != nil { | ||
return false, fmt.Errorf("decrypt fail: %v", err) | ||
} | ||
|
||
td := &models.TokenData{} | ||
err = json.Unmarshal(plaintext, &td) | ||
if err != nil { | ||
return false, fmt.Errorf("bad json: %v", err) | ||
} | ||
|
||
if td.Exp.Unix() < time.Now().Unix() { | ||
return false, fmt.Errorf("token expired") | ||
} | ||
|
||
return s.Token == td.SessionToken && s.UserID == td.UserID, nil | ||
} | ||
|
||
func PrefixedKey(key string) string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Выглядит вспомогательным методом, если не используется в других пакетах, сделать закрытым |
||
return fmt.Sprintf("%s:%s", key, "csrf") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import ( | |
type authService struct { | ||
UserDB UserDB | ||
SessionDB SessionDB | ||
CsrfDB CsrfDB | ||
} | ||
|
||
type UserDB interface { | ||
|
@@ -24,8 +25,13 @@ type SessionDB interface { | |
DeleteSession(ctx context.Context, token string) | ||
} | ||
|
||
func NewService(userDB UserDB, sessionDB SessionDB) authService { | ||
return authService{UserDB: userDB, SessionDB: sessionDB} | ||
type CsrfDB interface { | ||
CreateCSRF(ctx context.Context, encryptionKey []byte, s *models.Session) (string, error) | ||
CheckCSRF(ctx context.Context, encryptionKey []byte, s *models.Session, inputToken string) (bool, error) | ||
} | ||
|
||
func NewService(userDB UserDB, sessionDB SessionDB, csrfDB CsrfDB) authService { | ||
return authService{UserDB: userDB, SessionDB: sessionDB, CsrfDB: csrfDB} | ||
} | ||
|
||
func (a *authService) CheckSession(ctx context.Context, cookie string) (models.Session, bool) { | ||
|
@@ -62,3 +68,10 @@ func (a *authService) CreateSession(ctx context.Context, ID int) (models.Session | |
func (a *authService) DeleteSession(ctx context.Context, token string) { | ||
a.SessionDB.DeleteSession(ctx, token) | ||
} | ||
func (a *authService) CreateCSRF(ctx context.Context, encryptionKey []byte, s *models.Session) (string, error) { | ||
return a.CsrfDB.CreateCSRF(ctx, encryptionKey, s) | ||
} | ||
|
||
func (a *authService) CheckCSRF(ctx context.Context, encryptionKey []byte, s *models.Session, inputToken string) (bool, error) { | ||
return a.CsrfDB.CheckCSRF(ctx, encryptionKey, s, inputToken) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Собственно да, тут должна быть логика, которая сейчас в репозиториях |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Не очень вышло, что картинки в этом ПРе) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Еще с Ксюшиным кодом не мерджился? Там просто как раз конфиг был