API

Golang API

Golang API

In this quickstart, you are going to learn how to integrate a Golang resource server with FusionAuth. You will protect an API resource from unauthorized usage. You’ll be building it for ChangeBank, a global leader in converting dollars into coins.

The Docker Compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-golang-api.

Prerequisites

  • Go: This quickstart was built using Golang 1.21.0. This example may work on different versions of Golang, but it has not been tested.
  • Docker: The quickest way to stand up FusionAuth. Ensure you also have docker compose installed.
  • (Alternatively, you can Install FusionAuth Manually).

General Architecture

A client wants access to an API resource at /resource. However, it is denied this resource until it acquires an access token from FusionAuth.

ClientResource ServerFusionAuthGET /resource404 Not AuthorizedPOST /api/login200 Ok(token)GET /resource200 Ok(resource)ClientResource ServerFusionAuth

Resource Server Authentication with FusionAuth

While the access token is acquired via the Login API above, this is for simplicity of illustration. The token can be, and typically is, acquired through one of the OAuth grants.

Getting Started

In this section, you’ll get FusionAuth up and running and create a resource server which will serve the API.

Clone The Code

First off, grab the code from the repository and change into that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-golang-api.git
cd fusionauth-quickstart-golang-api

Run FusionAuth Via Docker

You'll find a Docker Compose file (docker-compose.yml) and an environment variables configuration file (.env) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

docker compose up -d

Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v, then re-run docker compose up -d.

FusionAuth will be initially configured with these settings:

  • Your client Id is e9fdb985-9173-4e01-9d73-ac2d60d1dc8e.
  • Your client secret is super-secret-secret-that-should-be-regenerated-for-production.
  • Your example teller username is teller@example.com and the password is password. They will have the role of teller.
  • Your example customer username is customer@example.com and the password is password. They will have the role of customer.
  • Your admin username is admin@example.com and the password is password.
  • The base URL of FusionAuth is http://localhost:9011/.

You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration section.

The .env file contains passwords. In a real application, always add this file to your .gitignore file and never commit secrets to version control.

Create Your Golang Resource Server Application

Now you are going to create a Golang API application. While this section builds a simple API, you can use the same configuration to integrate an existing API with FusionAuth.

We are going to be building an API backend for a banking application called ChangeBank. This API will have two endpoints:

  • make-change: This endpoint will allow you to call GET with a total amount and receive a response indicating how many nickels and pennies are needed to make change. Valid roles are customer and teller.
  • panic: Tellers may call this endpoint to call the police in case of an incident. The only valid role is teller.

Both endpoints will be protected such that a valid JSON web token (JWT) will be required in the Authorization header in order to be accessed. Additionally, the JWT must have a roles claim containing the appropriate role to use the endpoint.

If you simply want to run the application , there is a completed version in the ‘complete-application’ directory. You can use the following commands to get it up and running if you do not want to create your own.

cd complete-application
go get
go run main.go

You can then follow the instructions in the Run the API section started at Get a Token.

To create the application from scratch, make a directory for this API.

mkdir your-application && cd your-application

Initialize The Application

Initialize the Golang application using the following:

go mod init your-application

Write Golang Code

This tutorial puts all these classes in the same package for simplicity, but for a production application, you’d separate these out.

Now you are going to write some Golang code. You need to create a file called main.go. You’ll be building it step by step, adding code each step.

Here you will add the package clause and import declarations below:

package main

import (
  "crypto/rsa"
  "encoding/json"
  "errors"
  "fmt"
  "io"
  "log"
  "math"
  "net/http"
  "reflect"
  "runtime"
  "sort"
  "strconv"
  "strings"

  "github.com/golang-jwt/jwt/v4"
)

Next, you will need to add a couple of structures to help manage the data for making change.

type ChangeResponse struct {
  Message string
  Change  []DenominationCounter
}

type DenominationCounter struct {
  Denomination string
  Count        int
}

You will also need to declare a variable and assign the value of the public key for the application. The value for this key will be set later in the application. Declare this variable below the import declarations.

var verifyKey *rsa.PublicKey

Add Handlers and Functions

Create the main function for the application.

