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"}}}, }} }