DEV Community

Cover image for Wtf is Grpc? Part 1: Authentication and Authorization in Flutter and Golang.
Md. Mobin
Md. Mobin

Posted on • Edited on

23 2

Wtf is Grpc? Part 1: Authentication and Authorization in Flutter and Golang.

Greetings, fellow tech enthusiasts, and welcome to a tech journey like no other! 🚀 Today, we're embarking on a roller-coaster ride through the fascinating world of gRPC, Flutter, and Golang, but with a twist – we've spiced it up with a generous serving of humor. Buckle up; it's going to be a wild ride!

In our three-part series, we'll explore the ins and outs of gRPC, the delightful dance between Flutter and Golang, and discover how these technologies can come together to create harmonious applications that can bring a smile to even the most stoic developer's face.

Table of Contents

  1. Introduction

  2. Prerequisites

    • Create project
    • Setup Database model
    • Password Hashing
    • Environment
    • JWT
  3. Basic of Protocol Buffer

    • Service
    • Message
  4. Setup Basic Project in Golang

  5. Create Protocol Buffers

  6. Generate Golang code

  7. Authentication in Go

  8. Setup Server and Middleware

  9. Start Server

  10. Flutter Project

  11. Conclusion

A Quick Peek

  • Part 1: Authentication & Authorization
  • Part 2: Custom Flutter Notifications
  • Part 3: Chat Applications with gRPC

gRPC: A Not-So-Boring Bookish and Slightly Amusing Definition

Alright, time to don our academic spectacles and approach this with a hint of whimsy. Imagine you're at a fancy library soirée, and someone asks, "What, pray tell, is gRPC?" You respond with a grin:

"Gentlefolk, gRPC, which stands for 'Google Remote Procedure Call,' is like the telegraph system of the digital realm. It's a meticulously structured, high-tech method for different programs to chit-chat with each other, just like sending telegrams back in the day, but without the Morse code."

"But wait, there's more! Instead of tapping out dots and dashes, gRPC uses Protocol Buffers, which is like encoding your messages in secret spy code, only it's not so secret. This makes your data compact and zippy, perfect for today's speedy world."

"Picture it as a virtual switchboard operator connecting your apps with finesse. Plus, it plays nice with HTTP/2, which is like giving your data a sleek, sports car to zoom around in, making it super fast."

"So, in essence, gRPC is the gentleman's agreement between software, ensuring they can communicate efficiently and reliably, like two well-mannered tea-drinking robots having a chat. It's the technological equivalent of a courteous bow, allowing different systems to exchange information harmoniously."

gRPC vs REST: The Iron Throne of APIs

Aspect gRPC REST
Protocol Uses HTTP/2 for communication. Typically uses HTTP/1.1, but can use HTTP/2.
Data Serialization Uses Protocol Buffers (ProtoBuf) for efficient binary serialization. Uses human-readable formats like JSON or XML.
Communication Patterns Supports unary, server streaming, client streaming, and bidirectional streaming. Primarily supports request-response (HTTP GET, POST, etc.).
Payload Size Smaller payload due to binary serialization, making it more efficient. Larger payload due to text-based serialization.
Performance High performance and low latency, suitable for microservices. Slightly slower due to text-based serialization.
Language Agnostic Supports multiple programming languages, making it polyglot. Can be used with any language, but not as standardized.
Code Generation Generates client and server code automatically from Protobuf definitions. No automatic code generation for client or server.
Error Handling Uses status codes and detailed error messages for robust error handling. Typically relies on HTTP status codes and custom error messages.
Tooling Well-documented and supported by various libraries and tools. Widely supported with many tools and libraries available.

meme1

Types of gRPC

