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

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
pye-auth
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
(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.
Note: **JSON** is used for storage at proof-of-concept stage for ease of use

28
auth.go
View file

@ -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,8 +62,14 @@ 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

98
jwt.go
View file

@ -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)
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)
slog.Error("error generating key", "error", err)
os.Exit(1)
}
km, _ := x509.MarshalECPrivateKey(k)
slog.Info("Key", "key", km)
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")
}
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
}

30
main.go
View file

@ -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)
}

View file

@ -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,7 +60,7 @@ 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
}
}

View file

@ -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
}