In this article, you’ll learn how to implement JWT access and refresh tokens with gRPC using Golang, MongoDB-Go-driver, Gomail, Docker, and Docker-compose.

CRUD RESTful API with Golang + MongoDB Series:

  1. API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
  2. Golang & MongoDB: JWT Authentication and Authorization
  3. API with Golang + MongoDB: Send HTML Emails with Gomail
  4. API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
  5. Build Golang gRPC Server and Client: SignUp User & Verify Email
  6. Build Golang gRPC Server and Client: Access & Refresh Tokens
  7. Build CRUD RESTful API Server with Golang, Gin, and MongoDB
  8. Build CRUD gRPC Server API & Client with Golang and MongoDB
Build Golang gRPC Server and Client Access & Refresh Tokens

What the course will cover

  • How to create a gRPC API to sign in the registered user.
  • How to create a gRPC API to retrieve the authenticated user’s information.
  • How to create gRPC clients with Golang.

Prerequisites

Software

VS Code Extensions

  • DotENV – To get syntax highlighting in the .env file.
  • Proto3 – To get syntax highlighting, syntax validation, and code formatting in the .proto files.
  • MySQL – A GUI to view the data stored in the MongoDB database.

Create the gRPC Request and Response Messages

Create the gRPC User messages

To define a protocol buffer message, we specify the field type followed by the field name and a unique positive number starting from 1.

The unique number will be used by the gRPC framework to identify the specified fields in the message binary format.

Update the proto/user.proto file to have the UserResponse message.

proto/user.proto


syntax = "proto3";

package pb;

import "google/protobuf/timestamp.proto";
option go_package = "github.com/wpcodevo/golang-mongodb/pb";

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  string role = 4;
  google.protobuf.Timestamp created_at = 5;
  google.protobuf.Timestamp updated_at = 6;
}

message UserResponse { User user = 1; }

message GenericResponse {
  string status = 1;
  string message = 2;
}

Define the gRPC Request and Response Message to Login User

In the proto folder, create an rpc_signin_user.proto file to contain the request and response Protobuf messages needed to log in a user.

The SignInUserInput Protobuf message contains the fields required to sign in the registered user.

Next, create the SignInUserResponse Protobuf message to return the access and refresh tokens.

proto/rpc_signin_user.proto


syntax = "proto3";

package pb;

option go_package = "github.com/wpcodevo/golang-mongodb/pb";

message SignInUserInput {
  string email = 1;
  string password = 2;
}

message SignInUserResponse {
  string status = 1;
  string access_token = 2;
  string refresh_token = 3;
}

Update the Authentication gRPC Service

In the proto/auth_service.proto file, add a new RPC method to sign in the registered user.

Also, remember to import the rpc_signin_user.proto file since we are making use of the SignInUserInput and SignInUserResponse Protobuf messages.

proto/auth_service.proto


syntax = "proto3";

package pb;

import "rpc_signin_user.proto";
import "rpc_signup_user.proto";
import "user.proto";

option go_package = "github.com/wpcodevo/golang-mongodb/pb";

service AuthService {
  rpc SignUpUser(SignUpUserInput) returns (GenericResponse) {}
  rpc SignInUser(SignInUserInput) returns (SignInUserResponse) {}
  rpc VerifyEmail(VerifyEmailRequest) returns (GenericResponse) {}
}

message VerifyEmailRequest { string verificationCode = 1; }

Create a gRPC User Service

Instead of putting all the RPC methods in a single service, I decided to separate them to have everything related to authentication in one service and everything relating to a user also in another service.

Create a user_service.proto file in the proto folder and add the code snippets below.

proto/user_service.proto


syntax = "proto3";

package pb;

import "user.proto";

option go_package = "github.com/wpcodevo/golang-mongodb/pb";

service UserService {
  rpc GetMe(GetMeRequest) returns (UserResponse) {}
}

message GetMeRequest { string Id = 1; }


The GetMe RPC method will be responsible for returning the authenticated user’s credentials.

Create the gRPC Controllers

Create an rpc_signin_user.go file in the gapi folder and paste the code snippets below into it.

gapi/rpc_signin_user.go


package gapi

