gRPC Authentication — Advanced Patterns with JWT, mTLS, and OAuth2
In this tutorial, you will learn about grpc authentication. We cover key concepts, practical examples, and best practices to help you master this topic.
gRPC authentication covers JWT validation in interceptors, mutual TLS authentication between services, OAuth2 token exchange, and fine-grained per-method authorization using claims and roles.
What You'll Learn
- JWT authentication with custom claims
- mTLS for service-to-service authentication
- OAuth2 token validation in gRPC
- Per-method authorization with roles
- Token propagation across services
- Credential types and security best practices
Why It Matters
Authentication in Microservices is complex. You need to authenticate users (who is this person?) and services (is this caller allowed to call this API?). DodaTech's Durga Antivirus Pro uses JWT for user auth with device-level claims and mTLS for service-to-service communication, ensuring that only authorized users and services access each endpoint.
Real-World Use
A user calls the threat analysis API with a JWT from the auth service. The gRPC interceptor validates the JWT signature, extracts the user ID and role, and checks that the user has permission to call AnalyzeThreat. The interceptor also verifies that the calling service's TLS certificate is from the trusted CA.
flowchart TB
A["Client Request"] --> B["TLS Handshake\n(mTLS)"]
B --> C{"Certificate\nValid?"}
C -->|No| D["Reject"]
C -->|Yes| E["JWT Validation\nInterceptor"]
E --> F{"Token Valid\n& Not Expired?"}
F -->|No| G["Reject\nUNAUTHENTICATED"]
F -->|Yes| H["Authorization\nCheck"]
H --> I{"Has Role:\nthreat.reader?"}
I -->|No| J["Reject\nPERMISSION_DENIED"]
I -->|Yes| K["Allow Handler"]
style D fill:#fecaca,stroke:#dc2626
style G fill:#fecaca,stroke:#dc2626
style J fill:#fecaca,stroke:#dc2626
style K fill:#bbf7d0,stroke:#16a34a
Code Examples
Example 1: JWT Auth Interceptor with Custom Claims
package main
import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
jwt.RegisteredClaims
UserID string `json:"user_id"`
DeviceID string `json:"device_id"`
Roles []string `json:"roles"`
Tenant string `json:"tenant"`
}
func authInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Skip auth for health check
if info.FullMethod == "/grpc.health.v1.Health/Check" {
return handler(ctx, req)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated,
"missing metadata")
}
authHeader := md.Get("authorization")
if len(authHeader) == 0 {
return nil, status.Error(codes.Unauthenticated,
"missing authorization header")
}
token := strings.TrimPrefix(authHeader[0], "Bearer ")
claims := &Claims{}
parsedToken, err := jwt.ParseWithClaims(token, claims,
func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil || !parsedToken.Valid {
return nil, status.Error(codes.Unauthenticated,
"invalid token")
}
// Check required role for this method
requiredRole := getRequiredRole(info.FullMethod)
if requiredRole != "" && !hasRole(claims.Roles, requiredRole) {
return nil, status.Error(codes.PermissionDenied,
"insufficient permissions")
}
// Inject claims into context for resolvers
newCtx := context.WithValue(ctx, "claims", claims)
return handler(newCtx, req)
}
func getRequiredRole(method string) string {
switch method {
case "/threat.v1.ThreatService/DeleteThreat":
return "threat.admin"
case "/threat.v1.ThreatService/AnalyzeThreat":
return "threat.analyst"
default:
return "threat.reader"
}
}
Example 2: mTLS in Python
import grpc
import ssl
# Server with mTLS
def create_mtls_server():
with open("server.key") as f:
private_key = f.read()
with open("server.crt") as f:
cert_chain = f.read()
with open("ca.crt") as f:
root_certs = f.read()
credentials = grpc.ssl_server_credentials(
[(private_key, cert_chain)],
root_certificates=root_certs,
require_client_auth=True,
)
server = grpc.server(
grpc.insecure_server(),
credentials=credentials,
)
return server
# Client with mTLS
def create_mtls_client(address):
with open("client.key") as f:
private_key = f.read()
with open("client.crt") as f:
cert_chain = f.read()
with open("ca.crt") as f:
root_certs = f.read()
credentials = grpc.ssl_channel_credentials(
root_certificates=root_certs,
private_key=private_key,
certificate_chain=cert_chain,
)
channel = grpc.secure_channel(address, credentials)
return channel
# Caller identity from mTLS certificate
class CertIdentityInterceptor(ServerInterceptor):
def intercept(self, method, request, context, method_name):
# Extract client certificate info
auth_context = context.auth_context()
if auth_context:
client_cert = auth_context.get(
"x509_client_cert")[0]
# Extract common name as service identity
cn = extract_cn(client_cert)
context.set_identity(cn)
return method(request, context)
Example 3: OAuth2 Token Validation in Node.js
const grpc = require('@grpc/grpc-js');
const { OAuth2Client } = require('google-auth-library');
const oauth2Client = new OAuth2Client();
async function validateGoogleToken(token) {
const ticket = await oauth2Client.verifyIdToken({
idToken: token,
audience: 'your-client-id',
});
const payload = ticket.getPayload();
return {
userId: payload.sub,
email: payload.email,
name: payload.name,
};
}
// Auth interceptor with OAuth2
function oauthInterceptor(method, request, callback, metadata) {
const authHeader = metadata.get('authorization')[0];
if (!authHeader) {
return callback({ code: grpc.status.UNAUTHENTICATED, details: 'No auth' });
}
const token = authHeader.replace('Bearer ', '');
validateGoogleToken(token)
.then((user) => {
// Attach user info to call
metadata.set('x-user-id', user.userId);
metadata.set('x-user-email', user.email);
callback(null, request);
})
.catch(() => {
callback({ code: grpc.status.UNAUTHENTICATED, details: 'Invalid token' });
});
}
// Combined auth: JWT + mTLS
function multiFactorAuth(req) {
// Validate JWT
const user = validateJWT(req.metadata.get('authorization')[0]);
if (!user) throw new Error('Invalid JWT');
// Validate client certificate
const cert = req.call.getPeer();
if (!isAllowedService(cert)) throw new Error('Untrusted certificate');
return user;
}
Common Mistakes
- Rolling your own auth — use established libraries (jwt-go, pyjwt, jsonwebtoken). Custom JWT validation is error-prone (algorithm confusion, expiration checks).
- Not skipping auth for health checks — health check probes don't have tokens. Skip auth for Health/Check method.
- Using tokens in URLs or logs — tokens in query parameters or log files can leak. Always pass auth tokens in gRPC metadata headers.
- Not validating token expiration — always check exp, nbf, and iss claims. Reject expired tokens immediately.
- Mixing user auth and service auth — user tokens (JWT) and service certificates (mTLS) serve different purposes. Use both for defense in depth.
Practice Questions
- What is the difference between authentication and authorization in gRPC?
- How does mTLS authenticate both sides of a connection?
- What claims should a JWT for gRPC contain?
- How do you implement per-method authorization?
- Why should you use both JWT and mTLS together?
Challenge: Design an authentication system for a gRPC microservice with: JWT for user auth (with roles and tenant), mTLS for service-to-service auth, per-method role-based authorization, token refresh flow, and auth bypass only for health checks.
Mini Project
Build a comprehensive gRPC auth library with: JWT validation interceptor (RS256, custom claims), mTLS server and client setup, role-based access control per method, OAuth2 token validation (Google, GitHub), and tenant isolation based on JWT claims.
FAQ
What's Next
Learn about gRPC TLS configuration
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro