Service API Setup

Vote:
 

Hello,

Hopefully an easy one here. I'm new to Service API and am trying to setup the service according to the docs:
https://docs.developers.optimizely.com/customized-commerce/v1.3.0-service-api-developer-guide/docs/installation-and-configuration

I have added the following code to the Startup class:

services.AddOpenIDConnect<SiteUser>(
    useDevelopmentCertificate: true,
    signingCertificate: null,
    encryptionCertificate: null,
    createSchema: true,
    options =>
    {
        //options.RequireHttps = !_webHostingEnvironment.IsDevelopment();
        var application = new OpenIDConnectApplication
        {
            ClientId = "postman-client",
            ClientSecret = "postman",
            Scopes = { ServiceApiOptionsDefaults.Scope }
        };

        // Using Postman for testing purpose.
        // The authorization code is sent to postman after successful authentication.
        application.RedirectUris.Add(new Uri("https://oauth.pstmn.io/v1/callback"));
        options.Applications.Add(application);
    },
    configureSqlServerOptions: null);

services.AddServiceApiAuthorization(OpenIDConnectOptionsDefaults.AuthenticationScheme);

But upon starting the application I simply get an ArgumentNullException "Value cannot be null. (Parameter 'connectionString')"

Is the documentation still up to date here? What am I missing?

Regards, Stephen

#311436
Edited, Oct 25, 2023 20:02
Vote:
 

Can you get the full stacktrace? Maybe in log?

#311477
Oct 26, 2023 6:02
Vote:
 

