diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 00000000..77077e3d --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,175 @@ +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 { + 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) 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) +}