我们有以下环境。
客户端在IdentityServer上的配置。
new Client
{
ClientName = "App1",
ClientId = "App1",
AllowedGrantTypes = GrantTypes.Hybrid,
RequireConsent = false,
RedirectUris = "https://localhost:5000/siging-oidc",
PostLogoutRedirectUris = "https://localhost:5000/home/index",
FrontChannelLogoutUri = "https://localhost:5000/home/frontchannellogout",
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
ClientSecrets =
{
new Secret("secretApp1".Sha256())
}
},
new Client
{
ClientName = "App2",
ClientId = "App2",
AllowedGrantTypes = GrantTypes.Hybrid,
RequireConsent = false,
RedirectUris = "https://localhost:5001/siging-oidc",
PostLogoutRedirectUris = "https://localhost:5000/home/index",
FrontChannelLogoutUri = "https://localhost:5001/home/frontchannellogout",
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
ClientSecrets =
{
new Secret("secretApp2".Sha256())
}
}
客户端的配置
//For authentication App1
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:44329/";
options.ClientId = "App1";
options.ResponseType = "code id_token";
options.Scope.Add(OidcConstants.StandardScopes.OpenId);
options.Scope.Add(OidcConstants.StandardScopes.Profile);
options.SaveTokens = true;
options.ClientSecret = "secretApp1";
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.GivenName
};
options.ClaimActions.Remove("acr");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
n.ProtocolMessage.AcrValues = $"tenant:{configuration["TenantId"]}";
}
return Task.FromResult(0);
},
OnRemoteFailure = context =>
{
context.Response.Redirect("/home");
context.HandleResponse();
return Task.FromResult(0);
}
};
});
//For authentication App2
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:44329/";
options.ClientId = "App2";
options.ResponseType = "code id_token";
options.Scope.Add(OidcConstants.StandardScopes.OpenId);
options.Scope.Add(OidcConstants.StandardScopes.Profile);
options.SaveTokens = true;
options.ClientSecret = "secretApp2";
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.GivenName
};
options.ClaimActions.Remove("acr");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
n.ProtocolMessage.AcrValues = $"tenant:{configuration["TenantId"]}";
}
return Task.FromResult(0);
},
OnRemoteFailure = context =>
{
context.Response.Redirect("/home");
context.HandleResponse();
return Task.FromResult(0);
}
};
});
这些是两个应用程序上的Logout实现。
// Logout App1
public async Task<IActionResult> Logout()
{
if (User.Identity.IsAuthenticated)
{
var idToken = await HttpContext.GetTokenAsync("id_token");
HttpContext.Session.Clear();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var disco = await _discoveryCache.GetAsync();
var postLogoutUrl = "https://localhost:5001/home/index";// I want to go to App2 when I logout from App1
var urlEndSession = new RequestUrl(disco.EndSessionEndpoint).CreateEndSessionUrl(idToken, postLogoutUrl);
return Redirect(urlEndSession);
}
return View("Index");
}
// Logout App2
public async Task<IActionResult> Logout()
{
if (User.Identity.IsAuthenticated)
{
var idToken = await HttpContext.GetTokenAsync("id_token");
HttpContext.Session.Clear();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var disco = await _discoveryCache.GetAsync();
var postLogoutUrl = "https://localhost:5000/home/index"; // I want to go to App1 when I logout from App2
var urlEndSession = new RequestUrl(disco.EndSessionEndpoint).CreateEndSessionUrl(idToken, postLogoutUrl);
return Redirect(urlEndSession);
}
return View("Index");
}
这些是两个应用上的FrontChannelLogout实现。
[Authorize]
public async Task<IActionResult> Frontchannellogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid")?.Value ?? "";
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
HttpContext.Session.Clear();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
return NoContent();
}
我们正在努力实现,当用户从一个App中签出时,实际上是通过前台规范从整个系统中签出。
一旦你在App1中登录,然后移动到App2中,我们就有了SSO,用户已经在两个App中登录了。一旦用户决定从当前应用中注销,他也应该从其他应用和身份服务器中注销。
我注销的应用(App1)按规定删除了cookie,identityServer的cookie也被删除了,但另一个应用(App2)没有删除cookie,也没有点击前台注销,所以我仍然在第二个应用(App2)中登录。
我知道最近Google推出的与SameSite cookie选项有关的改变,我也更新了与最近回滚这一改变有关的信息,即使我不是100%确定那是我的问题的原因,我想它可能与此有关。 正因为如此,我已经尝试了一些我在网上找到的解决方法,但到目前为止我还没有成功。
我用了这个 ASP.NET和ASP.NET Core中即将到来的SameSite Cookie变更 作为解决我的问题的指南。这是我在StartUp.cs App1和App2类上的代码。
private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
if (options.SameSite == SameSiteMode.None)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
if (userAgent.Contains("Chrome/8") || userAgent.Contains("Firefox/7"))
{
options.SameSite = (SameSiteMode)(-1);
}
}
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = (SameSiteMode)(-1);
options.OnAppendCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});
// ... some configurations more like services.AddAuthentication(...) ...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ...
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
编辑:浏览器cookie存储
1-) 登录到App1
2-) 登录到App2
3-) 从App2中点击注销后的第一步
4-) identityserver重定向到App1后,因为postlogoutredirecturi在App2中是App1。
我得到了一些解决方案。
在IdentityServer内部的AccountControllerLogout Action中,我返回了一个重定向到postlogoutredirecturi,就像这样。
public class AccountController : Controller
{
//... code before this action
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated == true)
{
await HttpContext.SignOutAsync();
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
if (vm.TriggerExternalSignout)
{
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
// this line was here before I got the problem and I chenged later.
return Redirect(vm.PostLogoutRedirectUri);
}
//... code after this action
}
所以,我只是修改了最后一行,返回LoggedOut视图,其中一个iframe是由LogoutInputModel vm的SignOutIframeUrl属性渲染的。
public class AccountController : Controller
{
//... code before this action
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated == true)
{
await HttpContext.SignOutAsync();
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
if (vm.TriggerExternalSignout)
{
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
// this line is the one solve my problem.
return View("LoggedOut", vm);
}
//... code after this action
}
这是LoggedOut视图的代码。
@model LoggedOutViewModel
@{
// set this so the layout rendering sees an anonymous user
ViewData["signed-out"] = true;
}
<div id="main-panel" class="mt-5">
<div class="page-header logged-out text-center">
<div id="branding-area">
<img src="~/some_image.svg" alt="gov-easy" class="logo" />
</div>
<p class="display-4">You are now being logged out...</p>
<p>If you are not automatically redirected, click the button below.</p>
<button class="btn btn-primary mx-auto" id="returnHomeBtn">Return home page</button>
@if (Model.PostLogoutRedirectUri != null)
{
<div class="hidden">
Click <a class="PostLogoutRedirectUri" href="@Model.PostLogoutRedirectUri">here</a> to return to the
<span>@Model.client</span> application.
</div>
}
@if (Model.SignOutIframeUrl != null)
{
<iframe width="0" height="0" class="signout hidden" src="@Model.SignOutIframeUrl"></iframe>
}
</div>
</div>
@section scripts
{
@if (Model.AutomaticRedirectAfterSignOut)
{
<script src="~/js/signout-redirect.js"></script>
}
}
这是~jssignout-redirect.js里的javascript代码:
window.addEventListener("load", function () {
var a = document.querySelector("a.PostLogoutRedirectUri");
if (a) {
window.location = a.href;
}
});
$('#returnHomeBtn').on('click', function (e) {
$('.PostLogoutRedirectUri').click();
});