web
You’re offline. This is a read only version of the page.
close
Skip to main content
Community site session details

Community site session details

Session Id :
Dynamics 365 Community / Blogs / AX for Retail / Basics of building native c...

Basics of building native client capable of C2 authentication with Retail Server

SergeyP Profile Picture SergeyP 2,928

Introduction

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. Here we will leverage the Retail Server's capability to accept any Open ID Connect provider for C2 authentication so the calling 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>

The payload of the header consists of the prefix id_token, then there is a white space followed by 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 claims, for instance: issuer, expiration, audience.

To validate those parameters Retail Server uses set of settings specified in AX UI.

AAD Setup

2 AAD applications need to be created in your instance of Azure Active Directory, one representing the calling client application and another one is the target application - Retail Server.

Retail Server Application

Navigate to https://aad.portal.azure.com/ and then perform the below actions.

a) Click Azure Active Directory->App registrations

b) Click New registration

c) In the field Name specify any application name, for instance, Test Retail Server

d) Keep the default value for the field Account Type: "Accounts in this organizational directory only"

e) Hit the Register button

f) Once the application is created hit the link Expose an API 

g) Click Add a scope, accept default value, the one including GUID, for the scope and hit Save and continue button.

h) For the field Name specify Legacy.Access.Full

i) For the field Who can consent? select Admins and users

j) For the fields Admin consent display name and User consent display name type Access Retail Server

k) For the fields Admin consent description and User consent description type Gives an access to Retail Server's APIs.

l) Click Add Scope button
The created scope will have a value in the below form: api://<YourAppIdIsHere>/Legacy.Access.Full, you will need it later while setting up the Client Application and acquiring a security token.

Client Application

Navigate to https://aad.portal.azure.com/ and then perform the below actions.

a) Click Azure Active Directory->App registrations

b) Click New registration

c) In the field Name specify any application name, for instance, Test Client

d) Keep the default value for the field Account Type: "Accounts in this organizational directory only"

e) Hit the Register button

f) Once the application is created hit the link Add a Redirect URI under the section Redirect URIs

g) Click Add a platform

h) Under Custom Redirect URIs type http://localhost

i) Click Configure button.

j) Click API permissions and then Add a permission and select the tab APIs my organization uses

k) In the search box type the name of the Retail Server application created in the previous section, i. e. Test Retail Server

l) Mark the checkbox near the permission Legacy.Access.Full and then click the button Add permissions

HQ Setup

Navigate to HQ's section Retail and Commerce->Headquarters setup->Parameters->Commerce shared parameters->Identity Providers. You will see a UI with 3 grids allowing to manage Identity Providers and Relying Parties.

1. If not present yet, use the grid Issuers to add an issuer with type Open ID Connect and the value (specified in the column Issuer) https://login.microsoftonline.com/<ReplaceWithYourTenantId>/v2.0

2. For the just added issuer add a row in the grid Relying Party specifying the ClientID, corresponding to the AAD application you created, in the column ClientID. For the column Type use Public; for the column UserType use Customer.

3. Hit Save button and execute the job 1110 to bring the changes into the Channel DB. Due to the cache on Retail Server side you will either need to wait (up to 10 minutes) until the cache is invalidated, or, optionally, you might want to restart the Retail Server to avoid waiting. If you are doing this for PROD mind implications caused by the restart so avoid doing that during active business hours.

Flow description

Once the security token is validated, RS can 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.

Creating Client Application

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 be a Console app.

I will use Visual Studio 2022 to create the client application

1. Create a new project of type Console App. Use any Framework marked as Long-term-support, I will use .NET 6.0

2. Install NuGet Package Microsoft.Identity.Client to the just created project

3. In Manage NuGet Packages form configure the Package sources by adding the package dynamics365-commerce pointing to the https://pkgs.dev.azure.com/commerce-partner/Registry/_packaging/dynamics365-commerce/nuget/v3/index.json 

