package auth import ( "errors" "fmt" "math/rand" "time" jwt "github.com/dgrijalva/jwt-go" log "github.com/sirupsen/logrus" ) var expirationDelta = time.Hour * 12 type AuthType int const ( AuthTypeUnknown AuthType = iota AuthTypeBasic AuthTypeToken ) type AuthRequest struct { Username string `json:"username"` Password string `json:"password"` // JWT Token string AuthType AuthType `json:"-"` } type AuthResponse struct { Token string `json:"token"` User User `json:"-"` } type Authenticator interface { // indicates whether authentication is enabled Enabled() bool Authenticate(req *AuthRequest) (*AuthResponse, error) GenerateToken(u User) (*AuthResponse, error) } func New(opts *Opts) *DefaultAuthenticator { if len(opts.Secret) == 0 { opts.Secret = []byte(randStringRunes(23)) } return &DefaultAuthenticator{ opts: opts, secret: opts.Secret, } } type Opts struct { // Basic auth Username string Password string // Secret used to sign JWT tokens Secret []byte } type DefaultAuthenticator struct { opts *Opts secret []byte } var ( ErrUnauthorized = errors.New("unauthorized") ) func (a *DefaultAuthenticator) Enabled() bool { return a.opts.Username != "" && a.opts.Password != "" } func (a *DefaultAuthenticator) Authenticate(req *AuthRequest) (*AuthResponse, error) { switch req.AuthType { case AuthTypeToken: user, err := a.parseToken(req.Token) if err != nil { return nil, err } return a.GenerateToken(*user) case AuthTypeBasic: // ok default: return nil, fmt.Errorf("unknown auth type") } if a.opts.Username == "" && a.opts.Password == "" { // if basic auth not set - authenticating as guest return a.GenerateToken(User{Username: "guest"}) } if req.Username != a.opts.Username || req.Password != a.opts.Password { return nil, ErrUnauthorized } return a.GenerateToken(User{Username: req.Username}) } type User struct { Username string } func (a *DefaultAuthenticator) GenerateToken(u User) (*AuthResponse, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{ "username": "admin", "exp": time.Now().Add(expirationDelta).Unix(), "iat": time.Now().Unix(), }) // Sign and get the complete encoded token as a string using the secret tokenString, err := token.SignedString(a.secret) if err != nil { return nil, fmt.Errorf("failed to sign token, error: %s, s: %s", err, string(a.secret)) } return &AuthResponse{ Token: tokenString, }, nil } func (a *DefaultAuthenticator) parseToken(tokenString string) (*User, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return a.secret, nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { user := &User{} user.Username = parseString(claims, "username") if user.Username == "" { log.WithFields(log.Fields{ "token": tokenString, "error": "token is missing account username field", }).Warn("authenticator: malformed token") return nil, fmt.Errorf("malformed token") } // returning return user, nil } return nil, fmt.Errorf("invalid token") } func parseString(meta map[string]interface{}, key string) string { val, ok := meta[key] if !ok { return "" } s, ok := val.(string) if ok { return s } return "" } var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randStringRunes(n int) string { rand.Seed(time.Now().UnixNano()) b := make([]rune, n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) }