Authentication and Application Security (Delphi)
Introduction
Most systems require the enforcement of some level of security, which can include encryption, simple login and logout functionality or more complex user or role based logic applied when returning data or when updating to a data source.
The LoginSample
project is a comprehensive example of how to implement application security and authentication.
Note: We won't explore encryption in this sample as it is just a matter of setting the right properties in the Remoting SDK message components.
In the client application, the user will first need to login by providing a user name and a password.
Upon successfully logging in, the user can view the list of employees.
We will now examine the server and the client projects to see how we accomplished this.
The server RODL file
If you open the server project and launch the Service Builder, you will be presented with the following RODL file:
As you can see, this server exposes two services: LoginService
and LoginSampleService.
LoginService
LoginService
is the first service we coded in this sample and it exposes the following interface:
ILoginService = interface(DataAbstract4_intf.ISimpleLoginService)
['{4E74056F-733F-4415-81D2-D8E7297C9EC2}']
end;
It doesn't implement any own methods, because its ancestor ISimpleLoginService has Login and Logout methods that we need. In order to add our check of credentials, we handle two events of the TLoginService class: OnLogin and OnLogout:
LoginService.SimpleLoginServiceLogin
This method is fundamental and probably the most important in this example. It takes two input parameters (aUserID and aPassword) and returns two values: a structure called aUserInfo
of type UserInfo and a Boolean
value.
UserInfo is a type defined in the Data Abstract RODL file and is declared as:
UserInfo = class(TROComplexType)
...
property SessionID: ROUTF8String read fSessionID write fSessionID;
property UserID: ROUTF8String read fUserID write fUserID;
property Privileges: StringArray read GetPrivileges write fPrivileges;
property Attributes: VariantArray read GetAttributes write fAttributes;
property UserData: Binary read GetUserData write fUserData;
end;
Login
methods should always return data to the clients, so that they can adapt their user interface and perform security checks without the need to make frequent round trips to the server. The property UserInfo.SessionID is sent back, so that the client can store the information in a unique place, along with the other user specific values. In older versions of the Remoting SDK, this value had to be assigned to the TROMessage.ClientID property, since it was initialized server side. With more recent versions of the Remoting SDK, this is no longer necessary as the server creates new sessions using whatever value was sent by the client during its first call.
On the server side, the Login method is coded in SimpleLoginService_Impl
as follows:
function TSimpleLoginService.Login(const aUserID: ROUTF8String; const aPassword: ROUTF8String; out aUserInfo: UserInfo): Boolean;
var
lLoginSuccessful: Boolean;
begin
lLoginSuccessful := False;
if assigned(OnLogin) then
OnLogin(Self, aUserID, aPassword, aUserInfo, lLoginSuccessful);
Result := lLoginSuccessful;
if Result then CreateSession;
end;
And our onLogin
event implementation:
procedure TLoginService.SimpleLoginServiceLogin(Sender: TObject; aUserID,
aPassword: ROUTF8String; out aUserInfo: UserInfo;
var aLoginSuccessful: Boolean);
var
cmd: IDADataset;
begin
cmd := Schema.NewDataset(Schema.ConnectionManager.NewConnection(Schema.ConnectionManager.GetDefaultConnectionName), 'ValidateLogin', ['UserId', 'Pass'], [aUserID, aPassword]);
cmd.Open;
aLoginSuccessful := (cmd.Fields[0].AsInteger <> 0);
if (ALoginSuccessful) then begin
aUserInfo := UserInfo.Create;
aUserInfo.SessionID := GuidToString(ClientID);
aUserInfo.UserID := aUserID;
Session['UserID'] := aUserID;
end
else begin
DestroySession;
end;
end;
Pay particular attention to the code executed when the result is TRUE
and thus initializes the out parameter aUserInfo.
In that block, you can see the line:
aUserInfo.SessionID := GuidToString(ClientID);
The ClientID
property will be examined later in Additional considerations for sessions and LoginService.SimpleLoginServiceLogout
.
Session['UserID'] := aUserID;
The Session property is available in any RemObjects service and will be initialized if your service points to a session manager. The following screenshot shows the properties of the LoginService datamodule:
If the SessionManager property is not assigned, the Session property will return nil.
The UserInfo type should be used as a template; it's not required when creating your own login services. You can create a type that has more or less properties.
The client code that triggers the call to the Login method is associated with the LoginButton and is coded as:
procedure TLoginSample_ClientMainForm.LoginButtonClick(Sender: TObject);
begin
Login;
LoginButton.Enabled := not fLoggedIn;
LogoutButton.Enabled := fLoggedIn;
FillButton.Enabled := fLoggedIn;
end;
And the client's Login method:
procedure TLoginSample_ClientMainForm.Login;
begin
FreeAndNil(fUserInfo);
fLoggedIn := fLogin.Login(edUsername.Text, edPassword.Text, fUserInfo);
if not fLoggedIn then ShowMessage('Login failed!');
end;
Additional considerations for sessions and LoginService.SimpleLoginServiceLogout
When a client makes a remote call to a service, the value of its ClientID property is included in the request. This happens transparently and it's performed by both BIN and SOAP message components. This allows the service to locate a pre-existing user session in the session manager or initialize a new one whenever this particular client ID is not found. Once again, this new session will have a SessionID that matches the ClientID associated with the client's TROMessage.ClientID. If the session is not found, the one initialized is a temporary in-memory-only session that will only be saved back in the session manager if no calls to DestroySession are made.
The session lookup is done in the DoOnActivate
method of the TRORemoteDataModule class, from which your services will most likely inherit. This method and the associated DoOnDeactivate method are called automatically at the beginning and end of each remote method call.
procedure TRORemoteDataModule.DoOnActivate(aClientID: TGUID; const aMessage: IROMessage);
begin
if (csDesigning in ComponentState) then Exit;
fSession := nil;
if not CustomRequireSession(aMessage) then Exit;
if Assigned(fSessionManager) then begin
fNewSession := False;
fDestroySession := False; // Resets the flags
fSession := fSessionManager.FindSession(aClientID);
if (fSession = nil) then begin
if RequiresSession then
RaiseError(err_SessionNotFound, [GUIDToString(aClientID)], SessionNotFoundException)
else
fNewSession := True;
end;
end
else begin
if RequiresSession then RaiseError(err_SessionManagerRequiredButNotAssigned);
end;
end;
As you can see, the service asks the session manager to locate the session at the line
fSession := fSessionManager.FindSession(aClientID);
If the session is not found, it raises an exception if the value of the property RequiresSession is TRUE, otherwise it creates a new one.
RequiresSession is a very important property of RemObjects services as it ensures that service methods can never be called unless a session has been previously created and stored in a session manager, most likely through a call to a Login method like the one above.
Take a look at the value of this property in the two services we coded in this example:
LoginService has RequireSession set to FALSE to indicate that any client can call this service at any time. LoginSampleService will not allow for any method call to work unless a session has been previously created and stored in the session manager.
Once a method call is complete, TRORemoteDataModule.DoOnDeactivate is invoked. This method is coded as follows:
procedure TRORemoteDataModule.DoOnDeactivate(aClientID: TGUID);
var
l_sessionID: TGUID;
begin
if (csDesigning in ComponentState) then Exit;
if Assigned(fSessionManager) and (fSession <> nil) then begin
if fDestroySession then begin
if fNewSession then begin
fSessionManager.DeleteTemporarySession(fSession);
end
else begin
l_sessionID := fSession.SessionID;
fSessionManager.ReleaseSession(fSession, False);
fSessionManager.DeleteSession(l_sessionID, False);
fSession := nil;
end;
end
else begin
fSessionManager.ReleaseSession(fSession, fNewSession);
end;
end;
fTransport := nil;
end;
As previously stated, this method is responsible for storing or deleting sessions in the session manager and deciding what action to take according to the value of the flag fDestroySession, which can be set to FALSE through a call to the method DestroySession.
That is done in the onLogin event if the login fails:
procedure TLoginService.SimpleLoginServiceLogin(Sender: TObject; aUserID,
aPassword: ROUTF8string; out aUserInfo: UserInfo;
var aLoginSuccessful: Boolean);
[...]
if (ALoginSuccessful) then begin
[..]
end
else begin
DestroySession;
end;
end;
Or in the onLogout event:
procedure TLoginService.SimpleLoginServiceLogout(Sender: TObject);
begin
DestroySession;
end;
To summarize: Anytime a remote method call is made and a session manager is associated with a service, the service will try to locate an existing session. If that is not found, a temporary one will be created. You can access this session through your service's Session property and delete it with a call to DestroySession.
If you do not want temporary sessions to be created by the service and want to make sure a session has previously been created, just set RequiresSession to TRUE.
LoginSampleService
The LoginSampleService is really simple. It doesn't introduce any methods on top of what its IDataAbstractService ancestor offers.
Design Time consideration when accessing services that have RequireSession
set to true
There's one last important consideration regarding the accessing of services that have RequireSession set to true when at design time. If you start your server and try to open the dtOrders data table contained in the client form, you will get an error stating Session {xyz} not found
. Although it's good to see that security works even at design time, it certainly creates problems when developing client applications.
How can we access DataService, since it requires a login and we cannot login until we start the client application? The answer lies in the TDADesigntimeCall component, which was previously dropped. This component allows you to invoke a remote function of a service during design time, so all we need to do is to make it call the LoginService.Login
method specifying Nancy Davolio's login information. Upon double-clicking on it, the method will be executed and from that moment on, the Delphi IDE will behave like a client, logged in to the server.
Conclusion
The combination of the Remoting SDK and Data Abstract offers a lot of pre-built functionality to create secure systems. We hope this article helped you to understand how TROMessage.ClientID and sessions work together and why returning structures such as UserInfo allows you to create systems that require less network round trips to enforce client side security.