v2 / vlib / x / sessions / sessions.v
197 lines · 173 sloc · 5.98 KB · 07c796b670d9e498ccb25605af189617f61ec295
Raw
1module sessions
2
3import crypto.sha256
4import crypto.hmac
5import encoding.base64
6import net.http
7import rand
8import time
9
10const session_id_length = 32
11
12// new_session_id creates and returns a random session id and its signed version.
13// You can directly use the signed version as a cookie value
14pub fn new_session_id(secret []u8) (string, string) {
15 sid := rand.hex(session_id_length)
16
17 hashed := hmac.new(secret, sid.bytes(), sha256.sum, sha256.block_size)
18
19 // separate session id and hmac with a `.`
20 return sid, '${sid}.${base64.url_encode(hashed)}'
21}
22
23// verify_session_id verifies the signed session id with `secret`.
24// This function returns the session id and if the session id is valid.
25pub fn verify_session_id(raw_sid string, secret []u8) (string, bool) {
26 parts := raw_sid.split('.')
27 if parts.len != 2 {
28 return '', false
29 }
30
31 sid := parts[0]
32 actual_hmac := base64.url_decode(parts[1])
33
34 new_hmac := hmac.new(secret, sid.bytes(), sha256.sum, sha256.block_size)
35 // use `hmac.equal` to prevent leaking timing information
36 return sid, hmac.equal(actual_hmac, new_hmac)
37}
38
39// CurrentSession contains the session data during a request.
40// If you use veb you could embed it on your Context struct to have easy access to the session id and data.
41// Usage example:
42// ```v
43// struct Context {
44// veb.Context
45// sessions.CurrentSessions[User]
46// }
47// ```
48pub struct CurrentSession[T] {
49pub mut:
50 session_id string
51 session_data ?T
52}
53
54// CookieOptions contains the default settings for the cookie created in the `Sessions` struct.
55pub struct CookieOptions {
56pub:
57 cookie_name string = 'sid'
58 domain string
59 http_only bool = true
60 path string = '/'
61 same_site http.SameSite = .same_site_strict_mode
62 secure bool
63}
64
65// Sessions can be used to easily integrate sessions with veb.
66// This struct contains the store that holds all session data it also provides
67// an easy way to manage sessions in your veb app.
68// Usage example:
69// ```v
70// pub struct App {
71// pub mut:
72// sessions &sessions.Sessions[User]
73// }
74// ```
75@[heap]
76pub struct Sessions[T] {
77pub:
78 secret []u8 @[required]
79 cookie_options CookieOptions
80 // max age of session data and id, default is 30 days
81 max_age time.Duration = time.hour * 24 * 30
82 // set to true if you want to create a session if there isn't any data stored yet.
83 // Also called pre-sessions
84 save_uninitialized bool
85pub mut:
86 store Store[T] @[required]
87}
88
89// set_session_id generates a new session id and set a Set-Cookie header on the response.
90pub fn (mut s Sessions[T]) set_session_id[X](mut ctx X) string {
91 sid, signed := new_session_id(s.secret)
92 ctx.CurrentSession.session_id = sid
93
94 ctx.set_cookie(http.Cookie{
95 value: signed
96 max_age: int(s.max_age / time.second)
97 domain: s.cookie_options.domain
98 http_only: s.cookie_options.http_only
99 name: s.cookie_options.cookie_name
100 path: s.cookie_options.path
101 same_site: s.cookie_options.same_site
102 secure: s.cookie_options.secure
103 })
104 // indicate that the response should not be cached: we don't want the session id cookie
105 // to be cached by the browser, or any other agent
106 // https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.1.4
107 ctx.res.header.add(.cache_control, 'no-cache="Set-Cookie"')
108
109 return sid
110}
111
112// validate_session validates the current session, returns the session id and the validation status.
113pub fn (mut s Sessions[T]) validate_session[X](ctx X) (string, bool) {
114 cookie := ctx.get_cookie(s.cookie_options.cookie_name) or { return '', false }
115
116 return verify_session_id(cookie, s.secret)
117}
118
119// get the data associated with the current session, if it exists.
120pub fn (mut s Sessions[T]) get[X](ctx X) !T {
121 sid := s.get_session_id(ctx) or { return error('cannot find session id') }
122 return s.store.get(sid, s.max_age)!
123}
124
125// destroy the data for the current session.
126pub fn (mut s Sessions[T]) destroy[X](mut ctx X) ! {
127 if sid := s.get_session_id(ctx) {
128 s.store.destroy(sid)!
129 ctx.session_data = none
130 }
131}
132
133// logout destroys the data for the current session and removes the session id Cookie.
134pub fn (mut s Sessions[T]) logout[X](mut ctx X) ! {
135 s.destroy(mut ctx)!
136 ctx.set_cookie(http.Cookie{
137 name: s.cookie_options.cookie_name
138 value: ''
139 expires: time.unix(0)
140 })
141}
142
143// save `data` for the current session.
144pub fn (mut s Sessions[T]) save[X](mut ctx X, data T) ! {
145 if sid := s.get_session_id(ctx) {
146 s.store.set(sid, data)!
147 ctx.CurrentSession.session_data = data
148 } else {
149 if s.save_uninitialized == false {
150 // no valid session id, but the user only wants to create a session
151 // when data is saved. So we create the session here
152 sid := s.set_session_id(mut ctx)
153 s.store.set(sid, data)!
154 ctx.CurrentSession.session_data = data
155 }
156 eprintln('[veb.sessions] error: trying to save data without a valid session!')
157 }
158}
159
160// resave saves `data` for the current session and reset the session id.
161// You should use this function when the authentication or authorization status changes
162// e.g. when a user signs in or switches between accounts/permissions.
163// This function also destroys the data associated to the old session id.
164pub fn (mut s Sessions[T]) resave[X](mut ctx X, data T) ! {
165 if sid := s.get_session_id(ctx) {
166 s.store.destroy(sid)!
167 }
168
169 s.save(mut ctx, data)
170}
171
172// get_session_id retrieves the current session id, if it is set.
173// The HMAC signature is verified when extracting from cookies.
174pub fn (s &Sessions[T]) get_session_id[X](ctx X) ?string {
175 // first check session id from `ctx`
176 sid_from_ctx := ctx.CurrentSession.session_id
177 if sid_from_ctx != '' {
178 return sid_from_ctx
179 } else if cookie := ctx.get_cookie(s.cookie_options.cookie_name) {
180 // check request headers for the session_id cookie and verify HMAC signature
181 sid, valid := verify_session_id(cookie, s.secret)
182 if valid {
183 return sid
184 }
185 return none
186 } else {
187 // check the Set-Cookie headers on the response for a session id
188 for cookie in ctx.res.cookies() {
189 if cookie.name == s.cookie_options.cookie_name {
190 return cookie.value
191 }
192 }
193
194 // No session id is set
195 return none
196 }
197}
198