Appendix E. Veil2 Authentication Protocols

E.1. Shared Session Authentication

E.1.1. The Problem Space

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:

  • database connections are not dedicated to individual users;
  • each user may use a different connection for each interaction;
  • multiple connections may be in use simultaneously for a user;
  • usage of connections for a given user is not serialized.

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.

E.1.2. Requirements

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:

  • it must identify and authenticate each user;
  • it must know when the user is finished;
  • it must make it as difficult as possible to impersonate a user, or otherwise gain their access rights.

A further requirement is that it must all be done with as little overhead as possible.

E.1.3. The Threat Space

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:

  • it is the job of a security implementer to be paranoid;
  • many data breaches have arisen from hackers having long-term access to compromised servers;
  • even the paranoid may have enemies.

Given that with Veil2 we can attempt to protect ourselves from a compromised server, we should try to do so.

E.1.4. Properties of the Solution Space

This is what Veil2 provides, out-of-the-box, for session management:

  • a default authentication system that is not terrible (bcrypt);
  • extensible authentication methods (build your own);
  • minimal transmission of authentication tokens;
  • resistance to fishing for user-ids;
  • a replay attack-resistant protocol.

E.1.5. The Protocol

There are 3 functions that your application server's session management must call:

And the calling protocol has four distinct stages.

Begin New Session

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.

Authenticate New Session

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.

Close Connection

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.

Re-open Connection

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.

E.1.6. Function Calling Minutiae

E.1.6.1. create_session()

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.

E.1.6.2. open_connection() Authentication

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.

E.1.6.3. open_connection() Continuation

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.

E.1.6.4. close_connection()

This function has no parameters returns void.

E.2. Dedicated Database Sessions

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();
      

E.3. Resetting Cached Privileges

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.

E.4. Custom Database Sessions

If the built-in session management does not work for you, you can always build your own.