diff --git a/service/internal/api/api.go b/service/internal/api/api.go index b479e96..bf168a7 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -158,7 +158,12 @@ func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1 return connect.NewResponse(ret), nil } -func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, response *connect.Response[apiv1.LocalUserLoginResponse], match bool) { +func (api *oliveTinAPI) cookieSecure(header http.Header) bool { + useTLS := header.Get("X-Forwarded-Proto") == "https" + return useTLS || api.cfg.Security.ForceSecureCookies +} + +func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, response *connect.Response[apiv1.LocalUserLoginResponse], match bool, secure bool) { if match { user := api.cfg.FindUserByUsername(req.Username) if user != nil { @@ -171,6 +176,8 @@ func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, MaxAge: 31556952, HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Set("Set-Cookie", cookie.String()) } @@ -192,7 +199,7 @@ func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[api return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("checking password: %w", err)) } response := connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: match}) - api.applyLocalLoginResult(req.Msg, response, match) + api.applyLocalLoginResult(req.Msg, response, match, api.cookieSecure(req.Header())) return response, nil } @@ -389,6 +396,7 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou }).Info("Logout: User logged out") response := connect.NewResponse(&apiv1.LogoutResponse{}) + secure := api.cookieSecure(req.Header()) // Clear the local authentication cookie by setting it to expire localCookie := &http.Cookie{ @@ -397,6 +405,8 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou MaxAge: -1, // This tells the browser to delete the cookie HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Set("Set-Cookie", localCookie.String()) @@ -407,6 +417,8 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou MaxAge: -1, // This tells the browser to delete the cookie HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Add("Set-Cookie", oauth2Cookie.String()) diff --git a/service/internal/auth/otoauth2/restapi_auth_oauth2.go b/service/internal/auth/otoauth2/restapi_auth_oauth2.go index 1dea7d0..5266124 100644 --- a/service/internal/auth/otoauth2/restapi_auth_oauth2.go +++ b/service/internal/auth/otoauth2/restapi_auth_oauth2.go @@ -108,14 +108,20 @@ func randString(nByte int) (string, error) { return base64.URLEncoding.EncodeToString(b), nil } +func (h *OAuth2Handler) cookieSecure(r *http.Request) bool { + useTLS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" + return useTLS || h.cfg.Security.ForceSecureCookies +} + func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) { cookie := &http.Cookie{ Name: name, Value: value, MaxAge: 900, // 15 minutes - Secure: r.TLS != nil, + Secure: h.cookieSecure(r), HttpOnly: true, Path: "/", + SameSite: http.SameSiteLaxMode, } http.SetCookie(w, cookie) diff --git a/service/internal/config/config.go b/service/internal/config/config.go index aa35153..b235325 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -107,13 +107,14 @@ type PrometheusConfig struct { DefaultGoMetrics bool `koanf:"defaultGoMetrics"` } -// SecurityConfig allows users to fine tune the security related HTTP headers. +// SecurityConfig allows users to fine tune the security related HTTP headers and cookie options. type SecurityConfig struct { HeaderContentSecurityPolicy bool `koanf:"headerContentSecurityPolicy"` ContentSecurityPolicy string `koanf:"contentSecurityPolicy"` HeaderXContentTypeOptions bool `koanf:"headerXContentTypeOptions"` HeaderXFrameOptions bool `koanf:"headerXFrameOptions"` XFrameOptions string `koanf:"xFrameOptions"` + ForceSecureCookies bool `koanf:"forceSecureCookies"` } // Config is the global config used through the whole app.