func main() {
  fmt.Println("server")
  handleRequests()
}

Next, you are going to add some handlers. This tells the application how to handle specific requests. Notice the isAuthorized function wrapping around the panic and makeChange functions. We will implement this function later and it is what will validate the token and verify the roles. This function will also stand up the http listener on port 9001.

func handleRequests() {
  http.Handle("/make-change", isAuthorized(makeChange))
  http.Handle("/panic", isAuthorized(panic))
  log.Fatal(http.ListenAndServe(":9001", nil))
}

You will write two functions that will return values when users access them. These functions are called by the handlers. One is the panic which returns a message when a successful POST call is made to /panic. This will also display a message if the request is not made through a POST. This API has no relation to the panic method in go, it’s for a teller panicking when ChangeBank is being robbed.

func panic(w http.ResponseWriter, r *http.Request) {
  message := ""
  var status int

  switch r.Method {
  case "POST":
    message = "We've called the police!"
    status = http.StatusOK
  default:
    message = "Only POST method is supported."
    status = http.StatusNotImplemented
  }

  responseObject := make(map[string]string)
  responseObject["message"] = message

  SetWriterReturn(w, status, responseObject)
}

Next you need a function to show the breakdown from making change for a given dollar amount. This function will get the total value from the query string. If the total cannot be cast to a float64, the function will return a message stating that. If the total is a valid number, the function will return the description of the change.

func makeChange(w http.ResponseWriter, r *http.Request) {
  response := ChangeResponse{}

  switch r.Method {
  case "GET":
    var total = r.URL.Query().Get("total")
    var message = "We can make change using"
    remainingAmount, err := strconv.ParseFloat(total, 64)
    if err != nil {
      responseObject := make(map[string]string)
      responseObject["message"] = "Problem converting the submitted value to a decimal.  Value submitted: " + total
      SetWriterReturn(w, http.StatusBadRequest, responseObject)
      return
    }

    coins := make(map[float64]string)
    coins[.25] = "quarters"
    coins[.10] = "dimes"
    coins[.05] = "nickels"
    coins[.01] = "pennies"

    //since a map is an unordered list, we need another list to maintain the order
    denominationOrder := make([]float64, 0, len(coins))
    for value := range coins {
      denominationOrder = append(denominationOrder, value)
    }

    //then we order the list
    sort.Slice(denominationOrder, func(i, j int) bool {
      return denominationOrder[i] > denominationOrder[j]
    })

    //for each coin in the list, we figure out how many will fit into the remainingAmount
    for counter := range denominationOrder {
      value := denominationOrder[counter]
      coinName := coins[value]
      coinCount := int(remainingAmount / value)
      remainingAmount -= float64(coinCount) * value
      //had to add this to help with floating point rounding issues.
      remainingAmount = math.Round(remainingAmount*100) / 100

      message += " " + strconv.Itoa(coinCount) + " " + coinName
      denominationCount := DenominationCounter{}
      denominationCount.Denomination = coinName
      denominationCount.Count = coinCount
      response.Change = append(response.Change, denominationCount)
    }
    response.Message = message

    SetWriterReturn(w, http.StatusOK, response)

  default:
    responseObject := make(map[string]string)
    responseObject["message"] = "Only POST method is supported."
    SetWriterReturn(w, http.StatusNotImplemented, responseObject)
  }

}

Add Security

Now you need to implement the isAuthorized function used by the handlers. Now, you’ll protect your endpoints based on the roles encoded in the JWT you receive from FusionAuth. The decoded payload of a JWT for a teller might look like this:

{
  "aud": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
  "exp": 1689289585,
  "iat": 1689285985,
  "iss": "http://localhost:9011",
  "sub": "00000000-0000-0000-0000-111111111111",
  "jti": "ebaa4184-2320-47dd-925b-2e18756c635f",
  "authenticationType": "PASSWORD",
  "email": "teller@example.com",
  "email_verified": true,
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
  "roles": [
    "teller"
  ],
  "auth_time": 1689285985,
  "tid": "d7d09513-a3f5-401c-9685-34ab6c552453"
}

You need to validate the token and make sure the user has the appropriate role to access the API. You can do so by adding the following code:

