0

I've started working on a project which implements a microservice architecture and has a few .NET 6 backend microservices, one of which is an authentication service with IdentityServer 4.

The frontend application is a React app wrapped around a .NET 6 app which acts as a Middleware between the backend services and the React app (handles prerendering, routing, authentication handling etc.)

Now, an important thing to note before typing anything else is that the application works when deployed to a web server. There, the authentication application, as well as the frontend application are all hosted on the same domain and the authentication actually works as intended (or at least works).

The particular problem I have, is when trying to run all my microservices locally for development purposes. The frontend app works, the backend services work but I can't log in. Well, I do log in but my frontend application is unaware of it.

After successfully logging in locally, I think I am authenticated on the authentication services but my frontend application can't hit any endpoints on the backend sevice (I know this because when trying to retrieve the user profile I get back a 401).

After this introduction I'll share a few snippets of code. First, I'll post the setup on the backend app (the authentication microservice). The first snippet is how IdentityServer and Authentication is set up.

I think the whole problem boils down to Cookies because my AspNetCore.Cookies cookie is present on the deployed environment but locally it is not present after logging in. But I am not sure and don't know how to fix that.

IIdentityServerBuilder builder =
    services
        .AddIdentityServer(options => {
          options.Authentication.CookieLifetime =
              TimeSpan.FromMinutes(auth.ApplicationCookieLifetimeMinutes);

          options.Events.RaiseErrorEvents = true;
          options.Events.RaiseInformationEvents = true;
          options.Events.RaiseFailureEvents = true;
          options.Events.RaiseSuccessEvents = true;
        })
        //.AddInMemoryClients(DataIdentityConfigure.GetInstance().GetClients())
        .AddInMemoryIdentityResources(
            DataIdentityConfigure.GetInstance().GetIdentityResources())
        .AddInMemoryApiResources(
            DataIdentityConfigure.GetInstance().GetApiResources())
        .AddInMemoryApiScopes(
            DataIdentityConfigure.GetInstance().GetApiScopes())
        .AddAspNetIdentity<User>()
        .AddProfileService<ProfileService>()
        .AddConfigurationStore(options => {
          options.ConfigureDbContext = b =>
              b.UseSqlServer(connectionString,
                             sql => sql.MigrationsAssembly(migrationsAssembly));
        })
        .AddOperationalStore(options => {
          options.ConfigureDbContext = b =>
              b.UseSqlServer(connectionString,
                             sql => sql.MigrationsAssembly(migrationsAssembly));

          options.EnableTokenCleanup = true;
        });

services.AddAuthentication("Bearer")
    .AddIdentityServerAuthentication(options => {
      options.Authority = auth.Authority;
      options.RequireHttpsMetadata = auth.EnableHttps;
      options.ApiName = "apiname";
    })
    .AddFacebook(options => {
        // facebook auth
    })
    .AddGoogle(
        // google auth
});

This backend service, also serves the View containing the Login form:

[HttpGet("login")]
public async Task<IActionResult> GetLoginAsync(Uri returnUrl, string culture) {
  ExternalLoginViewModel vm = await BuildLoginViewModelAsync(returnUrl);

  if (User?.Identity.IsAuthenticated == true) {
    return await RedirectToClientAsync(vm.ReturnUrl);
  }

  // Add default language from web app request
  Response.Cookies.Append(
      CookieRequestCultureProvider.DefaultCookieName,
      CookieRequestCultureProvider.MakeCookieValue(
          new RequestCulture(culture ?? _setting.Language)),
      new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1),
                          Path = "/" });

  object error = TempData[ErrorMessage];

  if (error != null) {
    ModelState.AddModelError(ErrorMessage, error.ToString());
    TempData[ErrorMessage] = null;
  }

  if (vm.IsExternalLoginOnly) {
    return await ExternalLoginAsync(vm.ExternalLoginScheme, returnUrl);
  }

  if (Request.Cookies[FailedLoginMetadata] != null) {
    vm.Username = Request.Cookies[FailedLoginMetadata];
  }

  vm.AllowRememberLogin = true;

  return View("Login", vm);

And when a user inputs their username and password, this endpoint gets called to authenticate the user (the LogIn method actually logs the user in and returns a unique id of the user so that part works).

[HttpPost("login")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginAsync(LoginViewModel model, string button)
{
   TempData.Clear();

   if (Request.Cookies[FailedLoginMetadata] != null)
   {
      Response.Cookies.Delete(FailedLoginMetadata);
   }

   if (button != "login")
   {
       return await RedirectToClientAsync(model.ReturnUrl);
   }

   if (ModelState.IsValid)
   {
      try
      {
          Guid userId = await _authenticationService.LogIn(model);

          await _events.RaiseAsync(
                new UserLoginSuccessEvent(
                   model.Username,
                   userId.ToString(),
                   model.Username));

          Response.Cookies.Append(PersistentLogin, model.RememberLogin.ToString(),
                new CookieOptions
                {
                    Expires = DateTimeOffset.UtcNow.AddYears(1),
                    Path = "/"
                 });

          if (model.ReturnUrl != null)
          {
              var returnUrl = model.ReturnUrl.ToString();

              if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
              {
                   return Redirect(returnUrl);
              }
           }

              return Redirect("~/");
           }
           catch (Exception ex)
           {
              ...
           }
    }

           LoginViewModel vm = await BuildLoginViewModelAsync(model);
           return View("Login", vm);
}

Now the frontend part is a lot less understandable to me. So after logging in, my frontend application is actually the one tasked with storing the bearer token in the Cookies (at least I think so).

services
    .AddAuthentication(options => {
      options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme =
          OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
               options => { options.Cookie.Path = "/"; })
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => {
      options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

      options.Authority = auth.Authority;
      options.RequireHttpsMetadata = auth.EnableHttps;
      options.SaveTokens = true;

      options.NonceCookie.SameSite = (SameSiteMode)(-1);
      options.CorrelationCookie.SameSite = (SameSiteMode)(-1);

      options.ClientId = AuthSettings.ClientId;
      options.ClientSecret = auth.WebServerSecret;
      options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
      options.UsePkce = false;

      options.Scope.Add("scope"); // these are the backend microservices
      options.Scope.Add("scope");
      options.Scope.Add("scope");
      options.Scope.Add("scope");
      options.Scope.Add("scope");

      options.Events.OnAuthenticationFailed = context => {
        context.Response.Redirect(auth.FailureRedirectUrl);
        context.HandleResponse();

        return Task.CompletedTask;
      };

      options.Events.OnTicketReceived = context => {
        var PersistentLoginCookie = context.Request.Cookies["persistent_login"];
        if (!string.IsNullOrWhiteSpace(PersistentLoginCookie) &&
            PersistentLoginCookie == "True") {
          context.Properties.IsPersistent = true;
        }

        return Task.CompletedTask;
      };
    });

I couldn't find much on Google because I feel like the implementation is very specific and possibly even plain wrong but I have to make it work somehow because rewriting everything is not an option currently.

I've tried changing from http to https in local development, changing the domain from localhost to a named domain, changing the host, the authority, changing CORS settings, running it in different browsers, trying in IIS Express locally and so far nothing worked. I am unable to run the entire website locally currently (with authentication).

wuggs
  • 5
  • 2
  • you should set the samesite to the desired value, as browers might reject the cookies ohtherwise. probably string, Lax or None, depending on the cookie type. – Tore Nestenius Apr 18 '23 at 06:38

0 Answers0