DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • 7 Microservices Best Practices for Developers
  • Securing Spring Boot Microservices with JSON Web Tokens (JWT)
  • A Comparison of Current Kubernetes Distributions
  • Authentication With Remote LDAP Server in Spring Web MVC

Trending

  • Infrastructure as Code (IaC) Beyond the Basics
  • Operational Principles, Architecture, Benefits, and Limitations of Artificial Intelligence Large Language Models
  • Building Reliable LLM-Powered Microservices With Kubernetes on AWS
  • The Full-Stack Developer's Blind Spot: Why Data Cleansing Shouldn't Be an Afterthought
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Deployment
  4. Create Versatile Microservices in Golang — Part 4 (Authentication With JWT)

Create Versatile Microservices in Golang — Part 4 (Authentication With JWT)

Learn how to implement authentication with JWT, or JSON Web Tokens, in your miroservices built with Golang.

By 
Ewan Valentine user avatar
Ewan Valentine
·
Jun. 22, 18 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
9.9K Views

Join the DZone community and get the full member experience.

Join For Free

In the previous part in this series, we looked at creating a user service and started storing some users. Now we need to look at making our user service store users passwords securely, and create some functionality to validate users and issue secure tokens across our microservices.

Note, I have now split out our services into separate repositories. I find this is easier to deploy. Initially, I was going to attempt to do this as a monorepo, but I found it too tricky to set this up with Go's dep management without getting various conflicts. I will also begin to demonstrate how to run and test microservices independently.

Unfortunately, we'll be losing docker-compose with this approach. But that's fine for now. If you have any suggestions on this, feel free to send them over!

You will need to run your databases manually now:

$ docker run -d -p 5432:5432 postgres
$ docker run -d -p 27017:27017 mongo

The new repositories can be found here:

  • https://github.com/EwanValentine/shippy-consignment-service
  • https://github.com/EwanValentine/shippy-user-service
  • https://github.com/EwanValentine/shippy-vessel-service
  • https://github.com/EwanValentine/shippy-user-cli
  • https://github.com/EwanValentine/shippy-consignment-cli

First of all, let's update our user handler to hash our passwords, this is an absolute must. You should never, ever store plain-text passwords. Many of you will be thinking 'duh that's obvious', but unfortunately it's still done!

// shippy-user-service/handler.go
... 
func (srv *service) Auth(ctx context.Context, req *pb.User, res *pb.Token) error {
log.Println("Logging in with:", req.Email, req.Password)
user, err := srv.repo.GetByEmail(req.Email)
log.Println(user)
if err != nil {
return err
}

// Compares our given password against the hashed password
// stored in the database
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return err
}

token, err := srv.tokenService.Encode(user)
if err != nil {
return err
}
res.Token = token
return nil
}

func (srv *service) Create(ctx context.Context, req *pb.User, res *pb.Response) error {

// Generates a hashed version of our password
hashedPass, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
req.Password = string(hashedPass)
if err := srv.repo.Create(req); err != nil {
return err
}
res.User = req
return nil
}

Not a huge amount has changed here, except we've added our password hashing functionality, and we set it as our password before saving a new user. Also, on authentication, we check against the hashed password.

Now we can securely authenticate a user against the database, we need a mechanism in which we can do this across our user interfaces and distributed services. There are many ways in which to do this, but the simplest solution I've come across, which we can use across our services and web, is JWT.

But before we crack on, please do check the changes I've made to the Dockerfiles and the Makefiles of each service. I've also updated the imports to match the new git repositories.

JWT

JWT stands for JSON web tokens and is a distributed security protocol. Similar to OAuth. The concept is simple, you use an algorithm to generate a unique hash for a user, which can be compared and validated against. But not only that, the token itself can contain and be made up of our users' metadata. In other words, their data can itself become a part of the token. So let's look at an example of a JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ 

The token is separated into three by .'s. Each segment has a significance. The first segment is made up of some metadata about the token itself. Such as the type of token and the algorithm used to create the token. This allows clients to understand how to decode the token. The second segment is made up of user-defined metadata. This can be your user's details, an expiration time, anything you wish. The final segment is the verification signature, which is information on how to hash the token and what data to use.

Of course, there are also downsides and risks in using JWT; this article outlines those pretty well. Also, I'd recommend reading this article for security best practices.

One I'd recommend you look into in particular, is getting the users origin IP, and using that to form part of the token claims. This ensures someone can't steal your token and act as you on another device. Ensuring you're using https helps to mitigate this attack type, as it obscures your token from man-in-the-middle style attacks.

There are many different hashing algorithms you can use to hash JWT's, which commonly fall into two categories. Symmetric and Asymmetric. Symmetric is like the approach we're using, using a shared salt. Asymmetric utilizes public and private keys between a client and server. This is great for authenticating across services.