func isAuthorized(endpoint func(http.ResponseWriter, *http.Request)) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    reqToken := ""
    tokenCookie, err := r.Cookie("app.at")
    if err != nil {
      if errors.Is(err, http.ErrNoCookie) {
        reqToken = r.Header.Get("Authorization")
        reqToken = r.Header.Get("Authorization")
        splitToken := strings.Split(reqToken, "Bearer ")
        reqToken = splitToken[1]
      } else {
      }
    } else {
      reqToken = tokenCookie.Value 
    }

    responseObject := make(map[string]string)
    if reqToken == "" {
      responseObject["message"] = "No Token provided"
      SetWriterReturn(w, http.StatusUnauthorized, responseObject)
    } else {
      token, err := jwt.Parse(reqToken, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
          return nil, fmt.Errorf(("invalid signing method"))
        }
        aud := "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
        checkAudience := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
        if !checkAudience {
          return nil, fmt.Errorf(("invalid aud"))
        }
        // verify iss claim
        iss := "http://localhost:9011"
        checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
        if !checkIss {
          return nil, fmt.Errorf(("invalid iss"))
        }

        setPublicKey(token.Header["kid"].(string))
        return verifyKey, nil
      })
      if err != nil {
        fmt.Fprintf(w, err.Error())
        return
      }

      if token.Valid {
        var roles = token.Claims.(jwt.MapClaims)["roles"]
        var validRoles []string

        switch pageToGet := GetFunctionName((endpoint)); pageToGet {
        case "main.panic":
          validRoles = []string{"teller"}
        case "main.makeChange":
          validRoles = []string{"customer", "teller"}
        }

        result := containsRole([]string{roles.([]interface{})[0].(string)}, validRoles)

        if len(result) > 0 {
          endpoint(w, r)
        } else {
          responseObject := make(map[string]string)
          responseObject["message"] = "Proper role not found for user"
          SetWriterReturn(w, http.StatusUnauthorized, responseObject)
        }

      }

    }
  })
}

There is a lot of code in this function. Let’s break it down:

  • First you validate some claims. Notice the checkAudience that makes sure the token is intended for this application and checkIss to make sure the issuer is as expected.
  • SetPublicKey, which is a function we will create later, retrieves the public key for the JWT.
  • After the key has been set, the golang-jwt/jwt will validate the signature and expiration date.
  • Once the token is validated, you will need to take a look at the roles.
  • The code will look to see which page is being called and then create an array for the roles that are valid for that request in validRoles.
    • The containsRole function will be created as well and that creates an array with the intersection of roles that are allowed and roles that are required. If the length of that intersection is greater than 1 then the user has the proper role.
  • Once the roles have been validated, then the endpoint is served.

Now, you will add the code to set the public key. This function will accept the key Id from the token and call the authorization server to get the public key with that key Id and put it in the proper format to be used.

Different tokens can be signed with different keys. In this example there is only one key used. If the key is already set, the application will not attempt to set it again. In a production environment you could store this key on the server or set it once on initialization. If your api serves several applications, you will need to get the correct public key for each application if the signing keys are different between applications. It should also be noted that you could use JWKS, a standard for retrieving public keys, but this example uses FusionAuth’s proprietary APIs.

func setPublicKey(kid string) {
  if verifyKey == nil {
    response, err := http.Get("http://localhost:9011/api/jwt/public-key?kid=" + kid)
    if err != nil {
      log.Fatalln(err)
    }

    responseData, err := io.ReadAll(response.Body)
    if err != nil {
      log.Fatal(err)
    }

    var publicKey map[string]interface{}

    json.Unmarshal(responseData, &publicKey)

    var publicKeyPEM = publicKey["publicKey"].(string)

    var verifyBytes = []byte(publicKeyPEM)
    verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes)

    if err != nil {
      log.Fatalln(("problem retreiving public key"))
    }
  }
}

Then add the code to compare the required roles to the user’s roles based on the token.

// function for finding the intersection of two arrays
func containsRole(roles []string, rolesToCheck []string) []string {
  intersection := make([]string, 0)

  set := make(map[string]bool)

  // Create a set from the first array
  for _, role := range roles {
    set[role] = true // setting the initial value to true
  }

  // Check elements in the second array against the set
  for _, role := range rolesToCheck {
    if set[role] {
      intersection = append(intersection, role)
    }
  }

  return intersection
}

