You can manage server state with HTTP cookies using this Server.Session API. Server-rendered routes have a Server.Request.Request
argument that lets you inspect the incoming HTTP request, and return a response using the Server.Response.Response
type.
This API provides a higher-level abstraction for extracting data from the HTTP request, and setting data in the HTTP response.
It manages the session through key-value data stored in cookies, and lets you insert
, update
, and remove
values from the Session. It also provides an abstraction for flash session values through withFlash
.
Using these functions, you can store and read session data in cookies to maintain state between requests.
import Server.Session as Session
secrets : BackendTask FatalError (List String)
secrets =
Env.expect "SESSION_SECRET"
|> BackendTask.allowFatal
|> BackendTask.map List.singleton
type alias Data =
{ darkMode : Bool }
data : RouteParams -> Request -> BackendTask (Response Data ErrorPage)
data routeParams request =
request
|> Session.withSession
{ name = "mysession"
, secrets = secrets
, options = Nothing
}
(\session ->
let
darkMode : Bool
darkMode =
(session |> Session.get "mode" |> Maybe.withDefault "light")
== "dark"
in
( session
, { darkMode = darkMode }
)
)
The elm-pages framework will manage signing these cookies using the secrets : BackendTask (List String)
you pass in.
That means that the values you set in your session will be directly visible to anyone who has access to the cookie
(so don't directly store sensitive data in your session). Since the session cookie is signed using the secret you provide,
the cookie will be invalidated if it is tampered with because it won't match when elm-pages verifies that it has been
signed with your secrets. Of course you need to provide secure secrets and treat your secrets with care.
The first String in secrets : BackendTask (List String)
will be used to sign sessions, while the remaining String's will
still be used to attempt to "unsign" the cookies. So if you have a single secret:
Session.withSession
{ name = "mysession"
, secrets =
BackendTask.map List.singleton
(Env.expect "SESSION_SECRET2022-09-01")
, options = Nothing
}
Then you add a second secret
Session.withSession
{ name = "mysession"
, secrets =
BackendTask.map2
(\newSecret oldSecret -> [ newSecret, oldSecret ])
(Env.expect "SESSION_SECRET2022-12-01")
(Env.expect "SESSION_SECRET2022-09-01")
, options = Nothing
}
The new secret (2022-12-01
) will be used to sign all requests. This API always re-signs using the newest secret in the list
whenever a new request comes in (even if the Session key-value pairs are unchanged), so these cookies get "refreshed" with the latest
signing secret when a new request comes in.
However, incoming requests with a cookie signed using the old secret (2022-09-01
) will still successfully be unsigned
because they are still in the rotation (and then subsequently "refreshed" and signed using the new secret).
This allows you to rotate your session secrets (for security purposes). When a secret goes out of the rotation, it will invalidate all cookies signed with that. For example, if we remove our old secret from the rotation:
Session.withSession
{ name = "mysession"
, secrets =
BackendTask.map List.singleton
(Env.expect "SESSION_SECRET2022-12-01")
, options = Nothing
}
And then a user makes a request but had a session signed with our old secret (2022-09-01
), the session will be invalid
(so withSession
would parse the session for that request as Nothing
). It's standard for cookies to have an expiration date,
so there's nothing wrong with an old session expiring (and the browser will eventually delete old cookies), just be aware of that when rotating secrets.
withSession : { name : String, secrets : BackendTask error (List String), options : Maybe Server.SetCookie.Options } -> (Session -> BackendTask error ( Session, Server.Response.Response data errorPage )) -> Server.Request.Request -> BackendTask error (Server.Response.Response data errorPage)
The main function for using sessions. If you need more fine-grained control over cases where a session can't be loaded, see
withSessionResult
.
withSessionResult : { name : String, secrets : BackendTask error (List String), options : Maybe Server.SetCookie.Options } -> (Result NotLoadedReason Session -> BackendTask error ( Session, Server.Response.Response data errorPage )) -> Server.Request.Request -> BackendTask error (Server.Response.Response data errorPage)
Same as withSession
, but gives you an Err
with the reason why the Session couldn't be loaded instead of
using Session.empty
as a default in the cases where there is an error loading the session.
A session won't load if there is no session, or if it cannot be unsigned with your secrets. This could be because the cookie was tampered with or otherwise corrupted, or because the cookie was signed with a secret that is no longer in the rotation.
withSessionResult
will return a Result
with this type if it can't load a session.
Represents a Session with key-value Strings.
Use with withSession
to read in the Session
, and encode any changes you make to the Session
back through cookie storage
via the outgoing HTTP response.
empty : Session
An empty Session
with no key-value pairs.
get : String -> Session -> Maybe String
Retrieve a String value from the session for the given key (or Nothing
if the key is not present).
(session
|> Session.get "mode"
|> Maybe.withDefault "light"
)
== "dark"
insert : String -> String -> Session -> Session
Insert a value under the given key in the Session
.
session
|> Session.insert "mode" "dark"
remove : String -> Session -> Session
Remove a key from the Session
.
update : String -> (Maybe String -> Maybe String) -> Session -> Session
Update the Session
, given a Maybe String
of the current value for the given key, and returning a Maybe String
.
If you return Nothing
, the key-value pair will be removed from the Session
(or left out if it didn't exist in the first place).
session
|> Session.update "mode"
(\mode ->
case mode of
Just "dark" ->
Just "light"
Just "light" ->
Just "dark"
Nothing ->
Just "dark"
)
withFlash : String -> String -> Session -> Session
Flash session values are values that are only available for the next request.
session
|> Session.withFlash "message" "Your payment was successful!"