The article How to access Retail Server in managed code demonstrated a way to access Retail Server in Anonymous context. This article will explain how to communicate to Retail Server in C2 context, or, in other words, in context of a signed-in Customer.
As was mentioned in previous article C2 authentication is one out of 3 possible ways to access Retail Server. There are 2 ways to authenticate as C2:
- By leveraging Azure ACS. You can see this type of authentication in action in Online Store app deployed on the demo VM if you will sign-in with Microsoft or Facebook.
- By leveraging any Open ID Connect provider. This type, again, can be seen in action in the Online Store app if you will choose Google as Identity Provider.
Here we will look at Open ID Connect option. So, if an application wants to interact in C2 context with Retail Server it should "speak", in terms of authentication, Open ID Connect "language", in other words the application should provide Retail Server an ID Token in the Authorization header of every request in the following format:
Authorization: id_token <The Token Value>
So, the payload of the header consists of the prefix id_token, then there is a white space and finally the value of the ID Token provided by an Identity Provider.
When Retail Server detects the Authorization header with the aforementioned prefix it will interpret the request as C2 one but only if the token validation successfully passes. The token is validated by Retail Server to make sure it was signed with an expected signature and contains an expected values for number of parameters, for instance: issuer, expiration, audience.
To validate those parameters Retail Server uses set of settings specified in AX UI. If you go to Retail and commerce->Headquarters setup->Parameters->Retail shared parameters->OpenID providers
you will see a UI with 2 grids allowing to change OpenID Connect Providers (Identity Providers in other words) and Relying Parties (client applications), something like this:
So now you should know where to set those parameters (such as Issuer and ClientId) if you need to register your own Provider and Client app. Once the parameters on that form were modified you need to execute the job 1110 to bring the changes into the Channel DB.
So, once the token was validated, RS can now safely access information stored in the token, the main point of interest is Subject Identifier which allows to identify the user authenticated against the Identity Provider. Retail Server (to be specific CRT which is hosted by RS) maintains a map with a composite key ProviderID and SubjectID (which can also be called like External Identifier) and a value which is an AX Customer ID. This means that RS is able to know what Customer ID corresponds to a given External ID which was provided by the Identity Provider. That map is used for every C2 request and results in RS/CRT sets the tread's principal containing right AX Customer Account Number. Then it is used for other types of checks, such as Authorization for instance, in several different places.
As was mentioned in the previous article RS supports 3 authentication modes but there is one case which requires special attention - that is a call to Create a Customer. That operation is something in the middle between C2 and Anonymous so it could be set as partially anonymous and partially C2, this is why:
- it is partially anonymous because it will not fail just because the request to CreateCustomer contains id_token which doesn't have a corresponding record in the map (described above) to locate existing AX Customer. In fact, the map will be updated by this Create Customer call so it will have a new record corresponding to the token's ID (as a key) and AX Customer (as a value).
- it is partially C2 because this call requires the request's header to contain the id_token prefix followed by a valid token.
As a result of CreateCustomer call RS/CRT will (if all the validations described above pass):
- Create a new AX Customer
- Create a new record in the map where the value will match just created customer.
From now on, all future requests supplying a token corresponding to the same user will be able to map the External ID of the user to AX Customer and as a result all those requests will be executed in a context of right Customer.
How would you know whether, for given Id Token, AX already has a corresponding customer or not? You can execute method ICustomerManager.Read() and check whether it will succeed or whether it will throw UserAuthorizationException with error id "Microsoft_Dynamics_Commerce_Runtime_CommerceIdentityNotFound", if later happens - that is the indication that Id Token was successfully validated but there were no an AX customer mapped to the External Id retrieved from the ID Token provided, therefore, if you encounter that error code you should Create a Customer.
Simple app below demonstrates how to instantiate an instance of Retail Server Context by providing an Id Token and it also shows how to Read/Create a customer. I tried to present in this app only what is required for the subject of this article so you would have an idea where/how to start, but your real app will most likely be more "verbose" and not necessary use WPF.
To create the application use Visual Studio 2015 and create new WPF Project, then reference the same 2 DLLs which were references in How to access Retail Server in managed code. Then delete all auto generated C# and XAML files and add 2 files with the content below.
MainWindow.cs (ignore those numbers on the left - they, by some reason, were put by the editor when I was pasting the code):
using System; using System.Collections.Specialized; using System.Configuration; using System.Threading.Tasks; using System.Web; using System.Windows; using System.Windows.Controls; using System.Windows.Navigation; using Microsoft.Dynamics.Commerce.RetailProxy; class MainWindow : Window { [STAThread] static void Main() { Application app = new Application(); app.Run(new MainWindow()); } const string RequestFormatString = "{0}?client_id={1}&redirect_uri={2}&state={3}&scope={4}&response_type={5}&nonce={6}"; static readonly string AuthenticationEndpoint = ConfigurationManager.AppSettings["AuthenticationEndpoint"]; static readonly string ClientId = ConfigurationManager.AppSettings["ClientId"]; static readonly string RedirectUri = ConfigurationManager.AppSettings["RedirectUri"]; static readonly string AuthorizationRequest = string.Format(RequestFormatString, AuthenticationEndpoint, ClientId, RedirectUri, "myState", "openid", "code id_token", "MyNonce"); const string ErrorCommerceIdentityNotFound = "Microsoft_Dynamics_Commerce_Runtime_CommerceIdentityNotFound"; static ManagerFactory factory; public MainWindow() { WebBrowser browser = new WebBrowser(); Content = browser; browser.Navigated += browser_Navigated; browser.Navigate(AuthorizationRequest); } private async void browser_Navigated(object sender, NavigationEventArgs e) { dynamic doc = (sender as WebBrowser).Document; string title = doc.Title; if (title.StartsWith("Success", StringComparison.OrdinalIgnoreCase)) { NameValueCollection parameters = HttpUtility.ParseQueryString(title); string idToken = parameters["id_token"]; factory = CreateManagerFactory(idToken); Customer customer = await GetOrCreateCustomer(); Title = string.Format("Account Number:{0}, Email:{1}", customer.AccountNumber, customer.Email); } else { if (title.StartsWith("Denied", StringComparison.OrdinalIgnoreCase)) { Title = "Access denied."; } } } ManagerFactory CreateManagerFactory(string idToken) { // Creating an instance of RetailServerContext by supplying: // a) Retail Service URL // b) Operating Unit Number // c) Id Token. RetailServerContext context = RetailServerContext.Create( new Uri(ConfigurationManager.AppSettings["RetailServerRoot"]), ConfigurationManager.AppSettings["OperatingUnitNumber"], idToken); // Creating a factory based on RetailServerContext. ManagerFactory managerFactory = ManagerFactory.Create(context); return managerFactory; } async Task<Customer> GetOrCreateCustomer() { // Creating instance of Customer's Manager to send requests specific to Customer entity. ICustomerManager customerManager = factory.GetManager<ICustomerManager>(); Customer customer = null; // Trying to read existing customer mapped to an external user's ID provided by the Identity Provider via Id Token. try { customer = await customerManager.Read(string.Empty); } catch (UserAuthorizationException exception) { // In case Id Token is valid but there is no AX Customer mapped // to the external user's ID yet, RS will return the error code below. // This errr code should be an indication to the client app that it should // initiate the customer creation. if (exception.ErrorResourceId != ErrorCommerceIdentityNotFound) { // Something went wrong, let the exception to go up. throw; } } // If no customer was found create a new one. if (customer == null) { long salt = DateTime.Now.Ticks; customer = new Customer { AccountNumber = string.Empty, Email = "mail" + salt + "@contoso.com" }; customer = await customerManager.Create(customer); } return customer; } }
App.config (this is standard Application Configuration File), don't forget to update the server name for the parameter RetailServerRoot.
<configuration> <appSettings> <add key="OperatingUnitNumber" value="068" /> <add key="RetailServerRoot" value="https://PutYourServerNameHere.cloudax.test.dynamics.com/Commerce" /> <add key="AuthenticationEndPoint" value="https://accounts.google.com/o/oauth2/v2/auth"/> <add key="ClientId" value="58340890588-fj2b165t1n40c1s2pdeig9tv7cut70dk.apps.googleusercontent.com"/> <add key="RedirectUri" value="urn:ietf:wg:oauth:2.0:oob:auto"/> </appSettings> </configuration>
Note that the value of ClientId matches the value provided in AX form mentioned above, in its section RELYING PARTIES.
The application hosts WebBrowser control which is asked to issue an Authorization Request to the Identity Provider (this is needed to get an Id Token), as a result you will be redirected to the provider's site and will need to enter your credentials:
Once you provide valid credentials you will be redirected to a consent form:
And finally, once you click the Allow button the execution will continue in the following block of the browser_Navigated callback:
if (title.StartsWith("Success", StringComparison.OrdinalIgnoreCase)) { // ... }
From here on the application will try to read the customer by supplying the Retail Server the ID Token provided by the Identity Provider and then, if the customer doesn't exist yet - it will create a new customer. In real application you will most likely want to ask a customer for First/Last/Email and other information but for this demo purposes the email is just almost (with some salt) hardcoded.
One the customer info is retrieved it (the Account Number given by AX) will be displayed in the application Title's:
Now you should have an understanding how to interact with Retail Server in a context of authenticated customer. So, you can keep sending requests to Retail Server (by using Retail Proxy as was shown above) to work with any manager, for instance, you can create a Shopping Cart by leveraging ICartManager.Create() and then add products to it by calling ICartManager.AddCartlines().
The article uses WPF as a client framework to work with Retail Proxy but you can leverage any other UI framework (like ASP.NET, Windows Forms, ...) which better serves your needs.