4. Install the Nuget Package Microsoft.Dynamics.Commerce.Proxy.ScaleUnit from just added package source.

5. Modify the autogenerated file Program.cs by using the below template (modify the constants to match your tenant/applications):

using Microsoft.Dynamics.Commerce.RetailProxy;
using Microsoft.Identity.Client;

const string ClientId = "ReplaceWithYourClientAppId";
const string Authority = "https://login.windows.net/ReplaceWithYourTenantId/";
const string RedirectUri = "http://localhost";
const string RetailServerUri = "https://ReplaceWithYourRetailServerUrl/Commerce";
const string RetailServerScope = "ReplaceWithYourRetailServerAadAppId/.default";
const string OperatingUnitNumber = "068";

var builder = PublicClientApplicationBuilder.Create(ClientId)
  .WithAuthority(Authority)
  .WithRedirectUri(RedirectUri);

IPublicClientApplication app = builder.Build();
AuthenticationResult authResult = await app.AcquireTokenInteractive(new string[] { RetailServerScope   }).ExecuteAsync().ConfigureAwait(false);

RetailServerContext context = RetailServerContext.Create(
  new Uri(RetailServerUri), OperatingUnitNumber, authResult.IdToken);
ManagerFactory factory = ManagerFactory.Create(context);
ICustomerManager manager = 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 manager.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 != "Microsoft_Dynamics_Commerce_Runtime_CommerceIdentityNotFound")
  {
    // Something went wrong, let the exception to go up.
    throw;
  }
}

const int CustomerTypePerson = 1;

// 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", FirstName = "Demo", CustomerTypeValue = CustomerTypePerson };
  customer = await manager.Create(customer);
  Console.WriteLine("Successfully created the customer");

}
else
{
  Console.WriteLine($"The customer {customer.Name} was created previously");
}

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.

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().

In case you already have your own Identity Provider which doesn't support Open ID Connect and you want to keep that provider's users' database with all existing and future users you still can access Retail Server if you "wrap" your Identity Provider with Open ID Connect protocol. While doing so you can consider leveraging existing Open Source libraries suitable for your platform, some of them can be found here.

Comments

