Arturio
Arturio

Reputation: 466

Not able to SignOut using Saml2 from Sustainsys

This should be redirecting my app to my AdFs signOut Page, and then redirect me back to my app. However, it simply redirects me to my route "/logout". Watching the log on my ADFS server nothing happens.

        [AllowAnonymous]
        [HttpGet]
        [Route("api/logout")]
        public async Task<IActionResult> Logout()
        {
            return SignOut(new AuthenticationProperties()
            {
                RedirectUri = "/logout"
            },
            Saml2Defaults.Scheme);
        }

SignIn works fine. I even tried this same approach, but does not work. Here, the ReturnUrl method gets the location from HttpContext.Response.Header. When I try this for the logout, the location is always null.

        [AllowAnonymous]
        [HttpGet]
        [Route("api/login")]
        public async Task<string> LoginAdfs()
        {

            string redirectUri =  _appSettings.Saml.SpEntityId;

            await HttpContext.ChallengeAsync(new AuthenticationProperties
            {
                RedirectUri = string.Concat(redirectUri, "/autenticado")
            });
            return ReturnUrl();
        }

Any idea what could be happening?

UPDATE 21/11/2019

Turns out the Saml2Handler is simply not trying to send the request to the server. I'm getting these messages on my output window:

Sustainsys.Saml2.AspNetCore2.Saml2Handler: Debug: Initiating logout, checking requirements for federated logout
  Issuer of LogoutNameIdentifier claim (should be Idp entity id): 
  Issuer is a known Idp: False
  Session index claim (should have a value): 
  Idp has SingleLogoutServiceUrl: 
  There is a signingCertificate in SPOptions: True
  Idp configured to DisableOutboundLogoutRequests (should be false): 
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Information: Federated logout not possible, redirecting to post-logout

Here is my StartUp Configuration, I don't get what is wrong here:

            ServiceCertificate se = new ServiceCertificate()
            {
                Certificate = new X509Certificate2(SpCert, "",X509KeyStorageFlags.MachineKeySet),
                Use = CertificateUse.Signing
            };

            SPOptions sp = new SPOptions
            {
                AuthenticateRequestSigningBehavior = SigningBehavior.Never,
                EntityId = new EntityId(SpEntityId),
                ReturnUrl = new Uri("/login"),
                NameIdPolicy = new Sustainsys.Saml2.Saml2P.Saml2NameIdPolicy(null, Sustainsys.Saml2.Saml2P.NameIdFormat.Unspecified),

            };
            sp.ServiceCertificates.Add(se);

            IdentityProvider idp = new IdentityProvider(new EntityId(appSettings.Saml.EntityId), sp);
            idp.Binding = Saml2BindingType.HttpPost;
            idp.AllowUnsolicitedAuthnResponse = true;
            //idp.WantAuthnRequestsSigned = true;
            idp.SingleSignOnServiceUrl = new Uri("/login");
            //idp.LoadMetadata = true;
            idp.SigningKeys.AddConfiguredKey(new X509Certificate2(IdpCert));
            idp.MetadataLocation = theMetadata;
            idp.DisableOutboundLogoutRequests = true;

Upvotes: 2

Views: 5389

Answers (4)

Peter Drabik
Peter Drabik

Reputation: 21

After strugling with this for quite some time, here is the code implementing the logout functionality as an extension to the Saml2WebAPIAndAngularSpaExample (see https://github.com/hmacat/Saml2WebAPIAndAngularSpaExample).

Startup.cs

builder.Services.AddAuthentication(o =>
{
    o.DefaultScheme = ApplicationSamlConstants.Application;
    o.DefaultSignInScheme = ApplicationSamlConstants.External;
    o.DefaultAuthenticateScheme = ApplicationSamlConstants.Application; //needed for logout
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ClockSkew = TimeSpan.FromMinutes(Convert.ToDouble(builder.Configuration["Jwt:ExpireInMinutes"])),
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
    };
})
.AddCookie(ApplicationSamlConstants.Application)
.AddCookie(ApplicationSamlConstants.External)
.AddSaml2(options =>
{
    options.SPOptions.EntityId = new EntityId(builder.Configuration["Saml:SPEntityId"]);
    var idp = new IdentityProvider(new EntityId(builder.Configuration["Saml:IDPEntityId"]), options.SPOptions)
    {
        Binding = Saml2BindingType.HttpPost,
        SingleSignOnServiceUrl = new Uri(builder.Configuration["Saml:IDPSingleSignOnServiceUrl"]),
        SingleLogoutServiceBinding = Saml2BindingType.HttpRedirect,
        SingleLogoutServiceUrl = new Uri(builder.Configuration["Saml:IDPSingleLogoutServiceUrl"]),
        AllowUnsolicitedAuthnResponse = true,
        DisableOutboundLogoutRequests = false,
        //LoadMetadata = false
    };
    idp.SigningKeys.AddConfiguredKey(new X509Certificate2(builder.Configuration["Saml:IDPCertificateFileName"]));
    options.IdentityProviders.Add(idp);
    options.SPOptions.ServiceCertificates.Add(new X509Certificate2(builder.Configuration["Saml:SPCertificateFileName"]));
});

  • DefaultAuthenticateScheme needs to be set to the ApplicationSamlConstants.Application scheme in order for the logout to work. The Authorize attribute in the controller needs selecting the scheme JwtBearerDefaults.AuthenticationScheme explicitly - see below.
  • Sustainsys.Saml2 parameters SingleLogoutServiceBinding, SingleLogoutServiceUrl and DisableOutboundLogoutRequests need to be set.

