diff --git a/main/docs.json b/main/docs.json index f3bb283e0a..3a6e4404e1 100644 --- a/main/docs.json +++ b/main/docs.json @@ -2541,7 +2541,7 @@ "docs/quickstart/backend/python/index", "docs/quickstart/backend/fastapi/index", "docs/quickstart/backend/java-spring-security5/index", - "docs/quickstart/backend/golang/interactive", + "docs/quickstart/backend/golang/index", "docs/quickstart/backend/aspnet-core-webapi/index", "docs/quickstart/backend/webapi-owin/interactive", "docs/quickstart/backend/laravel/interactive", @@ -28156,6 +28156,22 @@ "source": "/docs/quickstart/backend/golang/00-getting-started", "destination": "/docs/quickstart/backend/golang" }, + { + "source": "/docs/quickstart/backend/golang/01-authorization", + "destination": "/docs/quickstart/backend/golang" + }, + { + "source": "/docs/quickstart/backend/golang/interactive", + "destination": "/docs/quickstart/backend/golang" + }, + { + "source": "/docs/quickstart/backend/golang/02-using", + "destination": "/docs/quickstart/backend/golang" + }, + { + "source": "/docs/quickstart/backend/golang/03-troubleshooting", + "destination": "/docs/quickstart/backend/golang" + }, { "source": "/docs/quickstart/", "destination": "/docs/quickstarts" diff --git a/main/docs/libraries.mdx b/main/docs/libraries.mdx index 8073996e9f..4e3a174fd1 100644 --- a/main/docs/libraries.mdx +++ b/main/docs/libraries.mdx @@ -382,15 +382,15 @@ Does your API or service need authentication? Auth0 has SDKs for common API and name: "Go API", subtext: "go-jwt-middleware", logo: "golang.svg", - date: "Jan 20, 2026", - badge: "v3.0.0", + date: "Apr 9, 2026", + badge: "v3.1.0", links: [ { label: "Github", url: "https://github.com/auth0/go-jwt-middleware" }, { label: "Sample App", url: "https://github.com/auth0-samples/auth0-golang-api-samples/tree/master/01-Quickstart-Go-API", }, - { label: "Quickstart", url: "/docs/quickstart/backend/golang/interactive" }, + { label: "Quickstart", url: "/docs/quickstart/backend/golang" }, ], }}/> @@ -667,8 +667,8 @@ Need to programmatically perform Auth0 administrative tasks? Choose from one of name: "Go JWT Middleware", subtext: "go-jwt-middleware", logo: "golang.svg", - date: "Mar 5, 2025", - badge: "v2.3.0", + date: "Apr 9, 2026", + badge: "v3.1.0", links: [ { label: "Github", url: "https://github.com/auth0/go-jwt-middleware" } ], diff --git a/main/docs/quickstart/backend/golang/02-using.mdx b/main/docs/quickstart/backend/golang/02-using.mdx deleted file mode 100644 index 155944abe1..0000000000 --- a/main/docs/quickstart/backend/golang/02-using.mdx +++ /dev/null @@ -1,564 +0,0 @@ ---- -title: "Auth0 Go API SDK Quickstarts: Using your API" ---- -import {AuthCodeBlock} from "/snippets/AuthCodeBlock.jsx"; - -import {AuthCodeGroup} from "/snippets/AuthCodeGroup.jsx"; - -##### By Kunal Dawar - -This tutorial demonstrates how to call your protected Go API from client applications. We recommend that you log in to follow this quickstart with examples configured for your account. - -## Calling the API From Your Application - -You can call your protected API from your application by passing an Access Token in the `Authorization` header of your HTTP request as a Bearer token. - - -```bash cURL lines -curl --request GET \ - --url http://localhost:8080/api/private \ - --header 'authorization: Bearer YOUR_ACCESS_TOKEN' -``` - -```csharp C# lines -var client = new RestClient("http://localhost:8080/api/private"); -var request = new RestRequest(Method.GET); -request.AddHeader("authorization", "Bearer YOUR_ACCESS_TOKEN"); -IRestResponse response = client.Execute(request); -``` - -```go Go lines -package main - -import ( - "fmt" - "net/http" - "io" -) - -func main() { - url := "http://localhost:8080/api/private" - - req, _ := http.NewRequest("GET", url, nil) - req.Header.Add("authorization", "Bearer YOUR_ACCESS_TOKEN") - - res, _ := http.DefaultClient.Do(req) - defer res.Body.Close() - - body, _ := io.ReadAll(res.Body) - - fmt.Println(res) - fmt.Println(string(body)) -} -``` - -```java Java lines -HttpResponse response = Unirest.get("http://localhost:8080/api/private") - .header("authorization", "Bearer YOUR_ACCESS_TOKEN") - .asString(); -``` - -```javascript Node.JS lines -const axios = require("axios").default; - -const options = { - method: 'GET', - url: 'http://localhost:8080/api/private', - headers: {authorization: 'Bearer YOUR_ACCESS_TOKEN'} -}; - -axios.request(options).then(function (response) { - console.log(response.data); -}).catch(function (error) { - console.error(error); -}); -``` - -```python Python lines -import http.client - -conn = http.client.HTTPConnection("localhost", 8080) -headers = { 'authorization': "Bearer YOUR_ACCESS_TOKEN" } - -conn.request("GET", "/api/private", headers=headers) - -res = conn.getresponse() -data = res.read() - -print(data.decode("utf-8")) -``` - -```ruby Ruby lines -require 'uri' -require 'net/http' - -url = URI("http://localhost:8080/api/private") - -http = Net::HTTP.new(url.host, url.port) - -request = Net::HTTP::Get.new(url) -request["authorization"] = 'Bearer YOUR_ACCESS_TOKEN' - -response = http.request(request) -puts response.read_body -``` - -```swift Swift lines -import Foundation - -let headers = ["authorization": "Bearer YOUR_ACCESS_TOKEN"] - -let request = NSMutableURLRequest(url: NSURL(string: "http://localhost:8080/api/private")! as URL, - cachePolicy: .useProtocolCachePolicy, - timeoutInterval: 10.0) -request.httpMethod = "GET" -request.allHTTPHeaderFields = headers - -let session = URLSession.shared -let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in - if (error != nil) { - print(error as Any) - } else { - let httpResponse = response as? HTTPURLResponse - print(httpResponse as Any) - } -}) - -dataTask.resume() -``` - - - -The examples use port **8080**. Update the port in your requests to match your server configuration. - - -## Obtaining an Access Token - - - - If you are calling the API from a Single-Page Application or a Mobile/Native application, after the authorization flow is completed, you will get an Access Token. How you get the token and how you make the call to the API will be dependent on the type of application you are developing and the framework you are using. - - - - React, Vue, Angular quickstarts with examples - - - iOS, Android, React Native quickstarts - - - - - - If you are calling the API from a command-line tool or another service where there isn't a user entering their credentials, you need to use the [OAuth Client Credentials flow](https://auth0.com/docs/api/authentication#client-credentials). - - - - Register a [Machine to Machine Application](https://manage.auth0.com/#/applications) in your Auth0 Dashboard. - - - - Copy your **Client ID** and **Client Secret** from [Application Settings](https://auth0.com/docs/get-started/dashboard/application-settings). - - - - Use the Client Credentials flow to obtain an access token (see code examples below). - - - - - **Token Reuse**: Auth0 customers are billed based on the number of Machine to Machine Access Tokens issued. Once your application gets an Access Token, it should keep using it until it expires to minimize the number of tokens requested. - - - - - -```bash cURL -curl --request POST \ - --url 'https://{yourDomain}/oauth/token' \ - --header 'content-type: application/x-www-form-urlencoded' \ - --data grant_type=client_credentials \ - --data 'client_id={yourClientId}' \ - --data client_secret={yourClientSecret} \ - --data audience=YOUR_API_IDENTIFIER -``` -```cs C# -var client = new RestClient("https://{yourDomain}/oauth/token"); -var request = new RestRequest(Method.POST); -request.AddHeader("content-type", "application/x-www-form-urlencoded"); -request.AddParameter("application/x-www-form-urlencoded", "grant_type=client_credentials&client_id={yourClientId}&client_secret={yourClientSecret}&audience=YOUR_API_IDENTIFIER", ParameterType.RequestBody); -IRestResponse response = client.Execute(request); -``` -```go Go -package main - -import ( - "fmt" - "strings" - "net/http" - "io" -) - -func main() { - url := "https://{yourDomain}/oauth/token" - - payload := strings.NewReader("grant_type=client_credentials&client_id={yourClientId}&client_secret={yourClientSecret}&audience=YOUR_API_IDENTIFIER") - - req, _ := http.NewRequest("POST", url, payload) - req.Header.Add("content-type", "application/x-www-form-urlencoded") - - res, _ := http.DefaultClient.Do(req) - defer res.Body.Close() - - body, _ := io.ReadAll(res.Body) - - fmt.Println(res) - fmt.Println(string(body)) -} -``` -```java Java -HttpResponse response = Unirest.post("https://{yourDomain}/oauth/token") - .header("content-type", "application/x-www-form-urlencoded") - .body("grant_type=client_credentials&client_id={yourClientId}&client_secret={yourClientSecret}&audience=YOUR_API_IDENTIFIER") - .asString(); -``` -```javascript Node.JS -const axios = require("axios").default; - -const options = { - method: 'POST', - url: 'https://{yourDomain}/oauth/token', - headers: {'content-type': 'application/x-www-form-urlencoded'}, - data: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: '{yourClientId}', - client_secret: '{yourClientSecret}', - audience: 'YOUR_API_IDENTIFIER' - }) -}; - -axios.request(options).then(function (response) { - console.log(response.data); -}).catch(function (error) { - console.error(error); -}); -``` -```python Python -import http.client - -conn = http.client.HTTPSConnection("{yourDomain}") - -payload = "grant_type=client_credentials&client_id={yourClientId}&client_secret={yourClientSecret}&audience=YOUR_API_IDENTIFIER" - -headers = { 'content-type': "application/x-www-form-urlencoded" } - -conn.request("POST", "/oauth/token", payload, headers) - -res = conn.getresponse() -data = res.read() - -print(data.decode("utf-8")) -``` -```ruby Ruby -require 'uri' -require 'net/http' -require 'openssl' - -url = URI("https://{yourDomain}/oauth/token") - -http = Net::HTTP.new(url.host, url.port) -http.use_ssl = true -http.verify_mode = OpenSSL::SSL::VERIFY_NONE - -request = Net::HTTP::Post.new(url) -request["content-type"] = 'application/x-www-form-urlencoded' -request.body = "grant_type=client_credentials&client_id={yourClientId}&client_secret={yourClientSecret}&audience=YOUR_API_IDENTIFIER" - -response = http.request(request) -puts response.read_body -``` -```swift Swift -import Foundation - -let headers = ["content-type": "application/x-www-form-urlencoded"] - -let postData = NSMutableData(data: "grant_type=client_credentials".data(using: String.Encoding.utf8)!) -postData.append("&client_id={yourClientId}".data(using: String.Encoding.utf8)!) -postData.append("&client_secret={yourClientSecret}".data(using: String.Encoding.utf8)!) -postData.append("&audience=YOUR_API_IDENTIFIER".data(using: String.Encoding.utf8)!) - -let request = NSMutableURLRequest(url: NSURL(string: "https://{yourDomain}/oauth/token")! as URL, - cachePolicy: .useProtocolCachePolicy, - timeoutInterval: 10.0) -request.httpMethod = "POST" -request.allHTTPHeaderFields = headers -request.httpBody = postData as Data - -let session = URLSession.shared -let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in - if (error != nil) { - print(error as Any) - } else { - let httpResponse = response as? HTTPURLResponse - print(httpResponse as Any) - } -}) - -dataTask.resume() -``` - - - -For testing purposes, you can also get an Access Token from the **Test** tab in your [API settings](https://manage.auth0.com/#/apis). - - -## Test Your Protected API - - - - -You can make a request to the `/api/public` endpoint without passing any Access Token: - - -```bash cURL lines -curl --request GET \ - --url http://localhost:8080/api/public -``` -```go Go lines -package main - -import ( - "fmt" - "net/http" - "io" -) - -func main() { - url := "http://localhost:8080/api/public" - req, _ := http.NewRequest("GET", url, nil) - - res, _ := http.DefaultClient.Do(req) - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(res) - fmt.Println(string(body)) -} -``` - - -**Expected**: 200 OK with message "Hello from a public endpoint! No authentication required." - - - -This should fail with a 401 error: - - -```bash cURL lines -curl --request GET \ - --url http://localhost:8080/api/private -``` - - -**Expected**: 401 Unauthorized `{"error": "invalid_token"}` - - - -This should succeed with a valid token: - - -```bash cURL lines -curl --request GET \ - --url http://localhost:8080/api/private \ - --header 'authorization: Bearer YOUR_ACCESS_TOKEN' -``` - -```go Go lines -package main - -import ( - "fmt" - "net/http" - "io" -) - -func main() { - url := "http://localhost:8080/api/private" - req, _ := http.NewRequest("GET", url, nil) - req.Header.Add("authorization", "Bearer YOUR_ACCESS_TOKEN") - - res, _ := http.DefaultClient.Do(req) - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(res) - fmt.Println(string(body)) -} -``` - - -**Expected**: 200 OK with user information - - - -To test the endpoint that requires a scope, pass the Access Token containing the correct scope (`read:messages`) as a Bearer token: - - -```bash cURL lines -curl --request GET \ - --url http://localhost:8080/api/private-scoped \ - --header 'authorization: Bearer YOUR_ACCESS_TOKEN' -``` - -```csharp C# lines -var client = new RestClient("http://localhost:8080/api/private-scoped"); -var request = new RestRequest(Method.GET); -request.AddHeader("authorization", "Bearer YOUR_ACCESS_TOKEN"); -IRestResponse response = client.Execute(request); -``` - -```go Go lines -package main - -import ( - "fmt" - "net/http" - "io" -) - -func main() { - url := "http://localhost:8080/api/private-scoped" - req, _ := http.NewRequest("GET", url, nil) - req.Header.Add("authorization", "Bearer YOUR_ACCESS_TOKEN") - - res, _ := http.DefaultClient.Do(req) - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(res) - fmt.Println(string(body)) -} -``` - -```java Java lines -HttpResponse response = Unirest.get("http://localhost:8080/api/private-scoped") - .header("authorization", "Bearer YOUR_ACCESS_TOKEN") - .asString(); -``` - -```javascript Node.JS lines -const axios = require("axios").default; - -const options = { - method: 'GET', - url: 'http://localhost:8080/api/private-scoped', - headers: {authorization: 'Bearer YOUR_ACCESS_TOKEN'} -}; - -axios.request(options).then(function (response) { - console.log(response.data); -}).catch(function (error) { - console.error(error); -}); -``` - -```python Python lines -import http.client - -conn = http.client.HTTPConnection("localhost", 8080) -headers = { 'authorization': "Bearer YOUR_ACCESS_TOKEN" } - -conn.request("GET", "/api/private-scoped", headers=headers) - -res = conn.getresponse() -data = res.read() - -print(data.decode("utf-8")) -``` - -```ruby Ruby lines -require 'uri' -require 'net/http' - -url = URI("http://localhost:8080/api/private-scoped") - -http = Net::HTTP.new(url.host, url.port) - -request = Net::HTTP::Get.new(url) -request["authorization"] = 'Bearer YOUR_ACCESS_TOKEN' - -response = http.request(request) -puts response.read_body -``` - -```swift Swift lines -import Foundation - -let headers = ["authorization": "Bearer YOUR_ACCESS_TOKEN"] - -let request = NSMutableURLRequest(url: NSURL(string: "http://localhost:8080/api/private-scoped")! as URL, - cachePolicy: .useProtocolCachePolicy, - timeoutInterval: 10.0) -request.httpMethod = "GET" -request.allHTTPHeaderFields = headers - -let session = URLSession.shared -let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in - if (error != nil) { - print(error as Any) - } else { - let httpResponse = response as? HTTPURLResponse - print(httpResponse as Any) - } -}) - -dataTask.resume() -``` - - -**Expected with scope**: 200 OK with `read:messages` permission - - -If the required scope is not present, the API returns a 403 HTTP Status (Forbidden): `Insufficient permissions` - - - - -## API Response Behavior - - -The middleware returns RFC 6750 compliant error responses with structured JSON: - -**401 Unauthorized** (token missing or invalid): -```json -{"error": "invalid_token", "description": "authorization header missing"} -``` - -**403 Forbidden** (insufficient permissions): -``` -Insufficient permissions -``` - -The response includes a `WWW-Authenticate` header for proper OAuth 2.0 compliance: -``` -WWW-Authenticate: Bearer error="invalid_token", error_description="token expired" -``` - - -## Next Steps - - - - Add domain-specific authorization logic with type-safe claims - - - Implement refresh token flow for long-lived sessions - - - Enable Demonstrating Proof-of-Possession for enhanced security - - - Pre-launch security review and best practices - - - -[Edit on GitHub](https://github.com/auth0/docs/edit/master/articles/quickstart/backend/golang/02-using.md) diff --git a/main/docs/quickstart/backend/golang/03-troubleshooting.mdx b/main/docs/quickstart/backend/golang/03-troubleshooting.mdx deleted file mode 100644 index 6a8ea298e0..0000000000 --- a/main/docs/quickstart/backend/golang/03-troubleshooting.mdx +++ /dev/null @@ -1,311 +0,0 @@ ---- -title: "Auth0 Go API SDK Quickstarts: Troubleshooting" ---- - -##### By Kunal Dawar - -This document will help you troubleshoot your configuration if you get unexpected responses from your API. We recommend that you log in to follow this quickstart with examples configured for your account. - -## go-jwt-middleware Troubleshooting - -If you configured JWT validation correctly, you will be able to get proper responses from your API when you make requests. However, if you get a 401 (Unauthorized) response from your API, it is because the configuration of your JWT middleware does not match the JWT that was passed. - -## How Does a Token Get Validated? - - -The middleware validates tokens through these steps: - - - - The token must conform to the JWT structure. Learn more about [JWT structure](/docs/secure/tokens/json-web-tokens#what-is-the-json-web-token-structure-). - - - - The signature verifies that the token was signed by the sender and not altered. The middleware uses a JWKS caching provider to fetch and validate against Auth0's public keys. - - - - Tokens are only valid for a specified time (expressed in the `exp` claim). You can configure clock skew via `validator.WithAllowedClockSkew()`. - - - - **Signature Verification**: The JWKS provider automatically fetches and caches the correct keys - - **Issuer Value**: The `iss` claim must match exactly (including trailing slash) - - - - The `aud` claim must match your API identifier exactly. - - - - -## Inspecting a Token - -Use [JWT.io](https://jwt.io/) to quickly inspect and debug JWTs. - -![Debugging a JWT on JWT.io](https://cdn2.auth0.com/docs/1.14550.0/media/articles/server-apis/aspnet-core-webapi/jwt-io-debugger-rs256.png) - -In the screenshot above, the token uses **RS256** algorithm. The **Issuer** is **https://jerrie.auth0.com/**, and the **Audience** is **https://rs256.test.api**. - -### Configuration Must Match Exactly - -Your validator configuration must match these values precisely: - -```go -issuerURL, _ := url.Parse("https://jerrie.auth0.com/") - -provider, _ := jwks.NewCachingProvider( - jwks.WithIssuerURL(issuerURL), - jwks.WithCacheTTL(5*time.Minute), -) - -jwtValidator, err := validator.New( - validator.WithKeyFunc(provider.KeyFunc), - validator.WithAlgorithm(validator.RS256), - validator.WithIssuer(issuerURL.String()), // Must match token's 'iss' claim exactly - validator.WithAudience("https://rs256.test.api"), // Must match token's 'aud' claim exactly - validator.WithAllowedClockSkew(30*time.Second), -) -``` - -**Important**: The audience should NOT have a trailing slash. - -## Common Troubleshooting Scenarios - -### 1. Invalid Audience Error - -**Error Message:** -```json -{"error": "invalid_token", "description": "aud claim mismatch"} -``` - -**Cause**: The audience in your configuration doesn't match the `aud` claim in the token. - -**Solution**: -- Verify `AUTH0_AUDIENCE` exactly matches your API Identifier in the Auth0 Dashboard -- Audience should NOT have a trailing slash -- Check that the token was requested with the correct `audience` parameter - -**Code Check:** -```go -jwtValidator, err := validator.New( - validator.WithKeyFunc(provider.KeyFunc), - validator.WithAlgorithm(validator.RS256), - validator.WithIssuer(issuerURL.String()), - validator.WithAudience(cfg.Audience), // Must match API Identifier exactly, no trailing slash -) -``` - -### 2. JWKS Endpoint Unreachable - -**Error Message:** -``` -error fetching keys: connection refused -``` - -**Cause**: The JWKS caching provider cannot reach Auth0's public key endpoint. - -**Solution**: -- Verify network connectivity to Auth0 (check firewall/proxy settings) -- Test JWKS endpoint manually: - ```bash - curl https://your-domain.auth0.com/.well-known/jwks.json - ``` -- Ensure correct Auth0 region (us/eu/au) -- Check if corporate proxy requires configuration - -**Code Check:** -```go -provider, err := jwks.NewCachingProvider( - jwks.WithIssuerURL(issuerURL), - jwks.WithCacheTTL(5*time.Minute), -) -// Ensure issuerURL is correct and accessible -``` - -### 3. Wrong Import Path - -**Error Message:** -``` -cannot find package "github.com/auth0/go-jwt-middleware/v3/..." -``` - -**Cause**: Import statements are incorrect. - -**Solution**: -Ensure all imports use the correct path: - -```go -// Correct -import ( - jwtmiddleware "github.com/auth0/go-jwt-middleware/v3" - "github.com/auth0/go-jwt-middleware/v3/jwks" - "github.com/auth0/go-jwt-middleware/v3/validator" -) -``` - -### 4. Claims Extraction Fails - -**Error Message:** -``` -Failed to retrieve claims -``` - -**Cause**: Incorrect generic type used with `GetClaims[T]()`. - -**Solution**: -Ensure you're using the correct type parameter: - -```go -// Correct - Extract validated claims with custom claims -claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context()) -if err != nil { - // Handle error -} - -// Access custom claims -customClaims, ok := claims.CustomClaims.(*auth.CustomClaims) -if !ok || !customClaims.HasScope("read:messages") { - // Insufficient permissions -} -``` - -### 5. Algorithm Mismatch - -**Error Message:** -```json -{"error": "invalid_token", "description": "unexpected signing method"} -``` - -**Cause**: Token algorithm doesn't match validator configuration. - -**Solution**: -- Auth0 uses RS256 by default (asymmetric) -- Ensure validator specifies `validator.RS256` -- Never use `validator.HS256` for Auth0 tokens (unless specifically configured) - -```go -jwtValidator, err := validator.New( - validator.WithKeyFunc(provider.KeyFunc), - validator.WithAlgorithm(validator.RS256), // Must match Auth0's signing algorithm - validator.WithIssuer(issuerURL.String()), - validator.WithAudience(cfg.Audience), -) -``` - -### 6. Clock Skew Issues - -**Error Message:** -```json -{"error": "invalid_token", "description": "token is expired"} -``` - -**Cause**: Server clock is out of sync, causing valid tokens to appear expired. - -**Solution**: -Configure clock skew tolerance: - -```go -jwtValidator, err := validator.New( - validator.WithKeyFunc(provider.KeyFunc), - validator.WithAlgorithm(validator.RS256), - validator.WithIssuer(issuerURL.String()), - validator.WithAudience(cfg.Audience), - validator.WithAllowedClockSkew(30*time.Second), // Allow 30s tolerance -) -``` - -### 7. Go Version Incompatibility - -**Error Message:** -``` -type parameter requires go1.24 or later (required by go-jwt-middleware v3) -``` - -**Cause**: Go version is too old for v3's generics usage. - -**Solution**: -- Upgrade to Go 1.24 or later -- Verify version: `go version` -- Update `go.mod`: `go 1.24` - -## Error Responses - -The middleware returns RFC 6750 compliant error responses with structured JSON: - -```json -{ - "error": "invalid_token", - "description": "token expired" -} -``` - -The `WWW-Authenticate` header also provides standard error information: - -``` -WWW-Authenticate: Bearer error="invalid_token", error_description="token expired" -``` - -## Debugging Tips - - - - The middleware includes built-in logging support: - - ```go - import "log/slog" - - middleware := jwtmiddleware.New( - jwtmiddleware.WithValidator(jwtValidator), - jwtmiddleware.WithLogger(slog.Default()), - ) - ``` - - This will log validation events, making debugging easier. - - - - Add logging to verify your configuration on startup: - - ```go - log.Printf("Validator configured:") - log.Printf(" Issuer: %s", issuerURL.String()) - log.Printf(" Audience: %v", cfg.Audience) - log.Printf(" Algorithm: RS256") - ``` - - - - Use the **Test** tab in your [API settings](https://manage.auth0.com/#/apis) to generate a test token, then verify it works with your API. - - - - Decode your token at [JWT.io](https://jwt.io/) and verify: - - `iss` matches your issuer (with trailing slash) - - `aud` contains your API identifier - - `exp` is in the future - - `alg` is RS256 - - - -## Getting Help - -If you're still experiencing issues after following this guide: - - - - Check the official SDK documentation and examples - - - Search for similar issues or report a new one - - - Ask questions and get help from the community - - - Contact Auth0 support for enterprise assistance - - - -[Edit on GitHub](https://github.com/auth0/docs/edit/master/articles/quickstart/backend/golang/03-troubleshooting.md) diff --git a/main/docs/quickstart/backend/golang/index.mdx b/main/docs/quickstart/backend/golang/index.mdx index 6f23869440..ddb5673612 100644 --- a/main/docs/quickstart/backend/golang/index.mdx +++ b/main/docs/quickstart/backend/golang/index.mdx @@ -1,704 +1,898 @@ --- -title: "Go API: Authorization with JWT Middleware" -description: Secure your Go API with Auth0 using go-jwt-middleware v3 +mode: wide +description: This guide demonstrates how to protect Go API endpoints using JWT access tokens with the go-jwt-middleware v3 SDK. +sidebarTitle: Go API +title: Protect Your Go API --- + import {AuthCodeBlock} from "/snippets/AuthCodeBlock.jsx"; -##### By Kunal Dawar +export const envSnippet = `# The URL of your Auth0 Tenant Domain. +# If you're using a Custom Domain, set this to that value instead. +AUTH0_DOMAIN='{yourDomain}' -This tutorial demonstrates how to add authorization to a Go API using **go-jwt-middleware v3**. Learn how to validate JWTs, protect endpoints, and enforce permission-based access control. +# Your Auth0 API's Identifier (from Step 2) +# Example: https://my-go-api.example.com +AUTH0_AUDIENCE='{yourApiIdentifier}'`; - - Complete working example with tests • Go 1.24+ required - + - -**New to Auth0?** Learn [how Auth0 works](/docs/get-started/auth0-overview) and read about [implementing API authentication and authorization](/docs/get-started/authentication-and-authorization-flow) using the OAuth 2.0 framework. - +If you use an AI coding assistant like Claude Code, Cursor, or GitHub Copilot, you can add Auth0 API authentication automatically in minutes using [agent skills](https://agentskills.io/home). -## What You'll Build +**Install:** - - - Routes accessible without authentication - - - JWT validation for authenticated users - - - Scope validation for fine-grained control - - - Graceful shutdown, timeouts, and error handling - - +```bash +npx skills add auth0/agent-skills --skill auth0-quickstart --skill go-jwt-middleware +``` -## Prerequisites +**Then ask your AI assistant:** -Before starting, ensure you have: +```text +Add Auth0 JWT authentication to my Go API +``` - - - go-jwt-middleware v3 requires Go 1.24+ and uses generics for type-safe claims. Check your version: - ```bash - go version - ``` - Install or update at [go.dev/doc/install](https://go.dev/doc/install) - +Your AI assistant will automatically create your Auth0 API, fetch credentials, install `go-jwt-middleware`, configure the validator, and protect your API endpoints with JWT validation. [Full agent skills documentation →](/quickstart/agent-skills) - - [Sign up for free](https://auth0.com/signup) if you don't have an account. - + - - Familiarity with Go syntax, HTTP servers, and package management. - - + + **Prerequisites:** Before you begin, ensure you have the following installed: -## Step 1: Configure Auth0 API + - **[Go](https://go.dev/doc/install)** 1.24 or newer (required for generics support in go-jwt-middleware v3) + - **[Git](https://git-scm.com/downloads)** for version control + + Verify installation: `go version` + + +## Get Started + +You'll build a Go API with three endpoints demonstrating different protection levels: public access, JWT-authenticated, and permission-scoped. The complete implementation uses [go-jwt-middleware v3](https://github.com/auth0/go-jwt-middleware) with Go's standard `net/http` library. + + + Complete working example with tests + - - In the [APIs](https://manage.auth0.com/#/apis) section of the Auth0 dashboard, click **Create API**. + + Create a new directory for your Go API and initialize a module. - ![Create API](https://cdn2.auth0.com/docs/1.14550.0/media/articles/server-apis/create-api.png) - + ```shellscript + mkdir myapi && cd myapi + go mod init github.com/yourorg/myapi + ``` - - - **Name**: `Quickstarts` (or any descriptive name) - - **Identifier**: `https://quickstarts/api` (this becomes your `audience`) - - **Signing Algorithm**: Leave as **RS256** (recommended) + Install the required dependencies: - - The API Identifier is a logical identifier - it doesn't need to be a real URL. Use a format like `https://quickstarts/api` for clarity. - - + ```shellscript + go get github.com/auth0/go-jwt-middleware/v3 + go get github.com/joho/godotenv + go mod download + ``` - - By default, your API uses **RS256** (asymmetric algorithm) for signing tokens: - - Auth0 signs tokens with a **private key** - - Your API verifies tokens with a **public key** (from JWKS) - - Public keys are automatically fetched from: `https://{yourDomain}/.well-known/jwks.json` + Create the project structure: - Learn more about [JSON Web Key Sets (JWKS)](/docs/secure/tokens/json-web-tokens/json-web-key-sets) - - + ```shellscript + mkdir -p cmd/server internal/auth internal/config internal/handlers + touch .env cmd/server/main.go internal/config/auth.go internal/auth/claims.go internal/auth/validator.go internal/auth/middleware.go internal/handlers/api.go + ``` -## Step 2: Define API Permissions + + ```go go.mod + module github.com/yourorg/myapi -Permissions (scopes) let you define how resources can be accessed. For example, grant `read` access to managers and `write` access to administrators. + go 1.24 - - - In your API settings, click the **Permissions** tab. + require ( + github.com/auth0/go-jwt-middleware/v3 v3.1.0 + github.com/joho/godotenv v1.5.1 + ) + ``` + + + + + Next, you need to create a new API on your Auth0 tenant and add the environment variables to your project. + + You have two options to set up your Auth0 API: use a CLI command or configure manually via the Dashboard: + + + + Run the following command in your project's root directory to create an Auth0 API: + + + ```shellscript Mac + # Install Auth0 CLI (if not already installed) + brew tap auth0/auth0-cli && brew install auth0 + + # Create Auth0 API + auth0 apis create \ + --name "My Go API" \ + --identifier https://my-go-api.example.com + ``` + + ```powershell Windows + # Install Auth0 CLI (if not already installed) + scoop bucket add auth0 https://github.com/auth0/scoop-auth0-cli.git + scoop install auth0 + + # Create Auth0 API + auth0 apis create ` + --name "My Go API" ` + --identifier https://my-go-api.example.com + ``` + + + After creation, copy the **Identifier** and your **Domain** values, then create your `.env` file: + + + + + This command will: + 1. Check if you're authenticated (and prompt for login if needed) + 2. Create an Auth0 API with the specified identifier + 3. Display the API details including the domain and identifier + + + + + 1. Go to the [Auth0 Dashboard](https://manage.auth0.com/dashboard/) + 2. Navigate to **Applications** → **APIs** → **Create API** + 3. Enter a name for your API (e.g., "My Go API") + 4. Set the **Identifier** (e.g., `https://my-go-api.example.com`) + - This is your API audience and must be a valid URL format + - It doesn't need to be a real URL, it's just an identifier + 5. Keep **Signing Algorithm** as **RS256** + 6. Click **Create** + 7. Copy the **Identifier** value from the **Settings** tab + + Create your `.env` file with the following values: + + ```bash .env + AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN + AUTH0_AUDIENCE=YOUR_API_IDENTIFIER + ``` + + + Replace `YOUR_AUTH0_DOMAIN` with your Auth0 tenant domain (e.g., `dev-abc123.us.auth0.com`) and `YOUR_API_IDENTIFIER` with your API identifier from the dashboard (e.g., `https://my-go-api.example.com`). + + + + + + **Security**: Never commit `.env` files to version control. Add `.env` to your `.gitignore` file. + - - Create the following permission for this tutorial: + + Permissions (scopes) let you define how resources can be accessed. For example, grant `read` access to managers and `write` access to administrators. + + 1. In your API settings, click the **Permissions** tab + 2. Create the following permission: | Permission | Description | |------------|--------------| | `read:messages` | Read messages from the API | - ![Configure Permissions](https://cdn2.auth0.com/docs/1.14550.0/media/articles/server-apis/configure-permissions.png) - - This tutorial uses the `read:messages` scope to protect the scoped endpoint. + This tutorial uses the `read:messages` scope to protect the scoped endpoint. You can define additional permissions based on your application's needs. - - - This example demonstrates: + + Create a config package to load and validate environment variables. - - ✅ Extracting JWTs from the `Authorization: Bearer ` header - - ✅ Validating tokens using Auth0's [JWKS](/docs/secure/tokens/json-web-tokens/json-web-key-sets) - - ✅ Checking token expiration and claims - - ✅ Enforcing permission-based access with scopes - - ✅ Type-safe claims extraction using Go generics + ```go internal/config/auth.go lines + package config - Learn more: [Validate Access Tokens](/docs/secure/tokens/access-tokens/validate-access-tokens) - + import ( + "fmt" + "os" + ) + + type AuthConfig struct { + Domain string + Audience string + } -## Step 3: Install Dependencies + func LoadAuthConfig() (*AuthConfig, error) { + domain := os.Getenv("AUTH0_DOMAIN") + if domain == "" { + return nil, fmt.Errorf("AUTH0_DOMAIN environment variable required") + } - - - Create your project directory and initialize a Go module: + audience := os.Getenv("AUTH0_AUDIENCE") + if audience == "" { + return nil, fmt.Errorf("AUTH0_AUDIENCE environment variable required") + } - ```bash - mkdir myapi && cd myapi - go mod init github.com/yourorg/myapi + return &AuthConfig{ + Domain: domain, + Audience: audience, + }, nil + } ``` + + **What this does:** + - Loads Auth0 domain and audience from environment variables + - Validates that required configuration is present at startup + - Returns a type-safe config struct for use across the application - - Install the v3 go-jwt-middleware package: + + Custom claims allow you to extract and validate application-specific data from JWTs. The validator is the core component that verifies tokens against Auth0. - ```bash - go get github.com/auth0/go-jwt-middleware/v3 - go get github.com/joho/godotenv - ``` + + + ```go internal/auth/claims.go lines + package auth - - **Version Important**: Ensure you install **v3** (not v2). V3 uses generics for type-safe claims handling and requires Go 1.24+. - - + import ( + "context" + "fmt" + "strings" + ) - - ```bash - go mod download - ``` + // CustomClaims contains custom data we want to parse from the JWT. + type CustomClaims struct { + Scope string `json:"scope"` + } - Your `go.mod` should look like this: + // Validate ensures the custom claims are properly formatted. + func (c *CustomClaims) Validate(ctx context.Context) error { + if c.Scope == "" { + return nil + } - ```go - module github.com/yourorg/myapi + if strings.TrimSpace(c.Scope) != c.Scope { + return fmt.Errorf("scope claim has invalid whitespace") + } - go 1.24 + if strings.Contains(c.Scope, " ") { + return fmt.Errorf("scope claim contains double spaces") + } - require ( - github.com/auth0/go-jwt-middleware/v3 v3.0.0 - github.com/joho/godotenv v1.5.1 - ) - ``` + 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 + } + ``` + + + + ```go internal/auth/validator.go lines + 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) { + // Construct issuer URL (must include trailing slash) + issuerURL, err := url.Parse("https://" + domain + "/") + if err != nil { + return nil, fmt.Errorf("failed to parse issuer URL: %w", err) + } + + // Initialize JWKS provider using v3 options pattern + 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) + } + + // Create validator using v3 options pattern + 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 + } + ``` + + + + **Key points:** + - The `Validate` method is called automatically by the middleware after parsing the JWT + - `HasScope` parses space-separated scopes for permission-based access control + - The validator uses JWKS caching (5 min TTL) and allows 30s clock skew + - RS256 algorithm is explicitly set to prevent [algorithm confusion attacks](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) - -## Step 4: Configure Environment Variables + + The middleware wraps the validator for HTTP requests. The handlers demonstrate three protection levels: public, private, and permission-scoped. + + + + ```go internal/auth/middleware.go lines + 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."}`)) + }), + ) + } + ``` + + + + ```go internal/handlers/api.go expandable lines + package handlers + + import ( + "encoding/json" + "net/http" + + "github.com/yourorg/myapi/internal/auth" + jwtmiddleware "github.com/auth0/go-jwt-middleware/v3" + "github.com/auth0/go-jwt-middleware/v3/validator" + ) + + // PublicHandler - no authentication required + 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) + } -Create a `.env` file in your project root to store Auth0 configuration: + // PrivateHandler - requires valid JWT + 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) + } -export const codeExample = `# The URL of your Auth0 Tenant Domain. -# If you're using a Custom Domain, set this to that value instead. -AUTH0_DOMAIN='{yourDomain}' + // ScopedHandler - requires 'read:messages' permission + 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) + } + ``` + + + + **Protection levels:** + - **Public** (`/api/public`) — No authentication required + - **Private** (`/api/private`) — Valid JWT required + - **Scoped** (`/api/private-scoped`) — Valid JWT + `read:messages` permission required + -# Your Auth0 API's Identifier (from Step 1) -# Example: https://quickstarts/api -AUTH0_AUDIENCE='{yourApiIdentifier}'`; + + Wire everything together in the main entry point with production-ready timeouts and graceful shutdown: - + ```go cmd/server/main.go expandable lines + package main - -**Security**: Never commit `.env` files to version control. Add `.env` to your `.gitignore` file. - + import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "time" -## Step 5: Create Configuration Package + "github.com/yourorg/myapi/internal/auth" + "github.com/yourorg/myapi/internal/config" + "github.com/yourorg/myapi/internal/handlers" + "github.com/joho/godotenv" + ) -Create `internal/config/auth.go` to load and validate environment variables: + func main() { + // Load environment variables from .env file + if err := godotenv.Load(); err != nil { + log.Println("No .env file found, using environment variables") + } -```go -package config + // Load Auth0 configuration + cfg, err := config.LoadAuthConfig() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } -import ( - "fmt" - "os" -) + // Create JWT validator + jwtValidator, err := auth.NewValidator(cfg.Domain, cfg.Audience) + if err != nil { + log.Fatalf("Failed to create validator: %v", err) + } -type AuthConfig struct { - Domain string - Audience string -} + // Create HTTP middleware + middleware, err := auth.NewMiddleware(jwtValidator) + if err != nil { + log.Fatalf("Failed to create middleware: %v", err) + } -func LoadAuthConfig() (*AuthConfig, error) { - domain := os.Getenv("AUTH0_DOMAIN") - if domain == "" { - return nil, fmt.Errorf("AUTH0_DOMAIN environment variable required") - } + // Setup routes + 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))) + + // Configure server with production timeouts + srv := &http.Server{ + Addr: ":8080", + Handler: mux, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } - audience := os.Getenv("AUTH0_AUDIENCE") - if audience == "" { - return nil, fmt.Errorf("AUTH0_AUDIENCE environment variable required") - } + // Start server in goroutine + go func() { + log.Println("Server starting on :8080") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } + }() + + // Graceful shutdown + 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) + } - return &AuthConfig{ - Domain: domain, - Audience: audience, - }, nil -} -``` + log.Println("Server exited") + } + ``` - - - **Benefits of centralized configuration**: - - Single source of truth for environment variables - - Clear error messages for missing configuration - - Type-safe configuration access - - Easy to test and mock in unit tests - - Fail fast on startup if config is invalid - + + ``` + myapi/ + ├── cmd/ + │ └── server/ + │ └── main.go # Application entry point + ├── internal/ + │ ├── auth/ + │ │ ├── claims.go # Custom JWT claims + │ │ ├── middleware.go # JWT middleware + │ │ └── validator.go # JWT validator + │ ├── config/ + │ │ └── auth.go # Configuration loader + │ └── handlers/ + │ └── api.go # HTTP handlers (public, private, scoped) + ├── .env # Environment variables (not committed) + ├── .gitignore + ├── go.mod + └── go.sum + ``` + + - - **Configuration management**: - - Validate all required config on startup (fail fast) - - Use different `.env` files for different environments - - Consider using config validation libraries for complex setups - - Document all environment variables in a `.env.example` file - - + + Start the development server: -## Step 6: Create Custom Claims + ```shellscript + go run cmd/server/main.go + ``` -Custom claims allow you to extract and validate application-specific data from JWTs. Create `internal/auth/claims.go`: + You should see: `Server starting on :8080` -```go -package auth + Test the public endpoint (no authentication required): -import ( - "context" - "fmt" - "strings" -) + ```bash + curl http://localhost:8080/api/public + ``` -// CustomClaims contains custom data we want to parse from the JWT. -type CustomClaims struct { - Scope string `json:"scope"` -} + You should see: -// 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 + ```json + { + "message": "Hello from a public endpoint! You don't need to be authenticated to see this." } + ``` - // 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") - } + Test the private endpoint without a token (should fail): - if strings.Contains(c.Scope, " ") { - return fmt.Errorf("scope claim contains double spaces") - } + ```bash + curl http://localhost:8080/api/private + ``` - return nil -} + You should see a 401 Unauthorized error: -// HasScope checks whether our claims have a specific scope. -func (c *CustomClaims) HasScope(expectedScope string) bool { - if c.Scope == "" { - return false + ```json + { + "message": "Failed to validate JWT." } + ``` - scopes := strings.Split(c.Scope, " ") - for _, scope := range scopes { - if scope == expectedScope { - return true - } - } - return false -} -``` + To test with a valid token, navigate to your API in the [Auth0 Dashboard](https://manage.auth0.com/#/apis), click the **Test** tab, and copy the access token. Then run: + + ```bash + curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8080/api/private + ``` - - **Automatic Validation**: The `Validate` method is called automatically by the middleware after parsing the JWT. You don't need to call it manually - it's part of the validation chain. + Test the scoped endpoint (requires `read:messages` permission): - **Scope is Optional**: The validation allows empty scopes because not all endpoints require permissions. This works for: - - `/api/private` - Requires only authentication (no scope needed) - - `/api/private-scoped` - Requires authentication + `read:messages` scope + ```bash + curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + http://localhost:8080/api/private-scoped + ``` + + - **Format Validation**: When scope is present, validation ensures: - - No leading/trailing whitespace - - No double spaces (ensures clean parsing) - - Properly formatted space-separated values + + **Checkpoint** - **HasScope Method**: Returns `false` if scope is empty, preventing false positives. + You should now have a protected Go API. Your API: + 1. Accepts requests to public endpoints without authentication + 2. Rejects requests to protected endpoints without a valid token + 3. Validates JWT tokens against your Auth0 domain and audience + 4. Enforces permission-based access control using scopes + - **Validation Flow**: - 1. JWT is extracted from the `Authorization` header - 2. Claims are unmarshaled into `CustomClaims` struct - 3. `Validate(ctx)` is automatically called - 4. If validation fails, JWT is rejected before reaching your handlers +--- - +## Calling Your API -## Step 7: Create JWT Validator +You can call your protected API from any application by passing an access token in the `Authorization` header as a Bearer token. -The validator is the core component that verifies tokens. Create `internal/auth/validator.go`: + -```go -package auth + +```bash cURL +curl --request GET \ + --url http://localhost:8080/api/private \ + --header 'authorization: Bearer YOUR_ACCESS_TOKEN' +``` + +```go Go +package main import ( "fmt" - "net/url" - "time" - - "github.com/auth0/go-jwt-middleware/v3/jwks" - "github.com/auth0/go-jwt-middleware/v3/validator" + "net/http" + "io" ) -func NewValidator(domain, audience string) (*validator.Validator, error) { - // Construct issuer URL (must include trailing slash) - issuerURL, err := url.Parse("https://" + domain + "/") - if err != nil { - return nil, fmt.Errorf("failed to parse issuer URL: %w", err) - } +func main() { + url := "http://localhost:8080/api/private" - // Initialize JWKS provider using v3 options pattern - 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) - } + req, _ := http.NewRequest("GET", url, nil) + req.Header.Add("authorization", "Bearer YOUR_ACCESS_TOKEN") - // Create validator using v3 options pattern - jwtValidator, err := validator.New( - validator.WithKeyFunc(provider.KeyFunc), // Provides public keys for RS256 - validator.WithAlgorithm(validator.RS256), // Algorithm (prevents confusion attacks) - validator.WithIssuer(issuerURL.String()), // Validates 'iss' claim - validator.WithAudience(audience), // Validates 'aud' claim - validator.WithCustomClaims(func() validator.CustomClaims { - return &CustomClaims{} // Returns our custom claims from claims.go - }), - validator.WithAllowedClockSkew(30*time.Second), // Allows 30s clock drift - ) - if err != nil { - return nil, fmt.Errorf("failed to create validator: %w", err) - } + res, _ := http.DefaultClient.Do(req) + defer res.Body.Close() + + body, _ := io.ReadAll(res.Body) - return jwtValidator, nil + fmt.Println(res) + fmt.Println(string(body)) } ``` - - - The validator performs these security checks on every JWT: - - 1. **Signature verification** - Using Auth0's public keys from JWKS - 2. **Issuer validation** - `iss` claim matches your Auth0 domain - 3. **Audience validation** - `aud` claim matches your API identifier - 4. **Expiration check** - Token hasn't expired (`exp` claim) - 5. **Time validity** - Token is currently valid (`nbf` and `iat` claims) - - - - **JWKS Caching**: Automatically fetches and caches Auth0's public keys every 5 minutes, reducing network calls. - - **Algorithm Specification**: Explicitly sets RS256 to prevent [algorithm confusion attacks](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). +```javascript Node.js +const axios = require("axios").default; - **Clock Skew Tolerance**: Allows 30 seconds for distributed system clock differences. +const options = { + method: 'GET', + url: 'http://localhost:8080/api/private', + headers: {authorization: 'Bearer YOUR_ACCESS_TOKEN'} +}; - **Options Pattern**: V3 uses functional options (`WithIssuerURL()`, `WithKeyFunc()`, `WithAlgorithm()`, etc.) for flexible, type-safe configuration. - - **Custom Claims**: The `CustomClaims` struct lets you extract custom data from JWTs, like permission scopes. - - - -## Step 8: Create HTTP Middleware +axios.request(options).then(function (response) { + console.log(response.data); +}).catch(function (error) { + console.error(error); +}); +``` -Create `internal/auth/middleware.go` to wrap your validator: +```python Python +import http.client -```go -package auth +conn = http.client.HTTPConnection("localhost", 8080) +headers = { 'authorization': "Bearer YOUR_ACCESS_TOKEN" } -import ( - "log/slog" - "net/http" +conn.request("GET", "/api/private", headers=headers) - jwtmiddleware "github.com/auth0/go-jwt-middleware/v3" - "github.com/auth0/go-jwt-middleware/v3/validator" -) +res = conn.getresponse() +data = res.read() -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."}`)) - }), - ) -} +print(data.decode("utf-8")) ``` + -The middleware: -- Extracts JWT from `Authorization: Bearer ` header -- Validates token using the core validator -- Skips validation for OPTIONS requests (CORS preflight) -- Injects validated claims into request context -- Returns custom error responses on validation failure with structured logging - -## Step 9: Create API Handlers + -Create `internal/handlers/api.go` with three handlers demonstrating different protection levels: + + + + + If you are calling the API from a Single-Page Application or a Mobile/Native application, after the authorization flow is completed, you will get an access token. How you get the token and how you make the call to the API will depend on the type of application you are developing and the framework you are using. + + + + React, Vue, Angular quickstarts with examples + + + iOS, Android, React Native quickstarts + + + + + + If you are calling the API from a command-line tool or another service where there isn't a user entering their credentials, you need to use the [OAuth Client Credentials flow](https://auth0.com/docs/api/authentication#client-credentials). + + + + Register a [Machine to Machine Application](https://manage.auth0.com/#/applications) in your Auth0 Dashboard. + + + + Copy your **Client ID** and **Client Secret** from [Application Settings](https://auth0.com/docs/get-started/dashboard/application-settings). + + + + Use the Client Credentials flow to obtain an access token: + + ```bash + curl --request POST \ + --url 'https://YOUR_AUTH0_DOMAIN/oauth/token' \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=client_credentials \ + --data 'client_id=YOUR_CLIENT_ID' \ + --data client_secret=YOUR_CLIENT_SECRET \ + --data audience=YOUR_API_IDENTIFIER + ``` + + -```go -package handlers + + **Token Reuse**: Auth0 customers are billed based on the number of Machine to Machine access tokens issued. Once your application gets an access token, it should keep using it until it expires to minimize the number of tokens requested. + + + -import ( - "encoding/json" - "net/http" + - "github.com/yourorg/myapi/internal/auth" - jwtmiddleware "github.com/auth0/go-jwt-middleware/v3" - "github.com/auth0/go-jwt-middleware/v3/validator" -) +--- -// PublicHandler - no authentication required -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) -} +## Advanced Usage -// PrivateHandler - requires valid JWT -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) -} + + **DPoP (Demonstrating Proof-of-Possession)** per RFC 9449 provides enhanced security by preventing token theft through cryptographic key binding. -// ScopedHandler - requires 'read:messages' permission -func ScopedHandler(w http.ResponseWriter, r *http.Request) { - // Extract validated claims using generics (v3 feature) - 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 - } + ```go internal/auth/middleware.go + func NewMiddleware(jwtValidator *validator.Validator) *jwtmiddleware.JWTMiddleware { + return jwtmiddleware.New( + jwtmiddleware.WithValidator(jwtValidator), + jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired), + jwtmiddleware.WithLogger(slog.Default()), + ) + } + ``` - // Check for required scope in custom claims - 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 - } + **DPoP Modes:** + - `DPoPAllowed` (default) — Accept both Bearer and DPoP tokens + - `DPoPRequired` — Only accept DPoP tokens, reject Bearer + - `DPoPDisabled` — Only accept Bearer tokens, reject DPoP - 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) -} -``` + + DPoP is recommended for financial APIs, healthcare APIs, and high-security enterprise applications. Learn more in the [DPoP documentation](https://github.com/auth0/go-jwt-middleware#dpop-support). + + -## Step 10: Create Main Server + + Enable CORS to allow requests from web applications. You can use a simple middleware or a library like [rs/cors](https://github.com/rs/cors): -Create `cmd/server/main.go` to wire everything together: + ```bash + go get github.com/rs/cors + ``` -```go -package main + ```go cmd/server/main.go + import "github.com/rs/cors" + + // Wrap the mux with CORS middleware + handler := cors.New(cors.Options{ + AllowedOrigins: []string{"http://localhost:3000"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowedHeaders: []string{"Authorization", "Content-Type"}, + AllowCredentials: true, + }).Handler(mux) + + srv := &http.Server{ + Addr: ":8080", + Handler: handler, + } + ``` -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "time" - - "github.com/yourorg/myapi/internal/auth" - "github.com/yourorg/myapi/internal/config" - "github.com/yourorg/myapi/internal/handlers" - "github.com/joho/godotenv" -) + For production, specify exact origins instead of wildcards. + -func main() { - // Load environment variables from .env file - if err := godotenv.Load(); err != nil { - log.Println("No .env file found, using environment variables") - } + + Enable detailed logging for debugging token validation: - // Load Auth0 configuration - cfg, err := config.LoadAuthConfig() - if err != nil { - log.Fatalf("Failed to load config: %v", err) - } + ```go internal/auth/middleware.go + middleware := jwtmiddleware.New( + jwtmiddleware.WithValidator(jwtValidator), + jwtmiddleware.WithLogger(slog.Default()), + ) + ``` - // Create JWT validator - jwtValidator, err := auth.NewValidator(cfg.Domain, cfg.Audience) - if err != nil { - log.Fatalf("Failed to create validator: %v", err) - } + Add startup verification: - // Create HTTP middleware - middleware, err := auth.NewMiddleware(jwtValidator) - if err != nil { - log.Fatalf("Failed to create middleware: %v", err) - } + ```go cmd/server/main.go + log.Printf("Validator configured:") + log.Printf(" Issuer: https://%s/", cfg.Domain) + log.Printf(" Audience: %s", cfg.Audience) + log.Printf(" Algorithm: RS256") + ``` + - // Setup routes - 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))) - - // Configure server with production timeouts - srv := &http.Server{ - Addr: ":8080", - Handler: mux, - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, - IdleTimeout: 60 * time.Second, - } +--- - // Start server in goroutine - go func() { - log.Println("Server starting on :8080") - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Server failed: %v", err) - } - }() +## Troubleshooting - // Graceful shutdown - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt) - <-quit + + + ### "Failed to validate JWT" or 401 Unauthorized - log.Println("Shutting down server...") - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + **Problem:** The API cannot find or validate the access token. - if err := srv.Shutdown(ctx); err != nil { - log.Fatalf("Server forced to shutdown: %v", err) - } + **Solutions:** + 1. Ensure the `Authorization` header is present: `Authorization: Bearer YOUR_TOKEN` + 2. Check that "Bearer" is included before the token + 3. Verify the token is not expired + 4. Ensure you're using an **access token**, not an ID token - log.Println("Server exited") -} -``` + ### "aud claim mismatch" - - ``` - myapi/ - ├── cmd/ - │ └── server/ - │ └── main.go # Application entry point - ├── internal/ - │ ├── auth/ - │ │ ├── claims.go # Custom JWT claims - │ │ ├── middleware.go # JWT middleware - │ │ └── validator.go # JWT validator - │ ├── config/ - │ │ └── auth.go # Configuration loader - │ └── handlers/ - │ └── api.go # HTTP handlers (public, private, scoped) - ├── .env # Environment variables (not committed) - ├── .env.example # Template for environment variables - ├── .gitignore - ├── go.mod - ├── go.sum - └── README.md - ``` - + **Problem:** The token's audience doesn't match your API. -## Step 11: Test Your API + **Solution:** Verify `AUTH0_AUDIENCE` exactly matches your API Identifier from the Auth0 Dashboard. The audience should NOT have a trailing slash: - - ```bash - go run cmd/server/main.go + # Correct + AUTH0_AUDIENCE=https://my-go-api.example.com + + # Wrong (no trailing slash) + AUTH0_AUDIENCE=https://my-go-api.example.com/ ``` - You should see: `Server starting on :8080` - + The client application must also request a token with the correct audience parameter. - - Navigate to your API in the [Auth0 Dashboard](https://manage.auth0.com/#/apis), click the **Test** tab, and copy the access token. - + ### "unexpected signing method" - - ```bash - curl http://localhost:8080/api/public - ``` + **Problem:** Token algorithm doesn't match validator configuration. - **Expected**: 200 OK with public message - + **Solutions:** + 1. Auth0 uses RS256 by default (asymmetric) + 2. Ensure your validator specifies `validator.RS256` + 3. Never use `validator.HS256` for Auth0 tokens unless specifically configured - - ```bash - curl http://localhost:8080/api/private - ``` + ### JWKS endpoint unreachable - **Expected**: 401 Unauthorized - + **Problem:** The JWKS caching provider cannot reach Auth0's public key endpoint. - - ```bash - curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ - http://localhost:8080/api/private - ``` + **Solutions:** + 1. Check network connectivity to Auth0 (firewall/proxy settings) + 2. Test JWKS endpoint manually: `curl https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json` + 3. Verify correct Auth0 region (us/eu/au) - **Expected**: 200 OK with user information - + ### Wrong import path - - ```bash - curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ - http://localhost:8080/api/private-scoped - ``` + **Problem:** `cannot find package "github.com/auth0/go-jwt-middleware/v3/..."` - **Expected**: 200 OK if token has `read:messages` scope, 403 Forbidden otherwise - - + **Solution:** Ensure all imports use the `/v3` suffix: + + ```go + // Correct + import "github.com/auth0/go-jwt-middleware/v3/validator" -## Continue Learning + // Wrong + import "github.com/auth0/go-jwt-middleware/validator" + ``` - - - Learn how to call your protected API from different client applications - - - Comprehensive debugging tips and solutions to common issues - - + ### Clock skew / token expired errors -## Quick Troubleshooting + **Problem:** Server clock is out of sync, causing valid tokens to appear expired. - - - **Error**: `{"error": "invalid_token", "description": "aud claim mismatch"}` + **Solution:** The validator already includes 30s clock skew tolerance. If you need more, adjust: - **Solution**: Verify `AUTH0_AUDIENCE` matches your API Identifier exactly from the Auth0 Dashboard. - + ```go + validator.WithAllowedClockSkew(60*time.Second) + ``` - - **Error**: `error fetching keys: connection refused` + ### Claims extraction fails - **Solution**: - - Check network connectivity to Auth0 - - Test JWKS endpoint: `curl https://your-tenant.us.auth0.com/.well-known/jwks.json` - - Verify firewall/proxy settings - + **Problem:** `Failed to retrieve claims` when using generics. - - **Error**: `{"error": "invalid_token", "description": "token is expired"}` + **Solution:** Ensure you're using the correct type parameter: - **Solution**: Get a new token from the Auth0 Dashboard Test tab. + ```go + claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context()) + ``` +--- + ## Next Steps - - - Add domain-specific authorization with type-safe claims - - - Implement refresh tokens for long-lived sessions - - - Enable proof-of-possession for enhanced security - - - Pre-launch security review and best practices - - +Now that you have a protected API, consider exploring: + +- **[Role-Based Access Control](https://auth0.com/docs/manage-users/access-control/rbac)** — Implement fine-grained permissions +- **[Access Token Best Practices](https://auth0.com/docs/secure/tokens/access-tokens)** — Learn about token security +- **[Monitor Your API](https://auth0.com/docs/deploy-monitor/logs)** — Set up logging and monitoring +- **[Production Checklist](https://auth0.com/docs/deploy-monitor/production-checks)** — Pre-launch security review --- -[Edit on GitHub](https://github.com/auth0/docs/edit/master/articles/quickstart/backend/golang/01-authorization.md) +## Resources ---- \ No newline at end of file +- **[go-jwt-middleware GitHub](https://github.com/auth0/go-jwt-middleware)** — Source code, examples, and DPoP support +- **[Go API Sample](https://github.com/auth0-samples/auth0-golang-api-samples)** — Complete working example +- **[Auth0 Community](https://community.auth0.com/)** — Get help from the community diff --git a/main/docs/quickstart/backend/golang/interactive.mdx b/main/docs/quickstart/backend/golang/interactive.mdx deleted file mode 100644 index 1bb7cebd00..0000000000 --- a/main/docs/quickstart/backend/golang/interactive.mdx +++ /dev/null @@ -1,609 +0,0 @@ ---- -mode: wide -description: This tutorial demonstrates how to add authorization to a Go API using go-jwt-middleware. -sidebarTitle: Go API -title: Add Authorization to Your Go Application ---- -import { Recipe, Content, Section, SideMenu, SideMenuSectionItem, SignUpForm } from "/snippets/recipe.jsx"; -import { LoggedInForm } from "/snippets/Login.jsx"; -import Config from "/snippets/quickstart/backend/golang/config.go.mdx"; -import Validator from "/snippets/quickstart/backend/golang/validator.go.mdx"; -import Claims from "/snippets/quickstart/backend/golang/claims.go.mdx"; -import Middleware from "/snippets/quickstart/backend/golang/middleware.go.mdx"; -import Handlers from "/snippets/quickstart/backend/golang/handlers.go.mdx"; -import Main from "/snippets/quickstart/backend/golang/main.go.mdx"; - -import {QuickstartButtons} from "/snippets/QuickstartButtons.jsx"; - -import {AuthCodeGroup} from "/snippets/AuthCodeGroup.jsx"; - - - -export const sections = [ - { id: "prerequisites", title: "Prerequisites" }, - { id: "configure-api", title: "Configure Auth0 API" }, - { id: "define-permissions", title: "Define permissions" }, - { id: "install-dependencies", title: "Install dependencies" }, - { id: "configure-your-application", title: "Configure your application" }, - { id: "create-custom-claims", title: "Create custom claims" }, - { id: "create-jwt-validator", title: "Create JWT validator" }, - { id: "create-http-middleware", title: "Create HTTP middleware" }, - { id: "protect-api-endpoints", title: "Protect API endpoints" }, - { id: "checkpoint", title: "Checkpoint" } -] - - - - This guide demonstrates how to integrate Auth0 with any new or existing Go API application using [go-jwt-middleware](https://github.com/auth0/go-jwt-middleware). - - Each Auth0 API uses the API Identifier, which your application needs to validate the access token. - - - **New to Auth0?** Learn [how Auth0 works](/docs/get-started/auth0-overview) and read about [implementing API authentication and authorization](/docs/get-started/authentication-and-authorization-flow) using the OAuth 2.0 framework. - - -
- Before we begin, make sure you have the following: - - - - Required for v3 of go-jwt-middleware (which uses generics). Check your version with `go version` - - - Sign up for free at [auth0.com/signup](https://auth0.com/signup) - - - VS Code, GoLand, or your preferred editor - - - Command line for running Go commands - - - - - **New to Go?** This tutorial assumes basic familiarity with Go syntax and HTTP servers. Check out [Go by Example](https://gobyexample.com/) if you need a refresher. - -
- -
- Create an API in your Auth0 Dashboard to get started. - - - - Go to the [APIs](https://manage.auth0.com/#/apis) section in the Auth0 dashboard and click **Create API**. - - ![Create API](https://cdn2.auth0.com/docs/1.14550.0/media/articles/server-apis/create-api.png) - - - - - **Name**: `Quickstarts` (or any descriptive name) - - **Identifier**: `https://quickstarts/api` (this becomes your `audience`) - - **Signing Algorithm**: Leave as **RS256** (recommended) - - - The API Identifier is a logical identifier - it doesn't need to be a real URL. - - - - - Your API uses **RS256** (asymmetric algorithm): - - Auth0 signs tokens with a **private key** - - Your API verifies tokens with a **public key** (from JWKS) - - Public keys are fetched from: `https://{yourDomain}/.well-known/jwks.json` - - -
- -
- Permissions let you define how resources can be accessed on behalf of the user with a given access token. - - In your API settings, click the **Permissions** tab and create the following permission: - - | Permission | Description | - |------------|--------------| - | `read:messages` | Read messages from the API | - - ![Configure Permissions](https://cdn2.auth0.com/docs/1.14550.0/media/articles/server-apis/configure-permissions.png) - - - This tutorial uses the `read:messages` scope to protect the scoped endpoint. - -
- -
- Set up your Go project structure and install the required dependencies. - - - - ```bash - mkdir myapi && cd myapi - ``` - - - - ```bash - go mod init github.com/yourorg/myapi - ``` - - - - ```bash - go get github.com/auth0/go-jwt-middleware/v3 - go get github.com/joho/godotenv - ``` - - - - ```bash - go mod download - ``` - - - - - ```go - // go.mod - module github.com/yourorg/myapi - - go 1.24 - - require ( - github.com/auth0/go-jwt-middleware/v3 v3.0.0 - github.com/joho/godotenv v1.5.1 - ) - ``` - - - - **Go 1.24+ Required**: This implementation uses generics for type-safe claims handling. Verify your version with `go version`. - -
- -
- Create a `.env` file in your project root to store Auth0 configuration: - - ```env lines - # The URL of your Auth0 Tenant Domain. - # If you're using a Custom Domain, set this to that value instead. - AUTH0_DOMAIN='{yourDomain}' - - # Your Auth0 API's Identifier - # Example: https://quickstarts/api - AUTH0_AUDIENCE='{yourApiIdentifier}' - ``` - - Create a config package to load and validate these environment variables in `internal/config/auth.go` (see the code in the right sidebar). - - - - **Benefits of centralized configuration**: - - Single source of truth for environment variables - - Clear error messages for missing configuration - - Type-safe configuration access - - Easy to test and mock - - - - - Never commit `.env` files to version control - - Add `.env` to your `.gitignore` file - - Use different `.env` files for different environments - - Validate all required config on startup (fail fast) - - -
- -
- Create custom claims to extract and validate application-specific data from JWTs in `internal/auth/claims.go` (see the code in the right sidebar). - - - - **Custom claims allow you to**: - - Extract permissions (scopes) from JWT tokens - - Validate claim formatting automatically - - Add domain-specific authorization logic - - Keep business logic separate from validation - - **Automatic Validation**: The `Validate` method is called automatically by the middleware after parsing the JWT. - - - - The `Validate` method ensures: - - **Scope is optional** - allows tokens without scopes (for `/api/private` endpoint) - - **Format validation** - when scope exists, checks for proper formatting - - **No whitespace issues** - prevents malformed scope strings - - **Called automatically** - runs before your handlers execute - - - - The `HasScope` helper method: - - Parses space-separated scopes (e.g., `"read:messages write:messages"`) - - Returns `false` if scope is empty (prevents false positives) - - Used in handlers to enforce permission-based access - - -
- -
- Create the JWT validator in `internal/auth/validator.go` (see the code in the right sidebar). - - - - The validator performs these security checks on every JWT: - - **Signature verification** using Auth0's public keys (JWKS) - - **Issuer validation** - `iss` claim matches your Auth0 domain - - **Audience validation** - `aud` claim matches your API identifier - - **Expiration check** - token hasn't expired (`exp` claim) - - **Time validity** - token is currently valid (`nbf` and `iat` claims) - - - - **Options Pattern**: Uses functional options like `validator.WithAllowedClockSkew()` for flexible configuration - - **JWKS Caching**: Automatically fetches and caches Auth0's public keys every 5 minutes, reducing network calls - - **Algorithm Specification**: Explicitly sets RS256 to prevent [algorithm confusion attacks](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) - - **Clock Skew Tolerance**: Allows 30 seconds for distributed system clock differences - - - - The **core-adapter architecture** separates JWT validation logic (core) from HTTP concerns (adapter). This means: - - You can reuse the same validator across different frameworks - - Easier to test validation logic in isolation - - Can use with HTTP, gRPC, or any other transport - - -
- -
- Create the HTTP middleware that wraps your validator in `internal/auth/middleware.go` (see the code in the right sidebar). - - Built-in structured logging using Go's standard `log/slog` package. The middleware will log token validation events for debugging. - - The middleware: - - Extracts JWT from `Authorization: Bearer ` header - - Validates token using the core validator - - Skips validation for OPTIONS requests (CORS preflight) - - Injects validated claims into request context - - Returns RFC 6750 compliant error responses on failure -
- -
- Create API handlers with different protection levels in `internal/handlers/api.go` and the main server in `cmd/server/main.go` (see the code in the right sidebar). - - **Generic Claims Feature**: - - `GetClaims[*validator.ValidatedClaims]` uses generics for compile-time type safety - - No runtime type assertions that can panic - - IDE autocomplete for claim fields - - Custom claims implement `Validate(ctx context.Context) error` for domain-specific validation - - **Endpoint Protection Levels:** - 1. **Public** (`/api/public`): No authentication required - 2. **Private** (`/api/private`): Valid JWT required - 3. **Scoped** (`/api/private-scoped`): Valid JWT + specific scope (`read:messages`) required - - **Production Features:** - - Graceful shutdown handling - - Configurable timeouts (read, write, idle) - - Proper error handling - - Environment variable configuration -
- -
- Now let's test your implementation to make sure everything works correctly. - - - - ```bash - go run cmd/server/main.go - ``` - - You should see: `Server starting on :8080` - - - The server automatically loads environment variables from your `.env` file using godotenv. - - - - - - - 1. Go to your API in the [Auth0 Dashboard](https://manage.auth0.com/#/apis) - 2. Click the **Test** tab - 3. Click **Copy Token** to get a valid JWT - - - - - - - - Implement the [Client Credentials Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow) for machine-to-machine authentication. - - - - - - This endpoint should work without authentication: - - ```bash - curl http://localhost:8080/api/public - ``` - - **Expected**: 200 OK with message "Hello from a public endpoint!" - - - - This should fail with a 401 error: - - ```bash - curl http://localhost:8080/api/private - ``` - - **Expected**: 401 Unauthorized `{"error": "invalid_token"}` - - - - This should succeed with a valid token: - - ```bash - curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ - http://localhost:8080/api/private - ``` - - **Expected**: 200 OK with user information - - - - This requires the `read:messages` scope: - - ```bash - curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ - http://localhost:8080/api/private-scoped - ``` - - **Expected**: 200 OK if token has `read:messages` scope, 403 Forbidden otherwise - - - - - **Checkpoint Complete!** If all tests passed, your Auth0 integration is working correctly. Your API now: - - Validates JWTs from Auth0 - - Protects private endpoints - - Enforces permission-based access - -
- - ## Continue the Tutorial - - - - Learn how to call your protected Go API from client applications with examples in cURL, Go, Java, Node.js, Python, Ruby, and Swift. - - - Comprehensive debugging guide covering common issues like invalid issuer, audience errors, JWKS problems, and more. - - - - ### Quick Troubleshooting - - - - **Error Message:** - ```json - {"error": "invalid_token", "description": "aud claim mismatch"} - ``` - - **Solution:** Verify `AUTH0_AUDIENCE` exactly matches your API Identifier from the Auth0 Dashboard. - - ```bash - # Correct - AUTH0_AUDIENCE=https://quickstarts/api - - # Wrong (no trailing slash should be added) - AUTH0_AUDIENCE=https://quickstarts/api/ - ``` - - - - **Error Message:** - ``` - error fetching keys: connection refused - ``` - - **Solutions:** - - Check network connectivity to Auth0 (firewall/proxy settings) - - Test JWKS endpoint manually: - ```bash - curl https://your-tenant.us.auth0.com/.well-known/jwks.json - ``` - - Verify correct Auth0 region (us/eu/au) - - - - **Error Message:** - ``` - cannot find package "github.com/auth0/go-jwt-middleware/v3/..." - ``` - - **Solution:** Ensure all imports use the `/v3` suffix: - ```go - // Correct - import "github.com/auth0/go-jwt-middleware/v3/validator" - - // Wrong - import "github.com/auth0/go-jwt-middleware/validator" - ``` - - - - **Error Message:** - ```json - {"error": "invalid_token", "description": "token is expired"} - ``` - - **Solutions:** - - Get a new token from the Auth0 Dashboard Test tab - - Check if your server clock is synchronized - - Adjust clock skew tolerance: - ```go - validator.WithAllowedClockSkew(60*time.Second) - ``` - - - - - **DPoP (Demonstrating Proof-of-Possession)** per RFC 9449 provides enhanced security by preventing token theft through cryptographic key binding. - - **When to use DPoP:** - - Financial APIs handling sensitive transactions - - Healthcare APIs with patient data - - High-security enterprise applications - - **Enable DPoP:** - ```go - // internal/auth/middleware.go - func NewMiddleware(jwtValidator *validator.Validator) *jwtmiddleware.JWTMiddleware { - return jwtmiddleware.New( - jwtmiddleware.WithValidator(jwtValidator), - jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired), - jwtmiddleware.WithLogger(slog.Default()), - ) - } - ``` - - **DPoP Modes:** - - `DPoPAllowed` (default): Accept both Bearer and DPoP tokens - - `DPoPRequired`: Only accept DPoP tokens, reject Bearer - - `DPoPDisabled`: Only accept Bearer tokens, reject DPoP - - Learn more in the [DPoP documentation](https://github.com/auth0/go-jwt-middleware#dpop-support). - - - ### Best Practices - - **Security**: - - Always use HTTPS in production - - Validate algorithms explicitly (prevents algorithm confusion attacks) - - Use short-lived access tokens (15-30 minutes) with refresh tokens - - Enable DPoP for sensitive APIs - - Never log tokens in plaintext - - **Performance**: - - Reuse validator instances (create once at startup) - - Configure JWKS cache appropriately (5-15 minute TTL) - - Set reasonable HTTP timeouts - - Use connection pooling (enabled by default in Go) - - **Maintainability**: - - Use environment variables for credentials - - Define type-safe custom claims structs - - Enable structured logging with `slog` - - Test with mock validators - - Document custom claims with JSON tags - - ### Project Structure - - Recommended structure for production Go APIs: - - ``` - myapi/ - ├── cmd/ - │ └── server/ - │ └── main.go # Application entry point - ├── internal/ - │ ├── auth/ - │ │ ├── claims.go # Custom claims definition - │ │ ├── middleware.go # JWT middleware setup - │ │ └── validator.go # Validator configuration - │ ├── config/ - │ │ └── auth.go # Environment configuration - │ └── handlers/ - │ └── api.go # HTTP handlers - ├── .env # Environment variables (not committed) - ├── .gitignore - ├── go.mod - ├── go.sum - └── README.md - ``` - - ### Next Steps - - Now that you have a working Go API with Auth0 authentication, explore advanced features: - - - - Add domain-specific authorization logic with type-safe claims - - - Implement refresh token flow for long-lived sessions - - - Enable proof-of-possession for enhanced security - - - Enable multi-tenant authorization for B2B apps - - - Pre-launch security review and best practices - - - Complete SDK reference and advanced examples - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - diff --git a/main/docs/quickstarts.mdx b/main/docs/quickstarts.mdx index 0e2bea6728..4958c18983 100644 --- a/main/docs/quickstarts.mdx +++ b/main/docs/quickstarts.mdx @@ -645,7 +645,7 @@ An API or service protected by Auth0 date: "", badge: "", links: [ - { label: "Quickstart", url: "/docs/quickstart/backend/golang/interactive" }, + { label: "Quickstart", url: "/docs/quickstart/backend/golang" }, ], }}/> diff --git a/main/snippets/QuickstartPage.mdx b/main/snippets/QuickstartPage.mdx index 6b6bfa71d6..3e710f884a 100644 --- a/main/snippets/QuickstartPage.mdx +++ b/main/snippets/QuickstartPage.mdx @@ -257,7 +257,7 @@ export const QuickstartPage = ({ sections }) => { }, { title: 'Go API', - href: '/docs/quickstart/backend/golang/interactive', + href: '/docs/quickstart/backend/golang', img: 'https://cdn2.auth0.com/docs/1.14494.0/img/platforms/golang.svg', }, { diff --git a/main/snippets/quickstart/backend/golang/claims.go.mdx b/main/snippets/quickstart/backend/golang/claims.go.mdx deleted file mode 100644 index 5b9f90679d..0000000000 --- a/main/snippets/quickstart/backend/golang/claims.go.mdx +++ /dev/null @@ -1,48 +0,0 @@ -```go title="internal/auth/claims.go" -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/main/snippets/quickstart/backend/golang/config.go.mdx b/main/snippets/quickstart/backend/golang/config.go.mdx deleted file mode 100644 index 54ad416ce5..0000000000 --- a/main/snippets/quickstart/backend/golang/config.go.mdx +++ /dev/null @@ -1,30 +0,0 @@ -```go title="internal/config/auth.go" -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/main/snippets/quickstart/backend/golang/handlers.go.mdx b/main/snippets/quickstart/backend/golang/handlers.go.mdx deleted file mode 100644 index 54b6698d13..0000000000 --- a/main/snippets/quickstart/backend/golang/handlers.go.mdx +++ /dev/null @@ -1,57 +0,0 @@ -```go title="internal/handlers/api.go" -package handlers - -import ( - "encoding/json" - "net/http" - - "github.com/yourorg/myapi/internal/auth" - jwtmiddleware "github.com/auth0/go-jwt-middleware/v3" - "github.com/auth0/go-jwt-middleware/v3/validator" -) - -// PublicHandler - no authentication required -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) -} - -// PrivateHandler - requires valid JWT -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) -} - -// ScopedHandler - requires 'read:messages' permission -func ScopedHandler(w http.ResponseWriter, r *http.Request) { - // Extract validated claims using generics (v3 feature) - 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 - } - - // Check for required scope in custom claims - 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/main/snippets/quickstart/backend/golang/main.go.mdx b/main/snippets/quickstart/backend/golang/main.go.mdx deleted file mode 100644 index 89b400bfb5..0000000000 --- a/main/snippets/quickstart/backend/golang/main.go.mdx +++ /dev/null @@ -1,80 +0,0 @@ -```go title="cmd/server/main.go" -package main - -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "time" - - "github.com/yourorg/myapi/internal/auth" - "github.com/yourorg/myapi/internal/config" - "github.com/yourorg/myapi/internal/handlers" - "github.com/joho/godotenv" -) - -func main() { - // Load environment variables from .env file - if err := godotenv.Load(); err != nil { - log.Println("No .env file found, using environment variables") - } - - // Load Auth0 configuration - cfg, err := config.LoadAuthConfig() - if err != nil { - log.Fatalf("Failed to load config: %v", err) - } - - // Create JWT validator - jwtValidator, err := auth.NewValidator(cfg.Domain, cfg.Audience) - if err != nil { - log.Fatalf("Failed to create validator: %v", err) - } - - // Create HTTP middleware - middleware, err := auth.NewMiddleware(jwtValidator) - if err != nil { - log.Fatalf("Failed to create middleware: %v", err) - } - - // Setup routes - 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))) - - // Configure server with production timeouts - srv := &http.Server{ - Addr: ":8080", - Handler: mux, - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, - IdleTimeout: 60 * time.Second, - } - - // Start server in goroutine - go func() { - log.Println("Server starting on :8080") - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Server failed: %v", err) - } - }() - - // Graceful shutdown - 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/main/snippets/quickstart/backend/golang/middleware.go.mdx b/main/snippets/quickstart/backend/golang/middleware.go.mdx deleted file mode 100644 index 30d326cb39..0000000000 --- a/main/snippets/quickstart/backend/golang/middleware.go.mdx +++ /dev/null @@ -1,24 +0,0 @@ -```go title="internal/auth/middleware.go" -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/main/snippets/quickstart/backend/golang/validator.go.mdx b/main/snippets/quickstart/backend/golang/validator.go.mdx deleted file mode 100644 index 5976aebd35..0000000000 --- a/main/snippets/quickstart/backend/golang/validator.go.mdx +++ /dev/null @@ -1,46 +0,0 @@ -```go title="internal/auth/validator.go" -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) { - // Construct issuer URL (must include trailing slash) - issuerURL, err := url.Parse("https://" + domain + "/") - if err != nil { - return nil, fmt.Errorf("failed to parse issuer URL: %w", err) - } - - // Initialize JWKS provider using v3 options pattern - 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) - } - - // Create validator using v3 options pattern - jwtValidator, err := validator.New( - validator.WithKeyFunc(provider.KeyFunc), // Provides public keys for RS256 - validator.WithAlgorithm(validator.RS256), // Algorithm (prevents confusion attacks) - validator.WithIssuer(issuerURL.String()), // Validates 'iss' claim - validator.WithAudience(audience), // Validates 'aud' claim - validator.WithCustomClaims(func() validator.CustomClaims { - return &CustomClaims{} // Returns our custom claims from claims.go - }), - validator.WithAllowedClockSkew(30*time.Second), // Allows 30s clock drift - ) - if err != nil { - return nil, fmt.Errorf("failed to create validator: %w", err) - } - - return jwtValidator, nil -} -``` diff --git a/main/snippets/sdks/SdkLibraries.mdx b/main/snippets/sdks/SdkLibraries.mdx index 62c91df93e..ebbe8cf4ea 100644 --- a/main/snippets/sdks/SdkLibraries.mdx +++ b/main/snippets/sdks/SdkLibraries.mdx @@ -263,7 +263,7 @@ export const SdkLibrariesPage = () => { label: "Sample App", url: "https://github.com/auth0-samples/auth0-golang-api-samples/tree/master/01-Quickstart-Go-API", }, - { label: "Quickstart", url: "/docs/quickstart/backend/golang/interactive" }, + { label: "Quickstart", url: "/docs/quickstart/backend/golang" }, ], }, {