Standard library · Network

net/http/session

User sessions over a cookie: keeping data between requests, and a CSRF token.

import net.http.session

A session keeps each visitor's data on the server and gives the browser nothing but a random id in an HttpOnly cookie. Use it for logins, carts and form tokens on a site built with net/http. When the client is not a browser, or several servers must accept the same login without sharing memory, a signed token from jwt is usually the better fit.

The flow in a handler: Start finds the visitor's session from the cookie or makes a new one (and sets the cookie), you read and change values with Value and SetValue, and Save writes the session back. Values are strings. A session expires after max_age seconds without a request; every request that finds it starts the clock again.

Things people get wrong:

  • Forgetting Save. Start returns a copy; changes reach the store only when you save it.
  • Keeping request strings. Param, Form and the like are freed when the handler returns, so copy them with str.Clone before you store them (see the cart).
  • Not calling Regenerate after login, which leaves an id an attacker may have planted in the browser valid.
  • Expecting the sessions to survive. They live in this process's memory: a restart logs everyone out and two server processes do not share them. Two requests from one visitor at the same moment each save their own copy, and the later Save wins.

Log in and log out

The usual shape: the login handler checks the password, calls Regenerate so the user gets a fresh id, stores who they are with SetValue and redirects. Every protected page calls Start and checks HasValue. Logging out calls End, which deletes the session on the server and clears the cookie.

net-http-session-login.salam
import net.http
import net.http.session
import net.router

func login(ctx: i64):
    if http.Form(ctx, "user") != "sara" or http.Form(ctx, "password") != "open sesame":
        http.SetStatus(ctx, 401)
        http.Text(ctx, "wrong user or password")
        ret
    end
    mut s := session.Start(ctx)
    session.Regenerate(ctx, s)
    session.SetValue(s, "user", "sara")
    session.Save(s)
    http.Redirect(ctx, "/me")
end

func me(ctx: i64):
    s := session.Start(ctx)
    if not session.HasValue(s, "user"):
        http.SetStatus(ctx, 401)
        http.Text(ctx, "please log in")
        ret
    end
    http.Text(ctx, "hello " + session.Value(s, "user"))
end

func logout(ctx: i64):
    session.End(ctx)
    http.Text(ctx, "logged out")
end

func main:
    mut r := router.NewRouter()
    router.Post(r, "/login", login)
    router.Get(r, "/me", me)
    router.Post(r, "/logout", logout)
    mut srv := http.NewServer(19770)
    http.Use(srv, r)
    _ := http.RunBackground(srv)
    base := "http://127.0.0.1:19770"
    mut browser := http.NewClient()
    browser.SetHeader("Content-Type", "application/x-www-form-urlencoded")
    println http.Body(browser.Get(base + "/me"))
    bad := browser.Post(base + "/login", "user=sara&password=guess")
    println http.Status(bad), http.Body(bad)
    ok := browser.Post(base + "/login", "user=sara&password=open+sesame")
    println http.Status(ok), http.GetHeader(ok, "location")
    println http.Body(browser.Get(base + "/me"))
    println http.Body(browser.Post(base + "/logout", ""))
    println http.Body(browser.Get(base + "/me"))
end
Outputplease log in 401 wrong user or password 302 /me hello sara logged out please log in

http.NewClient keeps cookies between requests the way a browser does, which is why the second /me sees the login. A plain http.Get sends no cookies and always looks like a new visitor.

Protect forms with a CSRF token

The browser sends the session cookie with every request to your site, even one started by a form on another site. A CSRF token stops that: CSRFToken stores a random token in the session, you put it in a hidden field, and the handler that receives the form checks it with CSRFVerify. A forged form does not know the token and is refused.

net-http-session-csrf.salam
import net.http
import net.http.session
import net.router

func form(ctx: i64):
    mut s := session.Start(ctx)
    token := session.CSRFToken(s)
    session.Save(s)
    http.Html(ctx, `<form method="post" action="/email"><input type="hidden" name="csrf" value="` + token + `"><input name="email"></form>`)
end

func change_email(ctx: i64):
    s := session.Start(ctx)
    if not session.CSRFVerify(s, http.Form(ctx, "csrf")):
        http.SetStatus(ctx, 403)
        http.Text(ctx, "the form has expired, reload the page")
        ret
    end
    http.Text(ctx, "email changed to " + http.Form(ctx, "email"))
end

func token_in(html: str): str:
    start := html.find(`value="`) + 7
    rest := html.substr(start, html.len() - start)
    ret rest.substr(0, rest.find(`"`))
end

func main:
    mut r := router.NewRouter()
    router.Get(r, "/email", form)
    router.Post(r, "/email", change_email)
    mut srv := http.NewServer(19771)
    http.Use(srv, r)
    _ := http.RunBackground(srv)
    url := "http://127.0.0.1:19771/email"
    mut browser := http.NewClient()
    browser.SetHeader("Content-Type", "application/x-www-form-urlencoded")
    token := token_in(http.Body(browser.Get(url)))
    println "token length:", token.len()
    forged := browser.Post(url, "email=evil%40example.com")
    println http.Status(forged), http.Body(forged)
    real := browser.Post(url, "csrf=" + token + "&email=sara%40example.com")
    println http.Status(real), http.Body(real)
end
Outputtoken length: 48 403 the form has expired, reload the page 200 email changed to sara@example.com

Call Save after CSRFToken, because the first call creates the token in your copy of the session.

Keep data across requests: a shopping cart

Values are strings, so numbers go through conv.Atoi and back. Each visitor has a separate session, so two clients filling carts at the same time never see each other's items. ClearValues empties a session but keeps the user's id.

net-http-session-cart.salam
import net.http
import net.http.session
import net.router
import conv
import str

func add(ctx: i64):
    item := str.Clone(http.Param(ctx, "item"))
    mut s := session.Start(ctx)
    mut count := 0
    if session.HasValue(s, item):
        count = conv.Atoi(session.Value(s, item))
    end
    session.SetValue(s, item, "" + (count + 1))
    session.Save(s)
    http.Text(ctx, "added " + item)
end

func cart(ctx: i64):
    s := session.Start(ctx)
    mut text := ""
    each item in ["tea", "bread", "cheese"]:
        if session.HasValue(s, item):
            text = text + (text == "" ? "" : ", ") + item + " x" + session.Value(s, item)
        end
    end
    http.Text(ctx, text == "" ? "the cart is empty" : text)
end

func empty(ctx: i64):
    mut s := session.Start(ctx)
    session.ClearValues(s)
    session.Save(s)
    http.Text(ctx, "emptied")
end

func main:
    mut r := router.NewRouter()
    router.Post(r, "/cart/:item", add)
    router.Get(r, "/cart", cart)
    router.Delete(r, "/cart", empty)
    mut srv := http.NewServer(19772)
    http.Use(srv, r)
    _ := http.RunBackground(srv)
    base := "http://127.0.0.1:19772/cart"
    mut sara := http.NewClient()
    mut reza := http.NewClient()
    _ := sara.Post(base + "/tea", "")
    _ := sara.Post(base + "/tea", "")
    _ := sara.Post(base + "/bread", "")
    _ := reza.Post(base + "/cheese", "")
    println "sara:", http.Body(sara.Get(base))
    println "reza:", http.Body(reza.Get(base))
    println http.Body(sara.Delete(base))
    println "sara:", http.Body(sara.Get(base))
    println "reza:", http.Body(reza.Get(base))
end
Outputsara: tea x2, bread x1 reza: cheese x1 emptied sara: the cart is empty reza: cheese x1

Note the str.Clone around Param. Values read from the request are freed when the handler returns, and a session outlives the request, so copy anything from the request before you use it as a key or value. Without the copy the cart keys point at freed memory and the cart looks empty.

Cookie settings and lifetime

Configure sets the cookie name, how long a session lives without activity, whether the cookie is sent over HTTPS only, and its SameSite mode. Call it once, before the server starts. On a site served over HTTPS, pass true for secure; Strict keeps the cookie off every request that starts on another site, and Lax, the default, still sends it when the user follows a link.

net-http-session-config.salam
import net.http
import net.http.session
import net.router
import str

func hello(ctx: i64):
    s := session.Start(ctx)
    http.Text(ctx, "session " + (s.id.len() > 0))
end

func main:
    session.Configure("shop_sid", 1800, true, "Strict")
    mut r := router.NewRouter()
    router.Get(r, "/", hello)
    mut srv := http.NewServer(19773)
    http.Use(srv, r)
    _ := http.RunBackground(srv)
    res := http.Get("http://127.0.0.1:19773/")
    println http.Body(res)
    line := http.SetCookieLines(res).get(0)
    each part in str.Split(line, "; "):
        if part.starts_with("shop_sid="):
            println "shop_sid=<random id>"
        else not part.starts_with("Expires="):
            println part
        end
    end
    mut s := session.New()
    session.SetValue(s, "theme", "dark")
    session.Save(s)
    println "kept for", s.max_age, "seconds"
    println "removed now:", session.CleanupExpired()
    session.Destroy(s.id)
    mut found := true
    _ := session.Get(s.id, found)
    println "after Destroy:", found
end
Outputsession true shop_sid=<random id> Path=/ Max-Age=1800 SameSite=Strict Secure HttpOnly kept for 1800 seconds removed now: 0 after Destroy: false

The cookie is always HttpOnly, so scripts in the page cannot read the id. Expired sessions are removed every hundred calls to New or Get; CleanupExpired does it at once and returns how many it removed. Sessions live in the memory of one process, so a restart logs everyone out, and two copies of the server behind a load balancer do not share them. For that, keep the data in a database such as db/redis.

Types

SessionData

struct SessionData
The keys and values of the session.
NameTypeDescription
itemsHashMapThe values.
net-http-session-Session.salam
import net.http
import net.http.session
import net.router
import conv

func visit(ctx: i64):
    mut s := session.Start(ctx)
    mut count := 1
    if session.HasValue(s, "visits"):
        count = conv.Atoi(session.Value(s, "visits")) + 1
    end
    session.SetValue(s, "visits", "" + count)
    session.Save(s)
    http.Text(ctx, "visit " + count)
