Pow Session Toolkit v0.1.2 PowSessionToolkit.SessionPlugs View Source

Pow session manager implementation.

This implementation attempts to achieve:

  • parity with the security provided by session cookies in browsers, which means it should not be possible to steal a valid auth token from JavaScript code
  • not being limited to the 4kB limit for cookies in browsers
  • short lived stateless access tokens separate from long-lived refresh tokens
  • ability to revoke other sessions in the form of revoking refresh tokens so that token renewal is not possible
  • native clients do not need to handle cookies but can use simple bearer tokens for access / refresh

Token handling

This implementation uses asymmetric Phoenix tokens (refresh and access), where the refresh token is tracked using session_store(config) so that sessions can be forcibly logged out and so that refresh tokens are single-use only. Access tokens are stateless and are not tracked server-side.

All tokens are passed using the "authorization" header. However, the signatures needed to verify the integrity of the tokens is either transported to the client as part of the token itself (token format "header.payload.signature") or separately as a cookie (in which case the token format will be "header.payload"). This behaviour is set for the lifetime of the session when the session is created, by specifying :bearer or :cookie as conn.private[:pow_session_toolkit_token_signature_transport] and is enforced when a token is verified. This means that the signature of a refresh token of a session with transport mechanism "bearer" MUST be passed to refresh/2 as part of the bearer token, and will be rejected when passed using a cookie, and vice-versa.

