sourceafSleepSafe::SessionHijackGuard.fan

using afIoc::Inject
using afIocConfig::Config
using afIocConfig::ConfigSource
using afBedSheet::HttpRequest
using afBedSheet::HttpResponse
using afBedSheet::HttpSession

** Guards against Session hijacking by caching browser user-agent parameters and checking them on each request. 
** The session is dropped and request rejected should the parameters change.  
** 
** 
** 
** IoC Configuration
** *****************
** 
**   table:
**   afIocConfig Key                     Value
**   ----------------------------------  ------------
**   'afSleepSafe.sessionHijackHeaders'  CSV of request headers that are to be cached and compared. Defaults to 'User-Agent, Accept-Language'.
**   'afSleepSafe.sessionHijackEncrypt'  If 'true' (the default) then a hash of the header parameters is cached, and not the actual parameter values themselves. This is a security measure against the server / database being breached.
** 
** Example:
** 
**   syntax: fantom 
**   @Contribute { serviceType=ApplicationDefaults# }
**   Void contributeAppDefaults(Configuration config) {
**       config["afSleepSafe.csrfTokenName"]    = "clickFast"
**       config["afSleepSafe.csrfTokenTimeout"] = 2sec
**   }
** 
** To disable, remove this class from the 'SleepSafeMiddleware' configuration:
** 
**   syntax: fantom 
**   @Contribute { serviceType=SleepSafeMiddleware# }
**   Void contributeSleepSafeMiddleware(Configuration config) {
**       config.remove(SessionHijackGuard#)
**   }
** 
const class SessionHijackGuard : Guard {

    @Inject private const HttpSession   httpSes
    @Config { id="afSleepSafe.sessionHijackEncrypt" }
            private const Bool          encrypt         
            private const Str[]         headers

    private new make(ConfigSource configSrc, |This| f) {
        f(this)
        csv     := (Str) configSrc.get("afSleepSafe.sessionHijackHeaders", Str#)
        headers = csv.split(',').exclude { it.isEmpty }     
    }
    
    @NoDoc
    override const Str protectsAgainst  := "Session Hijacking" 

    @NoDoc
    override Str? guard(HttpRequest httpReq, HttpResponse httpRes) {
        if (!httpSes.exists) {
            httpSes.onCreate {
                // cache the session hash params as soon as the session is created
                it["afSleepSafe.sessionHash"] = hashSessionParams(httpReq)
            }
            return null
        }
        
        hash    := hashSessionParams(httpReq)
        reject  := null as Str
        if (httpSes.containsKey("afSleepSafe.sessionHash")) {
            oldHash := httpSes["afSleepSafe.sessionHash"]
            if (oldHash != hash) {
                httpSes.delete
                return "Suspected Cookie Hijacking - Session parameters have changed: $oldHash != $hash"
            }
        }

        httpSes["afSleepSafe.sessionHash"] = hash
        
        return reject
    }
    
    private Str hashSessionParams(HttpRequest httpReq) {
        map := Str:Str[:] { ordered = true }
        headers.each |header| {
            map[header] = httpReq.headers.val.get(header, "")
        }
        
        hash := map.join(", ")
        if (encrypt)
            hash = hash.toBuf.toDigest("SHA-1").toBase64Uri
        
        return hash
    }
}