@@ -57,17 +57,25 @@ type PortalSession struct {
5757type PortalSessionManager struct {
5858 cookieManager
5959 db database.Database
60+ // ipBinding is the resolved SESSION_IP_BINDING mode (config.SessionIPBinding*)
61+ // that portal session validation applies to a client-IP change. An unknown
62+ // or zero value is treated as strict so managers built without config.Load
63+ // fail closed.
64+ ipBinding string
6065}
6166
6267// NewPortalSessionManager creates a new portal session manager with secure cookie handling.
6368// If cookieSecret is set, deterministic cookie keys are derived from it
6469// so that sessions survive process restarts with the same secret.
70+ // ipBinding is the resolved SESSION_IP_BINDING mode; portal sessions share the
71+ // setting with internal user sessions because they enforce the same binding.
6572// last review: ser, 210426, NOTE: Found hardcoded env var in caller
66- func NewPortalSessionManager (db database.Database , useSecureCookies , useProxy bool , additionalProxies []string , cookieSecret string ) * PortalSessionManager {
73+ func NewPortalSessionManager (db database.Database , useSecureCookies , useProxy bool , additionalProxies []string , cookieSecret , ipBinding string ) * PortalSessionManager {
6774 return & PortalSessionManager {
6875 cookieManager : newCookieManager (useSecureCookies , useProxy , additionalProxies , cookieSecret ,
6976 "windshift-portal-cookie-hash" , "windshift-portal-cookie-block" ),
70- db : db ,
77+ db : db ,
78+ ipBinding : ipBinding ,
7179 }
7280}
7381
@@ -169,26 +177,45 @@ func (sm *PortalSessionManager) ValidatePortalSession(token, ipAddress string) (
169177 }
170178
171179 // Validate IP address for security. Portal sessions store the client IP at
172- // creation; subsequent validations must match the same binding used by
173- // internal user sessions. Legacy rows with no recorded IP are accepted but
174- // logged so operators can investigate. Missing request IP or mismatch fails
175- // closed.
176- switch {
177- case session .IPAddress == "" :
180+ // creation; subsequent validations apply the same SESSION_IP_BINDING mode
181+ // as internal user sessions. Legacy rows with no recorded IP are accepted
182+ // but logged so operators can investigate. Under strict a missing request
183+ // IP or a mismatch fails closed; under log the session is followed to its
184+ // new IP; under off no comparison happens. No mode deactivates the session.
185+ switch decideIPBinding (sm .ipBinding , session .IPAddress , ipAddress ) {
186+ case ipBindingLegacyUnbound :
178187 slog .Warn ("portal session has no recorded IP, skipping bind check" ,
179188 slog .Int ("portal_customer_id" , session .PortalCustomerID ),
180189 slog .Int ("session_id" , session .ID ))
181- case ipAddress == "" :
190+ case ipBindingRejectNoRequestIP :
182191 slog .Warn ("request has no client IP, rejecting IP-bound portal session" ,
183192 slog .Int ("portal_customer_id" , session .PortalCustomerID ),
184193 slog .String ("session_ip" , session .IPAddress ))
185194 return nil , ErrPortalSessionInvalid
186- case session .IPAddress != ipAddress :
195+ case ipBindingAcceptNoRequestIP :
196+ slog .Warn ("request has no client IP, accepting IP-bound portal session" ,
197+ slog .Int ("portal_customer_id" , session .PortalCustomerID ),
198+ slog .String ("session_ip" , session .IPAddress ),
199+ slog .String ("session_ip_binding" , sm .ipBinding ))
200+ case ipBindingAcceptUnparsedIP :
201+ slog .Warn ("request client IP is not a valid address, accepting IP-bound portal session without rebinding" ,
202+ slog .Int ("portal_customer_id" , session .PortalCustomerID ),
203+ slog .String ("session_ip" , session .IPAddress ),
204+ slog .String ("request_ip" , ipAddress ),
205+ slog .String ("session_ip_binding" , sm .ipBinding ))
206+ case ipBindingRejectMismatch :
187207 slog .Warn ("portal session IP mismatch" ,
188208 slog .Int ("portal_customer_id" , session .PortalCustomerID ),
189209 slog .String ("session_ip" , session .IPAddress ),
190210 slog .String ("request_ip" , ipAddress ))
191211 return nil , ErrPortalSessionInvalid
212+ case ipBindingRebindMismatch :
213+ slog .Warn ("portal session IP mismatch" ,
214+ slog .Int ("portal_customer_id" , session .PortalCustomerID ),
215+ slog .String ("session_ip" , session .IPAddress ),
216+ slog .String ("request_ip" , ipAddress ))
217+ sm .rebindPortalSessionIP (token , session , ipAddress )
218+ case ipBindingMatch , ipBindingSkip :
192219 }
193220
194221 if channelID .Valid {
@@ -209,6 +236,27 @@ func (sm *PortalSessionManager) ValidatePortalSession(token, ipAddress string) (
209236 return session , nil
210237}
211238
239+ // rebindPortalSessionIP moves a portal session to the client IP it is now
240+ // presented from (log mode only). Like the user-session rebind, a failed write
241+ // costs bookkeeping rather than availability: the request is still served and
242+ // the next one retries. Portal sessions have no local validation cache, so
243+ // nothing needs invalidating; the in-memory session is advanced on success
244+ // because it is returned to the caller.
245+ func (sm * PortalSessionManager ) rebindPortalSessionIP (token string , session * PortalSession , ipAddress string ) {
246+ // Portal tokens are stored as digests with the same legacy plaintext
247+ // fallback as user sessions, so the predicate must match both forms.
248+ query := `UPDATE portal_customer_sessions SET ip_address = ? WHERE session_token IN (?, ?) AND is_active = true`
249+ if _ , err := sm .db .ExecWrite (query , ipAddress , hashSessionToken (token ), token ); err != nil {
250+ slog .Error ("failed to rebind portal session to the new client IP" ,
251+ slog .Int ("portal_customer_id" , session .PortalCustomerID ),
252+ slog .Int ("session_id" , session .ID ),
253+ slog .String ("request_ip" , ipAddress ),
254+ slog .Any ("error" , err ))
255+ return
256+ }
257+ session .IPAddress = ipAddress
258+ }
259+
212260// DeletePortalSession invalidates a session
213261// last review: ser, 210426, TODO: Remove inline sql
214262func (sm * PortalSessionManager ) DeletePortalSession (token string ) error {
0 commit comments