@@ -43,6 +43,10 @@ type OAuth2Config struct {
4343 // FixedRedirectURI is an optional fixed redirect URI used when proxying callbacks
4444 FixedRedirectURI string
4545
46+ // AllowedClientRedirectDomains is an optional comma-separated list of domain suffixes
47+ // that are allowed for client redirect URIs in fixed redirect mode (in addition to localhost).
48+ AllowedClientRedirectDomains string
49+
4650 // OIDC configuration
4751 Issuer string
4852 Audience string
@@ -186,22 +190,23 @@ func NewOAuth2ConfigFromConfig(cfg *Config, version string) *OAuth2Config {
186190 }
187191
188192 return & OAuth2Config {
189- Enabled : true ,
190- Mode : cfg .Mode ,
191- Provider : cfg .Provider ,
192- RedirectURIs : cfg .RedirectURIs ,
193- FixedRedirectURI : cfg .FixedRedirectURI ,
194- Issuer : cfg .Issuer ,
195- Audience : cfg .Audience ,
196- ClientID : cfg .ClientID ,
197- ClientSecret : cfg .ClientSecret ,
198- Scopes : scopes ,
199- MCPHost : mcpHost ,
200- MCPPort : mcpPort ,
201- MCPURL : mcpURL ,
202- Scheme : scheme ,
203- Version : version ,
204- stateSigningKey : cfg .JWTSecret ,
193+ Enabled : true ,
194+ Mode : cfg .Mode ,
195+ Provider : cfg .Provider ,
196+ RedirectURIs : cfg .RedirectURIs ,
197+ FixedRedirectURI : cfg .FixedRedirectURI ,
198+ AllowedClientRedirectDomains : cfg .AllowedClientRedirectDomains ,
199+ Issuer : cfg .Issuer ,
200+ Audience : cfg .Audience ,
201+ ClientID : cfg .ClientID ,
202+ ClientSecret : cfg .ClientSecret ,
203+ Scopes : scopes ,
204+ MCPHost : mcpHost ,
205+ MCPPort : mcpPort ,
206+ MCPURL : mcpURL ,
207+ Scheme : scheme ,
208+ Version : version ,
209+ stateSigningKey : cfg .JWTSecret ,
205210 }
206211}
207212
@@ -353,11 +358,11 @@ func (h *OAuth2Handler) HandleAuthorize(w http.ResponseWriter, r *http.Request)
353358 return
354359 }
355360
356- // Security: For fixed redirect mode, only allow localhost or loopback addresses
357- // This prevents open redirect attacks while still supporting development tools
358- if ! isLocalhostURI (clientRedirectURI ) {
359- h .logger .Warn ("SECURITY: Fixed redirect mode only allows localhost URIs , rejecting: %s from %s" , clientRedirectURI , r .RemoteAddr )
360- http .Error (w , "Fixed redirect mode only allows localhost redirect URIs for security. Use allowlist mode for production. " , http .StatusBadRequest )
361+ // Security: For fixed redirect mode, only allow localhost or explicitly configured domain suffixes.
362+ // This prevents open redirect attacks while still supporting development tools and trusted hosts.
363+ if ! h . isAllowedClientRedirectURI (clientRedirectURI ) {
364+ h .logger .Warn ("SECURITY: Fixed redirect mode only allows localhost or configured domains , rejecting: %s from %s" , clientRedirectURI , r .RemoteAddr )
365+ http .Error (w , "Invalid redirect_uri for fixed redirect mode" , http .StatusBadRequest )
361366 return
362367 }
363368 redirectURI = strings .TrimSpace (h .config .FixedRedirectURI )
@@ -466,9 +471,9 @@ func (h *OAuth2Handler) HandleCallback(w http.ResponseWriter, r *http.Request) {
466471
467472 if hasState && hasRedirect {
468473 // Re-validate redirect URI for defense in depth
469- // Even though state is HMAC-signed, validate the redirect URI is localhost
470- if ! isLocalhostURI (originalRedirectURI ) {
471- h .logger .Warn ("SECURITY: Callback redirect URI is not localhost (possible key compromise): %s" , originalRedirectURI )
474+ // Even though state is HMAC-signed, validate the redirect URI is localhost or an allowed domain
475+ if ! h . isAllowedClientRedirectURI (originalRedirectURI ) {
476+ h .logger .Warn ("SECURITY: Callback redirect URI is not allowed (possible key compromise): %s" , originalRedirectURI )
472477 http .Error (w , "Invalid redirect URI in state" , http .StatusBadRequest )
473478 return
474479 }
@@ -801,6 +806,47 @@ func isLocalhostURI(uri string) bool {
801806 return hostname == "localhost" || hostname == "127.0.0.1" || hostname == "::1"
802807}
803808
809+ // isAllowedClientRedirectURI checks whether a client redirect URI is allowed in fixed redirect mode.
810+ func (h * OAuth2Handler ) isAllowedClientRedirectURI (uri string ) bool {
811+ // Always allow localhost URIs for development tools
812+ if isLocalhostURI (uri ) {
813+ return true
814+ }
815+
816+ // For non-localhost URIs, require explicit domain suffix configuration
817+ if h .config .AllowedClientRedirectDomains == "" {
818+ return false
819+ }
820+
821+ parsedURI , err := url .Parse (uri )
822+ if err != nil {
823+ return false
824+ }
825+
826+ // Only allow HTTPS for non-localhost URIs
827+ if parsedURI .Scheme != "https" {
828+ return false
829+ }
830+
831+ host := strings .ToLower (parsedURI .Hostname ())
832+ if host == "" {
833+ return false
834+ }
835+
836+ // Check if host matches any configured suffix (exact match or subdomain)
837+ for _ , suffix := range strings .Split (h .config .AllowedClientRedirectDomains , "," ) {
838+ suffix = strings .TrimSpace (strings .ToLower (suffix ))
839+ if suffix == "" {
840+ continue
841+ }
842+ if host == suffix || strings .HasSuffix (host , "." + suffix ) {
843+ return true
844+ }
845+ }
846+
847+ return false
848+ }
849+
804850// isValidRedirectURI validates redirect URI against allowlist for security
805851func (h * OAuth2Handler ) isValidRedirectURI (uri string ) bool {
806852 if h .config .RedirectURIs == "" {
0 commit comments