-
Notifications
You must be signed in to change notification settings - Fork 0
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
Security add #18
base: develop
Are you sure you want to change the base?
Security add #18
Changes from 13 commits
3eef05f
b8a59ae
54806ac
1ace540
637af1b
54ae236
150d11b
4ce1972
df58392
9f675a1
373c115
46f43d0
e6ea080
85d9721
949683e
9a2a4f4
9b2344c
6864aee
eb0a310
ea00ae7
8f2a57c
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,55 @@ | ||
package models | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/dgrijalva/jwt-go" | ||
"github.com/pkg/errors" | ||
|
||
"time" | ||
) | ||
|
||
type JwtToken struct { | ||
Secret []byte | ||
} | ||
|
||
func NewJwtToken(secret string) (*JwtToken, error) { | ||
return &JwtToken{Secret: []byte(secret)}, nil | ||
} | ||
|
||
type JwtCsrfClaims struct { | ||
SessionID string `json:"sid"` | ||
jwt.StandardClaims | ||
} | ||
|
||
func (tk *JwtToken) Create(sID string, tokenExpTime int64) (string, error) { | ||
data := JwtCsrfClaims{ | ||
SessionID: sID, | ||
StandardClaims: jwt.StandardClaims{ | ||
ExpiresAt: tokenExpTime, | ||
IssuedAt: time.Now().Unix(), | ||
}, | ||
} | ||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, data) | ||
return token.SignedString(tk.Secret) | ||
} | ||
|
||
func (tk *JwtToken) parseSecretGetter(token *jwt.Token) (interface{}, error) { | ||
method, ok := token.Method.(*jwt.SigningMethodHMAC) | ||
if !ok || method.Alg() != "HS256" { | ||
return nil, errors.New("bad sign method") | ||
} | ||
return tk.Secret, nil | ||
} | ||
|
||
func (tk *JwtToken) Check(sID string, inputToken string) (bool, error) { | ||
payload := &JwtCsrfClaims{} | ||
_, err := jwt.ParseWithClaims(inputToken, payload, tk.parseSecretGetter) | ||
if err != nil { | ||
return false, fmt.Errorf("cant parse jwt token: %w", err) | ||
} | ||
if payload.Valid() != nil { | ||
return false, fmt.Errorf("invalid jwt token: %w", err) | ||
} | ||
return payload.SessionID == sID, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package middleware | ||
|
||
import ( | ||
"fmt" | ||
"slices" | ||
"time" | ||
|
||
csrf "github.com/go-park-mail-ru/2024_1_FullFocus/internal/models" | ||
"github.com/go-park-mail-ru/2024_1_FullFocus/internal/pkg/logger" | ||
"github.com/gorilla/mux" | ||
|
||
"net/http" | ||
) | ||
|
||
func CSRFMiddleware() mux.MiddlewareFunc { | ||
return func(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
ctx := r.Context() | ||
httpMethods := []string{http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch} | ||
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. проще проверить, что не GET и не HEAD, нет? |
||
found := slices.Contains(httpMethods, r.Method) | ||
if found { | ||
err := CheckSCRFToken(r) | ||
if err != nil { | ||
logger.Debug(ctx, fmt.Sprintf("csrf token creation error: %v", err)) | ||
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 | ||
} | ||
} | ||
if r.Method == http.MethodGet { | ||
err := SetSCRFToken(w, r) | ||
if err != nil { | ||
logger.Debug(ctx, fmt.Sprintf("csrf token creation error: %v", err)) | ||
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 | ||
} | ||
} | ||
next.ServeHTTP(w, r) | ||
}) | ||
} | ||
} | ||
|
||
func SetSCRFToken(w http.ResponseWriter, r *http.Request) error { | ||
tokens, _ := csrf.NewJwtToken("qsRY2e4hcM5T7X984E9WQ5uZ8Nty7fxB") | ||
session, err := r.Cookie("session_id") | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
sID := session.Value | ||
token, err := tokens.Create(sID, time.Now().Add(1*time.Hour).Unix()) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
w.Header().Set("X-Csrf-Token", token) | ||
|
||
return nil | ||
} | ||
|
||
func CheckSCRFToken(r *http.Request) error { | ||
tokens, _ := csrf.NewJwtToken("qsRY2e4hcM5T7X984E9WQ5uZ8Nty7fxB") | ||
session, err := r.Cookie("session_id") | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
sID := session.Value | ||
csrfToken := r.FormValue("X-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. Надо из хедера брать, у вас формы не используются в запросах |
||
_, err = tokens.Check(sID, csrfToken) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,21 +3,34 @@ package usecase | |
import ( | ||
"context" | ||
"crypto/md5" | ||
"crypto/rand" | ||
"encoding/hex" | ||
"strconv" | ||
"errors" | ||
|
||
"github.com/go-park-mail-ru/2024_1_FullFocus/internal/models" | ||
"github.com/go-park-mail-ru/2024_1_FullFocus/internal/pkg/helper" | ||
"github.com/go-park-mail-ru/2024_1_FullFocus/internal/repository" | ||
|
||
"golang.org/x/crypto/argon2" | ||
|
||
"strconv" | ||
) | ||
|
||
const ( | ||
_minLoginLength = 4 | ||
_maxLoginLength = 32 | ||
_minPasswordLength = 8 | ||
_maxPasswordLength = 32 | ||
_countBytes = 8 | ||
_countMemory = 65536 | ||
_countTreads = 4 | ||
_countKeyLen = 32 | ||
) | ||
|
||
func PasswordArgon2(plainPassword []byte, salt []byte) []byte { | ||
return argon2.IDKey(plainPassword, salt, 1, _countMemory, _countTreads, _countKeyLen) | ||
} | ||
|
||
type AuthUsecase struct { | ||
userRepo repository.Users | ||
sessionRepo repository.Sessions | ||
|
@@ -41,13 +54,20 @@ func (u *AuthUsecase) Login(ctx context.Context, login string, password string) | |
return "", models.NewValidationError("invalid password input", | ||
"Пароль должен содержать от 8 до 32 букв английского алфавита или цифр") | ||
} | ||
|
||
user, err := u.userRepo.GetUser(ctx, login) | ||
if err != nil { | ||
return "", models.ErrNoUser | ||
} | ||
if password != user.Password { | ||
|
||
salt := ([]byte(user.Password))[0:8] | ||
passwordHash := PasswordArgon2([]byte(password), salt) | ||
saltWithPasswordHash := string(salt) + string(passwordHash) | ||
|
||
if saltWithPasswordHash != user.Password { | ||
return "", models.ErrWrongPassword | ||
} | ||
|
||
return u.sessionRepo.CreateSession(ctx, user.ID), nil | ||
} | ||
|
||
|
@@ -62,10 +82,20 @@ func (u *AuthUsecase) Signup(ctx context.Context, login string, password string) | |
return "", "", models.NewValidationError("invalid password input", | ||
"Пароль должен содержать от 8 до 32 букв английского алфавита или цифр") | ||
} | ||
|
||
salt := make([]byte, _countBytes) | ||
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. сделай 1 функцию, которая хеширует пароль. при регистрации просто пришедший пароль хешируешь и дальше по цепочке передаешь. а при логине используешь эту же функцию для пришедшего пароля 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. Почему не будет работать? Сначала в signup рандомно генерим соль, после чего хешируем и клеим соль+хеш 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. ERROR: invalid byte sequence for encoding "UTF8": 0x83 (SQLSTATE 22021) |
||
_, errSalt := rand.Read(salt) | ||
if errSalt != nil { | ||
return "", "", errors.New("err with making salt") | ||
} | ||
passwordHash := PasswordArgon2([]byte(password), salt) | ||
saltWithPasswordHash := string(salt) + string(passwordHash) | ||
|
||
user := models.User{ | ||
Username: login, | ||
Password: password, | ||
Password: saltWithPasswordHash, | ||
} | ||
|
||
uID, err := u.userRepo.CreateUser(ctx, user) | ||
if err != nil { | ||
return "", "", models.ErrUserAlreadyExists | ||
|
@@ -74,6 +104,7 @@ func (u *AuthUsecase) Signup(ctx context.Context, login string, password string) | |
|
||
uIDHash := md5.Sum([]byte(strconv.Itoa(int(uID)))) | ||
stringUID := hex.EncodeToString(uIDHash[:]) | ||
|
||
return sID, stringUID, nil | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,8 +12,14 @@ import ( | |
"github.com/go-park-mail-ru/2024_1_FullFocus/internal/models" | ||
mock_repository "github.com/go-park-mail-ru/2024_1_FullFocus/internal/repository/mocks" | ||
"github.com/go-park-mail-ru/2024_1_FullFocus/internal/usecase" | ||
|
||
"golang.org/x/crypto/argon2" | ||
) | ||
|
||
func PasswordArgon2(plainPassword []byte, salt []byte) []byte { | ||
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 argon2.IDKey(plainPassword, salt, 1, 64*1024, 4, 32) | ||
} | ||
|
||
func TestNewAuthUsecase(t *testing.T) { | ||
t.Run("Check Auth Usecase creation", func(t *testing.T) { | ||
ctrl := gomock.NewController(t) | ||
|
@@ -41,8 +47,8 @@ func TestSignUp(t *testing.T) { | |
name: "Check valid user signup", | ||
login: "test123", | ||
password: "Qa5yAbrLhkwT4Y9u", | ||
userMockBehavior: func(r *mock_repository.MockUsers, user models.User) { | ||
r.EXPECT().CreateUser(context.Background(), user).Return(uint(0), nil) | ||
userMockBehavior: func(r *mock_repository.MockUsers, _ models.User) { | ||
r.EXPECT().CreateUser(context.Background(), gomock.Any()).Return(uint(0), nil) | ||
}, | ||
sessionMockBehavior: func(r *mock_repository.MockSessions, userID uint) { | ||
r.EXPECT().CreateSession(context.Background(), userID).Return("123") | ||
|
@@ -56,8 +62,8 @@ func TestSignUp(t *testing.T) { | |
name: "Check valid user signup", | ||
login: "test123", | ||
password: "testtest1", | ||
userMockBehavior: func(r *mock_repository.MockUsers, user models.User) { | ||
r.EXPECT().CreateUser(context.Background(), user).Return(uint(0), nil) | ||
userMockBehavior: func(r *mock_repository.MockUsers, _ models.User) { | ||
r.EXPECT().CreateUser(context.Background(), gomock.Any()).Return(uint(0), nil) | ||
}, | ||
sessionMockBehavior: func(r *mock_repository.MockSessions, userID uint) { | ||
r.EXPECT().CreateSession(context.Background(), userID).Return("123") | ||
|
@@ -71,8 +77,8 @@ func TestSignUp(t *testing.T) { | |
name: "Check duplicate user signup", | ||
login: "test123", | ||
password: "Qa5yAbrLhkwT4Y9u", | ||
userMockBehavior: func(r *mock_repository.MockUsers, user models.User) { | ||
r.EXPECT().CreateUser(context.Background(), user).Return(uint(0), models.ErrUserAlreadyExists) | ||
userMockBehavior: func(r *mock_repository.MockUsers, _ models.User) { | ||
r.EXPECT().CreateUser(context.Background(), gomock.Any()).Return(uint(0), models.ErrUserAlreadyExists) | ||
}, | ||
expectedSID: "", | ||
expectedErr: models.ErrUserAlreadyExists, | ||
|
@@ -114,15 +120,11 @@ func TestSignUp(t *testing.T) { | |
defer ctrl.Finish() | ||
mockUserRepo := mock_repository.NewMockUsers(ctrl) | ||
mockSessionRepo := mock_repository.NewMockSessions(ctrl) | ||
testUser := models.User{ | ||
ID: 0, | ||
Username: testCase.login, | ||
Password: testCase.password, | ||
} | ||
someUser := models.User{} | ||
if testCase.callUserMock { | ||
testCase.userMockBehavior(mockUserRepo, testUser) | ||
testCase.userMockBehavior(mockUserRepo, someUser) | ||
if testCase.callSessionMock { | ||
testCase.sessionMockBehavior(mockSessionRepo, testUser.ID) | ||
testCase.sessionMockBehavior(mockSessionRepo, someUser.ID) | ||
} | ||
} | ||
au := usecase.NewAuthUsecase(mockUserRepo, mockSessionRepo) | ||
|
@@ -150,7 +152,8 @@ func TestLogin(t *testing.T) { | |
login: "test123", | ||
password: "test12345", | ||
userMockBehavior: func(r *mock_repository.MockUsers, username string) { | ||
r.EXPECT().GetUser(context.Background(), username).Return(models.User{ID: 0, Username: "test123", Password: "test12345"}, nil) | ||
r.EXPECT().GetUser(context.Background(), username).Return(models.User{ID: 0, Username: "test123", | ||
Password: string([]byte{0xd7, 0xc2, 0xf2, 0x51, 0xaa, 0x6a, 0x4e, 0x7b}) + string(PasswordArgon2([]byte("test12345"), []byte{0xd7, 0xc2, 0xf2, 0x51, 0xaa, 0x6a, 0x4e, 0x7b}))}, nil) | ||
}, | ||
sessionMockBehavior: func(r *mock_repository.MockSessions, userID uint) { | ||
r.EXPECT().CreateSession(context.Background(), userID).Return("123") | ||
|
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.
нужны тесты