Skip to content
This repository has been archived by the owner on Mar 11, 2021. It is now read-only.

Commit

Permalink
Add support for last active timestamp for identities (#797)
Browse files Browse the repository at this point in the history
* ISSUE-796 add support for last active timestamp for identities

Signed-off-by: Shane Bryzak <[email protected]>

* ISSUE-796 added tests for all user activity scenarios

Signed-off-by: Shane Bryzak <[email protected]>

* ISSUE-796 fixed golangcibot issues

Signed-off-by: Shane Bryzak <[email protected]>

* ISSUE-796 update last active timestamp during token validation, updated test

Signed-off-by: Shane Bryzak <[email protected]>
  • Loading branch information
sbryzak authored Feb 28, 2019
1 parent 79d8f98 commit d9b7b20
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 6 deletions.
20 changes: 20 additions & 0 deletions authentication/account/repository/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ type Identity struct {
// Link to Resource
IdentityResourceID sql.NullString
IdentityResource resource.Resource `gorm:"foreignkey:IdentityResourceID;association_foreignkey:ResourceID"`
// Timestamp of the identity's last activity
LastActive *time.Time
}

// TableName overrides the table name settings in Gorm to force a specific table name
Expand Down Expand Up @@ -135,6 +137,7 @@ type IdentityRepository interface {
AddMember(ctx context.Context, identityID uuid.UUID, memberID uuid.UUID) error
RemoveMember(ctx context.Context, memberOf uuid.UUID, memberID uuid.UUID) error
FlagPrivilegeCacheStaleForMembershipChange(ctx context.Context, memberID uuid.UUID, memberOf uuid.UUID) error
TouchLastActive(ctx context.Context, identityID uuid.UUID) error
}

// TableName overrides the table name settings in Gorm to force a specific table name
Expand Down Expand Up @@ -832,3 +835,20 @@ WHERE

return nil
}

// TouchLastActive is intended to be a lightweight method that updates the last active column for a specified identity
// to the current timestamp
func (m *GormIdentityRepository) TouchLastActive(ctx context.Context, identityID uuid.UUID) error {
defer goa.MeasureSince([]string{"goa", "db", "identity", "TouchLastActive"}, time.Now())

err := m.db.Exec("UPDATE identities SET last_active = ? WHERE id = ?", time.Now(), identityID).Error
if err != nil {
log.Error(ctx, map[string]interface{}{
"id": identityID,
"err": err,
}, "unable to update last active time")
return errs.WithStack(err)
}

return nil
}
17 changes: 17 additions & 0 deletions authentication/account/repository/identity_blackbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package repository_test

import (
"testing"
"time"

"github.com/fabric8-services/fabric8-auth/authorization"
"github.com/fabric8-services/fabric8-auth/errors"
Expand Down Expand Up @@ -380,3 +381,19 @@ func (s *IdentityRepositoryTestSuite) TestRemoveMember() {
require.NoError(s.T(), err)
require.Len(s.T(), memberships, 0)
}

func (s *IdentityRepositoryTestSuite) TestTouchLastUpdated() {
identity := s.Graph.CreateIdentity()

require.Nil(s.T(), identity.Identity().LastActive)

now := time.Now()

err := s.Application.Identities().TouchLastActive(s.Ctx, identity.ID())
require.NoError(s.T(), err)

// Reload the identity
identity = s.Graph.LoadIdentity(identity.ID())

require.True(s.T(), now.Before(*identity.Identity().LastActive))
}
26 changes: 20 additions & 6 deletions authentication/logout/service/logout_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,29 @@ func (s *logoutServiceImpl) Logout(ctx context.Context, redirectURL string) (str
if sub == nil {
return "", errors.NewUnauthorizedError("missing 'sub' claim in the refresh token")
}
identityID, err := uuid.FromString(fmt.Sprintf("%s", sub))
if err != nil {
return "", errors.NewUnauthorizedError(err.Error())
}

err = s.Services().TokenService().SetStatusForAllIdentityTokens(ctx, identityID, token.TOKEN_STATUS_LOGGED_OUT)
err = s.ExecuteInTransaction(func() error {
identityID, err := uuid.FromString(fmt.Sprintf("%s", sub))
if err != nil {
return errors.NewUnauthorizedError(err.Error())
}

err = s.Services().TokenService().SetStatusForAllIdentityTokens(ctx, identityID, token.TOKEN_STATUS_LOGGED_OUT)
if err != nil {
return errors.NewInternalError(ctx, err)
}

// Update the identity's last active timestamp on logout
err = s.Repositories().Identities().TouchLastActive(ctx, identityID)
if err != nil {
return errors.NewInternalError(ctx, err)
}

return nil
})

if err != nil {
return "", errors.NewInternalError(ctx, err)
return "", err
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,13 @@ func (s *authenticationProviderServiceImpl) CreateOrUpdateIdentityAndUser(ctx co
"user_name": identity.Username,
}, "local user created/updated")

// Update the identity's last active timestamp
err = s.Repositories().Identities().TouchLastActive(ctx, identity.ID)
if err != nil {
log.Error(ctx, map[string]interface{}{"err": err, "identity_id": identity.ID.String()}, "failed to update last_active timestamp for identity")
return nil, nil, err
}

// Generate a new user token instead of using the original oauth provider token
userToken, err := tokenManager.GenerateUserTokenForIdentity(ctx, *identity, false)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,8 @@ func (s *authenticationProviderServiceTestSuite) TestCreateOrUpdateIdentityAndUs
}, nil
}

