Note: A previous Tech Tip, “Authentication in DevForce”, outlined the basics. This tip assumes familiarity
with the material presented there. You can find that Tech Tip,
and others, at http://www.ideablade.com/techtips_summary.html.
Your n-tier app naturally requires
that a user log in and be authenticated in order to access data
in the corporate data stores. Based on the user’s identity, certain
data is made available and other data is not; the same is true
for facilities in the user interface.
But your users also need to run when disconnected
from the data servers. And even in that circumstance, there is
still sensitive data at issue – that stored in one or more entity
set files on the local machine. You’d also like the UI to function
more or less as when connected, giving the user selective access
to appropriate facilities based on her identity.
How to
do it?
The Login() method
of the DevForce PersistenceManager causes the PersistenceManager
to request a login operation by the DevForce PersistenceServer.
The latter searches for a server-side class that implements DevForce’s
IPersistenceLoginManager interface; upon finding it, it calls another Login()
method in that class. Therefore, for login to run successfully,
the PersistenceManager must be able to connect to a PersistenceServer. It
will always be able to do so in an application deployed client-server,
because in that configuration the PersistenceServer runs on your
local machine. In the much more secure n-tier deployment configuration,
however, the PersistenceServer runs on a remote machine.
If the PersistenceManager cannot connect to
the PersistenceServer, every call to
PersistenceManager.Login()
will result in an exception. Therefore, your code that calls PersistenceManager.Login()
will either need to test whether it can connect to the PersistenceServer
before attempting PersistenceManager.Login(), or handle the exception
that occurs from a failed attempt, and then perform authentication
by other means.
A typical server-based authentication might proceed something like the following:
- The
end user supplies a user name, password, and possibly a domain
name to a login form.
- This
information (after the password is hashed) is folded into an
ILoginCredential object and passed to the PersistenceManager.Login()
method.
- PersistenceManager.Login() forwards the login request
to the PersistenceServer, which finds an IPersistenceLoginManager
class and runs its Login() method, passing the credential and
a server-side instance of the PersistenceManager.
- PersistenceServer.Login() performs authentication of
the user described in the credential by any desired means. It
might, for example, get an instance of a User object by calling PersistenceManager.GetEntity()
and specifying as a criterion that the UserName of the retrieved
User must match the user name included in the credential. If
such a User is found and retrieved successfully, the authentication
code then compares its password (stored in hashed form in the
database) to the hashed password passed in with the credential.
If the two match, the PersistenceServer.Login()
returns to the originating PersistenceManager an IPrincipal object
(representing the authenticated user) , wrapped in a DevForce
SessionBundle. The PersistenceManager then attaches the authenticated
IPrincipal to the client-side thread (whence it can be accessed,
any time, as System.Threading.Thread.CurrentPrincipal).
Since the above authentication process depends
upon successful access to the back-end database (to retrieve the
password and other information about the would-be user), it breaks
down entirely in disconnected mode. One certainly would not want
to solve this conundrum by retrieving and maintaining, client-side,
a complete table of Users with passwords, or even a subset thereof! So
how to authenticate a prospective user when disconnected?
As we mentioned in the opening comments, the
most critical resource at risk in a disconnected mode is the data
previously downloaded (while connected) and stored in a local entity
set file. Your application certainly took care to encrypt
that before writing it out [see
the “Non-Tutorial Solution” in the 100-series instructional unit, “Supporting
Disconnected Users”, for a sample of encrypting and decrypting
an EntitySet].
Let’s assume, further, that you used the end
user’s login password (or a hash of it) as the encryption key for
scrambling the entity set. If
you did, then the only way the current would-be user can successfully
access that data will be to supply the same password, so that it
can be used to decrypt the
entity set. So there’s our
authentication test: if we can turn the encrypted entity set back
into a valid PersistenceManager cache, then they must have supplied
the correct password!
Getting
Security Role Information While Disconnected
Information about the user’s security roles
was another tidbit we obtained from the back-end database when
authenticating in connected mode. How shall we get it when disconnected?
Well, how about we make sure it’s part of the
cache that we save while we are connected? All
we need is a User object (complete with security role information)
representing the authenticated user. Although
it probably wouldn’t hurt to include the user’s password
in that User object (since we’re encrypting it for storage anyway),
there’s really no need to do so. It’s only the role information
that we will need later when authenticating disconnected. After
performing our authentication-by-decryption, we can retrieve the
User’s role information from the decrypted cache. We can then create
our own IPrincipal object, complete with role information, and
attach it to the running client-side thread!
Things
That Go “Bump” When Disconnected
At that point most of our regular code should
work just fine, with the notable exception of things that must
have access to the server. Those
include:
- Data
retrievals that specify a trip to the datasource (specifically,
those whose FetchStrategy is DatasourceOnly or DatasourceThenCache).
- Pass-Thru
and Stored Procedure Queries
- Saves
to the datasource
- Remote
Procedure Calls
- Push
Notifications
Queries that specify a FetchStrategy of DatasourceOnly or DatasourceThenCache will trigger an exception
when called in disconnected mode, as will Pass-Through and Stored
Procedure Queries. Your
app must be ready for these exceptions, and must provide some reasonable
experience to the end user, even if it is just gently informing
them that they can’t do such and such while disconnected. Of
course, it’s quite a bit better if your app disables or makes invisible
controls that aren’t appropriate when disconnected.
As for saves, you might want to make your Save()
method call PersistenceManager.SaveChanges() when connected, but
call PersistenceManager.SaveEntitySet() when not connected. In the latter case, be sure you communicate
effectively with your end user so they know what they have and
haven’t accomplished. It won’t be good if they think they’ve saved
to the back-end database, and that their updates are now available
to other users, when in fact they aren’t!
A Sample
Solution
A new instructional unit, “Security_Disconnected
Authentication” will be available in the 300 Advanced Series as
of the 3.5.4 DevForce release (scheduled for a bit past mid-September,
2007). It illustrates the approach to disconnected authentication
discussed in this article. We’ll be happy to hear your feedback!
|