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:
- API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
- Golang & MongoDB: JWT Authentication and Authorization
- API with Golang + MongoDB: Send HTML Emails with Gomail
- API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
- Build Golang gRPC Server and Client: SignUp User & Verify Email
- Build Golang gRPC Server and Client: Access & Refresh Tokens
- Build CRUD RESTful API Server with Golang, Gin, and MongoDB
- Build CRUD gRPC Server API & Client with Golang and MongoDB
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
- Golang – to build the gRPC server and client
- Protocol Buffer Compiler – To compile the
.proto
files
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.