Skip to content

feat support reverse proxy#178

Open
wonyx wants to merge 3 commits intologto-io:v2from
wonyx:wonyx-feat-support-reverse-proxy
Open

feat support reverse proxy#178
wonyx wants to merge 3 commits intologto-io:v2from
wonyx:wonyx-feat-support-reverse-proxy

Conversation

@wonyx
Copy link
Copy Markdown

@wonyx wonyx commented Oct 6, 2025

Summary

fixed #177

This pull request introduces the WithTrustForwardedHeader option to the NewLogtoClient constructor. When this option is set to true, the Logto client will trust X-Forwarded-* headers (e.g., X-Forwarded-Host, X-Forwarded-Proto) to correctly construct the request's origin URL.

This is particularly useful for applications running behind a reverse proxy or load balancer, ensuring that redirect URIs for the sign-in and sign-out processes are generated with the correct public-facing scheme and host.

Usage example

logtoClient := client.NewLogtoClient(
	logtoConfig,
	storage,
	client.WithTrustForwardedHeader(true),
)

Testing

added unittest cases.

Also I have been tested it in firebase hosting with cloud run environment that described in #177 .

Checklist

  • [ ] .changeset
  • unit tests
  • integration tests
  • [ ] necessary TSDoc comments

@charIeszhao
Copy link
Copy Markdown
Member

Looks good to me. @xiaoyijun Please take a look, thx

Copy link
Copy Markdown

Copilot AI left a 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 support for applications running behind a reverse proxy by introducing a WithTrustForwardedHeader option that allows the Logto client to trust X-Forwarded-* headers when constructing callback URLs.

Changes:

  • Added WithTrustForwardedHeader functional option to configure whether to trust forwarded headers
  • Implemented utility functions to extract host, URI, and protocol from forwarded headers
  • Modified HandleSignInCallback to use forwarded headers when the option is enabled

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
client/client.go Adds WithTrustForwardedHeader option and trustForwardedHeader field to LogtoClient struct
client/util.go Implements helper functions for extracting URL components from X-Forwarded-* headers
client/handle_sign_in_callback.go Conditionally uses forwarded headers to construct callback URI based on configuration
client/util_test.go Adds unit tests for the new forwarded header utility functions
Comments suppressed due to low confidence (1)

client/util.go:45

  • Security concern: The getRequestProtocol function always trusts the X-Forwarded-Proto header regardless of the trustForwardedHeader setting. This means that even when trustForwardedHeader is false (the default), the protocol can be controlled by the client through headers, which could be a security issue.

The function should be refactored to accept a parameter indicating whether to trust forwarded headers, or there should be two separate functions: one that trusts forwarded headers and one that doesn't. When trustForwardedHeader is false, only request.TLS should be checked to determine if HTTPS is being used.

