Skip to content

Commit

Permalink
Auth0 integration
Browse files Browse the repository at this point in the history
- Add auth endpoints
- Sessions
- Add ability to remote debug with `make debug`
- README cleanup
- Remove old auth code
- `go mod tidy`
- Central `init()` function in the main.go file
  • Loading branch information
szabolcs-horvath committed Jan 2, 2025
1 parent 376d6d1 commit ca5691b
Show file tree
Hide file tree
Showing 12 changed files with 304 additions and 54 deletions.
3 changes: 2 additions & 1 deletion .github/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
PORT="6969"
DB_FILE="it/nutrition-tracker-test.db"
DB_FILE="it/nutrition-tracker-test.db"
AUTH0_DISABLED="true"
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum
Expand Down
17 changes: 14 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
SQLITE_DB_FILE ?= sqlite/nutrition-tracker.db
IT_SQLITE_DB_FILE ?= it/nutrition-tracker-test.db
SQLITE_MIGRATIONS_DIR ?= sqlite/migrations
GOCOVERDIR ?= coverage
CGO_ENABLED=1 # Required for sqlite3 driver

SQLC_VERSION ?= v1.27.0
GOLANG_MIGRATE_VERSION ?= v4.18.1
STRINGER_VERSION ?= v0.28.0
DELVE_VERSION ?= latest
HTMX_VERSION ?= 2.0.3
BOOTSTRAP_VERSION ?= 5.3.3
BOOTSTRAP_ICONS_VERSION ?= 1.11.3
GOCOVERDIR ?= coverage
CGO_ENABLED=1 # Required for sqlite3 driver

install-go-deps:
go install -v github.com/sqlc-dev/sqlc/cmd/sqlc@$(SQLC_VERSION)
Expand All @@ -21,6 +23,11 @@ init-db: migrate-up
build: sqlc generate
go build -o out/nutrition-tracker -mod=readonly

debug: sqlc generate
go install -v github.com/go-delve/delve/cmd/dlv@$(DELVE_VERSION)
go build -o out/nutrition-tracker-debug -mod=readonly -gcflags="all=-N -l"
dlv --listen=:443 --headless=true --api-version=2 --accept-multiclient exec ./out/nutrition-tracker-debug

sqlc:
sqlc generate

Expand All @@ -30,6 +37,8 @@ generate:
clean:
rm -rf generated out



ut: sqlc
rm -f $(GOCOVERDIR)/ut-coverage.out
mkdir -p $(GOCOVERDIR)
Expand Down Expand Up @@ -57,6 +66,8 @@ coverage-ut: ut
coverage-it: it
go tool cover -html=$(GOCOVERDIR)/it-coverage.out



create-migration:
ifneq ($(MIGRATION_NAME),)
migrate create -dir sqlite/migrations -ext .sql $(MIGRATION_NAME)
Expand Down Expand Up @@ -99,6 +110,7 @@ else
endif



download-htmx:
cd ./web/static/vendor/htmx; \
curl -O https://unpkg.com/htmx.org@$(HTMX_VERSION)/dist/htmx.min.js; \
Expand All @@ -119,4 +131,3 @@ download-bootstrap-icons:
rm -rf bootstrap-icons-$(BOOTSTRAP_ICONS_VERSION); \
rm bootstrap-icons-$(BOOTSTRAP_ICONS_VERSION).zip; \
cd -;

31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,29 @@ This project's purpose is to help Kinga track and plan her diet.
```

### Set up
1. #### Initialize the database
1. #### Create a `.env` file with the necessary environment variables
```shell
PORT="80"
DB_FILE="sqlite/nutrition-tracker.db"
AUTH0_DOMAIN="<AUTH0_DOMAIN>"
AUTH0_CLIENT_ID="<AUTH0_CLIENT_ID>"
AUTH0_CLIENT_SECRET="<AUTH0_CLIENT_SECRET>"
AUTH0_CALLBACK_URL="https://nutrition-tracking.com/auth/callback" # Should be the same as the one set in the Auth0 dashboard
SESSION_KEY="<KEY_TO_AUTHENTICATE_THE_COOKIE_STORE>" # Should ideally be at least 64 bytes of random data
```
2. #### Initialize the database
```shell
make init-db
```

2. #### Build the project
3. #### Build the project
```shell
make build
```

## Running
###### (The .env file only needs to be specified if it is not in the project root)
```shell
out/nutrition-tracker <path-to-.env-file>
out/nutrition-tracker <PATH_TO_YOUR_ENV_FILE>
```

## Checking test coverage
Expand All @@ -49,7 +59,16 @@ make unit-coverage
make integration-coverage
```

## Adding a new migration
## Migrations
###### Create a new migration
```shell
make create-migration MIGRATION_NAME=<MIGRATION_NAME>
```
###### Apply all migrations
```shell
make migrate-up
```
###### Rollback the last migration
```shell
make create-migration MIGRATION_NAME=<migration_name>
make migrate-down-1
```
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ module github.com/szabolcs-horvath/nutrition-tracker
go 1.23.1

require (
github.com/auth0/go-jwt-middleware/v2 v2.2.2
github.com/coreos/go-oidc/v3 v3.11.0
github.com/deckarep/golang-set/v2 v2.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.4.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/oauth2 v0.24.0
)

require (
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
)
22 changes: 14 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
github.com/auth0/go-jwt-middleware/v2 v2.2.2 h1:vrvkFZf72r3Qbt45KLjBG3/6Xq2r3NTixWKu2e8de9I=
github.com/auth0/go-jwt-middleware/v2 v2.2.2/go.mod h1:4vwxpVtu/Kl4c4HskT+gFLjq0dra8F1joxzamrje6J0=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
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/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
38 changes: 19 additions & 19 deletions http_server/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ package middleware
import (
"bytes"
"context"
"github.com/auth0/go-jwt-middleware/v2"
"github.com/auth0/go-jwt-middleware/v2/jwks"
"github.com/auth0/go-jwt-middleware/v2/validator"
"github.com/google/uuid"
"github.com/szabolcs-horvath/nutrition-tracker/util"
"io"
"log/slog"
"net/http"
"net/url"
"slices"
"time"
)
Expand Down Expand Up @@ -78,21 +75,24 @@ func LogIncomingRequest(next http.Handler) http.Handler {
})
}

func Authenticate(audience string, domain string) Middleware {
return func(next http.Handler) http.Handler {
issuerURL, _ := url.Parse("https://" + domain + "/")
provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)

jwtValidator, _ := validator.New(
provider.KeyFunc,
validator.RS256,
issuerURL.String(),
[]string{audience},
)

middleware := jwtmiddleware.New(jwtValidator.ValidateToken)
return middleware.CheckJWT(next)
}
func IsAuthenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := util.CookieStoreInstance.Get(r, "auth-session")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["profile"] == nil {
if r.URL.Path == "/auth/login" || r.URL.Path == "/auth/callback" {
slog.Warn("[IsAuthenticated] session.Values[\"profile\"] is nil", "PATH", r.URL.Path)
} else {
slog.Info("[IsAuthenticated] redirecting to the login path...", "PATH", r.URL.Path)
http.Redirect(w, r, "/auth/login", http.StatusSeeOther)
return
}
}
next.ServeHTTP(w, r)
})
}

func LogCompletedRequest(next http.Handler) http.Handler {
Expand Down
119 changes: 119 additions & 0 deletions http_server/routes/auth/routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package auth

import (
"crypto/rand"
"encoding/base64"
"github.com/szabolcs-horvath/nutrition-tracker/util"
"net/http"
"net/url"
)

const Prefix = "/auth"

func Routes() map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"GET /login": loginHandler,
"GET /callback": callbackHandler,
"GET /logout": logoutHandler,
}
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
state, err := generateRandomState()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

session, err := util.CookieStoreInstance.New(r, "auth-session")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["state"] = state
if err = session.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

http.Redirect(w, r, util.AuthenticatorInstance.AuthCodeURL(state), http.StatusTemporaryRedirect)
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
token, err := util.AuthenticatorInstance.Exchange(r.Context(), r.FormValue("code"))
if err != nil {
http.Error(w, "Failed to exchange an authorization code for a token.", http.StatusUnauthorized)
return
}
idToken, err := util.AuthenticatorInstance.VerifyIDToken(r.Context(), token)
if err != nil {
http.Error(w, "Failed to verify ID Token.", http.StatusInternalServerError)
return
}
var profile map[string]interface{}
if err = idToken.Claims(&profile); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

session, err := util.CookieStoreInstance.Get(r, "auth-session")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["access_token"] = token.AccessToken
session.Values["profile"] = profile
if err = session.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

http.Redirect(w, r, "/htmx", http.StatusTemporaryRedirect)
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
logoutUrl, err := url.Parse("https://" + util.SafeGetEnv("AUTH0_DOMAIN") + "/v2/logout")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
returnTo, err := url.Parse(scheme + "://" + r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

parameters := url.Values{}
parameters.Add("returnTo", returnTo.String())
parameters.Add("client_id", util.SafeGetEnv("AUTH0_CLIENT_ID"))
logoutUrl.RawQuery = parameters.Encode()

session, err := util.CookieStoreInstance.Get(r, "auth-session")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
clear(session.Values)
if err = session.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

http.Redirect(w, r, logoutUrl.String(), http.StatusTemporaryRedirect)
}

func generateRandomState() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}

state := base64.StdEncoding.EncodeToString(b)

return state, nil
}
9 changes: 9 additions & 0 deletions http_server/routes/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package routes

import (
"net/http"
)

func RootHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/htmx", http.StatusTemporaryRedirect)
}
Loading

0 comments on commit ca5691b

Please sign in to comment.