Finally you will add two helper functions. GetFunctionName will return the name of the function being called by the endpoint. SetWriterReturn will format the response properly.

func GetFunctionName(i interface{}) string {
  return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func SetWriterReturn(w http.ResponseWriter, statusCode int, returnObject interface{}) {
  w.Header().Set("Content-Type", "application/json")
  w.WriteHeader(statusCode)
  jsonResp, err := json.Marshal(returnObject)
  if err != nil {
    log.Fatalf("Error happened in JSON marshal. Err: %s", err)
  }
  w.Write(jsonResp)
}

That is all the code for the sample application. Save the main.go file.

Run the API

Start the API resource server by running:

In a command shell window, navigate to the root directory of your API.

cd your-application
go get
go run main.go

Get a Token

There are several ways to acquire a token in FusionAuth, but for this example you will use the Login API to keep things simple.

First let’s try the requests as the teller@example.com user. Based on the configuration this user has the teller role and should be able to use both /make-change and /panic.

  1. Acquire an access token for teller@example.com by making the following request
curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "teller@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'

Copy the token from the response, which should look like this:

{
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTMwNTksImlhdCI6MTY4OTM1Mjk5OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMTExMTExMTExMTExIiwianRpIjoiY2MzNWNiYjUtYzQzYy00OTRjLThmZjMtOGE4YWI1NTI0M2FjIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6InRlbGxlckBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiZTlmZGI5ODUtOTE3My00ZTAxLTlkNzMtYWMyZDYwZDFkYzhlIiwicm9sZXMiOlsiY3VzdG9tZXIiLCJ0ZWxsZXIiXSwiYXV0aF90aW1lIjoxNjg5MzUyOTk5LCJ0aWQiOiJkN2QwOTUxMy1hM2Y1LTQwMWMtOTY4NS0zNGFiNmM1NTI0NTMifQ.WLzI9hSsCDn3ZoHKA9gaifkd6ASjT03JUmROGFZaezz9xfVbO3quJXEpUpI3poLozYxVcj2hrxKpNT9b7Sp16CUahev5tM0-4_FaYlmUEoMZBKo2JRSH8hg-qVDvnpeu8nL6FXxJII0IK4FNVwrQVFmAz99ZCf7m5xruQSziXPrfDYSU-3OZJ3SRuvD8bMopSiyRvZLx6YjWfBsvGSmMXeh_8vHG5fVkq5w1IkaDdugHnivtJIivHuCfl38kQBgw9rAqJLJoKRHHW0Ha7vHIcS6OCWWMDIIVspLyQNcLC16pL9Nss_5v9HMofow1OvQ9sUSMrbbkipjKq2peSjG7qA",
    "tokenExpirationInstant": 1689353059670,
    "user": {
        ...
    }
}

Make the Request

The code is set up to extract the token from either a cookie or the Authorization header so depending on your preference you can replace --cookie 'app.at=<your_token>' with --header 'Authorization: Bearer <your_token>' when making requests to the API.

If you use a cookie, make sure you store it in a secure, HTTPOnly cookie to avoid exfiltration attacks. See Storing OAuth Tokens for more information.

Make a request to /make-change with a query parameter total=1.02. Use the token as the app.at cookie. Use the token as a Bearer token in the Authorization header. Make a request to /make-change with a query parameter total=5.12.

curl 'http://localhost:9001/make-change?total=1.02' --cookie 'app.at=<your_token>' 

The response should look similar to below, however it has been formatted here for readability:

{
  "Message": "We can make change using 20 quarters 1 dimes 0 nickels 2 pennies",
  "Change": [
    {
      "Denomination": "quarters",
      "Count": 20
    },
    {
      "Denomination": "dimes",
      "Count": 1
    },
    {
      "Denomination": "nickels",
      "Count": 0
    },
    {
      "Denomination": "pennies",
      "Count": 2
    }
  ]
}

You were authorized, success! You can try making the request without the Authorization header or with a different string rather than a valid token, and see that you are denied access.

Next call the /panic endpoint because you are in trouble!

curl --location --request POST 'http://localhost:9001/panic' \
--cookie 'app.at=<your_token>'

This is a POST not a get because you want all your emergency calls to be non-idempotent.

Your response should look like this:

{"message":"We've called the police!"}

Nice, help is on the way!

Now let’s try as customer@example.com who has the role customer. Acquire a token for customer@example.com.

curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "customer@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'

Your response should look like this:

{
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTQxMjMsImlhdCI6MTY4OTM1MzUyMywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMjIyMjIyMjIyMjIyIiwianRpIjoiYjc2YWMwMGMtMDdmNi00NzkzLTgzMjgtODM4M2M3MGU4MWUzIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6ImN1c3RvbWVyQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImFwcGxpY2F0aW9uSWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJyb2xlcyI6WyJjdXN0b21lciJdLCJhdXRoX3RpbWUiOjE2ODkzNTM1MjMsInRpZCI6ImQ3ZDA5NTEzLWEzZjUtNDAxYy05Njg1LTM0YWI2YzU1MjQ1MyJ9.T1bELQ6a_ItOS0_YYpvqhIVknVMNeamcoC7BWnPjg2lgA9XpCmFA2mVnycoeuz-mSOHbp2cCoauP5opxehBR2lCn4Sz0If6PqgJgXKEpxh5pAxCPt91UyfjH8hGDqE3rDh7E4Hqn7mb-dFFwdfX7CMdKvC3dppMbXAGCZTl0LizApw5KIG9Wmt670339pSf5lzD38P9WAG5Wr7fAmVrIJPVu6yv2FoR-pMYD2lnAF63HWKknrWB-khmhr9ZKRLXKhP1UK-ThY1FSnmpp8eNblsBqCxf6WaYxYkdp5_F2e56M4sQwHzrg4P9tZGVCmMri4dShF3Ck7OGa7hel-iIPew",
    "tokenExpirationInstant": 1689354123118,
    "user": {
        ...
    }
}

Now use that token to call /make-change with a query parameter total=3.24

curl --location 'http://localhost:9001/make-change?total=3.24' \
--cookie 'app.at=<your_token>'

The response should look similar to below, however it has been formatted here for readability:

{
  "Message": "We can make change using 12 quarters 2 dimes 0 nickels 4 pennies",
  "Change": [
    {
      "Denomination": "quarters",
      "Count": 12
    },
    {
      "Denomination": "dimes",
      "Count": 2
    },
    {
      "Denomination": "nickels",
      "Count": 0
    },
    {
      "Denomination": "pennies",
      "Count": 4
    }
  ]
}

So far so good. Now let’s try to call the /panic endpoint. (We’re adding the -i flag to see the headers of the response)

curl -i --request POST 'http://localhost:9001/panic' \
--cookie 'app.at=<your_token>'

Your should receive a 401 Unauthorized response with the following message:

{"message":"Proper role not found for user"}

Looks like this user does not have access to this function. Enjoy your secured resource server!

Made it this far? Get a free t-shirt, on us!

Thank you for spending some time getting familiar with FusionAuth.

*Offer only valid in the United States and Canada, while supplies last.

fusionauth tshirt

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your API in production, there are some things you're going to want to do.

FusionAuth Integration

Security

Troubleshooting

  • I get This site can’t be reached localhost refused to connect. when I call the Login API.

Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user, admin@example.com with a password of password at http://localhost:9011/admin.

  • The /panic endpoint doesn’t work when I call it.

Make sure you are making a POST call and using a token with the teller role.

  • I get an error message: curl: (52) Empty reply from server

Make sure you are using the correct Authorization header. If you don’t have Bearer in front of your token, you will see this error message.

  • I get a message invalid signing method.

Make sure you are providing the application Id on the login request. This ensures that the JWT is signed with the correct key.

  • It still doesn’t work

You can always pull down a complete running application and compare what’s different.

git clone https://github.com/FusionAuth/fusionauth-quickstart-golang-api.git
cd fusionauth-quickstart-golang-api
docker compose up -d
cd complete-application
go get
go run main.go