Initial commit
This commit is contained in:
commit
d1c7d522af
16 changed files with 338 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -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.
|
9
README.md
Normal file
9
README.md
Normal file
|
@ -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
|
46
cmd/microauth/main.go
Normal file
46
cmd/microauth/main.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
14
go.mod
Normal file
14
go.mod
Normal file
|
@ -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
|
||||
)
|
10
go.sum
Normal file
10
go.sum
Normal file
|
@ -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=
|
10
internal/microauth/delete.go
Normal file
10
internal/microauth/delete.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package microauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func delete(username string) (int, error) {
|
||||
return http.StatusInternalServerError, fmt.Errorf("not yet implemented")
|
||||
}
|
19
internal/microauth/login.go
Normal file
19
internal/microauth/login.go
Normal file
|
@ -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")
|
||||
}
|
8
internal/microauth/models.go
Normal file
8
internal/microauth/models.go
Normal file
|
@ -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)
|
23
internal/microauth/register.go
Normal file
23
internal/microauth/register.go
Normal file
|
@ -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
|
||||
}
|
30
internal/microauth/server.go
Normal file
30
internal/microauth/server.go
Normal file
|
@ -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)
|
||||
}
|
12
internal/microauth/update.go
Normal file
12
internal/microauth/update.go
Normal file
|
@ -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")
|
||||
}
|
||||
}
|
59
internal/microauth/validation.go
Normal file
59
internal/microauth/validation.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
1
thunder-tests/thunderActivity.json
Normal file
1
thunder-tests/thunderActivity.json
Normal file
|
@ -0,0 +1 @@
|
|||
[]
|
16
thunder-tests/thunderCollection.json
Normal file
16
thunder-tests/thunderCollection.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
1
thunder-tests/thunderEnvironment.json
Normal file
1
thunder-tests/thunderEnvironment.json
Normal file
|
@ -0,0 +1 @@
|
|||
[]
|
59
thunder-tests/thunderclient.json
Normal file
59
thunder-tests/thunderclient.json
Normal file
|
@ -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": []
|
||||
}
|
||||
]
|
Loading…
Reference in a new issue