.. _access_control: 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 :download:`here `. Overview -------- After an OPC UA client has got :term:`Endpoint`\ s of the server by using :ref:`discovery_process` and opened a :term:`Secure Channel` with suitable :ref:`security_policy` and :ref:`security_mode`, it should create and activate :term:`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 :term:`Session` with its user profile. Then the client finishes communication with the server it should close :term:`Session` by calling :term:`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 *OpcUaProjectBuilder3*, so see :ref:`hello_world` and :ref:`creating`. .. _access_control_callback_registration: 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: .. code-block:: cpp :emphasize-lines: 1-3,19-21 #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**: .. code-block:: 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**: .. code-block:: cpp class UserProfile : public UserContext { public: typedef boost::shared_ptr SPtr; typedef std::map 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: .. code-block:: cpp 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"] = constructSPtr("User_RW", "password1", "rw"); userMap_["User_R"] = constructSPtr("User_R", "password2", "r"); return true; } When we have the list of the allowed users, we can implement our authentication method: .. code-block:: cpp #include "OpcUaStackCore/ServiceSet/UserNameIdentityToken.h" // don't forget include this // ... 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) { ExtensibleParameter::SPtr parameter = contex->parameter_; UserNameIdentityToken::SPtr token = parameter->parameter(); // 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: .. code-block:: cpp contex->statusCode_ = BadIdentityTokenRejected; The stack denies to open the :term:`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: .. code-block:: cpp if (contex->authenticationType_ == OpcUaId_UserNameIdentityToken_Encoding_DefaultBinary) { ExtensibleParameter::SPtr parameter = contex->parameter_; UserNameIdentityToken::SPtr token = parameter->parameter(); // 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 :term:`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: .. code-block:: cpp void Library::autorizationCallback( ApplicationAutorizationContext* context) { if (!context->userContext_) { context->statusCode_ = BadUserAccessDenied; return; } auto user = boost::dynamic_pointer_cast(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 :ref:`access_control_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 :term:`Session` is authenticated at all. The authenticated :term:`Session` should have non-null pointer to *UserContext*: .. code-block:: cpp 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: .. code-block:: cpp auto user = boost::dynamic_pointer_cast(context->userContext_); Now we can allow users with access *rw* and *r* read and subscribe to all :term:`Node`\ s of :term:`Information Model` and allow users only with *rw* to write. .. code-block:: cpp 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 :term:`Session` is closed. To catch this event we have registered *closeSessionCallback* in :ref:`_access_control_callback_registration` section. Now we can make it write them name of the authenticated user when the user closes the :ref:`Session`. .. code-block:: cpp void Library::closeSessionCallback( ApplicationCloseSessionContext* context) { if (!context->userContext_) { return; } auto user = boost::dynamic_pointer_cast(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. References ----------- * :ref:`security` * :ref:`discovery_process` * Demo-Project_ OPC UA Specification -------------------- * Part 4 Services, 5.6 Session Service Set. .. _Demo-Project: https://github.com/ASNeG/ASNeG-Demo/blob/master/src/ASNeG-Demo/Library/Authentication.cpp