diff --git a/.gitignore b/.gitignore index ecdab7d..84695b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ pye-auth -key \ No newline at end of file +key +data.json \ No newline at end of file diff --git a/README.md b/README.md index 1a12a13..d9c5c82 100644 --- a/README.md +++ b/README.md @@ -6,28 +6,7 @@ This is the repository for my **JWT auth microservice assignment** with(out) blazingly fast cloud-native web3 memory-safe blockchain reactive AI (insert a dozen more buzzwords of your choosing) technologies. -This should be done by **October 17th 2024**, or at the very least, -in a shape that proves I am somewhat competent. +This should be done by **October 17th 2024**. Or, at the very least, +in a state that proves I am competent Go developer. -## Course of action - -How I currently see this going - -1. Make an HTTP Basic Auth -> JWT -> Open key API -2. Create simple frontend (really stretching the definition) to test it -3. Ask myself and others - "Is this a microservice?" -If the answer is yes, rejoice. -If the answer is no, rejoice for a different reason. -4. Once it's technically solid-ish, polish ever-so-slightly - -## "Technology stack" - -The technology I *intend* on using - -1. **Data storage - SQLite**. -Definitely want to avoid a full-sized DB because they're oversized for most -projects. To be honest, even **JSON** would do for this. -In fact, this might just be the way to go for the proof-of-concept, hm... -2. **Frontend - template/html module**. Duh, I am anti-bloat. -3. **HTTP routing - Chi**. -I'd use `net/http`, but a deadline of 1 week means speed is everything. \ No newline at end of file +Note: **JSON** is used for storage at proof-of-concept stage for ease of use \ No newline at end of file diff --git a/auth.go b/auth.go index 7de52f5..64ac598 100644 --- a/auth.go +++ b/auth.go @@ -12,7 +12,9 @@ func ValidEmail(email string) bool { return err == nil } func ValidPass(pass string) bool { - return len(pass) >= 8 // TODO: Obviously, we *might* want something more sophisticated here + // TODO: Obviously, we *might* want something more sophisticated here + return true + //return len(pass) >= 8 } func EmailTaken(email string) bool { // TODO: Implement properly @@ -21,19 +23,27 @@ func EmailTaken(email string) bool { func Register(w http.ResponseWriter, r *http.Request) { email, password, ok := r.BasicAuth() - if !ok { + if ok { email = strings.TrimSpace(email) password = strings.TrimSpace(password) - if !(ValidEmail(email) || ValidPass(password) || EmailTaken(email)) { + if !(ValidEmail(email) && ValidPass(password) && !EmailTaken(email)) { // TODO: Provide descriptive error and check if 400 is best code? + slog.Info("Outcome", + "email", ValidEmail(email), + "pass", ValidPass(password), + "taken", !EmailTaken(email)) http.Error(w, "Invalid auth credentials", http.StatusBadRequest) return } user, err := NewUser(email, password) if err != nil { - slog.Error("Error creating a new user", "error", err) + slog.Error("error creating a new user", "error", err) } + slog.Info("user", "user", user) AddUser(user) + w.WriteHeader(http.StatusCreated) + w.Write([]byte("User created")) + return } // No email and password was provided @@ -44,7 +54,7 @@ func Register(w http.ResponseWriter, r *http.Request) { func Login(w http.ResponseWriter, r *http.Request) { email, password, ok := r.BasicAuth() - if !ok { + if ok { email = strings.TrimSpace(email) password = strings.TrimSpace(password) user, ok := ByEmail(email) @@ -52,11 +62,17 @@ func Login(w http.ResponseWriter, r *http.Request) { http.Error(w, "You did something wrong", http.StatusUnauthorized) return } - w.WriteHeader(http.StatusOK) - + + s, err := CreateJWT(user) + if err != nil { + http.Error(w, "error creating jwt", http.StatusInternalServerError) + return + } + w.Write([]byte(s)) + return } // No email and password was provided w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) http.Error(w, "This API requires authorization", http.StatusUnauthorized) -} \ No newline at end of file +} diff --git a/jwt.go b/jwt.go index 22183f4..7c87649 100644 --- a/jwt.go +++ b/jwt.go @@ -5,40 +5,84 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/x509" + "encoding/pem" + "errors" "log/slog" + "net/http" + "os" - // "github.com/golang-jwt/jwt/v5" + "github.com/golang-jwt/jwt/v5" ) -// var ( -// key *ecdsa.PrivateKey -// t *jwt.Token -// s string -// key string -// ) +var KeyFile = "key" -func CreateKey() { - // TODO: Is this a secure key? - k, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - if err != nil { - slog.Error("Error generating key", "error", err) +var ( + key *ecdsa.PrivateKey + // t *jwt.Token +) + +// LoadKey attempts to load a private key from KeyFile. +// If the file does not exist, it generates a new key (and saves it) +func LoadKey() { + // If the key doesn't exist, create it + if _, err := os.Stat(KeyFile); errors.Is(err, os.ErrNotExist) { + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + slog.Error("error generating key", "error", err) + os.Exit(1) + } + km, err := x509.MarshalECPrivateKey(key) // Save private key to disk + if err != nil { + slog.Error("error marshalling key", "error", err) + os.Exit(1) + } + os.WriteFile(KeyFile, km, 0644) + slog.Info("generated new key") + } else { + km, err := os.ReadFile(KeyFile) + if err != nil { + slog.Error("error reading key", "error", err) + os.Exit(1) + } + key, err = x509.ParseECPrivateKey(km) + if err != nil { + slog.Error("error parsing key", "error", err) + os.Exit(1) + } + slog.Info("loaded private key") } - km, _ := x509.MarshalECPrivateKey(k) - slog.Info("Key", "key", km) + slog.Debug("private key", "key", key) } -// func CreateJWT(usr User) string { +// publicKey returns our public key in PKIX, ASN.1 DER form +func publicKey(w http.ResponseWriter, r *http.Request) { + key_marshalled, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + slog.Error("error marshalling public key", "error", err) + http.Error(w, "error marshalling public key", http.StatusInternalServerError) + return + } + // w.Write(key_marshalled) + block := pem.Block{Bytes: key_marshalled, Type: "ECDSA PUBLIC KEY"} + // slog.Info("public key", "orig", key_marshalled, "block", block) + pem.Encode(w, &block) +} -// t := jwt.NewWithClaims(jwt.SigningMethodES256, -// jwt.MapClaims{ -// "iss": "my-auth-server", -// "sub": "john", -// "foo": 2, -// }) -// s, err := t.SignedString(key) -// if err != nil { -// slog.Error("Error creating JWT", "error", err) -// // TODO: Something -// } -// return s -// } +func init() { + LoadKey() +} + +func CreateJWT(usr User) (string, error) { + t := jwt.NewWithClaims(jwt.SigningMethodES256, + jwt.MapClaims{ + "iss": "pye", + "sub": "john", + "foo": 2, + }) + s, err := t.SignedString(key) + if err != nil { + slog.Error("Error creating JWT", "error", err) + return "", err + } + return s, nil +} diff --git a/main.go b/main.go index a4fd821..f7bd9ef 100644 --- a/main.go +++ b/main.go @@ -2,34 +2,18 @@ package main import ( "fmt" - // "net/http" + "net/http" ) func main() { - fmt.Println("Test") + fmt.Println("=== PYE ===") - CreateKey() + router := http.NewServeMux() - // router := http.NewServeMux() + router.HandleFunc("GET /public-key", publicKey) - // router.HandleFunc("POST /todos", func(w http.ResponseWriter, r *http.Request) { - // fmt.Println("create a todo") - // }) + router.HandleFunc("POST /register", Register) + router.HandleFunc("POST /login", Login) - // // router.HandleFunc("GET /public-key", func(w http.ResponseWriter, r *http.Request) { - // // w.WriteHeader(http.StatusOK) - // // w.Write() - // // }) - - // router.HandleFunc("PATCH /todos/{id}", func(w http.ResponseWriter, r *http.Request) { - // id := r.PathValue("id") - // fmt.Println("update a todo by id", id) - // }) - - // router.HandleFunc("DELETE /todos/{id}", func(w http.ResponseWriter, r *http.Request) { - // id := r.PathValue("id") - // fmt.Println("delete a todo by id", id) - // }) - - // http.ListenAndServe(":7102", router) + http.ListenAndServe(":7102", router) } diff --git a/pseudo_db.go b/pseudo_db.go index 34ca77d..a7837ce 100644 --- a/pseudo_db.go +++ b/pseudo_db.go @@ -2,11 +2,11 @@ package main import ( "encoding/json" - "log" + "log/slog" "os" ) -// So SQLite seems to hate my Mac. +// SQLite seems to hate my Mac. // And I'd rather deal with something easily tinker-able in PoC stage // So................. // JSON. @@ -16,12 +16,12 @@ import ( func ReadUsers() []User { data, err := os.ReadFile("./data.json") if err != nil { - log.Fatal(err) + slog.Error("error reading file", "error", err) } var users []User err = json.Unmarshal(data, &users) if err != nil { - log.Fatal(err) + slog.Error("error unmarshalling data", "error", err) } return users } @@ -29,20 +29,28 @@ func ReadUsers() []User { func AddUser(user User) { users := ReadUsers() users = append(users, user) + // slog.Info("users", "users", users) data, err := json.Marshal(users) if err != nil { - log.Fatal(err) + slog.Error("error marshalling", "error", err) + return } - err = os.WriteFile("./data.json", data, 0644) + + f, err := os.OpenFile("./data.json", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { - log.Fatal(err) + slog.Error("error opening/creating file data.json") + return + } + if _, err := f.Write(data); err != nil { + slog.Error("error writing to file data.json", "error", err) + return } } func EmailExists(email string) bool { users := ReadUsers() for i := 0; i < len(users); i++ { - if users[i].email == email { + if users[i].Email == email { return true } } @@ -52,9 +60,9 @@ func EmailExists(email string) bool { func UserByEmail(email string) (User, bool) { users := ReadUsers() for i := 0; i < len(users); i++ { - if users[i].email == email { + if users[i].Email == email { return users[i], true } } return User{}, false -} \ No newline at end of file +} diff --git a/user.go b/user.go index 394f204..b10e514 100644 --- a/user.go +++ b/user.go @@ -6,13 +6,13 @@ import ( ) type User struct { - uuid uuid.UUID - email string - hash []byte // bcrypt hash of password + Uuid uuid.UUID + Email string + Hash []byte // bcrypt hash of password } func (u User) PasswordFits(password string) bool { - err := bcrypt.CompareHashAndPassword(u.hash, []byte(password)) + err := bcrypt.CompareHashAndPassword(u.Hash, []byte(password)) return err == nil }