Extending the Extended Logon functionality for MPOS and Cloud POS
Included in MPOS and CPOS are two methods for extended logon functionality: logging in by scanning a barcode or by swiping a card with a magnetic stripe reader (MSR). Setup for these are described here: https://docs.microsoft.com/en-us/en-us/dynamics-365/blog/unified-operations/retail/extended-logon.
One shortcoming of the out-of-the-box functionality, however, is that each of these methods only allows for five significant digits for a unique identifier. For example, if you have two cards with the IDs “1234567” and “1234578” you will find that they will be considered the same “12345” and the second card will fail when it is attempted to be assigned to an operator.
The reason for this problem is relatively simple: the implementation of the extended logon was originally intended to be a sample upon which developers could customize to fit the specific needs of a particular implementation. In prior versions of the product (going back to Dynamics AX 2012 R3) this was much easier since we shipped the full source code to the Commerce Runtime (CRT). However, because the CRT is now sealed, developers have to create a custom CRT service to override existing functionality – something that is difficult to do from scratch.
This blog post is an end-to-end sample that can be used as a starting point for such an implementation. It is intended as a specific workaround for the five-character limit for barcode and MSR scans, but does not discuss extending to other devices.
Here is a link to a zip file for the source code for the sample: Contoso.ExtendedLogonSample
Architecture
There are essentially two main pieces to the functionality for barcode and MSR extended logon: assigning the identifier (barcode or card number) to the user and then creating a hook on the logon screen to act on the event of a cardswipe or barcode scan.
The following is a simplified explanation of the logon screen: when the logon screen displays, MPOS automatically listens for one of the two events (a barcode scan or card swipe). If either of those events fires, instead of attempting to authenticate with an Operator ID and Password, the CRT logon request (UserLogOnServiceRequest ) is called with a special parameter. This parameter, GrantType, notifies the CRT what kind of event was fired. This, along with the actual text (card number or barcode) is used to perform the extended logon workflow instead of the standard workflow.
The second part of the functionality is the assigning the barcode or card number to the Operator ID. This process is similar and re-uses some of the same logic. On the Extended log on screen, the user searches for the Operator ID to which they will be adding an identifier (card number of barcode) and then swipes the card or scans the barcode. Pressing the OK button sends this identifier to the CRT which will then check to see if the identifier is already in use and if not, adds a record to a table to map the Operator ID and barcode or card number.
All of this is done with three CRT requests that you need to implement in your custom service: GetUserEnrollmentDetailsServiceRequest, GetUserAuthenticationCredentialIdServiceRequest, and ConfirmUserAuthenticationServiceRequest.
Notes on the Code
There are three requests that need to be handled by your custom code:
public IEnumerable<Type> SupportedRequestTypes { get { return new[] { typeof(GetUserEnrollmentDetailsServiceRequest), typeof(GetUserAuthenticationCredentialIdServiceRequest), typeof(ConfirmUserAuthenticationServiceRequest) }; } }
public Response Execute(Request request) { if (request == null) { throw new ArgumentNullException("request"); } Response response; Type requestType = request.GetType(); if (requestType == typeof(GetUserEnrollmentDetailsServiceRequest)) { response = this.GetUserEnrollmentDetails((GetUserEnrollmentDetailsServiceRequest)request); } else if (requestType == typeof(GetUserAuthenticationCredentialIdServiceRequest)) { response = this.GetUserAuthenticationCredentialId((GetUserAuthenticationCredentialIdServiceRequest)request); } else if (requestType == typeof(ConfirmUserAuthenticationServiceRequest)) { response = this.ConfirmUserAuthentication((ConfirmUserAuthenticationServiceRequest)request); } else { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Request '{0}' is not supported.", request)); } return response; }
GetUserAuthenticationCredentialIdServiceRequest and GetUserEnrollmentDetailsServiceRequest are very similar: each validates and returns the significant digits of the scanned barcode or swiped card. The first one is used during the logon process and the second one is used when assigning an extended identifier to an operator.
private GetUserAuthenticationCredentialIdServiceResponse GetUserAuthenticationCredentialId(GetUserAuthenticationCredentialIdServiceRequest request) { return this.GetUserAuthenticationCredentialId(request.Credential, request.RequestContext); } private GetUserEnrollmentDetailsServiceResponse GetUserEnrollmentDetails(GetUserEnrollmentDetailsServiceRequest request) { string credentialId = this.GetUserAuthenticationCredentialId(request.Credential, request.RequestContext).CredentialId; return new GetUserEnrollmentDetailsServiceResponse(credentialId, string.Empty); }
Each of these shares the same validation method (GetUserAuthenticationCredentialId). This method operates on two pieces of information: the full string that was just scanned in (barcode or card number) and the current device configuration. It performs three validations and throws specific exceptions if any fail: is the device is even configured to use this type of extended logon (as defined in the functionality profile), whether a good swipe or scan was made, and whether the barcode or card number had enough characters.
If everything looks good, a GetUserAuthenticationCredentialIdServiceResponse with the identifier is returned to the caller. The caller (CRT and then ultimately MPOS) will handle the three exceptions appropriately.
Note that this is the method where the out-of-the-box implementation is hard-coded to only five characters. This sample requires a barcode of at least ten characters and only the first ten characters are stored in the database.
private GetUserAuthenticationCredentialIdServiceResponse GetUserAuthenticationCredentialId(string credential, RequestContext requestContext) { DeviceConfiguration deviceConfiguration = requestContext.GetDeviceConfiguration(); if (!this.IsServiceEnabled(deviceConfiguration)) { throw new UserAuthenticationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_AuthenticationMethodDisabled, "Authentication service is disabled."); } if (string.IsNullOrWhiteSpace(credential)) { throw new DataValidationException(DataValidationErrors.Microsoft_Dynamics_Commerce_Runtime_MissingParameter, "credential"); } if (credential.Length < IdentifierLength) { throw new InsufficientCredentialLengthException(DataValidationErrors.Microsoft_Dynamics_Commerce_Runtime_InvalidFormat, credential.Length, IdentifierLength); } // Only use the first IdentifierLength (10) characters string credentialId = credential.Substring(0, IdentifierLength); return new GetUserAuthenticationCredentialIdServiceResponse(credentialId);
ConfirmUserAuthenticationServiceRequest: This request is the second stage of the logon process. At this point the extended identifier (card number or barcode) has been translated to an Operator ID (based on the mapping in the RetailStaffCredentialTable).
There are two scenarios that affect this request: whether the device is configured to challenge for a password after scanning or not. If the barcode or card swipe is enough (no password needed) then this request essentially does nothing; it returns a NullResponse object which is an indicator to MPOS that a successful logon was made.
However, if the device is configured to require a password, this request will get hit twice during the logon process.
The first pass happens before the user gets prompted for their password. In fact, the specific exception that gets thrown (Microsoft_Dynamics_Commerce_Runtime_PasswordRequired) is a signal to MPOS that it needs to prompt the user for a password. You may notice that this password entry dialog looks different than the standard login screen; it is actually a dialog box.
After the user enters the password, the ConfirmUserAuthenticationServiceRequest gets called a second time, this time with both an Operator ID and a password. The request then is responsible for calling the standard logon request (UserLogOnServiceRequest) before the user can gain access. If no exception is raised from that call, the NullResponse is again returned a successful logon. If an exception is raised (i.e., the password entered is incorrect) it will just bubble up to the caller and MPOS handles it just like if the user entered an incorrect password on the main logon screen.
private Response ConfirmUserAuthentication(ConfirmUserAuthenticationServiceRequest request) { DeviceConfiguration deviceConfiguration = request.RequestContext.GetDeviceConfiguration(); if (!this.IsServiceEnabled(deviceConfiguration)) { throw new UserAuthenticationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_AuthenticationMethodDisabled, "Authentication service is disabled."); } if (this.IsPasswordRequired(deviceConfiguration)) { if (string.IsNullOrWhiteSpace(request.Password)) { throw new UserAuthenticationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_PasswordRequired); } // call auth service for password check UserLogOnServiceRequest passwordAuthenticationRequest = new UserLogOnServiceRequest( request.UserId, request.Password, request.Credential, PasswordGrantType, request.ExtraAuthenticationParameters); request.RequestContext.Execute<Response>(passwordAuthenticationRequest); } return new NullResponse(); }
Additional Notes
- The class needs to implement a property named HandlerName. These are specific identifiers for card swipe nd barcode and are hard-coded in the MPOS code. Because of this, they are not easily changed and your implementation of either should just use the same hard-coded values.
public string HandlerName { get { return "auth://example.auth.contoso.com/barcode"; } }
- All of the data handling for the extended login information is taken care of behind the scenes. When the user is assigned a barcode or card number, it calls Realtime Service to save the number to the RETAILSTAFFCREDENTIALTABLE at Headquarters and also to the local (channel) version of the table for immediate use. Depending on how often you run CDX jobs, it will eventually also get pushed out to other stores as well.
- To test your plugin, compile and deploy to Retail Server as noted in these instructions: https://docs.microsoft.com/en-us/en-us/dynamics-365/blog/unified-operations/retail/dev-itpro/commerce-runtime-extensibility. You will need to add the Extended log on operation to a button grid to be able to assign barcodes to a user. The easiest way to test is to use the Barcode Scanner in the Virtual Peripherals tool – use “work as keyboard wedge” to avoid having to mess with OPOS drivers and hardware profiles.