aboutsummaryrefslogtreecommitdiff
path: root/kauth
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2023-10-24 08:38:40 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2023-10-24 08:38:40 +0200
commita9927763f43d59c418b264790ccee59defe8ca29 (patch)
tree5dfc4f1f1f29d1ef1cbca3406bf57d1667b93f76 /kauth
parented23d402fec268e163ec895748f43e3254ba3dfd (diff)
refactor: Rename packages for clarityv0.3.0
Diffstat (limited to 'kauth')
-rw-r--r--kauth/config.go34
-rw-r--r--kauth/cookie.go21
-rw-r--r--kauth/crypto.go54
-rw-r--r--kauth/middlewares.go92
4 files changed, 201 insertions, 0 deletions
diff --git a/kauth/config.go b/kauth/config.go
new file mode 100644
index 0000000..777a22d
--- /dev/null
+++ b/kauth/config.go
@@ -0,0 +1,34 @@
+package kauth
+
+import (
+ "encoding/hex"
+ "errors"
+ "os"
+
+ "github.com/martinlehoux/kagamigo/kcore"
+ "golang.org/x/exp/slog"
+)
+
+var (
+ ErrCookieBadLength = errors.New("cookie secret must be 32 bytes")
+)
+
+type AuthConfig struct {
+ Domain string
+ CookieSecret []byte
+}
+
+func LoadCookieSecret(cookieSecretString string) []byte {
+ cookiesSecret, err := hex.DecodeString(cookieSecretString)
+ if err != nil {
+ err = kcore.Wrap(err, "error decoding cookie secret")
+ slog.Error(err.Error())
+ os.Exit(1)
+ }
+ if len(cookiesSecret) != 32 {
+ slog.Error(ErrCookieBadLength.Error())
+ os.Exit(1)
+ }
+ slog.Info("cookie secret loaded")
+ return cookiesSecret
+}
diff --git a/kauth/cookie.go b/kauth/cookie.go
new file mode 100644
index 0000000..edc30b7
--- /dev/null
+++ b/kauth/cookie.go
@@ -0,0 +1,21 @@
+package kauth
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/martinlehoux/kagamigo/kcore"
+)
+
+func CraftCookie(id kcore.ID, config AuthConfig) http.Cookie {
+ expiresAt := time.Now().Add(24 * time.Hour)
+ cookieValue := encrypt(config.CookieSecret, fmt.Sprintf("%s:%d", id.String(), expiresAt.Unix()))
+ return http.Cookie{
+ Domain: config.Domain,
+ Name: "authentication",
+ Value: cookieValue,
+ Expires: expiresAt,
+ Path: "/",
+ }
+}
diff --git a/kauth/crypto.go b/kauth/crypto.go
new file mode 100644
index 0000000..702c9a7
--- /dev/null
+++ b/kauth/crypto.go
@@ -0,0 +1,54 @@
+package kauth
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+
+ "github.com/martinlehoux/kagamigo/kcore"
+)
+
+var (
+ ErrEncryptedTooShort = errors.New("encrypted text is too short")
+)
+
+func encrypt(secret []byte, plainText string) string {
+ block, err := aes.NewCipher(secret)
+ kcore.Expect(err, "error creating AES cipher")
+
+ aead, err := cipher.NewGCM(block)
+ kcore.Expect(err, "error creating AEAD")
+
+ nonce := make([]byte, aead.NonceSize())
+ _, err = rand.Read(nonce)
+ kcore.Expect(err, "error generating nonce")
+
+ return base64.URLEncoding.EncodeToString(aead.Seal(nonce, nonce, []byte(plainText), nil))
+}
+
+func decrypt(secret []byte, encryptedText string) (string, error) {
+ encryptedBytes, err := base64.URLEncoding.DecodeString(encryptedText)
+ if err != nil {
+ err = kcore.Wrap(err, "error decoding base64")
+ return "", err
+ }
+ block, err := aes.NewCipher(secret)
+ kcore.Expect(err, "error creating AES cipher")
+
+ aead, err := cipher.NewGCM(block)
+ kcore.Expect(err, "error creating AEAD")
+
+ nonceSize := aead.NonceSize()
+ if len(encryptedBytes) < nonceSize {
+ return "", ErrEncryptedTooShort
+ }
+ nonce, cipherText := encryptedBytes[:nonceSize], encryptedBytes[nonceSize:]
+ plainBytes, err := aead.Open(nil, nonce, cipherText, nil)
+ if err != nil {
+ err = kcore.Wrap(err, "error decrypting")
+ return "", err
+ }
+ return string(plainBytes), nil
+}
diff --git a/kauth/middlewares.go b/kauth/middlewares.go
new file mode 100644
index 0000000..23e9e71
--- /dev/null
+++ b/kauth/middlewares.go
@@ -0,0 +1,92 @@
+package kauth
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/martinlehoux/kagamigo/kcore"
+ "golang.org/x/exp/slog"
+)
+
+var (
+ ErrUserNotLoggedIn = errors.New("user not logged in")
+ ErrCookieExpired = errors.New("cookie expired")
+ ErrBadCookie = errors.New("invalid cookie")
+)
+
+type userContext struct{}
+
+func UserFromContext[U any](ctx context.Context) (U, bool) {
+ user, ok := ctx.Value(userContext{}).(U)
+ return user, ok
+}
+
+func LoginFromContext[U kcore.WithLanguage](ctx context.Context) kcore.Login[U] {
+ user, ok := UserFromContext[U](ctx)
+ return kcore.LoginFromUser(user, ok)
+}
+
+func Unauthorized(w http.ResponseWriter, err error) {
+ w.WriteHeader(http.StatusUnauthorized)
+ if err != nil {
+ _, err = w.Write([]byte(err.Error()))
+ kcore.Expect(err, "error writing response")
+ }
+}
+
+func CookieAuthMiddleware(loadUser func(context.Context, kcore.ID) (any, error), config AuthConfig) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ cookie, err := r.Cookie("authentication")
+ if errors.Is(err, http.ErrNoCookie) {
+ next.ServeHTTP(w, r)
+ return
+ }
+ kcore.Expect(err, "error reading cookie")
+
+ authentication, err := decrypt(config.CookieSecret, cookie.Value)
+ if err != nil {
+ err = kcore.Wrap(err, "error decrypting cookie")
+ slog.Warn(err.Error())
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ parts := strings.Split(authentication, ":")
+ if len(parts) != 2 {
+ slog.Warn(ErrBadCookie.Error())
+ http.Error(w, ErrBadCookie.Error(), http.StatusBadRequest)
+ return
+ }
+ userId, err := kcore.ParseID(parts[0])
+ if err != nil {
+ err = kcore.Wrap(err, "error parsing user id")
+ slog.Warn(err.Error())
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ expiresAtSeconds, err := strconv.Atoi(parts[1])
+ if err != nil {
+ err = kcore.Wrap(err, "error parsing expires at")
+ slog.Warn(err.Error())
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ expiresAt := time.Unix(int64(expiresAtSeconds), 0)
+ if time.Now().After(expiresAt) {
+ slog.Warn(ErrCookieExpired.Error())
+ http.Error(w, ErrCookieExpired.Error(), http.StatusUnauthorized)
+ return
+ }
+ user, err := loadUser(ctx, userId)
+ kcore.Expect(err, "error loading user")
+
+ ctx = context.WithValue(ctx, userContext{}, user)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+ }
+}