Let’s create an API to validate JWT in pre-selected endpoints.
What is a JSON Web Token?
JSON Web Tokens are an open, industry-standard RFC 7519 method for representing claims securely between two parties.
Use case of what we want to solve:
- API to access resources
- We want a validation to authorize or not which resource can be consumed
Requirements:
- GO installed
- GO dependencies used in this post:
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/pkg/errors v0.9.1
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
1 — Create the project:
mkdir jwt_validation
cd jwt_validation
go mod init github.com/<user>/jwt_validation
2 — Let’s create a test in which we will register a new user, get the access token and then consume a resource with that access token.
func TestRegister(t *testing.T) {
}
func TestToken(t *testing.T) {
}
func TestGetUser(t *testing.T) {
}
3 — We would like to have a persisting layer to create and store the creds used later for each user, this can be any persisting layer, Postgres, Redis, etc, but in order to simplify the example let’s just create a persisting layer in memory like a map:
package main
import "sync"
type userDB struct {
users map[string]*User
}
var o1 sync.Once
var singleton *userDB
func UserDB() *userDB {
o1.Do(func() {
singleton = initializeUserDaoService()
})
return singleton
}
func initializeUserDaoService() *userDB {
udb := &userDB{
users: make(map[string]*User),
}
return udb
}
func (udb *userDB) AddUser(user *User) {
udb.users[user.Email] = user
}
func (udb *userDB) GetUser(email string) *User {
u, ok := udb.users[email]
if ok {
return u
}
return nil
}
func (udb *userDB) GetUserById(uid int64) *User {
for _, v := range udb.users {
if v.ID == uid {
return v
}
}
return nil
}
Here we have created a singleton class in which we have a simple map of users.
We will use this DB to store users registered with our “/register” endpoint and retrieve users in the “/token” endpoint to validate later authorized users in any private endpoint.
4 — Before going further let’s create a server structure that will define the endpoints and handlers of our API.
We will like to have something that can
server := &Server{
port: os.Getenv("PORT"),
publicEndpoints: []Endpoint{
{
Path: "/token",
Handler: Token,
Methode: http.MethodPost,
},
{
Path: "/register",
Handler: Register,
Methode: http.MethodPost,
},
},
privateEndpoints: []Endpoint{
{
Path: "/user",
Handler: GetUser,
Methode: http.MethodGet,
},
},
middlewares: []mux.MiddlewareFunc{JwtValidation()},
publicRoot: "/",
privateRoot: "/api",
}
We create a server object which will receive a port where to run the server, a list of publicEndpoints and a list of privateEndpoints, we want to have resources under the JWT validation and others with public access, for instance, create a user and create a token, also we can notice a middlewares list in where we have defined the JwtValidation middleware, but we also can add more if we like, also we have defined two root paths one private and another public.
This server object will have a RunServer call and will look like this:
func (s *Server) RunServer() {
r := mux.NewRouter()
basePath := r.PathPrefix(s.publicRoot).Subrouter()
api := basePath.PathPrefix(s.privateRoot).Subrouter()
s.registerPublicEndpoints(basePath)
s.registerEndpoints(api)
for _, v := range s.middlewares {
api.Use(v)
}
log.Fatal(http.ListenAndServe(":"+s.port, r))
}
Where we can see that we are using a mux router.
5 — Let’s work in the Handlers:
Register
Handler to register a new user
type User struct {
Email string `json:"email"`
HashedPassword string `json:"hashed_password"`
ID int64 `json:"id"`
}
type UserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type UserRegisterRequest struct {
User UserRequest `json:"user"`
}
func Register(w http.ResponseWriter, r *http.Request) {
req := &UserRegisterRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user := UserDB().GetUser(req.User.Email)
if user != nil {
http.Error(w, errors.New("User already exist").Error(), http.StatusConflict)
return
}
user = &User{
Email: req.User.Email,
ID: int64(uuid.New().ID()),
}
user.HashedPassword = GenerateBCryptPasswordUtil([]byte(req.User.Password))
UserDB().AddUser(user)
toReturn := &UserRegisterResponse{}
toReturn.User = User{
Email: user.Email,
HashedPassword: "*********",
ID: user.ID,
}
response(w, toReturn)
}
In the handler, we are creating a new user, that is not already created.
Also, because is sensible data, we hash the password before we store it. For that, we are using golang.org/x/crypto/bcrypt.
Token
Handler to get a new access token
type UserTokenRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func Token(w http.ResponseWriter, r *http.Request) {
req := &UserTokenRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user := UserDB().GetUser(req.Email)
if user == nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if CompareHashAndPasswordUtil([]byte(user.HashedPassword), []byte(req.Password)) {
log.Printf("Token :: User login attempt (%s) successful!", user.Email)
trt, err := CreateTokenResponse(user.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
response(w, trt)
} else {
http.Error(w, err.Error(), http.StatusUnauthorized)
}
}
Here we are validating that the user exists and the password is the same as the one in the DB, if successful we create a temporal JWT token.
GetUser
Handler which will return a private resource if JWT is valid.
type UserResponse struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
}
func GetUser(w http.ResponseWriter, r *http.Request) {
uid := GetUserId(r)
if uid == 0 {
http.Error(w, "user ID can't be 0", http.StatusBadRequest)
return
}
user := UserDB().GetUserById(uid)
if user == nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
toReturn := &UserResponse{
ID: user.ID,
Email: user.Email,
}
response(w, toReturn)
}
Every time a new GET request through “/api/user” hit the server the JwtValidation middleware will be executed and will look for one heather like: “Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjI0MDk4NDgxNjYiLCJyb2xlcyI6WyJVU0VSIl0sImV4cCI6MTY4NjkzODY4NCwiaXNzIjoiVG9rZW5SZXNwb25zZSJ9.TDxWB-krvlzy1koS25tqWKkU83RRbyDeCrx53DXhnA7r1COOFd38q6npwKJI7rCLSk_zxWJA3gMUKtjzEIKGaA”
We can see that we request the user ID for the request because is coming in the JWT header information.
6 — JWT middleware validation
package main
import (
"context"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"net/http"
"strings"
)
const (
JwtPropsKey string = "props"
)
func JwtValidation() mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
processJWT(w, r, next)
})
}
}
func processJWT(w http.ResponseWriter, r *http.Request, next http.Handler) {
authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ")
if len(authHeader) != 2 {
errorValidation(w, http.StatusBadRequest, "", errors.New("malformed token"))
} else {
jwtToken := authHeader[1]
claims, err := ValidateToken(jwtToken)
if err != nil {
errorValidation(w, http.StatusUnauthorized, "unauthorized", err)
}
ctx := context.WithValue(r.Context(), JwtPropsKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
}
}
func errorValidation(w http.ResponseWriter, status int, msg string, err error) {
http.Error(w, errors.Wrap(err, msg).Error(), status)
}
The important part is in processJWT, in which we can see how we collect the token from the header and if is right formed we start the validation process.
ValidateToken
func ValidateToken(jwtToken string) (jwt.Claims, error) {
token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("SECRET_KEY")), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
for index, value := range claims {
if index == "roles" {
roles := value.([]interface{})
for _, v := range roles {
if v != "USER" && v != "SYSTEM" {
return nil, errors.New("invalid role")
}
}
}
}
return claims, nil
}
return nil, errors.New("not valid token")
}
jwt.Parse the token to validate a correct format creation, firstly if is inside the expected signing method, in the dep we are using these are the one can be expected:
SigningMethodHS256 *SigningMethodHMAC
SigningMethodHS384 *SigningMethodHMAC
SigningMethodHS512 *SigningMethodHMAC
But a JWT can be created with any of: https://jwt.io/
After that is serializing the token into the Claims object in order to validate that the right ROLE is consuming the resource, and checking if the token is not expired already.
Nice :), we have now all that is needed to finish our test.
7 — Final test:
package main
import (
"bytes"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strconv"
"testing"
)
type testData struct {
user *User
loginPassword string
}
var (
router *mux.Router
)
func init() {
router = mux.NewRouter()
router.HandleFunc("/register", Register)
router.HandleFunc("/token", Token)
router.HandleFunc("/api/user", GetUser)
}
func ExeRegistration(email string, password string) (*httptest.ResponseRecorder, error) {
data := getTestData(email, password)
ur := UserRegisterRequest{User: UserRequest{
Email: data.user.Email,
Password: data.user.HashedPassword,
}}
d, err := json.Marshal(&ur)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "/register", bytes.NewBuffer(d))
if err != nil {
return nil, err
}
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
return recorder, nil
}
func ExeToken(email string, password string) (*httptest.ResponseRecorder, error) {
login := UserTokenRequest{
Email: email,
Password: password,
}
data, err := json.Marshal(&login)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "/token", bytes.NewBuffer(data))
if err != nil {
return nil, err
}
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
return recorder, nil
}
func ExeGetUser(token string) (*httptest.ResponseRecorder, error) {
req, err := http.NewRequest("GET", "/api/user", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
recorder := httptest.NewRecorder()
router.Use(JwtValidation())
router.ServeHTTP(recorder, req)
return recorder, nil
}
func TestRegister(t *testing.T) {
email := "test@test.com"
password := "123456789"
value, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
t.Fatal(err)
}
recorder, err := ExeRegistration(email, string(value))
if err != nil {
t.Fatal(err)
}
if recorder.Code != http.StatusOK {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code)
}
r := &UserRegisterResponse{}
json.NewDecoder(recorder.Body).Decode(&r)
if r.User.Email != email {
t.Errorf("Expected body '%s', but got '%s'", email, r.User.Email)
}
recorder.Flush()
}
func TestToken(t *testing.T) {
password := "123456789"
email := "test@test.com"
data := getTestData(email, password)
UserDB().AddUser(data.user)
recorder, err := ExeToken(data.user.Email, data.loginPassword)
if err != nil {
t.Fatal(err)
}
if recorder.Code != http.StatusOK {
t.Errorf("expected status code %d, but got %d", http.StatusOK, recorder.Code)
}
r := &TokenResponse{}
json.NewDecoder(recorder.Body).Decode(&r)
if r.Token == "" {
t.Errorf("expected body to be not empty")
}
_, err = ValidateToken(r.Token)
if err != nil {
t.Fatal("token invalid")
}
}
func TestGetUser(t *testing.T) {
password := "123456789"
email := "test@test.com"
data := getTestData(email, password)
UserDB().AddUser(data.user)
recorder, err := ExeToken(data.user.Email, data.loginPassword)
if err != nil {
t.Fatal(err)
}
r := &TokenResponse{}
json.NewDecoder(recorder.Body).Decode(&r)
if r.Token == "" {
t.Errorf("expected body to be not empty")
}
type testCase struct {
name string
token string
data testData
expectedStatus int
validToken bool
}
testCases := []testCase{
{
name: "Valid Token",
token: r.Token,
data: data,
expectedStatus: 200,
validToken: true,
},
{
name: "Invalid Token",
token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMTEzNDQzNDUiLCJyb2xlcyI6WyJVU0VSIl0sImV4cCI6MTY4NjkyMjIwMiwiaXNzIjoiVG9rZW5SZXNwb25zZSJ9.0OaElAsUIbRKS7RFCRsG62EndW5azjML7RBVJRM252MoknqRDrJobO_3-a_LcJEnBwL-9KdXYj93MszzIWQqyA",
data: data,
expectedStatus: 401,
validToken: false,
},
{
name: "Invalid Token",
token: createToken("NOROLE", data.user.ID),
data: data,
expectedStatus: 401,
validToken: false,
},
{
name: "Malformed Token",
token: "malformed.token",
data: data,
expectedStatus: 401,
validToken: false,
},
}
for _, v := range testCases {
t.Run(v.name, func(t *testing.T) {
recorder, err = ExeGetUser(v.token)
if recorder.Code != v.expectedStatus {
t.Errorf("Expected status code %d, but got %d", v.expectedStatus, recorder.Code)
}
_, err := ValidateToken(v.token)
if v.validToken {
if err != nil {
t.Fatal("invalid token")
}
} else {
if err == nil {
t.Fatal("expected an invalid token")
}
}
if recorder.Code == http.StatusOK {
ur := &UserResponse{}
json.NewDecoder(recorder.Body).Decode(&ur)
if ur.Email != v.data.user.Email {
t.Fatalf("expected %s, got %s", v.data.user.Email, ur.Email)
}
}
})
}
}
func getTestData(email, password string) testData {
os.Setenv("TOKEN_EXP", "5")
os.Setenv("SECRET_KEY", "9191919191919")
value := GenerateBCryptPasswordUtil([]byte(password))
hashedPassword := value
storedPassword := GenerateBCryptPasswordUtil([]byte(hashedPassword))
u := &User{
Email: email,
HashedPassword: storedPassword,
ID: int64(uuid.New().ID()),
}
toReturn := testData{
user: u,
loginPassword: hashedPassword,
}
return toReturn
}
func createToken(role string, id int64) string {
token, _ := GenerateToken(strconv.FormatUint(uint64(id), 10), []string{role}, reflect.TypeOf(TokenResponse{}))
return token
}
In the test, we are testing “/register”, “/token”, “/api/user”, and also we are testing the JWT validation process.
Complete resources here: https://github.com/nlanatta/jwt_validation
8 — Let’s talk a little more about the token creation process:
func GenerateToken(id string, roles []string, classCreator reflect.Type) (string, error) {
te, _ := strconv.ParseInt(os.Getenv("TOKEN_EXP"), 10, 64)
claims := &customClaims{
UserId: id,
ClaimRoles: roles,
StandardClaims: jwt.StandardClaims{
ExpiresAt: jwt.NewTime(float64(time.Now().Add(time.Minute * time.Duration(te)).Unix())),
Issuer: classCreator.Name(),
},
}
return GenerateTokenWithClaims(claims)
}
func GenerateTokenWithClaims(claims *customClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY")))
if err != nil {
return "", errors.Wrap(err, fmt.Sprintf("creating a token %v", err))
}
return tokenString, err
}
One super important part of this process is that we don’t want a token living forever, and this is super important because if somebody takes our token in some way they can access our private resources, and we definitively don’t want that!!, because of that this env var “TOKEN_EXP”, is important because it will say how much minutes my token will be valid, I would recommend something between 5~15 minutes, in where after that our system will return an Unauthorised error and will force you to request a new token.
Another important part is the “SECRET_KEY”, which is used to create the token and also parse that token in the validation step (middleware)
This env is sensitive data and can’t be shared with the public.
Cool, that’s all for now, this is a very simple example of how you can use JWT in your system.
Resources:
Complete resources here: https://github.com/nlanatta/jwt_validation
JWT online tester: https://jwt.io/
Good JWT practices: https://www.rfc-editor.org/rfc/rfc8725