src.dualinventive.com/go/authentication-service/internal/authtokens/tokens_test.go

605 lines
16 KiB
Go

package authtokens
import (
"os"
"testing"
"time"
"bou.ke/monkey"
"github.com/alicebob/miniredis"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
"src.dualinventive.com/go/authentication-service/internal/domain"
"src.dualinventive.com/go/authentication-service/internal/storage"
"src.dualinventive.com/go/authentication-service/internal/storage/redis"
"src.dualinventive.com/go/lib/dilog"
)
const userAgent = "SomeUserAgent"
var (
miniRedis *miniredis.Miniredis //nolint:gochecknoglobals
testCredentialRepo *TestCredentialRepository //nolint:gochecknoglobals
testTokenRepo storage.TokenRepository //nolint:gochecknoglobals
testLogger dilog.Logger //nolint:gochecknoglobals
tokenService *Service //nolint:gochecknoglobals
)
func TestMain(m *testing.M) {
code := m.Run()
os.Exit(code)
}
func TestCreateToken(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil}
//when
token, err := tokenService.CreateToken(credentials, userAgent)
//then
require.Nil(t, err)
require.NotNil(t, token)
miniRedis.CheckGet(t, "token:"+token.Secret, "someName::someCompanyCode")
after()
}
func TestCreateTokenGivenInvalidCredentials(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someOtherSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil}
//when
token, err := tokenService.CreateToken(credentials, userAgent)
//then - should throw error
require.NotNil(t, err)
require.IsType(t, new(ErrInvalidCredentials), err)
require.Nil(t, token)
after()
}
func TestCreateTokenGivenMultipleEvocations(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil, nil, nil}
//when
token1, _ := tokenService.CreateToken(credentials, userAgent)
token2, _ := tokenService.CreateToken(credentials, userAgent)
token3, _ := tokenService.CreateToken(credentials, userAgent)
//then - redis contains 3 tokens
require.NotNil(t, token1)
require.NotNil(t, token2)
require.NotNil(t, token3)
miniRedis.CheckGet(t, "token:"+token1.Secret, "someName::someCompanyCode")
miniRedis.CheckGet(t, "token:"+token2.Secret, "someName::someCompanyCode")
miniRedis.CheckGet(t, "token:"+token3.Secret, "someName::someCompanyCode")
after()
}
func TestDeleteToken(t *testing.T) {
before(t)
//given
token := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
//when
err := tokenService.DeleteToken(token)
//then
require.Nil(t, err)
require.False(t, miniRedis.Exists("token:"+token.Secret))
after()
}
func TestDeleteTokenGivenNotExistingToken(t *testing.T) {
before(t)
//given
token := &domain.Token{Secret: "someInvalidToken"}
//when
err := tokenService.DeleteToken(token)
//then
require.NotNil(t, err)
require.IsType(t, new(ErrTokenNotFound), err)
after()
}
func TestDeleteTokenGivenNilToken(t *testing.T) {
before(t)
//when
err := tokenService.DeleteToken(nil)
//then
require.NotNil(t, err)
require.IsType(t, new(ErrNilToken), err)
after()
}
func TestGetUserByToken(t *testing.T) {
before(t)
//given
token := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
testCredentialRepo.callCount = 0 //reset
//when
user, err := tokenService.GetUserByToken(token)
//then
require.Nil(t, err)
require.NotNil(t, user)
require.Equal(t, "someUsername", user.Name)
after()
}
func TestGetUserByTokenGivenNilToken(t *testing.T) {
before(t)
//when
user, err := tokenService.GetUserByToken(nil)
// This test can hang on closing the miniRedis database (after()),
// because a connection is opened to miniRedis but no request is done.
// Fixed this in the latest release of kv.
//then
require.NotNil(t, err)
require.IsType(t, new(ErrNilToken), err)
require.Nil(t, user)
after()
}
func TestGetUserByTokenGivenNonExistingToken(t *testing.T) {
before(t)
//given
token := &domain.Token{Secret: "someNonExistingToken"}
//when
user, err := tokenService.GetUserByToken(token)
//then
require.NotNil(t, err)
require.IsType(t, new(ErrTokenNotFound), err)
require.Nil(t, user)
after()
}
func TestGetTokensByUser(t *testing.T) {
before(t)
//given
token1 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
token2 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
token3 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
token4 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
testCredentialRepo.callCount = 0 //reset
//when
tokens, err := tokenService.GetTokensByUser("someUsername", "someCompanyCode")
//then
require.Nil(t, err)
require.Contains(t, tokens, token1)
require.Contains(t, tokens, token2)
require.Contains(t, tokens, token3)
require.Contains(t, tokens, token4)
after()
}
func TestGetOpaqueTokensByToken(t *testing.T) {
before(t)
//given
token1 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
someToken(t, "someUsername", "someCompanyCode", "someSecret", "Web")
//when
tokens, err := tokenService.GetOpaqueTokensByToken(token1)
//then
require.Nil(t, err)
require.Len(t, tokens, 4)
after()
}
func TestDeleteTokenByOpaqueId_UsingTokenFromTheSameUser(t *testing.T) {
before(t)
//given
token1 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Test")
token2 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "SomeOther")
token3 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "SomeOther")
token4 := someToken(t, "someUsername", "someCompanyCode", "someSecret", "SomeOther")
opaqueTokens, err := tokenService.GetOpaqueTokensByToken(token1)
require.Nil(t, err)
require.Len(t, opaqueTokens, 4)
opaqueToken1 := findOpaqueID(opaqueTokens, "Test")
require.NotEmpty(t, opaqueToken1)
//when - deleting a token from the same user
err = tokenService.DeleteTokenByOpaqueID(token2, opaqueToken1)
//then
require.Nil(t, err)
tokens, err := tokenService.GetTokensByUser("someUsername", "someCompanyCode")
require.Nil(t, err)
require.Len(t, tokens, 3)
require.NotContains(t, tokens, token1)
require.Contains(t, tokens, token2)
require.Contains(t, tokens, token3)
require.Contains(t, tokens, token4)
after()
}
func TestDeleteTokenByOpaqueId_DeletingItself(t *testing.T) {
before(t)
//given
token := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Test")
opaqueTokens, err := tokenService.GetOpaqueTokensByToken(token)
require.Nil(t, err)
require.Len(t, opaqueTokens, 1)
opaqueToken := findOpaqueID(opaqueTokens, "Test")
require.NotEmpty(t, opaqueToken)
//when - deleting a the current token
err = tokenService.DeleteTokenByOpaqueID(token, opaqueToken)
//then
require.Nil(t, err)
tokens, err := tokenService.GetTokensByUser("someUsername", "someCompanyCode")
require.Nil(t, err)
require.Len(t, tokens, 0)
require.NotContains(t, tokens, token)
after()
}
func TestDeleteTokenByOpaqueId_UsingAnotherUsersToken(t *testing.T) {
before(t)
//given
targetToken := someToken(t, "someUsername", "someCompanyCode", "someSecret", "Test")
currentToken := someToken(t, "someUsername", "someCompanyCode", "someSecret", "SomeOther")
strangeToken := someToken(t, "someOtherUsername", "someCompanyCode", "someSecret", "SomeOther")
opaqueTokens, err := tokenService.GetOpaqueTokensByToken(currentToken)
require.Nil(t, err)
require.Len(t, opaqueTokens, 2)
opaqueTargetToken := findOpaqueID(opaqueTokens, "Test")
require.NotEmpty(t, opaqueTargetToken)
//when - deleting a token from another user fails
err = tokenService.DeleteTokenByOpaqueID(strangeToken, opaqueTargetToken)
//then
require.NotNil(t, err)
require.IsType(t, new(ErrTokenNotFound), err)
tokens, err := tokenService.GetTokensByUser("someUsername", "someCompanyCode")
require.Nil(t, err)
require.Len(t, tokens, 2)
require.Contains(t, tokens, targetToken)
require.Contains(t, tokens, currentToken)
after()
}
func TestDeleteTokenByOpaqueId_UsingNilToken(t *testing.T) {
before(t)
//when - deleting a token from another user fails
err := tokenService.DeleteTokenByOpaqueID(nil, "somethingFake")
//then
require.NotNil(t, err)
require.IsType(t, new(ErrNilToken), err)
after()
}
func TestDeleteTokenByOpaqueId_UsingEmptyToken(t *testing.T) {
before(t)
//when - deleting a token from another user fails
err := tokenService.DeleteTokenByOpaqueID(&domain.Token{}, "somethingFake")
//then
require.NotNil(t, err)
require.IsType(t, new(ErrInvalidToken), err)
after()
}
func TestDeleteTokenByOpaqueId_UsingEmptyTarget(t *testing.T) {
before(t)
//given
token := someToken(t, "someUsername", "someCompanyCode", "someSecret", "SomeOther")
//when - deleting a token from another user fails
err := tokenService.DeleteTokenByOpaqueID(token, "")
//then
require.NotNil(t, err)
require.IsType(t, new(ErrTokenNotFound), err)
after()
}
func TestDeleteTokenByOpaqueId_UsingInvalidTarget(t *testing.T) {
before(t)
//given
token := someToken(t, "someUsername", "someCompanyCode", "someSecret", "SomeOther")
//when - deleting a token from another user fails
err := tokenService.DeleteTokenByOpaqueID(token, "someUnknownID")
//then
require.NotNil(t, err)
require.IsType(t, new(ErrTokenNotFound), err)
after()
}
func TestVerifyToken(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil}
//when
token, _ := tokenService.CreateToken(credentials, userAgent)
token, err := tokenService.VerifyToken(token)
//then
require.Nil(t, err)
require.NotNil(t, token)
after()
}
func TestVerifyToken_GivenValidRights(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil}
//when
token, _ := tokenService.CreateToken(credentials, userAgent)
token, err := tokenService.VerifyToken(token, "a", "b", "e")
//then
require.Nil(t, err)
require.NotNil(t, token)
after()
}
func TestVerifyToken_GivenPartialValidRights(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil}
//when
token, _ := tokenService.CreateToken(credentials, userAgent)
_, err := tokenService.VerifyToken(token, "a", "b", "fail")
//then
require.NotNil(t, err)
require.EqualError(t, err, "unauthorized token")
after()
}
func TestVerifyToken_GivenInvalidRights(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil}
//when
token, _ := tokenService.CreateToken(credentials, userAgent)
_, err := tokenService.VerifyToken(token, "fail1", "fail2")
//then
require.NotNil(t, err)
require.EqualError(t, err, "unauthorized token")
after()
}
func TestVerifyTokenExpired(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil}
//when
token, _ := tokenService.CreateToken(credentials, userAgent)
// Shift the time by replacing time.Now()
future := time.Now().Add(25 * time.Hour)
patch := monkey.Patch(time.Now, func() time.Time { return future })
defer patch.Unpatch()
_, err := tokenService.VerifyToken(token)
//then
require.IsType(t, new(ErrInvalidToken), err)
after()
}
func TestVerifyTokenNotExpired(t *testing.T) {
before(t)
//given
credentials := domain.Credentials{User: "someName", CompanyCode: "someCompanyCode", Password: "someSecret"}
testCredentialRepo.returnMe = someUser(t, "someName", "someSecret", "someCompanyCode")
testCredentialRepo.returnErr = []error{nil, nil}
//when
// Create token1 @ t
token1, _ := tokenService.CreateToken(credentials, userAgent)
// Shift the time by replacing time.Now()
future := time.Now().Add(13 * time.Hour)
patch := monkey.Patch(time.Now, func() time.Time { return future })
defer patch.Unpatch()
// Create token2 @ t + 13h
token2, _ := tokenService.CreateToken(credentials, userAgent)
// Shift the time by replacing time.Now()
future = time.Now().Add(13 * time.Hour)
patch = monkey.Patch(time.Now, func() time.Time { return future })
defer patch.Unpatch()
// Verify token1 @ t + 26h (should be expired)
_, err1 := tokenService.VerifyToken(token1)
// Verify token2 @ t + 26h (should NOT be expired)
_, err2 := tokenService.VerifyToken(token2)
//then
require.IsType(t, new(ErrInvalidToken), err1)
require.Nil(t, err2)
after()
}
func findOpaqueID(opaqueIDs map[string]string, target string) string {
for opaqueID, userAgent := range opaqueIDs {
if userAgent == target {
return opaqueID
}
}
return ""
}
func someToken(t *testing.T, userName, companyCode, secret string, userAgent string) *domain.Token { //nolint:unparam
credentials := domain.Credentials{User: userName, CompanyCode: companyCode, Password: secret}
testCredentialRepo.returnMe = someUser(t, userName, secret, companyCode)
testCredentialRepo.returnErr = []error{nil}
token, err := tokenService.CreateToken(credentials, userAgent)
if err != nil {
t.Fatal(err)
}
require.True(t, miniRedis.Exists("token:"+token.Secret))
testCredentialRepo.callCount = 0
return token
}
type TestCredentialRepository struct {
callCount int
returnMe *domain.User
returnErr []error
}
func (r *TestCredentialRepository) GetUserByUserName(userName string, companyCode string) (*domain.User, error) {
err := r.returnErr[r.callCount]
r.callCount++
return r.returnMe, err
}
func (r *TestCredentialRepository) SetPassword(userName *domain.User, passwordHash []byte) error {
err := r.returnErr[r.callCount]
r.callCount++
return err
}
func before(t *testing.T) {
r, err := miniredis.Run()
if err != nil {
t.Fatal("failed to run miniredis")
}
miniRedis = r
testCredentialRepo = &TestCredentialRepository{0, nil, []error{nil}}
testLogger = dilog.NewNilLogger()
tokenRepo, err := redis.NewTokenRepository(
r.Host(), r.Port(), "testdata/key_rsa", "testdata/key_rsa.pub", testLogger,
)
if err != nil {
t.Fatal(err)
}
testTokenRepo = tokenRepo
tokenService = NewService(
testLogger,
testCredentialRepo,
testTokenRepo,
nil,
nil,
nil,
)
}
func after() {
miniRedis.Close()
}
func someUser(t *testing.T, userName, secret string, companyCode string) *domain.User { //nolint:unparam
hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
if err != nil {
t.Fatal(err)
}
return &domain.User{Name: userName, PasswordHash: string(hash), Roles: []domain.Role{
{Rights: []domain.Right{{Code: "a"}, {Code: "b"}, {Code: "c"}}},
{Rights: []domain.Right{{Code: "c"}, {Code: "d"}, {Code: "e"}}},
{Rights: []domain.Right{{Code: "e"}, {Code: "f"}, {Code: "g"}}},
}}
}