standard-library logo
golang logo

Golang API Authorization By Example

Published on July 17, 2024

This guide will help you learn how to do authorization for GoLang API. You'll learn how to integrate Auth0 by Okta with Golang Code to implement the following features:

  • How to setup Golang server and authentication middleware
  • How to do basic authorization.
  • How to do RBAC authorization.

This guide uses the go-jwt-middleware library, which provides developers with an authentication middleware for Golang that validates access tokens that follow the JSON Web Token (JWT) format. You can now secure your Golang APIs following security best practices while writing less code.

How Does Auth0 Work?

With the help of Auth0 by Okta, you don't need to be an expert on identity protocols, such as OAuth 2.0 or OpenID Connect, to understand how to secure your web application stack.

You first integrate your client applications with Auth0. Your application will then redirect users to an Auth0 customizable login page when they need to log in. Once your users log in successfully, Auth0 redirects them to your client app, returning an access token. The client application can then use that access token to authenticate itself with your Go API and access protected resources in your server or database layers.

Get the Starter Project

We have created a starter project to help you learn Golang security concepts through hands-on practice. You can focus on building Golang application middleware and services to secure your application.

Start by cloning the api_standard-library_golang_hello-world repository on its starter branch:

COMMAND
git clone -b starter [email protected]:auth0-developer-hub/api_standard-library_golang_hello-world.git

Once you clone the repo, make api_standard-library_golang_hello-world your current directory:

COMMAND
cd api_standard-library_golang_hello-world

Install the Go project dependencies as follows:

COMMAND
go mod download
go mod tidy

The starter Golang project defines the following API endpoints to let client applications access a simple message resource:

ENDPOINTS
# get a public message
GET /api/messages/public
# get a protected message
GET /api/messages/protected
# get an admin message
GET /api/messages/admin

The starter project does not implement access control for these endpoints. After you follow the steps in this guide, the API server will require each request to have a valid access token on their authorization header to access GET /api/messages/protected and GET /api/messages/admin. GET /api/messages/public would be the only public endpoint. Later, you'll integrate this Golang API with an actual client application using a frontend technology of your choice.

You'll run your Go server locally on port 6060. However, this starter application uses CORS to restrict which origins can request its resources.

You'll manage these configuration elements of your Go application using environment variables. As such, create a .env file under the root project directory:

COMMAND
touch .env

Populate your .env file with the following content:

.env
PORT=6060
CLIENT_ORIGIN_URL=http://localhost:4040

The PORT environment variable defines the port in which your Go API server will run: http://localhost:6060. The CLIENT_ORIGIN_URL variable defines the origin from which a browser can load resources from your Go API — other than your API's origin.

Finally, open another terminal tab and execute this command to run your Go application.

COMMAND
go run ./cmd/api-server/

You are ready to start implementing authorization in this Go project. First, you'll need to configure the Go API server to connect successfully to Auth0. Afterward, you'll use the go-jwt-middleware middleware to validate bearer tokens from incoming API requests.

Configure your application with Auth0

Follow these steps to get started with the Auth0 Identity Platform quickly:

Sign up and create an Auth0 Application

A free account also offers you:

During the sign-up process, you create something called an Auth0 Tenant, representing the product or service to which you are adding authentication.

Once you sign in, Auth0 takes you to the Dashboard. Open the "APIs" section of the Auth0 Dashboard.

Then, click the "Create API" button. A modal opens with a form to provide a name for the API registration and define an API Identifier. Use the following values:

Name
Auth0 Golang Sample Code
Identifier
https://hello-world.example.com

Click the "Create" button to complete the process. Your Auth0 API registration page loads up.

View 'Register APIs' document for more details.

A page loads up, presenting all the details about your application register with Auth0.

Next, locate the "General Settings" section.

Store the "Identifier" value in the following fields to set up your Golang API server in the next section:

In the next step, you'll learn how to help a Go API and Auth0 communicate.

Create a communication bridge between Go Server and Auth0

In this guide, you'll implement token-based authorization. That is, your Go API server will protect an endpoint by requiring that each request to that endpoint contains a valid access token.

