|
When figuring out how I was going to do authentication for Ulysses Agenda, I had a couple of design issues. The first is that the user needs to be persisted somewhere other than the game client itself - can't have players modifying their own data like the good old days of Nethack and Trek for Unix (yes, I am THAT old.. now get off my yard you whippersnappers!!).
So, I really had two competing alternatives as far as patterns go:
So I opted for approach #2. The use case is like this:
User fires up their Ulysses Agenda client and is prompted for their credentials. They hit OK after submitting their credentials. The credentials go out on the wire and the client has no idea about the location or physical configuration of the authenticator. All it knows is that some service, sitting somewhere on the wire, conforms to the IAuthenticator interface. The client application then greys out the GUI and says something like "Waiting for Network Authentication...". What should hopefully only be a few milliseconds later, a message comes into the client that invokes the AuthenticatorAck method on the client. This method is invoked when an authenticator validates a set of credentials on the net.p2p://ulyssesagenda/auth mesh.
So how does it all work. First, we start with the contract for the IAuthenticator service and for the channel it resides on, IAuthenticatorChannel:
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;
namespace UlyssesAgenda.NetworkLibrary
{
[ServiceContract(CallbackContract=typeof(IAuthenticator))]
public interface IAuthenticator
{
[OperationContract(IsOneWay=true)]
void Authenticate(string userName, string password);
[OperationContract(IsOneWay = true)]
void AuthenticateAck(string userName, Guid authToken);
}
public interface IAuthenticatorChannel : IAuthenticator, IClientChannel
{
}
}
What's interesting is that the game clients will have an implementation for the AuthenticateAck method while the authentication server will have an implementation for the Authenticate method, which actually calls the AuthenticateAck method on the mesh (remember that calling a method on a mesh is essentially equivalent to invoking a remote method on every client in a peer network.. though WCF optimizes the call paths immensely).
So, here's the server-side implementation that can be found inside the Universe Server application:
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;
using UlyssesAgenda.NetworkLibrary;
namespace UlyssesAgenda.UniverseServer.NetLibImplementations
{
public class Authenticator : IAuthenticator
{
#region IAuthenticator Members
// When this receives an authenticate message, send out the ack
// note that the authenticator has no implementation for the ack, that
// should be in the clients.
public void Authenticate(string userName, string password)
{
Console.WriteLine("Received auth request for {0}, sending Ack Back.", userName);
CommCentral.Authenticator.AuthenticateAck(userName, Guid.NewGuid());
}
public void AuthenticateAck(string userName, Guid authToken)
{
// ignore messages of this type.
}
#endregion
}
}
And here's the game client implementation:
using System;
using System.Collections.Generic;
using System.Text;
using UlyssesAgenda.NetworkLibrary;
namespace AuthTest.NetLibImplementations
{
public class Authenticator : IAuthenticator
{
#region IAuthenticator Members
public void Authenticate(string userName, string password)
{
// no implementation on clients
}
public void AuthenticateAck(string userName, Guid authToken)
{
Console.WriteLine("An auth server somewhere just authenticated {0} with Guid {1}",
userName, authToken.ToString());
}
#endregion
}
}
I personally think that I'm OK here keeping the authentication client and authentication server halves of the same coin in the same interface because, well, they're two halves of the same coin. The other alternative would be to split things into an IAuthenticatorClient and IAuthenticatorServer interface, each with a single method. To me, a single-method interface screams of over-re-factoring and probably won't buy me much. With this, I can look at the authentication contract (IAuthenticator) and quickly see that I can have applications that either perform authentications, or respond to authentications, or both - its totally flexible.
On the client, it becomes pretty simple to instantiate the service and drop credentials on the wire:
_auth = new NetLibImplementations.Authenticator();
_authSite = new InstanceContext(_auth);
_authChannelFactory = new DuplexChannelFactory<IAuthenticatorChannel>(_authSite, "AuthEndPoint");
_authProxy = (IAuthenticatorChannel)_authChannelFactory.CreateChannel();
_authProxy.Authenticate("Kevin", "Password");
Calling Authenticate actually calls Authenticate on all applications that are listening on the net.p2p://ulyssesagenda/auth mesh. Some clients will ignore that message (such as other game clients), while other applications will take the information and send out a message in response, such as the Universe Server's built-in authenticator.
Thoughts? Opinions? Hate mail? (well you can leave the hate mail off.. I'd just censor you anyway)
This is a good post. I have been waiting to hear some of these types of
details. One thing I have been wondering is (and this may be my lack of
p2p knowledge): How do you prevent someone from maliciously acting as an
auth service or sending bogus game state data to the universe server?
There are two ways of dealing with it. The easiest way to deal with it is
that the WCF allows for channel-level authentication via x.509
certificates, making it so that clients need a particular certificate to
even get into the mesh. At this point, you can be assured that you have a
private, secure, guaranteed mesh knowing that only code written by
publisher X (e.g. code with signature Y) can get into the mesh. You can
stop worrying there, or you can devise your own additional schemes above
and beyond that, but I think thats really unnecessary. The channel-level
security in WCF is fantastic. I'm hoping to have a discussion with Ravi Rao
shortly to find out what kind of channel security he recommends for a
situation like this.
Of course! I completely forgot about x.509 when I was posting. Great
solution. Please post the results of your conversation with Ravi. I'm
sure there are a lot of people who would be interested.
That sounds great, but how might one get away from having to have a central
server for credential management. If one wanted a truly decentralized
network, is there a pattern for authentication and authorization. In other
words, how could one take advantage of all the peers out there to do the
authentication and authorization without requiring a database server or AD
machine out there with credentials stored on it. Even though the bus
architecture enables location transparency and unlimited scalabality, it
doesn't enhance the redundancy. If my auth listeners are down, nobody can
get authenticated on the mesh. In addition, if you scale up to two auth
servers how would you determine which auth server a peer is going to in
order to distribute load? If you put in a load balancer, once again you
introduce a single point of failure. Any ideas on how you could truly have
a trusted mesh with distributed account storage and management?
I see two flaws in this design. Nothing keeps a client from creating their
own auth server and putting it on the mesh. Nothing keeps clients from
sniffing the mesh to see all the other client's usernames and passwords.
The password should be kept on the client side. The server should pass the
client a nonce (random crap) to hash using the password. If the server
agrees that the nonce has been properly hashed then the client is logged
in.