Security and Session Tracking in ACS
by
Jon Salz
ACS Documentation :
ACS Core Architecture Guide :
Security and Sessions in ACS
The Problem
HTTP is a stateless protocol, but nearly all nontrivial Web services are stateful. We need to
provide
a way to maintain state within a session. Such state includes the user's preferences, login
information if any (although the solution needs to be general enough to handle anonymous
sessions),
and an identifier for the session itself. This session identifier absolutely
must remain constant from the moment a user enters to the site to the moment he or she is
done (even if the user logs in or switches to HTTPS in the meantime!) so our clickstreaming
software can ably analyze the users' behavior.
We need to provide different levels of security for different parts of this state:
- No security at all, e.g., information used for clickstreaming analysis. We don't
care if a wrong session ID gets logged here and there.
- Basic security, e.g., login information. The user mustn't be able to edit his cookie
file and change his login to masquerade as someone else, or merely fiddle with his session
identifier to hijack someone else's session.
- Virtually bulletproof, e.g., ordering information during an e-commerce session.
We need to be absolutely sure that no information sent in the clear (over
insecure HTTP) can be sniffed and used to gain access to sensitive information sent
securely; thus we need separate protection mechanisms for insecure and secure connections.
The new security and session-tracking subsystem needs to consolidate the myriad ways in place now
for maintaining and securing session and persistent state (including login information). It must
remember the last time the user visited the site, so we can determine which material on the site
is new since the user's last visit.
The Solution
Tracking and Securing Sessions
We now use
only the following cookies to track sessions (ideally, no one will ever have to
set another cookie again):
- ad_browser_id is an integer which is unique to a particular browser. It is
persistent, set to expire way in the future, so it survives even when the users closes their
browsers. We issue ad_browser_id whenever we receive an HTTP request from a client that
doesn't already have it set.
- ad_session_id takes the following form:
ad_session_id := session-id,user-id,token-string,last-issue
- session-id is a unique integer identifier for the session. This is pulled
directly from a sequence - we don't care if a user can guess other users' session identifiers
(by adding or subtracting a small integer from their own).
- user-id is the user's ACS ID.
- token-string is a TokenLength-character random string constructed from
base64 characters (number, upper- and lowercase letters, period, and slash). It is used as a
secret to protect the session - we shouldn't allow access to session-specific data unless we know
the user has the token right. (We refer to this as the insecure token string since it may
be sent unencrypted.)
- last-issue is the time (in seconds since the epoch) when the cookie was last
issued. Whenever we see that this is more than SessionCookieReissue seconds in the past,
we reissue the cookie.
Note that TokenLength and SessionCookieReissue are parameters configurable in the
local .ini file.
We issue ad_session_id, like ad_browser_id, whenever we receive an HTTP request from a client that
doesn't already have it set. We keep this cookie set to expire SessionTimeout seconds in the future
(plus or minus SessionCookieReissue seconds).
- ad_secure_token is another TokenLength-character random string which is
only ever transmitted over SSL (it has Secure specified in the Set-Cookie
header). Even if someone sniffs the session identifier and grabs the insecure token
string, they will never be able to gain access to this secure token string.
This cookie is only ever sent to a client once, so there's positively no way we could make the
mistake of sending it to two users (one masquerading as the other). Furthermore, when the
secure token is issued (typically on a client's first access to the site via HTTPS) we reissue
the insecure token as well. This way, if Gus sniffs Mary's insecure token and proceeds to make
a secure access to the site (receiving her secure token), Mary's insecure session will stop
working, limiting Gus's ability to mess with her.
- ad_user_login is of the following form:
ad_user_login := user-id,permanent-login-token
permanent-login-token is a TokenLength-character random string
stored in the login_token field of a row in sec_login_tokens.
This cookie is persistent, and allows the user to log into
ACS without having to explicitly type a login name and password. If the user logged in securely
(over SSL), this cookie is only ever transmitted over SSL; otherwise, we assume the user doesn't
care about security anyway and let it be transmitted in the clear.
When appropriate we log this information to, and check it against, the following table (caching to
minimize hits to the database):
create table sec_sessions (
-- Unique ID (don't care if everyone knows this)
session_id integer primary key,
user_id references users,
-- A secret used for unencrypted connections
token varchar(50) not null,
-- A secret used for encrypted connections only. not generated until needed
secure_token varchar(50),
browser_id integer not null,
-- Make sure all hits in this session are from same host
last_ip varchar(50) not null,
-- When was the last hit from this session? (seconds since the epoch)
last_hit integer not null
);
We populate
secure_token only when we issue a secure token (the first time the client
makes an access to the site over HTTPS).
Maintaining Session- and Browser-Specific State
In order to let programmers write code to preserve state on a per-session or per-browser
basis without sending lots of cookies, we maintain the following tables:
create table sec_session_properties (
session_id references sec_sessions not null,
module varchar2(50) not null,
property_name varchar2(50) not null,
property_value varchar2(4000),
-- transmitted only across secure connections?
secure_p char(1) check(secure_p in ('t','f')),
primary key(session_id, module, property_name),
foreign key(session_id) references sec_sessions on delete cascade
);
create table sec_browser_properties (
browser_id integer not null,
module varchar2(50) not null,
property_name varchar2(50) not null,
property_value varchar2(4000),
-- transmitted only across secure connections?
secure_p char(1) check(secure_p in ('t','f')),
primary key(browser_id, module, property_name)
);
A client module needing to save or restore session- or browser-specific state uses the new
ad_get_client_property and
ad_set_client_property routines, which manage access to the
table (caching as appropriate). This way they don't have to set their own cookies, and as a
bonus they don't have to worry about users tampering with contents!
In general, use session-level properties when you want the properties to expire
when the current session ceases (e.g., items in a shopping cart). Use browser-level
properties which the properties should never expire (e.g., user preferences).
Tracking the User's Last Visit
The session-tracking subsystem maintains two special pieces of browser-specific state:
the
last_visit and
second_to_last_visit properties (with module
acs).
last_visit is the time at which the current session started, and
second_to_last_visit is the time at which the previous session started. This
state (accessible via the
ad_last_visit_ut and
ad_second_to_last_visit_ut
routines) allows client code to determine which material on the site is new since the
user's last visit.
Security
One really neat thing about properties is that if secure_p is true (i.e., the
secure_p flag was passed to ad_set_client_property - see below) the
ad_get_client_property routine will refuse to access the information except when the
connection is secure (HTTPS) and the secure token is correct. So the user can switch back and
forth between HTTP and HTTPS without giving anything away, and hijackers cannot tamper with
any state marked secure (even if they're sniffing for tokens). Note that this only works for
session-level state for the moment - browser-level state isn't protected by any kind of token.
The API
Summary
ad_validate_security_info [ -secure f ]
ad_get_user_id
ad_verify_and_get_user_id [ -secure f ]
ad_get_session_id
ad_verify_and_get_session_id [ -secure f ]
ad_last_visit_ut
ad_second_to_last_visit_ut
ad_set_client_property [ -browser f ] [ -secure f ] [ -deferred f ] [ -persistent t ] module name value
ad_get_client_property [ -browser f ] [ -cache t ] [ -cache_only f ] module name
Description
The heart of the new security system is ad_validate_security_info, which examines
the session information (including the user ID), returning 1 if it is valid or 0 if not. This
procedure takes an optional switch, -secure, taking a
argument. If -secure is true, the session won't be considered valid unless it's
being conducted over HTTPS, and a valid secure token was provided (useful, e.g., for e-commerce
applications). Typically client code will call ad_validate_security_info before
doing anything else, redirecting or returning an error message if the session is deemed
invalid.
The semantics of ad_get_user_id and ad_verify_and_get_user_id remain the same:
ad_get_user_id does absolutely no checking that the user ID isn't forged,
while ad_verify_and_get_user_id makes sure the user is properly logged
in. Correspondingly, the new routine ad_get_session_id returns a session ID (which may
be forged), whereas the new routine ad_verify_and_get_session_id first verifies that the
token is valid. Both verify routines take an optional
-secure switch, taking a Boolean (t/f) argument defaulting to f;
if true, only secure (HTTPS) connections will be considered valid.
ad_set_client_property is used to set a session- or browser-level property. It takes
three arguments: a module name, the name of the property, and the value of the property. In addition,
the Boolean -browser switch, defaulting to f, determines whether the property should be
persistent (i.e., browser-level); and the -secure switch, defaulting to f, determines
whether the property should only be transmitted when a valid, secure session is in place. If it is
supremely
important that the property be set quickly, with no immediate database access, use -deferred t,
causing the database hit to be deferred until after the HTTP connection is closed (so
ad_set_client_property will return immediately). If the data should
never be written to the database, use -persistent f.
ad_get_client_property retrieves a property. It takes two arguments: module name and
property name. Like ad_set_client_property it takes the optional -browser switch,
defaulting to f. ad_get_client_property maintains a cache; to force the cache to be
bypassed (in case accuracy is supremely important) specify -cache f. If only
the cache should be queried (a database hit should never be incurred) use -cache_only t.
If the property is
not marked secure, ad_get_client_property does no checking to make sure the session is valid - it is
the caller's responsibility to do this (usually using ad_validate_security_info).
Future Enhancements
We plan on modifying these cookies to support clusters of servers, i.e., sharing sessions amongst
servers in a common domain (
*.arsdigita.com).
Credits
This document (and the new security subsystem) ties together ideas introduced by lots of people,
including:
Thanks for their help and code!
jsalz@mit.edu