The issuer of those access tokens will be an Auth0 authorization server. Your Go API server needs to validate that the access token on a request comes from Auth0.

You'll need the Auth0 Domain and Auth0 Audience values to validate the access tokens.

When setting up APIs in the Auth0 Dashboard, we also refer to the API identifier as the Audience value, which you have already set up in the previous section.

Get the Auth0 domain

Now, follow these steps to get the Auth0 Domain value.

  • Open the Auth0 Domain Settings

  • Locate the bold text in the page description that follows this pattern: tenant-name.region.auth0.com. That's your Auth0 domain!

  • Paste the Auth0 domain value in the following field so that you can use it in the next section to set up your API server:

The region subdomain (au, us, or eu) is optional. Some Auth0 Domains don't have it.

Get an Auth0 access token

You'll also need a test access token to practice making secure calls to your API from a terminal application.

You can get a test access token from the Auth0 Dashboard by following these steps:

  • Head back to the Auth0 registration page of your Go APIs and click on the "Test" tab.
If this is the first time that you are setting up a testing application, click on the "Create & Authorize Test Application" button.
  • Locate the section called "Response" and click on the copy button in the top-right corner.

  • Paste the access token value in the following field so that you can use it in the next sections to test your API server:

When you enter a value in the input fields present on this page, any code snippet that uses such value updates to reflect it. Using the input fields makes it easy to copy and paste code as you follow along.

For security, these configuration values are stored in memory and only used locally. They are gone as soon as you refresh the page! As an extra precaution, you should use values from an Auth0 test application instead of a production one.

Define the Auth0 Environment Variables

Update the .env file under the project directory as follows to integrate the Auth0 Domain and Auth0 Audience values with your Go application:

.env
PORT=6060
CLIENT_ORIGIN_URL=http://localhost:4040
AUTH0_AUDIENCE=AUTH0-AUDIENCE
AUTH0_DOMAIN=AUTH0-DOMAIN

Restart your Go server for your project to become aware of these .env file changes:

COMMAND
go run ./cmd/api-server/

Once you reach the "Request Go API Resources From a Client App" section of this guide, you'll learn how to use CLIENT_ORIGIN_URL along with an Auth0 Audience value to request protected resources from your Go API using a client application that is also protected by Auth0.

Install and Set Up the Authorization Middleware

While OAuth 2.0 doesn't specify a format for access tokens, the Auth0 authorization server issues access tokens in JWT format. You'll need to decode and validate the JWT tokens in your Go API; for that, you'll use the go-jwt-middleware authorization middleware.

COMMAND
go get github.com/auth0/go-jwt-middleware/v2
go get github.com/auth0/go-jwt-middleware/v2/jwks

Update the Go dependencies after the library installation

COMMAND
go mod download

You'll integrate the go-jwt-middleware authentication middleware function with your Go API route handlers to limit who can access them. Go Server will execute this authentication middleware function before the callback function that handles the request is executed.

As you are restricting access to protect resources to requests with a valid access token, you are implementing basic authorization in your Go API endpoints. In this scenario, authorization is a byproduct of authentication. Your users authenticate themselves with Auth0 using your client application to get an access token. An access token is a form of credentials for the client application to authenticate itself with your resource server (your API server).

The access token may contain information that your API server can use to grant access to a resource. However, your API server may require the presence of a valid access token as the only authorization condition for accessing a resource. You can use Auth0 Role-Based Access Control (RBAC) to use permissions to increase those authorization requirements.

You can use the below pattern to integrate your endpoints with the authentication middleware function.

Enforce authorization requirements for endpoint

You can separate the public router handlers from the protected ones using the authentication middleware as a boundary between groups.

Create a new file called auth.go under the pkg/middleware directory:

COMMAND
touch pkg/middleware/auth.go

Then, populate it with the following content:

