Implement basic API

This commit is contained in:
Andrew-71 2024-10-12 09:55:58 +03:00
parent 94851e6104
commit 1f8393a985
7 changed files with 130 additions and 98 deletions

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
pye-auth pye-auth
key key
data.json

View file

@ -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 with(out) blazingly fast cloud-native web3 memory-safe blockchain reactive AI
(insert a dozen more buzzwords of your choosing) technologies. (insert a dozen more buzzwords of your choosing) technologies.
This should be done by **October 17th 2024**, or at the very least, This should be done by **October 17th 2024**. Or, at the very least,
in a shape that proves I am somewhat competent. in a state that proves I am competent Go developer.
## Course of action Note: **JSON** is used for storage at proof-of-concept stage for ease of use
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.

32
auth.go
View file

@ -12,7 +12,9 @@ func ValidEmail(email string) bool {
return err == nil return err == nil
} }
func ValidPass(pass string) bool { 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 { func EmailTaken(email string) bool {
// TODO: Implement properly // TODO: Implement properly
@ -21,19 +23,27 @@ func EmailTaken(email string) bool {
func Register(w http.ResponseWriter, r *http.Request) { func Register(w http.ResponseWriter, r *http.Request) {
email, password, ok := r.BasicAuth() email, password, ok := r.BasicAuth()
if !ok { if ok {
email = strings.TrimSpace(email) email = strings.TrimSpace(email)
password = strings.TrimSpace(password) 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? // 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) http.Error(w, "Invalid auth credentials", http.StatusBadRequest)
return return
} }
user, err := NewUser(email, password) user, err := NewUser(email, password)
if err != nil { 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) AddUser(user)
w.WriteHeader(http.StatusCreated)
w.Write([]byte("User created"))
return
} }
// No email and password was provided // 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) { func Login(w http.ResponseWriter, r *http.Request) {
email, password, ok := r.BasicAuth() email, password, ok := r.BasicAuth()
if !ok { if ok {
email = strings.TrimSpace(email) email = strings.TrimSpace(email)
password = strings.TrimSpace(password) password = strings.TrimSpace(password)
user, ok := ByEmail(email) 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) http.Error(w, "You did something wrong", http.StatusUnauthorized)
return 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 // No email and password was provided
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "This API requires authorization", http.StatusUnauthorized) http.Error(w, "This API requires authorization", http.StatusUnauthorized)
} }

100
jwt.go
View file

@ -5,40 +5,84 @@ import (
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/x509" "crypto/x509"
"encoding/pem"
"errors"
"log/slog" "log/slog"
"net/http"
"os"
// "github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
// var ( var KeyFile = "key"
// key *ecdsa.PrivateKey
// t *jwt.Token
// s string
// key string
// )
func CreateKey() { var (
// TODO: Is this a secure key? key *ecdsa.PrivateKey
k, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) // t *jwt.Token
if err != nil { )
slog.Error("Error generating key", "error", err)
// 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.Debug("private key", "key", key)
slog.Info("Key", "key", km)
} }
// 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, func init() {
// jwt.MapClaims{ LoadKey()
// "iss": "my-auth-server", }
// "sub": "john",
// "foo": 2, func CreateJWT(usr User) (string, error) {
// }) t := jwt.NewWithClaims(jwt.SigningMethodES256,
// s, err := t.SignedString(key) jwt.MapClaims{
// if err != nil { "iss": "pye",
// slog.Error("Error creating JWT", "error", err) "sub": "john",
// // TODO: Something "foo": 2,
// } })
// return s s, err := t.SignedString(key)
// } if err != nil {
slog.Error("Error creating JWT", "error", err)
return "", err
}
return s, nil
}

30
main.go
View file

@ -2,34 +2,18 @@ package main
import ( import (
"fmt" "fmt"
// "net/http" "net/http"
) )
func main() { 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) { router.HandleFunc("POST /register", Register)
// fmt.Println("create a todo") router.HandleFunc("POST /login", Login)
// })
// // router.HandleFunc("GET /public-key", func(w http.ResponseWriter, r *http.Request) { http.ListenAndServe(":7102", router)
// // 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)
} }

View file

@ -2,11 +2,11 @@ package main
import ( import (
"encoding/json" "encoding/json"
"log" "log/slog"
"os" "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 // And I'd rather deal with something easily tinker-able in PoC stage
// So................. // So.................
// JSON. // JSON.
@ -16,12 +16,12 @@ import (
func ReadUsers() []User { func ReadUsers() []User {
data, err := os.ReadFile("./data.json") data, err := os.ReadFile("./data.json")
if err != nil { if err != nil {
log.Fatal(err) slog.Error("error reading file", "error", err)
} }
var users []User var users []User
err = json.Unmarshal(data, &users) err = json.Unmarshal(data, &users)
if err != nil { if err != nil {
log.Fatal(err) slog.Error("error unmarshalling data", "error", err)
} }
return users return users
} }
@ -29,20 +29,28 @@ func ReadUsers() []User {
func AddUser(user User) { func AddUser(user User) {
users := ReadUsers() users := ReadUsers()
users = append(users, user) users = append(users, user)
// slog.Info("users", "users", users)
data, err := json.Marshal(users) data, err := json.Marshal(users)
if err != nil { 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 { 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 { func EmailExists(email string) bool {
users := ReadUsers() users := ReadUsers()
for i := 0; i < len(users); i++ { for i := 0; i < len(users); i++ {
if users[i].email == email { if users[i].Email == email {
return true return true
} }
} }
@ -52,9 +60,9 @@ func EmailExists(email string) bool {
func UserByEmail(email string) (User, bool) { func UserByEmail(email string) (User, bool) {
users := ReadUsers() users := ReadUsers()
for i := 0; i < len(users); i++ { for i := 0; i < len(users); i++ {
if users[i].email == email { if users[i].Email == email {
return users[i], true return users[i], true
} }
} }
return User{}, false return User{}, false
} }

View file

@ -6,13 +6,13 @@ import (
) )
type User struct { type User struct {
uuid uuid.UUID Uuid uuid.UUID
email string Email string
hash []byte // bcrypt hash of password Hash []byte // bcrypt hash of password
} }
func (u User) PasswordFits(password string) bool { func (u User) PasswordFits(password string) bool {
err := bcrypt.CompareHashAndPassword(u.hash, []byte(password)) err := bcrypt.CompareHashAndPassword(u.Hash, []byte(password))
return err == nil return err == nil
} }