func getRequestProtocol(request *http.Request) string {
	if request.TLS != nil {
		return "https"
	}
	proto := request.Header.Get("X-Forwarded-Proto")
	if proto != "" {
		extractedProto := strings.Split(proto, ",")[0]
		return strings.ToLower(strings.Trim(extractedProto, " "))
	}
	return "http"
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +15 to +34
func getForwaredRequestUrl(request *http.Request) string {
proto := getRequestProtocol(request)
host := getForwaredRequestHost(request)
uri := getForwaredRequestRequestUri(request)
return proto + "://" + host + uri
}
func getForwaredRequestHost(request *http.Request) string {
host := request.Header.Get("X-Forwarded-Host")
if host != "" {
return host
}
return request.Host
}
func getForwaredRequestRequestUri(request *http.Request) string {
uri := request.Header.Get("X-Forwarded-Url")
if uri != "" {
return uri
}
return request.RequestURI
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a spelling error in the function names throughout the code. "Forwared" should be spelled "Forwarded" (with two 'd's). This affects:

  • getForwaredRequestUrl
  • getForwaredRequestHost
  • getForwaredRequestRequestUri
  • Test function names: TestGetForwaredRequestUrlShouldReturnXForwardedIfPresent, TestGetForwaredRequestHostShouldFallbackToRequestHost, and TestGetForwaredRequestRequestUriShouldFallbackToRequestURI

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +172
func TestGetForwaredRequestUrlShouldReturnXForwardedIfPresent(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com/path?query=1", nil)
assert.Nil(t, err)
// Ensure RequestURI is set like in real servers
req.RequestURI = "/path?query=1"

req.Header.Add("X-Forwarded-Host", "forwarded.example.com")
req.Header.Add("X-Forwarded-Url", "/forwarded-path?query=2")
req.Header.Add("X-Forwarded-Proto", "https")

url := getForwaredRequestUrl(req)

assert.Equal(t, "https://forwarded.example.com/forwarded-path?query=2", url)
}

func TestGetForwaredRequestHostShouldFallbackToRequestHost(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/", nil)
req.RequestURI = "/"

host := getForwaredRequestHost(req)

assert.Equal(t, "example.com", host)

req.Header.Add("X-Forwarded-Host", "proxied.example.com")
host = getForwaredRequestHost(req)
assert.Equal(t, "proxied.example.com", host)
}

func TestGetForwaredRequestRequestUriShouldFallbackToRequestURI(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/api", nil)
req.RequestURI = "/api"

uri := getForwaredRequestRequestUri(req)
assert.Equal(t, "/api", uri)

req.Header.Add("X-Forwarded-Url", "/proxied/api?x=1")
uri = getForwaredRequestRequestUri(req)
assert.Equal(t, "/proxied/api?x=1", uri)
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a spelling error in the test function names. "Forwared" should be spelled "Forwarded" (with two 'd's). This affects:

  • TestGetForwaredRequestUrlShouldReturnXForwardedIfPresent
  • TestGetForwaredRequestHostShouldFallbackToRequestHost
  • TestGetForwaredRequestRequestUriShouldFallbackToRequestURI

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +20
func getForwaredRequestUrl(request *http.Request) string {
proto := getRequestProtocol(request)
host := getForwaredRequestHost(request)
uri := getForwaredRequestRequestUri(request)
return proto + "://" + host + uri
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getForwaredRequestUrl function calls getRequestProtocol, which unconditionally trusts the X-Forwarded-Proto header. This means the protocol portion is determined by forwarded headers regardless of the trustForwardedHeader setting.

For consistency and security, when constructing URLs for forwarded requests, the protocol should also respect the forwarded headers setting. Consider creating a separate protocol detection function that accepts a flag to control whether to trust forwarded headers.

Copilot uses AI. Check for mistakes.
Comment on lines 19 to +22
callbackUri := GetOriginRequestUrl(request)
if logtoClient.trustForwardedHeader {
callbackUri = getForwaredRequestUrl(request)
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage: There are no test cases that verify the HandleSignInCallback function correctly uses forwarded headers when trustForwardedHeader is enabled. The existing tests only cover the unit-level utility functions.

Consider adding an integration test that:

  1. Creates a LogtoClient with WithTrustForwardedHeader(true)
  2. Calls HandleSignInCallback with a request containing X-Forwarded-Host, X-Forwarded-Url, and X-Forwarded-Proto headers
  3. Verifies that the callback URI passed to core.VerifyAndParseCodeFromCallbackUri contains the forwarded values rather than the original request values

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +32
uri := request.Header.Get("X-Forwarded-Url")
if uri != "" {
return uri
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-standard header usage: The implementation uses X-Forwarded-Url header, which is not a standard forwarded header. The standard headers typically used by reverse proxies are:

  • X-Forwarded-Proto for protocol
  • X-Forwarded-Host for host
  • X-Forwarded-Path or X-Original-URI for the path/URI

X-Forwarded-Url is not widely supported by standard reverse proxies like nginx, Apache, or load balancers. Consider using X-Forwarded-Path or X-Original-URI instead, or document clearly that this requires custom proxy configuration. Alternatively, if the path should remain unchanged, consider using just the Host and Proto headers.

Suggested change
uri := request.Header.Get("X-Forwarded-Url")
if uri != "" {
return uri
}
uri := request.Header.Get("X-Original-URI")
if uri != "" {
return uri
}
uri = request.Header.Get("X-Forwarded-Path")
if uri != "" {
return uri
}

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +49
// WithTrustForwardedHeader sets whether to trust X-Forwarded-* headers for checking the request's origin, useful when behind a reverse proxy.
func WithTrustForwardedHeader(b bool) LogtoClientOption {
return func(c *LogtoClient) {
c.trustForwardedHeader = b
}
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security concern: When trustForwardedHeader is enabled, the forwarded headers are trusted without any validation. This could be exploited if the application is not actually behind a properly configured reverse proxy, or if the proxy doesn't strip client-provided X-Forwarded-* headers.

Consider adding:

  1. Documentation warning that this option should only be enabled when the application is behind a trusted reverse proxy that properly sets and strips forwarded headers
  2. Optional validation that the forwarded values are well-formed URLs/hosts
  3. A comment in the code explaining the security implications

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

feature request: Support running behind a reverse proxy

3 participants