pkg/middleware/auth.go
package middleware
import (
"log"
"net/http"
"net/url"
"time"
"github.com/auth0/go-jwt-middleware/v2"
"github.com/auth0/go-jwt-middleware/v2/jwks"
"github.com/auth0/go-jwt-middleware/v2/validator"
)
func ValidateJWT(audience, domain string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
issuerURL, err := url.Parse("https://" + domain + "/")
if err != nil {
log.Fatalf("Failed to parse the issuer url: %v", err)
}
provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)
jwtValidator, err := validator.New(
provider.KeyFunc,
validator.RS256,
issuerURL.String(),
[]string{audience},
)
if err != nil {
log.Fatalf("Failed to set up the jwt validator")
}
middleware := jwtmiddleware.New(
jwtValidator.ValidateToken,
)
middleware.CheckJWT(next).ServeHTTP(w, r)
})
}

The ValidateJWT function returns the middleware, which validates the JWT token and verifies the audience claim inside the token. The ValidateJWT uses the go-jwt-middleware library to validate the access token and verifies the audience that is being passed in the argument to the ValidateJWT. The ValidateJWT is being used as a wrapper at the routes declaration place. Notice the route /api/messages/protected below, which is being wrapped by ValidateJWT(audience, domain, http.HandlerFunc(middleware.ProtectedApiHandler)). Wrapping the routes with middleware will execute the middleware first and then the actual handler ProtectedApiHandler.

Open the router.go file and update its content with below:

pkg/router/router.go
package router
import (
"net/http"
"github.com/auth0-developer-hub/api_standard-library_golang_hello-world/pkg/middleware"
)
func Router(audience, domain string) http.Handler {
router := http.NewServeMux()
router.HandleFunc("/", middleware.NotFoundHandler)
router.HandleFunc("/api/messages/public", middleware.PublicApiHandler)
router.Handle("/api/messages/protected", middleware.ValidateJWT(audience, domain, http.HandlerFunc(middleware.ProtectedApiHandler)))
router.Handle("/api/messages/admin", middleware.ValidateJWT(audience, domain, http.HandlerFunc(middleware.AdminApiHandler)))
return middleware.HandleCacheControl(router)
}

By wrapping only the /api/messages/protected and api/messages/admin with the middleware function ValidateJWT, only these endpoints will require authentication from the client, and thus client applications can access the /api/messages/public endpoint without presenting any "proof of authorization" — it is a public endpoint.

For our middleware to work, it needs to know information about Auth0, such as the Audience and the Domain. These values are received from the Router instance. However, we are currently not sending these parameters to the router from the application.

Open the app.go file to specify two new config parameters and pass them over to the router.

cmd/api-server/app.go
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/auth0-developer-hub/api_standard-library_golang_hello-world/pkg/router"
"github.com/rs/cors"
"github.com/unrolled/secure"
)
type Config struct {
Port string
SecureOptions secure.Options
CorsOptions cors.Options
Audience string
Domain string
}
type App struct {
Config Config
}
func (app *App) RunServer() {
newRouter := router.Router(app.Config.Audience, app.Config.Domain)
corsMiddleware := cors.New(app.Config.CorsOptions)
routerWithCORS := corsMiddleware.Handler(newRouter)
secureMiddleware := secure.New(app.Config.SecureOptions)
finalHandler := secureMiddleware.Handler(routerWithCORS)
server := &http.Server{
Addr: ":" + app.Config.Port,
Handler: finalHandler,
}
log.Printf("API server listening on %s", server.Addr)
go func() {
err := server.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("API server closed: err: %v", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds.
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Println("got shutdown signal. shutting down server...")
localCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(localCtx); err != nil {
log.Fatalf("Error shutting down server: %v", err)
}
log.Println("server shutdown complete")
}

We are not done yet, we still need to specify to the application where to load these new config parameters from, which in our case, it will be the .env file we created at the start of this guide.

cmd/api-server/main.go
package main
import (
"github.com/auth0-developer-hub/api_standard-library_golang_hello-world/config"
"github.com/auth0-developer-hub/api_standard-library_golang_hello-world/pkg/helpers"
"github.com/joho/godotenv"
)
func main() {
godotenv.Load()
clientOriginUrl := helpers.SafeGetEnv("CLIENT_ORIGIN_URL")
port := helpers.SafeGetEnv("PORT")
audience := helpers.SafeGetEnv("AUTH0_AUDIENCE")
domain := helpers.SafeGetEnv("AUTH0_DOMAIN")
config := Config{
Port: port,
SecureOptions: config.SecureOptions(),
CorsOptions: config.CorsOptions(clientOriginUrl),
Audience: audience,
Domain: domain,
}
app := App{Config: config}
app.RunServer()
}

Handle Authorization Exceptions

In Go, the order in which you declare and invoke middleware is essential for the architecture of your application. What should happen when a client makes an API request without an access token to a protected API endpoint? The ideal behavior is to respond to the client with a 401 (Unauthorized) status code.

The starter Go project already includes middleware functions that handle server errors, 500 (Internal Server Error), and server requests that don't match any server routes, 404 (Not Found).

You can extend the functionality of the ValidateJWT middleware function defined in the pkg/middleware/auth.go to handle any errors related to unauthorized access. Update that file as follows:

pkg/middleware/auth.go
package middleware
import (
"errors"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/auth0-developer-hub/api_standard-library_golang_hello-world/pkg/helpers"
"github.com/auth0/go-jwt-middleware/v2"
"github.com/auth0/go-jwt-middleware/v2/jwks"
"github.com/auth0/go-jwt-middleware/v2/validator"
)
const (
missingJWTErrorMessage = "Requires authentication"
invalidJWTErrorMessage = "Bad credentials"
)
func ValidateJWT(audience, domain string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
issuerURL, err := url.Parse("https://" + domain + "/")
if err != nil {
log.Fatalf("Failed to parse the issuer url: %v", err)
}
provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)
jwtValidator, err := validator.New(
provider.KeyFunc,
validator.RS256,
issuerURL.String(),
[]string{audience},
)
if err != nil {
log.Fatalf("Failed to set up the jwt validator")
}
if authHeaderParts := strings.Fields(r.Header.Get("Authorization")); len(authHeaderParts) > 0 && strings.ToLower(authHeaderParts[0]) != "bearer" {
errorMessage := ErrorMessage{Message: invalidJWTErrorMessage}
if err := helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage); err != nil {
log.Printf("Failed to write error message: %v", err)
}
return
}
errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Encountered error while validating JWT: %v", err)
if errors.Is(err, jwtmiddleware.ErrJWTMissing) {
errorMessage := ErrorMessage{Message: missingJWTErrorMessage}
if err := helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage); err != nil {
log.Printf("Failed to write error message: %v", err)
}
return
}
if errors.Is(err, jwtmiddleware.ErrJWTInvalid) {
errorMessage := ErrorMessage{Message: invalidJWTErrorMessage}
if err := helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage); err != nil {
log.Printf("Failed to write error message: %v", err)
}
return
}
ServerError(w, err)
}
middleware := jwtmiddleware.New(
jwtValidator.ValidateToken,
jwtmiddleware.WithErrorHandler(errorHandler),
)
middleware.CheckJWT(next).ServeHTTP(w, r)
})
}

The jwtmiddleware.New(..) authentication middleware allows you to specify an error handler, which we can use to capture and process errors that happened during the authentication request.

When the ValidateJWT() middleware function cannot get the access token from the request, it will yield an error with a 401 status and a missingJWTErrorMessage message. However, when the access token is invalid, the middleware will yield an invalidJWTErrorMessage message with a 401 status.

When implementing error handling in APIs, OWASP recommends that developers gracefully handle all possible errors and only respond with error information that is helpful to the user without revealing unnecessary internal details.

The update to the ValidateJWT middleware function customizes the message response based on the instance of the error object that the jwtmiddleware.New() middleware throws. You preserve the 401 HTTP status code but change the response message to Requires authentication when it cannot get the access token from the request and Bad credentials when the access token is invalid.

According to OWASP, improper error handling can introduce various security problems for an application. Sending detailed internal error messages, such as stack traces and error codes, to the user may reveal implementation details that you should never reveal openly to the public. A malicious user could use those details to gain important clues on the potential security flaws of your application.

Test Your Protected API Endpoints

