diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecdab7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +pye-auth +key \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..62e8f1d --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +build: + go build + +run: + go build && ./pye-auth \ No newline at end of file diff --git a/README.md b/README.md index 0073a46..1a12a13 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ -# PYE +# PYE Auth -**Mission**: Science compels us to create a microservice! \ No newline at end of file +**Mission**: Science compels us to create a microservice! + +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. + +## 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 diff --git a/auth.go b/auth.go index 65f0f88..7de52f5 100644 --- a/auth.go +++ b/auth.go @@ -1,6 +1,7 @@ package main import ( + "log/slog" "net/http" "net/mail" "strings" @@ -13,9 +14,9 @@ func ValidEmail(email string) bool { func ValidPass(pass string) bool { return len(pass) >= 8 // TODO: Obviously, we *might* want something more sophisticated here } -func TakenEmail(email string) bool { - // TODO: Implement - return false +func EmailTaken(email string) bool { + // TODO: Implement properly + return EmailExists(email) } func Register(w http.ResponseWriter, r *http.Request) { email, password, ok := r.BasicAuth() @@ -23,13 +24,39 @@ func Register(w http.ResponseWriter, r *http.Request) { if !ok { email = strings.TrimSpace(email) password = strings.TrimSpace(password) - if !(ValidEmail(email) || ValidPass(password) || TakenEmail(email)) { + if !(ValidEmail(email) || ValidPass(password) || EmailTaken(email)) { // TODO: Provide descriptive error and check if 400 is best code? 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) + } + AddUser(user) } // 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) } + +func Login(w http.ResponseWriter, r *http.Request) { + email, password, ok := r.BasicAuth() + + if !ok { + email = strings.TrimSpace(email) + password = strings.TrimSpace(password) + user, ok := ByEmail(email) + if !ok || !user.PasswordFits(password) { + http.Error(w, "You did something wrong", http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusOK) + + } + + // 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/go.mod b/go.mod index 1aa3640..8675dd1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module pye-auth go 1.22 require ( + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 golang.org/x/crypto v0.28.0 ) diff --git a/go.sum b/go.sum index 303efc6..b6a6062 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= diff --git a/jwt.go b/jwt.go new file mode 100644 index 0000000..22183f4 --- /dev/null +++ b/jwt.go @@ -0,0 +1,44 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "log/slog" + + // "github.com/golang-jwt/jwt/v5" +) + +// var ( +// key *ecdsa.PrivateKey +// t *jwt.Token +// s string +// key string +// ) + +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) + } + km, _ := x509.MarshalECPrivateKey(k) + slog.Info("Key", "key", km) +} + +// func CreateJWT(usr User) string { + +// 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 +// } diff --git a/main.go b/main.go index 16dcb5e..a4fd821 100644 --- a/main.go +++ b/main.go @@ -2,31 +2,34 @@ package main import ( "fmt" - "net/http" + // "net/http" ) func main() { fmt.Println("Test") - router := http.NewServeMux() + CreateKey() - router.HandleFunc("POST /todos", func(w http.ResponseWriter, r *http.Request) { - fmt.Println("create a todo") - }) + // router := http.NewServeMux() - router.HandleFunc("GET /todos", func(w http.ResponseWriter, r *http.Request) { - fmt.Println("get all todos") - }) + // router.HandleFunc("POST /todos", func(w http.ResponseWriter, r *http.Request) { + // fmt.Println("create a todo") + // }) - 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("GET /public-key", func(w http.ResponseWriter, r *http.Request) { + // // w.WriteHeader(http.StatusOK) + // // w.Write() + // // }) - router.HandleFunc("DELETE /todos/{id}", func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - fmt.Println("delete a todo by id", id) - }) + // router.HandleFunc("PATCH /todos/{id}", func(w http.ResponseWriter, r *http.Request) { + // id := r.PathValue("id") + // fmt.Println("update a todo by id", id) + // }) - http.ListenAndServe(":7102", router) + // 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) } diff --git a/pseudo_db.go b/pseudo_db.go new file mode 100644 index 0000000..34ca77d --- /dev/null +++ b/pseudo_db.go @@ -0,0 +1,60 @@ +package main + +import ( + "encoding/json" + "log" + "os" +) + +// So SQLite seems to hate my Mac. +// And I'd rather deal with something easily tinker-able in PoC stage +// So................. +// JSON. +// +// TODO: Kill this, preferably with fire. + +func ReadUsers() []User { + data, err := os.ReadFile("./data.json") + if err != nil { + log.Fatal(err) + } + var users []User + err = json.Unmarshal(data, &users) + if err != nil { + log.Fatal(err) + } + return users +} + +func AddUser(user User) { + users := ReadUsers() + users = append(users, user) + data, err := json.Marshal(users) + if err != nil { + log.Fatal(err) + } + err = os.WriteFile("./data.json", data, 0644) + if err != nil { + log.Fatal(err) + } +} + +func EmailExists(email string) bool { + users := ReadUsers() + for i := 0; i < len(users); i++ { + if users[i].email == email { + return true + } + } + return false +} + +func UserByEmail(email string) (User, bool) { + users := ReadUsers() + for i := 0; i < len(users); i++ { + 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 83c1eaa..394f204 100644 --- a/user.go +++ b/user.go @@ -24,6 +24,7 @@ func NewUser(email, password string) (User, error) { return User{uuid.New(), email, hash}, nil } -func CreateUser(User) { - +// TODO: Implement +func ByEmail(email string) (User, bool) { + return UserByEmail(email) }