import (
	"context"

	"github.com/wpcodevo/golang-mongodb/pb"
	"github.com/wpcodevo/golang-mongodb/utils"
	"go.mongodb.org/mongo-driver/mongo"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (authServer *AuthServer) SignInUser(ctx context.Context, req *pb.SignInUserInput) (*pb.SignInUserResponse, error) {
	user, err := authServer.userService.FindUserByEmail(req.GetEmail())
	if err != nil {
		if err == mongo.ErrNoDocuments {

			return nil, status.Errorf(codes.InvalidArgument, "Invalid email or password")

		}

		return nil, status.Errorf(codes.Internal, err.Error())

	}

	if !user.Verified {

		return nil, status.Errorf(codes.PermissionDenied, "You are not verified, please verify your email to login")

	}

	if err := utils.VerifyPassword(user.Password, req.GetPassword()); err != nil {

		return nil, status.Errorf(codes.InvalidArgument, "Invalid email or Password")

	}

	// Generate Tokens
	access_token, err := utils.CreateToken(authServer.config.AccessTokenExpiresIn, user.ID, authServer.config.AccessTokenPrivateKey)
	if err != nil {

		return nil, status.Errorf(codes.PermissionDenied, err.Error())

	}

	refresh_token, err := utils.CreateToken(authServer.config.RefreshTokenExpiresIn, user.ID, authServer.config.RefreshTokenPrivateKey)
	if err != nil {
		return nil, status.Errorf(codes.PermissionDenied, err.Error())
	}

	res := &pb.SignInUserResponse{
		Status:       "success",
		AccessToken:  access_token,
		RefreshToken: refresh_token,
	}

	return res, nil
}


Below is a summary of the code above:

  • I first retrieved the user’s email from the request and made a query to the MongoDB database to check if a user with that email address exists.
  • Then I checked if the user’s email address is verified before validating the provided password.
  • Next, I generated both the access and refresh tokens and sent them in the response Protobuf message.

Next, create an rpc_get_me.go file to contain the code for returning the authenticated user’s information.

At the moment we’re extracting the user’s ID from the request but in an upcoming tutorial, we’ll get the ID from the decoded access token.

gapi/rpc_get_me.go


package gapi

import (
	"context"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (userServer *UserServer) GetMe(ctx context.Context, req *pb.GetMeRequest) (*pb.UserResponse, error) {
	id := req.GetId()
	user, err := userServer.userService.FindUserById(id)

	if err != nil {
		return nil, status.Errorf(codes.Unimplemented, err.Error())
	}

	res := &pb.UserResponse{
		User: &pb.User{
			Id:        user.ID.Hex(),
			Name:      user.Name,
			Email:     user.Email,
			Role:      user.Role,
			CreatedAt: timestamppb.New(user.CreatedAt),
			UpdatedAt: timestamppb.New(user.UpdatedAt),
		},
	}
	return res, nil
}


Create the gRPC Servers

The gRPC API now has the AuthServiceServer and UserServiceServer that we must implement in separate files.

If you are coming from the previous tutorial Build Golang gRPC Server and Client: SignUp User & Verify Email then rename the gapi/server.go file to gapi/auth-server.go and paste the code below into it.

gapi/auth-server.go


package gapi

import (
	"github.com/wpcodevo/golang-mongodb/config"
	"github.com/wpcodevo/golang-mongodb/pb"
	"github.com/wpcodevo/golang-mongodb/services"
	"go.mongodb.org/mongo-driver/mongo"
)

type AuthServer struct {
	pb.UnimplementedAuthServiceServer
	config         config.Config
	authService    services.AuthService
	userService    services.UserService
	userCollection *mongo.Collection
}

func NewGrpcAuthServer(config config.Config, authService services.AuthService,
	userService services.UserService, userCollection *mongo.Collection) (*AuthServer, error) {

	authServer := &AuthServer{
		config:         config,
		authService:    authService,
		userService:    userService,
		userCollection: userCollection,
	}

	return authServer, nil
}


We have now implemented the AuthServiceServer interface so it is ready to accept gRPC requests from the client.

Next, create a user-server.go file in the gapi folder to implement the UserServiceServer interface.

gapi/user-server.go


package gapi

import (
	"github.com/wpcodevo/golang-mongodb/config"
	"github.com/wpcodevo/golang-mongodb/pb"
	"github.com/wpcodevo/golang-mongodb/services"
	"go.mongodb.org/mongo-driver/mongo"
)

type UserServer struct {
	pb.UnimplementedUserServiceServer
	config         config.Config
	userService    services.UserService
	userCollection *mongo.Collection
}

func NewGrpcUserServer(config config.Config, userService services.UserService, userCollection *mongo.Collection) (*UserServer, error) {
	userServer := &UserServer{
		config:         config,
		userService:    userService,
		userCollection: userCollection,
	}

	return userServer, nil
}


Since we have implemented the two server interfaces in our own server structs, it’s now time to register them.

Register the gRPC Servers

If you followed the Build Golang gRPC Server and Client: SignUp User & Verify Email, create a cmd folder in the root directory and then create two folders named client and server in it.

Next, move the main.go file in the root directory into the cmd/server folder.

Now update the startGrpcServer function with the code snippets below.

cmd/server/main.go


func startGrpcServer(config config.Config) {
	authServer, err := gapi.NewGrpcAuthServer(config, authService, userService, authCollection)
	if err != nil {
		log.Fatal("cannot create grpc authServer: ", err)
	}

	userServer, err := gapi.NewGrpcUserServer(config, userService, authCollection)
	if err != nil {
		log.Fatal("cannot create grpc userServer: ", err)
	}

	grpcServer := grpc.NewServer()

	pb.RegisterAuthServiceServer(grpcServer, authServer)
	pb.RegisterUserServiceServer(grpcServer, userServer)
	reflection.Register(grpcServer)

	listener, err := net.Listen("tcp", config.GrpcServerAddress)
	if err != nil {
		log.Fatal("cannot create grpc server: ", err)
	}

	log.Printf("start gRPC server on %s", listener.Addr().String())
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatal("cannot create grpc server: ", err)
	}
}

Create the gRPC Clients in Golang

If you followed the previous tutorial Build Golang gRPC Server and Client: SignUp User & Verify Email then move the client/main.go file in the root directory into the cmd/client folder.

Within the client folder, create a signin_client.go file and add the code snippets below.

client/signin_client.go


package client

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
)

