mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
begin user auth token implementation
This commit is contained in:
parent
e08f61059b
commit
b0df7280be
170
pkg/services/auth/auth_token.go
Normal file
170
pkg/services/auth/auth_token.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
macaron "gopkg.in/macaron.v1"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterService(&UserAuthTokenService{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = time.Now
|
||||||
|
|
||||||
|
// UserAuthTokenService are used for generating and validating user auth tokens
|
||||||
|
type UserAuthTokenService struct {
|
||||||
|
SQLStore *sqlstore.SqlStore `inject:""`
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init this service
|
||||||
|
func (s *UserAuthTokenService) Init() error {
|
||||||
|
s.log = log.New("auth")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionCookieKey = "grafana_session"
|
||||||
|
|
||||||
|
func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error {
|
||||||
|
userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Resp.Header().Del("Set-Cookie")
|
||||||
|
c.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) {
|
||||||
|
c.SetCookie(sessionCookieKey, "", -1, setting.AppSubUrl+"/", setting.Domain, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserAuthTokenService) RequestMiddleware() macaron.Handler {
|
||||||
|
return func(ctx *models.ReqContext) {
|
||||||
|
authToken := ctx.GetCookie(sessionCookieKey)
|
||||||
|
userToken, err := s.lookupToken(authToken)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Next()
|
||||||
|
|
||||||
|
refreshed, err := s.refreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent())
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if refreshed {
|
||||||
|
ctx.Resp.Header().Del("Set-Cookie")
|
||||||
|
ctx.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {
|
||||||
|
clientIP = util.ParseIPAddress(clientIP)
|
||||||
|
token, err := util.RandomHex(16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedToken := hashToken(token)
|
||||||
|
|
||||||
|
userToken := userAuthToken{
|
||||||
|
UserId: userId,
|
||||||
|
AuthToken: hashedToken,
|
||||||
|
PrevAuthToken: hashedToken,
|
||||||
|
ClientIp: clientIP,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
RotatedAt: now().Unix(),
|
||||||
|
CreatedAt: now().Unix(),
|
||||||
|
UpdatedAt: now().Unix(),
|
||||||
|
SeenAt: 0,
|
||||||
|
AuthTokenSeen: false,
|
||||||
|
}
|
||||||
|
_, err = s.SQLStore.NewSession().Insert(&userToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userToken.unhashedToken = token
|
||||||
|
|
||||||
|
return &userToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserAuthTokenService) lookupToken(unhashedToken string) (*userAuthToken, error) {
|
||||||
|
hashedToken := hashToken(unhashedToken)
|
||||||
|
|
||||||
|
var userToken userAuthToken
|
||||||
|
exists, err := s.SQLStore.NewSession().Where("auth_token = ? OR prev_auth_token = ?", hashedToken, hashedToken).Get(&userToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrAuthTokenNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen {
|
||||||
|
userToken.AuthTokenSeen = false
|
||||||
|
expireBefore := now().Add(-1 * time.Minute).Unix()
|
||||||
|
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userToken.Id, userToken.PrevAuthToken, expireBefore).AllCols().Update(&userToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if affectedRows == 0 {
|
||||||
|
s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
|
||||||
|
} else {
|
||||||
|
s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken {
|
||||||
|
userTokenCopy := userToken
|
||||||
|
userTokenCopy.AuthTokenSeen = true
|
||||||
|
userTokenCopy.SeenAt = now().Unix()
|
||||||
|
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if affectedRows == 1 {
|
||||||
|
userToken = userTokenCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
if affectedRows == 0 {
|
||||||
|
s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
|
||||||
|
} else {
|
||||||
|
s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userToken.unhashedToken = unhashedToken
|
||||||
|
|
||||||
|
return &userToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserAuthTokenService) refreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) {
|
||||||
|
// lookup token in db
|
||||||
|
// refresh token if needed
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashToken(token string) string {
|
||||||
|
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
|
||||||
|
return hex.EncodeToString(hashBytes[:])
|
||||||
|
}
|
206
pkg/services/auth/auth_token_test.go
Normal file
206
pkg/services/auth/auth_token_test.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserAuthToken(t *testing.T) {
|
||||||
|
Convey("Test user auth token", t, func() {
|
||||||
|
ctx := createTestContext(t)
|
||||||
|
userAuthTokenService := ctx.tokenService
|
||||||
|
userID := int64(10)
|
||||||
|
|
||||||
|
t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
|
||||||
|
now = func() time.Time {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("When creating token", func() {
|
||||||
|
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(token, ShouldNotBeNil)
|
||||||
|
So(token.AuthTokenSeen, ShouldBeFalse)
|
||||||
|
|
||||||
|
Convey("When lookup unhashed token should return user auth token", func() {
|
||||||
|
lookupToken, err := userAuthTokenService.lookupToken(token.unhashedToken)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(lookupToken, ShouldNotBeNil)
|
||||||
|
So(lookupToken.UserId, ShouldEqual, userID)
|
||||||
|
So(lookupToken.AuthTokenSeen, ShouldBeTrue)
|
||||||
|
|
||||||
|
storedAuthToken, err := ctx.getAuthTokenByID(lookupToken.Id)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(storedAuthToken, ShouldNotBeNil)
|
||||||
|
So(storedAuthToken.AuthTokenSeen, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When lookup hashed token should return user auth token not found error", func() {
|
||||||
|
lookupToken, err := userAuthTokenService.lookupToken(token.AuthToken)
|
||||||
|
So(err, ShouldEqual, ErrAuthTokenNotFound)
|
||||||
|
So(lookupToken, ShouldBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("expires correctly", func() {
|
||||||
|
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(token, ShouldNotBeNil)
|
||||||
|
|
||||||
|
_, err = userAuthTokenService.lookupToken(token.unhashedToken)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
token, err = ctx.getAuthTokenByID(token.Id)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// set now (now - 23 hours)
|
||||||
|
_, err = userAuthTokenService.refreshToken(token, "192.168.10.11:1234", "some user agent")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
_, err = userAuthTokenService.lookupToken(token.unhashedToken)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
stillGood, err := userAuthTokenService.lookupToken(token.unhashedToken)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(stillGood, ShouldNotBeNil)
|
||||||
|
|
||||||
|
// set now (new - 2 hours)
|
||||||
|
notGood, err := userAuthTokenService.lookupToken(token.unhashedToken)
|
||||||
|
So(err, ShouldEqual, ErrAuthTokenNotFound)
|
||||||
|
So(notGood, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("can properly rotate tokens", func() {
|
||||||
|
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(token, ShouldNotBeNil)
|
||||||
|
|
||||||
|
prevToken := token.AuthToken
|
||||||
|
unhashedPrev := token.unhashedToken
|
||||||
|
|
||||||
|
refreshed, err := userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(refreshed, ShouldBeFalse)
|
||||||
|
|
||||||
|
ctx.markAuthTokenAsSeen(token.Id)
|
||||||
|
token, err = ctx.getAuthTokenByID(token.Id)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// ability to auth using an old token
|
||||||
|
now = func() time.Time {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshed, err = userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(refreshed, ShouldBeTrue)
|
||||||
|
|
||||||
|
unhashedToken := token.unhashedToken
|
||||||
|
|
||||||
|
token, err = ctx.getAuthTokenByID(token.Id)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
token.unhashedToken = unhashedToken
|
||||||
|
|
||||||
|
So(token.RotatedAt, ShouldEqual, t.Unix())
|
||||||
|
So(token.ClientIp, ShouldEqual, "192.168.10.12")
|
||||||
|
So(token.UserAgent, ShouldEqual, "a new user agent")
|
||||||
|
So(token.AuthTokenSeen, ShouldBeFalse)
|
||||||
|
So(token.SeenAt, ShouldEqual, 0)
|
||||||
|
So(token.PrevAuthToken, ShouldEqual, prevToken)
|
||||||
|
|
||||||
|
lookedUp, err := userAuthTokenService.lookupToken(token.unhashedToken)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(lookedUp, ShouldNotBeNil)
|
||||||
|
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
|
||||||
|
So(lookedUp.SeenAt, ShouldEqual, t.Unix())
|
||||||
|
|
||||||
|
lookedUp, err = userAuthTokenService.lookupToken(unhashedPrev)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(lookedUp, ShouldNotBeNil)
|
||||||
|
So(lookedUp.Id, ShouldEqual, token.Id)
|
||||||
|
|
||||||
|
now = func() time.Time {
|
||||||
|
return t.Add(2 * time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
lookedUp, err = userAuthTokenService.lookupToken(unhashedPrev)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(lookedUp, ShouldNotBeNil)
|
||||||
|
|
||||||
|
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(lookedUp, ShouldNotBeNil)
|
||||||
|
So(lookedUp.AuthTokenSeen, ShouldBeFalse)
|
||||||
|
|
||||||
|
refreshed, err = userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(refreshed, ShouldBeTrue)
|
||||||
|
|
||||||
|
token, err = ctx.getAuthTokenByID(token.Id)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(token, ShouldNotBeNil)
|
||||||
|
So(token.SeenAt, ShouldEqual, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("keeps prev token valid for 1 minute after it is confirmed", func() {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("will not mark token unseen when prev and current are the same", func() {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
Reset(func() {
|
||||||
|
now = time.Now
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestContext(t *testing.T) *testContext {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
sqlstore := sqlstore.InitTestDB(t)
|
||||||
|
tokenService := &UserAuthTokenService{
|
||||||
|
SQLStore: sqlstore,
|
||||||
|
log: log.New("test-logger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &testContext{
|
||||||
|
sqlstore: sqlstore,
|
||||||
|
tokenService: tokenService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testContext struct {
|
||||||
|
sqlstore *sqlstore.SqlStore
|
||||||
|
tokenService *UserAuthTokenService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
|
||||||
|
sess := c.sqlstore.NewSession()
|
||||||
|
var t userAuthToken
|
||||||
|
found, err := sess.ID(id).Get(&t)
|
||||||
|
if err != nil || !found {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
|
||||||
|
sess := c.sqlstore.NewSession()
|
||||||
|
res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return rowsAffected == 1, nil
|
||||||
|
}
|
25
pkg/services/auth/model.go
Normal file
25
pkg/services/auth/model.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Typed errors
|
||||||
|
var (
|
||||||
|
ErrAuthTokenNotFound = errors.New("User auth token not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type userAuthToken struct {
|
||||||
|
Id int64
|
||||||
|
UserId int64
|
||||||
|
AuthToken string
|
||||||
|
PrevAuthToken string
|
||||||
|
UserAgent string
|
||||||
|
ClientIp string
|
||||||
|
AuthTokenSeen bool
|
||||||
|
SeenAt int64
|
||||||
|
RotatedAt int64
|
||||||
|
CreatedAt int64
|
||||||
|
UpdatedAt int64
|
||||||
|
unhashedToken string `xorm:"-"`
|
||||||
|
}
|
@ -32,6 +32,7 @@ func AddMigrations(mg *Migrator) {
|
|||||||
addLoginAttemptMigrations(mg)
|
addLoginAttemptMigrations(mg)
|
||||||
addUserAuthMigrations(mg)
|
addUserAuthMigrations(mg)
|
||||||
addServerlockMigrations(mg)
|
addServerlockMigrations(mg)
|
||||||
|
addUserAuthTokenMigrations(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMigrationLogMigrations(mg *Migrator) {
|
func addMigrationLogMigrations(mg *Migrator) {
|
||||||
|
32
pkg/services/sqlstore/migrations/user_auth_token_mig.go
Normal file
32
pkg/services/sqlstore/migrations/user_auth_token_mig.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addUserAuthTokenMigrations(mg *Migrator) {
|
||||||
|
userAuthTokenV1 := Table{
|
||||||
|
Name: "user_auth_token",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||||
|
{Name: "user_id", Type: DB_BigInt, Nullable: false},
|
||||||
|
{Name: "auth_token", Type: DB_NVarchar, Length: 100, Nullable: false},
|
||||||
|
{Name: "prev_auth_token", Type: DB_NVarchar, Length: 100, Nullable: false},
|
||||||
|
{Name: "user_agent", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||||
|
{Name: "client_ip", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||||
|
{Name: "auth_token_seen", Type: DB_Bool, Nullable: false},
|
||||||
|
{Name: "seen_at", Type: DB_Int, Nullable: true},
|
||||||
|
{Name: "rotated_at", Type: DB_Int, Nullable: false},
|
||||||
|
{Name: "created_at", Type: DB_Int, Nullable: false},
|
||||||
|
{Name: "updated_at", Type: DB_Int, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*Index{
|
||||||
|
{Cols: []string{"auth_token"}, Type: UniqueIndex},
|
||||||
|
{Cols: []string{"prev_auth_token"}, Type: UniqueIndex},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create user auth token table", NewAddTableMigration(userAuthTokenV1))
|
||||||
|
mg.AddMigration("add unique index user_auth_token.auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[0]))
|
||||||
|
mg.AddMigration("add unique index user_auth_token.prev_auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[1]))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user