Skip to content

Commit

Permalink
reworked middleware to add token route and validate users
Browse files Browse the repository at this point in the history
  • Loading branch information
Linesmerrill committed May 30, 2024
1 parent 93de263 commit f199866
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export DB_NAME=knoldus
# Example: sRqXKbHFdQ8Wka6NWvk3ZaZhY3JD3CnUw7H5B42gETZS8feYUFUJ8k2jRvnNQrLwEyPWnwfuGN9XG3GQaPxC6HdtNNYmbZW6MEN32BfSpsDzdVtQJXuNHfmUPsRw4LDdyRxs4PVFHAmGfnF3BWPZgJpnMst28QCDbKpWfbM8G8vmPQD9fRh6KxsZqsGz3QpCmwekHPnuX3n9n9KHp8emReeLq7EKnSsmhCcVeQNB2psgSUkVYpgeaXBDHZ8TVjmq
export SECRET_KEY='your-256-bit-secret'

export PORT=8081
export PORT=8082
export BASE_URL=localhost
export ENV=local
10 changes: 10 additions & 0 deletions api/handlers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type App struct {

// New creates a new mux router and all the routes
func (a *App) New() *mux.Router {
// setup go-guardian for middleware
m := api.MiddlewareDB{DB: databases.NewUserDatabase(a.dbHelper)}
m.SetupGoGuardian()

r := mux.NewRouter()

u := User{DB: databases.NewUserDatabase(a.dbHelper)}
Expand All @@ -43,11 +47,17 @@ func (a *App) New() *mux.Router {

apiCreate := r.PathPrefix("/api/v1").Subrouter()

apiCreate.Handle("/auth/token", api.Middleware(http.HandlerFunc(api.CreateToken))).Methods("POST")

apiCreate.Handle("/community/{community_id}", api.Middleware(http.HandlerFunc(c.CommunityHandler))).Methods("GET")
apiCreate.Handle("/community/{community_id}/{owner_id}", api.Middleware(http.HandlerFunc(c.CommunityByCommunityAndOwnerIDHandler))).Methods("GET")
apiCreate.Handle("/communities/{owner_id}", api.Middleware(http.HandlerFunc(c.CommunitiesByOwnerIDHandler))).Methods("GET")

apiCreate.Handle("/user/{user_id}", api.Middleware(http.HandlerFunc(u.UserHandler))).Methods("GET")
apiCreate.Handle("/users/{active_community_id}", api.Middleware(http.HandlerFunc(u.UsersFindAllHandler))).Methods("GET")
// apiCreate.Handle("/user/login", http.HandlerFunc(u.UserLoginHandler)).Methods("POST")
// apiCreate.Handle("/user/logout", api.Middleware(http.HandlerFunc(u.UserLogoutHandler))).Methods("DELETE")

apiCreate.Handle("/civilian/{civilian_id}", api.Middleware(http.HandlerFunc(civ.CivilianByIDHandler))).Methods("GET")
apiCreate.Handle("/civilians", api.Middleware(http.HandlerFunc(civ.CivilianHandler))).Methods("GET")
apiCreate.Handle("/civilians/user/{user_id}", api.Middleware(http.HandlerFunc(civ.CiviliansByUserIDHandler))).Methods("GET")
Expand Down
76 changes: 72 additions & 4 deletions api/handlers/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ package handlers

import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"fmt"
"net/http"

"github.com/gorilla/mux"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"

"github.com/linesmerrill/police-cad-api/config"
"github.com/linesmerrill/police-cad-api/databases"
"github.com/linesmerrill/police-cad-api/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)

// User exported for testing purposes
type User struct {
DB databases.UserDatabase
}
Expand Down Expand Up @@ -70,3 +74,67 @@ func (u User) UsersFindAllHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(b)
}

// UserLoginHandler returns a session token for a user
func (u User) UserLoginHandler(w http.ResponseWriter, r *http.Request) {
email, password, ok := r.BasicAuth()
if ok {
usernameHash := sha256.Sum256([]byte(email))

// fetch email & pass from db
dbEmailResp, err := u.DB.Find(context.Background(), bson.M{"user.email": email})
if err != nil {
config.ErrorStatus("failed to get user by ID", http.StatusNotFound, w, err)
return
}
if len(dbEmailResp) == 0 {
config.ErrorStatus("no matching email found", http.StatusUnauthorized, w, fmt.Errorf("no matching email found"))
return
}

expectedUsernameHash := sha256.Sum256([]byte(dbEmailResp[0].Details.Email))
usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1

err = bcrypt.CompareHashAndPassword([]byte(dbEmailResp[0].Details.Password), []byte(password))
if err != nil {
config.ErrorStatus("failed to compare password", http.StatusUnauthorized, w, err)
return
}

if usernameMatch {
w.WriteHeader(http.StatusOK)
return
}
}

w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)

}

// UserLogoutHandler returns a status code of the user logging out
func (u User) UserLogoutHandler(w http.ResponseWriter, r *http.Request) {
commID := mux.Vars(r)["user_id"]

zap.S().Debugf("user_id: %v", commID)

cID, err := primitive.ObjectIDFromHex(commID)
if err != nil {
config.ErrorStatus("failed to get objectID from Hex", http.StatusBadRequest, w, err)
return
}

dbResp, err := u.DB.FindOne(context.Background(), bson.M{"_id": cID})
if err != nil {
config.ErrorStatus("failed to get user by ID", http.StatusNotFound, w, err)
return
}

b, err := json.Marshal(dbResp)
if err != nil {
config.ErrorStatus("failed to marshal response", http.StatusInternalServerError, w, err)
return
}
w.WriteHeader(http.StatusOK)
w.Write(b)
}
127 changes: 76 additions & 51 deletions api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,95 @@ package api

import (
"context"
"crypto/sha256"
"crypto/subtle"
"fmt"
"net/http"
"net/http/httputil"
"os"
"strings"
"time"

"github.com/form3tech-oss/jwt-go"
"github.com/google/uuid"
"github.com/linesmerrill/police-cad-api/databases"
"github.com/shaj13/go-guardian/auth"
"github.com/shaj13/go-guardian/auth/strategies/basic"
"github.com/shaj13/go-guardian/auth/strategies/bearer"
"github.com/shaj13/go-guardian/store"
"go.mongodb.org/mongo-driver/bson"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)

type key int
// MiddlewareDB is a struct that holds the database
type MiddlewareDB struct {
DB databases.UserDatabase
}

const (
keyPrincipalID key = iota
)
var authenticator auth.Authenticator
var cache store.Cache

// Middleware adds some basic header authentication around accessing the routes
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ")
// we don't really care about the error here, if it fails then oh well :shrug:
dump, _ := httputil.DumpRequest(r, true)
zap.S().Debugw("incoming request",
"request body", string(dump))
if len(authHeader) != 2 {
zap.S().Errorw("malformed token",
"url", r.URL,
"auth header", r.Header.Get("Authorization"),
)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error": "malformed authorization token, see https://github.com/Linesmerrill/police-cad-api#requirements for help"}`))
return
}
jwtToken := authHeader[1]
token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf(`{"error": "unexpected signing method, %v"}`, token.Header["alg"])
}
return []byte(os.Getenv("SECRET_KEY")), nil
})
user, err := authenticator.Authenticate(r)
if err != nil {
zap.S().With("error", err).Errorw("failed to parse token",
"url", r.URL,
"token", jwtToken,
)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf(`{"error": "failed to parse token, %v"}`, err)))
return
}

