web_session.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package model // import "miniflux.app/v2/internal/model"
  4. import (
  5. "crypto/rand"
  6. "crypto/sha256"
  7. "crypto/subtle"
  8. "database/sql"
  9. "encoding/json"
  10. "time"
  11. "github.com/go-webauthn/webauthn/webauthn"
  12. "miniflux.app/v2/internal/timezone"
  13. )
  14. const (
  15. defaultSessionLanguage = "en_US"
  16. defaultSessionTheme = "system_serif"
  17. )
  18. // WebSession represents a browser session persisted in the web_sessions table.
  19. type WebSession struct {
  20. ID string
  21. SecretHash []byte
  22. CreatedAt time.Time
  23. UserAgent string
  24. IP string
  25. userID *int64
  26. state webSessionState
  27. dirty bool
  28. }
  29. // webSessionState stores transient browser session state as a JSON blob.
  30. type webSessionState struct {
  31. CSRF string `json:"csrf,omitempty"`
  32. SuccessMessage string `json:"success_message,omitempty"`
  33. ErrorMessage string `json:"error_message,omitempty"`
  34. OAuth2 *WebSessionOAuth2 `json:"oauth2,omitempty"`
  35. WebAuthn *webauthn.SessionData `json:"webauthn,omitempty"`
  36. LastForceRefreshAt *time.Time `json:"last_force_refresh_at,omitempty"`
  37. Language string `json:"language,omitempty"`
  38. Theme string `json:"theme,omitempty"`
  39. }
  40. // WebSessionOAuth2 stores transient OAuth2 flow state.
  41. type WebSessionOAuth2 struct {
  42. State string `json:"state,omitempty"`
  43. CodeVerifier string `json:"code_verifier,omitempty"`
  44. }
  45. // NewWebSession builds an unauthenticated browser session with a fresh
  46. // identity and returns it along with the raw session secret.
  47. func NewWebSession(userAgent, ip string) (*WebSession, string) {
  48. secret := rand.Text()
  49. session := &WebSession{
  50. ID: rand.Text(),
  51. SecretHash: hashWebSessionSecret(secret),
  52. UserAgent: userAgent,
  53. IP: ip,
  54. }
  55. session.state.CSRF = rand.Text()
  56. return session, secret
  57. }
  58. // Rotate assigns a new ID and secret in place, returning the previous ID
  59. // and the new raw secret. Rotating on authentication prevents session fixation.
  60. func (s *WebSession) Rotate() (oldID, newSecret string) {
  61. oldID = s.ID
  62. newSecret = rand.Text()
  63. s.ID = rand.Text()
  64. s.SecretHash = hashWebSessionSecret(newSecret)
  65. return oldID, newSecret
  66. }
  67. // VerifySecret reports whether the given raw secret matches the stored hash.
  68. func (s *WebSession) VerifySecret(secret string) bool {
  69. if secret == "" || len(s.SecretHash) == 0 {
  70. return false
  71. }
  72. actual := hashWebSessionSecret(secret)
  73. return subtle.ConstantTimeCompare(actual, s.SecretHash) == 1
  74. }
  75. func hashWebSessionSecret(secret string) []byte {
  76. sum := sha256.Sum256([]byte(secret))
  77. return sum[:]
  78. }
  79. // IsDirty reports whether the session has been modified since it was loaded.
  80. func (s *WebSession) IsDirty() bool {
  81. return s.dirty
  82. }
  83. // IsAuthenticated reports whether the session is bound to a user.
  84. func (s *WebSession) IsAuthenticated() bool {
  85. return s.userID != nil
  86. }
  87. // UserID returns the authenticated user ID and whether the session is bound to a user.
  88. func (s *WebSession) UserID() (int64, bool) {
  89. if s.userID == nil {
  90. return 0, false
  91. }
  92. return *s.userID, true
  93. }
  94. // NullUserID returns the session user ID as a sql.NullInt64 for storage writes.
  95. func (s *WebSession) NullUserID() sql.NullInt64 {
  96. if s.userID == nil {
  97. return sql.NullInt64{}
  98. }
  99. return sql.NullInt64{Int64: *s.userID, Valid: true}
  100. }
  101. // ScanUserID sets the session user ID from a sql.NullInt64 loaded from storage.
  102. func (s *WebSession) ScanUserID(v sql.NullInt64) {
  103. if !v.Valid {
  104. s.userID = nil
  105. return
  106. }
  107. id := v.Int64
  108. s.userID = &id
  109. }
  110. // UseTimezone converts creation date to the given timezone.
  111. func (s *WebSession) UseTimezone(tz string) {
  112. s.CreatedAt = timezone.Convert(tz, s.CreatedAt)
  113. }
  114. // CSRF returns the CSRF token for this session.
  115. func (s *WebSession) CSRF() string {
  116. return s.state.CSRF
  117. }
  118. // Language returns the session language, or a default when unset.
  119. func (s *WebSession) Language() string {
  120. if s.state.Language != "" {
  121. return s.state.Language
  122. }
  123. return defaultSessionLanguage
  124. }
  125. // Theme returns the session theme, or a default when unset.
  126. func (s *WebSession) Theme() string {
  127. if s.state.Theme != "" {
  128. return s.state.Theme
  129. }
  130. return defaultSessionTheme
  131. }
  132. // OAuth2State returns the OAuth2 state parameter, or empty if not in an OAuth2 flow.
  133. func (s *WebSession) OAuth2State() string {
  134. if s.state.OAuth2 != nil {
  135. return s.state.OAuth2.State
  136. }
  137. return ""
  138. }
  139. // OAuth2CodeVerifier returns the PKCE code verifier, or empty if not in an OAuth2 flow.
  140. func (s *WebSession) OAuth2CodeVerifier() string {
  141. if s.state.OAuth2 != nil {
  142. return s.state.OAuth2.CodeVerifier
  143. }
  144. return ""
  145. }
  146. // ConsumeWebAuthnSession returns and clears the pending WebAuthn session data.
  147. func (s *WebSession) ConsumeWebAuthnSession() *webauthn.SessionData {
  148. data := s.state.WebAuthn
  149. if data == nil {
  150. return nil
  151. }
  152. s.dirty = true
  153. s.state.WebAuthn = nil
  154. return data
  155. }
  156. // LastForceRefresh returns the last force refresh timestamp, or zero time if unset.
  157. func (s *WebSession) LastForceRefresh() time.Time {
  158. if s.state.LastForceRefreshAt != nil {
  159. return *s.state.LastForceRefreshAt
  160. }
  161. return time.Time{}
  162. }
  163. // ConsumeMessages returns and clears the success and error messages.
  164. func (s *WebSession) ConsumeMessages() (string, string) {
  165. successMessage := s.state.SuccessMessage
  166. errorMessage := s.state.ErrorMessage
  167. if successMessage != "" || errorMessage != "" {
  168. s.dirty = true
  169. s.state.SuccessMessage = ""
  170. s.state.ErrorMessage = ""
  171. }
  172. return successMessage, errorMessage
  173. }
  174. // SetLanguage updates the language.
  175. func (s *WebSession) SetLanguage(language string) {
  176. s.dirty = true
  177. s.state.Language = language
  178. }
  179. // SetTheme updates the theme.
  180. func (s *WebSession) SetTheme(theme string) {
  181. s.dirty = true
  182. s.state.Theme = theme
  183. }
  184. // SetSuccessMessage stores a success message shown on the next page load.
  185. func (s *WebSession) SetSuccessMessage(message string) {
  186. s.dirty = true
  187. s.state.SuccessMessage = message
  188. }
  189. // SetErrorMessage stores an error message shown on the next page load.
  190. func (s *WebSession) SetErrorMessage(message string) {
  191. s.dirty = true
  192. s.state.ErrorMessage = message
  193. }
  194. // StartOAuth2Flow stores the OAuth2 state parameter and PKCE code verifier.
  195. func (s *WebSession) StartOAuth2Flow(state, codeVerifier string) {
  196. s.dirty = true
  197. s.state.OAuth2 = &WebSessionOAuth2{
  198. State: state,
  199. CodeVerifier: codeVerifier,
  200. }
  201. }
  202. // ClearOAuth2Flow discards any pending OAuth2 flow state.
  203. func (s *WebSession) ClearOAuth2Flow() {
  204. s.dirty = true
  205. s.state.OAuth2 = nil
  206. }
  207. // SetUser binds the session to an authenticated user and copies their preferences.
  208. func (s *WebSession) SetUser(user *User) {
  209. if user == nil {
  210. return
  211. }
  212. s.dirty = true
  213. userID := user.ID
  214. s.userID = &userID
  215. s.state.Language = user.Language
  216. s.state.Theme = user.Theme
  217. }
  218. // ClearUser removes the user binding from the session.
  219. func (s *WebSession) ClearUser() {
  220. s.dirty = true
  221. s.userID = nil
  222. }
  223. // MarkForceRefreshed records the current time as the last force refresh.
  224. func (s *WebSession) MarkForceRefreshed() {
  225. s.dirty = true
  226. now := time.Now().UTC()
  227. s.state.LastForceRefreshAt = &now
  228. }
  229. // SetWebAuthn stores or clears WebAuthn session data.
  230. func (s *WebSession) SetWebAuthn(data *webauthn.SessionData) {
  231. s.dirty = true
  232. s.state.WebAuthn = data
  233. }
  234. // MarshalState serializes the session state to JSON for storage.
  235. func (s *WebSession) MarshalState() ([]byte, error) {
  236. return json.Marshal(s.state)
  237. }
  238. // UnmarshalState populates the session state from raw JSON bytes.
  239. func (s *WebSession) UnmarshalState(data []byte) error {
  240. s.state = webSessionState{}
  241. if len(data) == 0 {
  242. return nil
  243. }
  244. return json.Unmarshal(data, &s.state)
  245. }