This adds a pattern for creating pages that require authentication. The settings page is currently empty, but it's helpful to figure out how to do this pattern.
212 lines
6.5 KiB
Go
212 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/enums"
|
|
"github.com/Gleipnir-Technology/nidus-sync/models"
|
|
"github.com/Gleipnir-Technology/nidus-sync/sql"
|
|
"github.com/aarondl/opt/omit"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type NoCredentialsError struct{}
|
|
|
|
func (e NoCredentialsError) Error() string { return "No credentials were present in the request" }
|
|
|
|
type NoUserError struct{}
|
|
|
|
func (e NoUserError) Error() string { return "That user does not exist" }
|
|
|
|
type InvalidCredentials struct{}
|
|
|
|
func (e InvalidCredentials) Error() string { return "No username with that password exists" }
|
|
|
|
type InvalidUsername struct{}
|
|
|
|
func (e InvalidUsername) Error() string { return "That username doesn't exist" }
|
|
|
|
type AuthenticatedHandler func(http.ResponseWriter, *http.Request, *models.User)
|
|
type EnsureAuth struct {
|
|
handler AuthenticatedHandler
|
|
}
|
|
|
|
func NewEnsureAuth(handlerToWrap AuthenticatedHandler) *EnsureAuth {
|
|
return &EnsureAuth{handlerToWrap}
|
|
}
|
|
|
|
func (ea *EnsureAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// If this is an API request respond with a more machine-readable error state
|
|
accept := r.Header.Values("Accept")
|
|
offers := []string{"application/json", "text/html"}
|
|
|
|
content_type := NegotiateContent(accept, offers)
|
|
user, err := getAuthenticatedUser(r)
|
|
if err != nil {
|
|
var msg []byte
|
|
// Separate return codes for different authentication failures
|
|
if _, ok := err.(*NoCredentialsError); ok {
|
|
fmt.Println("No credentials present and no session")
|
|
w.Header().Set("WWW-Authenticate-Error", "no-credentials")
|
|
msg = []byte("Please provide credentials.\n")
|
|
} else if _, ok := err.(*NoUserError); ok {
|
|
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
|
|
msg = []byte("Invalid credentials provided.\n")
|
|
} else if _, ok := err.(*InvalidCredentials); ok {
|
|
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
|
|
msg = []byte("Invalid credentials provided.\n")
|
|
}
|
|
|
|
if content_type == "text/html" {
|
|
http.Redirect(w, r, "/login?next="+r.URL.Path, http.StatusSeeOther)
|
|
return
|
|
}
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Nidus Sync"`)
|
|
w.WriteHeader(401)
|
|
w.Write(msg)
|
|
return
|
|
}
|
|
|
|
ea.handler(w, r, user)
|
|
}
|
|
func addUserSession(r *http.Request, user *models.User) {
|
|
id := strconv.Itoa(int(user.ID))
|
|
sessionManager.Put(r.Context(), "user_id", id)
|
|
sessionManager.Put(r.Context(), "username", user.Username)
|
|
slog.Info("Created new user session",
|
|
slog.String("username", user.Username),
|
|
slog.String("user_id", id))
|
|
}
|
|
|
|
// Helper function to translate strings into solid error types for operating on
|
|
func findUser(ctx context.Context, user_id int) (*models.User, error) {
|
|
user, err := models.FindUser(ctx, PGInstance.BobDB, int32(user_id))
|
|
if err != nil {
|
|
if err.Error() == "No such user" {
|
|
return nil, &NoUserError{}
|
|
} else {
|
|
LogErrorTypeInfo(err)
|
|
slog.Error("Unrecognized error. This should be updated in the findUser code", slog.String("err", err.Error()))
|
|
return nil, err
|
|
}
|
|
}
|
|
return user, err
|
|
}
|
|
|
|
func getAuthenticatedUser(r *http.Request) (*models.User, error) {
|
|
//user_id := sessionManager.GetInt(r.Context(), "user_id")
|
|
user_id_str := sessionManager.GetString(r.Context(), "user_id")
|
|
if user_id_str != "" {
|
|
user_id, err := strconv.Atoi(user_id_str)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to convert user_id to int: %v", err)
|
|
}
|
|
username := sessionManager.GetString(r.Context(), "username")
|
|
slog.Info("Current session info",
|
|
slog.Int("user_id", user_id),
|
|
slog.String("username", username))
|
|
if user_id > 0 && username != "" {
|
|
return findUser(r.Context(), user_id)
|
|
}
|
|
}
|
|
// If we can't get the user from the session try to get from auth headers
|
|
username, password, ok := r.BasicAuth()
|
|
if !ok {
|
|
return nil, &NoCredentialsError{}
|
|
}
|
|
user, err := validateUser(r.Context(), username, password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addUserSession(r, user)
|
|
return user, nil
|
|
}
|
|
|
|
func hashPassword(password string) (string, error) {
|
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
|
return string(bytes), err
|
|
}
|
|
|
|
func signinUser(r *http.Request, username string, password string) (*models.User, error) {
|
|
user, err := validateUser(r.Context(), username, password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user == nil {
|
|
return nil, errors.New("No matching user")
|
|
}
|
|
addUserSession(r, user)
|
|
return user, nil
|
|
}
|
|
|
|
func signupUser(username string, name string, password string) (*models.User, error) {
|
|
passwordHash, err := hashPassword(password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Cannot signup user: %v", err)
|
|
}
|
|
setter := models.UserSetter{
|
|
DisplayName: omit.From(name),
|
|
PasswordHash: omit.From(passwordHash),
|
|
PasswordHashType: omit.From(enums.HashtypeBcrypt14),
|
|
Username: omit.From(username),
|
|
}
|
|
u, err := models.Users.Insert(&setter).One(context.TODO(), PGInstance.BobDB)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to create user: %v", err)
|
|
}
|
|
slog.Info("Created user",
|
|
slog.Int("ID", int(u.ID)),
|
|
slog.String("username", u.Username))
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func validatePassword(password, hash string) bool {
|
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
|
return err == nil
|
|
}
|
|
|
|
func validateUser(ctx context.Context, username string, password string) (*models.User, error) {
|
|
passwordHash, err := hashPassword(password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to hash password: %v", err)
|
|
}
|
|
slog.Info("Validating user",
|
|
slog.String("username", username),
|
|
slog.String("password", password),
|
|
slog.String("hash", passwordHash))
|
|
result, err := sql.UserByUsername(username).All(ctx, PGInstance.BobDB)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to query for user: %v", err)
|
|
}
|
|
switch len(result) {
|
|
case 0:
|
|
return nil, InvalidUsername{}
|
|
case 1:
|
|
row := result[0]
|
|
if !validatePassword(password, row.PasswordHash) {
|
|
return nil, InvalidCredentials{}
|
|
}
|
|
user := models.User{
|
|
ID: row.ID,
|
|
ArcgisAccessToken: row.ArcgisAccessToken,
|
|
ArcgisLicense: row.ArcgisLicense,
|
|
ArcgisRefreshToken: row.ArcgisRefreshToken,
|
|
ArcgisRefreshTokenExpires: row.ArcgisRefreshTokenExpires,
|
|
ArcgisRole: row.ArcgisRole,
|
|
DisplayName: row.DisplayName,
|
|
Email: row.Email,
|
|
OrganizationID: row.OrganizationID,
|
|
Username: row.Username,
|
|
}
|
|
return &user, nil
|
|
default:
|
|
return nil, errors.New("More than one matching row, this should be impossible.")
|
|
|
|
}
|
|
}
|