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

    You will need to run your databases manually now:

    The new repositories can be found here:

    1. ...
    2. func (srv *service) Auth(ctx context.Context, req *pb.User, res *pb.Token) error {
    3. log.Println("Logging in with:", req.Email, req.Password)
    4. user, err := srv.repo.GetByEmail(req.Email)
    5. log.Println(user)
    6. if err != nil {
    7. return err
    8. }
    9. // Compares our given password against the hashed password
    10. // stored in the database
    11. if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
    12. return err
    13. }
    14. token, err := srv.tokenService.Encode(user)
    15. if err != nil {
    16. return err
    17. }
    18. res.Token = token
    19. return nil
    20. }
    21. func (srv *service) Create(ctx context.Context, req *pb.User, res *pb.Response) error {
    22. // Generates a hashed version of our password
    23. hashedPass, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
    24. if err != nil {
    25. return err
    26. }
    27. req.Password = string(hashedPass)
    28. if err := srv.repo.Create(req); err != nil {
    29. return err
    30. }
    31. res.User = req
    32. return nil
    33. }

    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 .

    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 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:

    1. 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 users 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.

    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 utilises public and private keys between a client and server. This is great for authenticating across services.

    Some more resources:

    • RFC spec for algorithms
      Now we've touched the 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.

    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:

    1. // shippy-user-cli/cli.go
    2. package main
    3. import (
    4. "log"
    5. "os"
    6. pb "github.com/EwanValentine/shippy-user-service/proto/user"
    7. micro "github.com/micro/go-micro"
    8. microclient "github.com/micro/go-micro/client"
    9. "golang.org/x/net/context"
    10. )
    11. func main() {
    12. srv := micro.NewService(
    13. micro.Name("go.micro.srv.user-cli"),
    14. micro.Version("latest"),
    15. )
    16. // Init will parse the command line flags.
    17. srv.Init()
    18. client := pb.NewUserServiceClient("go.micro.srv.user", microclient.DefaultClient)
    19. name := "Ewan Valentine"
    20. email := ""
    21. password := "test123"
    22. company := "BBC"
    23. r, err := client.Create(context.TODO(), &pb.User{
    24. Name: name,
    25. Email: email,
    26. Password: password,
    27. Company: company,
    28. })
    29. if err != nil {
    30. log.Fatalf("Could not create: %v", err)
    31. }
    32. log.Printf("Created: %s", r.User.Id)
    33. getAll, err := client.GetAll(context.Background(), &pb.Request{})
    34. if err != nil {
    35. log.Fatalf("Could not list users: %v", err)
    36. }
    37. for _, v := range getAll.Users {
    38. log.Println(v)
    39. }
    40. authResponse, err := client.Auth(context.TODO(), &pb.User{
    41. Email: email,
    42. Password: password,
    43. })
    44. if err != nil {
    45. log.Fatalf("Could not authenticate user: %s error: %v\n", email, err)
    46. }
    47. log.Printf("Your access token is: %s \n", authResponse.Token)
    48. // let's just exit because
    49. os.Exit(0)
    50. }

    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:

    1. // shippy-consignment-cli/cli.go
    2. ...
    3. func main() {
    4. cmd.Init()
    5. // Create new greeter client
    6. client := pb.NewShippingServiceClient("go.micro.srv.consignment", microclient.DefaultClient)
    7. // Contact the server and print out its response.
    8. file := defaultFilename
    9. var token string
    10. log.Println(os.Args)
    11. if len(os.Args) < 3 {
    12. log.Fatal(errors.New("Not enough arguments, expecing file and token."))
    13. }
    14. file = os.Args[1]
    15. token = os.Args[2]
    16. consignment, err := parseFile(file)
    17. if err != nil {
    18. log.Fatalf("Could not parse file: %v", err)
    19. // Create a new context which contains our given token.
    20. // This same context will be passed into both the calls we make
    21. // to our consignment-service.
    22. ctx := metadata.NewContext(context.Background(), map[string]string{
    23. "token": token,
    24. })
    25. // First call using our tokenised context
    26. r, err := client.CreateConsignment(ctx, consignment)
    27. if err != nil {
    28. log.Fatalf("Could not create: %v", err)
    29. }
    30. log.Printf("Created: %t", r.Created)
    31. // Second call
    32. getAll, err := client.GetConsignments(ctx, &pb.GetRequest{})
    33. if err != nil {
    34. log.Fatalf("Could not list consignments: %v", err)
    35. }
    36. for _, v := range getAll.Consignments {
    37. log.Println(v)
    38. }
    39. }

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

    1. $ make build
    2. $ docker run --net="host" \
    3. -e MICRO_REGISTRY=mdns \
    4. consignment-cli consignment.json \
    5. <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:

    1. func main() {
    2. ...
    3. myServer := grpc.NewServer(
    4. grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(AuthInterceptor),
    5. )
    6. ...
    7. }
    8. func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    9. // Set up a connection to the server.
    10. conn, err := grpc.Dial(authAddress, grpc.WithInsecure())
    11. if err != nil {
    12. log.Fatalf("did not connect: %v", err)
    13. }
    14. defer conn.Close()
    15. c := pb.NewAuthClient(conn)
    16. r, err := c.ValidateToken(ctx, &pb.ValidateToken{Token: token})
    17. if err != nil {
    18. log.Fatalf("could not authenticate: %v", err)
    19. }
    20. return handler(ctx, req)
    21. }

    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:

    Then add our new toggle in our Makefile:

    1. // shippy-user-service/Makefile
    2. ...
    3. run:
    4. docker run -d --net="host" \
    5. -p 50052 \
    6. -e MICRO_SERVER_ADDRESS=:50052 \
    7. -e MICRO_REGISTRY=mdns \
    8. 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 .

    If you are finding this series useful, and you use an ad-blocker (who can blame you). Please consider chucking me a couple of quid for my time and effort. Cheers! https://monzo.me/ewanvalentine