AuthenticateResult.Succeeded é falso com Okta e Sustainsys.SAML2

Sep 11 2020

Eu tenho um aplicativo .Net Core 2 que aproveita o Sustainsys.Saml2.AspNetCor2 (2.7.0). O front end é um aplicativo Angular. A abordagem SAML que estou adotando é baseada e muito semelhante à abordagem adotada nesta implementação de referência:https://github.com/hmacat/Saml2WebAPIAndAngularSpaExample

* Tudo funciona bem com o IDP de teste (https://stubidp.sustainsys.com)

Mas quando tentamos integrar com o Okta, a propriedade AuthenticateResult.Succeeded no método de retorno de chamada (veja abaixo) é sempre falsa, mesmo que o SAML postado no terminal ASC pareça indicar uma autenticação bem-sucedida. Não estamos vendo nenhum erro. Simplesmente não está dando certo.

(Observe que minha empresa não tem acesso ao Okta - que é mantido por uma empresa parceira.)

Aqui está o código do servidor no controlador:

[AllowAnonymous]
    [HttpPost, HttpGet]
    [Route("api/Security/InitiateSamlSingleSignOn")]
    public IActionResult InitiateSamlSingleSignOn(string returnUrl)
    {
      return new ChallengeResult(
          Saml2Defaults.Scheme,
          new AuthenticationProperties
          {
            RedirectUri = Url.Action(nameof(SamlLoginCallback), new { returnUrl })
          });
    }

    [AllowAnonymous]
    [HttpPost, HttpGet]
    [Route("api/Security/SamlLoginCallback")]
    public async Task<IActionResult> SamlLoginCallback(string returnUrl)
    {
      var authenticateResult = await HttpContext.AuthenticateAsync(ApplicationSamlConstants.External);

      if (!authenticateResult.Succeeded)
      {
        return Unauthorized();
      }
  
     // more code below, never reached
  
   }

Aqui está uma captura de tela de alguns dos SAML enviados pela Okta, capturados usando a extensão do Chrome, SAML-tracer:

Não sei como investigar isso mais a fundo. Qualquer ajuda seria muito apreciada!

No método ConfigureServices, caso seja útil, tenho o seguinte (na parte relevante):

public void ConfigureServices(IServiceCollection services)
{
  // [snip]
  if (usingSAML)
  {
    services.Configure<CookiePolicyOptions>(options =>
    {
      // SameSiteMode.None is required to support SAML SSO.
      options.MinimumSameSitePolicy = SameSiteMode.None;

      options.CheckConsentNeeded = context => false;

      // Some older browsers don't support SameSiteMode.None.
      options.OnAppendCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
      options.OnDeleteCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    });
    
    authBuilder = services.AddAuthentication(o =>
    {
      o.DefaultScheme = ApplicationSamlConstants.Application;
      o.DefaultSignInScheme = ApplicationSamlConstants.External;
      o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
      o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    });

    authBuilder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
    {
      // see https://stackoverflow.com/questions/46243697/asp-net-core-persistent-authentication-custom-cookie-authentication
      options.ExpireTimeSpan = new System.TimeSpan(365, 0, 0, 0, 0);
      options.AccessDeniedPath = new PathString("/login");
      options.LoginPath = new PathString("/login");
    })
    .AddCookie(ApplicationSamlConstants.Application)
    .AddCookie(ApplicationSamlConstants.External)
    .AddSaml2(options =>
    {
      options.SPOptions.EntityId = new EntityId(this.Configuration["Saml:SPEntityId"]);
      options.IdentityProviders.Add(
          new IdentityProvider(
              new EntityId(this.Configuration["Saml:IDPEntityId"]), options.SPOptions)
          {
            MetadataLocation = this.Configuration["Saml:IDPMetaDataBaseUrl"],
            LoadMetadata = true,
          });
      options.SPOptions.ServiceCertificates.Add(new X509Certificate2(this.Configuration["Saml:CertificateFileName"]));
    });
  }
 // [snip]
}

ATUALIZAÇÃO: modifiquei o código para capturar mais informações de registro e o que descobri é que, no endpoint Saml2 / Acs, o usuário está sendo autenticado. Nos arquivos de log, vejo o seguinte:

2020-09-14 09:28:09.307 -05:00 [DBG] Signature validation passed for Saml Response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id
2020-09-14 09:28:09.369 -05:00 [DBG] Extracted SAML assertion id1622894416505593469999142
2020-09-14 09:28:09.385 -05:00 [INF] Successfully processed SAML response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id and authenticated [email protected]

No entanto, quando chego ao método SamlLoginCallback, essas informações de autenticação não estão presentes no AuthenticateResult obtido por esta chamada:

 var authenticateResult = await HttpContext.AuthenticateAsync(ApplicationSamlConstants.External);

Minhas informações de registro personalizadas para o objeto de resultado de autenticação são assim:

2020-09-14 09:28:09.432 -05:00 [ERR] SAML Authentication Failure: authenticateResult.Failure (Exception object) is null; 
No information was returned for the authentication scheme; 
authenticateResult.Principal is null; 
authenticateResult.Properties is null.
authenticateResult.Ticket is null.

O que poderia dar errado?

Respostas

JRS Sep 14 2020 at 18:55

A causa raiz aqui foi, em última análise, o resultado das diferenças no caso do Url usado pelo Okta em relação ao nosso código na lógica de redirecionamento. Os URLs correspondem, mas o caso não. Isso tornava os cookies ilegíveis por métodos invocados posteriormente que estavam sendo enviados para uma URL diferente, embora a diferença estivesse apenas na caixa do caminho. Depois de nos certificarmos de que todos os caminhos correspondiam exatamente, até a caixa, ele começou a funcionar.