JSON Web Tokens

Introduction

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.

When should you use JSON Web Tokens?

Here are some scenarios where JSON Web Tokens are useful:

Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Single Sign On is a feature that widely uses JWT nowadays, because of its small overhead and its ability to be easily used across different domains.

Information Exchange: JSON Web Tokens are a good way of securely transmitting information between parties. Because JWTs can be signed—for example, using public/private key pairs—you can be sure the senders are who they say they are. Additionally, as the signature is calculated using the header and the payload, you can also verify that the content hasn't been tampered with.

Read more at: https://jwt.io/introduction

JWT in Go

In Go there are, mainly, two packages that you can use for easy JWT implementation:

The difference between the two of them, except the different API methods, is that the last one provides Token encryption and decryption functionality (https://www.rfc-editor.org/info/rfc7516) while the first does not.

In Iris there are also two implementations of JWT middleware respectfully:

Again, the first one is using the dgrijalva/jwt-go package and it's developed mostly by the Iris Community. While the second one, designed by the Iris author, is using the go-jose/v3 package one.

Now, depending on your application requirements you may choose the community edition or the builtin JWT middleware. If you need encryption (provides extended security but it generates and verifies a token a bit slower, as expected) then you go with the kataras/iris/middleware/jwt otherwise choose the iris-contrib/middleware/jwt. We will refer to the iris-contrib/middleware/jwt as the "Community Edition" and the kataras/iris/middleware/jwt as the "Builtin Edition".

Community Edition

Provides basic and well-tested JWT functionality for your APIs.

Example can be found at: https://github.com/iris-contrib/middleware/tree/master/jwt/_example/main.go

1. Install with go get github.com/iris-contrib/middleware/jwt@master

2. Import in your code import "github.com/iris-contrib/middleware/jwt"

3. Define a HS256 secret, e.g. const mySecret = []byte("My Secret"). In production you usually load it from a local file or from system environment variables

4. Initialize the middleware:

j := jwt.New(jwt.Config{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
return mySecret, nil
},
SigningMethod: jwt.SigningMethodHS256,
})

5. Generate a token:

token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"foo": "bar",
})
tokenString, _ := token.SignedString(mySecret)

6. Verify a token with the j.Serve before the main handler, e.g.:

app.Get("/protected", j.Serve, protectedHandler)

Or per group of routes:

usersAPI := app.Party("/users", j.Serve)
usersAPI.Get(protectedHandler)

7. Get the verified claims stored in Context's key of "jwt":

func protectedHandler(ctx iris.Context) {
// Get the Token verified in the previous handler (of `j.Serve`).
user := ctx.Values().Get("jwt").(*jwt.Token)
// A map type of our stored Claims on this verified Token.
foobar := user.Claims.(jwt.MapClaims)
for key, value := range foobar {
ctx.Writef("%s = %s", key, value)
}
}

By default the token is extracted by the Authorization: Bearer $TOKEN header. To change this behavior, you can set custom TokenExtractor in the JWT middleware's configuration. A TokenExtractor looks like that:

type TokenExtractor func(iris.Context) (string, error)

The middleware package contains some builtin extractors like the FromParameter one. For example, if you want to accept a token only by a "token" URL Query Parameter do that:

j := jwt.New(jwt.Config{
// [...other fields]
Extractor: jwt.FromParameter("token"),
})

You can also use more token extractors by wrapping them with the FromFirst extractor, e.g. try to receive the token from the "Authorization" Header and the "token" URL Query Parameter:

j := jwt.New(jwt.Config{
// [...other fields]
Extractor: jwt.FromFirst(FromAuthHeader, jwt.FromParameter("token")),
})

Builtin Edition

Provides advanced JWT functionality, ideal for web experts.

Example can be found at: https://github.com/kataras/iris/blob/master/_examples/auth/jwt

1. Import in your code import "github.com/kataras/iris/v12/middleware/jwt"

2. Initialize the middleware. There are plenty of options to choose. Let's continue by using HS256 key, we use the HMAC function which, under the hoods calls the New(..., jwt.HS256, ...) function (more options in the end of the section):

j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")

Arguments of HMAC:

  1. Expiration duration

  2. One or two string keys: the first key is used for the token generation. The second key, if exists, is used for the token encryption

3. Declare a struct of custom claims (optional):

// UserClaims a custom claims structure.
// * Fields should be exported in order
// to be able to decoded later on.
// * We could just use jwt.Claims.
type UserClaims struct {
jwt.Claims
Username string
}

4. Write a token as plain text with the WriteToken method:

app.Get("/authenticate", func(ctx iris.Context) {
claims := UserClaims{
Claims: j.Expiry(jwt.Claims{
Issuer: "an-issuer",
Audience: jwt.Audience{"an-audience"},
},
Username: "kataras",
}
j.WriteToken(ctx, claims)
})

4.1 Or, if you want to control the response type, generate a token with the Token method instead:

accessToken, err := j.Token(claims)
// returns the token as string and an error.

5 To verify a token, you have three options:

5.1 with the Verify method registered as middleware and ReadClaims to extract the claims:

app.Use(j.Verify)
app.Get("/protected", func(ctx iris.Context){
var claims UserClaims
err := j.ReadClaims(ctx, &claims)
// [claims.Username]
})

5.2 with the VerifyToken method inside the handler itself. The VerifyToken reads the Token from a list of registered Token Extractors (like the community edition, you can register custom extractors, will cover that later on):

app.Get("/protected", func(ctx iris.Context){
var claims UserClaims
if err := j.VerifyToken(ctx, &claims); err != nil {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
})

To manually verify a generated raw string Token use the VerifyTokenString method instead:

VerifyTokenString(ctx iris.Context, tok string, claimsPtr interface{}) error

5.3 if the JWT verification is used in a reasonable amount of route handlers, prefer the following way; create a custom middleware which will verify, extract and store the claims into the Context using the jwt.ClaimsContextKey and jwt.Get(ctx) to get the claims in the next handler(s). This should be done manually as the Claims can be a custom go struct which implements one of the following interfaces:

type (
claimsValidator interface {
ValidateWithLeeway(e jwt.Expected, leeway time.Duration) error
}
claimsAlternativeValidator interface {
Validate() error
}
claimsContextValidator interface {
Validate(ctx iris.Context) error
}
)

Note that, the jwt.Claims implements the claimsValidator - that's why we embedded it on our UserClaims custom go struct.

Let's create our custom verify middleware:

func verify(ctx iris.Context) {
var claims Userclaims
// Extract and verify Token. Bind the claims.
// If invalid token then stop the execution
// and send 401 HTTP status code to the client.
if err := j.VerifyToken(ctx, &claims); err != nil {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
// If all OK, set our custom claims to the Context.
ctx.Values().Set(jwt.ClaimsContextKey, claims)
// Proceed with the handler chain.
ctx.Next()
}

Now, register the above middleware. Retrieve the Claims through the jwt.Get helper function:

// Register our middleware.
app.Use(verify)
// Get the claims and work with them.
app.Get("/protected", func(ctx iris.Context) {
claims, _ := jwt.Get(ctx).(UserClaims)
// [claims.Username]
})

Token Extractors

The TokenExtractor type looks like that:

type TokenExtractor func(iris.Context) string

The middleware provides 3 token extractor helpers:

  1. FromHeader - extracts the token from the Authorization: Bearer {token} header (defaults).

  2. FromQuery - extracts the token from the "token" URL Query Parameter (defaults).

  3. FromJSON(jsonKey string) - extracts the token from a request payload(body) with a key of "jsonKey", e.g. FromJSON("access_token") will retrieve the token from a request body of: {"access_token": "$TOKEN", ...}.

The JWT.Extractors field (the result of the jwt.New/HMAC/RSA functions) defaults to: []TokenExtractor{FromHeader, FromQuery}, so it tries to read the token from the Authorization: Bearer header and if not found then it tries to extract it through the "token" URL Query Parameter. You can always customize this slice field to match your application's requirements, example of adding a custom extractor:

j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
j.Extractors = append(j.Extractors, func(ctx iris.Context) string {
// Return the raw token string from the request.
//
// For the sake of the example, we will try to
// retrieve the token, if all previous extractors failed,
// through a "X-Token" custom header:
return ctx.GetHeader("X-Token")
})

The New function

The jwt.New package-level function can be used to, manually, register an signing & verification algorithm.

New(d time.Duration, alg SignatureAlgorithm, k interface{}) (*JWT, error)

Example Code:

j, err := jwt.New(15*time.Minute, jwt.HS256, []byte("secret"))

Optionally, add token encryption after New with the WithEncryption method:

err = j.WithEncryption(jwt.A128GCM, jwt.DIRECT, []byte("itsa16bytesecret"))

The middleware contains two builtin helpers for initializing a new JWT instance.

HMAC(maxAge time.Duration, keys ...string) *JWT

The HMAC function returns a new JWT instance with HS256 and A128GCM signing & verification and encryption & decryption algorithms.

If "keys" argument is missing then the function tries to read hmac256 secret keys from system environment variables:

  • JWT_SECRET for signing and verification key and

  • JWT_SECRET_ENC for encryption and decryption key

It panics on errors.

RSA(maxAge time.Duration, filenames ...string) *JWT

The RSA function returns a new JWT instance with RS256 and A128CBCHS256 signing & verification and encryption & decryption algorithms.

It tries to parse RSA256 keys from the given "filenames[0]" (defaults to "jwt_sign.key") and "filenames[1]" (defaults to "jwt_enc.key") files. If the "filenames" argument is missing, it generates and exports new random keys.

It panics on errors.

Refresh Token

Access tokens are used to identify a user without tapping into the database.

Refresh tokens are used for refreshing expired access tokens. After an access token expires, the refresh token is used to get a new pair of access and refresh tokens.

Example Code:

Read the comments too.

http://localhost:8080
# (401)
http://localhost:8080/authenticate
# (200) (response JSON {access_token, refresh_token})
http://localhost:8080?token={access_token}
# (200)
http://localhost:8080?token={refresh_token}
# (401)
http://localhost:8080/refresh
# (request JSON{refresh_token = {refresh_token}})
# (200) (response JSON {access_token, refresh_token})
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
// UserClaims a custom claims structure. You can just use jwt.Claims too.
type UserClaims struct {
jwt.Claims
Username string
}
// TokenPair holds the access token and refresh token response.
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func main() {
app := iris.New()
// Access token, short-live.
accessJWT := jwt.HMAC(
15*time.Minute,
"secret",
"itsa16bytesecret",
)
// Refresh token, long-live. Important: Give different secret keys(!)
refreshJWT := jwt.HMAC(
1*time.Hour,
"other secret",
"other16bytesecre",
)
// On refresh token, we extract it only from a request body
// of JSON, e.g. {"refresh_token": $token }.
// You can also do it manually in the handler level though.
refreshJWT.Extractors = []jwt.TokenExtractor{
jwt.FromJSON("refresh_token"),
}
// Generate access and refresh tokens and send to the client.
app.Get("/authenticate", func(ctx iris.Context) {
tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
if err != nil {
ctx.StopWithStatus(iris.StatusInternalServerError)
return
}
ctx.JSON(tokenPair)
})
app.Get("/refresh", func(ctx iris.Context) {
// Manual (if jwt.FromJSON missing):
// var payload = struct {
// RefreshToken string `json:"refresh_token"`
// }{}
//
// err := ctx.ReadJSON(&payload)
// if err != nil {
// ctx.StatusCode(iris.StatusBadRequest)
// return
// }
//
// j.VerifyTokenString(ctx, payload.RefreshToken, &claims)
var claims jwt.Claims
if err := refreshJWT.VerifyToken(ctx, &claims); err != nil {
ctx.Application().Logger().Warnf("verify refresh token: %v",
err)
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
userID := claims.Subject
if userID == "" {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
// Simulate a database call against our jwt subject.
if userID != "53afcf05-38a3-43c3-82af-8bbbe0e4a149" {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
// All OK, re-generate the new pair and send to client.
tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
if err != nil {
ctx.StopWithStatus(iris.StatusInternalServerError)
return
}
ctx.JSON(tokenPair)
})
app.Get("/", func(ctx iris.Context) {
var claims UserClaims
if err := accessJWT.VerifyToken(ctx, &claims); err != nil {
ctx.StopWithStatus(iris.StatusUnauthorized)
return
}
ctx.Writef("Username: %s\nExpires at: %s\n",
claims.Username, claims.Expiry.Time())
})
app.Listen(":8080")
}
func generateTokenPair(acc, ref *jwt.JWT) (TokenPair, error) {
standardClaims := jwt.Claims{
Issuer: "an-issuer",
Audience: jwt.Audience{"an-audience"},
}
customClaims := UserClaims{
Claims: acc.Expiry(standardClaims),
Username: "kataras",
}
accessToken, err := acc.Token(customClaims)
if err != nil {
return TokenPair{}, err
}
// At refresh tokens you don't need any custom claims.
refreshClaims := ref.Expiry(jwt.Claims{
ID: "refresh_kataras",
// For example, the User ID,
// this is necessary to check against the database
// if the user still exist or has credentials to access our page.
Subject: "53afcf05-38a3-43c3-82af-8bbbe0e4a149",
})
refreshToken, err := ref.Token(refreshClaims)
if err != nil {
return TokenPair{}, err
}
return TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
}, nil
}

Using Dependency Injection to access Token Claims

As we've seen in the Dependency Injection section, we can register any function that returns any value and use that to bind a handler's input arguments. The same applies to the Token's Claims.

Here is a simple example:

package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
func main() {
app := iris.New()
app.ConfigureContainer(register)
app.Listen(":8080")
}
func register(api *iris.APIContainer) {
j := jwt.HMAC(15*time.Minute, "secret", "secretforencrypt")
api.RegisterDependency(func(ctx iris.Context) (claims userClaims) {
if err := j.VerifyToken(ctx, &claims); err != nil {
ctx.StopWithError(iris.StatusUnauthorized, err)
return
}
return
})
api.Get("/authenticate", writeToken(j))
api.Get("/restricted", restrictedPage)
}
type userClaims struct {
jwt.Claims
Username string
}
func writeToken(j *jwt.JWT) iris.Handler {
return func(ctx iris.Context) {
j.WriteToken(ctx, userClaims{
Claims: j.Expiry(jwt.Claims{Issuer: "an-issuer"}),
Username: "kataras",
})
}
}
func restrictedPage(claims userClaims) string {
// userClaims.Username: kataras
return "userClaims.Username: " + claims.Username
}