[
  {
    "path": ".gitignore",
    "content": "# 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 coverage tool\n*.out\ncoverage.html\n\n# Dependency directories\nvendor/\n\n# Go workspace file\ngo.work\n\n# Environment files (contain secrets)\n.env\n.env.local\n\n# IDE / Editor files\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n.DS_Store\n\n# Certificates and keys (sensitive)\n*.pem\n*.csr\n*.key\n.token\n\n# Deprecated dependency files\nGopkg.toml\nGopkg.lock\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Enrico Foltran\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# Configuration directory for certificates and keys\nCONFIG_PATH=${HOME}/.hello/\n\n# Default target\nall: init gencert build\n\n# Create configuration directory\ninit:\n\tmkdir -p ${CONFIG_PATH}\n\n# Build all binaries and generate protobuf code\nbuild:\n\tgo mod download\n\tprotoc --go_out=. --go_opt=paths=source_relative \\\n\t\t--go-grpc_out=. --go-grpc_opt=paths=source_relative \\\n\t\thello/hello.proto\n\tprotoc --go_out=. --go_opt=paths=source_relative \\\n\t\t--go-grpc_out=. --go-grpc_opt=paths=source_relative \\\n\t\tauth/auth.proto\n\tmkdir -p bin\n\tgo build -o bin/auth-client ./cmd/auth-client\n\tgo build -o bin/auth-server ./cmd/auth-server\n\tgo build -o bin/hello-client ./cmd/hello-client\n\tgo build -o bin/hello-server ./cmd/hello-server\n\n# Generate all certificates and keys using CFSSL\ngencert:\n\tcfssl gencert \\\n\t\t-initca certs/ca-csr.json | cfssljson -bare ca\n\n\tcfssl gencert \\\n\t\t-ca=ca.pem \\\n\t\t-ca-key=ca-key.pem \\\n\t\t-config=certs/ca-config.json \\\n\t\t-profile=server \\\n\t\tcerts/hello-csr.json | cfssljson -bare hello\n\n\tcfssl gencert \\\n\t\t-ca=ca.pem \\\n\t\t-ca-key=ca-key.pem \\\n\t\t-config=certs/ca-config.json \\\n\t\t-profile=server \\\n\t\tcerts/auth-csr.json | cfssljson -bare auth\n\n\tcfssl gencert \\\n\t\t-ca=ca.pem \\\n\t\t-ca-key=ca-key.pem \\\n\t\t-config=certs/ca-config.json \\\n\t\t-profile=client \\\n\t\tcerts/client-csr.json | cfssljson -bare client\n\n\tcfssl gencert \\\n\t\t-ca=ca.pem \\\n\t\t-ca-key=ca-key.pem \\\n\t\t-config=certs/ca-config.json \\\n\t\t-profile=signing \\\n\t\tcerts/jwt-csr.json | cfssljson -bare jwt\n\n\tmv *.pem *.csr ${CONFIG_PATH}\n\n# Clean build artifacts\nclean:\n\trm -rf bin/\n\trm -f hello/hello_grpc.pb.go hello/hello.pb.go\n\trm -f auth/auth_grpc.pb.go auth/auth.pb.go\n\n# Clean everything including certificates\nclean-all: clean\n\trm -rf ${CONFIG_PATH}\n\n# Run tests\ntest:\n\tgo test -v -race -coverprofile=coverage.out ./...\n\n# Show test coverage\ncoverage: test\n\tgo tool cover -html=coverage.out\n\n# Install protobuf compiler plugins\ninstall-tools:\n\tgo install google.golang.org/protobuf/cmd/protoc-gen-go@latest\n\tgo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest\n\n.PHONY: all init build gencert clean clean-all test coverage install-tools\n"
  },
  {
    "path": "README.md",
    "content": "# hello-auth-grpc\n\nA secure gRPC microservices demonstration implementing JWT-based authentication with mutual TLS encryption.\n\n## Overview\n\nThis project demonstrates production-ready security patterns for gRPC services in Go, including:\n\n- **JWT Authentication**: RSA-2048 signed tokens with comprehensive claims validation\n- **Mutual TLS (mTLS)**: Certificate-based encryption and authentication\n- **Rate Limiting**: Protection against brute force attacks\n- **Password Security**: bcrypt password hashing\n- **Input Validation**: Comprehensive validation of all user inputs\n- **Graceful Shutdown**: Proper signal handling for clean shutdowns\n- **Request Logging**: Structured logging with interceptors\n- **Health Checks**: Standard gRPC health checking protocol\n- **Panic Recovery**: Middleware to prevent crashes\n\n## Architecture\n\nThe project consists of two independent microservices:\n\n### 1. Auth Service (Port 20000)\n\nAuthenticates users and issues JWT tokens.\n\n- **Endpoint**: `Auth.Login`\n- **Input**: Username and password\n- **Output**: Signed JWT token (1-hour validity)\n- **Security Features**:\n  - bcrypt password hashing\n  - Rate limiting (1 request per 2 seconds per IP)\n  - Input validation (username: 1-64 chars, password: 8-128 chars)\n  - Constant-time comparison to prevent timing attacks\n\n### 2. Hello Service (Port 10000)\n\nProvides greeting functionality secured by JWT authentication.\n\n- **Endpoint**: `Greeter.SayHello`\n- **Input**: Empty (username extracted from JWT)\n- **Output**: Personalized greeting\n- **Security Features**:\n  - JWT signature verification (RSA)\n  - Comprehensive claims validation (aud, iss, exp, nbf)\n  - Token expiration checking\n\n## Prerequisites\n\n- **Go**: 1.21 or later\n- **protoc**: Protocol Buffer compiler (optional, for regenerating proto files)\n- **cfssl**: CloudFlare PKI toolkit for certificate generation\n\n### Installing Prerequisites\n\n```bash\n# Install Go (if not already installed)\n# See: https://golang.org/doc/install\n\n# Install cfssl\ngo install github.com/cloudflare/cfssl/cmd/...@latest\n\n# Install protoc (optional)\n# See: https://grpc.io/docs/protoc-installation/\n\n# Install protoc plugins (optional)\nmake install-tools\n```\n\n## Quick Start\n\n### 1. Setup\n\nClone and initialize the project:\n\n```bash\ngit clone <repository-url>\ncd hello-auth-grpc\nmake init      # Create ~/.hello/ directory\nmake gencert   # Generate certificates and keys\n```\n\n### 2. Set Credentials\n\n**IMPORTANT**: Use environment variables for credentials (not command-line flags):\n\n```bash\nexport AUTH_USERNAME=\"admin\"\nexport AUTH_PASSWORD=\"secureP@ssw0rd123\"  # Must be 8-128 characters\n```\n\n### 3. Build\n\n```bash\nmake build\n```\n\nThis will:\n- Download dependencies\n- Generate protobuf code (if protoc is installed)\n- Build all binaries to `bin/` directory\n\n### 4. Run Services\n\n**Terminal 1 - Start Auth Service:**\n```bash\nexport AUTH_USERNAME=\"admin\"\nexport AUTH_PASSWORD=\"secureP@ssw0rd123\"\n./bin/auth-server\n```\n\n**Terminal 2 - Start Hello Service:**\n```bash\n./bin/hello-server\n```\n\n### 5. Test the Services\n\n**Terminal 3 - Authenticate and Get Token:**\n```bash\nexport AUTH_USERNAME=\"admin\"\nexport AUTH_PASSWORD=\"secureP@ssw0rd123\"\n./bin/auth-client\n```\n\n**Terminal 4 - Call Authenticated Service:**\n```bash\n./bin/hello-client\n```\n\nExpected output:\n```\nhello-client: 2025/11/14 12:00:00 remote server says: Hello, admin!\n```\n\n## Configuration\n\n### Environment Variables\n\n| Variable | Description | Required | Default |\n|----------|-------------|----------|---------|\n| `AUTH_USERNAME` | Username for authentication | Yes | - |\n| `AUTH_PASSWORD` | Password (8-128 characters) | Yes | - |\n| `HELLO_CONFIG_DIR` | Configuration directory | No | `~/.hello` |\n\n### Command-Line Flags\n\n#### Auth Server\n```bash\n./bin/auth-server \\\n  --listen-addr 127.0.0.1:20000 \\\n  --jwt-key ~/.hello/jwt-key.pem \\\n  --tls-crt ~/.hello/auth.pem \\\n  --tls-key ~/.hello/auth-key.pem \\\n  --ca-crt ~/.hello/ca.pem\n```\n\n#### Hello Server\n```bash\n./bin/hello-server \\\n  --listen-addr 127.0.0.1:10000 \\\n  --jwt-key ~/.hello/jwt.pem \\\n  --tls-crt ~/.hello/hello.pem \\\n  --tls-key ~/.hello/hello-key.pem \\\n  --ca-crt ~/.hello/ca.pem\n```\n\n#### Auth Client\n```bash\n./bin/auth-client \\\n  --server-addr 127.0.0.1:20000 \\\n  --jwt-token ~/.hello/.token \\\n  --tls-crt ~/.hello/client.pem \\\n  --tls-key ~/.hello/client-key.pem \\\n  --ca-crt ~/.hello/ca.pem\n```\n\n#### Hello Client\n```bash\n./bin/hello-client \\\n  --server-addr 127.0.0.1:10000 \\\n  --jwt-token ~/.hello/.token \\\n  --tls-crt ~/.hello/client.pem \\\n  --tls-key ~/.hello/client-key.pem \\\n  --ca-crt ~/.hello/ca.pem\n```\n\n## Security Features\n\n### TLS Configuration\n\n- **Minimum Version**: TLS 1.2\n- **Cipher Suites**: Only strong ECDHE ciphers with AES-GCM\n- **Certificate Validation**: Mutual TLS with CA verification\n- **Key Size**: RSA 2048-bit (consider upgrading to 4096-bit for production)\n\n### JWT Configuration\n\n- **Algorithm**: RS256 (RSA with SHA-256)\n- **Key Size**: 2048-bit RSA\n- **Token Lifetime**: 1 hour\n- **Claims Validated**:\n  - `sub` (Subject): Username\n  - `aud` (Audience): \"hello.service\"\n  - `iss` (Issuer): \"auth.service\"\n  - `exp` (Expiration): Automatic validation\n  - `nbf` (Not Before): Automatic validation\n  - `iat` (Issued At): Timestamp\n\n### Rate Limiting\n\n- **Rate**: 0.5 requests/second (1 request every 2 seconds)\n- **Burst**: 3 requests\n- **Scope**: Per client IP address\n- **Response**: HTTP 429 (Resource Exhausted)\n\n### Password Requirements\n\n- **Minimum Length**: 8 characters\n- **Maximum Length**: 128 characters\n- **Storage**: bcrypt hashed (cost factor: 10)\n- **Comparison**: Constant-time to prevent timing attacks\n\n## Development\n\n### Project Structure\n\n```\nhello-auth-grpc/\n├── auth/                    # Auth service protobuf definitions\n│   ├── auth.proto\n│   └── auth.pb.go          # Generated code\n├── hello/                   # Hello service protobuf definitions\n│   ├── hello.proto\n│   └── hello.pb.go         # Generated code\n├── cmd/                     # Application entry points\n│   ├── auth-server/        # Auth server implementation\n│   ├── auth-client/        # Auth client implementation\n│   ├── hello-server/       # Hello server implementation\n│   └── hello-client/       # Hello client implementation\n├── pkg/                     # Shared packages\n│   ├── config/             # Configuration utilities\n│   └── logging/            # Logging and interceptors\n├── credentials/            # Custom credential providers\n│   └── jwt/                # JWT credential implementation\n├── certs/                  # Certificate configuration files\n├── Makefile               # Build automation\n└── README.md              # This file\n```\n\n### Makefile Targets\n\n```bash\nmake all          # Initialize, generate certificates, and build\nmake init         # Create configuration directory\nmake build        # Build all binaries\nmake gencert      # Generate certificates and keys\nmake clean        # Remove binaries and generated code\nmake clean-all    # Remove everything including certificates\nmake test         # Run tests with race detection and coverage\nmake coverage     # Generate and view coverage report\nmake install-tools # Install protobuf compiler plugins\n```\n\n### Running Tests\n\n```bash\n# Run all tests\nmake test\n\n# Run tests with coverage\nmake coverage\n\n# Run specific package tests\ngo test -v ./pkg/config/\ngo test -v ./cmd/auth-server/\n```\n\n### Adding New Services\n\n1. Define protobuf service in `<service>/<service>.proto`\n2. Generate Go code: `protoc --go_out=. --go-grpc_out=. <service>/<service>.proto`\n3. Implement server in `cmd/<service>-server/`\n4. Implement client in `cmd/<service>-client/`\n5. Add to Makefile build targets\n\n## Health Checks\n\nBoth services implement the standard gRPC health checking protocol:\n\n```bash\n# Using grpcurl (install: go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest)\ngrpcurl -plaintext localhost:20000 grpc.health.v1.Health/Check\ngrpcurl -plaintext localhost:10000 grpc.health.v1.Health/Check\n```\n\nExpected response:\n```json\n{\n  \"status\": \"SERVING\"\n}\n```\n\n## Logging\n\nAll services log to stderr with structured format:\n\n```\nauth-server: 2025/11/14 12:00:00 server is starting...\nauth-server: 2025/11/14 12:00:01 method=/auth.Auth/Login client=127.0.0.1:54321 code=OK duration=45ms\n```\n\n## Graceful Shutdown\n\nServices handle `SIGTERM` and `SIGINT` signals:\n\n```bash\n# Send SIGTERM\nkill -TERM <pid>\n\n# Or Ctrl+C (SIGINT)\n```\n\nServices will:\n1. Stop accepting new connections\n2. Wait for active requests to complete\n3. Clean up resources\n4. Exit\n\n## Troubleshooting\n\n### Common Issues\n\n**1. \"could not load CA certificate\"**\n- Run `make gencert` to generate certificates\n- Check that `~/.hello/` directory exists\n- Verify certificate files have correct permissions\n\n**2. \"please provide AUTH_USERNAME and AUTH_PASSWORD\"**\n- Set environment variables before running servers/clients\n- Don't use command-line flags for credentials (security risk)\n\n**3. \"could not connect to remote server\"**\n- Ensure server is running\n- Check firewall rules\n- Verify server address and port\n\n**4. \"invalid credentials\"**\n- Verify AUTH_USERNAME and AUTH_PASSWORD match server settings\n- Check password meets minimum length requirement (8 chars)\n\n**5. \"too many login attempts\"**\n- Rate limiting is active\n- Wait 2 seconds between attempts\n- Check for multiple clients from same IP\n\n**6. \"invalid authentication token\"**\n- Token may be expired (1-hour lifetime)\n- Run auth-client to get new token\n- Verify token file exists at `~/.hello/.token`\n\n### Debugging\n\nEnable verbose logging:\n\n```bash\n# Set Go's GODEBUG\nexport GODEBUG=http2debug=2\n\n# Run with race detector\ngo run -race ./cmd/auth-server/\n```\n\n## Security Considerations\n\n### Production Deployment\n\nFor production use, consider these additional hardening measures:\n\n1. **Certificates**\n   - Use 4096-bit RSA keys or ECDSA P-384\n   - Implement certificate rotation\n   - Use short-lived certificates (90 days or less)\n   - Store private keys in hardware security modules (HSM)\n\n2. **Authentication**\n   - Implement multi-factor authentication (MFA)\n   - Add account lockout after N failed attempts\n   - Use external identity providers (OAuth2, OIDC)\n   - Implement refresh token mechanism\n\n3. **Authorization**\n   - Add role-based access control (RBAC)\n   - Implement fine-grained permissions\n   - Audit all access attempts\n\n4. **Network**\n   - Deploy behind load balancer\n   - Use firewall rules to restrict access\n   - Enable DDoS protection\n   - Implement network segmentation\n\n5. **Monitoring**\n   - Add Prometheus metrics\n   - Set up alerting (PagerDuty, Opsgenie)\n   - Enable distributed tracing (Jaeger, Zipkin)\n   - Log aggregation (ELK, Loki)\n\n6. **Token Management**\n   - Implement token revocation/blacklisting\n   - Reduce token lifetime (15-30 minutes)\n   - Add refresh token rotation\n   - Store tokens securely (encrypted)\n\n## API Documentation\n\n### Auth Service\n\n#### Login\n\nAuthenticates a user and returns a JWT token.\n\n**Request:**\n```protobuf\nmessage Request {\n  string username = 1;  // 1-64 characters\n  string password = 2;  // 8-128 characters\n}\n```\n\n**Response:**\n```protobuf\nmessage Response {\n  string token = 1;  // JWT token (1-hour validity)\n}\n```\n\n**Errors:**\n- `InvalidArgument`: Invalid username or password format\n- `PermissionDenied`: Invalid credentials\n- `ResourceExhausted`: Rate limit exceeded\n- `Internal`: Server error\n\n### Hello Service\n\n#### SayHello\n\nReturns a personalized greeting for the authenticated user.\n\n**Request:**\n```protobuf\nmessage Request {}  // Empty - username from JWT\n```\n\n**Response:**\n```protobuf\nmessage Response {\n  string message = 1;  // Greeting message\n}\n```\n\n**Errors:**\n- `Unauthenticated`: Missing, invalid, or expired token\n- `Internal`: Server error\n\n**Authentication:**\nInclude JWT token in request metadata:\n```\nauthorization: <jwt-token>\n```\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Make your changes\n4. Run tests (`make test`)\n5. Commit your changes (`git commit -m 'Add amazing feature'`)\n6. Push to the branch (`git push origin feature/amazing-feature`)\n7. Open a Pull Request\n\n## License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n\n## Acknowledgments\n\n- Built with [gRPC](https://grpc.io/)\n- JWT handling with [golang-jwt](https://github.com/golang-jwt/jwt)\n- Certificate generation with [CFSSL](https://github.com/cloudflare/cfssl)\n\n## Support\n\nFor issues, questions, or contributions, please open an issue on GitHub.\n"
  },
  {
    "path": "auth/auth.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.30.2\n// source: auth/auth.proto\n\npackage auth\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\n// Request contains user credentials for authentication.\ntype Request struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// username is the user's login name (8-64 characters)\n\tUsername string `protobuf:\"bytes,1,opt,name=username,proto3\" json:\"username,omitempty\"`\n\t// password is the user's password (8-128 characters)\n\tPassword      string `protobuf:\"bytes,2,opt,name=password,proto3\" json:\"password,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Request) Reset() {\n\t*x = Request{}\n\tmi := &file_auth_auth_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Request) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Request) ProtoMessage() {}\n\nfunc (x *Request) ProtoReflect() protoreflect.Message {\n\tmi := &file_auth_auth_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Request.ProtoReflect.Descriptor instead.\nfunc (*Request) Descriptor() ([]byte, []int) {\n\treturn file_auth_auth_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Request) GetUsername() string {\n\tif x != nil {\n\t\treturn x.Username\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetPassword() string {\n\tif x != nil {\n\t\treturn x.Password\n\t}\n\treturn \"\"\n}\n\n// Response contains the authentication result.\ntype Response struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// token is a signed JWT token valid for 1 hour.\n\t// The token should be included in the \"authorization\" metadata\n\t// header for authenticated requests to other services.\n\tToken         string `protobuf:\"bytes,1,opt,name=token,proto3\" json:\"token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Response) Reset() {\n\t*x = Response{}\n\tmi := &file_auth_auth_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Response) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Response) ProtoMessage() {}\n\nfunc (x *Response) ProtoReflect() protoreflect.Message {\n\tmi := &file_auth_auth_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Response.ProtoReflect.Descriptor instead.\nfunc (*Response) Descriptor() ([]byte, []int) {\n\treturn file_auth_auth_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Response) GetToken() string {\n\tif x != nil {\n\t\treturn x.Token\n\t}\n\treturn \"\"\n}\n\nvar File_auth_auth_proto protoreflect.FileDescriptor\n\nconst file_auth_auth_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x0fauth/auth.proto\\x12\\x04auth\\\"A\\n\" +\n\t\"\\aRequest\\x12\\x1a\\n\" +\n\t\"\\busername\\x18\\x01 \\x01(\\tR\\busername\\x12\\x1a\\n\" +\n\t\"\\bpassword\\x18\\x02 \\x01(\\tR\\bpassword\\\" \\n\" +\n\t\"\\bResponse\\x12\\x14\\n\" +\n\t\"\\x05token\\x18\\x01 \\x01(\\tR\\x05token2.\\n\" +\n\t\"\\x04Auth\\x12&\\n\" +\n\t\"\\x05Login\\x12\\r.auth.Request\\x1a\\x0e.auth.ResponseB/Z-github.com/enricofoltran/hello-auth-grpc/authb\\x06proto3\"\n\nvar (\n\tfile_auth_auth_proto_rawDescOnce sync.Once\n\tfile_auth_auth_proto_rawDescData []byte\n)\n\nfunc file_auth_auth_proto_rawDescGZIP() []byte {\n\tfile_auth_auth_proto_rawDescOnce.Do(func() {\n\t\tfile_auth_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_auth_auth_proto_rawDesc), len(file_auth_auth_proto_rawDesc)))\n\t})\n\treturn file_auth_auth_proto_rawDescData\n}\n\nvar file_auth_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_auth_auth_proto_goTypes = []any{\n\t(*Request)(nil),  // 0: auth.Request\n\t(*Response)(nil), // 1: auth.Response\n}\nvar file_auth_auth_proto_depIdxs = []int32{\n\t0, // 0: auth.Auth.Login:input_type -> auth.Request\n\t1, // 1: auth.Auth.Login:output_type -> auth.Response\n\t1, // [1:2] is the sub-list for method output_type\n\t0, // [0:1] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_auth_auth_proto_init() }\nfunc file_auth_auth_proto_init() {\n\tif File_auth_auth_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_auth_auth_proto_rawDesc), len(file_auth_auth_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_auth_auth_proto_goTypes,\n\t\tDependencyIndexes: file_auth_auth_proto_depIdxs,\n\t\tMessageInfos:      file_auth_auth_proto_msgTypes,\n\t}.Build()\n\tFile_auth_auth_proto = out.File\n\tfile_auth_auth_proto_goTypes = nil\n\tfile_auth_auth_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "auth/auth.proto",
    "content": "syntax = \"proto3\";\n\npackage auth;\n\noption go_package = \"github.com/enricofoltran/hello-auth-grpc/auth\";\n\n// Auth service provides user authentication and JWT token issuance.\n// This service validates user credentials and returns signed JWT tokens\n// that can be used to authenticate requests to other services.\nservice Auth {\n    // Login authenticates a user with username and password.\n    // On successful authentication, returns a JWT token valid for 1 hour.\n    // Rate limited to prevent brute force attacks.\n    rpc Login (Request) returns (Response);\n}\n\n// Request contains user credentials for authentication.\nmessage Request {\n    // username is the user's login name (8-64 characters)\n    string username = 1;\n\n    // password is the user's password (8-128 characters)\n    string password = 2;\n}\n\n// Response contains the authentication result.\nmessage Response {\n    // token is a signed JWT token valid for 1 hour.\n    // The token should be included in the \"authorization\" metadata\n    // header for authenticated requests to other services.\n    string token = 1;\n}\n"
  },
  {
    "path": "auth/auth_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.5.1\n// - protoc             v6.30.2\n// source: auth/auth.proto\n\npackage auth\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tAuth_Login_FullMethodName = \"/auth.Auth/Login\"\n)\n\n// AuthClient is the client API for Auth service.\n//\n// 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.\n//\n// Auth service provides user authentication and JWT token issuance.\n// This service validates user credentials and returns signed JWT tokens\n// that can be used to authenticate requests to other services.\ntype AuthClient interface {\n\t// Login authenticates a user with username and password.\n\t// On successful authentication, returns a JWT token valid for 1 hour.\n\t// Rate limited to prevent brute force attacks.\n\tLogin(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)\n}\n\ntype authClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewAuthClient(cc grpc.ClientConnInterface) AuthClient {\n\treturn &authClient{cc}\n}\n\nfunc (c *authClient) Login(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Response)\n\terr := c.cc.Invoke(ctx, Auth_Login_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// AuthServer is the server API for Auth service.\n// All implementations must embed UnimplementedAuthServer\n// for forward compatibility.\n//\n// Auth service provides user authentication and JWT token issuance.\n// This service validates user credentials and returns signed JWT tokens\n// that can be used to authenticate requests to other services.\ntype AuthServer interface {\n\t// Login authenticates a user with username and password.\n\t// On successful authentication, returns a JWT token valid for 1 hour.\n\t// Rate limited to prevent brute force attacks.\n\tLogin(context.Context, *Request) (*Response, error)\n\tmustEmbedUnimplementedAuthServer()\n}\n\n// UnimplementedAuthServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedAuthServer struct{}\n\nfunc (UnimplementedAuthServer) Login(context.Context, *Request) (*Response, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Login not implemented\")\n}\nfunc (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {}\nfunc (UnimplementedAuthServer) testEmbeddedByValue()              {}\n\n// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to AuthServer will\n// result in compilation errors.\ntype UnsafeAuthServer interface {\n\tmustEmbedUnimplementedAuthServer()\n}\n\nfunc RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) {\n\t// If the following call pancis, it indicates UnimplementedAuthServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&Auth_ServiceDesc, srv)\n}\n\nfunc _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(Request)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AuthServer).Login(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Auth_Login_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AuthServer).Login(ctx, req.(*Request))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Auth_ServiceDesc is the grpc.ServiceDesc for Auth service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Auth_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"auth.Auth\",\n\tHandlerType: (*AuthServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Login\",\n\t\t\tHandler:    _Auth_Login_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"auth/auth.proto\",\n}\n"
  },
  {
    "path": "certs/auth-csr.json",
    "content": "{\n    \"CN\": \"localhost\",\n    \"hosts\": [\n        \"localhost\",\n        \"127.0.0.1\"\n    ],\n    \"key\": {\n        \"algo\": \"rsa\",\n        \"size\": 2048\n    },\n    \"names\": [\n        {\n            \"C\": \"US\",\n            \"L\": \"CA\",\n            \"ST\": \"San Francisco\",\n            \"O\": \"My Own Company\",\n            \"OU\": \"gRPC\"\n        }\n    ]\n}\n\n"
  },
  {
    "path": "certs/ca-config.json",
    "content": "{\n    \"signing\": {\n        \"default\": {\n            \"expiry\": \"168h\"\n        },\n        \"profiles\": {\n            \"server\": {\n                \"expiry\": \"8760h\",\n                \"usages\": [\n                    \"signing\",\n                    \"key encipherment\",\n                    \"server auth\"\n                ]\n            },\n            \"client\": {\n                \"expiry\": \"8760h\",\n                \"usages\": [\n                    \"signing\",\n                    \"key encipherment\",\n                    \"client auth\"\n                ]\n            },\n            \"signing\": {\n                \"expiry\": \"8760h\",\n                \"usages\": [\n                    \"signing\",\n                    \"key encipherment\"\n                ]\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "certs/ca-csr.json",
    "content": "{\n    \"CN\": \"gRPC CA\",\n    \"key\": {\n        \"algo\": \"rsa\",\n        \"size\": 2048\n    },\n    \"names\": [\n        {\n            \"C\": \"US\",\n            \"L\": \"CA\",\n            \"ST\": \"San Francisco\",\n            \"O\": \"My Own Company\",\n            \"OU\": \"gRPC\"\n        }\n    ]\n}\n\n"
  },
  {
    "path": "certs/client-csr.json",
    "content": "{\n    \"CN\": \"client\",\n    \"hosts\": [\"\"],\n    \"key\": {\n        \"algo\": \"rsa\",\n        \"size\": 2048\n    },\n    \"names\": [\n        {\n            \"C\": \"US\",\n            \"L\": \"CA\",\n            \"ST\": \"San Francisco\",\n            \"O\": \"My Own Company\",\n            \"OU\": \"gRPC\"\n        }\n    ]\n}\n\n"
  },
  {
    "path": "certs/hello-csr.json",
    "content": "{\n    \"CN\": \"localhost\",\n    \"hosts\": [\n        \"localhost\",\n        \"127.0.0.1\"\n    ],\n    \"key\": {\n        \"algo\": \"rsa\",\n        \"size\": 2048\n    },\n    \"names\": [\n        {\n            \"C\": \"US\",\n            \"L\": \"CA\",\n            \"ST\": \"San Francisco\",\n            \"O\": \"My Own Company\",\n            \"OU\": \"gRPC\"\n        }\n    ]\n}\n\n"
  },
  {
    "path": "certs/jwt-csr.json",
    "content": "{\n    \"CN\": \"client\",\n    \"hosts\": [\"\"],\n    \"key\": {\n        \"algo\": \"rsa\",\n        \"size\": 2048\n    },\n    \"names\": [\n        {\n            \"C\": \"US\",\n            \"L\": \"CA\",\n            \"ST\": \"San Francisco\",\n            \"O\": \"My Own Company\",\n            \"OU\": \"JWT\"\n        }\n    ]\n}\n\n"
  },
  {
    "path": "cmd/auth-client/main.go",
    "content": "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/enricofoltran/hello-auth-grpc/auth\"\n\t\"github.com/enricofoltran/hello-auth-grpc/pkg/config\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n)\n\nconst (\n\t// RequestTimeout is the maximum time to wait for a request\n\tRequestTimeout = 10 * time.Second\n)\n\nfunc main() {\n\t// Define flags\n\tserverAddr := flag.String(\"server-addr\", \"127.0.0.1:20000\", \"remote auth server address\")\n\ttlsCrt := flag.String(\"tls-crt\", config.WithConfigDir(\"client.pem\"), \"client certificate file\")\n\ttlsKey := flag.String(\"tls-key\", config.WithConfigDir(\"client-key.pem\"), \"client private key file\")\n\tcaCrt := flag.String(\"ca-crt\", config.WithConfigDir(\"ca.pem\"), \"CA certificate file\")\n\tjwtToken := flag.String(\"jwt-token\", config.WithConfigDir(\".token\"), \"the jwt auth token file\")\n\tflag.Parse()\n\n\tlogger := log.New(os.Stderr, \"auth-client: \", log.LstdFlags)\n\n\t// Get credentials from environment variables (more secure than CLI flags)\n\tusername := os.Getenv(\"AUTH_USERNAME\")\n\tpassword := os.Getenv(\"AUTH_PASSWORD\")\n\n\tif username == \"\" || password == \"\" {\n\t\tlogger.Fatalln(\"please provide AUTH_USERNAME and AUTH_PASSWORD environment variables\")\n\t}\n\n\t// Load TLS certificate and key\n\tcrt, err := tls.LoadX509KeyPair(*tlsCrt, *tlsKey)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load client key pair from file: %v\", err)\n\t}\n\n\t// Load CA certificate\n\trawCaCrt, err := os.ReadFile(*caCrt)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load CA certificate from file: %v\", err)\n\t}\n\n\tcaCrtPool := x509.NewCertPool()\n\tif ok := caCrtPool.AppendCertsFromPEM(rawCaCrt); !ok {\n\t\tlogger.Fatalf(\"could not append CA certificate to cert pool\")\n\t}\n\n\t// Hardened TLS configuration\n\ttlsConfig := &tls.Config{\n\t\tCertificates: []tls.Certificate{crt},\n\t\tRootCAs:      caCrtPool,\n\t\tMinVersion:   tls.VersionTLS12,\n\t}\n\n\ttlsCreds := credentials.NewTLS(tlsConfig)\n\n\t// Connect with dial options\n\tconn, err := grpc.NewClient(\n\t\t*serverAddr,\n\t\tgrpc.WithTransportCredentials(tlsCreds),\n\t)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not create client: %v\", err)\n\t}\n\tdefer conn.Close()\n\n\tclt := pb.NewAuthClient(conn)\n\n\t// Use context with timeout\n\tctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)\n\tdefer cancel()\n\n\treq := &pb.Request{Username: username, Password: password}\n\tres, err := clt.Login(ctx, req)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not login: %v\", err)\n\t}\n\n\t// Save token with restricted permissions (0600 = owner read/write only)\n\terr = os.WriteFile(*jwtToken, []byte(res.Token), 0600)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not save auth token to disk: %v\", err)\n\t}\n\n\tlogger.Println(\"login succeeded!\")\n}\n"
  },
  {
    "path": "cmd/auth-server/main.go",
    "content": "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/enricofoltran/hello-auth-grpc/auth\"\n\t\"github.com/enricofoltran/hello-auth-grpc/pkg/config\"\n\t\"github.com/enricofoltran/hello-auth-grpc/pkg/logging\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/health\"\n\thealthpb \"google.golang.org/grpc/health/grpc_health_v1\"\n)\n\nfunc main() {\n\t// Define flags\n\tlistenAddr := flag.String(\"listen-addr\", \"127.0.0.1:20000\", \"auth server listen address\")\n\tjwtKey := flag.String(\"jwt-key\", config.WithConfigDir(\"jwt-key.pem\"), \"the private key to use for signing JWT tokens\")\n\ttlsCrt := flag.String(\"tls-crt\", config.WithConfigDir(\"auth.pem\"), \"auth server certificate file\")\n\ttlsKey := flag.String(\"tls-key\", config.WithConfigDir(\"auth-key.pem\"), \"auth server private key file\")\n\tcaCrt := flag.String(\"ca-crt\", config.WithConfigDir(\"ca.pem\"), \"CA certificate file\")\n\tflag.Parse()\n\n\tlogger := log.New(os.Stderr, \"auth: \", log.LstdFlags)\n\tlogger.Printf(\"server is starting...\")\n\n\t// Get credentials from environment variables (more secure than CLI flags)\n\tusername := os.Getenv(\"AUTH_USERNAME\")\n\tpassword := os.Getenv(\"AUTH_PASSWORD\")\n\n\tif username == \"\" || password == \"\" {\n\t\tlogger.Fatalln(\"please provide AUTH_USERNAME and AUTH_PASSWORD environment variables\")\n\t}\n\n\t// Load TLS certificate and key\n\tcrt, err := tls.LoadX509KeyPair(*tlsCrt, *tlsKey)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load server key pair from file: %v\", err)\n\t}\n\n\t// Load CA certificate\n\trawCaCrt, err := os.ReadFile(*caCrt)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load CA certificate from file: %v\", err)\n\t}\n\n\tcaCrtPool := x509.NewCertPool()\n\tif ok := caCrtPool.AppendCertsFromPEM(rawCaCrt); !ok {\n\t\t// Fixed: Don't reference wrong error variable\n\t\tlogger.Fatalf(\"could not append CA certificate to cert pool\")\n\t}\n\n\t// Hardened TLS configuration\n\ttlsConfig := &tls.Config{\n\t\tCertificates: []tls.Certificate{crt},\n\t\tClientAuth:   tls.RequireAndVerifyClientCert,\n\t\tClientCAs:    caCrtPool,\n\t\tMinVersion:   tls.VersionTLS12, // Enforce minimum TLS 1.2\n\t\tCipherSuites: []uint16{\n\t\t\t// Strong cipher suites only\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n\t\t},\n\t\tPreferServerCipherSuites: true,\n\t}\n\n\ttlsCreds := credentials.NewTLS(tlsConfig)\n\n\t// Create listener\n\tln, err := net.Listen(\"tcp\", *listenAddr)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not listen: %v\", err)\n\t}\n\n\t// Create gRPC server with interceptors for logging and panic recovery\n\tgrpcServer := grpc.NewServer(\n\t\tgrpc.Creds(tlsCreds),\n\t\tgrpc.ChainUnaryInterceptor(\n\t\t\tlogging.PanicRecoveryInterceptor(logger),\n\t\t\tlogging.UnaryServerInterceptor(logger),\n\t\t),\n\t)\n\n\t// Create and register auth server\n\tauthServer, err := NewAuthServer(*jwtKey, username, password)\n\tif err != nil {\n\t\tlogger.Fatalf(\"%v\", err)\n\t}\n\n\tpb.RegisterAuthServer(grpcServer, authServer)\n\n\t// Register health check service\n\thealthServer := health.NewServer()\n\thealthpb.RegisterHealthServer(grpcServer, healthServer)\n\thealthServer.SetServingStatus(\"auth.Auth\", healthpb.HealthCheckResponse_SERVING)\n\n\t// Set up graceful shutdown\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n\n\tgo func() {\n\t\t<-sigChan\n\t\tlogger.Println(\"shutting down gracefully...\")\n\t\tgrpcServer.GracefulStop()\n\t}()\n\n\tlogger.Printf(\"server is listening on %s...\", *listenAddr)\n\tif err := grpcServer.Serve(ln); err != nil {\n\t\tlogger.Fatalf(\"failed to serve: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "cmd/auth-server/server.go",
    "content": "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/hello-auth-grpc/auth\"\n\t\"github.com/golang-jwt/jwt/v5\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"golang.org/x/time/rate\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/peer\"\n\t\"google.golang.org/grpc/status\"\n)\n\nconst (\n\t// MaxUsernameLength is the maximum allowed username length\n\tMaxUsernameLength = 64\n\t// MaxPasswordLength is the maximum allowed password length\n\tMaxPasswordLength = 128\n\t// MinPasswordLength is the minimum required password length\n\tMinPasswordLength = 8\n\t// TokenExpiration is the JWT token validity duration (1 hour)\n\tTokenExpiration = time.Hour\n\t// RateLimitBurst allows burst of login attempts\n\tRateLimitBurst = 3\n\t// RateLimitPerSecond limits login attempts per second per IP\n\tRateLimitPerSecond = 0.5 // 1 request every 2 seconds\n)\n\n// server implements the Auth service with security enhancements.\ntype server struct {\n\tpb.UnimplementedAuthServer\n\tjwtKey         *rsa.PrivateKey\n\tpasswordHash   []byte // bcrypt hash of the password\n\tusername       string\n\trateLimiters   map[string]*rate.Limiter\n\trateLimitersMu sync.RWMutex\n}\n\n// NewAuthServer creates a new auth server instance with bcrypt password hashing.\n// The password parameter should be the plaintext password which will be hashed.\nfunc NewAuthServer(jwtKeyPath, username, password string) (*server, error) {\n\t// Validate inputs\n\tif err := validateUsername(username); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid username: %w\", err)\n\t}\n\n\tif err := validatePassword(password); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid password: %w\", err)\n\t}\n\n\t// Load JWT private key\n\trawJwtKey, err := os.ReadFile(jwtKeyPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not load jwt private key from file: %w\", err)\n\t}\n\n\tparsedJwtKey, err := jwt.ParseRSAPrivateKeyFromPEM(rawJwtKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not parse jwt private key: %w\", err)\n\t}\n\n\t// Hash the password using bcrypt\n\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not hash password: %w\", err)\n\t}\n\n\treturn &server{\n\t\tjwtKey:       parsedJwtKey,\n\t\tusername:     username,\n\t\tpasswordHash: passwordHash,\n\t\trateLimiters: make(map[string]*rate.Limiter),\n\t}, nil\n}\n\n// validateUsername checks if the username meets requirements.\nfunc validateUsername(username string) error {\n\tusername = strings.TrimSpace(username)\n\tif username == \"\" {\n\t\treturn fmt.Errorf(\"username cannot be empty\")\n\t}\n\tif len(username) > MaxUsernameLength {\n\t\treturn fmt.Errorf(\"username exceeds maximum length of %d\", MaxUsernameLength)\n\t}\n\treturn nil\n}\n\n// validatePassword checks if the password meets requirements.\nfunc validatePassword(password string) error {\n\tif len(password) < MinPasswordLength {\n\t\treturn fmt.Errorf(\"password must be at least %d characters\", MinPasswordLength)\n\t}\n\tif len(password) > MaxPasswordLength {\n\t\treturn fmt.Errorf(\"password exceeds maximum length of %d\", MaxPasswordLength)\n\t}\n\treturn nil\n}\n\n// getRateLimiter returns a rate limiter for the given client address.\nfunc (s *server) getRateLimiter(clientAddr string) *rate.Limiter {\n\ts.rateLimitersMu.Lock()\n\tdefer s.rateLimitersMu.Unlock()\n\n\tlimiter, exists := s.rateLimiters[clientAddr]\n\tif !exists {\n\t\tlimiter = rate.NewLimiter(RateLimitPerSecond, RateLimitBurst)\n\t\ts.rateLimiters[clientAddr] = limiter\n\t}\n\n\treturn limiter\n}\n\n// Login authenticates a user and returns a JWT token.\n// Implements rate limiting, input validation, and secure password verification.\nfunc (s *server) Login(ctx context.Context, r *pb.Request) (*pb.Response, error) {\n\t// Get client address for rate limiting\n\tclientAddr := \"unknown\"\n\tif p, ok := peer.FromContext(ctx); ok {\n\t\tclientAddr = p.Addr.String()\n\t}\n\n\t// Apply rate limiting per client IP\n\tlimiter := s.getRateLimiter(clientAddr)\n\tif !limiter.Allow() {\n\t\treturn nil, status.Error(codes.ResourceExhausted, \"too many login attempts, please try again later\")\n\t}\n\n\t// Validate input\n\tif err := validateUsername(r.Username); err != nil {\n\t\treturn nil, status.Error(codes.InvalidArgument, \"invalid username format\")\n\t}\n\n\tif err := validatePassword(r.Password); err != nil {\n\t\treturn nil, status.Error(codes.InvalidArgument, \"invalid password format\")\n\t}\n\n\t// Verify username\n\tif r.Username != s.username {\n\t\t// Use constant-time comparison pattern (check password even if username wrong)\n\t\t// to prevent timing attacks\n\t\tbcrypt.CompareHashAndPassword(s.passwordHash, []byte(r.Password))\n\t\treturn nil, status.Error(codes.PermissionDenied, \"invalid credentials\")\n\t}\n\n\t// Verify password using bcrypt\n\tif err := bcrypt.CompareHashAndPassword(s.passwordHash, []byte(r.Password)); err != nil {\n\t\treturn nil, status.Error(codes.PermissionDenied, \"invalid credentials\")\n\t}\n\n\t// Generate JWT token\n\tnow := time.Now()\n\texp := now.Add(TokenExpiration)\n\n\tclaims := jwt.MapClaims{\n\t\t\"sub\": r.Username,\n\t\t\"aud\": \"hello.service\",\n\t\t\"iss\": \"auth.service\",\n\t\t\"exp\": exp.Unix(),\n\t\t\"iat\": now.Unix(),\n\t\t\"nbf\": now.Unix(),\n\t}\n\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n\n\ttokenStr, err := token.SignedString(s.jwtKey)\n\tif err != nil {\n\t\t// Don't leak internal error details to client\n\t\treturn nil, status.Error(codes.Internal, \"failed to generate token\")\n\t}\n\n\treturn &pb.Response{Token: tokenStr}, nil\n}\n"
  },
  {
    "path": "cmd/auth-server/server_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestValidateUsername(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tusername string\n\t\twantErr  bool\n\t}{\n\t\t{\"valid username\", \"testuser\", false},\n\t\t{\"empty username\", \"\", true},\n\t\t{\"whitespace only\", \"   \", true},\n\t\t{\"too long\", string(make([]byte, 65)), true},\n\t\t{\"max length\", string(make([]byte, 64)), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateUsername(tt.username)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"validateUsername() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidatePassword(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpassword string\n\t\twantErr  bool\n\t}{\n\t\t{\"valid password\", \"password123\", false},\n\t\t{\"too short\", \"pass\", true},\n\t\t{\"minimum length\", \"12345678\", false},\n\t\t{\"too long\", string(make([]byte, 129)), true},\n\t\t{\"max length\", string(make([]byte, 128)), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validatePassword(tt.password)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"validatePassword() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLogin_InvalidCredentials(t *testing.T) {\n\t// Note: This test requires a temporary RSA key for testing\n\t// In a real scenario, you'd generate a test key or use a fixture\n\tt.Skip(\"Requires RSA key setup - integration test\")\n}\n\nfunc TestLogin_ValidCredentials(t *testing.T) {\n\t// Note: This test requires a temporary RSA key and bcrypt setup\n\tt.Skip(\"Requires RSA key setup - integration test\")\n}\n\nfunc TestLogin_RateLimiting(t *testing.T) {\n\t// Note: This test would verify rate limiting works\n\tt.Skip(\"Requires full server setup - integration test\")\n}\n\nfunc TestJWTTokenExpiration(t *testing.T) {\n\t// Verify tokens expire in 1 hour\n\tnow := time.Now()\n\texp := now.Add(TokenExpiration)\n\n\tif exp.Sub(now) != time.Hour {\n\t\tt.Errorf(\"TokenExpiration should be 1 hour, got %v\", exp.Sub(now))\n\t}\n}\n\nfunc TestLogin_InputValidation(t *testing.T) {\n\tt.Skip(\"Requires full server setup - integration test\")\n\n\t// This test would verify:\n\t// - Empty username returns InvalidArgument\n\t// - Empty password returns InvalidArgument\n\t// - Too long username returns InvalidArgument\n\t// - Too short password returns InvalidArgument\n}\n\n// Helper function to create a test server\n// func newTestServer(t *testing.T) *server {\n// \t// Generate test RSA key\n// \tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n// \tif err != nil {\n// \t\tt.Fatalf(\"Failed to generate RSA key: %v\", err)\n// \t}\n//\n// \t// Hash test password\n// \tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(\"testpass123\"), bcrypt.DefaultCost)\n// \tif err != nil {\n// \t\tt.Fatalf(\"Failed to hash password: %v\", err)\n// \t}\n//\n// \treturn &server{\n// \t\tjwtKey:       privateKey,\n// \t\tusername:     \"testuser\",\n// \t\tpasswordHash: passwordHash,\n// \t\trateLimiters: make(map[string]*rate.Limiter),\n// \t}\n// }\n"
  },
  {
    "path": "cmd/hello-client/main.go",
    "content": "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/hello-auth-grpc/credentials/jwt\"\n\tpb \"github.com/enricofoltran/hello-auth-grpc/hello\"\n\t\"github.com/enricofoltran/hello-auth-grpc/pkg/config\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n)\n\nconst (\n\t// RequestTimeout is the maximum time to wait for a request\n\tRequestTimeout = 10 * time.Second\n)\n\nfunc main() {\n\t// Define flags\n\tserverAddr := flag.String(\"server-addr\", \"127.0.0.1:10000\", \"remote hello server address\")\n\ttlsCrt := flag.String(\"tls-crt\", config.WithConfigDir(\"client.pem\"), \"client certificate file\")\n\ttlsKey := flag.String(\"tls-key\", config.WithConfigDir(\"client-key.pem\"), \"client private key file\")\n\tcaCrt := flag.String(\"ca-crt\", config.WithConfigDir(\"ca.pem\"), \"CA certificate file\")\n\tjwtToken := flag.String(\"jwt-token\", config.WithConfigDir(\".token\"), \"the jwt auth token file\")\n\tflag.Parse()\n\n\tlogger := log.New(os.Stderr, \"hello-client: \", log.LstdFlags)\n\n\t// Load TLS certificate and key\n\tcrt, err := tls.LoadX509KeyPair(*tlsCrt, *tlsKey)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load client key pair from file: %v\", err)\n\t}\n\n\t// Load CA certificate\n\trawCaCrt, err := os.ReadFile(*caCrt)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load CA certificate from file: %v\", err)\n\t}\n\n\tcaCrtPool := x509.NewCertPool()\n\tif ok := caCrtPool.AppendCertsFromPEM(rawCaCrt); !ok {\n\t\tlogger.Fatalf(\"could not append CA certificate to cert pool\")\n\t}\n\n\t// Hardened TLS configuration\n\ttlsConfig := &tls.Config{\n\t\tCertificates: []tls.Certificate{crt},\n\t\tRootCAs:      caCrtPool,\n\t\tMinVersion:   tls.VersionTLS12,\n\t}\n\n\ttlsCreds := credentials.NewTLS(tlsConfig)\n\n\t// Load JWT credentials\n\tjwtCreds, err := jwt.NewFromTokenFile(*jwtToken)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load jwt token from file: %v\", err)\n\t}\n\n\t// Connect with dial options\n\tconn, err := grpc.NewClient(\n\t\t*serverAddr,\n\t\tgrpc.WithTransportCredentials(tlsCreds),\n\t\tgrpc.WithPerRPCCredentials(jwtCreds),\n\t)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not create client: %v\", err)\n\t}\n\tdefer conn.Close()\n\n\tclt := pb.NewGreeterClient(conn)\n\n\t// Use context with timeout\n\tctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)\n\tdefer cancel()\n\n\treq := &pb.Request{}\n\tres, err := clt.SayHello(ctx, req)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not say hello: %v\", err)\n\t}\n\n\tlogger.Printf(\"remote server says: %s\", res.Message)\n}\n"
  },
  {
    "path": "cmd/hello-server/main.go",
    "content": "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/enricofoltran/hello-auth-grpc/hello\"\n\t\"github.com/enricofoltran/hello-auth-grpc/pkg/config\"\n\t\"github.com/enricofoltran/hello-auth-grpc/pkg/logging\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/health\"\n\thealthpb \"google.golang.org/grpc/health/grpc_health_v1\"\n)\n\nfunc main() {\n\t// Define flags\n\tlistenAddr := flag.String(\"listen-addr\", \"127.0.0.1:10000\", \"hello server listen address\")\n\tjwtKey := flag.String(\"jwt-key\", config.WithConfigDir(\"jwt.pem\"), \"the public key to use for validating JWT tokens\")\n\ttlsCrt := flag.String(\"tls-crt\", config.WithConfigDir(\"hello.pem\"), \"hello server certificate file\")\n\ttlsKey := flag.String(\"tls-key\", config.WithConfigDir(\"hello-key.pem\"), \"hello server private key file\")\n\tcaCrt := flag.String(\"ca-crt\", config.WithConfigDir(\"ca.pem\"), \"CA certificate file\")\n\tflag.Parse()\n\n\tlogger := log.New(os.Stderr, \"hello: \", log.LstdFlags)\n\tlogger.Printf(\"server is starting...\")\n\n\t// Load TLS certificate and key\n\tcrt, err := tls.LoadX509KeyPair(*tlsCrt, *tlsKey)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load server key pair from file: %v\", err)\n\t}\n\n\t// Load CA certificate\n\trawCaCrt, err := os.ReadFile(*caCrt)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not load CA certificate from file: %v\", err)\n\t}\n\n\tcaCrtPool := x509.NewCertPool()\n\tif ok := caCrtPool.AppendCertsFromPEM(rawCaCrt); !ok {\n\t\t// Fixed: Don't reference wrong error variable\n\t\tlogger.Fatalf(\"could not append CA certificate to cert pool\")\n\t}\n\n\t// Hardened TLS configuration\n\ttlsConfig := &tls.Config{\n\t\tCertificates: []tls.Certificate{crt},\n\t\tClientAuth:   tls.RequireAndVerifyClientCert,\n\t\tClientCAs:    caCrtPool,\n\t\tMinVersion:   tls.VersionTLS12, // Enforce minimum TLS 1.2\n\t\tCipherSuites: []uint16{\n\t\t\t// Strong cipher suites only\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n\t\t},\n\t\tPreferServerCipherSuites: true,\n\t}\n\n\ttlsCreds := credentials.NewTLS(tlsConfig)\n\n\t// Create listener\n\tln, err := net.Listen(\"tcp\", *listenAddr)\n\tif err != nil {\n\t\tlogger.Fatalf(\"could not listen: %v\", err)\n\t}\n\n\t// Create gRPC server with interceptors for logging and panic recovery\n\tgrpcServer := grpc.NewServer(\n\t\tgrpc.Creds(tlsCreds),\n\t\tgrpc.ChainUnaryInterceptor(\n\t\t\tlogging.PanicRecoveryInterceptor(logger),\n\t\t\tlogging.UnaryServerInterceptor(logger),\n\t\t),\n\t)\n\n\t// Create and register hello server\n\thelloServer, err := NewHelloServer(*jwtKey)\n\tif err != nil {\n\t\tlogger.Fatalf(\"%v\", err)\n\t}\n\n\tpb.RegisterGreeterServer(grpcServer, helloServer)\n\n\t// Register health check service\n\thealthServer := health.NewServer()\n\thealthpb.RegisterHealthServer(grpcServer, healthServer)\n\thealthServer.SetServingStatus(\"hello.Greeter\", healthpb.HealthCheckResponse_SERVING)\n\n\t// Set up graceful shutdown\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n\n\tgo func() {\n\t\t<-sigChan\n\t\tlogger.Println(\"shutting down gracefully...\")\n\t\tgrpcServer.GracefulStop()\n\t}()\n\n\tlogger.Printf(\"server is listening on %s...\", *listenAddr)\n\tif err := grpcServer.Serve(ln); err != nil {\n\t\tlogger.Fatalf(\"failed to serve: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "cmd/hello-server/server.go",
    "content": "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\"github.com/golang-jwt/jwt/v5\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n)\n\nconst (\n\t// ExpectedAudience is the expected audience claim for JWT tokens\n\tExpectedAudience = \"hello.service\"\n\t// ExpectedIssuer is the expected issuer claim for JWT tokens\n\tExpectedIssuer = \"auth.service\"\n)\n\n// server implements the Greeter service with JWT authentication.\ntype server struct {\n\tpb.UnimplementedGreeterServer\n\tjwtKey *rsa.PublicKey\n}\n\n// claims represents the JWT claims structure with validation.\ntype claims struct {\n\tjwt.RegisteredClaims\n}\n\n// validateJwtToken validates a JWT token with comprehensive claims checking.\nfunc validateJwtToken(tokenString string, key *rsa.PublicKey) (*jwt.Token, *claims, error) {\n\t// Parse and validate the token\n\tjwtToken, err := jwt.ParseWithClaims(tokenString, &claims{}, func(t *jwt.Token) (interface{}, error) {\n\t\t// Verify signing method\n\t\tif _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {\n\t\t\treturn nil, fmt.Errorf(\"unexpected signing method: %v\", t.Header[\"alg\"])\n\t\t}\n\t\treturn key, nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"token parsing failed: %w\", err)\n\t}\n\n\t// Extract and validate claims\n\tclaims, ok := jwtToken.Claims.(*claims)\n\tif !ok || !jwtToken.Valid {\n\t\treturn nil, nil, fmt.Errorf(\"invalid token claims\")\n\t}\n\n\t// Validate audience\n\taudiences, err := claims.GetAudience()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid audience claim: %w\", err)\n\t}\n\tvalidAudience := false\n\tfor _, aud := range audiences {\n\t\tif aud == ExpectedAudience {\n\t\t\tvalidAudience = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !validAudience {\n\t\treturn nil, nil, fmt.Errorf(\"invalid audience: expected %s\", ExpectedAudience)\n\t}\n\n\t// Validate issuer\n\tissuer, err := claims.GetIssuer()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid issuer claim: %w\", err)\n\t}\n\tif issuer != ExpectedIssuer {\n\t\treturn nil, nil, fmt.Errorf(\"invalid issuer: expected %s, got %s\", ExpectedIssuer, issuer)\n\t}\n\n\t// Validate expiration (jwt library does this automatically, but we check explicitly)\n\tif exp, err := claims.GetExpirationTime(); err != nil || exp == nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid expiration claim\")\n\t}\n\n\t// Validate not before (jwt library does this automatically, but we check explicitly)\n\tif nbf, err := claims.GetNotBefore(); err != nil || nbf == nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid not-before claim\")\n\t}\n\n\treturn jwtToken, claims, nil\n}\n\n// NewHelloServer creates a new hello server instance.\nfunc NewHelloServer(jwtKeyPath string) (*server, error) {\n\trawJwtKey, err := os.ReadFile(jwtKeyPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not load jwt public key from file: %w\", err)\n\t}\n\n\tparsedJwtKey, err := jwt.ParseRSAPublicKeyFromPEM(rawJwtKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not parse jwt public key: %w\", err)\n\t}\n\n\treturn &server{jwtKey: parsedJwtKey}, nil\n}\n\n// SayHello implements the Greeter service with JWT authentication.\nfunc (s *server) SayHello(ctx context.Context, r *pb.Request) (*pb.Response, error) {\n\t// Extract metadata from context\n\tmd, ok := metadata.FromIncomingContext(ctx)\n\tif !ok {\n\t\treturn nil, status.Error(codes.Unauthenticated, \"missing authentication metadata\")\n\t}\n\n\t// Get authorization token\n\tjwtTokens, ok := md[\"authorization\"]\n\tif !ok || len(jwtTokens) == 0 {\n\t\treturn nil, status.Error(codes.Unauthenticated, \"missing authorization token\")\n\t}\n\n\t// Validate the JWT token\n\t_, claims, err := validateJwtToken(jwtTokens[0], s.jwtKey)\n\tif err != nil {\n\t\t// Don't leak validation error details to client\n\t\treturn nil, status.Error(codes.Unauthenticated, \"invalid authentication token\")\n\t}\n\n\t// Extract subject (username) from claims\n\tsubject, err := claims.GetSubject()\n\tif err != nil || subject == \"\" {\n\t\treturn nil, status.Error(codes.Unauthenticated, \"invalid token subject\")\n\t}\n\n\treturn &pb.Response{Message: \"Hello, \" + subject + \"!\"}, nil\n}\n"
  },
  {
    "path": "credentials/jwt/jwt.go",
    "content": "// Package jwt provides gRPC per-RPC credentials for JWT token authentication.\npackage jwt\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"google.golang.org/grpc/credentials\"\n)\n\n// jwt implements the credentials.PerRPCCredentials interface for JWT tokens.\ntype jwt struct {\n\ttoken string\n}\n\n// NewFromTokenFile creates a JWT credential from a token file.\n// The token file should contain a valid JWT token string.\nfunc NewFromTokenFile(tokenPath string) (credentials.PerRPCCredentials, error) {\n\tdata, err := os.ReadFile(tokenPath)\n\tif err != nil {\n\t\treturn jwt{}, fmt.Errorf(\"could not read token file: %w\", err)\n\t}\n\n\tif len(data) == 0 {\n\t\treturn jwt{}, fmt.Errorf(\"token cannot be empty\")\n\t}\n\n\treturn jwt{string(data)}, nil\n}\n\n// GetRequestMetadata implements credentials.PerRPCCredentials.\n// It adds the JWT token to the request metadata under the \"authorization\" key.\nfunc (j jwt) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {\n\treturn map[string]string{\n\t\t\"authorization\": j.token,\n\t}, nil\n}\n\n// RequireTransportSecurity implements credentials.PerRPCCredentials.\n// It returns true to enforce that JWT tokens are only sent over secure connections.\nfunc (j jwt) RequireTransportSecurity() bool {\n\treturn true\n}\n"
  },
  {
    "path": "go.mod",
    "content": "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/golang/protobuf v1.5.4\n\tgolang.org/x/crypto v0.44.0\n\tgolang.org/x/net v0.46.0\n\tgolang.org/x/time v0.14.0\n\tgoogle.golang.org/grpc v1.76.0\n)\n\nrequire (\n\tgolang.org/x/sys v0.38.0 // indirect\n\tgolang.org/x/text v0.31.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=\ngithub.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=\ngo.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=\ngo.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=\ngo.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=\ngo.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=\ngo.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=\ngo.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=\ngo.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=\ngo.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=\ngo.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=\ngolang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=\ngolang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=\ngolang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=\ngolang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=\ngolang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=\ngoogle.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=\ngoogle.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\n"
  },
  {
    "path": "hello/hello.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.30.2\n// source: hello/hello.proto\n\npackage hello\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\n// Request for SayHello RPC.\n// Currently empty as the username is extracted from the JWT token.\ntype Request struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Request) Reset() {\n\t*x = Request{}\n\tmi := &file_hello_hello_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Request) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Request) ProtoMessage() {}\n\nfunc (x *Request) ProtoReflect() protoreflect.Message {\n\tmi := &file_hello_hello_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Request.ProtoReflect.Descriptor instead.\nfunc (*Request) Descriptor() ([]byte, []int) {\n\treturn file_hello_hello_proto_rawDescGZIP(), []int{0}\n}\n\n// Response contains the greeting message.\ntype Response struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// message is a personalized greeting for the authenticated user\n\tMessage       string `protobuf:\"bytes,1,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Response) Reset() {\n\t*x = Response{}\n\tmi := &file_hello_hello_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Response) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Response) ProtoMessage() {}\n\nfunc (x *Response) ProtoReflect() protoreflect.Message {\n\tmi := &file_hello_hello_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Response.ProtoReflect.Descriptor instead.\nfunc (*Response) Descriptor() ([]byte, []int) {\n\treturn file_hello_hello_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Response) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nvar File_hello_hello_proto protoreflect.FileDescriptor\n\nconst file_hello_hello_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x11hello/hello.proto\\x12\\x05hello\\\"\\t\\n\" +\n\t\"\\aRequest\\\"$\\n\" +\n\t\"\\bResponse\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\x01 \\x01(\\tR\\amessage26\\n\" +\n\t\"\\aGreeter\\x12+\\n\" +\n\t\"\\bSayHello\\x12\\x0e.hello.Request\\x1a\\x0f.hello.ResponseB0Z.github.com/enricofoltran/hello-auth-grpc/hellob\\x06proto3\"\n\nvar (\n\tfile_hello_hello_proto_rawDescOnce sync.Once\n\tfile_hello_hello_proto_rawDescData []byte\n)\n\nfunc file_hello_hello_proto_rawDescGZIP() []byte {\n\tfile_hello_hello_proto_rawDescOnce.Do(func() {\n\t\tfile_hello_hello_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_hello_hello_proto_rawDesc), len(file_hello_hello_proto_rawDesc)))\n\t})\n\treturn file_hello_hello_proto_rawDescData\n}\n\nvar file_hello_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_hello_hello_proto_goTypes = []any{\n\t(*Request)(nil),  // 0: hello.Request\n\t(*Response)(nil), // 1: hello.Response\n}\nvar file_hello_hello_proto_depIdxs = []int32{\n\t0, // 0: hello.Greeter.SayHello:input_type -> hello.Request\n\t1, // 1: hello.Greeter.SayHello:output_type -> hello.Response\n\t1, // [1:2] is the sub-list for method output_type\n\t0, // [0:1] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_hello_hello_proto_init() }\nfunc file_hello_hello_proto_init() {\n\tif File_hello_hello_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_hello_hello_proto_rawDesc), len(file_hello_hello_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_hello_hello_proto_goTypes,\n\t\tDependencyIndexes: file_hello_hello_proto_depIdxs,\n\t\tMessageInfos:      file_hello_hello_proto_msgTypes,\n\t}.Build()\n\tFile_hello_hello_proto = out.File\n\tfile_hello_hello_proto_goTypes = nil\n\tfile_hello_hello_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "hello/hello.proto",
    "content": "syntax = \"proto3\";\n\npackage hello;\n\noption go_package = \"github.com/enricofoltran/hello-auth-grpc/hello\";\n\n// Greeter service provides a simple greeting functionality.\n// All RPC methods require JWT authentication via the \"authorization\"\n// metadata header. The JWT token must be obtained from the Auth service.\nservice Greeter {\n    // SayHello returns a personalized greeting message.\n    // Requires valid JWT token in request metadata.\n    rpc SayHello (Request) returns (Response);\n}\n\n// Request for SayHello RPC.\n// Currently empty as the username is extracted from the JWT token.\nmessage Request {}\n\n// Response contains the greeting message.\nmessage Response {\n    // message is a personalized greeting for the authenticated user\n    string message = 1;\n}\n"
  },
  {
    "path": "hello/hello_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.5.1\n// - protoc             v6.30.2\n// source: hello/hello.proto\n\npackage hello\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tGreeter_SayHello_FullMethodName = \"/hello.Greeter/SayHello\"\n)\n\n// GreeterClient is the client API for Greeter service.\n//\n// 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.\n//\n// Greeter service provides a simple greeting functionality.\n// All RPC methods require JWT authentication via the \"authorization\"\n// metadata header. The JWT token must be obtained from the Auth service.\ntype GreeterClient interface {\n\t// SayHello returns a personalized greeting message.\n\t// Requires valid JWT token in request metadata.\n\tSayHello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)\n}\n\ntype greeterClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {\n\treturn &greeterClient{cc}\n}\n\nfunc (c *greeterClient) SayHello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Response)\n\terr := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// GreeterServer is the server API for Greeter service.\n// All implementations must embed UnimplementedGreeterServer\n// for forward compatibility.\n//\n// Greeter service provides a simple greeting functionality.\n// All RPC methods require JWT authentication via the \"authorization\"\n// metadata header. The JWT token must be obtained from the Auth service.\ntype GreeterServer interface {\n\t// SayHello returns a personalized greeting message.\n\t// Requires valid JWT token in request metadata.\n\tSayHello(context.Context, *Request) (*Response, error)\n\tmustEmbedUnimplementedGreeterServer()\n}\n\n// UnimplementedGreeterServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedGreeterServer struct{}\n\nfunc (UnimplementedGreeterServer) SayHello(context.Context, *Request) (*Response, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method SayHello not implemented\")\n}\nfunc (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}\nfunc (UnimplementedGreeterServer) testEmbeddedByValue()                 {}\n\n// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to GreeterServer will\n// result in compilation errors.\ntype UnsafeGreeterServer interface {\n\tmustEmbedUnimplementedGreeterServer()\n}\n\nfunc RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {\n\t// If the following call pancis, it indicates UnimplementedGreeterServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&Greeter_ServiceDesc, srv)\n}\n\nfunc _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(Request)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GreeterServer).SayHello(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Greeter_SayHello_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GreeterServer).SayHello(ctx, req.(*Request))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Greeter_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"hello.Greeter\",\n\tHandlerType: (*GreeterServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"SayHello\",\n\t\t\tHandler:    _Greeter_SayHello_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"hello/hello.proto\",\n}\n"
  },
  {
    "path": "pkg/config/config.go",
    "content": "// Package config provides shared configuration utilities for the hello-auth-grpc services.\npackage config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// DefaultConfigDir returns the default configuration directory.\n// Can be overridden with the HELLO_CONFIG_DIR environment variable.\nfunc DefaultConfigDir() string {\n\tif dir := os.Getenv(\"HELLO_CONFIG_DIR\"); dir != \"\" {\n\t\treturn dir\n\t}\n\treturn filepath.Join(os.Getenv(\"HOME\"), \".hello\")\n}\n\n// WithConfigDir returns the full path for a file within the configuration directory.\nfunc WithConfigDir(path string) string {\n\treturn filepath.Join(DefaultConfigDir(), path)\n}\n"
  },
  {
    "path": "pkg/config/config_test.go",
    "content": "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 custom env var\n\tcustomDir := \"/tmp/custom-config\"\n\tos.Setenv(\"HELLO_CONFIG_DIR\", customDir)\n\tdefer os.Unsetenv(\"HELLO_CONFIG_DIR\")\n\n\tgot := DefaultConfigDir()\n\tif got != customDir {\n\t\tt.Errorf(\"DefaultConfigDir() = %v, want %v\", got, customDir)\n\t}\n\n\t// Test with default (HOME/.hello)\n\tos.Unsetenv(\"HELLO_CONFIG_DIR\")\n\tgot = DefaultConfigDir()\n\texpected := filepath.Join(os.Getenv(\"HOME\"), \".hello\")\n\tif got != expected {\n\t\tt.Errorf(\"DefaultConfigDir() = %v, want %v\", got, expected)\n\t}\n}\n\nfunc TestWithConfigDir(t *testing.T) {\n\ttestFile := \"test.pem\"\n\tgot := WithConfigDir(testFile)\n\n\t// Should combine config dir with file\n\tif !filepath.IsAbs(got) {\n\t\tt.Errorf(\"WithConfigDir() should return absolute path, got %v\", got)\n\t}\n\n\tif filepath.Base(got) != testFile {\n\t\tt.Errorf(\"WithConfigDir() should preserve filename, got %v\", got)\n\t}\n}\n"
  },
  {
    "path": "pkg/logging/logging.go",
    "content": "// Package logging provides structured logging utilities for gRPC services.\npackage logging\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/peer\"\n\t\"google.golang.org/grpc/status\"\n)\n\n// UnaryServerInterceptor returns a gRPC interceptor that logs all requests.\nfunc UnaryServerInterceptor(logger *log.Logger) grpc.UnaryServerInterceptor {\n\treturn func(\n\t\tctx context.Context,\n\t\treq interface{},\n\t\tinfo *grpc.UnaryServerInfo,\n\t\thandler grpc.UnaryHandler,\n\t) (interface{}, error) {\n\t\tstart := time.Now()\n\n\t\t// Get client address if available\n\t\tclientAddr := \"unknown\"\n\t\tif p, ok := peer.FromContext(ctx); ok {\n\t\t\tclientAddr = p.Addr.String()\n\t\t}\n\n\t\t// Call the handler\n\t\tresp, err := handler(ctx, req)\n\n\t\t// Log the request\n\t\tduration := time.Since(start)\n\t\tcode := codes.OK\n\t\tif err != nil {\n\t\t\tif st, ok := status.FromError(err); ok {\n\t\t\t\tcode = st.Code()\n\t\t\t} else {\n\t\t\t\tcode = codes.Unknown\n\t\t\t}\n\t\t}\n\n\t\tlogger.Printf(\"method=%s client=%s code=%s duration=%v\",\n\t\t\tinfo.FullMethod, clientAddr, code, duration)\n\n\t\tif err != nil {\n\t\t\tlogger.Printf(\"error: %v\", err)\n\t\t}\n\n\t\treturn resp, err\n\t}\n}\n\n// PanicRecoveryInterceptor returns a gRPC interceptor that recovers from panics.\nfunc PanicRecoveryInterceptor(logger *log.Logger) grpc.UnaryServerInterceptor {\n\treturn func(\n\t\tctx context.Context,\n\t\treq interface{},\n\t\tinfo *grpc.UnaryServerInfo,\n\t\thandler grpc.UnaryHandler,\n\t) (resp interface{}, err error) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlogger.Printf(\"PANIC recovered: method=%s panic=%v\", info.FullMethod, r)\n\t\t\t\terr = status.Errorf(codes.Internal, \"internal server error\")\n\t\t\t}\n\t\t}()\n\n\t\treturn handler(ctx, req)\n\t}\n}\n"
  }
]