4

I have the following:

  • Okta identity management w/ custom auth server
  • .net core web application
  • .net core web api application
  • Nopcommerce 4.2 web application

All are running in Azure app services. The web app and api app that I built are working great together. The web app passes the auth bearer token to the API, and the API validates it using the Aspnetcore.Okta middleware.

However, I now need to bring my Nopcommerce app into the fold. I have spent 3 days trying to build a plugin - even one that does basically nothing, and have had nothing but trouble. I used their "official" template, and it has its own problems. So I decided to just take the Facebook external auth plugin and start editing it. At least now I have something I can actually work with.

Here is where I am really stuck... can I just stick in the Okta middleware and be done with it? By that I mean the following steps:

  1. Add Login action controller to override default Nopcommerce login action
  2. If user is not authentictad, issue Challenge
  3. Middleware should pick that up and redirect to the login page (in my existing web app)
  4. User logs in there
  5. Middleware sets the session cookie and redirects user back to Nopcomm app
  6. Now back in Nopcomm, the middleware once again kicks in, sees the cookie, calls the /verify endpoint, and then populates a ClaimsPrincipal for the associated user.

My thought is that if the machine keys are the same on both apps, the cookie should work fine.

Do you believe that is the best approach, or should I instead follow the same steps in 1-4, except after 4, send the user back to the Nopcomm site with a token in the query string and then manually verify it on the Nopcomm side?

Buckles
  • 161
  • 1
  • 9

1 Answers1

4

I thought I would share the results of my tests thus far as it uncovered some things I really didn't expect. Maybe these things are obvious/known to most of you - but for me they definitely proved to be tricky.

The first important thing I learned is that you must be specific around the challenge types your app issues. If you are self-hosting a login page you need to use:

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })

If you are using the Okta (or other openId provider) login page, you need to issue a different type of challenge:

            builder.Services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })

What I learned is that if you're using OpenID Connect in a ASP.NET Core application, and you are self-hosting the login page, the flow kinda goes like this:

  1. User tries to access protected resource
  2. [Authorize] marker tells the Okta middleware to inject itself and do its thing. User.Identity.IsAuthenticated is false so it redirects to whatever is set under the cookie options as the login path:
            .AddCookie(options =>
            {
                options.LoginPath = new PathString("/Account/SignIn");
                options.Cookie.Name = ".AspNet.SharedCookie";
            })
  1. My login page is just using the Okta widget, so clicking login sends user to Okta, Okta does its thing, and then sends the user back to the app to the every mysterious "authorization-code/callback" url. That url is handled by the Okta middleware.

  2. The middleware processes the tokens being sent from Okta, calls the /userinfo endpoint to get the claims (presuming you have that property set to true) and populates a ClaimsPrincipal.

  3. Here is the part I missed What it also does is set a cookie(s). That cookie is what, from that point forward, the .net framework will be checking to determine who the user is and if they are authenticated. I completely misunderstood this. I thought that the middleware would be making calls to /authorize or something like that in the background.

So - the big lesson learned here for me is this: just because both apps are using the same OpenID server doesn't mean they can easily share auth/sessions. In order for true SSO to work seamlessly, they have to also share a cookie. And that brings me to my second discovery: sharing cookies across ASP.NET core applications. The short version is you have to:

a. Name the cookie the same in both apps b. Use the IDataProtection interface to store your cookie keys somewhere both apps can get to (I chose Azure). c. Make sure the cookie domain is the same for both apps

So at the end of the day, this is what my code looked like end-to-end (in each app's startup). I am still dealing with some oddities specific to Nopcommerce, but hopefully they will get sorted out here soon.

            var oktaMvcOptions = new OktaMvcOptions()
            {
                OktaDomain = oktaDomain,
                ClientId = clientId,
                ClientSecret = clientSecret,
                Scope = new List<string> { "openid", "profile", "email", "address", "groups" },
                AuthorizationServerId = authServerId,
                GetClaimsFromUserInfoEndpoint = true
            };

            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });


            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(storageConnectionString);
            CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
            CloudBlobContainer container = blobClient.GetContainerReference(storageContainerName);
            var blob = container.GetBlockBlobReference("somefilename.xml");

            services.AddDataProtection().SetApplicationName("somesharedappname").PersistKeysToAzureBlobStorage(blob);

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie(options =>
            {
                options.LoginPath = new PathString("/Account/SignIn");
                options.Cookie.Name = "somesharedcookiename";
            })
            .AddOktaMvc(oktaMvcOptions);

I hope this helps someone who ends up struggling with the same issues. If anyone can suggest a better mechanism for implementing this, I would love to hear your ideas and suggestions.

Buckles
  • 161
  • 1
  • 9