Skip to content
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

Open
wants to merge 6 commits into
base: db2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
db-data
/cmd/server/db-data
.git
.gitignore
10 changes: 6 additions & 4 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ package main

import (
"context"
"kudago/internal/db"
"log"
"net/http"

"kudago/internal/db"

"kudago/config"
_ "kudago/docs"
"kudago/internal/http/auth"
"kudago/internal/http/events"
"kudago/internal/middleware"
csrfRepository "kudago/internal/repository/csrf"
eventRepository "kudago/internal/repository/events"
sessionRepository "kudago/internal/repository/session"
userRepository "kudago/internal/repository/users"
Expand All @@ -33,6 +33,7 @@ import (

func main() {
port := config.LoadConfig()
encryptionKey := config.LoadEncriptionKey()

logger, err := zap.NewProduction()
if err != nil {
Expand All @@ -55,9 +56,10 @@ func main() {

userDB := userRepository.NewDB(pool)
sessionDB := sessionRepository.NewDB(redisClient)
csrfDB := csrfRepository.NewDB(redisClient)
eventDB := eventRepository.NewDB(pool)

authService := authService.NewService(userDB, sessionDB)
authService := authService.NewService(userDB, sessionDB, csrfDB)
eventService := eventService.NewService(eventDB)

authHandler := auth.NewAuthHandler(&authService)
Expand Down Expand Up @@ -96,7 +98,7 @@ func main() {
"/categories",
}

handlerWithAuth := middleware.AuthMiddleware(whitelist, authHandler, r)
handlerWithAuth := middleware.AuthWithCSRFMiddleware(whitelist, authHandler, encryptionKey, r)
handlerWithCORS := middleware.CORSMiddleware(handlerWithAuth)
handlerWithLogging := middleware.LoggingMiddleware(handlerWithCORS, sugar)
handler := middleware.PanicMiddleware(handlerWithLogging)
Expand Down
5 changes: 5 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ func LoadConfig() string {
log.Printf("Используется порт: %s", port)
return port
}

func LoadEncriptionKey() []byte {
key := os.Getenv("ENCRYPTION_KEY")
return []byte(key)
}
Comment on lines +22 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Еще с Ксюшиным кодом не мерджился? Там просто как раз конфиг был

12 changes: 6 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ services:
- "8080:8080"
depends_on:
- postgres
environment:
POSTGRES_DB: kudago_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
- redis
env_file:
- .env
volumes:
Expand All @@ -26,11 +23,14 @@ services:
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ${PWD}/db-data/:/var/lib/postgresql/data/
redis:
image: redis:latest
ports:
- "6379:6379"
- "6379:6379"
command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
env_file:
- .env

volumes:
postgres_data:
Expand Down
9 changes: 9 additions & 0 deletions internal/http/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Странно называть методы миддлварами, если это не миддлвары. Наверное, можно отказаться от этих методов, если передавать сервис вместо хэндлера в AuthWithCSRFMiddleware?

16 changes: 16 additions & 0 deletions internal/http/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type sessionKeyType struct{}

var sessionKey sessionKeyType

type csrfKeyType struct{}

var csrfKey sessionKeyType
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кажется, должно быть csrfKeyType


type requestIDKeyType struct{}

var requestIDKey requestIDKeyType
Expand All @@ -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
Expand Down
48 changes: 45 additions & 3 deletions internal/middleware/auth.go
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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")
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"})
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
})
}
10 changes: 10 additions & 0 deletions internal/models/csrf_token.go
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А юзер айди тут точно должен быть? Вроде он в сессии лежит

Exp time.Time
}
117 changes: 117 additions & 0 deletions internal/repository/csrf/csrf.go
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Выглядит вспомогательным методом, если не используется в других пакетах, сделать закрытым

return fmt.Sprintf("%s:%s", key, "csrf")
}
17 changes: 15 additions & 2 deletions internal/service/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
type authService struct {
UserDB UserDB
SessionDB SessionDB
CsrfDB CsrfDB
}

type UserDB interface {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Собственно да, тут должна быть логика, которая сейчас в репозиториях

Binary file added static/images/50ZHs2a9FW7smiOw_1730731958354.png
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не очень вышло, что картинки в этом ПРе)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/7iXdGW2jezgYjf_O_1730731977372.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/R-m1BgDB9rPrO1Vk_1730745578724.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/vmXI9F6xZz2vhoFG_1730732264682.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/ymGmjzxqZCyU3GVH_1730731901641.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.