diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index fc470e64..6d844c68 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -27,6 +27,7 @@ const ( DbProviderPostgres DbProvider = "postgres" MaxMindGeoLiteCityUrl string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz" defaultSqliteConnString string = "data/pocket-id.db" + AppUrl string = "http://localhost:1411" ) type EnvConfigSchema struct { @@ -53,6 +54,7 @@ type EnvConfigSchema struct { TrustProxy bool `env:"TRUST_PROXY"` AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"` AllowDowngrade bool `env:"ALLOW_DOWNGRADE"` + InternalAppURL string `env:"INTERNAL_APP_URL"` } var EnvConfig = defaultConfig() @@ -74,7 +76,7 @@ func defaultConfig() EnvConfigSchema { KeysPath: "data/keys", KeysStorage: "", // "database" or "file" EncryptionKey: nil, - AppURL: "http://localhost:1411", + AppURL: AppUrl, Port: "1411", Host: "0.0.0.0", UnixSocket: "", @@ -89,6 +91,7 @@ func defaultConfig() EnvConfigSchema { TrustProxy: false, AnalyticsDisabled: false, AllowDowngrade: false, + InternalAppURL: "", } } @@ -133,6 +136,19 @@ func parseEnvConfig() error { return errors.New("APP_URL must not contain a path") } + // Derive INTERNAL_APP_URL from APP_URL if not set; validate only when provided + if EnvConfig.InternalAppURL == "" { + EnvConfig.InternalAppURL = EnvConfig.AppURL + } else { + parsedInternalAppUrl, err := url.Parse(EnvConfig.InternalAppURL) + if err != nil { + return errors.New("INTERNAL_APP_URL is not a valid URL") + } + if parsedInternalAppUrl.Path != "" { + return errors.New("INTERNAL_APP_URL must not contain a path") + } + } + switch EnvConfig.KeysStorage { // KeysStorage defaults to "file" if empty case "": diff --git a/backend/internal/common/env_config_test.go b/backend/internal/common/env_config_test.go index ffef2852..b192adcd 100644 --- a/backend/internal/common/env_config_test.go +++ b/backend/internal/common/env_config_test.go @@ -91,6 +91,28 @@ func TestParseEnvConfig(t *testing.T) { assert.ErrorContains(t, err, "APP_URL must not contain a path") }) + t.Run("should fail with invalid INTERNAL_APP_URL", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("DB_PROVIDER", "sqlite") + t.Setenv("DB_CONNECTION_STRING", "file:test.db") + t.Setenv("INTERNAL_APP_URL", "€://not-a-valid-url") + + err := parseEnvConfig() + require.Error(t, err) + assert.ErrorContains(t, err, "INTERNAL_APP_URL is not a valid URL") + }) + + t.Run("should fail when INTERNAL_APP_URL contains path", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("DB_PROVIDER", "sqlite") + t.Setenv("DB_CONNECTION_STRING", "file:test.db") + t.Setenv("INTERNAL_APP_URL", "http://localhost:3000/path") + + err := parseEnvConfig() + require.Error(t, err) + assert.ErrorContains(t, err, "INTERNAL_APP_URL must not contain a path") + }) + t.Run("should default KEYS_STORAGE to 'file' when empty", func(t *testing.T) { EnvConfig = defaultConfig() t.Setenv("DB_PROVIDER", "sqlite") diff --git a/backend/internal/controller/well_known_controller.go b/backend/internal/controller/well_known_controller.go index 70d7c447..3ab35461 100644 --- a/backend/internal/controller/well_known_controller.go +++ b/backend/internal/controller/well_known_controller.go @@ -67,6 +67,9 @@ func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) { func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) { appUrl := common.EnvConfig.AppURL + + internalAppUrl := common.EnvConfig.InternalAppURL + alg, err := wkc.jwtService.GetKeyAlg() if err != nil { return nil, fmt.Errorf("failed to get key algorithm: %w", err) @@ -74,12 +77,12 @@ func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) { config := map[string]any{ "issuer": appUrl, "authorization_endpoint": appUrl + "/authorize", - "token_endpoint": appUrl + "/api/oidc/token", - "userinfo_endpoint": appUrl + "/api/oidc/userinfo", + "token_endpoint": internalAppUrl + "/api/oidc/token", + "userinfo_endpoint": internalAppUrl + "/api/oidc/userinfo", "end_session_endpoint": appUrl + "/api/oidc/end-session", - "introspection_endpoint": appUrl + "/api/oidc/introspect", + "introspection_endpoint": internalAppUrl + "/api/oidc/introspect", "device_authorization_endpoint": appUrl + "/api/oidc/device/authorize", - "jwks_uri": appUrl + "/.well-known/jwks.json", + "jwks_uri": internalAppUrl + "/.well-known/jwks.json", "grant_types_supported": []string{service.GrantTypeAuthorizationCode, service.GrantTypeRefreshToken, service.GrantTypeDeviceCode}, "scopes_supported": []string{"openid", "profile", "email", "groups"}, "claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "email_verified", "preferred_username", "picture", "groups"},