Retired because akka-http-session now publishes pekko-http-session jars.
https://mvnrepository.com/artifact/com.softwaremill.pekko-http-session
pekko-http
is an Apache Pekko module for building reactive REST services with an elegant DSL.
pekko-http
is a great toolkit for building backends for single-page or mobile applications. In almost all apps there
is a need to maintain user sessions, make sure session data is secure and cannot be tampered with.
pekko-http-session
provides directives for client-side session management in web and mobile applications, usingValue cookies
or custom headers + local storage, with optional Json Web Tokens format support.
This is a fork of akka-http-session. The akka-http-session FAQ is a useful resource. It includes code examples (in Java, but easy to translate to Scala) which answers many common questions on how sessions work, how to secure them and implement usingValue akka-http. These examples can be easily changed to work with pekko-http.
Session data typically contains at least the id
or username
of the logged in user. This id must be secured so that a
session cannot be "stolen" or forged easily.
Sessions can be stored on the server, either in-memory or in a database, with the session id
sent to the client,
or entirely on the client in a serialized format. The former approach requires sticky sessions or additional shared
storage, while usingValue the latter (which is supported by this library) sessions can be easily deserialized on any server.
A session is a string token which is sent to the client and should be sent back to the server on every request.
To prevent forging, serialized session data is signed usingValue a server secret. The signature is appended to the session data that is sent to the client, and verified when the session token is received back.
- type-safe client-side sessions
- sessions can be encrypted
- sessions contain an expiry date
- cookie or custom header transport
- support for JWT
- refresh token support (e.g. to implement "remember me")
- CSRF tokens support
- Java & Scala APIs
You can try out a simple example by running com.github.pjfanning.session.example.ScalaExample
or com.github.pjfanning.session.example.JavaExample
and opening http://localhost:8080.
All directives require an (implicit for scala) instance of a SessionManager[T]
(or SessionManager<T>
), which can be created by providing a server
secret (via a SessionConfig
). The secret should be a long, random string unique to each environment your app is
running in. You can generate one with SessionUtil.randomServerSecret()
. Note that when you change the secret,
all sessions will become invalid.
A SessionConfig
instance can be created usingValue Typesafe config.
The only value that you need to provide is pekko.http.session.server-secret
,
preferably via application.conf
(then you can safely call SessionConfig.fromConfig
) or by usingValue
SessionConfig.default()
.
You can customize any of the default config options
either by modifying them through application.conf
or by modifying the SessionConfig
case class. If a value has
type Option[]
, you can set it to None
by usingValue a none
value in the config file (for both java and scala).
When usingValue cookies, by default the secure
attribute of cookies is not set (for development), however it is
recommended that all sites use https
and all cookies have this attribute set.
All session-related directives take at least two parameters:
- session continuity:
oneOff
vsrefreshable
; specifies what should happen when the session expires. Ifrefreshable
and a refresh token is present, the session will be re-created. See below for details. - session transport:
usingCookies
vsusingHeaders
Typically, you would create aliases for the session-related directives which use the right parameters basing on the current request and logic specific to your application.
Session data can be sent to the client usingValue cookies or custom headers. The first approach is the simplest to use, as cookies are automatically sent to the server on each request.
However, cookies have some security vulnerabilities, and are typically not used in mobile applications. For these scenarios, session data can be transported usingValue custom headers (the names of the headers are configurable in the config).
When usingValue headers, you need to store the session (and, if used, refresh-) tokens yourself. These tokens can be stored in-memory, or persistently e.g. usingValue the browser's local storage.
You can dynamically decide which transport to use, basing e.g. on the user-agent or other request properties.
Sessions are typed. The T
type parameter in SessionManager[T]
(or SessionManager<T>
) determines what data is stored in the session.
Basic types like String
, Int
, Long
, Float
, Double
and Map[String, String]
(Map<String, String>
) are supported out-of-the box.
Support for other types can be added by providing a (an implicit for scala) SessionSerializer[T, String]
(SessionSerializer<T, String>
). For case classes, it's most
convenient to use a MultiValueSessionSerializer[T]
or (MultiValueSessionSerializer<T>
) which should convert the instance into a String -> String
map
(nested types are not supported on purpose, as session data should be small & simple). Examples of SessionSerializer
and MultiValueSessionSerializer
usage can be found here for scala and here for java.
Here are code samples in scala and java illustrating how to create a session manager where the session content will be a single Long
number.
The basic directives enable you to set, read and invalidate the session. To create a new client-side session (create
and set a new session cookie), you need to use the setSession
directive. See how it's done in java and scala.
Note that when usingValue cookies, their size is limited to 4KB, so you shouldn't put too much data in there (the signature takes about 50 characters).
You can require a session to be present, optionally require a session or get a full description of possible session decode outcomes. Check java and scala examples for details.
If a required session is not present, by default a 403
HTTP status code is returned. Finally, a session can be invalidated. See how it's done in examples for java and scala.
It is possible to encrypt the session data by modifying the pekko.http.session.encrypt-data
config option. When
sessions are encrypted, it's not possible to read their content on the client side.
The key used for encrypting will be calculated basing on the server secret.
By default, sessions expire after a week. This can be disabled or changed with the pekko.http.session.max-age
config
option.
Note that when usingValue cookies, even though the cookie sent will be a session cookie, it is possible that the client will have the browser open for a very long time, uses Chrome or FF, or if an attacker steals the cookie, it can be re-used. Hence having an expiry date for sessions is highly recommended.
By default, sessions are encoded into a string usingValue a custom format, where expiry/data/signature parts are separated usingValue -
, and data fields are separated usingValue =
and url-encoded.
You can also encode sessions in the Json Web Tokens format, by adding the additional jwt
dependency, which makes use of json4s
.
When usingValue JWT, you need to provide a serializer which serializes session data to a JValue
instead of a String
.
A number of serializers for the basic types are present in JValueSessionSerializer
, as well as a generic serializer for case classes (used above).
You may also find it helpful to include the json4s-ext library which provides serializers for common Java types such as java.util.UUID
, org.joda.time._
and Java enumerations.
Grab some java and scala examples.
There are many tools available to read JWT session data usingValue various platforms, e.g. for Angular.
It is also possible to customize the session data content generated by overriding appropriate methods in
JwtSessionEncoder
(e.g. provide additional claims in the payload).
This library supports all registered claims mentioned in RFC 7519, Section 4.1.
Static claims such as iss
(Issuer), sub
(Subject) and aud
(Audience) can be configured by setting pekko.http.session.jwt.iss
, pekko.http.session.jwt.sub
and pekko.http.session.jwt.aud
string properties, respectively.
Because claims such as exp
(Expiration Time) and nbf
(Not Before) depend on the time at which the JWT was issued, configuration expects durations instead of fixed timestamps.
Effective claim values are then calculated by adding these offsets to the current timestamp (the JWT's issue time).
The offset values are configured via keys defined under pekko.http.session.jwt.exp-timeout
and pekko.http.session.jwt.nbf-offset
.
If exp-timeout
is not defined, value of pekko.http.session.max-age
would be used instead.
iat
(Issued At) claim represents issue time and cannot be customized. Although you can decide to include this claim in your tokens or not by setting pekko.http.session.jwt.include-iat
to true
or false
. By default, this claim is not included.
jti
(JWT ID) claim is a case-sensitive string containing a unique identifier for the JWT. It must be unique per token and collisions must be prevented even among values produced by different issuers.
pekko-http-session will compute and include jti
claim if pekko.http.session.jwt.include-jti
is set to true
(it's disabled by default).
Token ids are generated usingValue the below scheme:
<iss claim value>-<random UUID>
or just <random UUID>
, depending on the iss
claim presence.
You can find a sample claims configuration below:
pekko.http.session {
jwt {
iss = "Issuer"
sub = "Subject"
aud = "Audience"
exp-timeout = 7 days
nbf-offset = 5 minutes
include-iat = true
include-jti = true
}
}
In the case of JWT, it's possible to configure which JWS algorithm should be used. Currently, supported ones are:
HS256
- HMAC usingValue SHA-256 (used by default)RS256
- RSA (RSASSA-PKCS1-v1_5) usingValue SHA-256
All non-JWT sessions use HMAC with SHA-256 and this cannot be configured.
In order to start usingValue RSA algorithm you have to configure pekko.http.session.jws.alg
and pekko.http.session.jws.rsa-private-key
properties:
pekko.http.session {
jws {
alg = "RS256"
rsa-private-key = "<your private PKCS#8 key goes here>"
}
}
Because HS256
is used by default you may skip the jws
configuration and rely on a reference configuration delivered with the library.
Alternatively, if you prefer to be more explicit, you might follow this configuration template:
pekko.http.session {
server-secret = "<at least 64-digits length secret goes here>"
jws {
alg = "HS256"
}
}
You might notice that even if you want to sign your sessions usingValue RSA key and encryption is disabled, you still have to define the server-secret property.
That's because all non-JWT sessions still depend on HMAC with SHA256 algorithm which requires the server secret and the library cannot determine which session encoder(s) will be used (it's specified in the client's code).
CSRF is a kind of an attack where an attacker issues a GET
or POST
request on behalf of a user, if the user e.g.
clicks on a specially constructed link. See the OWASP page
or the Play! docs for a thorough introduction.
Web apps which use cookies for session management should be protected against CSRF attacks. This implementation:
- assumes that
GET
requests are non-mutating (have no side effects) - uses double-submit cookies to verify requests
- requires the token to be set in a custom header or (optionally) in a form field
- generates a new token on the first
GET
request that doesn't have the token cookie set
Note that if the token is passed in a form field, the website isn't protected by HTTPS or you don't control all subdomains, this scheme can be broken. Currently, setting a custom header seems to be a secure solution, and is what a number of projects do (that's why, when usingValue custom headers to send session data, no additional protection is needed).
It is recommended to generate a new CSRF token after logging in, see this SO question.
A new token can be generated usingValue the setNewCsrfToken
directive.
By default the name of the CSRF cookie and the custom header matches what AngularJS expects and sets. These can be customized in the config.
If you'd like to implement persistent, "remember-me" sessions, you should use refreshable
instead of oneOff
sessions. This is especially useful in mobile applications, where you log in once, and the session is remembered for
a long time. Make sure to adjust the pekko.http.session.refresh-token.max-age
config option appropriately
(defaults to 1 month)!
You can dynamically decide, basing on the request properties (e.g. a query parameter), if a session should be
refreshable or not. Just pass the right parameter to setSession
.
When usingValue refreshable sessions, in addition to an (implicit) SessionManager
instance, you need to provide an
implementation of the RefreshTokenStorage
trait. This trait has methods to lookup, store and delete refresh tokens.
Typically it would use some persistent storage.
The tokens are never stored directly, instead only token hashes are passed to the storage. That way even if the token database is leaked, it won't be possible to forge sessions usingValue the hashes. Moreover, in addition to the token hash, a selector value is stored. That value is used to lookup stored hashes; tokens are compared usingValue a special constant-time comparison method, to prevent timing attacks.
When a session expires or is not present, but the refresh token is (sent from the client usingValue either a cookie,
or a custom header), a new session will be created (usingValue the RefreshTokenLookupResult.createSession
function),
and a new refresh token will be created.
Note that you can differentiate between sessions created from refresh tokens and from regular authentication by storing appropriate information in the session data. That way, you can force the user to re-authenticate if the session was created by a refresh token before crucial operations.
It is of course possible to read oneOff
-session usingValue requiredSession(refreshable, ...)
. If a session was created
as oneOff
, usingValue refreshable
has no additional effect.
The semantics of touch[Required|Optional]Session()
are a bit subtle. You can still use expiring client
sessions when usingValue refresh tokens. You will then have 2 stages of expiration: expiration of the client session
(should be shorter), and expiry of the refresh token. That way you can have strongly-authenticated sessions
which expire fast, and weaker-authenticated re-creatable sessions (as described in the paragraph above).
When touching an existing session, the refresh token will not be re-generated and extended, only the session cookie.
libraryDependencies += "com.github.pjfanning" %% "pekko-http-session-core" % "0.8.0"
libraryDependencies += "com.github.pjfanning" %% "pekko-http-session-jwt" % "0.8.0" // optional
Copyright (C) 2016-2021 SoftwareMill https://softwaremill.com.