Some more resources:

  • Auth0
  • RFC spec for algorithms

Now we've touched on the basics of what a JWT is, let's update our token_service.go to perform these operations. We'll be using a fantastic Go library for this: github.com/dgrijalva/jwt-go, which contains some great examples.

// shippy-user-service/token_service.go
package main

import (
"time"

pb "github.com/EwanValentine/shippy-user-service/proto/user"
"github.com/dgrijalva/jwt-go"
)

var (

// Define a secure key string used
// as a salt when hashing our tokens.
// Please make your own way more secure than this,
// use a randomly generated md5 hash or something.
key = []byte("mySuperSecretKeyLol")
)

// CustomClaims is our custom metadata, which will be hashed
// and sent as the second segment in our JWT
type CustomClaims struct {
User *pb.User
jwt.StandardClaims
}

type Authable interface {
Decode(token string) (*CustomClaims, error)
Encode(user *pb.User) (string, error)
}

type TokenService struct {
repo Repository
}

// Decode a token string into a token object
func (srv *TokenService) Decode(tokenString string) (*CustomClaims, error) {

// Parse the token
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return key, nil
})

// Validate the token and return the custom claims
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
} else {
return nil, err
}
}

// Encode a claim into a JWT
func (srv *TokenService) Encode(user *pb.User) (string, error) {

expireToken := time.Now().Add(time.Hour * 72).Unix()

// Create the Claims
claims := CustomClaims{
user,
jwt.StandardClaims{
ExpiresAt: expireToken,
Issuer:    "go.micro.srv.user",
},
}

// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Sign token and return
return token.SignedString(key)
}

As per, I've left comments explaining some of the finer details, but the premise here is fairly simple. Decode takes a string token, parses it into a token object, validates it, and returns the claims if valid. This will allow us to take the user metadata from the claims in order to validate that user.

The Encode method does the opposite, this takes your custom metadata, hashes it into a new JWT and returns it.

Note we also set a 'key' variable at the top, this is a secure salt, please use something more secure than this in production.

Now we have a validate token service. Let's update our user-cli, I've simplified this to just be a script for now as I was having issues with the previous CLI code, I'll come back to this, but this tool is just for testing:

// shippy-user-cli/cli.go
package main

import (
"log"
"os"

pb "github.com/EwanValentine/shippy-user-service/proto/user"
micro "github.com/micro/go-micro"
microclient "github.com/micro/go-micro/client"
"golang.org/x/net/context"
)

func main() {

srv := micro.NewService(

micro.Name("go.micro.srv.user-cli"),
micro.Version("latest"),
)

// Init will parse the command line flags.
srv.Init()

client := pb.NewUserServiceClient("go.micro.srv.user", microclient.DefaultClient)

name := "Ewan Valentine"
email := "ewan.valentine89@gmail.com"
password := "test123"
company := "BBC"

r, err := client.Create(context.TODO(), &pb.User{
Name:     name,
Email:    email,
Password: password,
Company:  company,
})
if err != nil {
log.Fatalf("Could not create: %v", err)
}
log.Printf("Created: %s", r.User.Id)

getAll, err := client.GetAll(context.Background(), &pb.Request{})
if err != nil {
log.Fatalf("Could not list users: %v", err)
}
for _, v := range getAll.Users {
log.Println(v)
}

authResponse, err := client.Auth(context.TODO(), &pb.User{
Email:    email,
Password: password,
})

if err != nil {
log.Fatalf("Could not authenticate user: %s error: %v\n", email, err)
}

log.Printf("Your access token is: %s \n", authResponse.Token)

// let's just exit because
os.Exit(0)
}

We just have some hard-coded values for now, replace those and run the script using $ make build && make run. You should see a token returned. Copy and paste this long token string, you will need it soon!

Now we need to update our consignment-cli to take a token string and pass it into the context to our consignment-service:

// shippy-consignment-cli/cli.go
...
func main() {

cmd.Init()

// Create new greeter client
client := pb.NewShippingServiceClient("go.micro.srv.consignment", microclient.DefaultClient)

// Contact the server and print out its response.
file := defaultFilename
var token string
log.Println(os.Args)

if len(os.Args) < 3 {
log.Fatal(errors.New("Not enough arguments, expecing file and token."))
}

file = os.Args[1]
token = os.Args[2]

consignment, err := parseFile(file)

if err != nil {
log.Fatalf("Could not parse file: %v", err)
}

// Create a new context which contains our given token.
// This same context will be passed into both the calls we make
// to our consignment-service.
ctx := metadata.NewContext(context.Background(), map[string]string{
"token": token,
})

// First call using our tokenised context
r, err := client.CreateConsignment(ctx, consignment)
if err != nil {
log.Fatalf("Could not create: %v", err)
}
log.Printf("Created: %t", r.Created)

// Second call
getAll, err := client.GetConsignments(ctx, &pb.GetRequest{})
if err != nil {
log.Fatalf("Could not list consignments: %v", err)
}
for _, v := range getAll.Consignments {
log.Println(v)
}
}

Now we need to update our consignment-service to check the request for a token, and pass it to our user-service:

// shippy-consignment-service/main.go
func main() {
    ... 
    // Create a new service. Optionally include some options here.
srv := micro.NewService(

// This name must match the package name given in your protobuf definition
micro.Name("go.micro.srv.consignment"),
micro.Version("latest"),
        // Our auth middleware
micro.WrapHandler(AuthWrapper),
)
    ...
}

... 

// AuthWrapper is a high-order function which takes a HandlerFunc
// and returns a function, which takes a context, request and response interface.
// The token is extracted from the context set in our consignment-cli, that
// token is then sent over to the user service to be validated.
// If valid, the call is passed along to the handler. If not,
// an error is returned.
func AuthWrapper(fn server.HandlerFunc) server.HandlerFunc {
return func(ctx context.Context, req server.Request, resp interface{}) error {
meta, ok := metadata.FromContext(ctx)
if !ok {
return errors.New("no auth meta-data found in request")
}

// Note this is now uppercase (not entirely sure why this is...)
token := meta["Token"]
log.Println("Authenticating with token: ", token)

// Auth here
authClient := userService.NewUserServiceClient("go.micro.srv.user", client.DefaultClient)
_, err := authClient.ValidateToken(context.Background(), &userService.Token{
Token: token,
})
if err != nil {
return err
}
err = fn(ctx, req, resp)
return err
}
}

Now let's run our consignment-cli tool, cd into our new shippy-consignment-cli repo and run $ make build to build our new docker image, now run:

$ make build
$ docker run --net="host" \
      -e MICRO_REGISTRY=mdns \
      consignment-cli consignment.json \
      <TOKEN_HERE>

Notice we're using the --net="host" flag when running our docker containers. This tells Docker to run our containers on our host network, i.e 127.0.0.1 or localhost, rather than an internal Docker network. Note, you won't need to do any port forwarding with this approach. So instead of -p 8080:8080 you can just do -p 8080. Read more about Docker networking.

Now when you run this, you should see a new consignment has been created. Try removing a few characters from the token, so that it becomes invalid. You should see an error.

So there we have it, we've created a JWT token service, and a middleware to validate JWT tokens to validate a user.

If you're not wanting to use go-micro and you're using vanilla grpc, you'll want your middleware to look something like:

func main() {
    ... 
    myServer := grpc.NewServer(
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(AuthInterceptor),
    )
    ... 
}

func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

    // Set up a connection to the server.
    conn, err := grpc.Dial(authAddress, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewAuthClient(conn)
    r, err := c.ValidateToken(ctx, &pb.ValidateToken{Token: token})

    if err != nil {
    log.Fatalf("could not authenticate: %v", err)
    }

    return handler(ctx, req)
}

This set-up's getting a little unwieldy to run locally. But we don't always need to run every service locally. We should be able to create services which are independent and can be tested in isolation. In our case, if we want to test our consignment-service, we might not necessarily want to have to run our auth-service. So one trick I use is to toggle calls to other services on or off.

I've updated our consignment-service auth wrapper:

// shippy-user-service/main.go
...
func AuthWrapper(fn server.HandlerFunc) server.HandlerFunc {
return func(ctx context.Context, req server.Request, resp interface{}) error {
        // This skips our auth check if DISABLE_AUTH is set to true
if os.Getenv("DISABLE_AUTH") == "true" {
return fn(ctx, req, resp)
}
...
}
}

Then add our new toggle in our Makefile:

// shippy-user-service/Makefile
...
run:
docker run -d --net="host" \
-p 50052 \
-e MICRO_SERVER_ADDRESS=:50052 \
-e MICRO_REGISTRY=mdns \
-e DISABLE_AUTH=true \
consignment-service

This approach makes it easier to run certain sub-sections of your microservices locally, there are a few different approaches to this problem, but I've found this to be the easiest. I hope you've found this useful, despite the slight change in direction. Also, any advice on running go microservices as a monorepo would be greatly welcome, as it would make this series a lot easier!

Any bugs, mistakes, or feedback on this article, or anything you would find helpful, please drop me an email.

Sponsor me on Patreon to support more content like this.

JWT (JSON Web Token) microservice Docker (software) Web Service authentication Golang

Published at DZone with permission of Ewan Valentine, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • 7 Microservices Best Practices for Developers
  • Securing Spring Boot Microservices with JSON Web Tokens (JWT)
  • A Comparison of Current Kubernetes Distributions
  • Authentication With Remote LDAP Server in Spring Web MVC

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!