Move to RSA and add verification
This commit is contained in:
parent
8fa3424a65
commit
170416e791
5 changed files with 65 additions and 49 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
pye-auth
|
pye-auth
|
||||||
key
|
private.key
|
||||||
data.json
|
data.json
|
|
@ -1,4 +1,4 @@
|
||||||
# PYE Auth
|
# Auth microservice
|
||||||
|
|
||||||
**Mission**: Science compels us to create a microservice!
|
**Mission**: Science compels us to create a microservice!
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@ obviously I'd use **SQL** for production
|
||||||
|
|
||||||
## Current functionality
|
## Current functionality
|
||||||
|
|
||||||
* Port 7102
|
* Port `7102`
|
||||||
* `POST /register` - register a user with Basic Auth
|
* `POST /register` - register a user with Basic Auth
|
||||||
* `POST /login` - get a JWT token by Basic Auth
|
* `POST /login` - get a JWT token by Basic Auth
|
||||||
* `GET /public-key` - get PEM-encoded public HS256 key
|
* `GET /pem` - get PEM-encoded public RS256 key
|
||||||
* Data persistently stored in... `data.json`, for convenience
|
* Data persistently stored in... `data.json`, for convenience
|
||||||
* HS256 key loaded from `key` file or generated on startup if missing
|
* RS256 key loaded from `private.key` file or generated on startup if missing
|
10
auth.go
10
auth.go
|
@ -13,11 +13,10 @@ func ValidEmail(email string) bool {
|
||||||
}
|
}
|
||||||
func ValidPass(pass string) bool {
|
func ValidPass(pass string) bool {
|
||||||
// TODO: Obviously, we *might* want something more sophisticated here
|
// TODO: Obviously, we *might* want something more sophisticated here
|
||||||
return true
|
return len(pass) >= 8
|
||||||
//return len(pass) >= 8
|
|
||||||
}
|
}
|
||||||
func EmailTaken(email string) bool {
|
func EmailTaken(email string) bool {
|
||||||
// TODO: Implement properly
|
// FIXME: Implement properly
|
||||||
return EmailExists(email)
|
return EmailExists(email)
|
||||||
}
|
}
|
||||||
func Register(w http.ResponseWriter, r *http.Request) {
|
func Register(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -27,12 +26,11 @@ func Register(w http.ResponseWriter, r *http.Request) {
|
||||||
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?
|
|
||||||
slog.Info("Outcome",
|
slog.Info("Outcome",
|
||||||
"email", ValidEmail(email),
|
"email", ValidEmail(email),
|
||||||
"pass", ValidPass(password),
|
"pass", ValidPass(password),
|
||||||
"taken", !EmailTaken(email))
|
"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)
|
||||||
|
@ -59,7 +57,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
|
||||||
password = strings.TrimSpace(password)
|
password = strings.TrimSpace(password)
|
||||||
user, ok := ByEmail(email)
|
user, ok := ByEmail(email)
|
||||||
if !ok || !user.PasswordFits(password) {
|
if !ok || !user.PasswordFits(password) {
|
||||||
http.Error(w, "You did something wrong", http.StatusUnauthorized)
|
http.Error(w, "you did something wrong", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
90
jwt.go
90
jwt.go
|
@ -1,9 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -15,10 +14,10 @@ import (
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var KeyFile = "key"
|
var KeyFile = "private.key"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key *ecdsa.PrivateKey
|
key *rsa.PrivateKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadKey attempts to load a private key from KeyFile.
|
// LoadKey attempts to load a private key from KeyFile.
|
||||||
|
@ -26,17 +25,25 @@ var (
|
||||||
func LoadKey() {
|
func LoadKey() {
|
||||||
// If the key doesn't exist, create it
|
// If the key doesn't exist, create it
|
||||||
if _, err := os.Stat(KeyFile); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(KeyFile); errors.Is(err, os.ErrNotExist) {
|
||||||
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
key, err = rsa.GenerateKey(rand.Reader, 4096)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error generating key", "error", err)
|
slog.Error("error generating key", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
km, err := x509.MarshalECPrivateKey(key) // Save private key to disk
|
|
||||||
|
// Save key to disk
|
||||||
|
km := x509.MarshalPKCS1PrivateKey(key)
|
||||||
|
block := pem.Block{Bytes: km, Type: "RSA PRIVATE KEY"}
|
||||||
|
f, err := os.OpenFile(KeyFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error marshalling key", "error", err)
|
slog.Error("error opening/creating file", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
f.Write(pem.EncodeToMemory(&block))
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
slog.Error("error closing file", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
os.WriteFile(KeyFile, km, 0644)
|
|
||||||
slog.Info("generated new key")
|
slog.Info("generated new key")
|
||||||
} else {
|
} else {
|
||||||
km, err := os.ReadFile(KeyFile)
|
km, err := os.ReadFile(KeyFile)
|
||||||
|
@ -44,47 +51,56 @@ func LoadKey() {
|
||||||
slog.Error("error reading key", "error", err)
|
slog.Error("error reading key", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
key, err = x509.ParseECPrivateKey(km)
|
key, err = jwt.ParseRSAPrivateKeyFromPEM(km)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error parsing key", "error", err)
|
slog.Error("error parsing key", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
slog.Info("loaded private key")
|
slog.Info("loaded private key")
|
||||||
}
|
}
|
||||||
slog.Debug("private key", "key", key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// publicKey returns our public key in PKIX, ASN.1 DER form
|
// publicKey returns our public key as PEM block
|
||||||
func publicKey(w http.ResponseWriter, r *http.Request) {
|
func publicKey(w http.ResponseWriter, r *http.Request) {
|
||||||
key_marshalled, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
key_marshalled := x509.MarshalPKCS1PublicKey(&key.PublicKey)
|
||||||
if err != nil {
|
block := pem.Block{Bytes: key_marshalled, Type: "RSA PUBLIC KEY"}
|
||||||
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)
|
pem.Encode(w, &block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateJWT(usr User) (string, error) {
|
||||||
|
t := jwt.NewWithClaims(jwt.SigningMethodRS256,
|
||||||
|
jwt.MapClaims{
|
||||||
|
"iss": "pye",
|
||||||
|
"uid": usr.Uuid,
|
||||||
|
"sub": usr.Email,
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
|
||||||
|
})
|
||||||
|
s, err := t.SignedString(key)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error creating JWT", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyToken receives a JWT and PEM-encoded public key,
|
||||||
|
// then returns whether the token is valid
|
||||||
|
func VerifyJWT(token string, publicKey []byte) bool {
|
||||||
|
_, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
key, err := jwt.ParseRSAPublicKeyFromPEM(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
})
|
||||||
|
slog.Info("Error check", "err", err)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
LoadKey()
|
LoadKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateJWT(usr User) (string, error) {
|
|
||||||
t := jwt.NewWithClaims(jwt.SigningMethodES256,
|
|
||||||
jwt.MapClaims{
|
|
||||||
"iss": "pye",
|
|
||||||
"uid": usr.Uuid,
|
|
||||||
"sub": usr.Email,
|
|
||||||
"iat": time.Now(),
|
|
||||||
"exp": time.Now().Add(time.Hour * 24 * 7),
|
|
||||||
})
|
|
||||||
s, err := t.SignedString(key)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error creating JWT", "error", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
4
main.go
4
main.go
|
@ -10,10 +10,12 @@ func main() {
|
||||||
|
|
||||||
router := http.NewServeMux()
|
router := http.NewServeMux()
|
||||||
|
|
||||||
router.HandleFunc("GET /public-key", publicKey)
|
router.HandleFunc("GET /pem", publicKey)
|
||||||
|
|
||||||
router.HandleFunc("POST /register", Register)
|
router.HandleFunc("POST /register", Register)
|
||||||
router.HandleFunc("POST /login", Login)
|
router.HandleFunc("POST /login", Login)
|
||||||
|
|
||||||
|
router.HandleFunc("GET /login", Login) // TODO: temp
|
||||||
|
|
||||||
http.ListenAndServe(":7102", router)
|
http.ListenAndServe(":7102", router)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue