JSON Web Token is one of the most straight forward ways to authenticate users in a web application. With a single simple token, you can authenticate that a user is who he claims to be without having to remember that specific token on the server. This is not only performant, it simplifies horizontal scaling on the server side by eliminating the need for server side session handling.
In this article, I’ll show you how to implement JWT logic as a web component using the Lit-Element framework. We will end up with a powerful and reuseable authentication solution for any secured web application. This solution will take advantage of existing projects and libraries for handling and processing the JWT standard, be super simple to use for any secured request from page renders to api calls and can be dropped-in any place that runs web components.
For this exercise, we’ll be using LitElement to accelerate our web components and a Go backend server to generate the tokens. Of course, any web component written in LitElement can be easily written in plain vanilla Javascript. And because JWT is a popular standard, you can find existing libraries to help with the JWT mechanisms in any back end – Python, Java, Node, Php just to name a few.
At the end of the day, we will have a web component that handles all our authentication logic simply by instantiating a web component at the beginning of a web page, like this:
constructor() {
super();
this.jwtauth = new JWTAuth();
...
}
Using our tokens in http requests is as simple as:
let options = {headers:{token:this.jwtauth.token}};
axios.get('https://www.enliten.dev/api', options).then((wire) => {
...
I don’t know about you, but whenever I see critical web page logic simplify down to just a line or two, I get excited!
OK, now when I design work on web applications, I will typically carve out the back end first. But for the purposes of this article, I’m going to build me JWT web component first and let that drive the back end.
So our web component is going to do a few things for us:
- Load an existing token from local storage
- Set and store a token in local storage
- Tests is a token exists and is valid
- Invalidate a token
Without further delay, let’s get started. First, we’ll create a file for our web component: jwt-auth.js. Here is the skeleton:
class JWTAuth {
constructor() {
this.token='';
this.timestamp = 0;
this.load();
this.TIMEOUT_MS = 6*(60*60*1000); //6 hours
}
set(token) {
;
}
load(token) {
;
}
clear() {
;
}
isValid() {
;
}
}
export default JWTAuth;
The constructor is the only code block filled in at this point. What we are doing is setting up this web component to track two things: a token and a timestamp. Plus, we’ll automatically call the load() function as soon as the component is instantiated.
Now let’s work on the set() and load() functions.
set(token) {
this.token = token;
this.timestamp = (new Date()).getTime();
localStorage.setItem('token', this.token);
localStorage.setItem('timestamp', this.timestamp);
}
load() {
let ctoken = localStorage.getItem('token');
let ctimestamp = localStorage.getItem('timestamp');
if (ctoken!='') this.token = ctoken;
if (ctimestamp) this.timestamp = parseInt(ctimestamp);
}
Let’s imagine that we have a back end service that takes in a username and password and returns a JWT token assuming the username and password supplied is correct. Once we receive a JWT token, we would want to set that token in our web component.
So the set function would then have a token as a parameter and set the value to the global token variable inside the web component. The web component would also record the exact time that we stored this token. This would allow us to set rules to expire the token on the client side if the token is too old.
Both of these variables are then stored in the browser’s built in localStorage engine.
When we load this web component, we would want the component to automatically load tokens from the localStorage if it exists.
Now, we’ll fill in the isValid and clear functions.
isValid() {
var ret = false;
if (!this.token) {
return ret;
} else {
if (this.timestamp + this.TIMEOUT_MS > Date.now()) {
return true;
} else {
this.clear();
return false;
}
}
}
clear() {
this.set('');
}
The isValid() function returns true or false. If the token value is blank, it will return false. If there is a non-blank token, it will see if it is expired by checking to see if it is more than 6 hours old. If it is expired, it clears the token and returns false. Otherwise, it will return true. The clear function simply sets the token as a blank string.
Now we’ll turn our attention to the Go middleware that will help us generate the token. We’ll be using GoFr is an “opinioned Go framework for accelerated microservice development” as they put it on their website: www.gofr.dev. I’ve found GoFr immensely useful in writing middleware of all sorts. One of the reasons why I like using GoFr is that the resulting code is succinct and readable. For JWT signing support we’ll use the golang-jwt project.
We’ll need the following server backend endpoints:
- POST /login with a username and password parameter.
- GET /test
- validate(token) function that validates a given token
- generate(username) function that generates a new token given a username
First let’s implement our generate function. The generate function will take in a username and create a JWT token using the username and expiry time 6 hours from when it was created. I won’t get into the signing code for the purposes of this blog post. More information on using the golang-jwt library can be found here: https://pkg.go.dev/github.com/golang-jwt/jwt/v5
var secret = []byte("some_secret_string")
func generate(username string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
jwt.MapClaims{
"username": username,
"expiry": time.Now().Add(time.Hour * 6).Unix(),
})
tokenString, err := token.SignedString(secret)
if err != nil {
return "", err
}
return tokenString, nil
}
Next, we’ll need a validate function that takes a given token and validates it. The following conditions will return false indicating an invalid token:
- Error on parsing the token
- token.Valid returns false
- Error on retrieving token claims
- username field is not blank
- expiry is before now
At the end of the function, if we haven’t returned a false, then the token is valid, carries a username and has not expired.
func validate(tokenString string) boolean {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil {
return false
}
if !token.Valid {
return false
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
if len(claims["username"]) <= 0 {
return false
}
if time.Now() > claims["expiry"] {
return false
}
} else {
return false
}
return true
}
Now for the /login endpoint. First, we start with a User struct where we can store details about a user. Using GoFr’s app instance, we create a HTTP Post endpoint with /login. The post body will include a json payload of an email and password. The ctx.Bind() method automatically extracts the email and password based on the json payload (one of the many perks of using an opinionated middleware framework). We then use the email and password to call the getUser() method to return a user object. The getUser method is not written here as it goes beyond the scope of this article. Presumably, getUser will connect to a database to validate the username and password supplied and return a user struct.
Once we have a user struct, we run the generate function using the username field.
type User struct {
userid int64
email string
first string
last string
company string
status string
parent string
password string
}
app.POST("/login", func(ctx *gofr.Context) (interface{}, error) {
type postLogin struct {
email string
password string
}
var postlogin postLogin
err := ctx.Bind(&postlogin)
if err != nil {
return nil, errors.New("invalid param: body")
}
user := getUser(postlogin.email, postlogin.password)
if user != nil {
return generate(user.username)
}
})
Now, we’ll create a /test GET method to test JWT authentication. The gofr.Context has a method Request() which returns *http.Request for the HTTP request. From this, we can pull the Authorization header field, which we expect a value of “Bearer token_value”. The reason why there is the string “Bearer” in front of the token string is to adhere to the bearer token standard.
app.GET("/test", func(ctx *gofr.Context) (interface{}, error) {
req := ctx.Request()
authString := req.Header.Get("Authorization")
tokenString := strings.Fields(authString)[1]
return validate(tokenString)
}