Experimenting with Sitecore non-interactive login, function delegate among other aspects

Non-interactive login to any authentication-based instance opens up a lot of interesting possibilities. For more information regarding how to setup non-interactive login to a Sitecore instance, check this url - https://doc.sitecore.com/en/developers/101/developer-tools/configure-a-non-interactive-client-login.html

Just thinking of the fact that I won't see the admin and b credentials anywhere in my code or scripts excited me to this approach.

I thought a better way to portray the benefits is to put up a blog that will use the non-interactive login and then pull a Sitecore item invoking the Sitecore itemservice API. So, here is the end-result:

I started with a clean-slate Commerce installation setup using SCIA, but a Sitecore 10.1 instance would do. Sitecore Launchpad just for reference:


Check this blog of mine, to see how to setup a local folder for Sitecore CLI.

Note that from here, I follow a free-hand approach to setting up the non-interactive login but if you are someone who believes in a step-by-step approach follow this excellent Sitecore documentation.

For my new local folder to understand Sitecore CLI commands, I decide to run a PS script  that holds all the necessary commands (named it arbitrarily as scs.ps1, choose your choice of name!). Note that I have added the command to include the Serialization plugin since I or anybody would need it as part of setting up serialization. 

***********************************************************************

Interactive Sitecore Login:

dotnet new tool-manifest; dotnet tool install Sitecore.CLI --add-source https://nuget.sitecore.com/resources/v3/index.json; dotnet sitecore init;dotnet sitecore login --authority https://sc1011identityserver.dev.local --cm https://sc1011sc.dev.local --allow-write true;dotnet sitecore plugin add -n Sitecore.DevEx.Extensibility.Serialization

Non-Interactive Sitecore Login:

dotnet new tool-manifest; dotnet tool install Sitecore.CLI --add-source https://nuget.sitecore.com/resources/v3/index.json; sitecore login --authority https://sc1011identityserver.dev.local --cm https://sc1011sc.dev.local --allow-write true --client-credentials true --client-id "NonInteractiveClient" --client-secret "SuperSecretNonInteractiveClient"  --allow-write true;dotnet sitecore plugin add -n Sitecore.DevEx.Extensibility.Serialization

***********************************************************************

Note that I named the above non-interactive login command as scs.ps1 and executed the PS file and received back this set of messages:


Everything looks good except this most important message - "Error while getting client credentials token: invalid_client"

Looks like the vital command of setting up the non-intrusive login didn't work. To confirm the same, I open the user.json under the newly created .sitecore folder and this is what I see or, in other words, there is no login/user information present:


Based on Sitecore documentation and the error thrown at me, I deduce that the non-interactive CLI login command is unable to find the specified client id - "NonInteractiveClient" in the identity server role. So, I take a step backward and add the Sitecore.IdentityServer.DevEx.xml under <identityserver root>\config folder with the following changes.  In other words, I match the values passed in the non-interactive login command.


When I execute the scs.ps1 file, I still get the same error as above - "Error while getting client credentials token: invalid_client"

The reason is, I have not yet setup the server side mapping. So, I add the same (Sitecore.Owin.Authentication.ClientCredentialsMapping.config) to the CM role. CM role here means the CM web root folder since both CM and ID Server are in the same machine in my case.


My client id must match what I added in the ID server Sitecore.IdentityServer.DevEx.xml.

Restart both the ID Server and CM instance and re-execute the PS command.

When I executed the scs.ps1 file, I was time and again getting the same  "Error while getting client credentials token: invalid_client" error and later I realized that the clientname and clientid tags must have the same value:


So, don't try giving a descriptive name (like me) different from the id!

But, the good news is, I wasn't popped the Sitecore id server instance asking for credentials and indeed the secrets were saved silently in user.json!


Next, its time to write some dirty code to access a Sitecore item using the non-interactive login.

For this exercise, I decide to invoke the following method from Sitecore.DevEx.Configuration package since it provides an option to pass client info and invoke a function delegate - 

public override async Task<HttpResponseMessage> MakeAuthenticatedRequest(
      HttpClient client,
      Func<HttpClient, Task<HttpResponseMessage>> requestAction)

Now, time for some action -

I create a asp.net core web application and reference Sitecore.DevEx.Configuration package.



I then add the HttpClient in startup.cs:

services.AddHttpClient("HttpClient", m =>
            {
                m.DefaultRequestHeaders.Accept.Clear();
                m.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
                m.BaseAddress = new Uri("https://sc1011sc.dev.local");
            }).SetHandlerLifetime(TimeSpan.FromMinutes(10))
            .ConfigurePrimaryHttpMessageHandler(() =>
            {
                var handler = new HttpClientHandler()
                {
                    AllowAutoRedirect = false,
                    UseCookies = false
                };

                handler.ClientCertificateOptions = ClientCertificateOption.Manual;
                handler.ServerCertificateCustomValidationCallback =
                               (httpRequestMessage, cert, cetChain, policyErrors) => true;

                return handler;
            }); 


By the way, those base address and httpclient strings in middle of  adding the httpclient is an example of "how not to write code!"

Next, some DI in action, pass the IHttpClientFactory through the constructor of the page we plan to write some more code:



Now, the actual code is broken down here line-by-line:

First, create an instance of EnvironmentConfiguration class and this is how it is done -

EnvironmentConfiguration environmentConfiguration = new EnvironmentConfiguration(new LoggerFactory(), null);

Now, I thought if I could do this in a better way and checked the class by itself, wasn't implementing an interface and, since this is an example, I decided to keep it simple. Sorry purists! 



The second arg in the EnvironmentConfiguration constructor is an action delegate that accepts EnvironmentConfiguration as an input arg and it didn't serve any purpose in my case. So, I decided to pass a null value.

Some basics here - An action delegate is any method that returns void but takes an arg specified in the delegate, in the above case, the arg is of type EnvironmentConfiguration.

Moving forward, with the next line of code, I want to create a model of the EnvironmentConfiguration and pass it around. So, decided to do it by invoking a method -

environmentConfiguration = GetEnvConfigurationData(environmentConfiguration);

The concerned method looks like this-

private EnvironmentConfiguration GetEnvConfigurationData(EnvironmentConfiguration envConfg)
        {
            EnvironmentConfiguration envConfig = envConfg;

            envConfig.Authority = new Uri("https://sc1011identityserver.dev.local");
            envConfig.Name = "SC1011Instance";
            envConfig.Host = new Uri("https://sc1011sc.dev.local");
            envConfig.ClientId = "NonInteractiveClient";
            envConfig.ClientSecret = "SuperSecretNonInteractiveClient";
            envConfig.AllowWrite = true;
            envConfig.UseClientCredentials = true;

            return envConfig;
        }

The actual action starts next, when we invoke one of the DevEx methods:

environmentConfiguration.Validate();

If you don't pass the Name highlighted above, Validate will throw an exception. So happy to see the first of the Sitecore.DevEx.Configuration.Authentication methods fire.

Since MakeAuthenticatedRequest expects an Function delegate, its time to get that prepared.

Func<HttpClient, Task<HttpResponseMessage>> myRespData = GetAsyncData;

This is how my GetAsyncData method looks:

public async Task<HttpResponseMessage> GetAsyncData(HttpClient client) { return await client.GetAsync("sitecore/api/ssc/item/110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9");

A function delegate is different from the action delegate that a function delegate method will return a type while action delegate method will return void but both can accept arguments.

So, in our case above, we need a method that will return a HttpResponseMessage task and takes HttpClient as an input arg. 

The name of such a method is GetAsyncData and myRespData is the instance name for the function delegate in this code of mine.

Now, time to invoke the MakeAuthenticatedRequest method -

using HttpClient httpClient = clientFactory.CreateClient("HttpClient");
                Task<HttpResponseMessage> myItemResp = environmentConfiguration.MakeAuthenticatedRequest(httpClient, myRespData);

No surprises here, we pass the httpClient variable along with the function delegate instance name and invoke the MakeAuthenticatedRequest method and this in turn invokes our GetAsyncData method and returns the data to the program.


Complete code in the page looks like this:

public IndexModel(ILogger<IndexModel> logger, IHttpClientFactory clientFactory)
        {
            _logger = logger;
            try
            {
                EnvironmentConfiguration environmentConfiguration = new EnvironmentConfiguration(new LoggerFactory(), null);

                environmentConfiguration = GetEnvConfigurationData(environmentConfiguration);
                environmentConfiguration.Validate();

                Func<HttpClient, Task<HttpResponseMessage>> myRespData = GetAsyncData;
                using HttpClient httpClient = clientFactory.CreateClient("HttpClient");
                Task<HttpResponseMessage> myItemResp = environmentConfiguration.MakeAuthenticatedRequest(httpClient, myRespData);

                myItemResp.Wait();
                if (myItemResp.IsCompletedSuccessfully)
                {
                    HttpResponseMessage respmsg = myItemResp.Result;
                    Task<string> str = myItemResp.Result.Content.ReadAsStringAsync();
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        private EnvironmentConfiguration GetEnvConfigurationData(EnvironmentConfiguration envConfg)
        {
            EnvironmentConfiguration envConfig = envConfg;

            envConfig.Authority = new Uri("https://sc1011identityserver.dev.local");
            envConfig.Name = "SC1011Instance";
            envConfig.Host = new Uri("https://sc1011sc.dev.local");
            envConfig.ClientId = "NonInteractiveClient";
            envConfig.ClientSecret = "SuperSecretNonInteractiveClient";
            envConfig.AllowWrite = true;
            envConfig.UseClientCredentials = true;

            return envConfig;
        }

        public async Task<HttpResponseMessage> GetAsyncData(HttpClient client) { return await client.GetAsync("sitecore/api/ssc/item/110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9");


Definitely there is scope for improvement in terms of housekeeping the strings, moving secrets to a keyvault, deserializing client info from user.json and using Helix pattern to name a few. But, most importantly, happy that I could pick up Sitecore content tree data using non-interactive login! 

Comments

Popular Posts