end

func login(ctx: i64):
    mut s := session.Start(ctx)
    session.Regenerate(ctx, s)
    session.SetValue(s, "user", "ali")
    token := session.CSRFToken(s)
    session.Save(s)
    http.Text(ctx, "" + session.CSRFVerify(s, token) + " " + session.CSRFVerify(s, "x"))
end

func logout(ctx: i64):
    session.End(ctx)
    http.Text(ctx, "goodbye")
end

func main:
    session.Configure("sid", 3600, false, "Lax")
    mut r := router.NewRouter()
    router.Get(r, "/", visit)
    router.Post(r, "/login", login)
    router.Post(r, "/logout", logout)
    mut srv := http.NewServer(19626)
    http.Use(srv, r)
    _ := http.RunBackground(srv)
    mut c := http.NewClient()
    base := "http://127.0.0.1:19626"
    println http.Body(c.Get(base + "/"))
    println http.Body(c.Get(base + "/"))
    println http.Body(c.Post(base + "/login", ""))
    println http.Body(c.Get(base + "/"))
    println http.Body(c.Post(base + "/logout", ""))
    println http.Body(c.Get(base + "/"))
    mut s := session.New()
    session.SetValue(s, "a", "1")
    session.SetValue(s, "b", "2")
    println session.DeleteValue(s, "a"), s.data.items.size()
    session.Save(s)
    mut found := false
    back := session.Get(s.id, found)
    println found, session.Value(back, "b"), back.max_age, s.id.len() > 0
    session.ClearValues(s)
    session.Destroy(s.id)
    _ := session.Get(s.id, found)
    println found, session.CleanupExpired() >= 0
    println session.DefaultCookieName, session.DefaultMaxAge
end
Outputvisit 1 visit 2 true false visit 3 goodbye visit 1 true 1 true 2 3600 true false true salam_sid 86400

Session

struct Session
One user's session.
NameTypeDescription
idstr = ""The random id that sits in the cookie.
dataSessionDataThe session data.
created_ati64 = 0When it was made.
last_seeni64 = 0When it was last used.
max_ageint = 0How many seconds the session lives without a request; 0 or less never expires.

The example for this item is shown with the ones above: net-http-session-Session.salam

Constants and variables

DefaultCookieName

const DefaultCookieName: str = "salam_sid"
The default name of the session cookie.

The example for this item is shown with the ones above: net-http-session-Session.salam

DefaultMaxAge

const DefaultMaxAge: i32 = 86400
The default lifetime: one day.

The example for this item is shown with the ones above: net-http-session-Session.salam

Functions

Configure

func Configure(cookie_name: str, max_age_seconds: int, secure: bool, same_site: str)
Sets the cookie name, the idle lifetime in seconds, whether the cookie is HTTPS only, and its SameSite mode, for the whole program. Sessions made earlier keep their old lifetime, so call it before the server starts.

The example for this item is shown with the ones above: net-http-session-Session.salam

CleanupExpired

func CleanupExpired(): int
Deletes the expired sessions and returns how many.

The example for this item is shown with the ones above: net-http-session-Session.salam

Get

func Get(id: str, ok &: bool): Session
Finds a session by id and marks it as used now; ok is false when there is none or it has expired.

The example for this item is shown with the ones above: net-http-session-Session.salam

Save

func Save(s: Session)
Writes the session back to the store, replacing what was there.

The example for this item is shown with the ones above: net-http-session-Session.salam

Value

func Value(s: Session, key: str): str
The value of one key; an empty string when there is none.

The example for this item is shown with the ones above: net-http-session-Session.salam

HasValue

func HasValue(s: Session, key: str): bool
True when the key is there.

The example for this item is shown with the ones above: net-http-session-Session.salam

SetValue

func SetValue(s &: Session, key: str, value: str)
Sets the value of a key.

The example for this item is shown with the ones above: net-http-session-Session.salam

DeleteValue

func DeleteValue(s &: Session, key: str): bool
Takes the key away and says whether it was there.

The example for this item is shown with the ones above: net-http-session-Session.salam

Start

func Start(h: i64): Session
Finds the session of this request from the cookie or makes a new one, and sets the cookie.

The example for this item is shown with the ones above: net-http-session-Session.salam

End

func End(h: i64)
Deletes the session and expires the cookie in the browser; for logging out.

The example for this item is shown with the ones above: net-http-session-Session.salam

Regenerate

func Regenerate(h: i64, s &: Session)
Gives the session a fresh id, deletes the earlier one and sets the new cookie; the values stay. Call it after logging in.

The example for this item is shown with the ones above: net-http-session-Session.salam

CSRFToken

func CSRFToken(s &: Session): str
Returns the CSRF token of the session, making one when there is none; put it in your forms.

The example for this item is shown with the ones above: net-http-session-Session.salam

CSRFVerify

func CSRFVerify(s: Session, token: str): bool
True when the token matches the session token, compared in constant time; false when either is empty.

The example for this item is shown with the ones above: net-http-session-Session.salam