Session and Access Control

In this tutorial we’ll learn how to implement authentication and authorization functionality in your application with ASNeG OPC UA Stack.

You can download the source code of the tutorial from here.

Overview

After an OPC UA client has got Endpoints of the server by using Discovery Process and opened a Secure Channel with suitable Security Policy and Security Mode, it should create and activate Session with the server to access to the OPC UA application’s data. During the session activation the client authenticates itself by userIdentityToken that allows the application to correspond the activated Session with its user profile. Then the client finishes communication with the server it should close Session by calling Service CloseSession or it is closed by the timeout.

ASNeG OPC UA Stack provides a callback mechanism to notify the user application about activating\closing session and accessing to the data during the current session so that a developer can builtin authentication and authorization in its OPC UA applications.

Client              ASNeG OPC UA Stack                  User Application
======              ==================                  ================
  |                         |                                  |
  |                         |  Register Callbacks              |
  I                         I<---------------------------------I
  I    ActivateSession()    I                                  |
  I------------------------>I                                  |
  I                         I  Call Authentication Callback    |
  I                         I--------------------------------->I
  I                         I  OpcUaStatusCode::Success        I
  I    result = Success     I<---------------------------------I
  I<------------------------I                                  |
  I                         I                                  |
  I                         I                                  |
  I                         I                                  |
  I       Read()            I                                  |
  I------------------------>I  Call Authorization Callback     |
  I                         I--------------------------------->I
  I                         I  OpcUaStatusCode::Success        I
  I    result = Success     I<---------------------------------I
  I<------------------------I                                  |
  I                         I                                  |

                        (other calls of Services)

  I                         I                                  |
  I    CloseSession()       I                                  |
  I------------------------>I  Call CloseSession Callback      |
  |                         |--------------------------------->I
  I                         I  OpcUaStatusCode::Success        I
  I    result = Success     I<---------------------------------I
  I<------------------------I                                  |
  |                         |                                  |

Now we’re going to see the callbacks in details. All the following examples are based on a user application generated by OpcUaProjectBuilder4, so see Hello, World of OPC UA! and Creating.

Callback Registration

The callback mechanism of the stack requires to create method-handlers and register them in the stack by the startup of the application. To do it, declare in Library.h the methods:

#include "OpcUaStackCore/Application/ApplicationAuthenticationContext.h"
#include "OpcUaStackCore/Application/ApplicationAutorizationContext.h"
#include "OpcUaStackCore/Application/ApplicationCloseSessionContext.h"

class Library
: public ApplicationIf
{
  public:
  Library(void);
  virtual ~Library(void);

  //- ApplicationIf -----------------------------------------------------
  virtual bool startup(void);
  virtual bool shutdown(void);
  virtual std::string version(void);
  //- ApplicationIf -----------------------------------------------------

  private:
  void authenticationCallback(ApplicationAuthenticationContext* context);
  void closeSessionCallback(ApplicationCloseSessionContext* context);
  void autorizationCallback(ApplicationAutorizationContext* context);
};

And add the following code to Library.cpp:

#include "OpcUaStackServer/ServiceSetApplication/RegisterForwardGlobal.h"

bool
Library::startup(void)
{

  RegisterForwardGlobal registerForwardGlobal;
  registerForwardGlobal.setAuthenticationCallback(boost::bind(&Library::authenticationCallback, this, _1));
  registerForwardGlobal.setAutorizationCallback(boost::bind(&Library::autorizationCallback, this, _1));
  registerForwardGlobal.setCloseSessionCallback(boost::bind(&Library::closeSessionCallback, this, _1));
  if (!registerForwardGlobal.query(&this->service())) {
      std::cout << "registerForwardGlobal response error" << std::endl;
      return false;
  }

  return true;
}

void
Library::authenticationCallback(
    ApplicationAuthenticationContext* context)
{

}

void
Library::closeSessionCallback(
    ApplicationCloseSessionContext* context)
{

}

void
Library::autorizationCallback(
    ApplicationAutorizationContext* context)
{

}

As you can see, we use RegisterForwardGlobal transaction for the registration our callbacks in the stack. We wrap our handler-methods in bind-objects and pass them to the transaction then we call query to send callbacks to the stack.