For native apps or other clients that have a secure way of storing tokens, "bearer" is recommended. For browser clients, it is recommended to use the "cookie" signature transport mechanism, which will prevent the client from having to use an insecure storage mechanism like LocalStorage for token signatures. The rest of the token, however, can simply be held in memory or stored in whatever way the client fancies. In case of a successful XSS breach of the web application only the token payloads will be exposed. Those payloads cannot be used as authentication tokens because they have no validity without their signature components, which cannot be accessed from JavaScript. The signature is a HMAC, which cannot be generated without the server-side secret key, and the payload cannot be altered without invalidating the signature. Splitting the token signature from the header and payload also means that tokens can (really shouldn't, but can) grow larger than the 4kB limit for a cookie without problems, because only the signature is held in a cookie and not the payload.

Note that this does not mean that all security issues have been solved. It is still possible to use XSS attacks to make API requests with a valid auth header while the XSS code has access to the browser context in which the tokens live, because the signature cookie is sent automatically by the browser.

Cross-site request forgery

Cross-site request forgery issues are left for controllers to deal with when applicable. As per OWASP guidelines, setting a custom header is sufficient to protect against CSRF for an API. Since the "authorization" header with a bearer token qualifies, such issues (apart from login-CSRF!) should not arrise when using this implentation to secure an API, but can and will arise when using this implementation to secure Phoenix HTML applications. So use Plug.CSRFProtection in that case.

Config values

The following values must be set in the config (or in the application environment under the matching otp_app) (example or - in case of optionals - default values):

pow_session_toolkit: [
  access_token_ttl: 30 * 60,
  refresh_token_ttl: 2 * 30 * 24 * 60 * 60,
  refresh_signature_cookie_name: "_pow_session_toolkit_refresh_signature",
  access_signature_cookie_name: "_pow_session_toolkit_access_signature",
  refresh_path: "/api/v1/current_session/refresh",
  session_store: MyMnesiaCacheModule,
  # optional (defaults shown first)
  session_ttl: nil || 365 * 24 * 60 * 60,
  access_token_salt: "access_token",
  refresh_token_salt: "refresh_token",
  refresh_token_key_digest: :sha512 || :sha256 || :sha384,
  access_token_key_digest: :sha256 || :sha384 || :sha512
]

The *_ttl values are in seconds, except for session_ttl which can also be nil. The salts are not cryptographic salts but behave like token namespaces, separating refresh and access tokens.

Maximum session age

A session can optionally have an absolute maximum age, which is set when the session is first created and is not affected by refreshes. It is calculated as session_ttl + session creation timestamp. By default, no maximum age is set (it defaults to nil). The maximum age can be set globally in the config, as shown above. It can also, however, be set using conn.private[:pow_session_toolkit_session_ttl], permitting advanced handling by controllers. This makes it possible to have a different session max age for users with different credential levels, session types, access levels etc etc. Note that the value of conn.private[:pow_session_toolkit_session_ttl] overrides the global config value.

Link to this section Summary

Functions

Configures the connection for Pow, and fetches user.

Create or update a session. If conn.private.pow_session_toolkit_session exists, the session is updated, otherwise a new one is created. These values are set by refresh/2 when appropriate.

Delete the persistent session identified by the session_id in the access token payload.

Calls create/3 and assigns the current user.

Calls delete/2 and removes the current user assigned to the conn.

Calls fetch/2 and assigns the current user to the conn.

Fetch the session state from the access token in the "authorization" header. The token must not be older than access_ttl seconds and correctly signed. The signature must originate from the correct signature transport channel. The token payload is put in conn.private.pow_session_toolkit_access_token_payload and the user is assigned to conn.assigns.current_user.

Create new access / refresh tokens if a valid refresh token is found.

Link to this section Functions

Configures the connection for Pow, and fetches user.

:plug is appended to the passed configuration, so the current plug will be used in any subsequent calls to create, update and delete user credentials from the connection. The configuration is then set for the conn with Pow.Plug.put_config/2.

If a user can't be fetched with Pow.Plug.current_user/2, do_fetch/2 will be called.

Link to this function

create(conn, user, config)

View Source
create(Plug.Conn.t(), map(), Pow.Config.t()) :: {Plug.Conn.t(), map()}

Create or update a session. If conn.private.pow_session_toolkit_session exists, the session is updated, otherwise a new one is created. These values are set by refresh/2 when appropriate.

In both cases, new access / refresh tokens are created and stored in the conn's private map. The server-side session stored in session_store(config) is created / updated as well.

The tokens' signatures are split off and sent as cookies if the session's token signature transport mechanism is set to :cookie.

The session can optionally have a maximum age set when it is created.

For access token signatures, a cookie named "access_sig_cookie_name" is sent with the following options: [http_only: true, extra: "SameSite=Strict", secure: true].

For refresh token signatures, a cookie named "refresh_sig_cookie_name" is sent with the following options: [http_only: true, extra: "SameSite=Strict", secure: true].

Delete the persistent session identified by the session_id in the access token payload.

Note that the access token remains valid until it expires, it is left up to the client to drop the access token. It will no longer be possible to refresh the session, however.

Link to this function

do_create(conn, user, config)

View Source
do_create(Plug.Conn.t(), map(), Pow.Config.t()) :: Plug.Conn.t()

Calls create/3 and assigns the current user.

The user is assigned to the conn with Pow.Plug.assign_current_user/3.

Link to this function

do_delete(conn, config)

View Source
do_delete(Plug.Conn.t(), Pow.Config.t()) :: Plug.Conn.t()

Calls delete/2 and removes the current user assigned to the conn.

The user assigned is removed from the conn with Pow.Plug.assign_current_user/3.

Calls fetch/2 and assigns the current user to the conn.

The user is assigned to the conn with Pow.Plug.assign_current_user/3.

Link to this function

fetch(conn, config)

View Source
fetch(Plug.Conn.t(), Pow.Config.t()) :: {Plug.Conn.t(), map() | nil}

Fetch the session state from the access token in the "authorization" header. The token must not be older than access_ttl seconds and correctly signed. The signature must originate from the correct signature transport channel. The token payload is put in conn.private.pow_session_toolkit_access_token_payload and the user is assigned to conn.assigns.current_user.

The user is NOT fetched from the database, it is left for controllers to decide if this is needed.

Link to this function

refresh(conn, config)

View Source
refresh(Plug.Conn.t(), Pow.Config.t()) :: {Plug.Conn.t(), map() | nil}

Create new access / refresh tokens if a valid refresh token is found.

The token is read from the authorization header, and the token's signature either from the header or from cookie "@refresh_sig_cookie_name". The token signature source (bearer or cookie) must match the token_signature_transport specified in the token payload.

A refresh token can only be used to refresh a session once. A single refresh token id is stored in the server-side session by create/2 to enforce this.