Authentication and Authorization
As I mentioned in my last post, Using the OSP to Get the Current User’s Name, the Oncology System Platform (OSP) provides many services. In this post, I'll show you how to use the OSP to authenticate a user. I'll also show you how to authorize a user based on his or her group.
To be clear, there's a difference between authentication and authorization. Authentication is the process of verifying a user's identity. Typically, this is done via a username and password. Authorization is the process of determining whether an authenticated user is allowed to perform a certain action.
In most cases, you won't have to worry about authentication because the Eclipse Scripting API does this for you automatically. For example, in a stand-alone app, creating the Application object automatically asks the user for his or her credentials.
The reason you may want to authenticate a user yourself is when you want to verify the user's identity when performing a sensitive action (e.g., signing off an important document). In addition, you may want to check that the user belongs to a specific group (e.g., oncologist) before allowing him or her to perform the action.
Authentication
Let's start with authentication. The API for the OSP includes an "Authenticate" method that will do most of the work for us. Here's its declaration:
namespace VMS.OSP.Services.Security
{
public interface ISecurity
{
// ... other methods
IUserInfo Authenticate(
string strUserID,
string strClearPassword,
string strApplicationName,
out ReturnCode returnCode);
}
}
It takes as input the user ID, the user's password, and your application's name. It returns two objects: the return code as an "out" parameter and the authenticated user information.
The return code is an enum that specifies the result of the authentication, like whether the user was authenticated or if some error occurred. The user information (IUserInfo interface) contains the user ID, the user's name, the user's group ID, and other information.
Instead of using this method directly, we're going to wrap it in a class of our own. We'll do this to keep our program independent of the technology used for authentication. Furthermore, we'll create an interface for our class, so that whatever module is using it doesn't even need to reference the OSP API. This architecture completely separates our program from the OSP.
Here are the two interfaces we'll use:
namespace MyApp.Authorization
{
public interface IAuthentication
{
IAuthenticatedUser Authenticate(string userId, string password);
}
}
namespace MyApp.Authorization
{
public interface IAuthenticatedUser
{
string GroupId { get; }
}
}
The IAuthentication interface has a single method: Authenticate. It takes the user ID and password, and it returns an IAuthenticatedUser object. The idea is that if the return value is not null, the authentication was successful. If the return value is null, however, it means the user wasn't authenticated.
The IAuthenticatedUser has just one property: the group ID of the user. We'll use this property later when we need to authorize the user. You could add more properties to this interface if they'll be used by your program.
Now let's look at their implementation. I've put the following classes in their own class library. This library needs to reference the following assemblies: VMS.OSP.ICommon.dll, VMS.OSP.Common.dll, VMS.OSP.IServices.dll, and VMS.OSP.Services.dll. On my computer, these are located in C:\Windows\Microsoft.NET\assembly\GAC_MSIL, under the directory with the same name as the assembly.
using System;
using VMS.OSP.Services;
using VMS.OSP.Services.Definitions;
namespace MyApp.Authorization.Osp
{
public class Authentication : IAuthentication
{
private readonly string _appName;
public Authentication(string appName)
{
_appName = appName;
}
public IAuthenticatedUser Authenticate(string userId, string password)
{
try
{
var ospClientFactory = new OSPClientLibraryFactory();
var ospClientServices = ospClientFactory.CreateOspClientServices();
var user = ospClientServices.Security
.Authenticate(userId, password, _appName, out ReturnCode returnCode);
return returnCode == ReturnCode.OK
? new AuthenticatedUser(user.UserGroupID)
: null;
}
catch (Exception e)
{
throw new AuthenticationException(
"An error occurred with the OSP service.", e);
}
}
}
}
namespace MyApp.Authorization.Osp
{
internal class AuthenticatedUser : IAuthenticatedUser
{
public AuthenticatedUser(string groupId)
{
GroupId = groupId;
}
public string GroupId { get; }
}
}
using System;
namespace MyApp.Authorization.Osp
{
public class AuthenticationException : Exception
{
public AuthenticationException(string message, Exception innerException)
: base(message, innerException)
{ }
}
}
The constructor of the Authentication class takes your application name, which will be passed to the Authenticate method of the OSP. (I'm not entirely sure why the OSP needs your application's name—probably to log it somewhere.)
Our Authenticate method creates the necessary OSP objects, and then calls the OSP's Authenticate method we saw earlier. If the return code is OK, we return an AuthenticatedUser object with the group ID. Otherwise, we return null. We've wrapped all this code in a try/catch statement in case something goes wrong.
Authorization
Let's begin with the interfaces.
namespace MyApp.Authorization
{
public interface IAuthorization
{
IAuthorizedUser AuthorizeOncologist(IAuthenticatedUser user);
IAuthorizedUser AuthorizePhysicist(IAuthenticatedUser user);
// ... more methods as needed
}
}
namespace MyApp.Authorization
{
public interface IAuthorizedUser { }
}
In the IAuthorization interface, we have two methods: one for authorizing an oncologist and another for authorizing a physicist. You can add more methods, depending on the groups for which you'd like to authorize.
These methods return an IAuthorizedUser object. If it's not null, the user is authorized; otherwise, the user is not authorized. This interface doesn't contain anything—it's simply used as a way to indicate that the user was authorized.
The following are the implementations. I've put these in the same class library as the authentication implementation.
namespace MyApp.Authorization.Osp
{
public class Authorization : IAuthorization
{
private readonly string _oncologistGroupId;
private readonly string _physicistGroupId;
public Authorization(string oncologistGroupId, string physicistGroupId)
{
_oncologistGroupId = oncologistGroupId;
_physicistGroupId = physicistGroupId;
}
public IAuthorizedUser AuthorizeOncologist(IAuthenticatedUser user)
{
return user.GroupId == _oncologistGroupId ? new AuthorizedUser() : null;
}
public IAuthorizedUser AuthorizePhysicist(IAuthenticatedUser user)
{
return user.GroupId == _physicistGroupId ? new AuthorizedUser() : null;
}
}
}
namespace MyApp.Authorization.Osp
{
public class AuthorizedUser : IAuthorizedUser { }
}
The constructor of the Authorization class takes the group IDs for each of the groups we're interested in. The reason we don't hard-code these as constants is that we may want to put these IDs in the application's configuration file.
The methods are straightforward: check whether the user's group ID matches the appropriate ID. The group ID is obtained from an IAuthenticatedUser, so we know the user's identity has been verified.
Final Thoughts
Now that we've wrapped the authentication and authorization functionality in interfaces and classes, we can use them as needed. I recommend using the interfaces wherever possible by passing them to the classes that need them (see dependency injection).
Ideally, the implementation classes will be instantiated only once, somewhere at the start of your program (e.g., App.xaml.cs in a stand-alone app). Only that project will need to reference the OSP library, while all other projects will simply reference your authentication/authorization interface library.
Finally, here's an implementation note. In the IAuthorization interface, the authorize methods take an IAuthenticatedUser, which means that the user must have been authenticated previously. Alternatively, we could assume that the current user has already been authenticated, given that he or she logged in through Eclipse. In that case, we could change our authorize methods to simply take a user ID. We would then need to use the OSP to look up the user's group ID and then determine whether it matches with the desired group ID.
Comments