When the subscribed events happen, the stack calls handler-methods and pass them context with input information and get result of the callback with it as well.

We’ll show you how it’s working in the next sections.

Authentication

To implement the authentication, our example should have some list of allowed users. Since we’ll need to pass information about the current user between the stack and the application, we represent the user as a class based on UserContext and make a map (userMap_) of them in Library.h:

class UserProfile : public UserContext {
public:
  typedef boost::shared_ptr<UserProfile> SPtr;
  typedef std::map<std::string, UserProfile::SPtr> Map;
      UserProfile(std::string username, std::string password, std::string access)
              : username_(username)
              , password_(password)
              , access_(access)
      {

      }

      std::string username_;
      std::string password_;
      std::string access_;
};

class Library
: public ApplicationIf
{
  public:
  Library(void);
  virtual ~Library(void);

  //- ApplicationIf -----------------------------------------------------
  virtual bool startup(void);
  virtual bool shutdown(void);
  virtual std::string version(void);
  //- ApplicationIf -----------------------------------------------------

  private:
    void authenticationCallback(ApplicationAuthenticationContext* context);
    void closeSessionCallback(ApplicationCloseSessionContext* context);
    void autorizationCallback(ApplicationAutorizationContext* context);

    UserProfile::Map userMap_;
};

Now we’re placing two users into the map in method startup. User_RW has right to read and write data, User_R can only read:

bool
Library::startup(void)
{
    RegisterForwardGlobal registerForwardGlobal;
    registerForwardGlobal.setAuthenticationCallback(boost::bind(&Library::authenticationCallback, this, _1));
    registerForwardGlobal.setAutorizationCallback(boost::bind(&Library::autorizationCallback, this, _1));
    registerForwardGlobal.setCloseSessionCallback(boost::bind(&Library::closeSessionCallback, this, _1));
    if (!registerForwardGlobal.query(&this->service())) {
      std::cout << "registerForwardGlobal response error" << std::endl;
      return false;
    }

    userMap_ = UserProfile::Map();
    userMap_["User_RW"] = boost::make_shared<UserProfile>("User_RW", "password1", "rw");
    userMap_["User_R"] = boost::make_shared<UserProfile>("User_R", "password2", "r");

    return true;
}

When we have the list of the allowed users, we can implement our authentication method:

#include "OpcUaStackCore/StandardDataTypes/UserNameIdentityToken.h"

// ...
void
Library::authenticationCallback(
              ApplicationAuthenticationContext* contex)
{
      Log(Debug, "Event::authenticationCallback")
              .parameter("SessionId", contex->sessionId_);


      if (contex->authenticationType_ == OpcUaId_AnonymousIdentityToken_Encoding_DefaultBinary) {
              contex->statusCode_ = BadIdentityTokenRejected;
      }
      else if (contex->authenticationType_ == OpcUaId_UserNameIdentityToken_Encoding_DefaultBinary) {

              OpcUaExtensibleParameter::SPtr parameter = contex->parameter_;
              UserNameIdentityToken::SPtr token = parameter->parameter<UserNameIdentityToken>();

              // find user profile
              UserProfile::Map::iterator it;
              it = userMap_.find(token->userName());
              if (it == userMap_.end()) {
                      contex->statusCode_ = BadUserAccessDenied;
                      return;
              }

              UserProfile::SPtr userProfile = it->second;

              // check password
              if (token->password() != userProfile->password_) {
                      contex->statusCode_ = BadUserAccessDenied;
                      return;
              }

              contex->userContext_ = userProfile;
              contex->statusCode_ = Success;
      }
      else if (contex->authenticationType_ == OpcUaId_X509IdentityToken_Encoding_DefaultBinary) {
              contex->statusCode_ = BadIdentityTokenRejected;
      }
      else {
              contex->statusCode_ = BadIdentityTokenInvalid;
      }
}

OPC UA Specification determines several kinds of authentication and the example application supports only the identification by username and password. If the client tries to authenticate itself with the unsupported type, the method notifies the stack about it by writing status BadIdentityTokenRejected to the context:

contex->statusCode_ = BadIdentityTokenRejected;

The stack denies to open the Session with the client.

In case, where the client uses OpcUaId_UserNameIdentityToken_Encoding_DefaultBinary identity token, we can get from it the username and the password to check them:

if (contex->authenticationType_ == OpcUaId_UserNameIdentityToken_Encoding_DefaultBinary) {

  ExtensibleParameter::SPtr parameter = contex->parameter_;
  UserNameIdentityToken::SPtr token = parameter->parameter<UserNameIdentityToken>();

  // find user profile
  UserProfile::Map::iterator it;
  it = userMap_.find(token->userName());
  if (it == userMap_.end()) {
    contex->statusCode_ = BadUserAccessDenied;
    return;
  }

  UserProfile::SPtr userProfile = it->second;

  // check password
  if (token->password() != userProfile->password_) {
    contex->statusCode_ = BadUserAccessDenied;
    return;
  }

  contex->userContext_ = userProfile;
  contex->statusCode_ = Success;
}

The authentication method should write into the context BadUserAccessDenied status if there is no allowed user with the given username or the password mismatches. The method should write into the context Success status if the authentication is successful, so that the stack allows to open the session with the client. Pay attention, that we’ve saved the authenticated user into the context->userContext_. The stack connects the user to the activated session and passes it as a current user with the context to all other method-handlers of services during the Session.

Authorization

In the previous section we’ve learned how we can implement authentication in our application by using the stack. Now we’re going to figure out how to give the authenticated users permissions to write or to read the data or denied it.

The following code implement a very simple access control:

void
Library::autorizationCallback(
  ApplicationAutorizationContext* context)
{
  if (!context->userContext_) {
    context->statusCode_ = BadUserAccessDenied;
    return;
  }

  auto user = boost::dynamic_pointer_cast<UserProfile>(context->userContext_);

  bool allowed = false;
  switch (context->serviceOperation_) {
    case ServiceOperation::Read:
    case ServiceOperation::MonitoredItem:
      allowed = user->access_ == "r" || user->access_ == "rw";
      break;
    case ServiceOperation::Write:
      allowed = user->access_ == "rw";
      break;
    default:
      break;
  }

  context->statusCode_ = allowed ? Success : BadUserAccessDenied;
}

Method autorizationCallback is the callback which we registered in the stack (see Callback Registration). It is called every time when the client makes an attempt to subscribe, write or read data from the server. First of all our method checks if the current Session is authenticated at all. The authenticated Session should have non-null pointer to UserContext:

if (!context->userContext_) {
  context->statusCode_ = BadUserAccessDenied;
  return;
}

Our application doesn’t allow non-authenticated clients to do anything, so we set BadUserAccessDenied into the status and stop handling the callback. Of course you can follow some different policy.

The next step is to get our UserProfile instance, which we’ve passed to the stack in authenticationCallback,from the context by dynamic casting:

auto user = boost::dynamic_pointer_cast<UserProfile>(context->userContext_);

Now we can allow users with access rw and r read and subscribe to all Nodes of Information Model and allow users only with rw to write.

bool allowed = false;
switch (context->serviceOperation_) {
  case ServiceOperation::Read:
  case ServiceOperation::MonitoredItem:
    allowed = user->access_ == "r" || user->access_ == "rw";
    break;
  case ServiceOperation::Write:
    allowed = user->access_ == "rw";
    break;
  default:
    break;
}

context->statusCode_ = allowed ? Success : BadUserAccessDenied;

As you can see, we should assign Success to the context’s status if the access is allowed and BadUserAccessDenied if the access is denied.

Close Session

Sometimes a user application needs to be notified when the Session is closed. To catch this event we have registered closeSessionCallback in _access_control_callback_registration section. Now we can make it write them name of the authenticated user when the user closes the Session.

void
Library::closeSessionCallback(
  ApplicationCloseSessionContext* context)
{
  if (!context->userContext_) {
    return;
  }

  auto user = boost::dynamic_pointer_cast<UserProfile>(context->userContext_);

  Log(Info, "User close the session.")
    .parameter("Username", user->username_)
    .parameter("SessionId", context->sessionId_);
}

What Next?

The access control is a enough complicated topic and we couldn’t describe it completely. You can find on our Demo-Project a more complex implementation of the authentication and authorization.

OPC UA Specification

  • Part 4 Services, 5.6 Session Service Set.