Securing a Go API With JWTs
On this page
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 Name | Example value | Description |
---|---|---|
PORT | 8080 | The port the API is exposed on |
AUD | www | Value to match with the aud claim |
ISS | https://idsvr.example.com/oauth/v2/oauth-anonymous | Value to match with the iss claim |
JWKS | https://idsvr.example.com/oauth/v2/oauth-anonymous/jwks | Endpoint to resolve public key for JWT signature verification |
SCOPE | records openid api | Value(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.
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)))}
EdDSA
func setAlgorithm(jwksURI string) {hs = jwt.NewEd25519(jwt.Ed25519PublicKey(getKey(jwksURI)))}func getKey(jwksEndpoint string) ed25519.PublicKey {...X, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(Keys.Key[0].X);return ed25519.PublicKey(X)}
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 Payloaderr = json.Unmarshal([]byte(payload), &rawPayload)...configuredScopes := strings.Split(os.Getenv("SCOPE"), " ")payloadScopes := rawPayload.Scope//Check if configured(required) scope values are missingif(!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 UnauthorizedDate: Mon, 16 Aug 2021 20:57:53 GMTContent-Length: 25Content-Type: text/plain; charset=utf-8Missing 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.
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial