diff --git a/README.md b/README.md index 6fc34c1..780edb7 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,92 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/go-phings/umbrella.svg)](https://pkg.go.dev/github.com/go-phings/umbrella) [![Go Report Card](https://goreportcard.com/badge/github.com/go-phings/umbrella)](https://goreportcard.com/report/github.com/go-phings/umbrella) -Package umbrella is meant to work as a authentication mechanism for HTTP endpoint. It can be used to wrap HTTP server handler and check if any user has been logged in. In addition to that, module serves its own endpoints for registration, activations, login and logout. +Package umbrella provides a simple authentication mechanism for an HTTP endpoint. With it, you can wrap any endpoint that should have its access restricted. In addition, it provides additional its own handler for registering new user, activating it and, naturally, signing in and out. -## Example usage +> ⚠️ The project is in beta, under heavy construction, and may introduce breaking changes in releases before `v1.0.0`. -Have a butcher's at `main_test.go` to see a sample usage. +## Table of Contents +1. [Sample code](#sample-code) +2. [Database connection](#database-connection) +3. [User model](#user-model) +4. [Features + Roadmap](#features) +5. [Motivation](#motivation) + +## Sample code +The following code snippet shows how the module can be used. + +```go +// database connection +dbConn, _ = sql.Open("postgres", "host=localhost user=myuser password=mypass port=5432 dbname=mydb sslmode=disable") + +// umbrella controller +u := NewUmbrella(dbConn, "tblprefix_", &JWTConfig{ + Key: "SomeSecretKey--.", + Issuer: "SomeIssuer", + ExpirationMinutes: 15, +}, nil) + +// create db tables +_ := u.CreateDBTables() + +// http server +// uri with registration, activation, login (returns auth token), logout endpoint +http.Handle("/umbrella/", u.GetHTTPHandler("/umbrella/")) +// restricted stuff that requires signing in (a token in http header) +http.Handle("/restricted_stuff/", u.GetHTTPHandlerWrapper( + getRestrictedStuffHTTPHandler(), + umbrella.HandlerConfig{}, +)) +http.ListenAndServe(":8001", nil) + +// wrap http handler with a check for logged user +func getRestrictedStuffHTTPHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userID := umbrella.GetUserIDFromRequest(r) + if userID != 0 { + w.WriteHeader(http.StatusOK) + w.Write([]byte("RestrictedAreaContent")) + } else { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("NoAccess")) + } + }) +} +``` + +## Database connection +The module needs to store users and sessions in the database. If not attached otherwise, [struct-db-postgres](https://github.com/go-phings/struct-db-postgres) will be used as an ORM by default. + +To attach a custom ORM, it needs to implement the `ORM` interface. In the `orm.go` file, there is an example on how the previously mentioned DB module is wrapped in a struct that has all the methods required by `ORM` interface. +Pass `&UmbrellaConfig` instance with `ORM` field to the `NewUmbrella` constructor to attach your object. + +## User model +Umbrella comes with its own `User` and `Session` structs. However, there might be a need to use a different user model containing more fields, with a separate ORM. Hence, similarily to previous paragraph, an interface called `UserInterface` has been defined. A custom user struct must implement that interface's methods. + +To do the above: +1. set `NoUserConstructor` to true in the `&UmbrellaConfig` argument when calling `NewUmbrella` +2. create new `&umbrella.Interfaces` object with `User` field and attach it to `Interfaces` field of umbrella controller. + +## Features +- [X] Wrapper support for any HTTP handler +- [X] Data storage in PostgreSQL database by default +- [X] Customisable database driver and ORM +- [X] Flexible User model +- [X] Optional endpoints for sign-in (creating session objects with access tokens) and sign-out (deactivating sessions and tokens) +- [X] Optional endpoints for user registration and activation +- [X] Hooks triggered after successful actions like registration or sign-in +- [X] Option to use cookies instead of the authorisation header +- [X] Support for redirection headers after successful or failed sign-in attempts +- [X] User struct validation during user registration +- [X] Customisable tag names for field validation + +### Roadmap +- [ ] Simple permission system + +## Motivation +While building a backend REST API for a colleague in retail, I needed a simple way to secure HTTP endpoints with basic authentication. The goal was straightforward: users would log in with an email and password, receive a token with an expiration time, and use it to interact with the backend API. A frontend application handled this flow. + +A few months later, I was approached with a similar request, this time for an internal company application that required user registration and activation. + +More recently, as I began developing a platform for prototyping where I used the code, I realised that this small yet essential piece of code could be valuable to others. And so, I decided to share it here. diff --git a/go.mod b/go.mod index 33014e5..32ff0a3 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/go-phings/umbrella go 1.23.4 require ( - github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-phings/struct-db-postgres v0.7.0 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 github.com/lib/pq v1.10.9 github.com/ory/dockertest/v3 v3.11.0 diff --git a/go.sum b/go.sum index ac48300..32fa832 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,6 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= @@ -35,6 +33,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= diff --git a/internal.go b/internal.go index 988a8b8..fc40303 100644 --- a/internal.go +++ b/internal.go @@ -9,7 +9,7 @@ import ( "regexp" "time" - "github.com/dgrijalva/jwt-go" + "github.com/golang-jwt/jwt" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) diff --git a/umbrella.go b/umbrella.go index f17baf0..00b73c4 100644 --- a/umbrella.go +++ b/umbrella.go @@ -9,8 +9,8 @@ import ( "regexp" "strings" - "github.com/dgrijalva/jwt-go" sdb "github.com/go-phings/struct-db-postgres" + "github.com/golang-jwt/jwt" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" )