commit d1c7d522af73c08cdb5534953ac040617e119c5b Author: William Brawner Date: Wed Oct 26 22:29:02 2022 -0600 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e9b49d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 William Brawner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..206b841 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Microauth + +The goal of this project is to provide myself with a microservice responsible for all things related to authentication for future web apps I build, including the following features: + +- login +- registration +- password reset +- session management (creation, validation, revocation, etc) +- user deletion \ No newline at end of file diff --git a/cmd/microauth/main.go b/cmd/microauth/main.go new file mode 100644 index 0000000..01841bc --- /dev/null +++ b/cmd/microauth/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" + "github.com/wbrawner/microauth/internal/microauth" +) + +const FLAG_API_KEY string = "api-key" +const FLAG_PORT string = "port" + +func main() { + app := &cli.App{ + Name: "microauth", + Usage: "a microservice for authenticating users", + // TODO: Add flags for database connection parameters + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: FLAG_API_KEY, + Aliases: []string{"a"}, + Usage: "`KEY` to use when authenticating with the server", + EnvVars: []string{ + "MICROAUTH_API_KEY", + }, + }, + &cli.IntFlag{ + Name: FLAG_PORT, + Aliases: []string{"p"}, + Value: 8080, + Usage: "`PORT` for server to listen on", + EnvVars: []string{ + "MICROAUTH_PORT", + }, + }, + }, + Action: func(c *cli.Context) error { + return microauth.StartServer(c.Int(FLAG_PORT), c.String(FLAG_API_KEY)) + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6b6e520 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/wbrawner/microauth + +go 1.19 + +require ( + github.com/urfave/cli/v2 v2.20.3 + golang.org/x/crypto v0.1.0 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..32ca788 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.20.3 h1:lOgGidH/N5loaigd9HjFsOIhXSTrzl7tBpHswZ428w4= +github.com/urfave/cli/v2 v2.20.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= diff --git a/internal/microauth/delete.go b/internal/microauth/delete.go new file mode 100644 index 0000000..dfc85a2 --- /dev/null +++ b/internal/microauth/delete.go @@ -0,0 +1,10 @@ +package microauth + +import ( + "fmt" + "net/http" +) + +func delete(username string) (int, error) { + return http.StatusInternalServerError, fmt.Errorf("not yet implemented") +} diff --git a/internal/microauth/login.go b/internal/microauth/login.go new file mode 100644 index 0000000..9eb212c --- /dev/null +++ b/internal/microauth/login.go @@ -0,0 +1,19 @@ +package microauth + +import ( + "fmt" + "net/http" + + "golang.org/x/crypto/bcrypt" +) + +func login(user User) (int, []byte, error) { + passwordBytes := []byte(user.Password) + for _, u := range users { + if bcrypt.CompareHashAndPassword([]byte(u.Password), passwordBytes) == nil { + return http.StatusAccepted, []byte{}, nil + } + } + // TODO: Generate a session token, persist in database, and return in response + return http.StatusUnauthorized, []byte{}, fmt.Errorf("credentials invalid") +} diff --git a/internal/microauth/models.go b/internal/microauth/models.go new file mode 100644 index 0000000..03cf668 --- /dev/null +++ b/internal/microauth/models.go @@ -0,0 +1,8 @@ +package microauth + +type User struct { + Name string `json:"name"` + Password string `json:"password"` +} + +type UserCallback func(user User) (int, []byte, error) diff --git a/internal/microauth/register.go b/internal/microauth/register.go new file mode 100644 index 0000000..d9920e6 --- /dev/null +++ b/internal/microauth/register.go @@ -0,0 +1,23 @@ +package microauth + +import ( + "fmt" + "net/http" + + "golang.org/x/crypto/bcrypt" +) + +func register(user User) (int, []byte, error) { + for _, u := range users { + if user.Name == u.Name { + return http.StatusBadRequest, []byte{}, fmt.Errorf("username already taken") + } + } + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + return http.StatusInternalServerError, []byte{}, fmt.Errorf("failed to hash password: %v", err) + } + users = append(users, User{Name: user.Name, Password: string(hashedPassword)}) + // TODO: Generate a session token, persist in database, and return in response + return http.StatusNoContent, []byte{}, nil +} diff --git a/internal/microauth/server.go b/internal/microauth/server.go new file mode 100644 index 0000000..3b9b4a8 --- /dev/null +++ b/internal/microauth/server.go @@ -0,0 +1,30 @@ +package microauth + +import ( + "fmt" + "log" + "net/http" +) + +// TODO: Save this in a database, or maybe wrap in an interface to be able to use an in-memory store for tests +var users []User + +func StartServer(port int, apiKey string) error { + http.HandleFunc("/login", validateApiKey(apiKey, validateHttpMethod(http.MethodPost, validateUserBody(login)))) + http.HandleFunc("/register", validateApiKey(apiKey, validateHttpMethod(http.MethodPost, validateUserBody(register)))) + http.HandleFunc("/update", validateApiKey(apiKey, validateHttpMethod(http.MethodPut, func(w http.ResponseWriter, r *http.Request) { + validateUserBody(update(r.URL.Query().Get("user")))(w, r) + }))) + http.HandleFunc("/delete", validateApiKey(apiKey, validateHttpMethod(http.MethodDelete, func(w http.ResponseWriter, r *http.Request) { + returnCode, err := delete(r.URL.Query().Get("user")) + if err != nil { + http.Error(w, fmt.Sprintf("%v", err), returnCode) + return + } + + w.WriteHeader(returnCode) + w.Write([]byte{}) + }))) + log.Printf("microauth server listening on :%d", port) + return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) +} diff --git a/internal/microauth/update.go b/internal/microauth/update.go new file mode 100644 index 0000000..bb3b134 --- /dev/null +++ b/internal/microauth/update.go @@ -0,0 +1,12 @@ +package microauth + +import ( + "fmt" + "net/http" +) + +func update(username string) UserCallback { + return func(user User) (int, []byte, error) { + return http.StatusInternalServerError, []byte{}, fmt.Errorf("not yet implemented") + } +} diff --git a/internal/microauth/validation.go b/internal/microauth/validation.go new file mode 100644 index 0000000..8c5cd1f --- /dev/null +++ b/internal/microauth/validation.go @@ -0,0 +1,59 @@ +package microauth + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" +) + +func validateApiKey(apiKey string, callback http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("X-API-Key") + if apiKey != authHeader { + http.Error(w, "invalid api key", http.StatusForbidden) + } else { + callback(w, r) + } + } +} + +func validateHttpMethod(allowedMethod string, callback http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != allowedMethod { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } else { + callback(w, r) + } + } +} + +func validateUserBody(callback UserCallback) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "", http.StatusInternalServerError) + log.Printf("error: failed to read request body: %v", err) + return + } + + var user User + err = json.Unmarshal(body, &user) + if err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + log.Printf("error: failed to unmarshal login request: %v", err) + return + } + + returnCode, responseBody, err := callback(user) + + if err != nil { + http.Error(w, fmt.Sprintf("%v", err), returnCode) + return + } + + w.WriteHeader(returnCode) + w.Write(responseBody) + } +} diff --git a/thunder-tests/thunderActivity.json b/thunder-tests/thunderActivity.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/thunder-tests/thunderActivity.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/thunder-tests/thunderCollection.json b/thunder-tests/thunderCollection.json new file mode 100644 index 0000000..98accb6 --- /dev/null +++ b/thunder-tests/thunderCollection.json @@ -0,0 +1,16 @@ +[ + { + "_id": "6365a485-1944-4c50-83f4-e70a16caef30", + "colName": "Default Collection", + "created": "2022-10-27T03:37:35.592Z", + "sortNum": 10000, + "folders": [], + "settings": { + "headers": [], + "tests": [], + "options": { + "baseUrl": "http://localhost:8080" + } + } + } +] \ No newline at end of file diff --git a/thunder-tests/thunderEnvironment.json b/thunder-tests/thunderEnvironment.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/thunder-tests/thunderEnvironment.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/thunder-tests/thunderclient.json b/thunder-tests/thunderclient.json new file mode 100644 index 0000000..0e455fd --- /dev/null +++ b/thunder-tests/thunderclient.json @@ -0,0 +1,59 @@ +[ + { + "_id": "7ae231ce-802e-4331-b460-7a9b7212bad2", + "colId": "6365a485-1944-4c50-83f4-e70a16caef30", + "containerId": "", + "name": "Login", + "url": "/login", + "method": "POST", + "sortNum": 10000, + "created": "2022-10-27T03:37:49.269Z", + "modified": "2022-10-27T03:40:09.670Z", + "headers": [], + "params": [], + "body": { + "type": "json", + "raw": "{\n \"username\": \"test\",\n \"password\": \"test\"\n}", + "form": [] + }, + "tests": [] + }, + { + "_id": "f60dffeb-3908-4216-9c6c-3c16862ddb10", + "colId": "6365a485-1944-4c50-83f4-e70a16caef30", + "containerId": "", + "name": "Register", + "url": "/register", + "method": "POST", + "sortNum": 20000, + "created": "2022-10-27T04:00:13.863Z", + "modified": "2022-10-27T04:00:24.493Z", + "headers": [], + "params": [], + "body": { + "type": "json", + "raw": "{\n \"username\": \"test\",\n \"password\": \"test\"\n}", + "form": [] + }, + "tests": [] + }, + { + "_id": "18794265-35e5-46e6-b6f5-91f7e3936060", + "colId": "6365a485-1944-4c50-83f4-e70a16caef30", + "containerId": "", + "name": "Update", + "url": "/delete", + "method": "DELETE", + "sortNum": 30000, + "created": "2022-10-27T04:20:26.185Z", + "modified": "2022-10-27T04:27:22.760Z", + "headers": [], + "params": [], + "body": { + "type": "json", + "raw": "{\n \"username\": \"test\",\n \"password\": \"test\"\n}", + "form": [] + }, + "tests": [] + } +] \ No newline at end of file