now := time.Now()

// when
tm := testtoken.TokenManager
ctx := manager.ContextWithTokenManager(context.Background(), tm)
Expand All @@ -1084,6 +1086,10 @@ func (s *authenticationProviderServiceTestSuite) TestCreateOrUpdateIdentityAndUs
assert.NotEmpty(s.T(), resultAccessTokenClaims.SessionState)
s.T().Logf("token claim `session_state`: %v", resultAccessTokenClaims.SessionState)

// Confirm that the identity's last active field was updated
identity := s.Graph.LoadIdentity(user.IdentityID())
require.True(s.T(), now.Before(*identity.Identity().LastActive))

// Confirm that both an access token and refresh token were created for the user's identity
tokens, err := s.Application.TokenRepository().ListForIdentity(s.Ctx, user.IdentityID())
require.NoError(s.T(), err)
Expand Down
17 changes: 17 additions & 0 deletions authorization/token/service/token_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,9 @@ func (s *tokenServiceImpl) CleanupExpiredTokens(ctx context.Context) error {
return nil
}

// ValidateToken extracts the token ID (the "jti" claim) from the token and uses it to perform a db lookup of the token's
// status, and if the status is invalid will return an unauthorized error. For valid tokens, it will also update the
// identity's (determined from the token's "sub" claim) last active timestamp
func (s *tokenServiceImpl) ValidateToken(ctx context.Context, accessToken *jwt.Token) error {
claims := accessToken.Claims.(jwt.MapClaims)

Expand Down Expand Up @@ -784,6 +787,20 @@ func (s *tokenServiceImpl) ValidateToken(ctx context.Context, accessToken *jwt.T
return errors.NewUnauthorizedError("invalid token")
}

identityID, err := uuid.FromString(claims["sub"].(string))
if err != nil {
log.Error(ctx, map[string]interface{}{"error": err}, "could not extract identity ID ('sub' claim) from token")
return errors.NewBadParameterErrorFromString("token", accessToken.Raw,
"could not extract identity ID from token")
}

// Update the identity's last active timestamp
err = s.Repositories().Identities().TouchLastActive(ctx, identityID)
if err != nil {
log.Error(ctx, map[string]interface{}{"error": err}, "could not update last active timestamp")
return errors.NewInternalError(ctx, err)
}

return nil
}

Expand Down
25 changes: 25 additions & 0 deletions authorization/token/service/token_service_blackbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,31 @@ func (s *tokenServiceBlackboxTest) TestExchangeRefreshTokenWithRPTTokenRevoked()
assert.Empty(s.T(), userToken)
}

func (s *tokenServiceBlackboxTest) TestExchangeRefreshTokenUpdatesLastActive() {

tm := testtoken.TokenManager
ctx := manager.ContextWithTokenManager(testtoken.ContextWithRequest(context.Background()), tm)
// create a user
user := s.Graph.CreateUser()
// Create an initial access token for the user
at, err := tm.GenerateUserTokenForIdentity(ctx, *user.Identity(), false)
require.NoError(s.T(), err)

// Register the refresh token
_, err = s.Application.TokenService().RegisterToken(ctx, user.IdentityID(), at.RefreshToken, token.TOKEN_TYPE_REFRESH, nil)
require.NoError(s.T(), err)

now := time.Now()

// refresh the user token
_, err = s.Application.TokenService().ExchangeRefreshToken(ctx, at.RefreshToken, "")
require.NoError(s.T(), err)

identity := s.Graph.LoadIdentity(user.IdentityID())

require.True(s.T(), now.Before(*identity.Identity().LastActive))
}

func (s *tokenServiceBlackboxTest) TestRegisterInvalidToken() {
// First test an invalid token string
_, err := s.Application.TokenService().RegisterToken(s.Ctx, uuid.NewV4(), "foo", token.TOKEN_TYPE_ACCESS, nil)
Expand Down
8 changes: 8 additions & 0 deletions controller/logout_blackbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/fabric8-services/fabric8-auth/authorization/token"
"github.com/fabric8-services/fabric8-auth/authorization/token/manager"
"testing"
"time"

"context"
"net/http"
Expand Down Expand Up @@ -161,6 +162,9 @@ func (s *LogoutControllerTestSuite) TestLogoutV2TokensInvalidated() {
ctx := manager.ContextWithTokenManager(testtoken.ContextWithRequest(context.Background()), tm)
// create a user
user := s.Graph.CreateUser()

now := time.Now()

// Create an initial access token for the user
at, err := tm.GenerateUserTokenForIdentity(ctx, *user.Identity(), false)
require.NoError(s.T(), err)
Expand Down Expand Up @@ -195,6 +199,10 @@ func (s *LogoutControllerTestSuite) TestLogoutV2TokensInvalidated() {

test.Logoutv2LogoutOK(s.T(), goajwt.WithJWT(svc.Context, tk), svc, ctrl, &redirect, nil)

identity := s.Graph.LoadIdentity(user.IdentityID())
// Confirm that the last active field has been updated during logout
require.True(s.T(), now.Before(*identity.Identity().LastActive))

// Load the token again
loadedToken, err = s.Application.TokenRepository().Load(s.Ctx, tokenID)
require.NoError(s.T(), err)
Expand Down
5 changes: 5 additions & 0 deletions goamiddleware/jwt_token_context_whitebox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http/httptest"
"net/textproto"
"testing"
"time"

"github.com/fabric8-services/fabric8-auth/gormtestsupport"
testtoken "github.com/fabric8-services/fabric8-auth/test/token"
Expand Down Expand Up @@ -76,12 +77,16 @@ func (s *testJWTokenContextSuite) TestHandler() {
rw = httptest.NewRecorder()
tkn := s.Graph.CreateToken()
rq.Header.Set("Authorization", "Bearer "+tkn.TokenString())
now := time.Now()
err = h(context.Background(), rw, rq)
require.Error(s.T(), err)
assert.Equal(s.T(), "next-handler-error", err.Error())
header = textproto.MIMEHeader(rw.Header())
require.Empty(s.T(), header.Get("WWW-Authenticate"))
require.Empty(s.T(), header.Get("Access-Control-Expose-Headers"))
// Confirm that the identity's last active timestamp has been updated
identity := s.Graph.LoadIdentity(tkn.Token().IdentityID)
require.True(s.T(), now.Before(*identity.Identity().LastActive))

// Test with an invalid user token
rw = httptest.NewRecorder()
Expand Down
3 changes: 3 additions & 0 deletions migration/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ func GetMigrations(configuration MigrationConfiguration) Migrations {
// Version 44
m = append(m, steps{ExecuteSQLFile("044-user-active.sql")})

// Version 45
m = append(m, steps{ExecuteSQLFile("045-identity-last-active.sql")})

// Version N
//
// In order to add an upgrade, simply append an array of MigrationFunc to the
Expand Down
1 change: 1 addition & 0 deletions migration/sql-files/045-identity-last-active.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE identities ADD COLUMN last_active timestamp with time zone;

0 comments on commit d9b7b20

Please sign in to comment.