type SignInUserClient struct {
	service pb.AuthServiceClient
}

func NewSignInUserClient(conn *grpc.ClientConn) *SignInUserClient {
	service := pb.NewAuthServiceClient(conn)

	return &SignInUserClient{service}
}

func (signInUserClient *SignInUserClient) SignInUser(credentials *pb.SignInUserInput) {

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	res, err := signInUserClient.service.SignInUser(ctx, credentials)

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

	fmt.Println(res)
}


The code in the SignInUser function is responsible for logging in the registered user to receive the access and refresh tokens.

Next, create a client/signup_client.go file and add the code below to register a new user.

client/signup_client.go


package client

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
)

type SignUpUserClient struct {
	service pb.AuthServiceClient
}

func NewSignUpUserClient(conn *grpc.ClientConn) *SignUpUserClient {
	service := pb.NewAuthServiceClient(conn)

	return &SignUpUserClient{service}
}

func (signUpUserClient *SignUpUserClient) SignUpUser(credentials *pb.SignUpUserInput) {

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*5000))
	defer cancel()

	res, err := signUpUserClient.service.SignUpUser(ctx, credentials)

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

	fmt.Println(res)
}


You need to increase the context timeout duration in the SignUpUser function since sending the verification email takes time.

Finally, create a client/geMe_client.go file and paste the code below into it.

client/geMe_client.go


package client

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
)

type GetMeClient struct {
	service pb.UserServiceClient
}

func NewGetMeClient(conn *grpc.ClientConn) *GetMeClient {
	service := pb.NewUserServiceClient(conn)

	return &GetMeClient{service}
}

func (getMeClient *GetMeClient) GetMeUser(credentials *pb.GetMeRequest) {

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*5000))
	defer cancel()

	res, err := getMeClient.service.GetMe(ctx, credentials)

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

	fmt.Println(res)
}


The GetMeUser method is responsible for retrieving the authenticated user’s information.

Connect the gRPC Client to the gRPC Server

Update the cmd/client/main.go file with the code snippets below.

cmd/client/main.go


package main

import (
	"log"

	"github.com/wpcodevo/golang-mongodb/client"
	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	address = "0.0.0.0:8080"
)

func main() {
	conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())

	if err != nil {
		log.Fatalf("failed to connect: %v", err)
	}

	defer conn.Close()

	// Sign Up
	if false {
		signUpUserClient := client.NewSignUpUserClient(conn)
		newUser := &pb.SignUpUserInput{
			Name:            "Jane Smith",
			Email:           "janesmith@gmail.com",
			Password:        "password123",
			PasswordConfirm: "password123",
		}
		signUpUserClient.SignUpUser(newUser)
	}

	// Sign In
	if true {
		signInUserClient := client.NewSignInUserClient(conn)

		credentials := &pb.SignInUserInput{
			Email:    "janesmith@gmail.com",
			Password: "password123",
		}
		signInUserClient.SignInUser(credentials)
	}

	// Get Me
	if false {

		getMeClient := client.NewGetMeClient(conn)
		id := &pb.GetMeRequest{
			Id: "628cffb91e50302d360c1a2c",
		}
		getMeClient.GetMeUser(id)

	}

}


At the moment I used if statements to conditional run the clients but later we’ll refactor them.

Conclusion

In this article, you learned how to implement JWT access and refresh tokens with gRPC using Golang, MongoDB, Gomail, and Docker-compose.

Check out the source code on GitHub.