| 1 | module sessions |
| 2 | |
| 3 | import crypto.sha256 |
| 4 | import crypto.hmac |
| 5 | import encoding.base64 |
| 6 | import net.http |
| 7 | import rand |
| 8 | import time |
| 9 | |
| 10 | const 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 |
| 14 | pub 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. |
| 25 | pub 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 | // ``` |
| 48 | pub struct CurrentSession[T] { |
| 49 | pub 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. |
| 55 | pub struct CookieOptions { |
| 56 | pub: |
| 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] |
| 76 | pub struct Sessions[T] { |
| 77 | pub: |
| 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 |
| 85 | pub 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. |
| 90 | pub 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. |
| 113 | pub 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. |
| 120 | pub 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. |
| 126 | pub 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. |
| 134 | pub 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. |
| 144 | pub 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. |
| 164 | pub 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. |
| 174 | pub 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 | |