605 lines
16 KiB
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"}}},
|
|
}}
|
|
}
|