Reputation: 466
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
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();
}
SignInAsync
logic in LoginCallback
is crucial for the logout to work. The two specific claims need to be present in the ClaimPrincipal
(see https://stackoverflow.com/a/59107708/5956120). Then sign in the ClaimPrincipal
using the ApplicationSamlConstants.Application
scheme.AuthorizationController.cs
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("TestAuthorization")]
public ActionResult TestAuthorization()
{
return this.Ok("Congratulations, you are authorized.");
}
[Authorize]
attribute, the scheme needs to be explicitly selected. For details see: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0.Upvotes: 0
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
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