Shared sessions are database connections that are shared amongst many users. Typically these are used by web application servers. When a user does something that requires a database interaction, a database connection is released from the pool, temporarily assigned to that user's session, and when the interaction is complete, returned to the pool.
A further complication is that many applications will perform multiple parallel database operations for a page, each requiring its own connection.
What this means is that:
What the last item means is that the order of requests arriving at the database server may not, due to network lag and application task handling, be the same order in which the user session initiated those requests.
There is only one fundamental requirement of a session management protocol. That is, that the session should know at all times which user is accessing the system.
What this means is:
A further requirement is that it must all be done with as little overhead as possible.
Ordinarily, web systems aim to protect themselves from user-space only. It is assumed that we are protected from bad actors in the network layer by appropriate use of TLS, and that if the server is compromised the game is over anyway.
However, by placing our primary security in the database server, and the database server in a secured part of our network, we can actually hope to protect ourselves against compromised application servers.
If this sounds paranoid, then consider this:
Given that with Veil2
we
can attempt to protect ourselves from a
compromised server, we should try to do so.
This is what Veil2
provides,
out-of-the-box, for session management:
There are 3 functions that your application server's session management must call:
And the calling protocol has four distinct stages.
We begin a session by calling
create_session()
. This returns a
session_id
and
session_token
which we must record and
use on subsequent calls.
Sessions remain active, with minimal overhead, until they
time-out. The timeout period is defined in the
veil2.system_parameters
table, in the
parameter called shared session timeout
.
Each time a connection is opened, the timeout is reset. If
a session times-out while a connection is active, the
connection will be unaffected, but re-connections will no
longer be possible.
Having created a session and been given a
session_id
by
create_session()
, we authenticate and
open the session by calling
open_connection()
with parameters
appropriate to the chosen authentication method. If this
succeeds, our connection is considered open and we can start
executing queries.
What this means is that it takes 2 database round-trips before we can begin executing queries on the user's behalf. While this may seem an unreasonable overhead, it only occurs when we first create a session. Subsequent usage requires only a single round-trip, which, given that the database has to be informed of which user is connected, seems like a pretty reasonable and unavoidable overhead.
When we are done and are ready to return the database
connection to the pool for use by another user, we call
close_connection()
. This is important.
If you do not do this everything will continue to work, but
the connection can be taken over by another user, thereby
inheriting the previous user's access rights.
As long as a session has not timed-out, new connections can be opened using that session. These re-opened connections do not require re-authentication using the original credentials. Instead we use a proof that the session is being used by the original authenticator. This may be computationally cheaper than providing the original credentials, but more importantly it means that those credentials are not being continually re-transmitted. To prevent replay attacks we use a nonce as part of the authentication token. Any attempt to re-use a nonce will be noticed and the connection attempt will fail.
Note that multiple simultaneous connections can be opened on the same session. The only requirement is that each is opened with its own nonce, and that the nonces are more-or-less sequentially numbered.
We provide the following parameters to
create_session()
:
username
;
This is a string that uniquely identifies the user
within the context provided by the context parameters.
This is converted into an
accessor_id
by the function
veil2.get_accessor()
which you must define as part of your implementation.
authent_type
;
This is the name of an authentication method recorded
in veil2.authentication_types
,
and for which an authentication function has been
provided.
context_type_id
;
This is an integer identifying the type of context
that the user is authenticating within. See the veil2_demo
for how this is used to enable different users to
belong to different companies.
This will match a scope_type as defined in
veil2.scope_types
.
context_id
.
This is an integer identifying, in combination with
context_type_id
, the context that
the user is authenticating within. See the veil2_demo
for how this is used to enable different users to
belong to different companies.
The function always appears to succeed, and it returns the following result columns:
session_id;
.
This is an integer that identifies the session for
your connections. If your parameters successfully
identified a legitimate user, a record with this key
will have been inserted into
veil2.sessions
. The user will not
be able to see this record, or determine that it is
absent.
The caller will need to provide this value as a
parameter in subsequent calls to
open_connection()
.
session_token;
.
This is a randomly generated
base64
text value that the caller must use in subsequent,
continuation, calls to
open_connection()
.
Possession of this token is assumed to be proof that the holder is the same user that authenticated themselves to us.
session_supplemental.
.
This is a string containing extra information that some user-provided authentication methods may require. This is for your use. Be imaginative.
This call is used to authenticate a user to a previously
created session. It must be the next database call after
create_session()
has returned.
You may wonder why this is not simply rolled into the
create_session()
call, thereby reducing
the number of round trips. The answer is that some
authentication methods will require initial tokens to be
created before allowing authentication to proceed. An
example of this would be a
Diffie–Hellman Key Exchange-based protocol. If you
implement such a thing please contact the author who would love
to see it.
The parameters to this function are:
session_id
;
This will be the value returned from
create_session()
.
nonce
;
This will be an integer value. You should allocate this sequentially for each session, though it does not need to start at 1.
authent_token
.
This is a string value. Its value will depend on the authentication method being used. Ideally that authentication method will not require a plaintext password to be sent. Note that the bcrypt authentication method does require the plaintext password. The only advantage of this over the plaintext authentication method is that the password cannot be easily extracted from the database.
This function returns the following result columns:
success;
.
A boolean. The meaning should be obvious.
errmsg
.
If authentication failed, this provides additional information to the caller. The possible values are:
AUTHFAIL
;
This means that the user could not be
authenticated. This might be because the user
and context for the session were invalid or
because authent_token
was
invalid. It could even mean that you haven't
called create_session()
but
if not, where did you get the value for
session_id
?
More information about the failure is recorded in the postgres logs, but this is not available to the user session.
If authent_token
was invalid,
at least for the bcrypt
authentication method, you can retry with the
correct token.
EXPIRED
;
This means that the session has timed-out. You
will need to create a new session using
create_session()
.
NONCEFAIL
.
This should not be possible in this, the authentication call.
Once a session has been successfully authenticated,
subsequent calls to open_connection()
are
considered to be continuations. Multiple open connections
for a session may be used simultaneously, but they must each
use their own nonce values.
The parameters to this function are:
session_id
;
This is just as for the Authentication version of this function call.
nonce
;
This integer must be previously unused for this session, and should be allocated sequentially in ascending order. They may arrive out of sequence but not by more than a value of 32.
authent_token
.
This is created from the value of
authent_token
returned from the
create_session()
call that created
the session, along with the nonce. It should be
created as follows:
Concatenate authent_token
with the lower-case hexadecimal representation
of the nonce parameter.
Create a sha-1 hash of the concatenated string.
Encode the hash as a base64 string.
The open_connection()
function will
create the same encoded hash internally and check that
it matches. If it does, we can be sure that the
caller is in possession of the original
authent_token
from the creation of
the session.
This function returns the following result columns:
success;
.
A boolean. The meaning should be obvious.
errmsg
.
This provides similar results to those for authentication.
AUTHFAIL
;
Assuming your session was previously valid, you
got the value of
authent_token
wrong. You can
retry, but you will need a new nonce value.
More information about the failure is recorded in the postgres logs, but this is not available to the user session.
EXPIRED
;
This means that the session has timed-out. You
will need to create a new session using
create_session()
.
NONCEFAIL.
;
You have attempted to re-use a nonce that was previously used. Try again with a new nonce. Try adding 4 or more to the latest successfully-used value.
If you are accessing your secured database using dedicated database connections, things are much simpler than for shared sessions. With a dedicated database connection you are the sole user of the connection, and there is no need for a complex create-open-close protocol dance to prevent your credentials from being used by someone else.
Typically, dedicated database connections will be used for running reports or ad-hoc queries, and each user will have their own database credentials (typically username and password).
Veil2
associates a database user with its own
accessor record by recording the database username in the
veil2.accessors
record's
username
column.
For dedicated sessions, you just have to tell
Veil2
to load your accessor privileges by
making a single, parameterless function call:
select veil2.hello();
Sometimes you may need changes to a user's privileges to happen
immediately, rather than the next time that they connect. In
this case, your session should call veil2.reload_connection_privs()
.
This will rarely be needed in a shared session environment as connections are usually only held for as long as it takes to perform a query, and the next connection will reload the privileges anyway.
You would use Postgres' listen/notify
mechanism to inform the affected session that a reload was needed.