Restart your API server to become aware of the changes before testing the API endpoints are working as expected:

COMMAND
go run ./cmd/api-server/

Use an application like Postman or a terminal to test that your API server is working as prescribed.

As an anonymous user:

Make API requests to your Go API server without sending any access token:

Request:

COMMAND
curl localhost:6060/api/messages/public

Response:

Status code: 200

{
"text": "This is a public message."
}

Request:

COMMAND
curl localhost:6060/api/messages/protected

Response:

Status code: 401

{
"message": "Requires authentication"
}

Request:

COMMAND
curl localhost:6060/api/messages/admin

Response:

Status code: 401

{
"message": "Requires authentication"
}

Request:

COMMAND
curl localhost:6060/api/messages/invalid

Response:

Status code: 404

{
"message": "Not Found"
}

Any other errors as needed if no other status codes apply:

Response:

Status code: 500

{
"message": "Relevant description message"
}

As an authenticated user

Make an authenticated request to your Go API server by including an access token in the authorization header:

You learned on the "Configure Go application with Auth0" section how to get a test access token. You will use that access token in this section. The access token value should auto-populate if you added it to the corresponding field during the configuration steps.

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/public \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

Response:

Status code: 200

{
"text": "This is a public message."
}

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/protected \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

Response:

Status code: 200

{
"text": "This is a protected message."
}

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/admin \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

Response:

Status code: 200

{
"text": "This is an admin message."
}

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/invalid \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

Response:

Status code: 404

{
"message": "Not Found"
}

Any other errors as needed if no other status codes apply:

Response:

Status code: 500

{
"message": "Relevant message"
}

When using an invalid test access token:

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/protected \
--header 'authorization: Bearer invalidtoken1234567890'

Response:

Status code: 401

{
"message": "Bad credentials"
}

Request Go API Resources From a Client App

Let's simulate an essential feature of an API: serving data to client applications.

You can pair this API server with a client application that matches the technology stack that you use at work. Any "Hello World" client application can communicate with this "Hello World" API server sample.

You can simulate a secure full-stack application system in no time. Each client application sample gives you clear instructions to quickly get it up and running.

Once set up, you can test the client-server connection in the http://localhost:4040/protected or http://localhost:4040/admin pages of your client application.

Pick a Single-Page Application (SPA) code sample in your preferred frontend framework and language:

angular
typescript
Angular Standalone Components Code Sample:Basic Authentication
This code sample uses Angular Standalone Components with TypeScript to implement single-page application authentication using the Auth0 Angular SDK.
angular
typescript
Angular Code Sample:Basic Authentication
This code sample uses Angular with TypeScript to implement single-page application authentication using the Auth0 Angular SDK.
react
javascript
React Router 6 Code Sample:Basic Authentication
Code sample showing how to protect a simple React single-page application using React Router 6, Auth0, and JavaScript.
react
typescript
React Router 6/TypeScript Code Sample:Basic Authentication
Code sample showing how to protect a simple React single-page application using React Router 6, Auth0, and TypeScript.
react
javascript
React Code Sample:Basic Authentication
JavaScript code that implements user login, logout and sign-up features to secure a React Single-Page Application (SPA) using Auth0.
react
typescript
React/TypeScript Code Sample:Basic Authentication
Code sample of a simple React single-page application built TypeScript that implements authentication using Auth0.
svelte
javascript
Svelte Code Sample:Basic Authentication
JavaScript code that implements user login, logout and sign-up features to secure a Svelte Single-Page Application (SPA), using routing middleware.
vue
javascript
Vue.js Composition API Code Sample:Basic Authentication
This code sample uses Vue.js 3 with JavaScript and the Composition API to implement single-page application authentication using the Auth0 Vue SDK.
vue
javascript
Vue.js Options API Code Sample:Basic Authentication
This code sample uses Vue.js 3 with JavaScript and the Options API to implement single-page application authentication using the Auth0 Vue SDK.
vue
javascript
Vue.js 2 Code Sample:Basic Authentication
This code sample uses Vue.js 2 with JavaScript to implement single-page application authentication using the Auth0 SPA SDK.