*This post is locked for comments

  • SergeyP Profile Picture SergeyP 2,928
    Posted at

    to:  Vishal Patel

    That looks like the token you provided was not accepted by Retail Server, please look into Retail Server log for warnings/errors. One of possible reasons is: you forgot to setup Identity Provider and Relying Party and/or didn't execute a job and/or didn't make sure it completed

  • Community Member Profile Picture Community Member
    Posted at

    Hi all ,

    1)We have followed the above sample to access retail server in C2 context .

    2)We are getting access token from google api's .

    3)then passing the token to create manager factory

    4) then while creating customer .

      customer = await customerManager.Read(string.Empty);

    We are getting the below exception.

    "Microsoft.Dynamics.Commerce.RetailProxy.UserAuthenticationException"

    Any suggestion to resolve this errors  ?

    Thanks and Regards ,

    Vishal .

  • SergeyP Profile Picture SergeyP 2,928
    Posted at

    to AJ1: that might be cache related - how much time did you wait before trying to access newly entered (in AX) client? If you need to invalidate the cache "right now" please recycle Retail Server'a app pool (or you can do iisreset but that would be more rough way) and see whether you will be unblocked.

  • Community Member Profile Picture Community Member
    Posted at

    Hi Sergey,

    I have tried about sample WPF project for a new Client Id.

    1. Just changed the Client Id value with my new Client Id in App.config.

    2. New Client Id is setup in AX as described above in article and ran 1110 job as well. I can see my new Relying party values in the DBs and 1110-job has run successfully.

    3. Run the WPF project in the debug mode

    4.  Receive a new Token Id after Google Sign-in (it means, credentials has been authenticated from Google provider)

    5. I continue running in the debug mode

    6. It fails in the run time at customerManager.Read(string.Empty)

    // 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);

               }

    7. I have found RetailServer log:

    An error occurred during the Retail Server Request. RequestUri: ax7rtw1devret.cloudax.dynamics.com/.../Customers('')?api-version=7.1. RequestId: . Exception: Microsoft.Dynamics.Commerce.Runtime.UserAuthenticationException: Identity token validation failed.

      at Microsoft.Dynamics.Retail.RetailServerLibrary.Middlewares.Authentication.ExternalIdentityAuthenticationMiddleware.d__11.MoveNext()

    --- End of stack trace from previous location where exception was thrown ---

      at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)

      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)

      at Microsoft.Dynamics.Retail.RetailServerLibrary.Middlewares.Authentication.OperatingUnitAuthenticationMiddleware.d__1.MoveNext()

    --- End of stack trace from previous location where exception was thrown ---

      at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)

      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)

      at Microsoft.Dynamics.Retail.RetailServerLibrary.Middlewares.Authentication.DeviceTokenAuthenticationMiddleware.d__2.MoveNext()

    --- End of stack trace from previous location where exception was thrown ---

      at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)

      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)

      at Microsoft.Dynamics.Retail.RetailServerLibrary.Middlewares.Authentication.CommerceIdTokenAuthenticationMiddleware.d__3.MoveNext()

    --- End of stack trace from previous location where exception was thrown ---

      at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)

      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)

      at Microsoft.Dynamics.Retail.RetailServerLibrary.Middlewares.Instrumentation.InstrumentationMiddleware.d__3.MoveNext().

    Can you please unblock me. Your help is much appreciated.

    Thanks,

    Ajay

  • Mohd saddaf khan Profile Picture Mohd saddaf khan 20
    Posted at

    Hi Sergey,

    Need your help in below post. Its regarding authentication of retail server using separate client ID...

    community.dynamics.com/.../205387

    Thanks.

  • Ajay J Profile Picture Ajay J 106
    Posted at

    Thanks Sergey.

    I was missing step to delete all auto generated C# and XAML files. After deleting auto-generated .XAML file, it has worked for me.

    Thanks for this post showing C2 authentication with Retail Server.

  • SergeyP Profile Picture SergeyP 2,928
    Posted at

    HI Ajay,

    I have just tried to follow the steps on LCS VM and could not reproduce the issue - I didn't see a blank screen, instead I saw Google's UI to authenticate.

    Several questions:

    a) Are you able to navigate to the google web site if you are using just IE ?

    b) Is Java Script enabled in your browser?

    c) Can you enable script debugging (Internet Options->Advanced->Disable script debugging) and see whether you will see any script errors?

    d) Any special (around security I think) settings in your web site?

    e) Can you try to execute the code on another machine?

    You said "I continue" - can you provide more details - what exactly you clicked on that blank screen?

    As for your question why you don't see the token: if you said you were not able to authentication to Google then it is expected then you will not have a token.

    The token is retrieved in the following line:

    string idToken = parameters["id_token"];

    But since you didn't have a chance to enter Google credentials that code was not executed.

  • Ajay J Profile Picture Ajay J 106
    Posted at

    Hi Sergey,

    I followed the above example with a WPF project, copied the above code and updated the RetailServerName in App.config file.

    When i run program in debug mode it opens a Main window with blank white screen. I don't see Google Sign-in page. I continue and it returns false at below line and jump into else condition.

    if (title.StartsWith("Success", StringComparison.OrdinalIgnoreCase))

    I checked AuthorizationRequest value, it is:

    accounts.google.com/.../auth;redirect_uri=urn:ietf:wg:oauth:2.0:oob:auto&state=myState&scope=openid&response_type=code id_token&nonce=MyNonce

    Questions:

    1. Why do i not see Google Sign-In page, am i missing anything?

    2. I don't see Authorization header part with "id_token #TokenValue" in the above code. Where do you get and set id_token in the above example.

    Can you please help me to understand.

    Best Regards,

    Ajay