Securing a Go API With JWTs

Securing a Go API With JWTs

Code Examples / api-integration

The JSON Web Token for Go library can be used to validate and verify JWTs to protect a Go API.

Tutorial code

The complete code and Dockerfile to run this tutorial in Docker are available in the go-api-jwt-validation GitHub repository.

The API

There are several frameworks that can be used to build an API in Go. In this article the Gorilla Mux request router is used to route an incoming request to a handler.

The example data (records) is defined in records.json and the data model in records-data-model.go. The records API is defined in records.go, where a method for retrieving all records as well as a specific record is implemented.

func main() {
    ...
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/api/records", getRecords)
    router.HandleFunc("/api/records/{id}", getRecord)
    ...
}

JSON Web Token for Go

The JSON Web Token for Go library has functions to verify the signature of the JWT as well as validating for example issuer, audience, expiration, subject, and more.

However, it does not implement a function to verify the scope claim and whether it contains values needed to allow access to the API. This is something that can easily be implemented and executed after the required validations and verifications have been performed.

Each handler calls the Authorize method (in authorize.go) to verify and validate the JWT from the Authorize header.

Configuration

The example externalizes some parts of the configuration, making it more adaptable to different environments. The required configurations are set as environment variables in .env.

Parameter NameExample valueDescription
PORT8080The port the API is exposed on
AUDwwwValue to match with the aud claim
ISShttps://idsvr.example.com/oauth/v2/oauth-anonymousValue to match with the iss claim
JWKShttps://idsvr.example.com/oauth/v2/oauth-anonymous/jwksEndpoint to resolve public key for JWT signature verification
SCOPErecords openid apiValue(s) needed in the scope claim. Defined as a space-separated string.

The aud and iss claims depend on the Curity Identity Server installation. The aud claim will, by default, be the ID of the client that is issuing the token.

Opaque Tokens

Typically, an Authorization Server would issue opaque tokens and not JWTs, and The Curity Identity Server issues opaque tokens by default. The use of opaque tokens is the best practice from a security standpoint for external clients.

However, APIs and Microservices typically consume JWTs. To handle this, it is recommended to use the Phantom token or the Split token approach and have the API gateway exchange an opaque token for a JWT and pass that to the API.

Curity Configuration

JWT Verification

Instantiate an algorithm using the public key exposed on the JWKS endpoint. The example uses RS256, but several other algorithms are also supported.

func setAlgorithm(jwksURI string) {
    hs = jwt.NewRS256(jwt.RSAPublicKey(getKey(jwksURI)))
}

To validate that the alg claim in the JWT header matches the one used by the algorithm, call the Verify() method and pass the JWT, the algorithm, a payload pointer, and the jwt.ValidateHeader option.

if _, err := jwt.Verify([]byte(jwtToken), hs, &pl, jwt.ValidateHeader); 

Algorithm Resolver

It is also possible to use an Algorithm Resolver to resolve the algorithm based on the alg claim in the header, making the instantiation of the algorithm used for verification more dynamic.

If the alg claim is successfully validated, the JWT signature can be verified using the algorithm.

if _, err := jwt.Verify([]byte(jwtToken), hs, &pl);

JWT Validation

ValidatePayload can run several validators against a payload. In the example below, the nbf, exp, iss, and aud claims are validated. The values to validate the claims against are picked up from the configuration in the .env file for aud and iss. For the time-based claims, the current time is used.

aud := jwt.Audience{os.Getenv("AUD")}
iss := os.Getenv("ISS")
now := time.Now()

nbfValidator := jwt.NotBeforeValidator(now)
expValidator := jwt.ExpirationTimeValidator(now)
issValidator := jwt.IssuerValidator(iss)    
audValidator := jwt.AudienceValidator(aud)  

validatePayload := jwt.ValidatePayload(&pl, nbfValidator, expValidator, issValidator, audValidator)

After verification and validation have passed, the scope claim can be checked for matching values to authorize the API call. There is no function for this in the JSON Web Token for Go library. Instead, the JWT can be decoded and the scope claim checked.

tokenParts := strings.Split(jwtToken, ".")
encodedPayload := tokenParts[1]

payload, err := base64.RawURLEncoding.DecodeString(encodedPayload)

...

var rawPayload Payload
err = json.Unmarshal([]byte(payload), &rawPayload)

...

configuredScopes := strings.Split(os.Getenv("SCOPE"), " ")
payloadScopes := rawPayload.Scope

//Check if configured(required) scope values are missing
if(!checkScopes(configuredScopes, payloadScopes)) {
    writer.WriteHeader(403)
    writer.Write([]byte("Missing required scope(s)"))
    return false
}

Code snippet truncated for better readability.

Try It Out

Run the example API by following the instructions to build and start the Docker image.

Calling the API and passing the JWT in the Authorize header can be done using your tool of choice. OAuth.tools can handle this well with a flow to obtain the JWT and a subsequent flow to call an external API. It is also possible to make a simple request using cURL.

curl -i http://localhost:8080/api/records -H "Authorization: Bearer eyJraWQiOiItMTMxNTcwN ..."

The response for a JWT that does not pass verification and validation would look similar to this:

HTTP/1.1 401 Unauthorized
Date: Mon, 16 Aug 2021 20:57:53 GMT
Content-Length: 25
Content-Type: text/plain; charset=utf-8

Missing required scope(s)

Conclusion

The JSON Web Token for Go library has rudimentary functions available to verify JWTs and to validate standard claims. This tutorial showcases both and explains how to decode a verified and validated JWT to extract additional claims and use them to authorize access to an API in Go.

Further information on API authorization is available in Scope Best Practices and Claims Best Practices.

Keep up with our latest articles and how-tos RSS feeds.