Implement basic API
This commit is contained in:
parent
94851e6104
commit
1f8393a985
7 changed files with 130 additions and 98 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
pye-auth
|
pye-auth
|
||||||
key
|
key
|
||||||
|
data.json
|
27
README.md
27
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
|
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.
|
|
28
auth.go
28
auth.go
|
@ -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,8 +62,14 @@ 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
|
||||||
|
|
100
jwt.go
100
jwt.go
|
@ -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
30
main.go
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
26
pseudo_db.go
26
pseudo_db.go
|
@ -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,7 +60,7 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8
user.go
8
user.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue