package internal import ( "context" "errors" "fmt" "net/http" "time" jwt "github.com/dgrijalva/jwt-go" "github.com/julienschmidt/httprouter" log "github.com/sirupsen/logrus" "git.mills.io/prologic/spyda/internal/passwords" "git.mills.io/prologic/spyda/types" ) // ContextKey ... type ContextKey int const ( TokenContextKey ContextKey = iota UserContextKey ) var ( // ErrInvalidCredentials is returned for invalid credentials against /auth ErrInvalidCredentials = errors.New("error: invalid credentials") // ErrInvalidToken is returned for expired or invalid tokens used in Authorizeation headers ErrInvalidToken = errors.New("error: invalid token") ) // API ... type API struct { router *Router config *Config cache *Cache archive Archiver db Store pm passwords.Passwords tasks *Dispatcher } // NewAPI ... func NewAPI(router *Router, config *Config, cache *Cache, archive Archiver, db Store, pm passwords.Passwords, tasks *Dispatcher) *API { api := &API{router, config, cache, archive, db, pm, tasks} api.initRoutes() return api } func (a *API) initRoutes() { router := a.router.Group("/api/v1") router.GET("/ping", a.PingEndpoint()) router.POST("/auth", a.AuthEndpoint()) // Support / Report endpoints router.POST("/support", a.isAuthorized(a.SupportEndpoint())) router.POST("/report", a.isAuthorized(a.ReportEndpoint())) } // CreateToken ... func (a *API) CreateToken(user *User, r *http.Request) (*Token, error) { claims := jwt.MapClaims{} claims["username"] = user.Username createdAt := time.Now() token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(a.config.APISigningKey)) if err != nil { log.WithError(err).Error("error creating signed token") return nil, err } signedToken, err := jwt.Parse(tokenString, a.jwtKeyFunc) if err != nil { log.WithError(err).Error("error creating signed token") return nil, err } tkn := &Token{ Signature: signedToken.Signature, Value: tokenString, UserAgent: r.UserAgent(), CreatedAt: createdAt, } return tkn, nil } func (a *API) jwtKeyFunc(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("There was an error") } return []byte(a.config.APISigningKey), nil } func (a *API) getLoggedInUser(r *http.Request) *User { token, err := jwt.Parse(r.Header.Get("Token"), a.jwtKeyFunc) if err != nil { return nil } if !token.Valid { return nil } claims := token.Claims.(jwt.MapClaims) username := claims["username"].(string) user, err := a.db.GetUser(username) if err != nil { log.WithError(err).Error("error loading user object") return nil } // Every registered new user follows themselves // TODO: Make this configurable server behaviour? if user.Following == nil { user.Following = make(map[string]string) } user.Following[user.Username] = user.URL return user } func (a *API) isAuthorized(endpoint httprouter.Handle) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { if r.Header.Get("Token") == "" { http.Error(w, "No Token Provided", http.StatusUnauthorized) return } token, err := jwt.Parse(r.Header.Get("Token"), a.jwtKeyFunc) if err != nil { log.WithError(err).Error("error parsing token") http.Error(w, "Bad Request", http.StatusBadRequest) return } if token.Valid { claims := token.Claims.(jwt.MapClaims) username := claims["username"].(string) user, err := a.db.GetUser(username) if err != nil { log.WithError(err).Error("error loading user object") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } // Every registered new user follows themselves // TODO: Make this configurable server behaviour? if user.Following == nil { user.Following = make(map[string]string) } user.Following[user.Username] = user.URL ctx := context.WithValue(r.Context(), TokenContextKey, token) ctx = context.WithValue(ctx, UserContextKey, user) endpoint(w, r.WithContext(ctx), p) } else { http.Error(w, "Invalid Token", http.StatusUnauthorized) return } } } // PingEndpoint ... func (a *API) PingEndpoint() httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{}`)) } } // AuthEndpoint ... func (a *API) AuthEndpoint() httprouter.Handle { // #239: Throttle failed login attempts and lock user account. failures := NewTTLCache(5 * time.Minute) return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { req, err := types.NewAuthRequest(r.Body) if err != nil { log.WithError(err).Error("error parsing auth request") http.Error(w, "Bad Request", http.StatusBadRequest) return } username := NormalizeUsername(req.Username) password := req.Password // Error: no username or password provided if username == "" || password == "" { log.Warn("no username or password provided") http.Error(w, "Bad Request", http.StatusBadRequest) return } // Lookup user user, err := a.db.GetUser(username) if err != nil { log.WithField("username", username).Warn("login attempt from non-existent user") http.Error(w, "Invalid Credentials", http.StatusUnauthorized) return } // #239: Throttle failed login attempts and lock user account. if failures.Get(user.Username) > MaxFailedLogins { http.Error(w, "Account Locked", http.StatusTooManyRequests) return } // Validate cleartext password against KDF hash err = a.pm.CheckPassword(user.Password, password) if err != nil { // #239: Throttle failed login attempts and lock user account. failed := failures.Inc(user.Username) time.Sleep(time.Duration(IntPow(2, failed)) * time.Second) log.WithField("username", username).Warn("login attempt with invalid credentials") http.Error(w, "Invalid Credentials", http.StatusUnauthorized) return } // #239: Throttle failed login attempts and lock user account. failures.Reset(user.Username) // Login successful log.WithField("username", username).Info("login successful") token, err := a.CreateToken(user, r) if err != nil { log.WithError(err).Error("error creating token") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } user.AddToken(token) if err := a.db.SetToken(token.Signature, token); err != nil { log.WithError(err).Error("error saving token object") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } if err := a.db.SetUser(user.Username, user); err != nil { log.WithError(err).Error("error saving user object") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } res := types.AuthResponse{Token: token.Value} body, err := res.Bytes() if err != nil { log.WithError(err).Error("error serializing response") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write(body) } }