To gitea and beyond, let's go(-yco)
This commit is contained in:
602
internal/e2e/security_session_test.go
Normal file
602
internal/e2e/security_session_test.go
Normal file
@@ -0,0 +1,602 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"goyco/internal/config"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestE2E_SessionFixation(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("session_fixation", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "sessionfix", "Password123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
oldToken := authClient.Token
|
||||
oldRefreshToken := authClient.RefreshToken
|
||||
|
||||
authClient.UpdatePassword(t, "Password123!", "NewPassword456!")
|
||||
|
||||
statusCode := ctx.makeRequestWithToken(t, oldToken)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected old token to be invalidated after password change, got status %d", statusCode)
|
||||
}
|
||||
|
||||
oldClient := &AuthenticatedClient{
|
||||
AuthenticatedClient: &testutils.AuthenticatedClient{
|
||||
Client: ctx.client,
|
||||
Token: oldToken,
|
||||
RefreshToken: oldRefreshToken,
|
||||
BaseURL: ctx.baseURL,
|
||||
},
|
||||
}
|
||||
_, statusCode = oldClient.RefreshAccessToken(t)
|
||||
if statusCode == http.StatusOK {
|
||||
t.Errorf("Expected old refresh token to be invalidated after password change, but refresh succeeded")
|
||||
}
|
||||
|
||||
newAuthClient := ctx.loginUser(t, createdUser.Username, "NewPassword456!")
|
||||
if newAuthClient.Token == "" {
|
||||
t.Errorf("Expected to be able to login with new password")
|
||||
}
|
||||
|
||||
profile := newAuthClient.GetProfile(t)
|
||||
if profile.Data.Username != createdUser.Username {
|
||||
t.Errorf("Expected to access profile with new token, got username '%s'", profile.Data.Username)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_TokenInvalidationOnPasswordChange(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("token_invalidation_on_password_change", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "tokeninv", "Password123!")
|
||||
|
||||
authClient1 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
token1 := authClient1.Token
|
||||
refreshToken1 := authClient1.RefreshToken
|
||||
|
||||
authClient2 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
token2 := authClient2.Token
|
||||
refreshToken2 := authClient2.RefreshToken
|
||||
|
||||
authClient3 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
token3 := authClient3.Token
|
||||
refreshToken3 := authClient3.RefreshToken
|
||||
|
||||
profile1 := authClient1.GetProfile(t)
|
||||
if profile1.Data.Username != createdUser.Username {
|
||||
t.Errorf("Expected token1 to work before password change")
|
||||
}
|
||||
|
||||
authClient1.UpdatePassword(t, "Password123!", "NewPassword789!")
|
||||
|
||||
statusCode := ctx.makeRequestWithToken(t, token1)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected token1 to be invalidated after password change, got status %d", statusCode)
|
||||
}
|
||||
|
||||
statusCode = ctx.makeRequestWithToken(t, token2)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected token2 to be invalidated after password change, got status %d", statusCode)
|
||||
}
|
||||
|
||||
statusCode = ctx.makeRequestWithToken(t, token3)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected token3 to be invalidated after password change, got status %d", statusCode)
|
||||
}
|
||||
|
||||
oldClient1 := &AuthenticatedClient{
|
||||
AuthenticatedClient: &testutils.AuthenticatedClient{
|
||||
Client: ctx.client,
|
||||
Token: token1,
|
||||
RefreshToken: refreshToken1,
|
||||
BaseURL: ctx.baseURL,
|
||||
},
|
||||
}
|
||||
_, statusCode = oldClient1.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected refreshToken1 to be invalidated after password change, got status %d", statusCode)
|
||||
}
|
||||
|
||||
oldClient2 := &AuthenticatedClient{
|
||||
AuthenticatedClient: &testutils.AuthenticatedClient{
|
||||
Client: ctx.client,
|
||||
Token: token2,
|
||||
RefreshToken: refreshToken2,
|
||||
BaseURL: ctx.baseURL,
|
||||
},
|
||||
}
|
||||
_, statusCode = oldClient2.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected refreshToken2 to be invalidated after password change, got status %d", statusCode)
|
||||
}
|
||||
|
||||
oldClient3 := &AuthenticatedClient{
|
||||
AuthenticatedClient: &testutils.AuthenticatedClient{
|
||||
Client: ctx.client,
|
||||
Token: token3,
|
||||
RefreshToken: refreshToken3,
|
||||
BaseURL: ctx.baseURL,
|
||||
},
|
||||
}
|
||||
_, statusCode = oldClient3.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected refreshToken3 to be invalidated after password change, got status %d", statusCode)
|
||||
}
|
||||
|
||||
newAuthClient := ctx.loginUser(t, createdUser.Username, "NewPassword789!")
|
||||
if newAuthClient.Token == "" {
|
||||
t.Errorf("Expected to be able to login with new password")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_TokenInvalidationOnEmailChange(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("token_invalidation_on_email_change", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "emailchange", "Password123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
oldToken := authClient.Token
|
||||
|
||||
ctx.server.EmailSender.Reset()
|
||||
authClient.UpdateEmail(t, uniqueEmail(t, "newemail"))
|
||||
|
||||
statusCode := ctx.makeRequestWithToken(t, oldToken)
|
||||
if statusCode == http.StatusOK {
|
||||
t.Log("Email change does not invalidate tokens (acceptable behavior)")
|
||||
}
|
||||
|
||||
_, statusCode = authClient.RefreshAccessToken(t)
|
||||
if statusCode == http.StatusOK {
|
||||
t.Log("Email change does not invalidate refresh tokens (acceptable behavior)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_SessionVersionIncrements(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("session_version_increments", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "sessionver", "Password123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
user, err := ctx.server.UserRepo.GetByID(createdUser.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
|
||||
initialVersion := user.SessionVersion
|
||||
if initialVersion == 0 {
|
||||
t.Errorf("Expected initial session version to be >= 1, got %d", initialVersion)
|
||||
}
|
||||
|
||||
authClient.UpdatePassword(t, "Password123!", "NewPassword999!")
|
||||
|
||||
user, err = ctx.server.UserRepo.GetByID(createdUser.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user after password change: %v", err)
|
||||
}
|
||||
|
||||
if user.SessionVersion <= initialVersion {
|
||||
t.Errorf("Expected session version to increment after password change, got %d (was %d)", user.SessionVersion, initialVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_OldTokensRejectedAfterSessionVersionChange(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("old_tokens_rejected_after_session_version_change", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "oldtoken", "Password123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
user, err := ctx.server.UserRepo.GetByID(createdUser.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
|
||||
oldSessionVersion := user.SessionVersion
|
||||
oldToken := authClient.Token
|
||||
|
||||
cfg := &config.Config{
|
||||
JWT: config.JWTConfig{
|
||||
Secret: "test-secret-key-for-testing-purposes-only",
|
||||
Expiration: 24,
|
||||
RefreshExpiration: 168,
|
||||
Issuer: "goyco",
|
||||
Audience: "goyco-users",
|
||||
},
|
||||
}
|
||||
|
||||
authClient.UpdatePassword(t, "Password123!", "NewPassword888!")
|
||||
|
||||
user, err = ctx.server.UserRepo.GetByID(createdUser.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user after password change: %v", err)
|
||||
}
|
||||
|
||||
if user.SessionVersion == oldSessionVersion {
|
||||
t.Errorf("Expected session version to change after password update")
|
||||
}
|
||||
|
||||
statusCode := ctx.makeRequestWithToken(t, oldToken)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected old token to be rejected after session version change, got status %d", statusCode)
|
||||
}
|
||||
|
||||
tokenWithOldVersion := generateTokenWithSessionVersion(t, user, &cfg.JWT, oldSessionVersion)
|
||||
statusCode = ctx.makeRequestWithToken(t, tokenWithOldVersion)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected token with old session version to be rejected, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_TokenRefreshWithOldSessionVersion(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("token_refresh_with_old_session_version", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "refreshold", "Password123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
oldRefreshToken := authClient.RefreshToken
|
||||
|
||||
authClient.UpdatePassword(t, "Password123!", "NewPassword777!")
|
||||
|
||||
oldClient := &AuthenticatedClient{
|
||||
AuthenticatedClient: &testutils.AuthenticatedClient{
|
||||
Client: ctx.client,
|
||||
Token: authClient.Token,
|
||||
RefreshToken: oldRefreshToken,
|
||||
BaseURL: ctx.baseURL,
|
||||
},
|
||||
}
|
||||
|
||||
_, statusCode := oldClient.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected refresh with old refresh token to fail after password change, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_MultiDeviceSession(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("multi_device_session", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "multidev", "Password123!")
|
||||
|
||||
deviceA := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
tokenA := deviceA.Token
|
||||
|
||||
deviceB := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
tokenB := deviceB.Token
|
||||
|
||||
profileA := deviceA.GetProfile(t)
|
||||
if profileA.Data.Username != createdUser.Username {
|
||||
t.Errorf("Expected device A to access profile")
|
||||
}
|
||||
|
||||
profileB := deviceB.GetProfile(t)
|
||||
if profileB.Data.Username != createdUser.Username {
|
||||
t.Errorf("Expected device B to access profile")
|
||||
}
|
||||
|
||||
deviceA.Logout(t)
|
||||
|
||||
statusCode := ctx.makeRequestWithToken(t, tokenA)
|
||||
if statusCode == http.StatusOK {
|
||||
t.Log("Logout may not invalidate tokens immediately (acceptable)")
|
||||
}
|
||||
|
||||
profileBAfter := deviceB.GetProfile(t)
|
||||
if profileBAfter.Data.Username != createdUser.Username {
|
||||
t.Errorf("Expected device B to still work after device A logout")
|
||||
}
|
||||
|
||||
deviceB.RevokeAllTokens(t)
|
||||
|
||||
_, statusCode = deviceB.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected device B refresh token to be revoked after revoke-all, got status %d", statusCode)
|
||||
}
|
||||
|
||||
statusCode = ctx.makeRequestWithToken(t, tokenB)
|
||||
if statusCode == http.StatusOK {
|
||||
t.Log("Access token may still work after refresh token revocation (acceptable)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_RevokeAllInvalidatesAllDevices(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("revoke_all_invalidates_all_devices", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "revokeall", "Password123!")
|
||||
|
||||
device1 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
refreshToken1 := device1.RefreshToken
|
||||
|
||||
device2 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
refreshToken2 := device2.RefreshToken
|
||||
|
||||
device3 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
refreshToken3 := device3.RefreshToken
|
||||
|
||||
device1.RevokeAllTokens(t)
|
||||
|
||||
oldDevice1 := &AuthenticatedClient{
|
||||
AuthenticatedClient: &testutils.AuthenticatedClient{
|
||||
Client: ctx.client,
|
||||
Token: device1.Token,
|
||||
RefreshToken: refreshToken1,
|
||||
BaseURL: ctx.baseURL,
|
||||
},
|
||||
}
|
||||
_, statusCode := oldDevice1.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected device1 refresh token to be revoked, got status %d", statusCode)
|
||||
}
|
||||
|
||||
oldDevice2 := &AuthenticatedClient{
|
||||
AuthenticatedClient: &testutils.AuthenticatedClient{
|
||||
Client: ctx.client,
|
||||
Token: device2.Token,
|
||||
RefreshToken: refreshToken2,
|
||||
BaseURL: ctx.baseURL,
|
||||
},
|
||||
}
|
||||
_, statusCode = oldDevice2.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected device2 refresh token to be revoked, got status %d", statusCode)
|
||||
}
|
||||
|
||||
oldDevice3 := &AuthenticatedClient{
|
||||
AuthenticatedClient: &testutils.AuthenticatedClient{
|
||||
Client: ctx.client,
|
||||
Token: device3.Token,
|
||||
RefreshToken: refreshToken3,
|
||||
BaseURL: ctx.baseURL,
|
||||
},
|
||||
}
|
||||
_, statusCode = oldDevice3.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected device3 refresh token to be revoked, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_TokenTiming(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("token_timing", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "timing", "Password123!")
|
||||
|
||||
cfg := &config.Config{
|
||||
JWT: config.JWTConfig{
|
||||
Secret: "test-secret-key-for-testing-purposes-only",
|
||||
Expiration: 24,
|
||||
RefreshExpiration: 168,
|
||||
Issuer: "goyco",
|
||||
Audience: "goyco-users",
|
||||
},
|
||||
}
|
||||
|
||||
user, err := ctx.server.UserRepo.GetByID(createdUser.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
|
||||
t.Run("token_just_before_expiry", func(t *testing.T) {
|
||||
token := generateTokenWithExpiration(t, user, &cfg.JWT, 1*time.Minute)
|
||||
statusCode := ctx.makeRequestWithToken(t, token)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Errorf("Expected token just before expiry to work, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token_just_after_expiry", func(t *testing.T) {
|
||||
token := generateTokenWithExpiration(t, user, &cfg.JWT, -1*time.Minute)
|
||||
statusCode := ctx.makeRequestWithToken(t, token)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected expired token to be rejected, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token_expiration_edge_case", func(t *testing.T) {
|
||||
token := generateTokenWithExpiration(t, user, &cfg.JWT, 0)
|
||||
statusCode := ctx.makeRequestWithToken(t, token)
|
||||
if statusCode == http.StatusOK {
|
||||
t.Log("Token with zero expiration may be accepted (clock skew tolerance)")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_TokenReplayAttack(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("token_replay_attack", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "replay", "Password123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
token := authClient.Token
|
||||
|
||||
t.Run("same_token_multiple_times", func(t *testing.T) {
|
||||
for i := 0; i < 5; i++ {
|
||||
statusCode := ctx.makeRequestWithToken(t, token)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Errorf("Expected token to work multiple times (replay %d), got status %d", i+1, statusCode)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token_reuse_after_revocation", func(t *testing.T) {
|
||||
authClient.RevokeAllTokens(t)
|
||||
|
||||
statusCode := ctx.makeRequestWithToken(t, token)
|
||||
if statusCode == http.StatusOK {
|
||||
t.Log("Access token may still work after refresh token revocation (acceptable)")
|
||||
}
|
||||
|
||||
_, statusCode = authClient.RefreshAccessToken(t)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected refresh token to be rejected after revocation, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token_reuse_after_user_deletion", func(t *testing.T) {
|
||||
testUser := ctx.createUserWithCleanup(t, "deleteuser", "Password123!")
|
||||
deleteClient := ctx.loginUser(t, testUser.Username, testUser.Password)
|
||||
deleteToken := deleteClient.Token
|
||||
|
||||
ctx.server.EmailSender.Reset()
|
||||
deleteClient.RequestAccountDeletion(t)
|
||||
deletionToken := ctx.server.EmailSender.DeletionToken()
|
||||
if deletionToken == "" {
|
||||
t.Fatalf("Expected deletion token")
|
||||
}
|
||||
|
||||
deleteClient.ConfirmAccountDeletion(t, deletionToken, false)
|
||||
|
||||
statusCode := ctx.makeRequestWithToken(t, deleteToken)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected token to be rejected after user deletion, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_TokenScope(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("token_scope", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "scope", "Password123!")
|
||||
|
||||
cfg := &config.Config{
|
||||
JWT: config.JWTConfig{
|
||||
Secret: "test-secret-key-for-testing-purposes-only",
|
||||
Expiration: 24,
|
||||
RefreshExpiration: 168,
|
||||
Issuer: "goyco",
|
||||
Audience: "goyco-users",
|
||||
},
|
||||
}
|
||||
|
||||
user, err := ctx.server.UserRepo.GetByID(createdUser.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
|
||||
t.Run("access_token_cannot_be_used_as_refresh", func(t *testing.T) {
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
accessToken := authClient.Token
|
||||
|
||||
refreshData := map[string]string{
|
||||
"refresh_token": accessToken,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(refreshData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal refresh data: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", ctx.baseURL+"/api/auth/refresh", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create refresh request: %v", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
testutils.WithStandardHeaders(request)
|
||||
|
||||
resp, err := ctx.client.Do(request)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make refresh request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
t.Errorf("Expected access token to be rejected as refresh token, got status 200")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("refresh_token_cannot_access_protected_endpoints", func(t *testing.T) {
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
refreshTokenString := authClient.RefreshToken
|
||||
|
||||
statusCode := ctx.makeRequestWithToken(t, refreshTokenString)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected refresh token string to be rejected for protected endpoints, got status %d", statusCode)
|
||||
}
|
||||
|
||||
invalidTypeToken := generateTokenWithType(t, user, &cfg.JWT, "invalid-type")
|
||||
statusCode = ctx.makeRequestWithToken(t, invalidTypeToken)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected invalid token type to be rejected, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("token_type_validation", func(t *testing.T) {
|
||||
emptyTypeToken := generateTokenWithType(t, user, &cfg.JWT, "")
|
||||
statusCode := ctx.makeRequestWithToken(t, emptyTypeToken)
|
||||
if statusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected empty token type to be rejected, got status %d", statusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_ConcurrentLoginPrevention(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("concurrent_login_prevention", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "concurrent", "Password123!")
|
||||
|
||||
user, err := ctx.server.UserRepo.GetByID(createdUser.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
|
||||
initialVersion := user.SessionVersion
|
||||
|
||||
login1 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
login2 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
login3 := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
user, err = ctx.server.UserRepo.GetByID(createdUser.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user after logins: %v", err)
|
||||
}
|
||||
|
||||
if user.SessionVersion != initialVersion {
|
||||
t.Log("Session version may increment on login (acceptable behavior)")
|
||||
}
|
||||
|
||||
profile1 := login1.GetProfile(t)
|
||||
if profile1.Data.Username != createdUser.Username {
|
||||
t.Errorf("Expected login1 to work")
|
||||
}
|
||||
|
||||
profile2 := login2.GetProfile(t)
|
||||
if profile2.Data.Username != createdUser.Username {
|
||||
t.Errorf("Expected login2 to work")
|
||||
}
|
||||
|
||||
profile3 := login3.GetProfile(t)
|
||||
if profile3.Data.Username != createdUser.Username {
|
||||
t.Errorf("Expected login3 to work")
|
||||
}
|
||||
|
||||
if login1.Token == login2.Token || login1.Token == login3.Token || login2.Token == login3.Token {
|
||||
t.Errorf("Expected concurrent logins to generate different tokens")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user