Repository: enricofoltran/hello-auth-grpc
Branch: master
Commit: e85f129d8bb7
Files: 29
Total size: 71.6 KB
Directory structure:
gitextract_3qqoywek/
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── auth/
│ ├── auth.pb.go
│ ├── auth.proto
│ └── auth_grpc.pb.go
├── certs/
│ ├── auth-csr.json
│ ├── ca-config.json
│ ├── ca-csr.json
│ ├── client-csr.json
│ ├── hello-csr.json
│ └── jwt-csr.json
├── cmd/
│ ├── auth-client/
│ │ └── main.go
│ ├── auth-server/
│ │ ├── main.go
│ │ ├── server.go
│ │ └── server_test.go
│ ├── hello-client/
│ │ └── main.go
│ └── hello-server/
│ ├── main.go
│ └── server.go
├── credentials/
│ └── jwt/
│ └── jwt.go
├── go.mod
├── go.sum
├── hello/
│ ├── hello.pb.go
│ ├── hello.proto
│ └── hello_grpc.pb.go
└── pkg/
├── config/
│ ├── config.go
│ └── config_test.go
└── logging/
└── logging.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Binaries
bin/
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
coverage.html
# Dependency directories
vendor/
# Go workspace file
go.work
# Environment files (contain secrets)
.env
.env.local
# IDE / Editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Certificates and keys (sensitive)
*.pem
*.csr
*.key
.token
# Deprecated dependency files
Gopkg.toml
Gopkg.lock
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Enrico Foltran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
# Configuration directory for certificates and keys
CONFIG_PATH=${HOME}/.hello/
# Default target
all: init gencert build
# Create configuration directory
init:
mkdir -p ${CONFIG_PATH}
# Build all binaries and generate protobuf code
build:
go mod download
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
hello/hello.proto
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
auth/auth.proto
mkdir -p bin
go build -o bin/auth-client ./cmd/auth-client
go build -o bin/auth-server ./cmd/auth-server
go build -o bin/hello-client ./cmd/hello-client
go build -o bin/hello-server ./cmd/hello-server
# Generate all certificates and keys using CFSSL
gencert:
cfssl gencert \
-initca certs/ca-csr.json | cfssljson -bare ca
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=certs/ca-config.json \
-profile=server \
certs/hello-csr.json | cfssljson -bare hello
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=certs/ca-config.json \
-profile=server \
certs/auth-csr.json | cfssljson -bare auth
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=certs/ca-config.json \
-profile=client \
certs/client-csr.json | cfssljson -bare client
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=certs/ca-config.json \
-profile=signing \
certs/jwt-csr.json | cfssljson -bare jwt
mv *.pem *.csr ${CONFIG_PATH}
# Clean build artifacts
clean:
rm -rf bin/
rm -f hello/hello_grpc.pb.go hello/hello.pb.go
rm -f auth/auth_grpc.pb.go auth/auth.pb.go
# Clean everything including certificates
clean-all: clean
rm -rf ${CONFIG_PATH}
# Run tests
test:
go test -v -race -coverprofile=coverage.out ./...
# Show test coverage
coverage: test
go tool cover -html=coverage.out
# Install protobuf compiler plugins
install-tools:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
.PHONY: all init build gencert clean clean-all test coverage install-tools
================================================
FILE: README.md
================================================
# hello-auth-grpc
A secure gRPC microservices demonstration implementing JWT-based authentication with mutual TLS encryption.
## Overview
This project demonstrates production-ready security patterns for gRPC services in Go, including:
- **JWT Authentication**: RSA-2048 signed tokens with comprehensive claims validation
- **Mutual TLS (mTLS)**: Certificate-based encryption and authentication
- **Rate Limiting**: Protection against brute force attacks
- **Password Security**: bcrypt password hashing
- **Input Validation**: Comprehensive validation of all user inputs
- **Graceful Shutdown**: Proper signal handling for clean shutdowns
- **Request Logging**: Structured logging with interceptors
- **Health Checks**: Standard gRPC health checking protocol
- **Panic Recovery**: Middleware to prevent crashes
## Architecture
The project consists of two independent microservices:
### 1. Auth Service (Port 20000)
Authenticates users and issues JWT tokens.
- **Endpoint**: `Auth.Login`
- **Input**: Username and password
- **Output**: Signed JWT token (1-hour validity)
- **Security Features**:
- bcrypt password hashing
- Rate limiting (1 request per 2 seconds per IP)
- Input validation (username: 1-64 chars, password: 8-128 chars)
- Constant-time comparison to prevent timing attacks
### 2. Hello Service (Port 10000)
Provides greeting functionality secured by JWT authentication.
- **Endpoint**: `Greeter.SayHello`
- **Input**: Empty (username extracted from JWT)
- **Output**: Personalized greeting
- **Security Features**:
- JWT signature verification (RSA)
- Comprehensive claims validation (aud, iss, exp, nbf)
- Token expiration checking
## Prerequisites
- **Go**: 1.21 or later
- **protoc**: Protocol Buffer compiler (optional, for regenerating proto files)
- **cfssl**: CloudFlare PKI toolkit for certificate generation
### Installing Prerequisites
```bash
# Install Go (if not already installed)
# See: https://golang.org/doc/install
# Install cfssl
go install github.com/cloudflare/cfssl/cmd/...@latest
# Install protoc (optional)
# See: https://grpc.io/docs/protoc-installation/
# Install protoc plugins (optional)
make install-tools
```
## Quick Start
### 1. Setup
Clone and initialize the project:
```bash
git clone <repository-url>
cd hello-auth-grpc
make init # Create ~/.hello/ directory
make gencert # Generate certificates and keys
```
### 2. Set Credentials
**IMPORTANT**: Use environment variables for credentials (not command-line flags):
```bash
export AUTH_USERNAME="admin"
export AUTH_PASSWORD="secureP@ssw0rd123" # Must be 8-128 characters
```
### 3. Build
```bash
make build
```
This will:
- Download dependencies
- Generate protobuf code (if protoc is installed)
- Build all binaries to `bin/` directory
### 4. Run Services
**Terminal 1 - Start Auth Service:**
```bash
export AUTH_USERNAME="admin"
export AUTH_PASSWORD="secureP@ssw0rd123"
./bin/auth-server
```
**Terminal 2 - Start Hello Service:**
```bash
./bin/hello-server
```
### 5. Test the Services
**Terminal 3 - Authenticate and Get Token:**
```bash
export AUTH_USERNAME="admin"
export AUTH_PASSWORD="secureP@ssw0rd123"
./bin/auth-client
```
**Terminal 4 - Call Authenticated Service:**
```bash
./bin/hello-client
```
Expected output:
```
hello-client: 2025/11/14 12:00:00 remote server says: Hello, admin!
```
## Configuration
### Environment Variables
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `AUTH_USERNAME` | Username for authentication | Yes | - |
| `AUTH_PASSWORD` | Password (8-128 characters) | Yes | - |
| `HELLO_CONFIG_DIR` | Configuration directory | No | `~/.hello` |
### Command-Line Flags
#### Auth Server
```bash
./bin/auth-server \
--listen-addr 127.0.0.1:20000 \
--jwt-key ~/.hello/jwt-key.pem \
--tls-crt ~/.hello/auth.pem \
--tls-key ~/.hello/auth-key.pem \
--ca-crt ~/.hello/ca.pem
```
#### Hello Server
```bash
./bin/hello-server \
--listen-addr 127.0.0.1:10000 \
--jwt-key ~/.hello/jwt.pem \
--tls-crt ~/.hello/hello.pem \
--tls-key ~/.hello/hello-key.pem \
--ca-crt ~/.hello/ca.pem
```
#### Auth Client
```bash
./bin/auth-client \
--server-addr 127.0.0.1:20000 \
--jwt-token ~/.hello/.token \
--tls-crt ~/.hello/client.pem \
--tls-key ~/.hello/client-key.pem \
--ca-crt ~/.hello/ca.pem
```
#### Hello Client
```bash
./bin/hello-client \
--server-addr 127.0.0.1:10000 \
--jwt-token ~/.hello/.token \
--tls-crt ~/.hello/client.pem \
--tls-key ~/.hello/client-key.pem \
--ca-crt ~/.hello/ca.pem
```
## Security Features
### TLS Configuration
- **Minimum Version**: TLS 1.2
- **Cipher Suites**: Only strong ECDHE ciphers with AES-GCM
- **Certificate Validation**: Mutual TLS with CA verification
- **Key Size**: RSA 2048-bit (consider upgrading to 4096-bit for production)
### JWT Configuration
- **Algorithm**: RS256 (RSA with SHA-256)
- **Key Size**: 2048-bit RSA
- **Token Lifetime**: 1 hour
- **Claims Validated**:
- `sub` (Subject): Username
- `aud` (Audience): "hello.service"
- `iss` (Issuer): "auth.service"
- `exp` (Expiration): Automatic validation
- `nbf` (Not Before): Automatic validation
- `iat` (Issued At): Timestamp
### Rate Limiting
- **Rate**: 0.5 requests/second (1 request every 2 seconds)
- **Burst**: 3 requests
- **Scope**: Per client IP address
- **Response**: HTTP 429 (Resource Exhausted)
### Password Requirements
- **Minimum Length**: 8 characters
- **Maximum Length**: 128 characters
- **Storage**: bcrypt hashed (cost factor: 10)
- **Comparison**: Constant-time to prevent timing attacks
## Development
### Project Structure
```
hello-auth-grpc/
├── auth/ # Auth service protobuf definitions
│ ├── auth.proto
│ └── auth.pb.go # Generated code
├── hello/ # Hello service protobuf definitions
│ ├── hello.proto
│ └── hello.pb.go # Generated code
├── cmd/ # Application entry points
│ ├── auth-server/ # Auth server implementation
│ ├── auth-client/ # Auth client implementation
│ ├── hello-server/ # Hello server implementation
│ └── hello-client/ # Hello client implementation
├── pkg/ # Shared packages
│ ├── config/ # Configuration utilities
│ └── logging/ # Logging and interceptors
├── credentials/ # Custom credential providers
│ └── jwt/ # JWT credential implementation
├── certs/ # Certificate configuration files
├── Makefile # Build automation
└── README.md # This file
```
### Makefile Targets
```bash
make all # Initialize, generate certificates, and build
make init # Create configuration directory
make build # Build all binaries
make gencert # Generate certificates and keys
make clean # Remove binaries and generated code
make clean-all # Remove everything including certificates
make test # Run tests with race detection and coverage
make coverage # Generate and view coverage report
make install-tools # Install protobuf compiler plugins
```
### Running Tests
```bash
# Run all tests
make test
# Run tests with coverage
make coverage
# Run specific package tests
go test -v ./pkg/config/
go test -v ./cmd/auth-server/
```
### Adding New Services
1. Define protobuf service in `<service>/<service>.proto`
2. Generate Go code: `protoc --go_out=. --go-grpc_out=. <service>/<service>.proto`
3. Implement server in `cmd/<service>-server/`
4. Implement client in `cmd/<service>-client/`
5. Add to Makefile build targets
## Health Checks
Both services implement the standard gRPC health checking protocol:
```bash
# Using grpcurl (install: go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest)
grpcurl -plaintext localhost:20000 grpc.health.v1.Health/Check
grpcurl -plaintext localhost:10000 grpc.health.v1.Health/Check
```
Expected response:
```json
{
"status": "SERVING"
}
```
## Logging
All services log to stderr with structured format:
```
auth-server: 2025/11/14 12:00:00 server is starting...
auth-server: 2025/11/14 12:00:01 method=/auth.Auth/Login client=127.0.0.1:54321 code=OK duration=45ms
```
## Graceful Shutdown
Services handle `SIGTERM` and `SIGINT` signals:
```bash
# Send SIGTERM
kill -TERM <pid>
# Or Ctrl+C (SIGINT)
```
Services will:
1. Stop accepting new connections
2. Wait for active requests to complete
3. Clean up resources
4. Exit
## Troubleshooting
### Common Issues
**1. "could not load CA certificate"**
- Run `make gencert` to generate certificates
- Check that `~/.hello/` directory exists
- Verify certificate files have correct permissions
**2. "please provide AUTH_USERNAME and AUTH_PASSWORD"**
- Set environment variables before running servers/clients
- Don't use command-line flags for credentials (security risk)
**3. "could not connect to remote server"**
- Ensure server is running
- Check firewall rules
- Verify server address and port
**4. "invalid credentials"**
- Verify AUTH_USERNAME and AUTH_PASSWORD match server settings
- Check password meets minimum length requirement (8 chars)
**5. "too many login attempts"**
- Rate limiting is active
- Wait 2 seconds between attempts
- Check for multiple clients from same IP
**6. "invalid authentication token"**
- Token may be expired (1-hour lifetime)
- Run auth-client to get new token
- Verify token file exists at `~/.hello/.token`
### Debugging
Enable verbose logging:
```bash
# Set Go's GODEBUG
export GODEBUG=http2debug=2
# Run with race detector
go run -race ./cmd/auth-server/
```
## Security Considerations
### Production Deployment
For production use, consider these additional hardening measures:
1. **Certificates**
- Use 4096-bit RSA keys or ECDSA P-384
- Implement certificate rotation
- Use short-lived certificates (90 days or less)
- Store private keys in hardware security modules (HSM)
2. **Authentication**
- Implement multi-factor authentication (MFA)
- Add account lockout after N failed attempts
- Use external identity providers (OAuth2, OIDC)
- Implement refresh token mechanism
3. **Authorization**
- Add role-based access control (RBAC)
- Implement fine-grained permissions
- Audit all access attempts
4. **Network**
- Deploy behind load balancer
- Use firewall rules to restrict access
- Enable DDoS protection
- Implement network segmentation
5. **Monitoring**
- Add Prometheus metrics
- Set up alerting (PagerDuty, Opsgenie)
- Enable distributed tracing (Jaeger, Zipkin)
- Log aggregation (ELK, Loki)
6. **Token Management**
- Implement token revocation/blacklisting
- Reduce token lifetime (15-30 minutes)
- Add refresh token rotation
- Store tokens securely (encrypted)
## API Documentation
### Auth Service
#### Login
Authenticates a user and returns a JWT token.
**Request:**
```protobuf
message Request {
string username = 1; // 1-64 characters
string password = 2; // 8-128 characters
}
```
**Response:**
```protobuf
message Response {
string token = 1; // JWT token (1-hour validity)
}
```
**Errors:**
- `InvalidArgument`: Invalid username or password format
- `PermissionDenied`: Invalid credentials
- `ResourceExhausted`: Rate limit exceeded
- `Internal`: Server error
### Hello Service
#### SayHello
Returns a personalized greeting for the authenticated user.
**Request:**
```protobuf
message Request {} // Empty - username from JWT
```
**Response:**
```protobuf
message Response {
string message = 1; // Greeting message
}
```
**Errors:**
- `Unauthenticated`: Missing, invalid, or expired token
- `Internal`: Server error
**Authentication:**
Include JWT token in request metadata:
```
authorization: <jwt-token>
```
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run tests (`make test`)
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Acknowledgments
- Built with [gRPC](https://grpc.io/)
- JWT handling with [golang-jwt](https://github.com/golang-jwt/jwt)
- Certificate generation with [CFSSL](https://github.com/cloudflare/cfssl)
## Support
For issues, questions, or contributions, please open an issue on GitHub.
================================================
FILE: auth/auth.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc v6.30.2
// source: auth/auth.proto
package auth
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Request contains user credentials for authentication.
type Request struct {
state protoimpl.MessageState `protogen:"open.v1"`
// username is the user's login name (8-64 characters)
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
// password is the user's password (8-128 characters)
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Request) Reset() {
*x = Request{}
mi := &file_auth_auth_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_auth_auth_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_auth_auth_proto_rawDescGZIP(), []int{0}
}
func (x *Request) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *Request) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
// Response contains the authentication result.
type Response struct {
state protoimpl.MessageState `protogen:"open.v1"`
// token is a signed JWT token valid for 1 hour.
// The token should be included in the "authorization" metadata
// header for authenticated requests to other services.
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Response) Reset() {
*x = Response{}
mi := &file_auth_auth_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_auth_auth_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_auth_auth_proto_rawDescGZIP(), []int{1}
}
func (x *Response) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
var File_auth_auth_proto protoreflect.FileDescriptor
const file_auth_auth_proto_rawDesc = "" +
"\n" +
"\x0fauth/auth.proto\x12\x04auth\"A\n" +
"\aRequest\x12\x1a\n" +
"\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" +
"\bpassword\x18\x02 \x01(\tR\bpassword\" \n" +
"\bResponse\x12\x14\n" +
"\x05token\x18\x01 \x01(\tR\x05token2.\n" +
"\x04Auth\x12&\n" +
"\x05Login\x12\r.auth.Request\x1a\x0e.auth.ResponseB/Z-github.com/enricofoltran/hello-auth-grpc/authb\x06proto3"
var (
file_auth_auth_proto_rawDescOnce sync.Once
file_auth_auth_proto_rawDescData []byte
)
func file_auth_auth_proto_rawDescGZIP() []byte {
file_auth_auth_proto_rawDescOnce.Do(func() {
file_auth_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_auth_auth_proto_rawDesc), len(file_auth_auth_proto_rawDesc)))
})
return file_auth_auth_proto_rawDescData
}
var file_auth_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_auth_auth_proto_goTypes = []any{
(*Request)(nil), // 0: auth.Request
(*Response)(nil), // 1: auth.Response
}
var file_auth_auth_proto_depIdxs = []int32{
0, // 0: auth.Auth.Login:input_type -> auth.Request
1, // 1: auth.Auth.Login:output_type -> auth.Response
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_auth_auth_proto_init() }
func file_auth_auth_proto_init() {
if File_auth_auth_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth_auth_proto_rawDesc), len(file_auth_auth_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_auth_auth_proto_goTypes,
DependencyIndexes: file_auth_auth_proto_depIdxs,
MessageInfos: file_auth_auth_proto_msgTypes,
}.Build()
File_auth_auth_proto = out.File
file_auth_auth_proto_goTypes = nil
file_auth_auth_proto_depIdxs = nil
}
================================================
FILE: auth/auth.proto
================================================
syntax = "proto3";
package auth;
option go_package = "github.com/enricofoltran/hello-auth-grpc/auth";
// Auth service provides user authentication and JWT token issuance.
// This service validates user credentials and returns signed JWT tokens
// that can be used to authenticate requests to other services.
service Auth {
// Login authenticates a user with username and password.
// On successful authentication, returns a JWT token valid for 1 hour.
// Rate limited to prevent brute force attacks.
rpc Login (Request) returns (Response);
}
// Request contains user credentials for authentication.
message Request {
// username is the user's login name (8-64 characters)
string username = 1;
// password is the user's password (8-128 characters)
string password = 2;
}
// Response contains the authentication result.
message Response {
// token is a signed JWT token valid for 1 hour.
// The token should be included in the "authorization" metadata
// header for authenticated requests to other services.
string token = 1;
}
================================================
FILE: auth/auth_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.30.2
// source: auth/auth.proto
package auth
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Auth_Login_FullMethodName = "/auth.Auth/Login"
)
// AuthClient is the client API for Auth service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// Auth service provides user authentication and JWT token issuance.
// This service validates user credentials and returns signed JWT tokens
// that can be used to authenticate requests to other services.
type AuthClient interface {
// Login authenticates a user with username and password.
// On successful authentication, returns a JWT token valid for 1 hour.
// Rate limited to prevent brute force attacks.
Login(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}
type authClient struct {
cc grpc.ClientConnInterface
}
func NewAuthClient(cc grpc.ClientConnInterface) AuthClient {
return &authClient{cc}
}
func (c *authClient) Login(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Response)
err := c.cc.Invoke(ctx, Auth_Login_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthServer is the server API for Auth service.
// All implementations must embed UnimplementedAuthServer
// for forward compatibility.
//
// Auth service provides user authentication and JWT token issuance.
// This service validates user credentials and returns signed JWT tokens
// that can be used to authenticate requests to other services.
type AuthServer interface {
// Login authenticates a user with username and password.
// On successful authentication, returns a JWT token valid for 1 hour.
// Rate limited to prevent brute force attacks.
Login(context.Context, *Request) (*Response, error)
mustEmbedUnimplementedAuthServer()
}
// UnimplementedAuthServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAuthServer struct{}
func (UnimplementedAuthServer) Login(context.Context, *Request) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
}
func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {}
func (UnimplementedAuthServer) testEmbeddedByValue() {}
// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AuthServer will
// result in compilation errors.
type UnsafeAuthServer interface {
mustEmbedUnimplementedAuthServer()
}
func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) {
// If the following call pancis, it indicates UnimplementedAuthServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Auth_ServiceDesc, srv)
}
func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServer).Login(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Auth_Login_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServer).Login(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
// Auth_ServiceDesc is the grpc.ServiceDesc for Auth service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Auth_ServiceDesc = grpc.ServiceDesc{
ServiceName: "auth.Auth",
HandlerType: (*AuthServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Login",
Handler: _Auth_Login_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "auth/auth.proto",
}
================================================
FILE: certs/auth-csr.json
================================================
{
"CN": "localhost",
"hosts": [
"localhost",
"127.0.0.1"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "US",
"L": "CA",
"ST": "San Francisco",
"O": "My Own Company",
"OU": "gRPC"
}
]
}
================================================
FILE: certs/ca-config.json
================================================
{
"signing": {
"default": {
"expiry": "168h"
},
"profiles": {
"server": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment",
"server auth"
]
},
"client": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
},
"signing": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment"
]
}
}
}
}
================================================
FILE: certs/ca-csr.json
================================================
{
"CN": "gRPC CA",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "US",
"L": "CA",
"ST": "San Francisco",
"O": "My Own Company",
"OU": "gRPC"
}
]
}
================================================
FILE: certs/client-csr.json
================================================
{
"CN": "client",
"hosts": [""],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "US",
"L": "CA",
"ST": "San Francisco",
"O": "My Own Company",
"OU": "gRPC"
}
]
}
================================================
FILE: certs/hello-csr.json
================================================
{
"CN": "localhost",
"hosts": [
"localhost",
"127.0.0.1"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "US",
"L": "CA",
"ST": "San Francisco",
"O": "My Own Company",
"OU": "gRPC"
}
]
}
================================================
FILE: certs/jwt-csr.json
================================================
{
"CN": "client",
"hosts": [""],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "US",
"L": "CA",
"ST": "San Francisco",
"O": "My Own Company",
"OU": "JWT"
}
]
}
================================================
FILE: cmd/auth-client/main.go
================================================
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"flag"
"log"
"os"
"time"
pb "github.com/enricofoltran/hello-auth-grpc/auth"
"github.com/enricofoltran/hello-auth-grpc/pkg/config"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
// RequestTimeout is the maximum time to wait for a request
RequestTimeout = 10 * time.Second
)
func main() {
// Define flags
serverAddr := flag.String("server-addr", "127.0.0.1:20000", "remote auth server address")
tlsCrt := flag.String("tls-crt", config.WithConfigDir("client.pem"), "client certificate file")
tlsKey := flag.String("tls-key", config.WithConfigDir("client-key.pem"), "client private key file")
caCrt := flag.String("ca-crt", config.WithConfigDir("ca.pem"), "CA certificate file")
jwtToken := flag.String("jwt-token", config.WithConfigDir(".token"), "the jwt auth token file")
flag.Parse()
logger := log.New(os.Stderr, "auth-client: ", log.LstdFlags)
// Get credentials from environment variables (more secure than CLI flags)
username := os.Getenv("AUTH_USERNAME")
password := os.Getenv("AUTH_PASSWORD")
if username == "" || password == "" {
logger.Fatalln("please provide AUTH_USERNAME and AUTH_PASSWORD environment variables")
}
// Load TLS certificate and key
crt, err := tls.LoadX509KeyPair(*tlsCrt, *tlsKey)
if err != nil {
logger.Fatalf("could not load client key pair from file: %v", err)
}
// Load CA certificate
rawCaCrt, err := os.ReadFile(*caCrt)
if err != nil {
logger.Fatalf("could not load CA certificate from file: %v", err)
}
caCrtPool := x509.NewCertPool()
if ok := caCrtPool.AppendCertsFromPEM(rawCaCrt); !ok {
logger.Fatalf("could not append CA certificate to cert pool")
}
// Hardened TLS configuration
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{crt},
RootCAs: caCrtPool,
MinVersion: tls.VersionTLS12,
}
tlsCreds := credentials.NewTLS(tlsConfig)
// Connect with dial options
conn, err := grpc.NewClient(
*serverAddr,
grpc.WithTransportCredentials(tlsCreds),
)
if err != nil {
logger.Fatalf("could not create client: %v", err)
}
defer conn.Close()
clt := pb.NewAuthClient(conn)
// Use context with timeout
ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
defer cancel()
req := &pb.Request{Username: username, Password: password}
res, err := clt.Login(ctx, req)
if err != nil {
logger.Fatalf("could not login: %v", err)
}
// Save token with restricted permissions (0600 = owner read/write only)
err = os.WriteFile(*jwtToken, []byte(res.Token), 0600)
if err != nil {
logger.Fatalf("could not save auth token to disk: %v", err)
}
logger.Println("login succeeded!")
}
================================================
FILE: cmd/auth-server/main.go
================================================
package main
import (
"crypto/tls"
"crypto/x509"
"flag"
"log"
"net"
"os"
"os/signal"
"syscall"
pb "github.com/enricofoltran/hello-auth-grpc/auth"
"github.com/enricofoltran/hello-auth-grpc/pkg/config"
"github.com/enricofoltran/hello-auth-grpc/pkg/logging"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
func main() {
// Define flags
listenAddr := flag.String("listen-addr", "127.0.0.1:20000", "auth server listen address")
jwtKey := flag.String("jwt-key", config.WithConfigDir("jwt-key.pem"), "the private key to use for signing JWT tokens")
tlsCrt := flag.String("tls-crt", config.WithConfigDir("auth.pem"), "auth server certificate file")
tlsKey := flag.String("tls-key", config.WithConfigDir("auth-key.pem"), "auth server private key file")
caCrt := flag.String("ca-crt", config.WithConfigDir("ca.pem"), "CA certificate file")
flag.Parse()
logger := log.New(os.Stderr, "auth: ", log.LstdFlags)
logger.Printf("server is starting...")
// Get credentials from environment variables (more secure than CLI flags)
username := os.Getenv("AUTH_USERNAME")
password := os.Getenv("AUTH_PASSWORD")
if username == "" || password == "" {
logger.Fatalln("please provide AUTH_USERNAME and AUTH_PASSWORD environment variables")
}
// Load TLS certificate and key
crt, err := tls.LoadX509KeyPair(*tlsCrt, *tlsKey)
if err != nil {
logger.Fatalf("could not load server key pair from file: %v", err)
}
// Load CA certificate
rawCaCrt, err := os.ReadFile(*caCrt)
if err != nil {
logger.Fatalf("could not load CA certificate from file: %v", err)
}
caCrtPool := x509.NewCertPool()
if ok := caCrtPool.AppendCertsFromPEM(rawCaCrt); !ok {
// Fixed: Don't reference wrong error variable
logger.Fatalf("could not append CA certificate to cert pool")
}
// Hardened TLS configuration
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{crt},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCrtPool,
MinVersion: tls.VersionTLS12, // Enforce minimum TLS 1.2
CipherSuites: []uint16{
// Strong cipher suites only
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},
PreferServerCipherSuites: true,
}
tlsCreds := credentials.NewTLS(tlsConfig)
// Create listener
ln, err := net.Listen("tcp", *listenAddr)
if err != nil {
logger.Fatalf("could not listen: %v", err)
}
// Create gRPC server with interceptors for logging and panic recovery
grpcServer := grpc.NewServer(
grpc.Creds(tlsCreds),
grpc.ChainUnaryInterceptor(
logging.PanicRecoveryInterceptor(logger),
logging.UnaryServerInterceptor(logger),
),
)
// Create and register auth server
authServer, err := NewAuthServer(*jwtKey, username, password)
if err != nil {
logger.Fatalf("%v", err)
}
pb.RegisterAuthServer(grpcServer, authServer)
// Register health check service
healthServer := health.NewServer()
healthpb.RegisterHealthServer(grpcServer, healthServer)
healthServer.SetServingStatus("auth.Auth", healthpb.HealthCheckResponse_SERVING)
// Set up graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
logger.Println("shutting down gracefully...")
grpcServer.GracefulStop()
}()
logger.Printf("server is listening on %s...", *listenAddr)
if err := grpcServer.Serve(ln); err != nil {
logger.Fatalf("failed to serve: %v", err)
}
}
================================================
FILE: cmd/auth-server/server.go
================================================
package main
import (
"context"
"crypto/rsa"
"fmt"
"os"
"strings"
"sync"
"time"
pb "github.com/enricofoltran/hello-auth-grpc/auth"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"golang.org/x/time/rate"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
const (
// MaxUsernameLength is the maximum allowed username length
MaxUsernameLength = 64
// MaxPasswordLength is the maximum allowed password length
MaxPasswordLength = 128
// MinPasswordLength is the minimum required password length
MinPasswordLength = 8
// TokenExpiration is the JWT token validity duration (1 hour)
TokenExpiration = time.Hour
// RateLimitBurst allows burst of login attempts
RateLimitBurst = 3
// RateLimitPerSecond limits login attempts per second per IP
RateLimitPerSecond = 0.5 // 1 request every 2 seconds
)
// server implements the Auth service with security enhancements.
type server struct {
pb.UnimplementedAuthServer
jwtKey *rsa.PrivateKey
passwordHash []byte // bcrypt hash of the password
username string
rateLimiters map[string]*rate.Limiter
rateLimitersMu sync.RWMutex
}
// NewAuthServer creates a new auth server instance with bcrypt password hashing.
// The password parameter should be the plaintext password which will be hashed.
func NewAuthServer(jwtKeyPath, username, password string) (*server, error) {
// Validate inputs
if err := validateUsername(username); err != nil {
return nil, fmt.Errorf("invalid username: %w", err)
}
if err := validatePassword(password); err != nil {
return nil, fmt.Errorf("invalid password: %w", err)
}
// Load JWT private key
rawJwtKey, err := os.ReadFile(jwtKeyPath)
if err != nil {
return nil, fmt.Errorf("could not load jwt private key from file: %w", err)
}
parsedJwtKey, err := jwt.ParseRSAPrivateKeyFromPEM(rawJwtKey)
if err != nil {
return nil, fmt.Errorf("could not parse jwt private key: %w", err)
}
// Hash the password using bcrypt
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("could not hash password: %w", err)
}
return &server{
jwtKey: parsedJwtKey,
username: username,
passwordHash: passwordHash,
rateLimiters: make(map[string]*rate.Limiter),
}, nil
}
// validateUsername checks if the username meets requirements.
func validateUsername(username string) error {
username = strings.TrimSpace(username)
if username == "" {
return fmt.Errorf("username cannot be empty")
}
if len(username) > MaxUsernameLength {
return fmt.Errorf("username exceeds maximum length of %d", MaxUsernameLength)
}
return nil
}
// validatePassword checks if the password meets requirements.
func validatePassword(password string) error {
if len(password) < MinPasswordLength {
return fmt.Errorf("password must be at least %d characters", MinPasswordLength)
}
if len(password) > MaxPasswordLength {
return fmt.Errorf("password exceeds maximum length of %d", MaxPasswordLength)
}
return nil
}
// getRateLimiter returns a rate limiter for the given client address.
func (s *server) getRateLimiter(clientAddr string) *rate.Limiter {
s.rateLimitersMu.Lock()
defer s.rateLimitersMu.Unlock()
limiter, exists := s.rateLimiters[clientAddr]
if !exists {
limiter = rate.NewLimiter(RateLimitPerSecond, RateLimitBurst)
s.rateLimiters[clientAddr] = limiter
}
return limiter
}
// Login authenticates a user and returns a JWT token.
// Implements rate limiting, input validation, and secure password verification.
func (s *server) Login(ctx context.Context, r *pb.Request) (*pb.Response, error) {
// Get client address for rate limiting
clientAddr := "unknown"
if p, ok := peer.FromContext(ctx); ok {
clientAddr = p.Addr.String()
}
// Apply rate limiting per client IP
limiter := s.getRateLimiter(clientAddr)
if !limiter.Allow() {
return nil, status.Error(codes.ResourceExhausted, "too many login attempts, please try again later")
}
// Validate input
if err := validateUsername(r.Username); err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid username format")
}
if err := validatePassword(r.Password); err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid password format")
}
// Verify username
if r.Username != s.username {
// Use constant-time comparison pattern (check password even if username wrong)
// to prevent timing attacks
bcrypt.CompareHashAndPassword(s.passwordHash, []byte(r.Password))
return nil, status.Error(codes.PermissionDenied, "invalid credentials")
}
// Verify password using bcrypt
if err := bcrypt.CompareHashAndPassword(s.passwordHash, []byte(r.Password)); err != nil {
return nil, status.Error(codes.PermissionDenied, "invalid credentials")
}
// Generate JWT token
now := time.Now()
exp := now.Add(TokenExpiration)
claims := jwt.MapClaims{
"sub": r.Username,
"aud": "hello.service",
"iss": "auth.service",
"exp": exp.Unix(),
"iat": now.Unix(),
"nbf": now.Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
tokenStr, err := token.SignedString(s.jwtKey)
if err != nil {
// Don't leak internal error details to client
return nil, status.Error(codes.Internal, "failed to generate token")
}
return &pb.Response{Token: tokenStr}, nil
}
================================================
FILE: cmd/auth-server/server_test.go
================================================
package main
import (
"testing"
"time"
)
func TestValidateUsername(t *testing.T) {
tests := []struct {
name string
username string
wantErr bool
}{
{"valid username", "testuser", false},
{"empty username", "", true},
{"whitespace only", " ", true},
{"too long", string(make([]byte, 65)), true},
{"max length", string(make([]byte, 64)), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateUsername(tt.username)
if (err != nil) != tt.wantErr {
t.Errorf("validateUsername() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestValidatePassword(t *testing.T) {
tests := []struct {
name string
password string
wantErr bool
}{
{"valid password", "password123", false},
{"too short", "pass", true},
{"minimum length", "12345678", false},
{"too long", string(make([]byte, 129)), true},
{"max length", string(make([]byte, 128)), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePassword(tt.password)
if (err != nil) != tt.wantErr {
t.Errorf("validatePassword() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestLogin_InvalidCredentials(t *testing.T) {
// Note: This test requires a temporary RSA key for testing
// In a real scenario, you'd generate a test key or use a fixture
t.Skip("Requires RSA key setup - integration test")
}
func TestLogin_ValidCredentials(t *testing.T) {
// Note: This test requires a temporary RSA key and bcrypt setup
t.Skip("Requires RSA key setup - integration test")
}
func TestLogin_RateLimiting(t *testing.T) {
// Note: This test would verify rate limiting works
t.Skip("Requires full server setup - integration test")
}
func TestJWTTokenExpiration(t *testing.T) {
// Verify tokens expire in 1 hour
now := time.Now()
exp := now.Add(TokenExpiration)
if exp.Sub(now) != time.Hour {
t.Errorf("TokenExpiration should be 1 hour, got %v", exp.Sub(now))
}
}
func TestLogin_InputValidation(t *testing.T) {
t.Skip("Requires full server setup - integration test")
// This test would verify:
// - Empty username returns InvalidArgument
// - Empty password returns InvalidArgument
// - Too long username returns InvalidArgument
// - Too short password returns InvalidArgument
}
// Helper function to create a test server
// func newTestServer(t *testing.T) *server {
// // Generate test RSA key
// privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
// if err != nil {
// t.Fatalf("Failed to generate RSA key: %v", err)
// }
//
// // Hash test password
// passwordHash, err := bcrypt.GenerateFromPassword([]byte("testpass123"), bcrypt.DefaultCost)
// if err != nil {
// t.Fatalf("Failed to hash password: %v", err)
// }
//
// return &server{
// jwtKey: privateKey,
// username: "testuser",
// passwordHash: passwordHash,
// rateLimiters: make(map[string]*rate.Limiter),
// }
// }
================================================
FILE: cmd/hello-client/main.go
================================================
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"flag"
"log"
"os"
"time"
"github.com/enricofoltran/hello-auth-grpc/credentials/jwt"
pb "github.com/enricofoltran/hello-auth-grpc/hello"
"github.com/enricofoltran/hello-auth-grpc/pkg/config"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
// RequestTimeout is the maximum time to wait for a request
RequestTimeout = 10 * time.Second
)
func main() {
// Define flags
serverAddr := flag.String("server-addr", "127.0.0.1:10000", "remote hello server address")
tlsCrt := flag.String("tls-crt", config.WithConfigDir("client.pem"), "client certificate file")
tlsKey := flag.String("tls-key", config.WithConfigDir("client-key.pem"), "client private key file")
caCrt := flag.String("ca-crt", config.WithConfigDir("ca.pem"), "CA certificate file")
jwtToken := flag.String("jwt-token", config.WithConfigDir(".token"), "the jwt auth token file")
flag.Parse()
logger := log.New(os.Stderr, "hello-client: ", log.LstdFlags)
// Load TLS certificate and key
crt, err := tls.LoadX509KeyPair(*tlsCrt, *tlsKey)
if err != nil {
logger.Fatalf("could not load client key pair from file: %v", err)
}
// Load CA certificate
rawCaCrt, err := os.ReadFile(*caCrt)
if err != nil {
logger.Fatalf("could not load CA certificate from file: %v", err)
}
caCrtPool := x509.NewCertPool()
if ok := caCrtPool.AppendCertsFromPEM(rawCaCrt); !ok {
logger.Fatalf("could not append CA certificate to cert pool")
}
// Hardened TLS configuration
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{crt},
RootCAs: caCrtPool,
MinVersion: tls.VersionTLS12,
}
tlsCreds := credentials.NewTLS(tlsConfig)
// Load JWT credentials
jwtCreds, err := jwt.NewFromTokenFile(*jwtToken)
if err != nil {
logger.Fatalf("could not load jwt token from file: %v", err)
}
// Connect with dial options
conn, err := grpc.NewClient(
*serverAddr,
grpc.WithTransportCredentials(tlsCreds),
grpc.WithPerRPCCredentials(jwtCreds),
)
if err != nil {
logger.Fatalf("could not create client: %v", err)
}
defer conn.Close()
clt := pb.NewGreeterClient(conn)
// Use context with timeout
ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
defer cancel()
req := &pb.Request{}
res, err := clt.SayHello(ctx, req)
if err != nil {
logger.Fatalf("could not say hello: %v", err)
}
logger.Printf("remote server says: %s", res.Message)
}
================================================
FILE: cmd/hello-server/main.go
================================================
package main
import (
"crypto/tls"
"crypto/x509"
"flag"
"log"
"net"
"os"
"os/signal"
"syscall"
pb "github.com/enricofoltran/hello-auth-grpc/hello"
"github.com/enricofoltran/hello-auth-grpc/pkg/config"
"github.com/enricofoltran/hello-auth-grpc/pkg/logging"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
func main() {
// Define flags
listenAddr := flag.String("listen-addr", "127.0.0.1:10000", "hello server listen address")
jwtKey := flag.String("jwt-key", config.WithConfigDir("jwt.pem"), "the public key to use for validating JWT tokens")
tlsCrt := flag.String("tls-crt", config.WithConfigDir("hello.pem"), "hello server certificate file")
tlsKey := flag.String("tls-key", config.WithConfigDir("hello-key.pem"), "hello server private key file")
caCrt := flag.String("ca-crt", config.WithConfigDir("ca.pem"), "CA certificate file")
flag.Parse()
logger := log.New(os.Stderr, "hello: ", log.LstdFlags)
logger.Printf("server is starting...")
// Load TLS certificate and key
crt, err := tls.LoadX509KeyPair(*tlsCrt, *tlsKey)
if err != nil {
logger.Fatalf("could not load server key pair from file: %v", err)
}
// Load CA certificate
rawCaCrt, err := os.ReadFile(*caCrt)
if err != nil {
logger.Fatalf("could not load CA certificate from file: %v", err)
}
caCrtPool := x509.NewCertPool()
if ok := caCrtPool.AppendCertsFromPEM(rawCaCrt); !ok {
// Fixed: Don't reference wrong error variable
logger.Fatalf("could not append CA certificate to cert pool")
}
// Hardened TLS configuration
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{crt},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCrtPool,
MinVersion: tls.VersionTLS12, // Enforce minimum TLS 1.2
CipherSuites: []uint16{
// Strong cipher suites only
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},
PreferServerCipherSuites: true,
}
tlsCreds := credentials.NewTLS(tlsConfig)
// Create listener
ln, err := net.Listen("tcp", *listenAddr)
if err != nil {
logger.Fatalf("could not listen: %v", err)
}
// Create gRPC server with interceptors for logging and panic recovery
grpcServer := grpc.NewServer(
grpc.Creds(tlsCreds),
grpc.ChainUnaryInterceptor(
logging.PanicRecoveryInterceptor(logger),
logging.UnaryServerInterceptor(logger),
),
)
// Create and register hello server
helloServer, err := NewHelloServer(*jwtKey)
if err != nil {
logger.Fatalf("%v", err)
}
pb.RegisterGreeterServer(grpcServer, helloServer)
// Register health check service
healthServer := health.NewServer()
healthpb.RegisterHealthServer(grpcServer, healthServer)
healthServer.SetServingStatus("hello.Greeter", healthpb.HealthCheckResponse_SERVING)
// Set up graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
logger.Println("shutting down gracefully...")
grpcServer.GracefulStop()
}()
logger.Printf("server is listening on %s...", *listenAddr)
if err := grpcServer.Serve(ln); err != nil {
logger.Fatalf("failed to serve: %v", err)
}
}
================================================
FILE: cmd/hello-server/server.go
================================================
package main
import (
"context"
"crypto/rsa"
"fmt"
"os"
pb "github.com/enricofoltran/hello-auth-grpc/hello"
"github.com/golang-jwt/jwt/v5"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
const (
// ExpectedAudience is the expected audience claim for JWT tokens
ExpectedAudience = "hello.service"
// ExpectedIssuer is the expected issuer claim for JWT tokens
ExpectedIssuer = "auth.service"
)
// server implements the Greeter service with JWT authentication.
type server struct {
pb.UnimplementedGreeterServer
jwtKey *rsa.PublicKey
}
// claims represents the JWT claims structure with validation.
type claims struct {
jwt.RegisteredClaims
}
// validateJwtToken validates a JWT token with comprehensive claims checking.
func validateJwtToken(tokenString string, key *rsa.PublicKey) (*jwt.Token, *claims, error) {
// Parse and validate the token
jwtToken, err := jwt.ParseWithClaims(tokenString, &claims{}, func(t *jwt.Token) (interface{}, error) {
// Verify signing method
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return key, nil
})
if err != nil {
return nil, nil, fmt.Errorf("token parsing failed: %w", err)
}
// Extract and validate claims
claims, ok := jwtToken.Claims.(*claims)
if !ok || !jwtToken.Valid {
return nil, nil, fmt.Errorf("invalid token claims")
}
// Validate audience
audiences, err := claims.GetAudience()
if err != nil {
return nil, nil, fmt.Errorf("invalid audience claim: %w", err)
}
validAudience := false
for _, aud := range audiences {
if aud == ExpectedAudience {
validAudience = true
break
}
}
if !validAudience {
return nil, nil, fmt.Errorf("invalid audience: expected %s", ExpectedAudience)
}
// Validate issuer
issuer, err := claims.GetIssuer()
if err != nil {
return nil, nil, fmt.Errorf("invalid issuer claim: %w", err)
}
if issuer != ExpectedIssuer {
return nil, nil, fmt.Errorf("invalid issuer: expected %s, got %s", ExpectedIssuer, issuer)
}
// Validate expiration (jwt library does this automatically, but we check explicitly)
if exp, err := claims.GetExpirationTime(); err != nil || exp == nil {
return nil, nil, fmt.Errorf("invalid expiration claim")
}
// Validate not before (jwt library does this automatically, but we check explicitly)
if nbf, err := claims.GetNotBefore(); err != nil || nbf == nil {
return nil, nil, fmt.Errorf("invalid not-before claim")
}
return jwtToken, claims, nil
}
// NewHelloServer creates a new hello server instance.
func NewHelloServer(jwtKeyPath string) (*server, error) {
rawJwtKey, err := os.ReadFile(jwtKeyPath)
if err != nil {
return nil, fmt.Errorf("could not load jwt public key from file: %w", err)
}
parsedJwtKey, err := jwt.ParseRSAPublicKeyFromPEM(rawJwtKey)
if err != nil {
return nil, fmt.Errorf("could not parse jwt public key: %w", err)
}
return &server{jwtKey: parsedJwtKey}, nil
}
// SayHello implements the Greeter service with JWT authentication.
func (s *server) SayHello(ctx context.Context, r *pb.Request) (*pb.Response, error) {
// Extract metadata from context
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing authentication metadata")
}
// Get authorization token
jwtTokens, ok := md["authorization"]
if !ok || len(jwtTokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing authorization token")
}
// Validate the JWT token
_, claims, err := validateJwtToken(jwtTokens[0], s.jwtKey)
if err != nil {
// Don't leak validation error details to client
return nil, status.Error(codes.Unauthenticated, "invalid authentication token")
}
// Extract subject (username) from claims
subject, err := claims.GetSubject()
if err != nil || subject == "" {
return nil, status.Error(codes.Unauthenticated, "invalid token subject")
}
return &pb.Response{Message: "Hello, " + subject + "!"}, nil
}
================================================
FILE: credentials/jwt/jwt.go
================================================
// Package jwt provides gRPC per-RPC credentials for JWT token authentication.
package jwt
import (
"context"
"fmt"
"os"
"google.golang.org/grpc/credentials"
)
// jwt implements the credentials.PerRPCCredentials interface for JWT tokens.
type jwt struct {
token string
}
// NewFromTokenFile creates a JWT credential from a token file.
// The token file should contain a valid JWT token string.
func NewFromTokenFile(tokenPath string) (credentials.PerRPCCredentials, error) {
data, err := os.ReadFile(tokenPath)
if err != nil {
return jwt{}, fmt.Errorf("could not read token file: %w", err)
}
if len(data) == 0 {
return jwt{}, fmt.Errorf("token cannot be empty")
}
return jwt{string(data)}, nil
}
// GetRequestMetadata implements credentials.PerRPCCredentials.
// It adds the JWT token to the request metadata under the "authorization" key.
func (j jwt) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"authorization": j.token,
}, nil
}
// RequireTransportSecurity implements credentials.PerRPCCredentials.
// It returns true to enforce that JWT tokens are only sent over secure connections.
func (j jwt) RequireTransportSecurity() bool {
return true
}
================================================
FILE: go.mod
================================================
module github.com/enricofoltran/hello-auth-grpc
go 1.24.7
require (
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang/protobuf v1.5.4
golang.org/x/crypto v0.44.0
golang.org/x/net v0.46.0
golang.org/x/time v0.14.0
google.golang.org/grpc v1.76.0
)
require (
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
================================================
FILE: go.sum
================================================
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
================================================
FILE: hello/hello.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc v6.30.2
// source: hello/hello.proto
package hello
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Request for SayHello RPC.
// Currently empty as the username is extracted from the JWT token.
type Request struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Request) Reset() {
*x = Request{}
mi := &file_hello_hello_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_hello_hello_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_hello_hello_proto_rawDescGZIP(), []int{0}
}
// Response contains the greeting message.
type Response struct {
state protoimpl.MessageState `protogen:"open.v1"`
// message is a personalized greeting for the authenticated user
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Response) Reset() {
*x = Response{}
mi := &file_hello_hello_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_hello_hello_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_hello_hello_proto_rawDescGZIP(), []int{1}
}
func (x *Response) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_hello_hello_proto protoreflect.FileDescriptor
const file_hello_hello_proto_rawDesc = "" +
"\n" +
"\x11hello/hello.proto\x12\x05hello\"\t\n" +
"\aRequest\"$\n" +
"\bResponse\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage26\n" +
"\aGreeter\x12+\n" +
"\bSayHello\x12\x0e.hello.Request\x1a\x0f.hello.ResponseB0Z.github.com/enricofoltran/hello-auth-grpc/hellob\x06proto3"
var (
file_hello_hello_proto_rawDescOnce sync.Once
file_hello_hello_proto_rawDescData []byte
)
func file_hello_hello_proto_rawDescGZIP() []byte {
file_hello_hello_proto_rawDescOnce.Do(func() {
file_hello_hello_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_hello_hello_proto_rawDesc), len(file_hello_hello_proto_rawDesc)))
})
return file_hello_hello_proto_rawDescData
}
var file_hello_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_hello_hello_proto_goTypes = []any{
(*Request)(nil), // 0: hello.Request
(*Response)(nil), // 1: hello.Response
}
var file_hello_hello_proto_depIdxs = []int32{
0, // 0: hello.Greeter.SayHello:input_type -> hello.Request
1, // 1: hello.Greeter.SayHello:output_type -> hello.Response
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_hello_hello_proto_init() }
func file_hello_hello_proto_init() {
if File_hello_hello_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_hello_hello_proto_rawDesc), len(file_hello_hello_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_hello_hello_proto_goTypes,
DependencyIndexes: file_hello_hello_proto_depIdxs,
MessageInfos: file_hello_hello_proto_msgTypes,
}.Build()
File_hello_hello_proto = out.File
file_hello_hello_proto_goTypes = nil
file_hello_hello_proto_depIdxs = nil
}
================================================
FILE: hello/hello.proto
================================================
syntax = "proto3";
package hello;
option go_package = "github.com/enricofoltran/hello-auth-grpc/hello";
// Greeter service provides a simple greeting functionality.
// All RPC methods require JWT authentication via the "authorization"
// metadata header. The JWT token must be obtained from the Auth service.
service Greeter {
// SayHello returns a personalized greeting message.
// Requires valid JWT token in request metadata.
rpc SayHello (Request) returns (Response);
}
// Request for SayHello RPC.
// Currently empty as the username is extracted from the JWT token.
message Request {}
// Response contains the greeting message.
message Response {
// message is a personalized greeting for the authenticated user
string message = 1;
}
================================================
FILE: hello/hello_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.30.2
// source: hello/hello.proto
package hello
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Greeter_SayHello_FullMethodName = "/hello.Greeter/SayHello"
)
// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// Greeter service provides a simple greeting functionality.
// All RPC methods require JWT authentication via the "authorization"
// metadata header. The JWT token must be obtained from the Auth service.
type GreeterClient interface {
// SayHello returns a personalized greeting message.
// Requires valid JWT token in request metadata.
SayHello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}
type greeterClient struct {
cc grpc.ClientConnInterface
}
func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) SayHello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Response)
err := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// GreeterServer is the server API for Greeter service.
// All implementations must embed UnimplementedGreeterServer
// for forward compatibility.
//
// Greeter service provides a simple greeting functionality.
// All RPC methods require JWT authentication via the "authorization"
// metadata header. The JWT token must be obtained from the Auth service.
type GreeterServer interface {
// SayHello returns a personalized greeting message.
// Requires valid JWT token in request metadata.
SayHello(context.Context, *Request) (*Response, error)
mustEmbedUnimplementedGreeterServer()
}
// UnimplementedGreeterServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedGreeterServer struct{}
func (UnimplementedGreeterServer) SayHello(context.Context, *Request) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
func (UnimplementedGreeterServer) testEmbeddedByValue() {}
// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to GreeterServer will
// result in compilation errors.
type UnsafeGreeterServer interface {
mustEmbedUnimplementedGreeterServer()
}
func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
// If the following call pancis, it indicates UnimplementedGreeterServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Greeter_ServiceDesc, srv)
}
func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Greeter_SayHello_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).SayHello(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Greeter_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hello.Greeter",
HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello",
Handler: _Greeter_SayHello_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "hello/hello.proto",
}
================================================
FILE: pkg/config/config.go
================================================
// Package config provides shared configuration utilities for the hello-auth-grpc services.
package config
import (
"os"
"path/filepath"
)
// DefaultConfigDir returns the default configuration directory.
// Can be overridden with the HELLO_CONFIG_DIR environment variable.
func DefaultConfigDir() string {
if dir := os.Getenv("HELLO_CONFIG_DIR"); dir != "" {
return dir
}
return filepath.Join(os.Getenv("HOME"), ".hello")
}
// WithConfigDir returns the full path for a file within the configuration directory.
func WithConfigDir(path string) string {
return filepath.Join(DefaultConfigDir(), path)
}
================================================
FILE: pkg/config/config_test.go
================================================
package config
import (
"os"
"path/filepath"
"testing"
)
func TestDefaultConfigDir(t *testing.T) {
// Test with custom env var
customDir := "/tmp/custom-config"
os.Setenv("HELLO_CONFIG_DIR", customDir)
defer os.Unsetenv("HELLO_CONFIG_DIR")
got := DefaultConfigDir()
if got != customDir {
t.Errorf("DefaultConfigDir() = %v, want %v", got, customDir)
}
// Test with default (HOME/.hello)
os.Unsetenv("HELLO_CONFIG_DIR")
got = DefaultConfigDir()
expected := filepath.Join(os.Getenv("HOME"), ".hello")
if got != expected {
t.Errorf("DefaultConfigDir() = %v, want %v", got, expected)
}
}
func TestWithConfigDir(t *testing.T) {
testFile := "test.pem"
got := WithConfigDir(testFile)
// Should combine config dir with file
if !filepath.IsAbs(got) {
t.Errorf("WithConfigDir() should return absolute path, got %v", got)
}
if filepath.Base(got) != testFile {
t.Errorf("WithConfigDir() should preserve filename, got %v", got)
}
}
================================================
FILE: pkg/logging/logging.go
================================================
// Package logging provides structured logging utilities for gRPC services.
package logging
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
// UnaryServerInterceptor returns a gRPC interceptor that logs all requests.
func UnaryServerInterceptor(logger *log.Logger) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// Get client address if available
clientAddr := "unknown"
if p, ok := peer.FromContext(ctx); ok {
clientAddr = p.Addr.String()
}
// Call the handler
resp, err := handler(ctx, req)
// Log the request
duration := time.Since(start)
code := codes.OK
if err != nil {
if st, ok := status.FromError(err); ok {
code = st.Code()
} else {
code = codes.Unknown
}
}
logger.Printf("method=%s client=%s code=%s duration=%v",
info.FullMethod, clientAddr, code, duration)
if err != nil {
logger.Printf("error: %v", err)
}
return resp, err
}
}
// PanicRecoveryInterceptor returns a gRPC interceptor that recovers from panics.
func PanicRecoveryInterceptor(logger *log.Logger) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
logger.Printf("PANIC recovered: method=%s panic=%v", info.FullMethod, r)
err = status.Errorf(codes.Internal, "internal server error")
}
}()
return handler(ctx, req)
}
}
gitextract_3qqoywek/
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── auth/
│ ├── auth.pb.go
│ ├── auth.proto
│ └── auth_grpc.pb.go
├── certs/
│ ├── auth-csr.json
│ ├── ca-config.json
│ ├── ca-csr.json
│ ├── client-csr.json
│ ├── hello-csr.json
│ └── jwt-csr.json
├── cmd/
│ ├── auth-client/
│ │ └── main.go
│ ├── auth-server/
│ │ ├── main.go
│ │ ├── server.go
│ │ └── server_test.go
│ ├── hello-client/
│ │ └── main.go
│ └── hello-server/
│ ├── main.go
│ └── server.go
├── credentials/
│ └── jwt/
│ └── jwt.go
├── go.mod
├── go.sum
├── hello/
│ ├── hello.pb.go
│ ├── hello.proto
│ └── hello_grpc.pb.go
└── pkg/
├── config/
│ ├── config.go
│ └── config_test.go
└── logging/
└── logging.go
SYMBOL INDEX (110 symbols across 15 files)
FILE: auth/auth.pb.go
constant _ (line 19) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
constant _ (line 21) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
type Request (line 25) | type Request struct
method Reset (line 35) | func (x *Request) Reset() {
method String (line 42) | func (x *Request) String() string {
method ProtoMessage (line 46) | func (*Request) ProtoMessage() {}
method ProtoReflect (line 48) | func (x *Request) ProtoReflect() protoreflect.Message {
method Descriptor (line 61) | func (*Request) Descriptor() ([]byte, []int) {
method GetUsername (line 65) | func (x *Request) GetUsername() string {
method GetPassword (line 72) | func (x *Request) GetPassword() string {
type Response (line 80) | type Response struct
method Reset (line 90) | func (x *Response) Reset() {
method String (line 97) | func (x *Response) String() string {
method ProtoMessage (line 101) | func (*Response) ProtoMessage() {}
method ProtoReflect (line 103) | func (x *Response) ProtoReflect() protoreflect.Message {
method Descriptor (line 116) | func (*Response) Descriptor() ([]byte, []int) {
method GetToken (line 120) | func (x *Response) GetToken() string {
constant file_auth_auth_proto_rawDesc (line 129) | file_auth_auth_proto_rawDesc = "" +
function file_auth_auth_proto_rawDescGZIP (line 145) | func file_auth_auth_proto_rawDescGZIP() []byte {
function init (line 167) | func init() { file_auth_auth_proto_init() }
function file_auth_auth_proto_init (line 168) | func file_auth_auth_proto_init() {
FILE: auth/auth_grpc.pb.go
constant _ (line 19) | _ = grpc.SupportPackageIsVersion9
constant Auth_Login_FullMethodName (line 22) | Auth_Login_FullMethodName = "/auth.Auth/Login"
type AuthClient (line 32) | type AuthClient interface
type authClient (line 39) | type authClient struct
method Login (line 47) | func (c *authClient) Login(ctx context.Context, in *Request, opts ...g...
function NewAuthClient (line 43) | func NewAuthClient(cc grpc.ClientConnInterface) AuthClient {
type AuthServer (line 64) | type AuthServer interface
type UnimplementedAuthServer (line 77) | type UnimplementedAuthServer struct
method Login (line 79) | func (UnimplementedAuthServer) Login(context.Context, *Request) (*Resp...
method mustEmbedUnimplementedAuthServer (line 82) | func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {}
method testEmbeddedByValue (line 83) | func (UnimplementedAuthServer) testEmbeddedByValue() {}
type UnsafeAuthServer (line 88) | type UnsafeAuthServer interface
function RegisterAuthServer (line 92) | func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) {
function _Auth_Login_Handler (line 103) | func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(...
FILE: cmd/auth-client/main.go
constant RequestTimeout (line 20) | RequestTimeout = 10 * time.Second
function main (line 23) | func main() {
FILE: cmd/auth-server/main.go
function main (line 22) | func main() {
FILE: cmd/auth-server/server.go
constant MaxUsernameLength (line 23) | MaxUsernameLength = 64
constant MaxPasswordLength (line 25) | MaxPasswordLength = 128
constant MinPasswordLength (line 27) | MinPasswordLength = 8
constant TokenExpiration (line 29) | TokenExpiration = time.Hour
constant RateLimitBurst (line 31) | RateLimitBurst = 3
constant RateLimitPerSecond (line 33) | RateLimitPerSecond = 0.5
type server (line 37) | type server struct
method getRateLimiter (line 107) | func (s *server) getRateLimiter(clientAddr string) *rate.Limiter {
method Login (line 122) | func (s *server) Login(ctx context.Context, r *pb.Request) (*pb.Respon...
function NewAuthServer (line 48) | func NewAuthServer(jwtKeyPath, username, password string) (*server, erro...
function validateUsername (line 84) | func validateUsername(username string) error {
function validatePassword (line 96) | func validatePassword(password string) error {
FILE: cmd/auth-server/server_test.go
function TestValidateUsername (line 8) | func TestValidateUsername(t *testing.T) {
function TestValidatePassword (line 31) | func TestValidatePassword(t *testing.T) {
function TestLogin_InvalidCredentials (line 54) | func TestLogin_InvalidCredentials(t *testing.T) {
function TestLogin_ValidCredentials (line 60) | func TestLogin_ValidCredentials(t *testing.T) {
function TestLogin_RateLimiting (line 65) | func TestLogin_RateLimiting(t *testing.T) {
function TestJWTTokenExpiration (line 70) | func TestJWTTokenExpiration(t *testing.T) {
function TestLogin_InputValidation (line 80) | func TestLogin_InputValidation(t *testing.T) {
FILE: cmd/hello-client/main.go
constant RequestTimeout (line 21) | RequestTimeout = 10 * time.Second
function main (line 24) | func main() {
FILE: cmd/hello-server/main.go
function main (line 22) | func main() {
FILE: cmd/hello-server/server.go
constant ExpectedAudience (line 18) | ExpectedAudience = "hello.service"
constant ExpectedIssuer (line 20) | ExpectedIssuer = "auth.service"
type server (line 24) | type server struct
method SayHello (line 109) | func (s *server) SayHello(ctx context.Context, r *pb.Request) (*pb.Res...
type claims (line 30) | type claims struct
function validateJwtToken (line 35) | func validateJwtToken(tokenString string, key *rsa.PublicKey) (*jwt.Toke...
function NewHelloServer (line 94) | func NewHelloServer(jwtKeyPath string) (*server, error) {
FILE: credentials/jwt/jwt.go
type jwt (line 13) | type jwt struct
method GetRequestMetadata (line 34) | func (j jwt) GetRequestMetadata(ctx context.Context, uri ...string) (m...
method RequireTransportSecurity (line 42) | func (j jwt) RequireTransportSecurity() bool {
function NewFromTokenFile (line 19) | func NewFromTokenFile(tokenPath string) (credentials.PerRPCCredentials, ...
FILE: hello/hello.pb.go
constant _ (line 19) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
constant _ (line 21) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
type Request (line 26) | type Request struct
method Reset (line 32) | func (x *Request) Reset() {
method String (line 39) | func (x *Request) String() string {
method ProtoMessage (line 43) | func (*Request) ProtoMessage() {}
method ProtoReflect (line 45) | func (x *Request) ProtoReflect() protoreflect.Message {
method Descriptor (line 58) | func (*Request) Descriptor() ([]byte, []int) {
type Response (line 63) | type Response struct
method Reset (line 71) | func (x *Response) Reset() {
method String (line 78) | func (x *Response) String() string {
method ProtoMessage (line 82) | func (*Response) ProtoMessage() {}
method ProtoReflect (line 84) | func (x *Response) ProtoReflect() protoreflect.Message {
method Descriptor (line 97) | func (*Response) Descriptor() ([]byte, []int) {
method GetMessage (line 101) | func (x *Response) GetMessage() string {
constant file_hello_hello_proto_rawDesc (line 110) | file_hello_hello_proto_rawDesc = "" +
function file_hello_hello_proto_rawDescGZIP (line 124) | func file_hello_hello_proto_rawDescGZIP() []byte {
function init (line 146) | func init() { file_hello_hello_proto_init() }
function file_hello_hello_proto_init (line 147) | func file_hello_hello_proto_init() {
FILE: hello/hello_grpc.pb.go
constant _ (line 19) | _ = grpc.SupportPackageIsVersion9
constant Greeter_SayHello_FullMethodName (line 22) | Greeter_SayHello_FullMethodName = "/hello.Greeter/SayHello"
type GreeterClient (line 32) | type GreeterClient interface
type greeterClient (line 38) | type greeterClient struct
method SayHello (line 46) | func (c *greeterClient) SayHello(ctx context.Context, in *Request, opt...
function NewGreeterClient (line 42) | func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
type GreeterServer (line 63) | type GreeterServer interface
type UnimplementedGreeterServer (line 75) | type UnimplementedGreeterServer struct
method SayHello (line 77) | func (UnimplementedGreeterServer) SayHello(context.Context, *Request) ...
method mustEmbedUnimplementedGreeterServer (line 80) | func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer(...
method testEmbeddedByValue (line 81) | func (UnimplementedGreeterServer) testEmbeddedByValue() ...
type UnsafeGreeterServer (line 86) | type UnsafeGreeterServer interface
function RegisterGreeterServer (line 90) | func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
function _Greeter_SayHello_Handler (line 101) | func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec...
FILE: pkg/config/config.go
function DefaultConfigDir (line 11) | func DefaultConfigDir() string {
function WithConfigDir (line 19) | func WithConfigDir(path string) string {
FILE: pkg/config/config_test.go
function TestDefaultConfigDir (line 9) | func TestDefaultConfigDir(t *testing.T) {
function TestWithConfigDir (line 29) | func TestWithConfigDir(t *testing.T) {
FILE: pkg/logging/logging.go
function UnaryServerInterceptor (line 16) | func UnaryServerInterceptor(logger *log.Logger) grpc.UnaryServerIntercep...
function PanicRecoveryInterceptor (line 57) | func PanicRecoveryInterceptor(logger *log.Logger) grpc.UnaryServerInterc...
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (80K chars).
[
{
"path": ".gitignore",
"chars": 445,
"preview": "# Binaries\nbin/\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go cover"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2017 Enrico Foltran\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "Makefile",
"chars": 2116,
"preview": "# Configuration directory for certificates and keys\nCONFIG_PATH=${HOME}/.hello/\n\n# Default target\nall: init gencert buil"
},
{
"path": "README.md",
"chars": 12517,
"preview": "# hello-auth-grpc\n\nA secure gRPC microservices demonstration implementing JWT-based authentication with mutual TLS encry"
},
{
"path": "auth/auth.pb.go",
"chars": 5484,
"preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc v6.30.2\n// so"
},
{
"path": "auth/auth.proto",
"chars": 1080,
"preview": "syntax = \"proto3\";\n\npackage auth;\n\noption go_package = \"github.com/enricofoltran/hello-auth-grpc/auth\";\n\n// Auth service"
},
{
"path": "auth/auth_grpc.pb.go",
"chars": 4774,
"preview": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.5.1\n// - protoc "
},
{
"path": "certs/auth-csr.json",
"chars": 337,
"preview": "{\n \"CN\": \"localhost\",\n \"hosts\": [\n \"localhost\",\n \"127.0.0.1\"\n ],\n \"key\": {\n \"algo\": \"rs"
},
{
"path": "certs/ca-config.json",
"chars": 761,
"preview": "{\n \"signing\": {\n \"default\": {\n \"expiry\": \"168h\"\n },\n \"profiles\": {\n \"serve"
},
{
"path": "certs/ca-csr.json",
"chars": 272,
"preview": "{\n \"CN\": \"gRPC CA\",\n \"key\": {\n \"algo\": \"rsa\",\n \"size\": 2048\n },\n \"names\": [\n {\n "
},
{
"path": "certs/client-csr.json",
"chars": 290,
"preview": "{\n \"CN\": \"client\",\n \"hosts\": [\"\"],\n \"key\": {\n \"algo\": \"rsa\",\n \"size\": 2048\n },\n \"names\": [\n"
},
{
"path": "certs/hello-csr.json",
"chars": 337,
"preview": "{\n \"CN\": \"localhost\",\n \"hosts\": [\n \"localhost\",\n \"127.0.0.1\"\n ],\n \"key\": {\n \"algo\": \"rs"
},
{
"path": "certs/jwt-csr.json",
"chars": 289,
"preview": "{\n \"CN\": \"client\",\n \"hosts\": [\"\"],\n \"key\": {\n \"algo\": \"rsa\",\n \"size\": 2048\n },\n \"names\": [\n"
},
{
"path": "cmd/auth-client/main.go",
"chars": 2709,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"flag\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\tpb \"github.com/enricofoltr"
},
{
"path": "cmd/auth-server/main.go",
"chars": 3627,
"preview": "package main\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"flag\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\tpb \"github.com"
},
{
"path": "cmd/auth-server/server.go",
"chars": 5363,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tpb \"github.com/enricofoltran/h"
},
{
"path": "cmd/auth-server/server_test.go",
"chars": 2939,
"preview": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestValidateUsername(t *testing.T) {\n\ttests := []struct {\n\t\tname s"
},
{
"path": "cmd/hello-client/main.go",
"chars": 2459,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"flag\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/enricofoltran/"
},
{
"path": "cmd/hello-server/main.go",
"chars": 3334,
"preview": "package main\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"flag\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\tpb \"github.com"
},
{
"path": "cmd/hello-server/server.go",
"chars": 4029,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"os\"\n\n\tpb \"github.com/enricofoltran/hello-auth-grpc/hello\"\n\t\"git"
},
{
"path": "credentials/jwt/jwt.go",
"chars": 1242,
"preview": "// Package jwt provides gRPC per-RPC credentials for JWT token authentication.\npackage jwt\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t"
},
{
"path": "go.mod",
"chars": 487,
"preview": "module github.com/enricofoltran/hello-auth-grpc\n\ngo 1.24.7\n\nrequire (\n\tgithub.com/golang-jwt/jwt/v5 v5.3.0\n\tgithub.com/g"
},
{
"path": "go.sum",
"chars": 3611,
"preview": "github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:"
},
{
"path": "hello/hello.pb.go",
"chars": 4935,
"preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc v6.30.2\n// so"
},
{
"path": "hello/hello.proto",
"chars": 764,
"preview": "syntax = \"proto3\";\n\npackage hello;\n\noption go_package = \"github.com/enricofoltran/hello-auth-grpc/hello\";\n\n// Greeter se"
},
{
"path": "hello/hello_grpc.pb.go",
"chars": 4792,
"preview": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.5.1\n// - protoc "
},
{
"path": "pkg/config/config.go",
"chars": 611,
"preview": "// Package config provides shared configuration utilities for the hello-auth-grpc services.\npackage config\n\nimport (\n\t\"o"
},
{
"path": "pkg/config/config_test.go",
"chars": 956,
"preview": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestDefaultConfigDir(t *testing.T) {\n\t// Test with cu"
},
{
"path": "pkg/logging/logging.go",
"chars": 1712,
"preview": "// Package logging provides structured logging utilities for gRPC services.\npackage logging\n\nimport (\n\t\"context\"\n\t\"log\"\n"
}
]
About this extraction
This page contains the full source code of the enricofoltran/hello-auth-grpc GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (71.6 KB), approximately 20.9k tokens, and a symbol index with 110 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.