diff options
| author | mo khan <mo@mokhan.ca> | 2022-05-10 18:19:34 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2022-05-10 18:19:36 -0600 |
| commit | 4aee5f9b3e00bf0c94099f18d48fc71ae18d4519 (patch) | |
| tree | eedf1ef21e132427c16e8a0df9cf64bb0e56c7cf | |
complete auth0 guide
* https://auth0.com/docs/quickstart/backend/golang
| -rw-r--r-- | .env | 2 | ||||
| -rw-r--r-- | go.mod | 14 | ||||
| -rw-r--r-- | go.sum | 15 | ||||
| -rw-r--r-- | main.go | 60 | ||||
| -rw-r--r-- | pkg/middleware/jwt.go | 72 |
5 files changed, 163 insertions, 0 deletions
@@ -0,0 +1,2 @@ +AUTH0_DOMAIN='cmd0-dev.us.auth0.com' +AUTH0_AUDIENCE='https://api.cmdzero.io' @@ -0,0 +1,14 @@ +module github.com/xlgmokha/api-auth0 + +go 1.18 + +require ( + github.com/auth0/go-jwt-middleware/v2 v2.0.1 + github.com/joho/godotenv v1.4.0 +) + +require ( + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect +) @@ -0,0 +1,15 @@ +github.com/auth0/go-jwt-middleware/v2 v2.0.1 h1:zAgDKL7nsfVBFl31GGxsSXkhuRzYe1fVtJcO3aMSrFU= +github.com/auth0/go-jwt-middleware/v2 v2.0.1/go.mod h1:kDt7JgUuDEp1VutfUmO4ZxBLL51vlNu/56oDfXc5E0Y= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= @@ -0,0 +1,60 @@ +package main + +import ( + "log" + "net/http" + + jwtmiddleware "github.com/auth0/go-jwt-middleware/v2" + "github.com/auth0/go-jwt-middleware/v2/validator" + "github.com/joho/godotenv" + "github.com/xlgmokha/api-auth0/pkg/middleware" +) + +func main() { + if err := godotenv.Load(); err != nil { + log.Fatal(err) + } + + router := http.NewServeMux() + + router.Handle("/api/public", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"public"}`)) + })) + + router.Handle("/api/private", middleware.EnsureValidToken()( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") + w.Header().Set("Access-Control-Allow-Headers", "Authorization") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"private"}`)) + }), + )) + + router.Handle("/api/private-scoped", middleware.EnsureValidToken()( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") + w.Header().Set("Access-Control-Allow-Headers", "Authorization") + + w.Header().Set("Content-Type", "application/json") + + token := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims) + claims := token.CustomClaims.(*middleware.CustomClaims) + if !claims.HasScope("read:messages") { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(`{"message":"insufficient scope."}`)) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"private-scoped"}`)) + }), + )) + + log.Fatal(http.ListenAndServe("0.0.0.0:3000", router)) +} diff --git a/pkg/middleware/jwt.go b/pkg/middleware/jwt.go new file mode 100644 index 0000000..a58eff6 --- /dev/null +++ b/pkg/middleware/jwt.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "context" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" + + jwtmiddleware "github.com/auth0/go-jwt-middleware/v2" + "github.com/auth0/go-jwt-middleware/v2/jwks" + "github.com/auth0/go-jwt-middleware/v2/validator" +) + +type CustomClaims struct { + Scope string `json:"scope"` +} + +func (c CustomClaims) Validate(ctx context.Context) error { + return nil +} + +func (c CustomClaims) HasScope(expectedScope string) bool { + result := strings.Split(c.Scope, " ") + for i := range result { + if result[i] == expectedScope { + return true + } + } + return false +} + +func EnsureValidToken() func(next http.Handler) http.Handler { + issuerURL, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/") + if err != nil { + log.Fatal(err) + } + + provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute) + jwtValidator, err := validator.New( + provider.KeyFunc, + validator.RS256, + issuerURL.String(), + []string{os.Getenv("AUTH0_AUDIENCE")}, + validator.WithCustomClaims( + func() validator.CustomClaims { + return &CustomClaims{} + }, + ), + validator.WithAllowedClockSkew(time.Minute), + ) + if err != nil { + log.Fatal(err) + } + + errorHandler := func(w http.ResponseWriter, r *http.Request, err error) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"message":"Failed to validate JWT."}`)) + } + + middleware := jwtmiddleware.New( + jwtValidator.ValidateToken, + jwtmiddleware.WithErrorHandler(errorHandler), + ) + + return func(next http.Handler) http.Handler { + return middleware.CheckJWT(next) + } +} |