Here are the primary types of gRPC communication:

  • Unary RPC (Request-Response): This is the simplest form of gRPC communication. The client sends a single request to the server and waits for a single response. It's similar to traditional request-response interactions.

  • Server Streaming RPC: In this pattern, the client sends a single request, but the server responds with a stream of messages. It's useful when the server needs to push multiple pieces of data to the client, such as real-time updates.

  • Client Streaming RPC: Here, the client sends a stream of messages to the server, and the server responds with a single message. This can be beneficial for scenarios like uploading large files.

  • Bidirectional Streaming RPC: In this type, both the client and server can send a stream of messages to each other concurrently. It's ideal for interactive and real-time applications like chat or gaming.

meme2

In this we are going to explore unary gRPC using login,signup and get-user request.

Prerequisites

  • Basic understanding of Golang,Flutter and MongoDb.
  • Excitement to learn something new.

Setup Basic Project in Golang

  • Create new project ```

go mod init

- Install Mongo package for go

Enter fullscreen mode Exit fullscreen mode

go get go.mongodb.org/mongo-driver

- Setup Database Model
    - Create package `db` 
      - Create Struct to access database
        
package db
import (
"go.mongodb.org/mongo-driver/mongo"
)
type MongoCollections struct {
Users *mongo.Collection
}
view raw database.go hosted with ❤ by GitHub
  - Create new model `user.go`
Enter fullscreen mode Exit fullscreen mode
package model
import "go.mongodb.org/mongo-driver/bson/primitive"
type UserModel struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Username string `bson:"username"`
Password string `bson:"password"`
Name string `bson:"name"`
}
view raw user.go hosted with ❤ by GitHub
  • Password hashing : we are going to create function for hashing password and check password, Never store password in simple text.

    • Create package utils in root.
    • Install required package go get golang.org/x/crypto/bcrypt.
    • Create HashPasword and CheckPassword function in password.go
package utils
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
// convert password to hash string
func HashPassword(password string) (string, error) {
hashPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password %w", err)
}
return string(hashPassword), nil
}
// check password is valid or not
func CheckPassword(password string, hashPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashPassword), []byte(password))
}
view raw password.go hosted with ❤ by GitHub
  • Setup Environment: We required few environment variable like server_address,mongodburl and jwt_key etc.

    • Install Viper package for loading app.env.

      
      go get github.com/spf13/viper
      
      Enter fullscreen mode Exit fullscreen mode
Enter fullscreen mode Exit fullscreen mode

for more information read about viper

  • Create function in utils to load app.env file.
package utils
import (
"time"
"github.com/spf13/viper"
)
type ViperConfig struct {
DBNAME string `mapstructure:"DB_NAME"`
DBSource string `mapstructure:"DB_SOURCE"`
RPCSERVERADDRESS string `mapstructure:"RPC_SERVER_ADDRESS"`
TokkenStructureKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"`
}
func LoadConfiguration(path string) (config ViperConfig, err error) {
viper.AddConfigPath(path)
viper.SetConfigName("app")
viper.SetConfigType("env")
viper.AutomaticEnv()
err = viper.ReadInConfig()
if err != nil {
return
}
err = viper.Unmarshal(&config)
return
}
view raw viper.config.go hosted with ❤ by GitHub
  • Create app.env in root folder.


DB_NAME=grpc 
DB_SOURCE=mongodb://localhost:27017
RPC_SERVER_ADDRESS=0.0.0.0:9090
GIN_MODE=debug
TOKEN_SYMMETRIC_KEY=12345678123456781234567812345678
ACCESS_TOKEN_DURATION=600m



Enter fullscreen mode Exit fullscreen mode
  • Setup Jwt: We need authorization in few requests so its going to bearer token based authentication.
    • Install required Jwt package


     go get github.com/dgrijalva/jwt-go


Enter fullscreen mode Exit fullscreen mode