That's all it takes to integrate a client application with an Go API server that is also secured by Auth0 and to use an access token to consume protected server resources from the client application.

Set Up Role-Based Access Control (RBAC)

Within the context of Auth0, Role-based access control (RBAC) systems assign permissions to users based on their role within an organization. Everyone who holds that role has the same set of access rights. Those who hold different roles have different access rights.

Developers who use Role-based access control (RBAC) for access management can mitigate the errors that come from assigning permissions to users individually.

You can use the Auth0 Dashboard to enable Role-Based Access Control (RBAC) in any API that you have already registered with Auth0. You then implement RBAC by creating API permissions, assigning those permissions to a role, and assigning that role to any of your users.

Whenever a user logs in to one of your client applications, the Auth0 authorization server issues an access token that the client can use to make authenticated requests to an API server. Auth0 authorization servers issue access tokens in JSON Web Token (JWT) format.

When you enable Auth0 Role-Based Access Control (RBAC) for an API, the access token will include a permissions claim that has all the permissions associated with any roles that you have assigned to that user.

For this particular API code sample, the access token present in the authorization header of a request must include a permissions claim that contains the read:admin-messages permission to access the GET /api/messages/admin endpoint.

Enable Role-Based Access Control (RBAC)

  • Open the APIs section of the Auth0 Dashboard and select your "Hello World API Server" registration.

  • Click on the "Settings" tab and locate the "RBAC Settings" section.

  • Switch on the "Enable RBAC" and "Add Permissions in the Access Token" options.

Visit the "Role-Based Access Control" document for more details.

Create an API permission

In the same Auth0 API registration page, follow these steps:

  • Click on the "Permissions" tab and fill a field from the "Add a Permission (Scope)" section with the following information:
Permission (Scope)
read:admin-messages
Description
Read admin messages
  • Click the "+ Add" button to store the permission.
Visit the "Add API Permissions" document for more details.

Create a role with permissions

Create a role

  • Open the User Management > Roles section of the Auth0 Dashboard.

  • Click on the Create role button and fill out the "New Role" form with the following values:

Name
messages-admin
Description
Access admin messaging features
  • Click on the Create button.
Visit the "Create Roles" document for more details.

Add permissions to the role

  • Click on the "Permissions" tab of the roles page.

  • Click on the "Add Permissions" button.

  • Select the "Hello World API Server" from the dropdown menu that comes up and click the "Add Permissions" button.

  • Select all the permissions available by clicking on them one by one or by using the "All" link.

  • Finally, click on the "Add Permissions" button to finish up.

Visit the "Add Permissions to Roles" document for more details.

Create an admin user

  • Open the User Management > Users section from the Auth0 Dashboard.

  • Click on the "Create user" button and fill out the form with the required information. Alternatively, you can also click on any of your existing users to give one of them the admin role.

  • On the user's page, click on the "Roles" tab and then click on the "Assign Roles" button.

  • Select the messages-admin role from the dropdown menu and click on the "Assign" button.

Visit the "Assign Roles to Users" document for more details.

Implement RBAC in a Golang API

Implementing RBAC is consists of checking that the access token has the required permissions. Since we already have most of the logic in place, we just need to create a new handler to validate if the permissions claim of the access token contains the required permissions to access the endpoint.

First, update the below file to add a Contains helper function, which will check if a string is a subset of an array of strings:

pkg/helpers/helpers.go
package helpers
import (
"encoding/json"
"log"
"net/http"
"os"
)
func Contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func SafeGetEnv(key string) string {
if os.Getenv(key) == "" {
log.Fatalf("The environment variable '%s' doesn't exist or is not set", key)
}
return os.Getenv(key)
}
func WriteJSON(rw http.ResponseWriter, status int, data interface{}) error {
js, err := json.Marshal(data)
if err != nil {
return err
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(status)
_, err = rw.Write(js)
if err != nil {
return err
}
return nil
}

Now we have the above function added, we will update our auth.go file to check the appropriate permissions.

pkg/middleware/auth.go
package middleware
import (
"context"
"errors"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/auth0-developer-hub/api_standard-library_golang_hello-world/pkg/helpers"
"github.com/auth0/go-jwt-middleware/v2"
"github.com/auth0/go-jwt-middleware/v2/jwks"
"github.com/auth0/go-jwt-middleware/v2/validator"
)
const (
missingJWTErrorMessage = "Requires authentication"
invalidJWTErrorMessage = "Bad credentials"
permissionDeniedErrorMessage = "Permission denied"
)
type CustomClaims struct {
Permissions []string `json:"permissions"`
}
func (c CustomClaims) Validate(ctx context.Context) error {
return nil
}
func (c CustomClaims) HasPermissions(expectedClaims []string) bool {
if len(expectedClaims) == 0 {
return false
}
for _, scope := range expectedClaims {
if !helpers.Contains(c.Permissions, scope) {
return false
}
}
return true
}
func ValidatePermissions(expectedClaims []string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
claims := token.CustomClaims.(*CustomClaims)
if !claims.HasPermissions(expectedClaims) {
errorMessage := ErrorMessage{Message: permissionDeniedErrorMessage}
if err := helpers.WriteJSON(w, http.StatusForbidden, errorMessage); err != nil {
log.Printf("Failed to write error message: %v", err)
}
return
}
next.ServeHTTP(w, r)
})
}
func ValidateJWT(audience, domain string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
issuerURL, err := url.Parse("https://" + domain + "/")
if err != nil {
log.Fatalf("Failed to parse the issuer url: %v", err)
}
provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)
jwtValidator, err := validator.New(
provider.KeyFunc,
validator.RS256,
issuerURL.String(),
[]string{audience},
validator.WithCustomClaims(func() validator.CustomClaims {
return new(CustomClaims)
}),
)
if err != nil {
log.Fatalf("Failed to set up the jwt validator")
}
if authHeaderParts := strings.Fields(r.Header.Get("Authorization")); len(authHeaderParts) > 0 && strings.ToLower(authHeaderParts[0]) != "bearer" {
errorMessage := ErrorMessage{Message: invalidJWTErrorMessage}
if err := helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage); err != nil {
log.Printf("Failed to write error message: %v", err)
}
return
}
errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Encountered error while validating JWT: %v", err)
if errors.Is(err, jwtmiddleware.ErrJWTMissing) {
errorMessage := ErrorMessage{Message: missingJWTErrorMessage}
if err := helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage); err != nil {
log.Printf("Failed to write error message: %v", err)
}
return
}
if errors.Is(err, jwtmiddleware.ErrJWTInvalid) {
errorMessage := ErrorMessage{Message: invalidJWTErrorMessage}
if err := helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage); err != nil {
log.Printf("Failed to write error message: %v", err)
}
return
}
ServerError(w, err)
}
middleware := jwtmiddleware.New(
jwtValidator.ValidateToken,
jwtmiddleware.WithErrorHandler(errorHandler),
)
middleware.CheckJWT(next).ServeHTTP(w, r)
})
}

Let's break down the new code piece by piece:

  • The constant permissionDeniedErrorMessage is the error response that will be returned if the token doesn't have the required permissions.
  • The CustomClaims struct contains the claims that we want from the token, in this case we want to check the permissions claim.
  • The Validate function does nothing, but is needed to satisfy the validator.CustomClaims interface.
  • The HasPermissions function will be used to check that the permissions claim contains the required permissions.
  • The ValidatePermissions handler validates if the token has the required permissions before passing the control to the next handler in the chain. If the token doesn't have the required permissions it will send a 403 response with an appropriate error message.
  • Now, the jwtValidator is instanciated with an additional option in order to return the object CustomClaims from the token.

Next, we'll wrap the endpoint, api/messages/admin, with the ValidatePermissions middleware to authorize only those requests that contains the read:admin-messages permission.

