Go gRPC authentication service + cross-platform client
Demo application https://auth-demo.dmitrii.app
Demo application https://auth-demo.dmitrii.app
Go is exceptionally well-suited for crafting contemporary, dynamic, and highly scalable services. One of the most prevalent tasks in this context is authentication and user management, which aligns seamlessly with Go’s capabilities when implemented as a gRPC service. The gRPC protocol offers a structured and efficient communication method, particularly well-suited for message-based interactions and tasks that eschew large file transfers.
This article serves as a starting point to discuss best practices in implementing Go services. Welcome to share your ideas, and feel free to create pull requests with improvements. You can find the service source code shared on GitHub: https://github.com/zs-dima/auth-service
Project structure based on the standard Go project layout. The entry point to start the service is ‘cmd/main.go’. gRPC protocols are in the proto folder. Sql scripts are in the db folder.
Authentication relies on a combination of refresh tokens and access JWT tokens for securely accessing services. Clients leverage a refresh token to acquire and refresh an access token, a short-lived token used to request services securely. To enhance security and efficiently manage user sessions, the backend stores refresh tokens in the user_session database table, facilitating access revocation when necessary. This simple design ensures a reasonably solid and flexible authentication system, enhancing overall security and user experience.
‘sqlc’ is almost the only type-safe SQL compiler for Golang today. It is much simpler than EF in .Net or Prisma and Drift ORM in Dart. On the other hand, type safety is the only way to build a reliable backend, so the choice of ‘sqlc’ is obvious.
The current implementation has been significantly streamlined, omitting the following features:
- Role hierarchy and role permissions.
- Audit logging for secure user action tracking and change monitoring.
- Password reset emails, email verification, and guest access.
- Two-factor authentication (2FA) like application-based, SMS, etc.
- Social authentication, including options for logging in with external accounts like Google.
- Event bus to notify clients of changes and events, likely Centrifugo
The service relies on the following environment variables:
GRPC_ADDRESS={HOST:PORT}
HTTP_ADDRESS={HOST:PORT}
JWT_SECRET_KEY={JWT_KEY}
DB_URI=postgresql://{DB_USER}:@{DB_HOST}:{DB_PORT}/{DB_NAME}
DB_PASSWORD={DB_PASSWORD}Please keep in mind to replace {HOST:PORT}, {JWT_KEY}, {DB_…} with your environment variables. You can also use masks like {HOST:PORT} as well: ‘[::123]:123’ for example. {DB_PASSWORD} separated to be able to store a password as a secret, then you will have to bind a secret to the DB_PASSWORD_FILE environment variable. The same goes for the JWT key — you can add _FILE to the variable name to read it from secret.
The ‘\configs\dev.env’ file facilitates storing environment variables for development purposes. If you are using Visual Studio Code, you can take advantage of the ‘launch.json’ ‘envFile’ setting. This setting allows you to create multiple launch configurations where ‘envFile’ points to dev.env, staging.env, etc. This approach streamlines the handling of environment variables, ensuring an efficient development experience:
* * *
"configurations": [
{
"name": "Service",
"program": "${workspaceFolder}/cmd/main.go",
"envFile": "${workspaceFolder}/configs/dev.env",
* * *
{
"name": "Service (staging)",
"program": "${workspaceFolder}/cmd/main.go",
"envFile": "${workspaceFolder}/configs/staging.env"
* * *Service use interceptors to validate tokens:
grpc.ChainStreamInterceptor(
grpc_logging.StreamServerInterceptor(logger.InterceptorLogger(*log)),
jwt_interceptor.StreamServerInterceptor(jwtOptions),
grpc_recovery.StreamServerInterceptor(),
grpc_prometheus.StreamServerInterceptor,
),
grpc.ChainUnaryInterceptor(
grpc_logging.UnaryServerInterceptor(logger.InterceptorLogger(*log)),
jwt_interceptor.UnaryServerInterceptor(jwtOptions),
grpc_recovery.UnaryServerInterceptor(),
grpc_prometheus.UnaryServerInterceptor,
),The ‘\internal\api\interceptor\jwt_interceptor.go’ interceptor validates tokens, extracts claims, and stores them as an object in the context for future service use:
authInfo, err := extractAuthInfo(claims)
if err != nil {
return ctx, status.Errorf(codes.Unauthenticated, "unauthenticated")
}
return context.WithValue(ctx, tool.UserClaimsKey, authInfo), nilfunc (s *AuthServiceServer) RefreshToken(ctx context.Context, request *pb.RefreshTokenRequest) (*pb.RefreshTokenReply, error) {
authInfo := jwt.ExtractAuthInfo(ctx)
userEmail := authInfo.UserInfo.Email
userId := authInfo.UserInfo.IdGo language has limitations and makes code less readable. There are no extension methods, method overloading with different parameters, and more. It has no mature ORM and useful dependency injection helpers, as in .Net Core.
Although the Go programming language is renowned for its simplicity and efficiency, it also has certain limitations that impact code readability. It has no extension methods, method overloading with different parameters, and other valuable basics usual for modern languages. Additionally, Go lacks a mature ORM and convenient dependency injection, similar to those in .NET Core.
On the other hand, Go services excel in effectiveness, boasting low memory consumption, making them exceptionally scalable. Furthermore, the Go community has contributed a rich ecosystem of libraries to handle common cases: awesome-go.
Client application
https://github.com/zs-dima/auth-app
Simple client application to demonstrate Go service possibilities. Flutter is a modern framework with multi-platform capabilities that easily create beautiful mobile, desktop, and web applications. So Flutter is the perfect choice for the multi-platform client for our case.
While this client application could be the focus of a separate article, let’s delve into some of its key highlights.
The application delivers essential functionality, encompassing user login user management. The application offers users the flexibility to personalize their experience by adjusting its theme and language settings to enhance usability. Application initialization inspired by PlugFox/spinify and hawkkiller/sizzle_starter. Login screen copied from the PlugFox/spinify project.
Flutter lacks an adaptive theme solution. To address this gap, an adaptive theme was implemented using theme extensions: ‘/lib/app/theme/extension/theme_sizes.dart’
Refresh and access tokens are stored in a secure storage apart from other preferences.
BLoC and freezed libraries are imperfect, although let’s keep them for now and wait for a better alternative approach, metaprogramming, and data classes.
DI is based on InheritedWidget’s as they are pretty simple to use out of the box — no need to add extra complexity by wrapping them with unneeded libraries.
Depending on the project, you can implement local storage based on SQLite using Drift ORM, offline mode, and other enhancements.
The previous article explains an approach to accessing environment variables, including Web applications:

You are warmly invited to share suggestions, engage in discussions, and contribute to enhancing service. Your input and insights are highly valued and can help to improve code quality and share best practices with the community.
🚀 Try It
auth-app is open-source and production-ready. Go build something amazing.
- ⚡ Live Demo – See the auth flow in action
- ⭐ Star on GitHub Service & Client – Support the project
- 🐳 Get Docker Image – Deploy in seconds
- 📘 Read Documentation – Integration guide & API reference
📚 Further Reading
- Go Documentation — Start here for Go fundamentals
- Flutter Documentation — Flutter deep dive
🤝 Let's Connect
I'm a software engineer building high-performance systems. If you have questions or want to clarify details, feel free to reach out.
Blog · GitHub · LinkedIn · X / Twitter · Email
with ❤️ Dmitrii Zusmanovich