Note: Not Going to cover Jwt Basics.

  • Create new package token and create interface to create token and verify token.
    package token
    import (
    "errors"
    "time"
    )
    var (
    ErrExpiredToken = errors.New("token is expired")
    ErrInvalidToken = errors.New("")
    )
    type Payload struct {
    ID int64 `json:"id"`
    Username string `json:"username"`
    IssuedAt time.Time `json:"issued_at"`
    ExpiredAt time.Time `json:"expired_at"`
    }
    func NewPayLoad(id int64, username string, duration time.Duration) (*Payload, error) {
    payload := &Payload{
    Username: username,
    ID: id,
    IssuedAt: time.Now(),
    ExpiredAt: time.Now().Add(duration),
    }
    return payload, nil
    }
    func (payload *Payload) Valid() error {
    if time.Now().After(payload.ExpiredAt) {
    return ErrExpiredToken
    }
    return nil
    }
    view raw payload.go hosted with ❤ by GitHub
package token
import "time"
type Maker interface {
CreateToken(id int64, username string, duration time.Duration) (string, error)
VerifyToken(token string) (*Payload, error)
}
view raw token_maker.go hosted with ❤ by GitHub
package token
import (
"errors"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
const minSecertKeySize = 32
type JwtMaker struct {
secret string
}
func NewJwtMaker(secret string) (*JwtMaker, error) {
if len(secret) < minSecertKeySize {
return nil, fmt.Errorf("invalid secret key size: %d", len(secret))
}
return &JwtMaker{
secret: secret,
}, nil
}
func (maker *JwtMaker) CreateToken(id int64, username string, duration time.Duration) (string, error) {
payload, err := NewPayLoad(id, username, duration)
if err != nil {
return "", err
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
return jwtToken.SignedString([]byte(maker.secret))
}
func (maker *JwtMaker) VerifyToken(token string) (*Payload, error) {
keyFunc := func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, ErrInvalidToken
}
return []byte(maker.secret), nil
}
jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
if err != nil {
verr, ok := err.(*jwt.ValidationError)
if ok && errors.Is(verr.Inner, ErrExpiredToken) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
payload, ok := jwtToken.Claims.(*Payload)
if !ok {
return nil, ErrInvalidToken
}
return payload, nil
}
view raw jwt_maker.go hosted with ❤ by GitHub

Basic of Protocol Buffer (.proto).

  • Service: A unary service in gRPC involves a single request from the client to the server, which then sends a single response back to the client. To define a unary service, you need to create a .proto file that describes the service and the message types it uses.

  • Message: A message is a data structure that represents the information being sent between the client and server. It's defined in your .proto file using the message keyword. Here's a simple example:

    
    

syntax = "proto3";

message MyRequest {
string name = 1;
int32 age = 2;
}

message MyResponse {
string greeting = 1;
}

In this example, we have two messages, `MyRequest`and `MyResponse`. `MyRequest` has two fields, name and age, while `MyResponse` has a single field, greeting.

- Importing Data Structures from Another Package:

Enter fullscreen mode Exit fullscreen mode

import "other_package.proto";

message MyRequest {
other_package.SomeMessage some_data = 1;
}


## Create Protocol buffers for our project:

- Services: We will have following services called `login`, `signup` and `get-user`. 

- Create `user.proto`, User object to return to the client.
Enter fullscreen mode Exit fullscreen mode

syntax = "proto3";

package pb;
option go_package="github.com/djsmk123/server/pb";

message User{
int32 id=1;
string username=2;
string name=3;
}


- Create `rpc_login.proto`, it will have LoginRequestMessage and LoginReponseMessage

Enter fullscreen mode Exit fullscreen mode

syntax="proto3";
package pb;
import "user.proto";
option go_package="github.com/djsmk123/server/pb";

message LoginRequestMessage{
string username=1;
string password=2;
}

message LoginResponseMessage{
User user=1;
string access_token=2;
}


- Same for `rpc_signup.proto`:

Enter fullscreen mode Exit fullscreen mode

syntax="proto3";
package pb;
import "user.proto";
option go_package="github.com/djsmk123/server/pb";

message SignupRequestMessage{
string username=1;
string password=2;
string name=3;
}

message SignupResponseMessage{
User user=1;

}

- To create message `rpc_get_user.proto`: 
Enter fullscreen mode Exit fullscreen mode

syntax="proto3";
package pb;
import "user.proto";
option go_package="github.com/djsmk123/server/pb";

message GetUserResponse{
User user=1;
}


- Create service `rpc_services.proto`
Enter fullscreen mode Exit fullscreen mode

syntax="proto3";
package pb;
option go_package="github.com/djsmk123/server/pb";
import "empty_request.proto";
import "rpc_get_user.proto";
import "rpc_login.proto";
import "rpc_signup.proto";

service GrpcServerService {
rpc SignUp(SignupRequestMessage) returns (SignupResponseMessage){};

rpc login(LoginRequestMessage) returns (LoginResponseMessage){};
rpc GetUser(EmptyRequest) returns (GetUserResponse) {};
Enter fullscreen mode Exit fullscreen mode

}
message EmptyRequest{

}


## Generate code for Golang

- Create `pb` package in root folder.
- Run command to generate equivalent code for golang 
Enter fullscreen mode Exit fullscreen mode

protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
--go-grpc_out=pb --go-grpc_opt=paths=source_relative \
proto/*.proto


![image 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xrjl4bhl3vh8woq1pzsv.png)

## Create Basic Auth function
- Create `Auth` Package: This package has basic function like `get-user`,`login`, `signup`,`tokenCreation` and function to convert MongoDB userModel to gRPC `user` Messsage.

- Create `converter.go` in `auth` package.
package auth
import (
"github.com/djsmk123/server/db/model"
pb "github.com/djsmk123/server/pb"
)
func ConvertUserObjectToUser(model *model.UserModel) *pb.User {
return &pb.User{
Username: model.Username,
Name: model.Name,
Id: int32(model.ID.Timestamp().Day()),
}
}
view raw converter.go hosted with ❤ by GitHub
  • Create token_generate.go to generate token.

    package auth
    import (
    "time"
    "github.com/djsmk123/server/token"
    )
    func CreateToken(tokenMaker token.Maker, username string, userId int64, duration time.Duration) (string, error) {
    accesstoken, err := tokenMaker.CreateToken(userId, username, duration)
    if err != nil {
    return "", err
    }
    return accesstoken, nil
    }
  • Create login.go

    package auth
    import (
    "context"
    "errors"
    "github.com/djsmk123/server/db/model"
    "github.com/djsmk123/server/utils"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    )
    var ErrUserNotFound = errors.New("user not found")
    var ErrInvalidCredentials = errors.New("credentials are not valid")
    func LoginUser(username string, password string, collection *mongo.Collection, context context.Context) (*model.UserModel, error) {
    filter := bson.M{"username": username}
    var user model.UserModel
    err := collection.FindOne(context, filter).Decode(&user)
    if err == mongo.ErrNoDocuments {
    return nil, ErrUserNotFound
    } else if err != nil {
    return nil, err
    }
    err = utils.CheckPassword(password, user.Password)
    if err != nil {
    return nil, ErrInvalidCredentials
    }
    return &user, nil
    }
    view raw login.go hosted with ❤ by GitHub
  • Create signup.go

package auth
import (
"context"
"errors"
"fmt"
"github.com/djsmk123/server/db/model"
"github.com/djsmk123/server/utils"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
var ErrUserAlreadyRegistered = errors.New("user already registered")
func RegisterUser(username string, password string, name string, collection *mongo.Collection, context context.Context) (*model.UserModel, error) {
filter := bson.M{"username": username}
var existingUser model.UserModel
err := collection.FindOne(context, filter).Decode(&existingUser)
if err == mongo.ErrNoDocuments {
passwordhash, err := utils.HashPassword(password)
if err != nil {
return nil, fmt.Errorf("unable to parse password hash: %v", err)
}
newUser := model.UserModel{
Username: username,
Password: passwordhash,
Name: name,
}
result, err := collection.InsertOne(context, newUser)
if err != nil {
return nil, fmt.Errorf("unable to create user: %v", err)
}
newUser.ID = result.InsertedID.(primitive.ObjectID)
return &newUser, nil
} else if err != nil {
return nil, fmt.Errorf("unable to create user:" + err.Error())
}
return nil, ErrUserAlreadyRegistered
}
view raw signup.go hosted with ❤ by GitHub
  • Create get-user.go
    package auth
    import (
    "context"
    "github.com/djsmk123/server/db/model"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    )
    func GetUser(collection *mongo.Collection, context context.Context, username string) (*model.UserModel, error) {
    filter := bson.M{"username": username}
    var user model.UserModel
    err := collection.FindOne(context, filter).Decode(&user)
    if err == mongo.ErrNoDocuments {
    return nil, ErrUserNotFound
    } else if err != nil {
    return nil, err
    }
    return &user, nil
    }
    view raw get-user.go hosted with ❤ by GitHub

Setup Server and Middlware

  • Create server.go in gapi package struct to register GrpcServiceServer.
package gapi
import (
"fmt"
"github.com/djsmk123/server/db"
"github.com/djsmk123/server/pb"
"github.com/djsmk123/server/token"
"github.com/djsmk123/server/utils"
)
type Server struct {
pb.GrpcServerServiceServer
config utils.ViperConfig
dbCollection db.MongoCollections
tokenMaker token.Maker
}
func NewServer(config utils.ViperConfig, dbCollection db.MongoCollections) (*Server, error) {
tokenMaker, err := token.NewJwtMaker(config.TokkenStructureKey)
if err != nil {
return nil, fmt.Errorf("cannot create token maker: %w", err)
}
server := &Server{
config: config,
dbCollection: dbCollection,
tokenMaker: tokenMaker,
}
return server, nil
}
view raw server.go hosted with ❤ by GitHub
  • For creating middlware for Authentication, we will need Unary Interceptors but before that we will check,if request service required authentication or not, we will list all method that are not required token. > Note by default all the method have restriction for auth token.

Create services.go

package gapi
import (
"fmt"
"strings"
)
func methodRequiresAuthentication(fullMethod string) bool {
m := extractMethodName(fullMethod)
m = strings.ToLower(m)
fmt.Println(m)
// Define a list of methods that require authentication.
NonAuthRequiredMethods := []string{
"login",
"signup",
}
// Check if the requested method is in the list.
for _, method := range NonAuthRequiredMethods {
if m == method {
return false
}
}
return true
}
func extractMethodName(fullMethod string) string {
// Define the prefix to remove
prefix := "/pb.GrpcServerService/"
// Use TrimPrefix to remove the prefix from the full method name
method := strings.TrimPrefix(fullMethod, prefix)
return method
}
view raw services.go hosted with ❤ by GitHub
  • Create middlware.go
package gapi
import (
"context"
"strings"
"github.com/djsmk123/server/token"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
const (
authorizationHeader = "authorization"
authorizationBearer = "bearer"
payloadHeader = "payload"
)
func (server *Server) AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Check if the service name is in a list of services that require authentication.
// Replace "Service1" and "Service2" with the actual service names you want to authenticate.
//requiredServices := []string{"pb.GrpcServerService"}
//serviceName := info.FullMethod
if methodRequiresAuthentication(info.FullMethod) {
// Extract the metadata from the context.
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "metadata not found")
}
// Get the authorization token from metadata.
authTokens := md[authorizationHeader]
if len(authTokens) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "authorization token is missing")
}
authHeader := authTokens[0] // Assuming a single token is sent in the header.
fields := strings.Fields(authHeader)
if len(fields) < 2 {
return nil, status.Errorf(codes.Unauthenticated, "invalid auth header format: %v", fields)
}
authType := strings.ToLower(fields[0])
if authType != authorizationBearer {
return nil, status.Errorf(codes.Unauthenticated, "invalid authorization type: %v", authType)
}
accessToken := fields[1]
payload, err := server.tokenMaker.VerifyToken(accessToken)
if err != nil {
if err == token.ErrInvalidToken {
return nil, status.Errorf(codes.Unauthenticated, "invalid token %v", authType)
}
if err == token.ErrExpiredToken {
return nil, status.Errorf(codes.Unauthenticated, "token %v expired", authType)
}
}
ctx = context.WithValue(ctx, payloadHeader, payload)
return handler(ctx, req)
}
return handler(ctx, req)
}
view raw middlware.go hosted with ❤ by GitHub

Connect whole and start server

finally we are going to main function in main.go,lets load environment, connect database(MongoDb) and RunServer.

package main
import (
"context"
"fmt"
"log"
"net"
"github.com/djsmk123/server/db"
"github.com/djsmk123/server/gapi"
"github.com/djsmk123/server/pb"
"github.com/djsmk123/server/utils"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
func main() {
config, err := utils.LoadConfiguration(".")
if err != nil {
log.Fatal("Failed to load configuration", err)
}
database, err := ConnectDatabase(config)
if err != nil {
log.Fatal(err)
}
conn := db.MongoCollections{
Users: database.Collection("users"),
}
// Start the gRPC server
runGrpcServer(config, conn)
}
// Connect to database
func ConnectDatabase(config utils.ViperConfig) (*mongo.Database, error) {
clientOptions := options.Client().ApplyURI(config.DBSource)
ctx := context.TODO()
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
return nil, fmt.Errorf("error connecting to MongoDB: %v", err)
}
database := client.Database(config.DBNAME)
return database, nil
}
// start runGrpcService
func runGrpcServer(config utils.ViperConfig, collection db.MongoCollections) {
server, err := gapi.NewServer(config, collection)
if err != nil {
log.Fatal("Error creating server", err)
}
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(server.AuthInterceptor),
)
pb.RegisterGrpcServerServiceServer(grpcServer, server)
reflection.Register(grpcServer)
listener, err := net.Listen("tcp", config.RPCSERVERADDRESS)
if err != nil {
log.Fatal("Error creating server", err)
}
log.Printf("gRPC server listening on %s", config.RPCSERVERADDRESS)
err = grpcServer.Serve(listener)
if err != nil {
log.Fatal("Error serving gRPC server", err)
}
}
view raw main.go hosted with ❤ by GitHub

Run Server

Enter fullscreen mode Exit fullscreen mode

go run .


## Test server

You can test gRPC services using [`evans-cli`](https://github.com/ktr0731/evans).
- Start CLI Tool

Enter fullscreen mode Exit fullscreen mode

evans --host localhost --port 9090 -r repl


- Call `login` service

Enter fullscreen mode Exit fullscreen mode

call login




![output1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vkdhip9jx584ne8z3h8p.png)

!!!Oops,we forgot to define `login`,`get-user` and `signup` from `pb` interface.


- Create `auth.go` in `gapi` package.

package gapi
import (
"context"
"github.com/djsmk123/server/auth"
pb "github.com/djsmk123/server/pb"
"github.com/djsmk123/server/token"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (server *Server) Login(ctx context.Context, req *pb.LoginRequestMessage) (*pb.LoginResponseMessage, error) {
user, err := auth.LoginUser(req.GetUsername(), req.GetPassword(), server.dbCollection.Users, context.TODO())
if err != nil {
if err == auth.ErrUserNotFound {
return nil, status.Errorf(codes.NotFound, err.Error())
}
if err == auth.ErrInvalidCredentials {
return nil, status.Errorf(codes.Unauthenticated, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
}
//
resp := auth.ConvertUserObjectToUser(user)
token, err := auth.CreateToken(server.tokenMaker, resp.Username, int64(resp.Id), server.config.AccessTokenDuration)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
return &pb.LoginResponseMessage{
User: resp,
AccessToken: token,
}, nil
}
func (server *Server) SignUp(ctx context.Context, req *pb.SignupRequestMessage) (*pb.SignupResponseMessage, error) {
user, err := auth.RegisterUser(req.GetUsername(), req.GetPassword(), req.GetName(), server.dbCollection.Users, context.TODO())
if err != nil {
if err == auth.ErrUserAlreadyRegistered {
return nil, status.Errorf(codes.AlreadyExists, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
}
resp := auth.ConvertUserObjectToUser(user)
return &pb.SignupResponseMessage{
User: resp,
}, nil
}
func (server *Server) GetUser(ctx context.Context, req *pb.EmptyRequest) (*pb.GetUserResponse, error) {
payload, ok := ctx.Value(payloadHeader).(*token.Payload)
if !ok {
return nil, status.Errorf(codes.Internal, "missing required token")
}
user, err := auth.GetUser(server.dbCollection.Users, context.TODO(), payload.Username)
if err != nil {
if err == auth.ErrUserNotFound {
return nil, status.Errorf(codes.NotFound, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
}
return &pb.GetUserResponse{
User: auth.ConvertUserObjectToUser(user),
}, nil
}
view raw auth.go hosted with ❤ by GitHub

Try again

output2


Flutter Project

  • This Flutter application features four key pages: SplashScreen, LoginScreen, SignUpScreen, and HomeScreen.

  • SplashScreen: This initial page verifies the user's authentication status. If authenticated, it communicates with the gRPC server using the get-user service and redirects the user to the HomeScreen. Otherwise, it navigates to the LoginScreen.

  • LoginScreen: The LoginScreen presents fields for entering a username and password. Upon pressing the submit button, the application invokes the login service to authenticate the user. The access_token is saved to local storage, and the user is redirected to the HomeScreen.

  • SignUpScreen: The SignUpScreen offers fields for name, username, password, and confirmpassword. When the user submits the form, the application calls the signup service on the gRPC server.

> Note: Not going to create whole UI and remaining Login,but only will cover required parts. You are free to create your own ui or can refer from whole in github repo.

Setup Project

  • Add following dependencies into pubspec.yaml
Enter fullscreen mode Exit fullscreen mode

shared_preferences: ^2.2.1
protobuf: ^3.1.0
grpc: ^3.2.3

- Install `protoc_plugin` for dart.
Enter fullscreen mode Exit fullscreen mode

dart pub global activate protoc_plugin


- Copy `.proto` files from server to root folder of the flutter project.

- Created equivalent code in dart

Enter fullscreen mode Exit fullscreen mode

protoc --proto_path=proto --dart_out=grpc:lib/pb proto/*.proto

- Create channel for gRPC services `services/grpc_services.dart`
class GrpcService {
static String host = "10.0.2.2"; //default for android emulator
static updateChannel() {
channel = ClientChannel(host,
port: 9090,
options:
const ChannelOptions(credentials: ChannelCredentials.insecure()));
}
static var channel = ClientChannel(host,
port: 9090,
options: const ChannelOptions(
credentials: ChannelCredentials.insecure(),
));
static var client = GrpcServerServiceClient(channel);
}
  • Create AuthServices for different method like login,signup and get-user
import 'dart:developer';
import 'package:flutter_app/models/user.dart';
import 'package:flutter_app/pb/empty_request.pb.dart';
import 'package:flutter_app/pb/rpc_login.pb.dart';
import 'package:flutter_app/pb/rpc_signup.pb.dart';
import 'package:flutter_app/services/grpc_services.dart';
import 'package:grpc/service_api.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthService {
static String? authToken;
static UserModel? user;
static Future<SharedPreferences> getSharedPreferences() async {
return await SharedPreferences.getInstance();
}
static Future<bool> isAuthAvailable() async {
final sharedPreferences = await getSharedPreferences();
authToken = sharedPreferences.getString('token');
return authToken != null;
}
static Future<bool?> updateToken(String token) async {
final sharedPreferences = await getSharedPreferences();
authToken = token;
return sharedPreferences.setString('token', token);
}
static Future<bool?> logout() async {
final sharedPreferences = await getSharedPreferences();
authToken = null;
user = null;
return sharedPreferences.remove('token');
}
static Future<UserModel?> login(String username, String password) async {
try {
final request =
LoginRequestMessage(username: username, password: password);
final response = await GrpcService.client.login(request);
await updateToken(response.accessToken);
user = UserModel(
response.user.id, response.user.username, response.user.name);
return user;
} catch (e) {
log(e.toString());
rethrow;
}
}
static Future<UserModel?> signup(
String username, String password, String name) async {
try {
final request = SignupRequestMessage(
username: username, password: password, name: name);
final response = await GrpcService.client.signUp(request);
return UserModel(
response.user.id, response.user.username, response.user.name);
} catch (e) {
log(e.toString());
rethrow;
}
}
static Future<UserModel?> getUser() async {
try {
final response = await GrpcService.client.getUser(
EmptyRequest(),
options: CallOptions(metadata: {'authorization': 'bearer $authToken'}),
);
user = UserModel(
response.user.id, response.user.username, response.user.name);
return user;
} catch (e) {
log(e.toString());
rethrow;
}
}
}
  • We are done just call function in presentation layer and add your own logic.

OUTPUT

1

2

3

4

5

6

Thanks for joining me on this tech adventure! We hope you've enjoyed the journey through gRPC, Flutter, and Golang as much as I have. But wait, the fun doesn't stop here!

Stay tuned for my upcoming blogs, where I'll sprinkle more humor, insights, and tech wizardry into your day. Who knows, in my next episode, we might just unveil the secret to debugging code with a magic wand (disclaimer: we can't actually do that, but a coder can dream, right?).

Until then, keep coding, keep laughing, and keep those fingers ready on the refresh button because the next tech tale is just around the corner! 🚀✨"

Source code

Github Repo

Follow me on

Enter fullscreen mode Exit fullscreen mode

Sentry mobile image

Tired of users complaining about slow app loading and janky UI?

Improve performance with key strategies like TTID/TTFD & app start analysis.

Read the blog post

Top comments (6)

Collapse
 
maximsaplin profile image
Maxim Saplin

One interesting topic is authentication with Web Client. Assume you added gRPC Web Proxy in front of your server to serve Web clients over HTTP 1.1. You still need the JWT token for authentication and client need to store is somewhere. The most apparent place is local storage. Yet this values can be read without any limmits and hence the token can be stolen. In typical Web apps this kind of issues is solved by server-side cookies (cookies which the client acepts and resends to server with every request yet can't read via JS). Anh there no server sode cookies in gRPC :)

If you happen to consider this use case - I am very much interested. I beleive when using Web handling of JWT and translating it to server side auth cookie must be done by the web proxy.

Collapse
 
djsmk123 profile image
Md. Mobin

Thanks for feedback.
I consider server side cookies using gRPC gateway to that will verify but It will add complexity to the blog.
I could cover this topic in anotherb blog but before that I have to test and verify the solution.

Collapse
 
maximsaplin profile image
Maxim Saplin

Indeed, that is not a small/easy topic.. And it tends more towards infrastructure rather than programming.

Collapse
 
epi2024 profile image
Je Phiri

Great tutorial. I love it 😍

Collapse
 
djsmk123 profile image
Md. Mobin

Thanks

Collapse
 
djsmk123 profile image
Md. Mobin • Edited

In case of you found typos,mistake in code and any other feedback.
Please comment and let me know

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay