-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add configurable service-to-service authentication for APIs #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
appleboy
commented
Jan 17, 2026
- Add service-to-service authentication options for external HTTP APIs, supporting none, simple secret, or HMAC modes
- Introduce configuration for authentication mode, secret, and header names for both Auth API and Token API
- Implement a reusable AuthConfig with logic for adding authentication headers to requests and verifying server-side HMAC signatures
- Update external authentication and token provider flows to attach authentication headers to API requests based on configuration
- Add comprehensive unit tests for all service-to-service authentication header modes and HMAC verification
- Document authentication modes and secure API configuration in detail in both CLAUDE.md and README.md
- Add service-to-service authentication options for external HTTP APIs, supporting none, simple secret, or HMAC modes - Introduce configuration for authentication mode, secret, and header names for both Auth API and Token API - Implement a reusable AuthConfig with logic for adding authentication headers to requests and verifying server-side HMAC signatures - Update external authentication and token provider flows to attach authentication headers to API requests based on configuration - Add comprehensive unit tests for all service-to-service authentication header modes and HMAC verification - Document authentication modes and secure API configuration in detail in both CLAUDE.md and README.md Signed-off-by: appleboy <appleboy.tw@gmail.com>
✅ Deploy Preview for authgate-demo ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds configurable service-to-service authentication for external HTTP APIs, enabling AuthGate to securely communicate with external authentication and token services. The feature supports three authentication modes (none, simple secret, and HMAC-SHA256) with comprehensive configuration options.
Changes:
- Introduced a new
httpclient.AuthConfigstruct with methods for adding authentication headers (simple and HMAC modes) and server-side HMAC signature verification - Updated external authentication provider and token provider to integrate authentication headers into API requests
- Added environment variables for configuring authentication modes, secrets, and custom headers for both Auth API and Token API
- Documented authentication modes, configuration examples, and server-side verification in README.md and CLAUDE.md
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/httpclient/auth.go | New authentication configuration module with header generation and HMAC verification logic |
| internal/httpclient/auth_test.go | Comprehensive test suite covering all authentication modes and edge cases |
| internal/token/http_api.go | Integration of AuthConfig to add authentication headers to all token API requests |
| internal/auth/http_api.go | Integration of AuthConfig to add authentication headers to authentication API requests |
| internal/config/config.go | Added configuration fields for HTTP API and Token API authentication settings |
| README.md | Documentation of authentication modes, configuration examples, and usage instructions |
| CLAUDE.md | Detailed technical documentation of service-to-service authentication implementation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func TestAuthConfig_VerifyHMACSignature_ExpiredTimestamp(t *testing.T) { | ||
| config := &AuthConfig{ | ||
| Secret: "test-secret", | ||
| } | ||
|
|
||
| body := []byte(`{"username":"test"}`) | ||
| // Timestamp from 10 minutes ago | ||
| timestamp := time.Now().Add(-10 * time.Minute).Unix() | ||
| signature := config.calculateHMACSignature(timestamp, "POST", "/api/auth", body) | ||
|
|
||
| req, err := http.NewRequestWithContext( | ||
| context.Background(), | ||
| "POST", | ||
| testExampleAuthURL, | ||
| bytes.NewBuffer(body), | ||
| ) | ||
| if err != nil { | ||
| t.Fatalf("Failed to create request: %v", err) | ||
| } | ||
|
|
||
| req.Header.Set(testXSignature, signature) | ||
| req.Header.Set(testXTimestamp, strconv.FormatInt(timestamp, 10)) | ||
|
|
||
| // Verify with 5 minute max age - should fail | ||
| err = config.VerifyHMACSignature(req, 5*time.Minute) | ||
| if err == nil { | ||
| t.Errorf("VerifyHMACSignature() error = nil, want expired error") | ||
| } | ||
| } |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for future timestamps in HMAC verification. The VerifyHMACSignature function should reject timestamps that are too far in the future (clock skew attacks), but there's no test case to verify this behavior. Add a test case that sends a timestamp 10 minutes in the future and verifies it's rejected.
| } | ||
|
|
||
| requestTime := time.Unix(timestamp, 0) | ||
| if time.Since(requestTime) > maxAge { |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timestamp validation only checks if the timestamp is too old (in the past) but does not validate if the timestamp is in the future. An attacker could send a request with a timestamp far in the future, which would bypass the replay protection. Add a check to ensure the timestamp is not more than a reasonable amount (e.g., 1 minute) in the future.
| if time.Since(requestTime) > maxAge { | |
| // Reject timestamps that are too old or too far in the future. | |
| // We treat maxAge as a symmetric window around the current time. | |
| delta := time.Since(requestTime) | |
| if delta > maxAge || delta < -maxAge { |
| import "github.com/appleboy/authgate/internal/httpclient" | ||
|
|
||
| // Initialize auth config (server side) | ||
| authConfig := httpclient.NewAuthConfig("hmac", "your-hmac-secret") | ||
|
|
||
| // Verify incoming request | ||
| err := authConfig.VerifyHMACSignature(req, 5*time.Minute) | ||
| if err != nil { | ||
| http.Error(w, "Authentication failed", http.StatusUnauthorized) | ||
| return | ||
| } |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server-side verification example is incomplete. After calling VerifyHMACSignature, the request body is consumed and needs to be restored before the handler can read it. The example should include a note or code showing that after verification, the body must be manually restored or that VerifyHMACSignature should be updated to restore the body automatically.
| import "github.com/appleboy/authgate/internal/httpclient" | |
| // Initialize auth config (server side) | |
| authConfig := httpclient.NewAuthConfig("hmac", "your-hmac-secret") | |
| // Verify incoming request | |
| err := authConfig.VerifyHMACSignature(req, 5*time.Minute) | |
| if err != nil { | |
| http.Error(w, "Authentication failed", http.StatusUnauthorized) | |
| return | |
| } | |
| import ( | |
| "bytes" | |
| "io" | |
| "net/http" | |
| "time" | |
| "github.com/appleboy/authgate/internal/httpclient" | |
| ) | |
| // Initialize auth config (server side) | |
| authConfig := httpclient.NewAuthConfig("hmac", "your-hmac-secret") | |
| // Read and buffer the request body so it can be reused | |
| bodyBytes, err := io.ReadAll(req.Body) | |
| if err != nil { | |
| http.Error(w, "Failed to read request body", http.StatusBadRequest) | |
| return | |
| } | |
| // Close the original body reader | |
| _ = req.Body.Close() | |
| // Set body for signature verification | |
| req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) | |
| // Verify incoming request | |
| err = authConfig.VerifyHMACSignature(req, 5*time.Minute) | |
| if err != nil { | |
| http.Error(w, "Authentication failed", http.StatusUnauthorized) | |
| return | |
| } | |
| // Restore body for further handler processing | |
| req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) |
| import "github.com/appleboy/authgate/internal/httpclient" | ||
|
|
||
| authConfig := &httpclient.AuthConfig{ | ||
| Mode: httpclient.AuthModeHMAC, | ||
| Secret: "your-shared-secret-key", | ||
| } | ||
|
|
||
| // Verify signature (checks timestamp age and signature validity) | ||
| err := authConfig.VerifyHMACSignature(req, 5*time.Minute) | ||
| if err != nil { | ||
| return http.StatusUnauthorized | ||
| } |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server-side validation example is incomplete. After calling VerifyHMACSignature, the request body is consumed and needs to be restored before the handler can read it. The example should include a note or code showing that after verification, the body must be manually restored or that VerifyHMACSignature should be updated to restore the body automatically.
| import "github.com/appleboy/authgate/internal/httpclient" | |
| authConfig := &httpclient.AuthConfig{ | |
| Mode: httpclient.AuthModeHMAC, | |
| Secret: "your-shared-secret-key", | |
| } | |
| // Verify signature (checks timestamp age and signature validity) | |
| err := authConfig.VerifyHMACSignature(req, 5*time.Minute) | |
| if err != nil { | |
| return http.StatusUnauthorized | |
| } | |
| import ( | |
| "bytes" | |
| "io" | |
| "net/http" | |
| "time" | |
| "github.com/appleboy/authgate/internal/httpclient" | |
| ) | |
| authConfig := &httpclient.AuthConfig{ | |
| Mode: httpclient.AuthModeHMAC, | |
| Secret: "your-shared-secret-key", | |
| } | |
| // Read and buffer the body so it can be used both for verification and by handlers. | |
| bodyBytes, err := io.ReadAll(req.Body) | |
| if err != nil { | |
| return http.StatusBadRequest | |
| } | |
| // Restore body for signature verification. | |
| req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) | |
| // Verify signature (checks timestamp age and signature validity). | |
| err = authConfig.VerifyHMACSignature(req, 5*time.Minute) | |
| if err != nil { | |
| return http.StatusUnauthorized | |
| } | |
| // Restore body again so downstream handlers can read it as usual. | |
| req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) |
| func TestAuthConfig_AddAuthHeaders_HMAC(t *testing.T) { | ||
| config := &AuthConfig{ | ||
| Mode: AuthModeHMAC, | ||
| Secret: "test-secret-hmac", | ||
| } | ||
|
|
||
| body := []byte(`{"username":"test","password":"pass123"}`) | ||
| req, err := http.NewRequestWithContext( | ||
| context.Background(), | ||
| "POST", | ||
| testExampleAuthURL, | ||
| bytes.NewBuffer(body), | ||
| ) | ||
| if err != nil { | ||
| t.Fatalf("Failed to create request: %v", err) | ||
| } | ||
|
|
||
| err = config.AddAuthHeaders(req, body) | ||
| if err != nil { | ||
| t.Fatalf("AddAuthHeaders() error = %v", err) | ||
| } | ||
|
|
||
| // Check that all required headers are present | ||
| signature := req.Header.Get(testXSignature) | ||
| if signature == "" { | ||
| t.Errorf("Expected X-Signature header to be set") | ||
| } | ||
|
|
||
| timestamp := req.Header.Get(testXTimestamp) | ||
| if timestamp == "" { | ||
| t.Errorf("Expected X-Timestamp header to be set") | ||
| } | ||
|
|
||
| nonce := req.Header.Get(testXNonce) | ||
| if nonce == "" { | ||
| t.Errorf("Expected X-Nonce header to be set") | ||
| } | ||
|
|
||
| // Verify signature format (should be hex string) | ||
| if len(signature) != 64 { // SHA256 hex string is 64 characters | ||
| t.Errorf("Signature length = %d, want 64", len(signature)) | ||
| } | ||
|
|
||
| // Verify timestamp is recent (within 1 second) | ||
| ts, err := strconv.ParseInt(timestamp, 10, 64) | ||
| if err != nil { | ||
| t.Errorf("Failed to parse timestamp: %v", err) | ||
| } | ||
|
|
||
| timeDiff := time.Now().Unix() - ts | ||
| if timeDiff > 1 { | ||
| t.Errorf("Timestamp is too old: %d seconds", timeDiff) | ||
| } | ||
| } |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for HMAC mode without a secret. While the addHMACAuth method does check for an empty secret and returns an error, there should be a test case similar to the "Simple mode without secret" test to verify this error handling works correctly for HMAC mode as well.
- Update HMAC signature calculation to include query parameters - Restore request body after signature verification to allow further processing - Add utility to obtain full request path with query parameters - Introduce tests to verify body preservation after HMAC signature verification - Add tests ensuring HMAC signature covers query parameters for improved security Signed-off-by: appleboy <appleboy.tw@gmail.com>