Procházet zdrojové kódy

Merge pull request #15839 from grafana/15836_revoke_auth_tokens

Support list and revoke of user auth tokens in HTTP API
Carl Bergquist před 6 roky
rodič
revize
23852b59c9

+ 102 - 0
docs/sources/http_api/admin.md

@@ -341,3 +341,105 @@ Content-Type: application/json
 
 {"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
 ```
+
+## Auth tokens for User
+
+`GET /api/admin/users/:id/auth-tokens`
+
+Return a list of all auth tokens (devices) that the user currently have logged in from.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+GET /api/admin/users/1/auth-tokens HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+  {
+    "id": 361,
+    "isActive": false,
+    "clientIp": "127.0.0.1",
+    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+    "createdAt": "2019-03-05T21:22:54+01:00",
+    "seenAt": "2019-03-06T19:41:06+01:00"
+  },
+  {
+    "id": 364,
+    "isActive": false,
+    "clientIp": "127.0.0.1",
+    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+    "createdAt": "2019-03-06T19:41:19+01:00",
+    "seenAt": "2019-03-06T19:41:21+01:00"
+  }
+]
+```
+
+## Revoke auth token for User
+
+`POST /api/admin/users/:id/revoke-auth-token`
+
+Revokes the given auth token (device) for the user. User of issued auth token (device) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+POST /api/admin/users/1/revoke-auth-token HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+  "authTokenId": 364
+}
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+  "message": "User auth token revoked"
+}
+```
+
+## Logout User
+
+`POST /api/admin/users/:id/logout`
+
+Logout user revokes all auth tokens (devices) for the user. User of issued auth tokens (devices) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+POST /api/admin/users/1/logout HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+  "message": "User auth token revoked"
+}
+```

+ 72 - 0
docs/sources/http_api/user.md