pkg/router/router.go
package router
import (
"net/http"
"github.com/auth0-developer-hub/api_standard-library_golang_hello-world/pkg/middleware"
)
func Router(audience, domain string) http.Handler {
router := http.NewServeMux()
// public endpoint
router.HandleFunc("/api/messages/public", middleware.PublicApiHandler)
// protected endpoint
router.Handle("/api/messages/protected", middleware.ValidateJWT(audience, domain, http.HandlerFunc(middleware.ProtectedApiHandler)))
// Add ValidatePermissions middleware function to validate the RBAC permissions
router.Handle("/api/messages/admin",
middleware.ValidateJWT(audience, domain,
middleware.ValidatePermissions([]string{"read:admin-messages"},
http.HandlerFunc(middleware.AdminApiHandler))))
return router
}

Now, restart the API server to make sure your Golang API is aware of the RBAC implementation changes:

COMMAND
go run ./cmd/api-server

Access the Admin Endpoint

Let's test access to the GET /api/messages/admin endpoint by simulating a real user login and requesting that protected resource using a real access token.

You can pair this API server with a client application that matches the technology stack that you use at work. Any "Hello World" client application can communicate with this "Hello World" API server sample.

When you log in to a "Hello World" client application as a user who has the messages-admin role, your access token will have the required permissions to access the GET /api/messages/admin endpoint.

You can simulate a secure full-stack application system in no time. Each client application sample gives you clear instructions to get it up and running quickly.

Pick a Single-Page Application (SPA) code sample in your preferred frontend framework and language:

angular
typescript
Angular Standalone Components Code Sample:Basic Authentication
This code sample uses Angular Standalone Components with TypeScript to implement single-page application authentication using the Auth0 Angular SDK.
angular
typescript
Angular Code Sample:Basic Authentication
This code sample uses Angular with TypeScript to implement single-page application authentication using the Auth0 Angular SDK.
react
javascript
React Router 6 Code Sample:Basic Authentication
Code sample showing how to protect a simple React single-page application using React Router 6, Auth0, and JavaScript.
react
typescript
React Router 6/TypeScript Code Sample:Basic Authentication
Code sample showing how to protect a simple React single-page application using React Router 6, Auth0, and TypeScript.
react
javascript
React Code Sample:Basic Authentication
JavaScript code that implements user login, logout and sign-up features to secure a React Single-Page Application (SPA) using Auth0.
react
typescript
React/TypeScript Code Sample:Basic Authentication
Code sample of a simple React single-page application built TypeScript that implements authentication using Auth0.
svelte
javascript
Svelte Code Sample:Basic Authentication
JavaScript code that implements user login, logout and sign-up features to secure a Svelte Single-Page Application (SPA), using routing middleware.
vue
javascript
Vue.js Composition API Code Sample:Basic Authentication
This code sample uses Vue.js 3 with JavaScript and the Composition API to implement single-page application authentication using the Auth0 Vue SDK.
vue
javascript
Vue.js Options API Code Sample:Basic Authentication
This code sample uses Vue.js 3 with JavaScript and the Options API to implement single-page application authentication using the Auth0 Vue SDK.
vue
javascript
Vue.js 2 Code Sample:Basic Authentication
This code sample uses Vue.js 2 with JavaScript to implement single-page application authentication using the Auth0 SPA SDK.

Once you set up the client application, log in and visit the guarded "Admin" page (http://localhost:4040/admin).

When you log in to a "Hello World" client application as a user who has the messages-admin role, your access token will have the required permissions to access the GET /api/messages/admin endpoint, and you'll get the following response:

{
"text": "This is an admin message."
}

However, when you log in as a user who doesn't have the messages-admin role, you'll get a 403 Forbidden status code and the following response:

{
"message": "Permission denied"
}

Next Steps

You have implemented token-based authorization in Go to restrict access to server resources.

This guide covered the most common authorization use case for a Golang API: use middleware to enforce security policies, validate access tokens, make authenticated requests to your API, and implement Role-Based Access Control (RBAC). However, Auth0 is an extensible and flexible identity platform that can help you achieve even more. If you have a more complex use case, check out the Auth0 Architecture Scenarios to learn more about the typical architecture scenarios we have identified when working with customers on implementing Auth0.

You can simulate a secure full-stack application system in no time. Each client application sample gives you clear instructions to quickly get it up and running.