Here is the stack trace:

Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'connectionString')
   at Microsoft.EntityFrameworkCore.Utilities.Check.NotEmpty(String value, String parameterName)
   at Microsoft.EntityFrameworkCore.SqlServerDbContextOptionsExtensions.UseSqlServer(DbContextOptionsBuilder optionsBuilder, String connectionString, Action`1 sqlServerOptionsAction)
   at Microsoft.Extensions.DependencyInjection.OpenIDConnectServiceCollectionExtensions.<>c__DisplayClass1_0`1.<AddOpenIDConnect>b__1(IServiceProvider sp, DbContextOptionsBuilder options)
   at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.CreateDbContextOptions[TContext](IServiceProvider applicationServiceProvider, Action`2 optionsAction)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at OpenIddict.EntityFrameworkCore.OpenIddictEntityFrameworkCoreApplicationStoreResolver.Get[TApplication]()
   at OpenIddict.Core.OpenIddictApplicationCache`1..ctor(IOptionsMonitor`1 options, IOpenIddictApplicationStoreResolver resolver)
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Span`1& arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.OpenIddictCoreExtensions.<>c.<AddCore>b__0_0(IServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at EPiServer.OpenIDConnect.Internal.OpenIDConnectApplicationHostedService.RegisterApplication(IServiceScope scope)
   at EPiServer.OpenIDConnect.Internal.OpenIDConnectApplicationHostedService.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at CommerceSite.Program.Main(String[] args) in C:\Projects\CommerceSite\commerce-site\CommerceDigital\Program.cs:line 11
#311481
Edited, Oct 26, 2023 7:18
Vote:
 

We have OpenIdConnect configured with Azure AD. Could it be that I need to create this user in Azure AD instead of in Startup? Is it this that I've mis-understood perhaps?

#311493
Oct 26, 2023 9:33
Vote:
 

I've removed the code above and in the Startup class I have included AddOpenIDConnectUI to be able to add the application manually within the backoffice.

BUT, when I add the credentials and click "Save", nothing happens. The user I am logged in with has the "WebAdmins" role, so permissions should not be an issue.

Interestingly I get the following error when clicking "Save":

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.InvalidOperationException: Unable to resolve service for type 'OpenIddict.Abstractions.IOpenIddictApplicationManager' while attempting to activate 'EPiServer.OpenIDConnect.UI.Applications.Internal.ApplicationsController'.
         at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
         at lambda_method3425(Closure , IServiceProvider , Object[] )
         at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.<>c__DisplayClass7_0.<CreateActivator>b__0(ControllerContext controllerContext)
         at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass6_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddleware.Invoke(HttpContext httpContext, Boolean retry)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
#311498
Oct 26, 2023 12:01
Vote:
 

Good Morning,

So I now made one small step forward. In the Startup, I have updated the following call:

services.AddServiceApiAuthorization(OpenIdConnectOptionDefaults.AuthenticationScheme);

So that it now reads:

services.AddServiceApiAuthorization(OpenIdConnectDefaults.AuthenticationScheme);

And now I get the following error when querying a ServiceApi endpoint:

{
    "message": "User is not authorized for this request."
}

Of course I made the query using an access token from our Azure AD.

So now is the question, what am I missing? The error in my previous message still exists when trying to add an Application (which I believe is likely the missing link here).

/Stephen

#311558
Oct 27, 2023 7:22
Vote:
 

what user was used to get the access token?

#311560
Oct 27, 2023 8:12
Vote:
 

We are using the client_credentials flow, so we aquire a token using the clientId and clientSecret

#311561
Oct 27, 2023 8:20
Vote:
 

For the next person who encounters this problem. We managed to solve this by ensuring that we have the following setup in out Startup class.

Its worth poiniting out that our OpenId configuration uses idToken ResponseType which differs from the standard/Foundation configuration of OpenId.

The following steps were crucial in getting it to work for us. And by "working" I mean that the application would start up and we could assign the SiteUser created for ServiceApi ReadAccess and WriteAccess.

  1. .AddCmsAspNetIdentity<SiteUser>() for setting the Commerce db connectionString.
    I'm still not 100% sure why we need this, but without it we simply end up with a connectionString cannot be null exception.
  2. .AddAuthentication AFTER .AddCmsAspNetIdentity to use OpenId authentication
  3. .AddOpenIdConnect<SiteUser>(...) and .AddOpenIdConnectUI() for ServiceApi at the very end of the Startup.

Hopefully this will help someone else with the same problem!

services.AddCmsAspNetIdentity<SiteUser>(o =>
{
    if (string.IsNullOrEmpty(o.ConnectionStringOptions?.ConnectionString))
    {
        o.ConnectionStringOptions = new ConnectionStringOptions
        {
            Name = "EcfSqlConnection",
            ConnectionString = _configuration.GetConnectionString("EcfSqlConnection")
        };
    }
});

var azureSection = _configuration.GetSection("AzureAD");

services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = "azure-cookie";
        options.DefaultChallengeScheme = "azure";
    })
    .AddCookie("azure-cookie", options =>
    {
        options.Events.OnSignedIn = async ctx =>
        {
            if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
            {
                // Syncs user and roles so they are available to the CMS
                var synchronizingUserService = ctx
                    .HttpContext
                    .RequestServices
                    .GetRequiredService<ISynchronizingUserService>();

                await synchronizingUserService.SynchronizeAsync(claimsIdentity);
            }
        };
    })
    .AddOpenIdConnect("azure", options =>
    {
        azureSection.Bind(options);

        options.SignInScheme = "azure-cookie";
        options.SignOutScheme = "azure-cookie";
        options.ResponseType = OpenIdConnectResponseType.IdToken;
        options.UsePkce = true;

        options.Scope.Clear();
        options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
        options.Scope.Add(OpenIdConnectScope.OfflineAccess);
        options.Scope.Add(OpenIdConnectScope.Email);
        options.MapInboundClaims = false;

        options.TokenValidationParameters = new TokenValidationParameters
        {
            RoleClaimType = ClaimConstants.Roles,
            NameClaimType = "preferred_username",
            ValidateIssuer = false
        };

        options.Events.OnRedirectToIdentityProvider = ctx =>
        {
            // Prevent redirect loop
            if (ctx.Response.StatusCode == 401) ctx.HandleResponse();
            return Task.CompletedTask;
        };

        options.Events.OnAuthenticationFailed = context =>
        {
            context.HandleResponse();
            context.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(context.Exception.Message));
            return Task.CompletedTask;
        };
    });

services.AddCommerce()
    .AddFind()
    .AddMvc();

services.AddMoreServices();
// such as session
services.AddSession();

// etc., etc.
services.ConfigureTinyMce()
    .ConfigureKestrel();

var serviceApiSection = _configuration.GetSection("ServiceApi");

services.AddOpenIDConnect<SiteUser>(
    useDevelopmentCertificate: true,
    signingCertificate: null,
    encryptionCertificate: null,
    createSchema: true,
    options =>
    {
        var application = new OpenIDConnectApplication
        {
            Scopes = { ServiceApiOptionsDefaults.Scope }
        };

        serviceApiSection.Bind(application);
        
        options.Applications.Add(application);

        options.AllowResourceOwnerPasswordFlow = true;
        options.AllowAnonymousFlow = true;
    });

services.AddServiceApiAuthorization(OpenIDConnectOptionsDefaults.AuthenticationScheme);

services.AddOpenIDConnectUI();

If you are currious what we are binding, then our json config contains the following relevant settings (sensitive data removed):

"AzureAD": {
  "Authority": "",
  "ClientId": "",
  "CallbackPath": "/signin-oidc"
},
"ServiceApi": {
  "ClientId": "postman-client",
  "ClientSecret": "postman"
},
#312475
Nov 13, 2023 21:02
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.