if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
ctx := context.WithValue(r.Context(), keyPrincipalID, claims)
// Access context values in handlers like this
// props, _ := r.Context().Value("props").(jwt.MapClaims)
next.ServeHTTP(w, r.WithContext(ctx))
} else {
zap.S().Errorw("unauthorized",
"url", r.URL,
"token", jwtToken,
)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error": "unauthorized"}"`))
code := http.StatusUnauthorized
http.Error(w, http.StatusText(code), code)
return
}
zap.S().Debugf("User %s Authenticated\n", user.UserName())
next.ServeHTTP(w, r)
})
}

// CreateToken returns a token
func CreateToken(w http.ResponseWriter, r *http.Request) {
email, _, ok := r.BasicAuth()
if !ok {
http.Error(w, "basic auth failed", http.StatusUnauthorized)
return
}
token := uuid.New().String()
user := auth.NewDefaultUser(email, "1", nil, nil)
tokenStrategy := authenticator.Strategy(bearer.CachedStrategyKey)
auth.Append(tokenStrategy, token, user, r)
body := fmt.Sprintf("token: %s \n", token)
w.Write([]byte(body))
}

// SetupGoGuardian sets up the go-guardian middleware
func (m MiddlewareDB) SetupGoGuardian() {
authenticator = auth.New()
cache = store.NewFIFO(context.Background(), time.Minute*10)
basicStrategy := basic.New(m.ValidateUser, cache)
tokenStrategy := bearer.New(bearer.NoOpAuthenticate, cache)

authenticator.EnableStrategy(basic.StrategyKey, basicStrategy)
authenticator.EnableStrategy(bearer.CachedStrategyKey, tokenStrategy)
}

// ValidateUser validates a user
func (m MiddlewareDB) ValidateUser(ctx context.Context, r *http.Request, email, password string) (auth.Info, error) {

usernameHash := sha256.Sum256([]byte(email))

// fetch email & pass from db
dbEmailResp, err := m.DB.Find(context.Background(), bson.M{"user.email": email})
if err != nil {
return nil, fmt.Errorf("failed to get user by ID")
}
if len(dbEmailResp) == 0 {
return nil, fmt.Errorf("no matching email found")
}

expectedUsernameHash := sha256.Sum256([]byte(dbEmailResp[0].Details.Email))
usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1

err = bcrypt.CompareHashAndPassword([]byte(dbEmailResp[0].Details.Password), []byte(password))
if err != nil {
return nil, fmt.Errorf("failed to compare password")
}

if usernameMatch {
return auth.NewDefaultUser(email, "1", nil, nil), nil
}
return nil, fmt.Errorf("invalid credentials")
}
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
module github.com/linesmerrill/police-cad-api

go 1.17
go 1.22

require (
github.com/form3tech-oss/jwt-go v3.2.5+incompatible
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.0
github.com/shaj13/go-guardian v1.5.11
github.com/stretchr/testify v1.7.1
go.mongodb.org/mongo-driver v1.8.4
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.23.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/klauspost/compress v1.13.6 // indirect
Expand All @@ -24,9 +27,8 @@ require (
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/text v0.15.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

Expand Down
Loading

0 comments on commit f199866

Please sign in to comment.