From 0a5c8ad3e92d5a26487c3ceb4f2ccc781abdb2ad Mon Sep 17 00:00:00 2001 From: Mikhail Klementev Date: Sat, 14 Jan 2023 13:42:44 +0000 Subject: [PATCH] Auth API --- auth/auth.go | 187 ++++++++++++++++++++++++++++++++++++++++++++ auth/go.mod | 14 ++++ auth/go.sum | 21 +++++ mastodon.nix | 28 +++++++ secrets.nix.example | 2 + 5 files changed, 252 insertions(+) create mode 100644 auth/auth.go create mode 100644 auth/go.mod create mode 100644 auth/go.sum diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..9d89ca8 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,187 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "os" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "golang.org/x/crypto/bcrypt" +) + +func lookupEmail(conn *pgx.Conn, username string) (email string, err error) { + query := "select id from accounts where username=$1" + rows, err := conn.Query(context.Background(), query, username) + if err != nil { + return + } + var ids []int + for rows.Next() { + var id int + rows.Scan(&id) + ids = append(ids, id) + } + rows.Close() + if rows.Err() != nil { + return + } + + for _, id := range ids { + query = "select email from users where account_id=$1" + err = conn.QueryRow(context.Background(), query, id).Scan(&email) + if err == nil && email != "" { + return + } + } + return +} + +func lookupUsername(conn *pgx.Conn, email string) (username string, err error) { + query := "select account_id from users where email=$1" + var id int + err = conn.QueryRow(context.Background(), query, email).Scan(&id) + if err != nil { + return + } + + query = "select username from accounts where id=$1" + err = conn.QueryRow(context.Background(), query, id).Scan(&username) + return +} + +func auth(conn *pgx.Conn, email, password string) (err error) { + query := "select account_id,confirmed_at,approved,disabled," + + "encrypted_password from users where email=$1" + + var id int + var confirmed pgtype.Timestamptz + var approved, disabled bool + var hash string + + err = conn.QueryRow(context.Background(), query, email). + Scan(&id, &confirmed, &approved, &disabled, &hash) + if err != nil { + return + } + + if !confirmed.Valid { + return errors.New("account is not confirmed") + } + if !approved { + return errors.New("account is not approved") + } + if disabled { + return errors.New("account disabled") + } + + query = "select suspended_at from accounts where id=$1" + + var suspended pgtype.Timestamptz + + err = conn.QueryRow(context.Background(), query, id).Scan(&suspended) + if err != nil { + return + } + + if suspended.Valid { + return errors.New("account suspended") + } + + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) +} + +type handler struct { + Banlist map[string]bool +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + _, banned := h.Banlist[r.Header.Get("X-Forwarded-For")] + if banned { + return + } + + if r.Method != "POST" { + fmt.Println("method is not POST", r) + return + } + + var params struct { + Secret, Username, Email, Password string + } + + err := json.NewDecoder(r.Body).Decode(¶ms) + if err != nil { + fmt.Println(err, r) + return + } + + if params.Secret != os.Getenv("AUTH_SECRET") { + fmt.Println("wrong secret", r) + h.Banlist[r.Header.Get("X-Forwarded-For")] = true + return + } + + var response struct { + Username, Email, Error string + } + + defer func() { + buf, _ := json.Marshal(response) + w.Write(buf) + }() + + path := "dbname=" + os.Getenv("DATABASE") + conn, err := pgx.Connect(context.Background(), path) + if err != nil { + response.Error = fmt.Sprint(err) + return + } + defer conn.Close(context.Background()) + + if params.Email == "" { + params.Email, err = lookupEmail(conn, params.Username) + if err != nil { + response.Error = fmt.Sprint(err) + return + } + } + + if params.Username == "" { + params.Username, err = lookupUsername(conn, params.Email) + if err != nil { + response.Error = fmt.Sprint(err) + return + } + } + + err = auth(conn, params.Email, params.Password) + if err != nil { + response.Error = fmt.Sprint(err) + return + } + + response.Username = params.Username + response.Email = params.Email +} + +func main() { + if os.Getenv("AUTH_SECRET") == "" { + panic("AUTH_SECRET is empty") + } + + socket, err := net.Listen("unix", os.Getenv("SOCKET")) + if err != nil { + panic(err) + } + os.Chmod(os.Getenv("SOCKET"), 0770) + + server := http.Server{ + Handler: &handler{Banlist: make(map[string]bool)}, + } + server.Serve(socket) +} diff --git a/auth/go.mod b/auth/go.mod new file mode 100644 index 0000000..d335f00 --- /dev/null +++ b/auth/go.mod @@ -0,0 +1,14 @@ +module code.dumpstack.io/infra/lor.sh/auth + +go 1.19 + +require ( + github.com/jackc/pgx/v5 v5.2.0 + golang.org/x/crypto v0.5.0 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + golang.org/x/text v0.6.0 // indirect +) diff --git a/auth/go.sum b/auth/go.sum new file mode 100644 index 0000000..ff6c80d --- /dev/null +++ b/auth/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8= +github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/mastodon.nix b/mastodon.nix index 940596a..5a40ef8 100644 --- a/mastodon.nix +++ b/mastodon.nix @@ -64,10 +64,34 @@ let $@ ''; + auth = pkgs.buildGoModule rec { + name = "auth"; + src = ./auth; + vendorHash = "sha256-cLn1tZL+LVMmSpLZYA7uRkEW7eFWGf+NFdvBEvQtjH4="; + }; + bucket = secrets.backup.bucket; domainName = "lor.sh"; in { + systemd.services."mastodon-auth" = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + environment = { + SOCKET = "/var/run/mastodon-auth/auth.socket"; + DATABASE = "mastodon"; + AUTH_SECRET = secrets.authSecret; + }; + serviceConfig = { + Restart = "always"; + RestartSec = 30; + ExecStart = "${auth}/bin/auth"; + User = "mastodon"; + RuntimeDirectory = "mastodon-auth"; + RuntimeDirectoryMode = "0750"; + }; + }; + services.postgresqlBackup = { enable = true; databases = [ "mastodon" ]; @@ -110,6 +134,10 @@ in { reverse_proxy unix//run/mastodon-streaming/streaming.socket } + handle /api/v0/auth* { + reverse_proxy unix//run/mastodon-auth/auth.socket + } + handle { reverse_proxy unix//run/mastodon-web/web.socket } diff --git a/secrets.nix.example b/secrets.nix.example index ba40413..b34841b 100644 --- a/secrets.nix.example +++ b/secrets.nix.example @@ -12,6 +12,8 @@ bucket = ""; }; + authSecret = ""; + smtpPassword = ""; vapidPublicKey = "";