@@ -478,3 +478,75 @@ Content-Type: application/json
 
 {"message":"Dashboard unstarred"}
 ```
+
+## Auth tokens of the actual User
+
+`GET /api/user/auth-tokens`
+
+Return a list of all auth tokens (devices) that the actual user currently have logged in from.
+
+**Example Request**:
+
+```http
+GET /api/user/auth-tokens HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+  {
+    "id": 361,
+    "isActive": true,
+    "clientIp": "127.0.0.1",
+    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+    "createdAt": "2019-03-05T21:22:54+01:00",
+    "seenAt": "2019-03-06T19:41:06+01:00"
+  },
+  {
+    "id": 364,
+    "isActive": false,
+    "clientIp": "127.0.0.1",
+    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+    "createdAt": "2019-03-06T19:41:19+01:00",
+    "seenAt": "2019-03-06T19:41:21+01:00"
+  }
+]
+```
+
+## Revoke an auth token of the actual User
+
+`POST /api/user/revoke-auth-token`
+
+Revokes the given auth token (device) for the actual user. User of issued auth token (device) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+**Example Request**:
+
+```http
+POST /api/user/revoke-auth-token HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+{
+  "authTokenId": 364
+}
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+  "message": "User auth token revoked"
+}
+```

+ 23 - 0
pkg/api/admin_users.go

@@ -110,3 +110,26 @@ func AdminDeleteUser(c *m.ReqContext) {
 
 	c.JsonOK("User deleted")
 }
+
+// POST /api/admin/users/:id/logout
+func (server *HTTPServer) AdminLogoutUser(c *m.ReqContext) Response {
+	userID := c.ParamsInt64(":id")
+
+	if c.UserId == userID {
+		return Error(400, "You cannot logout yourself", nil)
+	}
+
+	return server.logoutUserFromAllDevicesInternal(userID)
+}
+
+// GET /api/admin/users/:id/auth-tokens
+func (server *HTTPServer) AdminGetUserAuthTokens(c *m.ReqContext) Response {
+	userID := c.ParamsInt64(":id")
+	return server.getUserAuthTokensInternal(c, userID)
+}
+
+// POST /api/admin/users/:id/revoke-auth-token
+func (server *HTTPServer) AdminRevokeUserAuthToken(c *m.ReqContext, cmd m.RevokeAuthTokenCmd) Response {
+	userID := c.ParamsInt64(":id")
+	return server.revokeUserAuthTokenInternal(c, userID, cmd)
+}

+ 138 - 0
pkg/api/admin_users_test.go

@@ -6,6 +6,7 @@ import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/auth"
 
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -27,6 +28,62 @@ func TestAdminApiEndpoint(t *testing.T) {
 			So(sc.resp.Code, ShouldEqual, 400)
 		})
 	})
+
+	Convey("When a server admin attempts to logout himself from all devices", t, func() {
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			cmd.Result = &m.User{Id: TestUserID}
+			return nil
+		})
+
+		adminLogoutUserScenario("Should not be allowed when calling POST on", "/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
+			sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 400)
+		})
+	})
+
+	Convey("When a server admin attempts to logout a non-existing user from all devices", t, func() {
+		userId := int64(0)
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			userId = cmd.Id
+			return m.ErrUserNotFound
+		})
+
+		adminLogoutUserScenario("Should return not found when calling POST on", "/api/admin/users/200/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
+			sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 404)
+			So(userId, ShouldEqual, 200)
+		})
+	})
+
+	Convey("When a server admin attempts to revoke an auth token for a non-existing user", t, func() {
+		userId := int64(0)
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			userId = cmd.Id
+			return m.ErrUserNotFound
+		})
+
+		cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
+
+		adminRevokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/admin/users/200/revoke-auth-token", "/api/admin/users/:id/revoke-auth-token", cmd, func(sc *scenarioContext) {
+			sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 404)
+			So(userId, ShouldEqual, 200)
+		})
+	})
+
+	Convey("When a server admin gets auth tokens for a non-existing user", t, func() {
+		userId := int64(0)
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			userId = cmd.Id
+			return m.ErrUserNotFound
+		})
+
+		adminGetUserAuthTokensScenario("Should return not found when calling GET on", "/api/admin/users/200/auth-tokens", "/api/admin/users/:id/auth-tokens", func(sc *scenarioContext) {
+			sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 404)
+			So(userId, ShouldEqual, 200)
+		})
+	})
 }
 
 func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
@@ -48,3 +105,84 @@ func putAdminScenario(desc string, url string, routePattern string, role m.RoleT
 		fn(sc)
 	})
 }
+
+func adminLogoutUserScenario(desc string, url string, routePattern string, fn scenarioFunc) {
+	Convey(desc+" "+url, func() {
+		defer bus.ClearBusHandlers()
+
+		hs := HTTPServer{
+			Bus:              bus.GetBus(),
+			AuthTokenService: auth.NewFakeUserAuthTokenService(),
+		}
+
+		sc := setupScenarioContext(url)
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = TestUserID
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = m.ROLE_ADMIN
+
+			return hs.AdminLogoutUser(c)
+		})
+
+		sc.m.Post(routePattern, sc.defaultHandler)
+
+		fn(sc)
+	})
+}
+
+func adminRevokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd m.RevokeAuthTokenCmd, fn scenarioFunc) {
+	Convey(desc+" "+url, func() {
+		defer bus.ClearBusHandlers()
+
+		fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+		hs := HTTPServer{
+			Bus:              bus.GetBus(),
+			AuthTokenService: fakeAuthTokenService,
+		}
+
+		sc := setupScenarioContext(url)
+		sc.userAuthTokenService = fakeAuthTokenService
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = TestUserID
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = m.ROLE_ADMIN
+
+			return hs.AdminRevokeUserAuthToken(c, cmd)
+		})
+
+		sc.m.Post(routePattern, sc.defaultHandler)
+
+		fn(sc)
+	})
+}
+
+func adminGetUserAuthTokensScenario(desc string, url string, routePattern string, fn scenarioFunc) {
+	Convey(desc+" "+url, func() {
+		defer bus.ClearBusHandlers()
+
+		fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+		hs := HTTPServer{
+			Bus:              bus.GetBus(),
+			AuthTokenService: fakeAuthTokenService,
+		}
+
+		sc := setupScenarioContext(url)
+		sc.userAuthTokenService = fakeAuthTokenService
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = TestUserID
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = m.ROLE_ADMIN
+
+			return hs.AdminGetUserAuthTokens(c)
+		})
+
+		sc.m.Get(routePattern, sc.defaultHandler)
+
+		fn(sc)
+	})
+}

+ 7 - 0
pkg/api/api.go

@@ -133,6 +133,9 @@ func (hs *HTTPServer) registerRoutes() {
 
 			userRoute.Get("/preferences", Wrap(GetUserPreferences))
 			userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
+
+			userRoute.Get("/auth-tokens", Wrap(hs.GetUserAuthTokens))
+			userRoute.Post("/revoke-auth-token", bind(m.RevokeAuthTokenCmd{}), Wrap(hs.RevokeUserAuthToken))
 		})
 
 		// users (admin permission required)
@@ -375,6 +378,10 @@ func (hs *HTTPServer) registerRoutes() {
 		adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
 		adminRoute.Get("/stats", AdminGetStats)
 		adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
+
+		adminRoute.Post("/users/:id/logout", Wrap(hs.AdminLogoutUser))
+		adminRoute.Get("/users/:id/auth-tokens", Wrap(hs.AdminGetUserAuthTokens))
+		adminRoute.Post("/users/:id/revoke-auth-token", bind(m.RevokeAuthTokenCmd{}), Wrap(hs.AdminRevokeUserAuthToken))
 	}, reqGrafanaAdmin)
 
 	// rendering

+ 9 - 7
pkg/api/common_test.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/auth"
 	"gopkg.in/macaron.v1"
 
 	. "github.com/smartystreets/goconvey/convey"
@@ -94,13 +95,14 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
 }
 
 type scenarioContext struct {
-	m              *macaron.Macaron
-	context        *m.ReqContext
-	resp           *httptest.ResponseRecorder
-	handlerFunc    handlerFunc
-	defaultHandler macaron.Handler
-	req            *http.Request
-	url            string
+	m                    *macaron.Macaron
+	context              *m.ReqContext
+	resp                 *httptest.ResponseRecorder
+	handlerFunc          handlerFunc
+	defaultHandler       macaron.Handler
+	req                  *http.Request
+	url                  string
+	userAuthTokenService *auth.FakeUserAuthTokenService
 }
 
 func (sc *scenarioContext) exec() {

+ 12 - 0
pkg/api/dtos/user_token.go

@@ -0,0 +1,12 @@
+package dtos
+
+import "time"
+
+type UserToken struct {
+	Id        int64     `json:"id"`
+	IsActive  bool      `json:"isActive"`
+	ClientIp  string    `json:"clientIp"`
+	UserAgent string    `json:"userAgent"`
+	CreatedAt time.Time `json:"createdAt"`
+	SeenAt    time.Time `json:"seenAt"`
+}

+ 110 - 0
pkg/api/user_token.go

@@ -0,0 +1,110 @@
+package api
+
+import (
+	"time"
+
+	"github.com/grafana/grafana/pkg/api/dtos"
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+// GET /api/user/auth-tokens
+func (server *HTTPServer) GetUserAuthTokens(c *models.ReqContext) Response {
+	return server.getUserAuthTokensInternal(c, c.UserId)
+}
+
+// POST /api/user/revoke-auth-token
+func (server *HTTPServer) RevokeUserAuthToken(c *models.ReqContext, cmd models.RevokeAuthTokenCmd) Response {
+	return server.revokeUserAuthTokenInternal(c, c.UserId, cmd)
+}
+
+func (server *HTTPServer) logoutUserFromAllDevicesInternal(userID int64) Response {
+	userQuery := models.GetUserByIdQuery{Id: userID}
+
+	if err := bus.Dispatch(&userQuery); err != nil {
+		if err == models.ErrUserNotFound {
+			return Error(404, "User not found", err)
+		}
+		return Error(500, "Could not read user from database", err)
+	}
+
+	err := server.AuthTokenService.RevokeAllUserTokens(userID)
+	if err != nil {
+		return Error(500, "Failed to logout user", err)
+	}
+
+	return JSON(200, util.DynMap{
+		"message": "User logged out",
+	})
+}
+
+func (server *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int64) Response {
+	userQuery := models.GetUserByIdQuery{Id: userID}
+
+	if err := bus.Dispatch(&userQuery); err != nil {
+		if err == models.ErrUserNotFound {
+			return Error(404, "User not found", err)
+		}
+		return Error(500, "Failed to get user", err)
+	}
+
+	tokens, err := server.AuthTokenService.GetUserTokens(userID)
+	if err != nil {
+		return Error(500, "Failed to get user auth tokens", err)
+	}
+
+	result := []*dtos.UserToken{}
+	for _, token := range tokens {
+		isActive := false
+		if c.UserToken != nil && c.UserToken.Id == token.Id {
+			isActive = true
+		}
+
+		result = append(result, &dtos.UserToken{
+			Id:        token.Id,
+			IsActive:  isActive,
+			ClientIp:  token.ClientIp,
+			UserAgent: token.UserAgent,
+			CreatedAt: time.Unix(token.CreatedAt, 0),
+			SeenAt:    time.Unix(token.SeenAt, 0),
+		})
+	}
+
+	return JSON(200, result)
+}
+
+func (server *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd models.RevokeAuthTokenCmd) Response {
+	userQuery := models.GetUserByIdQuery{Id: userID}
+
+	if err := bus.Dispatch(&userQuery); err != nil {
+		if err == models.ErrUserNotFound {
+			return Error(404, "User not found", err)
+		}
+		return Error(500, "Failed to get user", err)
+	}
+
+	token, err := server.AuthTokenService.GetUserToken(userID, cmd.AuthTokenId)
+	if err != nil {
+		if err == models.ErrUserTokenNotFound {
+			return Error(404, "User auth token not found", err)
+		}
+		return Error(500, "Failed to get user auth token", err)
+	}
+
+	if c.UserToken != nil && c.UserToken.Id == token.Id {
+		return Error(400, "Cannot revoke active user auth token", nil)
+	}
+
+	err = server.AuthTokenService.RevokeToken(token)
+	if err != nil {
+		if err == models.ErrUserTokenNotFound {
+			return Error(404, "User auth token not found", err)
+		}
+		return Error(500, "Failed to revoke user auth token", err)
+	}
+
+	return JSON(200, util.DynMap{
+		"message": "User auth token revoked",
+	})
+}

+ 294 - 0
pkg/api/user_token_test.go

@@ -0,0 +1,294 @@
+package api
+
+import (
+	"testing"
+	"time"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/auth"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUserTokenApiEndpoint(t *testing.T) {
+	Convey("When current user attempts to revoke an auth token for a non-existing user", t, func() {
+		userId := int64(0)
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			userId = cmd.Id
+			return m.ErrUserNotFound
+		})
+
+		cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
+
+		revokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/user/revoke-auth-token", "/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) {
+			sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 404)
+			So(userId, ShouldEqual, 200)
+		})
+	})
+
+	Convey("When current user gets auth tokens for a non-existing user", t, func() {
+		userId := int64(0)
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			userId = cmd.Id
+			return m.ErrUserNotFound
+		})
+
+		getUserAuthTokensScenario("Should return not found when calling GET on", "/api/user/auth-tokens", "/api/user/auth-tokens", 200, func(sc *scenarioContext) {
+			sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 404)
+			So(userId, ShouldEqual, 200)
+		})
+	})
+
+	Convey("When logout an existing user from all devices", t, func() {
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			cmd.Result = &m.User{Id: 200}
+			return nil
+		})
+
+		logoutUserFromAllDevicesInternalScenario("Should be successful", 1, func(sc *scenarioContext) {
+			sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 200)
+		})
+	})
+
+	Convey("When logout a non-existing user from all devices", t, func() {
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			return m.ErrUserNotFound
+		})
+
+		logoutUserFromAllDevicesInternalScenario("Should return not found", TestUserID, func(sc *scenarioContext) {
+			sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 404)
+		})
+	})
+
+	Convey("When revoke an auth token for a user", t, func() {
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			cmd.Result = &m.User{Id: 200}
+			return nil
+		})
+
+		cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
+		token := &m.UserToken{Id: 1}
+
+		revokeUserAuthTokenInternalScenario("Should be successful", cmd, 200, token, func(sc *scenarioContext) {
+			sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) {
+				return &m.UserToken{Id: 2}, nil
+			}
+			sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 200)
+		})
+	})
+
+	Convey("When revoke the active auth token used by himself", t, func() {
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			cmd.Result = &m.User{Id: TestUserID}
+			return nil
+		})
+
+		cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
+		token := &m.UserToken{Id: 2}
+
+		revokeUserAuthTokenInternalScenario("Should not be successful", cmd, TestUserID, token, func(sc *scenarioContext) {
+			sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) {
+				return token, nil
+			}
+			sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 400)
+		})
+	})
+
+	Convey("When gets auth tokens for a user", t, func() {
+		bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
+			cmd.Result = &m.User{Id: TestUserID}
+			return nil
+		})
+
+		currentToken := &m.UserToken{Id: 1}
+
+		getUserAuthTokensInternalScenario("Should be successful", currentToken, func(sc *scenarioContext) {
+			tokens := []*m.UserToken{
+				{
+					Id:        1,
+					ClientIp:  "127.0.0.1",
+					UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+					CreatedAt: time.Now().Unix(),
+					SeenAt:    time.Now().Unix(),
+				},
+				{
+					Id:        2,
+					ClientIp:  "127.0.0.2",
+					UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+					CreatedAt: time.Now().Unix(),
+					SeenAt:    time.Now().Unix(),
+				},
+			}
+			sc.userAuthTokenService.GetUserTokensProvider = func(userId int64) ([]*m.UserToken, error) {
+				return tokens, nil
+			}
+			sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+			So(sc.resp.Code, ShouldEqual, 200)
+			result := sc.ToJSON()
+			So(result.MustArray(), ShouldHaveLength, 2)
+
+			resultOne := result.GetIndex(0)
+			So(resultOne.Get("id").MustInt64(), ShouldEqual, tokens[0].Id)
+			So(resultOne.Get("isActive").MustBool(), ShouldBeTrue)
+			So(resultOne.Get("clientIp").MustString(), ShouldEqual, "127.0.0.1")
+			So(resultOne.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36")
+			So(resultOne.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[0].CreatedAt, 0).Format(time.RFC3339))
+			So(resultOne.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[0].SeenAt, 0).Format(time.RFC3339))
+
+			resultTwo := result.GetIndex(1)
+			So(resultTwo.Get("id").MustInt64(), ShouldEqual, tokens[1].Id)
+			So(resultTwo.Get("isActive").MustBool(), ShouldBeFalse)
+			So(resultTwo.Get("clientIp").MustString(), ShouldEqual, "127.0.0.2")
+			So(resultTwo.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1")
+			So(resultTwo.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339))
+			So(resultTwo.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[1].SeenAt, 0).Format(time.RFC3339))
+		})
+	})
+}
+
+func revokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd m.RevokeAuthTokenCmd, userId int64, fn scenarioFunc) {
+	Convey(desc+" "+url, func() {
+		defer bus.ClearBusHandlers()
+
+		fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+		hs := HTTPServer{
+			Bus:              bus.GetBus(),
+			AuthTokenService: fakeAuthTokenService,
+		}
+
+		sc := setupScenarioContext(url)
+		sc.userAuthTokenService = fakeAuthTokenService
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = userId
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = m.ROLE_ADMIN
+
+			return hs.RevokeUserAuthToken(c, cmd)
+		})
+
+		sc.m.Post(routePattern, sc.defaultHandler)
+
+		fn(sc)
+	})
+}
+
+func getUserAuthTokensScenario(desc string, url string, routePattern string, userId int64, fn scenarioFunc) {
+	Convey(desc+" "+url, func() {
+		defer bus.ClearBusHandlers()
+
+		fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+		hs := HTTPServer{
+			Bus:              bus.GetBus(),
+			AuthTokenService: fakeAuthTokenService,
+		}
+
+		sc := setupScenarioContext(url)
+		sc.userAuthTokenService = fakeAuthTokenService
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = userId
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = m.ROLE_ADMIN
+
+			return hs.GetUserAuthTokens(c)
+		})
+
+		sc.m.Get(routePattern, sc.defaultHandler)
+
+		fn(sc)
+	})
+}
+
+func logoutUserFromAllDevicesInternalScenario(desc string, userId int64, fn scenarioFunc) {
+	Convey(desc, func() {
+		defer bus.ClearBusHandlers()
+
+		hs := HTTPServer{
+			Bus:              bus.GetBus(),
+			AuthTokenService: auth.NewFakeUserAuthTokenService(),
+		}
+
+		sc := setupScenarioContext("/")
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = TestUserID
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = m.ROLE_ADMIN
+
+			return hs.logoutUserFromAllDevicesInternal(userId)
+		})
+
+		sc.m.Post("/", sc.defaultHandler)
+
+		fn(sc)
+	})
+}
+
+func revokeUserAuthTokenInternalScenario(desc string, cmd m.RevokeAuthTokenCmd, userId int64, token *m.UserToken, fn scenarioFunc) {
+	Convey(desc, func() {
+		defer bus.ClearBusHandlers()
+
+		fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+		hs := HTTPServer{
+			Bus:              bus.GetBus(),
+			AuthTokenService: fakeAuthTokenService,
+		}
+
+		sc := setupScenarioContext("/")
+		sc.userAuthTokenService = fakeAuthTokenService
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = TestUserID
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = m.ROLE_ADMIN
+			sc.context.UserToken = token
+
+			return hs.revokeUserAuthTokenInternal(c, userId, cmd)
+		})
+
+		sc.m.Post("/", sc.defaultHandler)
+
+		fn(sc)
+	})
+}
+
+func getUserAuthTokensInternalScenario(desc string, token *m.UserToken, fn scenarioFunc) {
+	Convey(desc, func() {
+		defer bus.ClearBusHandlers()
+
+		fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
+
+		hs := HTTPServer{
+			Bus:              bus.GetBus(),
+			AuthTokenService: fakeAuthTokenService,
+		}
+
+		sc := setupScenarioContext("/")
+		sc.userAuthTokenService = fakeAuthTokenService
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = TestUserID
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = m.ROLE_ADMIN
+			sc.context.UserToken = token
+
+			return hs.getUserAuthTokensInternal(c, TestUserID)
+		})
+
+		sc.m.Get("/", sc.defaultHandler)
+
+		fn(sc)
+	})
+}

+ 7 - 60
pkg/middleware/middleware_test.go

@@ -11,6 +11,7 @@ import (
 	msession "github.com/go-macaron/session"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/auth"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
@@ -155,7 +156,7 @@ func TestMiddlewareContext(t *testing.T) {
 				return nil
 			})
 
-			sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+			sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
 				return &m.UserToken{
 					UserId:        12,
 					UnhashedToken: unhashedToken,
@@ -184,14 +185,14 @@ func TestMiddlewareContext(t *testing.T) {
 				return nil
 			})
 
-			sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+			sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
 				return &m.UserToken{
 					UserId:        12,
 					UnhashedToken: "",
 				}, nil
 			}
 
-			sc.userAuthTokenService.tryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) {
+			sc.userAuthTokenService.TryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) {
 				userToken.UnhashedToken = "rotated"
 				return true, nil
 			}
@@ -226,7 +227,7 @@ func TestMiddlewareContext(t *testing.T) {
 		middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) {
 			sc.withTokenSessionCookie("token")
 
-			sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+			sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
 				return nil, m.ErrUserTokenNotFound
 			}
 
@@ -562,7 +563,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 		}))
 
 		session.Init(&msession.Options{}, 0)
-		sc.userAuthTokenService = newFakeUserAuthTokenService()
+		sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
 		sc.m.Use(GetContextHandler(sc.userAuthTokenService))
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
@@ -595,7 +596,7 @@ type scenarioContext struct {
 	handlerFunc          handlerFunc
 	defaultHandler       macaron.Handler
 	url                  string
-	userAuthTokenService *fakeUserAuthTokenService
+	userAuthTokenService *auth.FakeUserAuthTokenService
 
 	req *http.Request
 }
@@ -676,57 +677,3 @@ func (sc *scenarioContext) exec() {
 
 type scenarioFunc func(c *scenarioContext)
 type handlerFunc func(c *m.ReqContext)
-
-type fakeUserAuthTokenService struct {
-	createTokenProvider    func(userId int64, clientIP, userAgent string) (*m.UserToken, error)
-	tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error)
-	lookupTokenProvider    func(unhashedToken string) (*m.UserToken, error)
-	revokeTokenProvider    func(token *m.UserToken) error
-	activeAuthTokenCount   func() (int64, error)
-}
-
-func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
-	return &fakeUserAuthTokenService{
-		createTokenProvider: func(userId int64, clientIP, userAgent string) (*m.UserToken, error) {
-			return &m.UserToken{
-				UserId:        0,
-				UnhashedToken: "",
-			}, nil
-		},
-		tryRotateTokenProvider: func(token *m.UserToken, clientIP, userAgent string) (bool, error) {
-			return false, nil
-		},
-		lookupTokenProvider: func(unhashedToken string) (*m.UserToken, error) {
-			return &m.UserToken{
-				UserId:        0,
-				UnhashedToken: "",
-			}, nil
-		},
-		revokeTokenProvider: func(token *m.UserToken) error {
-			return nil
-		},
-		activeAuthTokenCount: func() (int64, error) {
-			return 10, nil
-		},
-	}
-}
-
-func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*m.UserToken, error) {
-	return s.createTokenProvider(userId, clientIP, userAgent)
-}
-
-func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (*m.UserToken, error) {
-	return s.lookupTokenProvider(unhashedToken)
-}
-
-func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP, userAgent string) (bool, error) {
-	return s.tryRotateTokenProvider(token, clientIP, userAgent)
-}
-
-func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error {
-	return s.revokeTokenProvider(token)
-}
-
-func (s *fakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
-	return s.activeAuthTokenCount()
-}

+ 2 - 2
pkg/middleware/org_redirect_test.go

@@ -24,7 +24,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
 				return nil
 			})
 
-			sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+			sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
 				return &m.UserToken{
 					UserId:        0,
 					UnhashedToken: "",
@@ -50,7 +50,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
 				return nil
 			})
 
-			sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+			sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
 				return &m.UserToken{
 					UserId:        12,
 					UnhashedToken: "",

+ 3 - 2
pkg/middleware/quota_test.go

@@ -3,6 +3,7 @@ package middleware
 import (
 	"testing"
 
+	"github.com/grafana/grafana/pkg/services/auth"
 	"github.com/grafana/grafana/pkg/services/quota"
 
 	"github.com/grafana/grafana/pkg/bus"
@@ -36,7 +37,7 @@ func TestMiddlewareQuota(t *testing.T) {
 			},
 		}
 
-		fakeAuthTokenService := newFakeUserAuthTokenService()
+		fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
 		qs := &quota.QuotaService{
 			AuthTokenService: fakeAuthTokenService,
 		}
@@ -87,7 +88,7 @@ func TestMiddlewareQuota(t *testing.T) {
 				return nil
 			})
 
-			sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
+			sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
 				return &m.UserToken{
 					UserId:        12,
 					UnhashedToken: "",

+ 2 - 1
pkg/middleware/recovery_test.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/auth"
 	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
 	macaron "gopkg.in/macaron.v1"
@@ -62,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
 			Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 		}))
 
-		sc.userAuthTokenService = newFakeUserAuthTokenService()
+		sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
 		sc.m.Use(GetContextHandler(sc.userAuthTokenService))
 		// mock out gc goroutine
 		sc.m.Use(OrgRedirect())

+ 10 - 1
pkg/models/user_token.go

@@ -1,6 +1,8 @@
 package models
 
-import "errors"
+import (
+	"errors"
+)
 
 // Typed errors
 var (
@@ -23,11 +25,18 @@ type UserToken struct {
 	UnhashedToken string
 }
 
+type RevokeAuthTokenCmd struct {
+	AuthTokenId int64 `json:"authTokenId"`
+}
+
 // UserTokenService are used for generating and validating user tokens
 type UserTokenService interface {
 	CreateToken(userId int64, clientIP, userAgent string) (*UserToken, error)
 	LookupToken(unhashedToken string) (*UserToken, error)
 	TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
 	RevokeToken(token *UserToken) error
+	RevokeAllUserTokens(userId int64) error
 	ActiveTokenCount() (int64, error)
+	GetUserToken(userId, userTokenId int64) (*UserToken, error)
+	GetUserTokens(userId int64) ([]*UserToken, error)
 }

+ 51 - 0
pkg/services/auth/auth_token.go

@@ -221,6 +221,57 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
 	return nil
 }
 
+func (s *UserAuthTokenService) RevokeAllUserTokens(userId int64) error {
+	sql := `DELETE from user_auth_token WHERE user_id = ?`
+	res, err := s.SQLStore.NewSession().Exec(sql, userId)
+	if err != nil {
+		return err
+	}
+
+	affected, err := res.RowsAffected()
+	if err != nil {
+		return err
+	}
+
+	s.log.Debug("all user tokens for user revoked", "userId", userId, "count", affected)
+
+	return nil
+}
+
+func (s *UserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) {
+	var token userAuthToken
+	exists, err := s.SQLStore.NewSession().Where("id = ? AND user_id = ?", userTokenId, userId).Get(&token)
+	if err != nil {
+		return nil, err
+	}
+
+	if !exists {
+		return nil, models.ErrUserTokenNotFound
+	}
+
+	var result models.UserToken
+	token.toUserToken(&result)
+
+	return &result, nil
+}
+
+func (s *UserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) {
+	var tokens []*userAuthToken
+	err := s.SQLStore.NewSession().Where("user_id = ? AND created_at > ? AND rotated_at > ?", userId, s.createdAfterParam(), s.rotatedAfterParam()).Find(&tokens)
+	if err != nil {
+		return nil, err
+	}
+
+	result := []*models.UserToken{}
+	for _, token := range tokens {
+		var userToken models.UserToken
+		token.toUserToken(&userToken)
+		result = append(result, &userToken)
+	}
+
+	return result, nil
+}
+
 func (s *UserAuthTokenService) createdAfterParam() int64 {
 	tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
 	return getTime().Add(-tokenMaxLifetime).Unix()

+ 41 - 0
pkg/services/auth/auth_token_test.go

@@ -75,6 +75,47 @@ func TestUserAuthToken(t *testing.T) {
 				err = userAuthTokenService.RevokeToken(userToken)
 				So(err, ShouldEqual, models.ErrUserTokenNotFound)
 			})
+
+			Convey("When creating an additional token", func() {
+				userToken2, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+				So(err, ShouldBeNil)
+				So(userToken2, ShouldNotBeNil)
+
+				Convey("Can get first user token", func() {
+					token, err := userAuthTokenService.GetUserToken(userID, userToken.Id)
+					So(err, ShouldBeNil)
+					So(token, ShouldNotBeNil)
+					So(token.Id, ShouldEqual, userToken.Id)
+				})
+
+				Convey("Can get second user token", func() {
+					token, err := userAuthTokenService.GetUserToken(userID, userToken2.Id)
+					So(err, ShouldBeNil)
+					So(token, ShouldNotBeNil)
+					So(token.Id, ShouldEqual, userToken2.Id)
+				})
+
+				Convey("Can get user tokens", func() {
+					tokens, err := userAuthTokenService.GetUserTokens(userID)
+					So(err, ShouldBeNil)
+					So(tokens, ShouldHaveLength, 2)
+					So(tokens[0].Id, ShouldEqual, userToken.Id)
+					So(tokens[1].Id, ShouldEqual, userToken2.Id)
+				})
+
+				Convey("Can revoke all user tokens", func() {
+					err := userAuthTokenService.RevokeAllUserTokens(userID)
+					So(err, ShouldBeNil)
+
+					model, err := ctx.getAuthTokenByID(userToken.Id)
+					So(err, ShouldBeNil)
+					So(model, ShouldBeNil)
+
+					model2, err := ctx.getAuthTokenByID(userToken2.Id)
+					So(err, ShouldBeNil)
+					So(model2, ShouldBeNil)
+				})
+			})
 		})
 
 		Convey("expires correctly", func() {

+ 81 - 0
pkg/services/auth/testing.go

@@ -0,0 +1,81 @@
+package auth
+
+import "github.com/grafana/grafana/pkg/models"
+
+type FakeUserAuthTokenService struct {
+	CreateTokenProvider         func(userId int64, clientIP, userAgent string) (*models.UserToken, error)
+	TryRotateTokenProvider      func(token *models.UserToken, clientIP, userAgent string) (bool, error)
+	LookupTokenProvider         func(unhashedToken string) (*models.UserToken, error)
+	RevokeTokenProvider         func(token *models.UserToken) error
+	RevokeAllUserTokensProvider func(userId int64) error
+	ActiveAuthTokenCount        func() (int64, error)
+	GetUserTokenProvider        func(userId, userTokenId int64) (*models.UserToken, error)
+	GetUserTokensProvider       func(userId int64) ([]*models.UserToken, error)
+}
+
+func NewFakeUserAuthTokenService() *FakeUserAuthTokenService {
+	return &FakeUserAuthTokenService{
+		CreateTokenProvider: func(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
+			return &models.UserToken{
+				UserId:        0,
+				UnhashedToken: "",
+			}, nil
+		},
+		TryRotateTokenProvider: func(token *models.UserToken, clientIP, userAgent string) (bool, error) {
+			return false, nil
+		},
+		LookupTokenProvider: func(unhashedToken string) (*models.UserToken, error) {
+			return &models.UserToken{
+				UserId:        0,
+				UnhashedToken: "",
+			}, nil
+		},
+		RevokeTokenProvider: func(token *models.UserToken) error {
+			return nil
+		},
+		RevokeAllUserTokensProvider: func(userId int64) error {
+			return nil
+		},
+		ActiveAuthTokenCount: func() (int64, error) {
+			return 10, nil
+		},
+		GetUserTokenProvider: func(userId, userTokenId int64) (*models.UserToken, error) {
+			return nil, nil
+		},
+		GetUserTokensProvider: func(userId int64) ([]*models.UserToken, error) {
+			return nil, nil
+		},
+	}
+}
+
+func (s *FakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
+	return s.CreateTokenProvider(userId, clientIP, userAgent)
+}
+
+func (s *FakeUserAuthTokenService) LookupToken(unhashedToken string) (*models.UserToken, error) {
+	return s.LookupTokenProvider(unhashedToken)
+}
+
+func (s *FakeUserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, userAgent string) (bool, error) {
+	return s.TryRotateTokenProvider(token, clientIP, userAgent)
+}
+
+func (s *FakeUserAuthTokenService) RevokeToken(token *models.UserToken) error {
+	return s.RevokeTokenProvider(token)
+}
+
+func (s *FakeUserAuthTokenService) RevokeAllUserTokens(userId int64) error {
+	return s.RevokeAllUserTokensProvider(userId)
+}
+
+func (s *FakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
+	return s.ActiveAuthTokenCount()
+}
+
+func (s *FakeUserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) {
+	return s.GetUserTokenProvider(userId, userTokenId)
+}
+
+func (s *FakeUserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) {
+	return s.GetUserTokensProvider(userId)
+}