122 Case Study: JWT Registration & Login System with gqlgen
122 Case Study: JWT Registration & Login System with gqlgen
Building a secure and efficient authentication system is one of the foundations of almost every modern backend application. In the era of API-first development and microservices, JSON Web Token (JWT) has become one of the most popular authentication solutions. In this case study, I’ll walk through building a registration & login system using JWT with GraphQL in Go, leveraging the powerful gqlgen
library.
We’ll cover the architecture, code examples, and a flow diagram of the request flow, complete with a login simulation and endpoint protection.
System Overview
The system we’re going to build includes the following features:
- User Registration
- User Login
- Endpoint Protection with JWT
The main components used:
- Go as the backend language
- gqlgen as the GraphQL code generator
- gorilla/mux (optional) as the HTTP router
- JWT (github.com/golang-jwt/jwt/v5) for token generation and validation
- bcrypt for password hashing
Database and User Structure
For simplicity, we’ll use in-memory storage in the form of a map (map[string]*User). In the real world, you can easily swap this out for Postgres or MongoDB.
1type User struct {
2 ID string
3 Username string
4 Password string // Hashed!
5}In simple terms, the storage structure looks like this:
| Field | Type | Description |
|---|---|---|
| ID | string | UUID |
| Username | string | Unique, used for login |
| Password | string | Already hashed with bcrypt |
GraphQL Schema
For registration and login, two simple mutations are enough:
1type User {
2 id: ID!
3 username: String!
4}
5
6type AuthPayload {
7 token: String!
8 user: User!
9}
10
11type Mutation {
12 register(username: String!, password: String!): AuthPayload!
13 login(username: String!, password: String!): AuthPayload!
14}
15
16type Query {
17 me: User
18}The Registration Process
Steps:
- Input the user’s username & password.
- Check whether the username is already taken.
- Hash the password: bcrypt.
- Save the new user to the database.
- Generate a JWT token.
- Return the token & user data.
1func (r *mutationResolver) Register(ctx context.Context, username string, password string) (*model.AuthPayload, error) {
2 if _, exists := users[username]; exists {
3 return nil, errors.New("username already exists")
4 }
5 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
6 if err != nil {
7 return nil, err
8 }
9 user := &User{
10 ID: uuid.NewString(),
11 Username: username,
12 Password: string(hashed),
13 }
14 users[username] = user
15 token, err := generateJWT(user)
16 if err != nil {
17 return nil, err
18 }
19 return &model.AuthPayload{
20 Token: token,
21 User: &model.User{ID: user.ID, Username: user.Username},
22 }, nil
23}The Login Process
The flow is very similar; only the password validation step is replaced with a hash compare.
1func (r *mutationResolver) Login(ctx context.Context, username string, password string) (*model.AuthPayload, error) {
2 user, exists := users[username]
3 if !exists {
4 return nil, errors.New("invalid credentials")
5 }
6 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
7 return nil, errors.New("invalid credentials")
8 }
9 token, err := generateJWT(user)
10 if err != nil {
11 return nil, err
12 }
13 return &model.AuthPayload{
14 Token: token,
15 User: &model.User{ID: user.ID, Username: user.Username},
16 }, nil
17}JWT Generation & Validation
A JWT is created during register/login and validated every time a protected endpoint is accessed.
The generateJWT() function:
1func generateJWT(user *User) (string, error) {
2 claims := jwt.MapClaims{
3 "sub": user.ID,
4 "username": user.Username,
5 "exp": time.Now().Add(time.Hour * 24).Unix(),
6 }
7 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
8 return token.SignedString([]byte(SECRET_KEY))
9}The parseJWT() function:
1func parseJWT(tokenStr string) (*User, error) {
2 token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
3 return []byte(SECRET_KEY), nil
4 })
5 if err != nil || !token.Valid {
6 return nil, errors.New("invalid token")
7 }
8 claims, ok := token.Claims.(jwt.MapClaims)
9 if !ok {
10 return nil, errors.New("invalid token claims")
11 }
12 username := claims["username"].(string)
13 user, exists := users[username]
14 if !exists {
15 return nil, errors.New("user not found")
16 }
17 return user, nil
18}Endpoint Protection Middleware
In gqlgen, we inject the user into the context, and then in specific resolvers we can check authentication.
1func AuthMiddleware() func(ctx context.Context) context.Context {
2 return func(ctx context.Context) context.Context {
3 token := extractBearerToken(ctx)
4 if token == "" {
5 return ctx
6 }
7 user, err := parseJWT(token)
8 if err != nil {
9 return ctx
10 }
11 return context.WithValue(ctx, "user", user)
12 }
13}Then, for example, in the me query:
1func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
2 user := ctx.Value("user")
3 if user == nil {
4 return nil, errors.New("unauthenticated")
5 }
6 u := user.(*User)
7 return &model.User{ID: u.ID, Username: u.Username}, nil
8}Flow Diagram
Let’s illustrate the registration and login flow using Mermaid:
sequenceDiagram
participant Client
participant Server
Client->>Server: register(username, password)
Server->>Server: cek username unik?
alt username sudah ada
Server-->>Client: error
else username OK
Server->>Server: hash password
Server->>Server: simpan user ke DB
Server->>Server: generate JWT
Server-->>Client: {token, user}
end
Client->>Server: login(username, password)
Server->>Server: ambil data user dari DB
alt user tidak ditemukan
Server-->>Client: error
else ada user
Server->>Server: verifikasi hash password
alt invalid credentials
Server-->>Client: error
else sukses
Server->>Server: generate JWT
Server-->>Client: {token, user}
end
end
Client Simulation
For example, here are the GraphQL mutations being posted:
1. Registration
1mutation {
2 register(username: "andi", password: "rahasia") {
3 token
4 user { id username }
5 }
6}Response:
1{
2 "data": {
3 "register": {
4 "token": "eyJhbGciOiJIUzI1NiIsInR5cCI... (JWT)",
5 "user": { "id": "c780...", "username": "andi" }
6 }
7 }
8}2. Login
1mutation {
2 login(username: "andi", password: "rahasia") {
3 token
4 user { id username }
5 }
6}3. Querying Your Own Data (Protected)
Include the header: Authorization: Bearer {token}
1query {
2 me { id username }
3}Comparison Table: REST vs GraphQL + JWT
| Feature | REST + JWT | GraphQL + JWT |
|---|---|---|
| Registration/Login Endpoint | /register, /login | 1 endpoint (/graphql), different mutations |
| Auth Protection | Middleware per route | Middleware per field/resolver |
| Extensibility | Needs new endpoint | Just add it to the schema |
| Transport | JSON | GraphQL Query |
Security Notes
- Never store passwords without hashing.
- JWTs should be given a reasonable expiry (
exp). - Use HTTPS!
- Validate user input (XSS, SQLI, etc.).
Conclusion
With a pattern like the one above, you can deploy a GraphQL + JWT authentication application quickly and securely, taking advantage of Go’s type safety plus gqlgen’s performance. In the real world, you simply add a connection to Postgres or MongoDB and apply stricter security measures such as rate limiting and monitoring.
Hopefully this simple case study of JWT registration & login with gqlgen can serve as a foundation for implementing modern authentication in your next project.
Happy hacking! 🚀
Full Demo Code:
github.com/youruser/gqlgen-jwt-example
(dummy repository, please replace it with your own repo)