Implement user authorization (login)

This commit is contained in:
ChronosX88 2021-04-06 16:55:41 +03:00
parent 123610d33b
commit 0f6d2ff3d9
Signed by: ChronosXYZ
GPG Key ID: 085A69A82C8C511A
17 changed files with 337 additions and 105 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/zr /zr
/.config.toml

View File

@ -16,8 +16,10 @@ func main() {
var cfg core.Config var cfg core.Config
var configPath string var configPath string
var generateConfig bool var generateConfig bool
defer logger.Init("auth-server", true, false, ioutil.Discard).Close() // TODO Make ability to use file for log output
flag.StringVar(&configPath, "config", "", "Path to config") flag.StringVar(&configPath, "config", "", "Path to config")
flag.BoolVar(&generateConfig, "gen_config", false, "Generate the config") flag.BoolVar(&generateConfig, "gen-config", false, "Generate the config")
flag.Parse() flag.Parse()
if generateConfig == true { if generateConfig == true {
sampleConfig := &core.Config{} sampleConfig := &core.Config{}

View File

@ -10,13 +10,13 @@ import (
) )
type AppContext struct { type AppContext struct {
router *Router router *Router
authManager *AuthManager authManager *AuthManager
connectionHandler *ConnectionHandler sessionManager *SessionManager
websocketServer *WebsocketServer websocketServer *WebsocketServer
cfg *Config cfg *Config
database *mongo.Database database *mongo.Database
userManager *UserManager userManager *UserManager
} }
func NewAppContext(cfg *Config) *AppContext { func NewAppContext(cfg *Config) *AppContext {
@ -32,19 +32,22 @@ func NewAppContext(cfg *Config) *AppContext {
} }
appContext.router = router appContext.router = router
authManager, err := NewAuthManager() um, err := NewUserManager(appContext.database)
if err != nil {
logger.Fatalf("Unable to initialize user manager: %s", err.Error())
}
appContext.userManager = um
authManager, err := NewAuthManager(um, cfg.ServerID, router)
if err != nil { if err != nil {
logger.Fatalf("Unable to initialize authentication manager: %s", err.Error()) logger.Fatalf("Unable to initialize authentication manager: %s", err.Error())
} }
appContext.authManager = authManager appContext.authManager = authManager
um := NewUserManager(appContext.database) sessionManager := NewSessionManager(router)
appContext.userManager = um appContext.sessionManager = sessionManager
connHandler := NewConnectionHandler(router) wss := NewWebsocketServer(cfg, sessionManager)
appContext.connectionHandler = connHandler
wss := NewWebsocketServer(cfg, connHandler)
appContext.websocketServer = wss appContext.websocketServer = wss
return appContext return appContext
@ -72,6 +75,5 @@ func (ac *AppContext) connectToDatabase() {
} }
func (ac *AppContext) Run() error { func (ac *AppContext) Run() error {
// TODO return ac.websocketServer.Run()
return nil
} }

81
core/auth_handler.go Normal file
View File

@ -0,0 +1,81 @@
package core
import (
"github.com/cadmium-im/zirconium-go/core/models"
"github.com/cadmium-im/zirconium-go/core/models/auth"
"github.com/cadmium-im/zirconium-go/core/utils"
"github.com/fatih/structs"
"github.com/google/logger"
"github.com/mitchellh/mapstructure"
"go.mongodb.org/mongo-driver/mongo"
)
type AuthHandler struct {
serverID string
authManager *AuthManager
}
func NewAuthHandler(am *AuthManager, serverID string) *AuthHandler {
return &AuthHandler{
authManager: am,
serverID: serverID,
}
}
func (ah *AuthHandler) HandleMessage(s *Session, message models.BaseMessage) {
var authRequest auth.AuthRequest
err := mapstructure.Decode(message.Payload, &authRequest)
if err != nil {
logger.Errorf(err.Error())
return
}
switch authRequest.Type {
case "urn:cadmium:auth:simple":
{
token, claims, err := ah.authManager.HandleSimpleAuth(authRequest.Fields["username"].(string), authRequest.Fields["password"].(string))
if err != nil {
if mongo.ErrNoDocuments == err {
msg := utils.PrepareErrorMessage(message, "auth-failed", "invalid username", ah.serverID)
_ = s.Send(msg)
return
}
logger.Errorf(err.Error())
msg := utils.PrepareMessageInternalServerError(message, err, ah.serverID)
_ = s.Send(msg)
return
}
ar := auth.AuthResponse{
Token: token,
DeviceID: claims.DeviceID,
}
payload := structs.Map(ar)
msg := models.NewBaseMessage(message.ID, message.MessageType, ah.serverID, nil, true, payload)
_ = s.Send(msg)
s.Claims = claims
return
}
case "urn:cadmium:auth:token":
{
claims, err := ah.authManager.ValidateToken(authRequest.Fields["token"].(string))
if err != nil {
logger.Errorf(err.Error())
msg := utils.PrepareMessageInternalServerError(message, err, ah.serverID)
_ = s.Send(msg)
return
}
s.Claims = claims
msg := models.NewBaseMessage(message.ID, message.MessageType, ah.serverID, nil, true, nil)
_ = s.Send(msg)
return
}
}
}
func (ah *AuthHandler) IsAuthorizationRequired() bool {
return false
}
func (ah *AuthHandler) HandlingType() string {
return "urn:cadmium:auth"
}

View File

@ -3,6 +3,8 @@ package core
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/alexedwards/argon2id"
"github.com/cadmium-im/zirconium-go/core/utils"
"time" "time"
"github.com/cadmium-im/zirconium-go/core/models" "github.com/cadmium-im/zirconium-go/core/models"
@ -16,7 +18,9 @@ const (
) )
type AuthManager struct { type AuthManager struct {
signingKey string // For now it is random bytes string represented in Base64 serverID string
userManager *UserManager
signingKey string // For now it is random bytes string represented in Base64
} }
type JWTCustomClaims struct { type JWTCustomClaims struct {
@ -25,17 +29,22 @@ type JWTCustomClaims struct {
jwt.StandardClaims jwt.StandardClaims
} }
func NewAuthManager() (*AuthManager, error) { func NewAuthManager(um *UserManager, serverID string, r *Router) (*AuthManager, error) {
am := &AuthManager{} am := &AuthManager{
bytes, err := GenRandomBytes(SigningKeyBytesAmount) userManager: um,
serverID: serverID,
}
bytes, err := utils.GenRandomBytes(SigningKeyBytesAmount)
if err != nil { if err != nil {
return nil, err return nil, err
} }
am.signingKey = base64.RawStdEncoding.EncodeToString(bytes) am.signingKey = base64.RawStdEncoding.EncodeToString(bytes)
ah := NewAuthHandler(am, serverID)
r.RegisterC2SHandler(ah)
return am, nil return am, nil
} }
func (am *AuthManager) CreateNewToken(entityID *models.EntityID, deviceID string, tokenExpireTimeDuration time.Duration) (string, error) { func (am *AuthManager) CreateNewToken(entityID *models.EntityID, deviceID string, tokenExpireTimeDuration time.Duration) (*JWTCustomClaims, string, error) {
timeNow := time.Now() timeNow := time.Now()
expiringTime := timeNow.Add(tokenExpireTimeDuration) expiringTime := timeNow.Add(tokenExpireTimeDuration)
@ -59,9 +68,9 @@ func (am *AuthManager) CreateNewToken(entityID *models.EntityID, deviceID string
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(am.signingKey) tokenString, err := token.SignedString(am.signingKey)
if err != nil { if err != nil {
return "", err return nil, "", err
} }
return tokenString, nil return &claims, tokenString, nil
} }
func (am *AuthManager) ValidateToken(tokenString string) (*JWTCustomClaims, error) { func (am *AuthManager) ValidateToken(tokenString string) (*JWTCustomClaims, error) {
@ -84,3 +93,28 @@ func (am *AuthManager) ValidateToken(tokenString string) (*JWTCustomClaims, erro
} }
return nil, err return nil, err
} }
func (am *AuthManager) HandleSimpleAuth(username string, pass string) (string, *JWTCustomClaims, error) {
user, err := am.userManager.GetByUsername(username)
if err != nil {
return "", nil, err
}
match, err := argon2id.ComparePasswordAndHash(pass, user.PasswordHash)
if err != nil {
return "", nil, err
}
if !match {
return "", nil, fmt.Errorf("incorrect password")
}
eid, err := models.NewEntityID("@", user.UUID, am.serverID)
if err != nil {
return "", nil, err
}
claims, token, err := am.CreateNewToken(eid, "ABCDEF", TokenExpireTimeDuration)
if err != nil {
return "", nil, err
}
return token, claims, nil
}

View File

@ -5,7 +5,7 @@ type BaseMessage struct {
ID string `json:"id"` ID string `json:"id"`
MessageType string `json:"type"` MessageType string `json:"type"`
From string `json:"from"` From string `json:"from"`
To []string `json:"to"` To []string `json:"to,omitempty"`
Ok bool `json:"ok"` Ok bool `json:"ok"`
Payload map[string]interface{} `json:"payload"` Payload map[string]interface{} `json:"payload"`
} }

View File

@ -1,7 +1,7 @@
package models package models
type ProtocolError struct { type ProtocolError struct {
ErrCode string `json:"code"` ErrCode string `structs:"code"`
ErrText string `json:"text"` ErrText string `structs:"text"`
ErrPayload map[string]interface{} `json:"payload"` ErrPayload map[string]interface{} `structs:"payload,omitempty"`
} }

10
core/models/user.go Normal file
View File

@ -0,0 +1,10 @@
package models
import "go.mongodb.org/mongo-driver/bson/primitive"
type User struct {
ID primitive.ObjectID `json:"-" bson:"_id"`
UUID string `json:"uuid"`
Username string `json:"username"`
PasswordHash string `json:"-" bson:"passwordHash"`
}

View File

@ -2,13 +2,14 @@ package core
import ( import (
"github.com/cadmium-im/zirconium-go/core/models" "github.com/cadmium-im/zirconium-go/core/models"
"github.com/cadmium-im/zirconium-go/core/utils"
"github.com/fatih/structs"
"github.com/google/logger" "github.com/google/logger"
) )
type Router struct { type Router struct {
appContext *AppContext appContext *AppContext
handlers map[string][]C2SMessageHandler handlers map[string][]C2SMessageHandler
connections []*Session
} }
type C2SMessageHandler interface { type C2SMessageHandler interface {
@ -30,10 +31,10 @@ func (r *Router) RouteMessage(origin *Session, message models.BaseMessage) {
if handlers != nil { if handlers != nil {
for _, v := range handlers { for _, v := range handlers {
if v.IsAuthorizationRequired() { if v.IsAuthorizationRequired() {
if len(origin.entityID) == 0 { if origin.Claims == nil {
logger.Warningf("Connection %s isn't authorized", origin.connID) logger.Warningf("Connection %s isn't authorized", origin.connID)
msg := PrepareMessageUnauthorized(message, r.appContext.cfg.ServerDomains[0]) // fixme: domain msg := utils.PrepareMessageUnauthorized(message, r.appContext.cfg.ServerDomains[0]) // fixme: domain
_ = origin.Send(msg) _ = origin.Send(msg)
} }
} }
@ -45,7 +46,7 @@ func (r *Router) RouteMessage(origin *Session, message models.BaseMessage) {
ErrText: "Server doesn't implement message type " + message.MessageType, ErrText: "Server doesn't implement message type " + message.MessageType,
ErrPayload: make(map[string]interface{}), ErrPayload: make(map[string]interface{}),
} }
errMsg := models.NewBaseMessage(message.ID, message.MessageType, r.appContext.cfg.ServerID, []string{message.From}, false, StructToMap(protocolError)) errMsg := models.NewBaseMessage(message.ID, message.MessageType, r.appContext.cfg.ServerID, []string{message.From}, false, structs.Map(protocolError))
logger.Infof("Drop message with type %s because server hasn't proper handlers", message.MessageType) logger.Infof("Drop message with type %s because server hasn't proper handlers", message.MessageType)
_ = origin.Send(errMsg) _ = origin.Send(errMsg)
} }

View File

@ -6,10 +6,9 @@ import (
) )
type Session struct { type Session struct {
wsConn *websocket.Conn wsConn *websocket.Conn
connID string connID string
entityID []*models.EntityID Claims *JWTCustomClaims
deviceID *string
} }
func (s *Session) Send(message models.BaseMessage) error { func (s *Session) Send(message models.BaseMessage) error {

View File

@ -7,24 +7,23 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
type ConnectionHandler struct { type SessionManager struct {
router *Router router *Router
connections map[string]*Session connections map[string]*Session
} }
func NewConnectionHandler(r *Router) *ConnectionHandler { func NewSessionManager(r *Router) *SessionManager {
return &ConnectionHandler{ return &SessionManager{
router: r, router: r,
connections: make(map[string]*Session), connections: make(map[string]*Session),
} }
} }
func (ch *ConnectionHandler) HandleNewConnection(wsocket *websocket.Conn) { func (ch *SessionManager) HandleNewConnection(wsocket *websocket.Conn) {
randomUUID, _ := uuid.NewRandom() randomUUID := uuid.New().String()
uuidStr := randomUUID.String()
o := &Session{ o := &Session{
wsConn: wsocket, wsConn: wsocket,
connID: uuidStr, connID: randomUUID,
} }
ch.connections[o.connID] = o ch.connections[o.connID] = o
go func() { go func() {

View File

@ -1,6 +1,15 @@
package core package core
import "go.mongodb.org/mongo-driver/mongo" import (
"context"
"github.com/cadmium-im/zirconium-go/core/models"
"github.com/cadmium-im/zirconium-go/core/utils"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const ( const (
UsersCollectionName = "users" UsersCollectionName = "users"
@ -10,12 +19,52 @@ type UserManager struct {
usersCol *mongo.Collection usersCol *mongo.Collection
} }
func NewUserManager(db *mongo.Database) *UserManager { func NewUserManager(db *mongo.Database) (*UserManager, error) {
col := db.Collection(UsersCollectionName) col := db.Collection(UsersCollectionName)
um := &UserManager{ um := &UserManager{
usersCol: col, usersCol: col,
} }
return um err := um.initMongo()
if err != nil {
return nil, err
}
return um, nil
}
func (um *UserManager) initMongo() error {
usernameIndex := mongo.IndexModel{
Keys: bson.M{
"username": 1,
}, Options: options.Index().SetUnique(true),
}
exists, err := utils.IsCollectionExists(context.Background(), um.usersCol.Database(), um.usersCol.Name())
if err != nil {
return err
}
if exists {
_, err := um.usersCol.Indexes().DropAll(context.Background())
if err != nil {
return err
}
}
_, err = um.usersCol.Indexes().CreateMany(context.Background(), []mongo.IndexModel{usernameIndex})
return err
}
func (um *UserManager) SaveUser(user *models.User) error {
user.ID = primitive.NewObjectID()
user.UUID = uuid.New().String()
_, err := um.usersCol.InsertOne(context.Background(), user)
return err
}
func (um *UserManager) GetByUsername(username string) (*models.User, error) {
var user models.User
err := um.usersCol.FindOne(context.Background(), bson.D{{"username", username}}).Decode(&user)
return &user, err
} }

View File

@ -1,50 +0,0 @@
package core
import (
"crypto/rand"
"reflect"
"github.com/cadmium-im/zirconium-go/core/models"
)
func GenRandomBytes(size int) (blk []byte, err error) {
blk = make([]byte, size)
_, err = rand.Read(blk)
return
}
func StructToMap(item interface{}) map[string]interface{} {
res := map[string]interface{}{}
if item == nil {
return res
}
v := reflect.TypeOf(item)
reflectValue := reflect.ValueOf(item)
reflectValue = reflect.Indirect(reflectValue)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
for i := 0; i < v.NumField(); i++ {
tag := v.Field(i).Tag.Get("json")
field := reflectValue.Field(i).Interface()
if tag != "" && tag != "-" {
if v.Field(i).Type.Kind() == reflect.Struct {
res[tag] = StructToMap(field)
} else {
res[tag] = field
}
}
}
return res
}
func PrepareMessageUnauthorized(msg models.BaseMessage, serverDomain string) models.BaseMessage {
protocolError := models.ProtocolError{
ErrCode: "unauthorized",
ErrText: "Unauthorized access",
ErrPayload: make(map[string]interface{}),
}
errMsg := models.NewBaseMessage(msg.ID, msg.MessageType, serverDomain, []string{msg.From}, false, StructToMap(protocolError))
return errMsg
}

91
core/utils/utils.go Normal file
View File

@ -0,0 +1,91 @@
package utils
import (
"context"
"crypto/rand"
"fmt"
"github.com/alexedwards/argon2id"
"github.com/cadmium-im/zirconium-go/core/models"
"github.com/fatih/structs"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
var (
ErrPasswordIsEmpty = fmt.Errorf("the password is empty")
ErrPasswordIsTooShort = fmt.Errorf("the password is too short")
)
func GenRandomBytes(size int) (blk []byte, err error) {
blk = make([]byte, size)
_, err = rand.Read(blk)
return
}
func PrepareMessageUnauthorized(msg models.BaseMessage, serverDomain string) models.BaseMessage {
protocolError := models.ProtocolError{
ErrCode: "unauthorized",
ErrText: "Unauthorized access",
ErrPayload: make(map[string]interface{}),
}
errMsg := models.NewBaseMessage(msg.ID, msg.MessageType, serverDomain, []string{msg.From}, false, structs.Map(protocolError))
return errMsg
}
func PrepareMessageInternalServerError(msg models.BaseMessage, err error, serverID string) models.BaseMessage {
protocolError := models.ProtocolError{
ErrCode: "internal-server-error",
ErrText: err.Error(),
ErrPayload: nil,
}
var to []string
if msg.From != "" {
to = append(to, msg.From)
}
errMsg := models.NewBaseMessage(msg.ID, msg.MessageType, serverID, to, false, structs.Map(protocolError))
return errMsg
}
func PrepareErrorMessage(msg models.BaseMessage, errorType string, errorText string, serverID string) models.BaseMessage {
protocolError := models.ProtocolError{
ErrCode: errorType,
ErrText: errorText,
ErrPayload: nil,
}
var to []string
if msg.From != "" {
to = append(to, msg.From)
}
errMsg := models.NewBaseMessage(msg.ID, msg.MessageType, serverID, to, false, structs.Map(protocolError))
return errMsg
}
func HashPassword(password string) (string, error) {
return argon2id.CreateHash(password, argon2id.DefaultParams)
}
func ValidatePassword(password string) error {
if password == "" {
return ErrPasswordIsEmpty
}
if len(password) < 4 {
return ErrPasswordIsTooShort
}
return nil
}
func IsCollectionExists(ctx context.Context, db *mongo.Database, collectionName string) (bool, error) {
isExists := false
names, err := db.ListCollectionNames(ctx, bson.D{{"name", collectionName}})
if err != nil {
return false, err
}
for _, name := range names {
if name == collectionName {
isExists = true
break
}
}
return isExists, nil
}

View File

@ -15,15 +15,15 @@ var wsUpgrader = websocket.Upgrader{
} }
type WebsocketServer struct { type WebsocketServer struct {
r *mux.Router r *mux.Router
connHandler *ConnectionHandler sessionManager *SessionManager
cfg *Config cfg *Config
} }
func NewWebsocketServer(cfg *Config, connHandler *ConnectionHandler) *WebsocketServer { func NewWebsocketServer(cfg *Config, sessionManager *SessionManager) *WebsocketServer {
wss := &WebsocketServer{} wss := &WebsocketServer{}
wss.connHandler = connHandler wss.sessionManager = sessionManager
wss.cfg = cfg wss.cfg = cfg
r := mux.NewRouter() r := mux.NewRouter()
wss.r = r wss.r = r
@ -36,7 +36,7 @@ func NewWebsocketServer(cfg *Config, connHandler *ConnectionHandler) *WebsocketS
logger.Errorf(err.Error()) logger.Errorf(err.Error())
return return
} }
wss.connHandler.HandleNewConnection(ws) wss.sessionManager.HandleNewConnection(ws)
}) })
return wss return wss

4
go.mod
View File

@ -3,12 +3,14 @@ module github.com/cadmium-im/zirconium-go
go 1.13 go 1.13
require ( require (
github.com/BurntSushi/toml v0.3.1 github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fatih/structs v1.1.0 // indirect
github.com/google/logger v1.0.1 github.com/google/logger v1.0.1
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.4.1
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pelletier/go-toml v1.7.0 github.com/pelletier/go-toml v1.7.0
go.mongodb.org/mongo-driver v1.5.1 go.mongodb.org/mongo-driver v1.5.1
) )

11
go.sum
View File

@ -1,5 +1,7 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77 h1:X6U+/fhTYeDYS3sN4xHcoORJhhar+zSgrNeraapuRK4=
github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77/go.mod h1:Kmn5t2Rb93Q4NTprN4+CCgARGvigKMJyxP0WckpTUp0=
github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk= github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -7,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -65,6 +69,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
@ -105,6 +111,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
@ -122,6 +130,9 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=