diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index daccf8e..0000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-# Common Logic
-machine: &machine-cfg
- image: ubuntu-2004:202107-02
-
-defaults: &defaults
- steps:
- - attach_workspace:
- at: ~/
- - run:
- name: Prepare environment variables
- command: |
- cd $AUTH0_CFG
- mv .env.example .env
- sed -i 's|{DOMAIN}|'$auth0_domain'|g' .env
- sed -i 's|{API_IDENTIFIER}|'$api_identifier'|g' .env
- - run:
- name: Background Server
- command: cd $AUTH0_CFG && sh exec.sh
- background: true
- - run:
- name: Wait until server is online
- command: |
- until $(curl --silent --head --output /dev/null --fail http://localhost:3010/api/public); do
- sleep 5
- done
- - run:
- name: Prepare tests
- command: |
- cd test
- echo "AUTH0_DOMAIN=$auth0_domain" >> .env
- echo "API_IDENTIFIER=$api_identifier" >> .env
- echo "AUTH0_CLIENT_ID_1=$client_id_scopes_none" >> .env
- echo "AUTH0_CLIENT_SECRET_1=$client_secret_scopes_none" >> .env
- echo "AUTH0_CLIENT_ID_2=$client_id_scopes_read" >> .env
- echo "AUTH0_CLIENT_SECRET_2=$client_secret_scopes_read" >> .env
- echo "AUTH0_CLIENT_ID_3=$client_id_scopes_write" >> .env
- echo "AUTH0_CLIENT_SECRET_3=$client_secret_scopes_write" >> .env
- echo "AUTH0_CLIENT_ID_4=$client_id_scopes_readwrite" >> .env
- echo "AUTH0_CLIENT_SECRET_4=$client_secret_scopes_readwrite" >> .env
- echo "API_URL=http://localhost:3010" >> .env
- npm install
- - run:
- name: Execute automated tests
- command: cd test && npm test
-
-# Jobs and Workflows
-version: 2
-jobs:
- checkout:
- machine:
- <<: *machine-cfg
- steps:
- - checkout
- - run:
- name: Clone test script
- command: git clone -b v0.0.1 --depth 1 https://github.com/auth0-samples/api-quickstarts-tests test
- - persist_to_workspace:
- root: ~/
- paths:
- - project
- - test
- 01-Authorization-RS256:
- machine:
- <<: *machine-cfg
- environment:
- AUTH0_CFG: 01-Authorization-RS256
- SAMPLE_PATH: 01-Authorization-RS256
- <<: *defaults
-
-workflows:
- version: 2
- API-Tests:
- jobs:
- - checkout:
- context: Quickstart API Tests
- - 01-Authorization-RS256:
- context: Quickstart API Tests
- requires:
- - checkout
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..17a0f8a
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,42 @@
+
+
+### 🔧 Changes
+
+
+
+### 📚 References
+
+
+
+### 🔬 Testing
+
+
+
+### 📝 Checklist
+
+- [ ] All new/changed/fixed functionality is covered by tests (or N/A)
+- [ ] I have added documentation for all new/changed functionality (or N/A)
+
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 63eafc1..5e85025 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,10 +1,19 @@
version: 2
updates:
+ - package-ecosystem: "gomod"
+ directory: "/01-Authorization-RS256"
+ schedule:
+ interval: "daily"
+ ignore:
+ - dependency-name: "*"
+ update-types:
+ ["version-update:semver-major", "version-update:semver-patch"]
- - package-ecosystem: "gomod"
- directory: "/01-Authorization-RS256"
+ - package-ecosystem: "gomod"
+ directory: "/01-Quickstart-Go-API"
schedule:
interval: "daily"
ignore:
- dependency-name: "*"
- update-types: ["version-update:semver-major", "version-update:semver-patch"]
+ update-types:
+ ["version-update:semver-major", "version-update:semver-patch"]
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
deleted file mode 100644
index 916745e..0000000
--- a/.github/workflows/semgrep.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Semgrep
-
-on:
- pull_request: {}
-
- push:
- branches: ["master", "main"]
-
- schedule:
- - cron: '30 0 1,15 * *'
-
-jobs:
- semgrep:
- name: Scan
- runs-on: ubuntu-latest
- container:
- image: returntocorp/semgrep
- # Skip any PR created by dependabot to avoid permission issues
- if: (github.actor != 'dependabot[bot]')
- steps:
- - uses: actions/checkout@v3
-
- - run: semgrep ci
- env:
- SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..a524bd2
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,170 @@
+name: API Tests
+
+on:
+ push:
+ branches: [master, main]
+ pull_request:
+ branches: [master, main]
+
+jobs:
+ test-01-authorization-rs256:
+ runs-on: ubuntu-latest
+
+ env:
+ AUTH0_CFG: 01-Authorization-RS256
+ SAMPLE_PATH: 01-Authorization-RS256
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.19"
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Clone test scripts
+ run: git clone --depth 1 https://github.com/auth0-samples/api-quickstarts-tests test
+
+ - name: Prepare environment variables
+ env:
+ AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
+ API_IDENTIFIER: ${{ secrets.API_IDENTIFIER }}
+ run: |
+ cd $AUTH0_CFG
+ mv .env.example .env
+ sed -i 's|{DOMAIN}|'$AUTH0_DOMAIN'|g' .env
+ sed -i 's|{API_IDENTIFIER}|'$API_IDENTIFIER'|g' .env
+
+ - name: Install Go dependencies
+ run: cd $AUTH0_CFG && go mod download
+
+ - name: Build Docker image
+ run: cd $AUTH0_CFG && docker build -t auth0-golang-api .
+
+ - name: Start server in detached mode
+ run: cd $AUTH0_CFG && docker run -d --env-file .env -p 3010:3010 auth0-golang-api
+
+ - name: Wait for server to be ready
+ run: |
+ sleep 3
+ until $(curl --silent --head --output /dev/null --fail http://localhost:3010/api/public); do
+ echo "Waiting for server to start..."
+ sleep 5
+ done
+
+ - name: Prepare test environment
+ env:
+ AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
+ API_IDENTIFIER: ${{ secrets.API_IDENTIFIER }}
+ CLIENT_ID_SCOPES_NONE: ${{ secrets.CLIENT_ID_SCOPES_NONE }}
+ CLIENT_SECRET_SCOPES_NONE: ${{ secrets.CLIENT_SECRET_SCOPES_NONE }}
+ CLIENT_ID_SCOPES_READ: ${{ secrets.CLIENT_ID_SCOPES_READ }}
+ CLIENT_SECRET_SCOPES_READ: ${{ secrets.CLIENT_SECRET_SCOPES_READ }}
+ CLIENT_ID_SCOPES_WRITE: ${{ secrets.CLIENT_ID_SCOPES_WRITE }}
+ CLIENT_SECRET_SCOPES_WRITE: ${{ secrets.CLIENT_SECRET_SCOPES_WRITE }}
+ CLIENT_ID_SCOPES_READWRITE: ${{ secrets.CLIENT_ID_SCOPES_READWRITE }}
+ CLIENT_SECRET_SCOPES_READWRITE: ${{ secrets.CLIENT_SECRET_SCOPES_READWRITE }}
+ run: |
+ cd test
+ echo "AUTH0_DOMAIN=$AUTH0_DOMAIN" >> .env
+ echo "API_IDENTIFIER=$API_IDENTIFIER" >> .env
+ echo "AUTH0_CLIENT_ID_1=$CLIENT_ID_SCOPES_NONE" >> .env
+ echo "AUTH0_CLIENT_SECRET_1=$CLIENT_SECRET_SCOPES_NONE" >> .env
+ echo "AUTH0_CLIENT_ID_2=$CLIENT_ID_SCOPES_READ" >> .env
+ echo "AUTH0_CLIENT_SECRET_2=$CLIENT_SECRET_SCOPES_READ" >> .env
+ echo "AUTH0_CLIENT_ID_3=$CLIENT_ID_SCOPES_WRITE" >> .env
+ echo "AUTH0_CLIENT_SECRET_3=$CLIENT_SECRET_SCOPES_WRITE" >> .env
+ echo "AUTH0_CLIENT_ID_4=$CLIENT_ID_SCOPES_READWRITE" >> .env
+ echo "AUTH0_CLIENT_SECRET_4=$CLIENT_SECRET_SCOPES_READWRITE" >> .env
+ echo "API_URL=http://localhost:3010" >> .env
+ npm install
+
+ - name: Run automated tests
+ run: cd test && npm test
+
+ test-01-quickstart-go-api:
+ runs-on: ubuntu-latest
+
+ env:
+ AUTH0_CFG: 01-Quickstart-Go-API
+ SAMPLE_PATH: 01-Quickstart-Go-API
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.24"
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Clone test scripts
+ run: git clone --depth 1 https://github.com/auth0-samples/api-quickstarts-tests test
+
+ - name: Prepare environment variables
+ env:
+ AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
+ API_IDENTIFIER: ${{ secrets.API_IDENTIFIER }}
+ run: |
+ cd $AUTH0_CFG
+ mv .env.example .env
+ sed -i 's|{DOMAIN}|'$AUTH0_DOMAIN'|g' .env
+ sed -i 's|{API_IDENTIFIER}|'$API_IDENTIFIER'|g' .env
+
+ - name: Install Go dependencies
+ run: cd $AUTH0_CFG && go mod download
+
+ - name: Build Docker image
+ run: cd $AUTH0_CFG && docker build -t auth0-golang-api .
+
+ - name: Start server in detached mode
+ run: cd $AUTH0_CFG && docker run -d --env-file .env -p 8080:8080 auth0-golang-api
+
+ - name: Wait for server to be ready
+ run: |
+ sleep 3
+ until $(curl --silent --head --output /dev/null --fail http://localhost:8080/api/public); do
+ echo "Waiting for server to start..."
+ sleep 5
+ done
+
+ - name: Prepare test environment
+ env:
+ AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
+ API_IDENTIFIER: ${{ secrets.API_IDENTIFIER }}
+ CLIENT_ID_SCOPES_NONE: ${{ secrets.CLIENT_ID_SCOPES_NONE }}
+ CLIENT_SECRET_SCOPES_NONE: ${{ secrets.CLIENT_SECRET_SCOPES_NONE }}
+ CLIENT_ID_SCOPES_READ: ${{ secrets.CLIENT_ID_SCOPES_READ }}
+ CLIENT_SECRET_SCOPES_READ: ${{ secrets.CLIENT_SECRET_SCOPES_READ }}
+ CLIENT_ID_SCOPES_WRITE: ${{ secrets.CLIENT_ID_SCOPES_WRITE }}
+ CLIENT_SECRET_SCOPES_WRITE: ${{ secrets.CLIENT_SECRET_SCOPES_WRITE }}
+ CLIENT_ID_SCOPES_READWRITE: ${{ secrets.CLIENT_ID_SCOPES_READWRITE }}
+ CLIENT_SECRET_SCOPES_READWRITE: ${{ secrets.CLIENT_SECRET_SCOPES_READWRITE }}
+ run: |
+ cd test
+ echo "AUTH0_DOMAIN=$AUTH0_DOMAIN" >> .env
+ echo "API_IDENTIFIER=$API_IDENTIFIER" >> .env
+ echo "AUTH0_CLIENT_ID_1=$CLIENT_ID_SCOPES_NONE" >> .env
+ echo "AUTH0_CLIENT_SECRET_1=$CLIENT_SECRET_SCOPES_NONE" >> .env
+ echo "AUTH0_CLIENT_ID_2=$CLIENT_ID_SCOPES_READ" >> .env
+ echo "AUTH0_CLIENT_SECRET_2=$CLIENT_SECRET_SCOPES_READ" >> .env
+ echo "AUTH0_CLIENT_ID_3=$CLIENT_ID_SCOPES_WRITE" >> .env
+ echo "AUTH0_CLIENT_SECRET_3=$CLIENT_SECRET_SCOPES_WRITE" >> .env
+ echo "AUTH0_CLIENT_ID_4=$CLIENT_ID_SCOPES_READWRITE" >> .env
+ echo "AUTH0_CLIENT_SECRET_4=$CLIENT_SECRET_SCOPES_READWRITE" >> .env
+ echo "API_URL=http://localhost:8080" >> .env
+ npm install
+
+ - name: Run automated tests
+ run: cd test && npm test
diff --git a/.gitignore b/.gitignore
index ed3bb2a..60ed564 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
.DS_Store
01-Authorization-RS256/.env
01-Authorization-RS256/vendor/
+01-Quickstart-Go-API/.env
+01-Quickstart-Go-API/vendor/
diff --git a/01-Quickstart-Go-API/.dockerignore b/01-Quickstart-Go-API/.dockerignore
new file mode 100644
index 0000000..d3c9987
--- /dev/null
+++ b/01-Quickstart-Go-API/.dockerignore
@@ -0,0 +1,3 @@
+README.md
+exec.sh
+exec.ps1
diff --git a/01-Quickstart-Go-API/.env.example b/01-Quickstart-Go-API/.env.example
new file mode 100644
index 0000000..6b907e4
--- /dev/null
+++ b/01-Quickstart-Go-API/.env.example
@@ -0,0 +1,2 @@
+AUTH0_DOMAIN={DOMAIN}
+AUTH0_AUDIENCE={API_IDENTIFIER}
diff --git a/01-Quickstart-Go-API/Dockerfile b/01-Quickstart-Go-API/Dockerfile
new file mode 100644
index 0000000..231524c
--- /dev/null
+++ b/01-Quickstart-Go-API/Dockerfile
@@ -0,0 +1,23 @@
+FROM golang:1.24-alpine
+
+# Install git
+RUN apk --update add \
+ git openssl \
+ && rm /var/cache/apk/*
+
+# Define current working directory
+WORKDIR /01-Quickstart-Go-API
+
+# Download modules to local cache so we can skip re-
+# downloading on consecutive docker build commands
+COPY go.mod .
+COPY go.sum .
+RUN go mod download
+
+# Add sources
+COPY . .
+
+# Expose port 8080 for our api binary
+EXPOSE 8080
+
+CMD ["go", "run", "cmd/server/main.go"]
diff --git a/01-Quickstart-Go-API/README.md b/01-Quickstart-Go-API/README.md
new file mode 100644
index 0000000..87fac10
--- /dev/null
+++ b/01-Quickstart-Go-API/README.md
@@ -0,0 +1,168 @@
+# Auth0 Golang API Sample - Authorization (RS256)
+
+This sample demonstrates how to protect a Go API using Auth0 with RS256 signed JWT tokens.
+
+## Prerequisites
+
+- Go 1.24 or higher
+- An Auth0 account ([Sign up for free](https://auth0.com/signup))
+
+## Setup
+
+1. **Create an API in Auth0 Dashboard:**
+ - Go to APIs in your Auth0 Dashboard
+ - Click "Create API"
+ - Give it a name and identifier (audience)
+ - Select RS256 signing algorithm
+
+2. **Add permissions to your API:**
+ - Select your API in the Auth0 Dashboard
+ - Go to the **Permissions** tab
+ - Add a new permission:
+ - **Permission (Scope):** `read:messages`
+ - **Description:** Read messages from the API
+ - Click **Add**
+
+3. **Enable RBAC for your API (Important!):**
+ - Select your API in the Auth0 Dashboard
+ - Go to the **Settings** tab
+ - Scroll down to **RBAC Settings**
+ - Enable **Enable RBAC**
+ - Enable **Add Permissions in the Access Token**
+ - Click **Save**
+
+
+
+ > ⚠️ **Note:** Without enabling these settings, the `permissions` array in your access token will be empty, causing the scoped endpoint to fail with a 403 error.
+
+4. **Configure Machine-to-Machine Application (for testing):**
+ - Go to **Applications > Applications** in your Auth0 Dashboard
+ - Select your Machine-to-Machine application (or create one)
+ - Go to the **APIs** tab
+ - Find your API and expand it
+ - Toggle **Authorize** to enable it
+ - Select the permissions you want to grant (e.g., `read:messages`)
+ - Click **Update**
+
+
+
+ > 💡 This step ensures that tokens generated for this application will include the selected permissions in the `permissions` array.
+
+5. **Configure environment variables:**
+ ```bash
+ cp .env.example .env
+ ```
+
+ Edit `.env` and set:
+ ```
+ AUTH0_DOMAIN=your-tenant.auth0.com
+ AUTH0_AUDIENCE=https://your-api-identifier
+ ```
+
+6. **Install dependencies:**
+ ```bash
+ go mod download
+ ```
+
+## Running the Sample
+
+```bash
+go run cmd/server/main.go
+```
+
+The server will start on port 8080.
+
+## API Endpoints
+
+### Public Endpoint (No Authentication Required)
+```bash
+curl http://localhost:8080/api/public
+```
+
+### Private Endpoint (Requires Valid JWT)
+```bash
+curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
+ http://localhost:8080/api/private
+```
+
+### Scoped Endpoint (Requires JWT with `read:messages` scope)
+```bash
+curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
+ http://localhost:8080/api/private-scoped
+```
+
+## Getting an Access Token
+
+To test the protected endpoints, you need an access token:
+
+1. Go to the **Test** tab of your API in the Auth0 Dashboard
+2. In the **Scopes** field, add `read:messages` (for testing the scoped endpoint)
+3. Copy the provided access token or `curl` command
+4. Use it in the `Authorization` header as shown above
+
+**Note:** Tokens without the `read:messages` scope will receive a 403 Forbidden error when accessing the scoped endpoint.
+
+### Verifying Your Token Contains Permissions
+
+You can decode your JWT token at [jwt.io](https://jwt.io) to verify it contains the permissions. A properly configured token should include:
+
+```json
+{
+ "iss": "https://your-tenant.auth0.com/",
+ "sub": "...",
+ "aud": "https://your-api-identifier",
+ "iat": 1697988893,
+ "exp": 1698075293,
+ "azp": "...",
+ "scope": "read:messages",
+ "gty": "client-credentials",
+ "permissions": [
+ "read:messages"
+ ]
+}
+```
+
+If the `permissions` array is empty, please verify you've completed steps 3 and 4 in the Setup section above.
+
+## Project Structure
+
+```
+01-Quickstart-Go-API/
+├── cmd/
+│ └── server/
+│ └── main.go # Application entry point
+├── internal/
+│ ├── auth/
+│ │ ├── validator.go # JWT validator setup
+│ │ └── middleware.go # JWT middleware
+│ ├── config/
+│ │ └── auth.go # Configuration loading
+│ └── handlers/
+│ └── api.go # HTTP handlers
+├── .env.example
+├── go.mod
+└── README.md
+```
+
+## Key Features
+
+- ✅ RS256 JWT validation
+- ✅ Custom claims support
+- ✅ Scope-based authorization
+- ✅ Graceful shutdown
+- ✅ Production-ready timeouts
+- ✅ Structured logging with slog
+
+## Learn More
+
+- [Auth0 Go SDK Documentation](https://github.com/auth0/go-jwt-middleware)
+- [Auth0 Documentation](https://auth0.com/docs)
+- [Securing Go APIs with
+## Project Structure
+
+```
+01-Autckstart/backend/golang)
+
+## License
+
+This project is licensed under the MIT license. See the [LICENSE](../../LICENSE) file for more info.
diff --git a/01-Quickstart-Go-API/cmd/server/main.go b/01-Quickstart-Go-API/cmd/server/main.go
new file mode 100644
index 0000000..44a4abd
--- /dev/null
+++ b/01-Quickstart-Go-API/cmd/server/main.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "time"
+
+ "github.com/auth0-samples/auth0-golang-api-samples/01-Quickstart-Go-API/internal/auth"
+ "github.com/auth0-samples/auth0-golang-api-samples/01-Quickstart-Go-API/internal/config"
+ "github.com/auth0-samples/auth0-golang-api-samples/01-Quickstart-Go-API/internal/handlers"
+ "github.com/joho/godotenv"
+)
+
+func main() {
+ if err := godotenv.Load(); err != nil {
+ log.Println("No .env file found, using environment variables")
+ }
+
+ cfg, err := config.LoadAuthConfig()
+ if err != nil {
+ log.Fatalf("Failed to load config: %v", err)
+ }
+
+ jwtValidator, err := auth.NewValidator(cfg.Domain, cfg.Audience)
+ if err != nil {
+ log.Fatalf("Failed to create validator: %v", err)
+ }
+
+ middleware, err := auth.NewMiddleware(jwtValidator)
+ if err != nil {
+ log.Fatalf("Failed to create middleware: %v", err)
+ }
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/api/public", handlers.PublicHandler)
+ mux.Handle("/api/private", middleware.CheckJWT(http.HandlerFunc(handlers.PrivateHandler)))
+ mux.Handle("/api/private-scoped", middleware.CheckJWT(http.HandlerFunc(handlers.ScopedHandler)))
+
+ srv := &http.Server{
+ Addr: ":8080",
+ Handler: mux,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 15 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+
+ go func() {
+ log.Println("Server starting on :8080")
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("Server failed: %v", err)
+ }
+ }()
+
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, os.Interrupt)
+ <-quit
+
+ log.Println("Shutting down server...")
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ if err := srv.Shutdown(ctx); err != nil {
+ log.Fatalf("Server forced to shutdown: %v", err)
+ }
+
+ log.Println("Server exited")
+}
diff --git a/01-Quickstart-Go-API/exec.ps1 b/01-Quickstart-Go-API/exec.ps1
new file mode 100644
index 0000000..5a9854a
--- /dev/null
+++ b/01-Quickstart-Go-API/exec.ps1
@@ -0,0 +1,2 @@
+docker build -t auth0-golang-api .
+docker run --env-file .env -p 8080:8080 -it auth0-golang-api
diff --git a/01-Quickstart-Go-API/exec.sh b/01-Quickstart-Go-API/exec.sh
new file mode 100644
index 0000000..49bc7a4
--- /dev/null
+++ b/01-Quickstart-Go-API/exec.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+docker build -t auth0-golang-api .
+docker run --env-file .env -p 8080:8080 -it auth0-golang-api
diff --git a/01-Quickstart-Go-API/go.mod b/01-Quickstart-Go-API/go.mod
new file mode 100644
index 0000000..d6ce969
--- /dev/null
+++ b/01-Quickstart-Go-API/go.mod
@@ -0,0 +1,24 @@
+module github.com/auth0-samples/auth0-golang-api-samples/01-Quickstart-Go-API
+
+go 1.24.0
+
+require (
+ github.com/auth0/go-jwt-middleware/v3 v3.0.0
+ github.com/joho/godotenv v1.5.1
+)
+
+require (
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/lestrrat-go/blackmagic v1.0.4 // indirect
+ github.com/lestrrat-go/dsig v1.0.0 // indirect
+ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
+ github.com/lestrrat-go/httpcc v1.0.1 // indirect
+ github.com/lestrrat-go/httprc/v3 v3.0.3 // indirect
+ github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect
+ github.com/lestrrat-go/option/v2 v2.0.0 // indirect
+ github.com/segmentio/asm v1.2.1 // indirect
+ github.com/valyala/fastjson v1.6.7 // indirect
+ golang.org/x/crypto v0.46.0 // indirect
+ golang.org/x/sys v0.40.0 // indirect
+)
diff --git a/01-Quickstart-Go-API/go.sum b/01-Quickstart-Go-API/go.sum
new file mode 100644
index 0000000..6f8d5ca
--- /dev/null
+++ b/01-Quickstart-Go-API/go.sum
@@ -0,0 +1,45 @@
+github.com/auth0/go-jwt-middleware/v3 v3.0.0 h1:+rvUPCT+VbAuK4tpS13fWfZrMyqTwLopt3VoY0Y7kvA=
+github.com/auth0/go-jwt-middleware/v3 v3.0.0/go.mod h1:iU42jqjRyeKbf9YYSnRnolr836gk6Ty/jnUNuVq2b0o=
+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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
+github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
+github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
+github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
+github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
+github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc/v3 v3.0.3 h1:WjLHWkDkgWXeIUrKi/7lS/sGq2DjkSAwdTbH5RHXAKs=
+github.com/lestrrat-go/httprc/v3 v3.0.3/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
+github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
+github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
+github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
+github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
+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/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
+github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
+golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/01-Quickstart-Go-API/internal/auth/claims.go b/01-Quickstart-Go-API/internal/auth/claims.go
new file mode 100644
index 0000000..6ab660f
--- /dev/null
+++ b/01-Quickstart-Go-API/internal/auth/claims.go
@@ -0,0 +1,46 @@
+package auth
+
+import (
+ "context"
+ "fmt"
+ "strings"
+)
+
+// CustomClaims contains custom data we want to parse from the JWT.
+type CustomClaims struct {
+ Scope string `json:"scope"`
+}
+
+// Validate ensures the custom claims are properly formatted.
+func (c *CustomClaims) Validate(ctx context.Context) error {
+ // Scope is optional, but if present, must be properly formatted
+ if c.Scope == "" {
+ return nil // No scope is valid - not all endpoints require permissions
+ }
+
+ // Validate scope format (no leading/trailing spaces, no double spaces)
+ if strings.TrimSpace(c.Scope) != c.Scope {
+ return fmt.Errorf("scope claim has invalid whitespace")
+ }
+
+ if strings.Contains(c.Scope, " ") {
+ return fmt.Errorf("scope claim contains double spaces")
+ }
+
+ return nil
+}
+
+// HasScope checks whether our claims have a specific scope.
+func (c *CustomClaims) HasScope(expectedScope string) bool {
+ if c.Scope == "" {
+ return false
+ }
+
+ scopes := strings.Split(c.Scope, " ")
+ for _, scope := range scopes {
+ if scope == expectedScope {
+ return true
+ }
+ }
+ return false
+}
diff --git a/01-Quickstart-Go-API/internal/auth/middleware.go b/01-Quickstart-Go-API/internal/auth/middleware.go
new file mode 100644
index 0000000..4d754e2
--- /dev/null
+++ b/01-Quickstart-Go-API/internal/auth/middleware.go
@@ -0,0 +1,22 @@
+package auth
+
+import (
+ "log/slog"
+ "net/http"
+
+ jwtmiddleware "github.com/auth0/go-jwt-middleware/v3"
+ "github.com/auth0/go-jwt-middleware/v3/validator"
+)
+
+func NewMiddleware(jwtValidator *validator.Validator) (*jwtmiddleware.JWTMiddleware, error) {
+ return jwtmiddleware.New(
+ jwtmiddleware.WithValidator(jwtValidator),
+ jwtmiddleware.WithValidateOnOptions(false),
+ jwtmiddleware.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
+ slog.Error("JWT validation failed", "error", err, "path", r.URL.Path)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusUnauthorized)
+ w.Write([]byte(`{"message":"Failed to validate JWT."}`))
+ }),
+ )
+}
diff --git a/01-Quickstart-Go-API/internal/auth/validator.go b/01-Quickstart-Go-API/internal/auth/validator.go
new file mode 100644
index 0000000..be304a2
--- /dev/null
+++ b/01-Quickstart-Go-API/internal/auth/validator.go
@@ -0,0 +1,41 @@
+package auth
+
+import (
+ "fmt"
+ "net/url"
+ "time"
+
+ "github.com/auth0/go-jwt-middleware/v3/jwks"
+ "github.com/auth0/go-jwt-middleware/v3/validator"
+)
+
+func NewValidator(domain, audience string) (*validator.Validator, error) {
+ issuerURL, err := url.Parse("https://" + domain + "/")
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse issuer URL: %w", err)
+ }
+
+ provider, err := jwks.NewCachingProvider(
+ jwks.WithIssuerURL(issuerURL),
+ jwks.WithCacheTTL(5*time.Minute),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create JWKS provider: %w", err)
+ }
+
+ jwtValidator, err := validator.New(
+ validator.WithKeyFunc(provider.KeyFunc),
+ validator.WithAlgorithm(validator.RS256),
+ validator.WithIssuer(issuerURL.String()),
+ validator.WithAudience(audience),
+ validator.WithCustomClaims(func() validator.CustomClaims {
+ return &CustomClaims{}
+ }),
+ validator.WithAllowedClockSkew(30*time.Second),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create validator: %w", err)
+ }
+
+ return jwtValidator, nil
+}
diff --git a/01-Quickstart-Go-API/internal/config/auth.go b/01-Quickstart-Go-API/internal/config/auth.go
new file mode 100644
index 0000000..290eb54
--- /dev/null
+++ b/01-Quickstart-Go-API/internal/config/auth.go
@@ -0,0 +1,28 @@
+package config
+
+import (
+ "fmt"
+ "os"
+)
+
+type AuthConfig struct {
+ Domain string
+ Audience string
+}
+
+func LoadAuthConfig() (*AuthConfig, error) {
+ domain := os.Getenv("AUTH0_DOMAIN")
+ if domain == "" {
+ return nil, fmt.Errorf("AUTH0_DOMAIN environment variable required")
+ }
+
+ audience := os.Getenv("AUTH0_AUDIENCE")
+ if audience == "" {
+ return nil, fmt.Errorf("AUTH0_AUDIENCE environment variable required")
+ }
+
+ return &AuthConfig{
+ Domain: domain,
+ Audience: audience,
+ }, nil
+}
diff --git a/01-Quickstart-Go-API/internal/handlers/api.go b/01-Quickstart-Go-API/internal/handlers/api.go
new file mode 100644
index 0000000..52dd99d
--- /dev/null
+++ b/01-Quickstart-Go-API/internal/handlers/api.go
@@ -0,0 +1,50 @@
+package handlers
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/auth0-samples/auth0-golang-api-samples/01-Quickstart-Go-API/internal/auth"
+ jwtmiddleware "github.com/auth0/go-jwt-middleware/v3"
+ "github.com/auth0/go-jwt-middleware/v3/validator"
+)
+
+func PublicHandler(w http.ResponseWriter, r *http.Request) {
+ response := map[string]string{
+ "message": "Hello from a public endpoint! You don't need to be authenticated to see this.",
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+func PrivateHandler(w http.ResponseWriter, r *http.Request) {
+ response := map[string]string{
+ "message": "Hello from a private endpoint! You need to be authenticated to see this.",
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+func ScopedHandler(w http.ResponseWriter, r *http.Request) {
+ claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
+ if err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusUnauthorized)
+ w.Write([]byte(`{"message":"Unauthorized."}`))
+ return
+ }
+
+ customClaims, ok := claims.CustomClaims.(*auth.CustomClaims)
+ if !ok || !customClaims.HasScope("read:messages") {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ w.Write([]byte(`{"message":"Insufficient scope."}`))
+ return
+ }
+
+ response := map[string]string{
+ "message": "Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.",
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
diff --git a/LICENSE b/LICENSE
index 86bcdb8..71d9d2c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017 Auth0 Samples
+Copyright (c) 2026 Auth0 Samples
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index d2fda52..8acb5e2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Auth0 Golang API Samples
-[](https://circleci.com/gh/auth0-samples/auth0-golang-api-samples/tree/master)
+[](https://github.com/auth0-samples/auth0-golang-api-samples/actions)
These samples demonstrate how to create an API with Go which only permits access to resources if a valid **access token** is included. This verification is done by validating the signature and claims in a JSON Web Token (JWT) signed by Auth0.
diff --git a/assets/enable-rbac-settings.png b/assets/enable-rbac-settings.png
new file mode 100644
index 0000000..a5d5903
Binary files /dev/null and b/assets/enable-rbac-settings.png differ
diff --git a/assets/m2m-permissions.png b/assets/m2m-permissions.png
new file mode 100644
index 0000000..0325429
Binary files /dev/null and b/assets/m2m-permissions.png differ