SamlController.cs

[AllowAnonymous]
[HttpGet("InitiateSingleSignOn")]
public IActionResult InitiateSingleSignOn(string returnUrl)
{
    // challenge the user to sign in using SAML
    return new ChallengeResult(
        Saml2Defaults.Scheme,
        new AuthenticationProperties
        {
            RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl })
        });
}

[AllowAnonymous]
[HttpGet("Callback")]
public async Task<IActionResult> LoginCallback(string returnUrl)
{
    // authenticate and sign in
    // (see https://stackoverflow.com/questions/53654020/how-to-implement-google-login-in-net-core-without-an-entityframework-provider for details)
    var authenticateResult = await HttpContext.AuthenticateAsync(ApplicationSamlConstants.External);

    if (!authenticateResult.Succeeded)
    {
        return Unauthorized();
    }

    var claimsIdentity = new ClaimsIdentity(ApplicationSamlConstants.Application);

    claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
    claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst("http://Sustainsys.se/Saml2/LogoutNameIdentifier"));
    claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst("http://Sustainsys.se/Saml2/SessionIndex"));

    await HttpContext.SignInAsync(
        ApplicationSamlConstants.Application,
        new ClaimsPrincipal(claimsIdentity));


    // issue JWT token
    var token = this.CreateJwtSecurityToken(authenticateResult);
    HttpContext.Session.SetString("JWT", new JwtSecurityTokenHandler().WriteToken(token));


    // redirect to the page that caused our first challenge
    if (!string.IsNullOrEmpty(returnUrl))
    {
        return Redirect(returnUrl);
    }

    return this.Ok();
}


[AllowAnonymous]
[HttpGet("InitiateSingleLogout")]
public IActionResult InitiateSingleLogout(string returnUrl)
{
    HttpContext.Session.SetString("JWT", "");

    return SignOut(
        new AuthenticationProperties()
        {
            RedirectUri = Url.Action(nameof(LogoutCallback), new { returnUrl })
        },
        ApplicationSamlConstants.External,
        ApplicationSamlConstants.Application,
        Saml2Defaults.Scheme
    );
}

[AllowAnonymous]
[HttpGet("LogoutCallback")]
public IActionResult LogoutCallback(string returnUrl)
{
    if (!string.IsNullOrEmpty(returnUrl))
    {
        return Redirect(returnUrl);
    }

    return this.Ok();
}

AuthorizationController.cs

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("TestAuthorization")]
public ActionResult TestAuthorization()
{
    return this.Ok("Congratulations, you are authorized.");
}

Upvotes: 0

Cameron Tinker
Cameron Tinker

Reputation: 9789

From my own experience, the two claims, as mentioned by Anders Abel, should be present on the user. I had not seen these claims until I passed all of the claims along with the sign-in request. ASP.NET Core recreates the principal on SignInAsync and needs claims to be passed in with the request.

With the following, I am able to fulfill a SingleLogout with my service:

await HttpContext.SignInAsync(user.SubjectId, user.Username, props, user.Claims.ToArray());

Upvotes: 1

Anders Abel
Anders Abel

Reputation: 69280

For the logout to work, two special claims "LogoutNameIdentifier" and "SessionIndex" (full names are http://Sustainsys.se/Saml2/LogoutNameIdentifier and http://Sustainsys.se/Saml2/SessionIndex need to be present on the user. Those carries information about the current session that the Saml2 library needs to be able to do a logout.

Now I don't see your entire Startup, so I cannot understand your application's flow. But those claims should be present in the identity returned by the library - possibly stored in an External cookie (if you are using asp.net identity). When your application then sets the application cookie those two claims must be carried over to the session identity.

Also you have actually disabled outbound logout with DisableOutboundLogoutRequests. But that's not the main problem here as your logs indicates that the required claims are not present.

Upvotes: 5

Nandhu Nandini
Nandhu Nandini

Reputation: 19

what you are using as a service provider.

Upvotes: -2

Related Questions