部署到 Azure App Service 时 IdentityServer 前通道注销问题
Issue with IdentityServer Front Channel Logout when deployed to Azure App Service
我在部署到 Azure 应用服务时遇到 IdentityServer 前通道注销问题。我已将三个应用程序(Idp 和两个 SP)配置为使用 Front Channel Logout,如下所示:
IdP 客户端配置:
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "Authorization.Service.UI.DEV",
ClientName = "Authorization Service UI [Development]",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowOfflineAccess = true,
// where to redirect to after login
RedirectUris = new List<string>
{
"https://localhost:44305/signin-oidc",
},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44305/signout-callback-oidc",
},
FrontChannelLogoutUri = "https://localhost:44305/Account/FrontChannelLogout",
FrontChannelLogoutSessionRequired = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
"Authorization.Service.API.Read",
"Authorization.Service.API.Write"
},
AccessTokenLifetime = Convert.ToInt32((new TimeSpan(1,0,0,0)).TotalSeconds)
},
new Client
{
ClientId = "Authorization.Service.UI",
ClientName = "Authorization Service UI",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowOfflineAccess = true,
// where to redirect to after login
RedirectUris = new List<string>
{
"https://as-ui-cdcavell.azurewebsites.net/signin-oidc"
},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>
{
"https://as-ui-cdcavell.azurewebsites.net/signout-callback-oidc"
},
FrontChannelLogoutUri = "https://as-ui-cdcavell.azurewebsites.net/Account/FrontChannelLogout",
FrontChannelLogoutSessionRequired = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
"Authorization.Service.API.Read",
"Authorization.Service.API.Write"
},
AccessTokenLifetime = Convert.ToInt32((new TimeSpan(1,0,0,0)).TotalSeconds)
},
new Client
{
ClientId = "cdcavell.name.DEV",
ClientName = "Personal Website of Christopher D. Cavell [Development]",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowOfflineAccess = true,
// where to redirect to after login
RedirectUris = new List<string>
{
"https://localhost:44349/signin-oidc",
},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44349/signout-callback-oidc",
},
FrontChannelLogoutSessionRequired = true,
FrontChannelLogoutUri = "https://localhost:44349/Account/FrontChannelLogout",
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
"Authorization.Service.API.Read"
},
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenLifetime = Convert.ToInt32((new TimeSpan(1,0,0,0)).TotalSeconds)
},
new Client
{
ClientId = "cdcavell.name",
ClientName = "Personal Website of Christopher D. Cavell",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowOfflineAccess = true,
// where to redirect to after login
RedirectUris = new List<string>
{
"https://cdcavell.name/signin-oidc"
},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>
{
"https://cdcavell.name/signout-callback-oidc"
},
FrontChannelLogoutSessionRequired = true,
FrontChannelLogoutUri = "https://cdcavell.name/Account/FrontChannelLogout",
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
"Authorization.Service.API.Read"
},
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenLifetime = Convert.ToInt32((new TimeSpan(1,0,0,0)).TotalSeconds)
}
};
SP 注销操作:
/// <summary>
/// Logout method
/// </summary>
/// <returns>Task<IActionResult></returns>
/// <method>Logout()</method>
[AllowAnonymous]
[HttpGet]
public async Task<IActionResult> Logout()
{
if (User.Identity.IsAuthenticated)
{
// Remove Authorization record
Data.Authorization authorization = Data.Authorization.GetRecord(User.Claims, _dbContext);
authorization.Delete(_dbContext);
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
SignOut(CookieAuthenticationDefaults.AuthenticationScheme, "oidc");
DiscoveryCache discoveryCache = (DiscoveryCache)HttpContext
.RequestServices.GetService(typeof(IDiscoveryCache));
DiscoveryDocumentResponse discovery = discoveryCache.GetAsync().Result;
if (!discovery.IsError)
return Redirect(discovery.EndSessionEndpoint);
}
return RedirectToAction("Index", "Home");
}
/// <summary>
/// Front Channel SLO Logout method
/// <br /><br />
/// https://andersonnjen.com/2019/03/22/identityserver4-global-logout/
/// </summary>
/// <returns>Task<IActionResult></returns>
/// <method>FrontChannelLogout(string sid)</method>
[AllowAnonymous]
[HttpGet]
public async Task<IActionResult> FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid")?.Value ?? "";
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
return NoContent();
}
在本地主机 运行 的不同端口上进行测试时,两个 SP 都调用了 FrontChannelLogout 操作,并且都已退出 IdP。当代码部署到 Azure App Services 时,只有发起注销的 SP 被注销。第二个 SP 仍然保持登录到 IdP。
我认为这与内容安全策略有关,但按如下方式配置 CSP 后仍然得到相同的结果:
IdP 内容安全策略:
csp += "frame-ancestors 'self' https://cdcavell.name https://as-ui-cdcavell.azurewebsites.net; ";
csp += "frame-src 'self'; ";
SP 内容安全策略:
csp += "frame-ancestors 'self'; ";
csp += "frame-src 'self' https://dis5-cdcavell.azurewebsites.net https://www.google.com; ";
想知道是否有人遇到过这种情况,或者它是否与 Azue App Service 配置有关?
完整来源:https://github.com/cdcavell/cdcavell.name
更新:
我已经尝试了几种方法,例如添加 samesite = none
问题的修复程序
// Override the CookieAuthenticationOptions for DefaultCookieAuthenticationScheme
// https://github.com/IdentityServer/IdentityServer4/blob/c30de032ec1dedc3b17dfa342043850638e84b43/src/IdentityServer4/src/Configuration/DependencyInjection/ConfigureInternalCookieOptions.cs#L28
services.Configure<CookieAuthenticationOptions>(IdentityServerConstants.DefaultCookieAuthenticationScheme, options =>
{
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.IsEssential = true;
});
以及禁用 Arr Session Affinity cookie
<httpProtocol>
<customHeaders>
<add name="Arr-Disable-Session-Affinity" value="true"/>
</customHeaders>
</httpProtocol>
这些都不起作用,所以我放弃了 FrontChanelLogout,并将尝试使用 Redis 缓存实现 BackChanelLogout,如 damienbod's article
所述
当您调用注销时,您不应该return您自己的结果或查看。
相反,操作方法应如下所示:
public async Task DoLogout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
这是因为 SignOutAsync 创建了自己的响应,当您 return 响应时,您会覆盖此内部响应。
问题已解决:
这是由于混合域(cdcavell.name 和 azurewebsites.net)。一项应用服务在 cdcavell.name 下,另外两个在 azurewebsites.net.
下
在 Azure 应用服务中设置自定义域,然后添加通配符 SSL 绑定。
我在部署到 Azure 应用服务时遇到 IdentityServer 前通道注销问题。我已将三个应用程序(Idp 和两个 SP)配置为使用 Front Channel Logout,如下所示:
IdP 客户端配置:
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "Authorization.Service.UI.DEV",
ClientName = "Authorization Service UI [Development]",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowOfflineAccess = true,
// where to redirect to after login
RedirectUris = new List<string>
{
"https://localhost:44305/signin-oidc",
},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44305/signout-callback-oidc",
},
FrontChannelLogoutUri = "https://localhost:44305/Account/FrontChannelLogout",
FrontChannelLogoutSessionRequired = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
"Authorization.Service.API.Read",
"Authorization.Service.API.Write"
},
AccessTokenLifetime = Convert.ToInt32((new TimeSpan(1,0,0,0)).TotalSeconds)
},
new Client
{
ClientId = "Authorization.Service.UI",
ClientName = "Authorization Service UI",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowOfflineAccess = true,
// where to redirect to after login
RedirectUris = new List<string>
{
"https://as-ui-cdcavell.azurewebsites.net/signin-oidc"
},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>
{
"https://as-ui-cdcavell.azurewebsites.net/signout-callback-oidc"
},
FrontChannelLogoutUri = "https://as-ui-cdcavell.azurewebsites.net/Account/FrontChannelLogout",
FrontChannelLogoutSessionRequired = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
"Authorization.Service.API.Read",
"Authorization.Service.API.Write"
},
AccessTokenLifetime = Convert.ToInt32((new TimeSpan(1,0,0,0)).TotalSeconds)
},
new Client
{
ClientId = "cdcavell.name.DEV",
ClientName = "Personal Website of Christopher D. Cavell [Development]",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowOfflineAccess = true,
// where to redirect to after login
RedirectUris = new List<string>
{
"https://localhost:44349/signin-oidc",
},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44349/signout-callback-oidc",
},
FrontChannelLogoutSessionRequired = true,
FrontChannelLogoutUri = "https://localhost:44349/Account/FrontChannelLogout",
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
"Authorization.Service.API.Read"
},
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenLifetime = Convert.ToInt32((new TimeSpan(1,0,0,0)).TotalSeconds)
},
new Client
{
ClientId = "cdcavell.name",
ClientName = "Personal Website of Christopher D. Cavell",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowOfflineAccess = true,
// where to redirect to after login
RedirectUris = new List<string>
{
"https://cdcavell.name/signin-oidc"
},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>
{
"https://cdcavell.name/signout-callback-oidc"
},
FrontChannelLogoutSessionRequired = true,
FrontChannelLogoutUri = "https://cdcavell.name/Account/FrontChannelLogout",
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
"Authorization.Service.API.Read"
},
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenLifetime = Convert.ToInt32((new TimeSpan(1,0,0,0)).TotalSeconds)
}
};
SP 注销操作:
/// <summary>
/// Logout method
/// </summary>
/// <returns>Task<IActionResult></returns>
/// <method>Logout()</method>
[AllowAnonymous]
[HttpGet]
public async Task<IActionResult> Logout()
{
if (User.Identity.IsAuthenticated)
{
// Remove Authorization record
Data.Authorization authorization = Data.Authorization.GetRecord(User.Claims, _dbContext);
authorization.Delete(_dbContext);
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
SignOut(CookieAuthenticationDefaults.AuthenticationScheme, "oidc");
DiscoveryCache discoveryCache = (DiscoveryCache)HttpContext
.RequestServices.GetService(typeof(IDiscoveryCache));
DiscoveryDocumentResponse discovery = discoveryCache.GetAsync().Result;
if (!discovery.IsError)
return Redirect(discovery.EndSessionEndpoint);
}
return RedirectToAction("Index", "Home");
}
/// <summary>
/// Front Channel SLO Logout method
/// <br /><br />
/// https://andersonnjen.com/2019/03/22/identityserver4-global-logout/
/// </summary>
/// <returns>Task<IActionResult></returns>
/// <method>FrontChannelLogout(string sid)</method>
[AllowAnonymous]
[HttpGet]
public async Task<IActionResult> FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid")?.Value ?? "";
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
return NoContent();
}
在本地主机 运行 的不同端口上进行测试时,两个 SP 都调用了 FrontChannelLogout 操作,并且都已退出 IdP。当代码部署到 Azure App Services 时,只有发起注销的 SP 被注销。第二个 SP 仍然保持登录到 IdP。
我认为这与内容安全策略有关,但按如下方式配置 CSP 后仍然得到相同的结果:
IdP 内容安全策略:
csp += "frame-ancestors 'self' https://cdcavell.name https://as-ui-cdcavell.azurewebsites.net; ";
csp += "frame-src 'self'; ";
SP 内容安全策略:
csp += "frame-ancestors 'self'; ";
csp += "frame-src 'self' https://dis5-cdcavell.azurewebsites.net https://www.google.com; ";
想知道是否有人遇到过这种情况,或者它是否与 Azue App Service 配置有关?
完整来源:https://github.com/cdcavell/cdcavell.name
更新:
我已经尝试了几种方法,例如添加 samesite = none
问题的修复程序
// Override the CookieAuthenticationOptions for DefaultCookieAuthenticationScheme
// https://github.com/IdentityServer/IdentityServer4/blob/c30de032ec1dedc3b17dfa342043850638e84b43/src/IdentityServer4/src/Configuration/DependencyInjection/ConfigureInternalCookieOptions.cs#L28
services.Configure<CookieAuthenticationOptions>(IdentityServerConstants.DefaultCookieAuthenticationScheme, options =>
{
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.IsEssential = true;
});
以及禁用 Arr Session Affinity cookie
<httpProtocol>
<customHeaders>
<add name="Arr-Disable-Session-Affinity" value="true"/>
</customHeaders>
</httpProtocol>
这些都不起作用,所以我放弃了 FrontChanelLogout,并将尝试使用 Redis 缓存实现 BackChanelLogout,如 damienbod's article
所述当您调用注销时,您不应该return您自己的结果或查看。
相反,操作方法应如下所示:
public async Task DoLogout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
这是因为 SignOutAsync 创建了自己的响应,当您 return 响应时,您会覆盖此内部响应。
问题已解决:
这是由于混合域(cdcavell.name 和 azurewebsites.net)。一项应用服务在 cdcavell.name 下,另外两个在 azurewebsites.net.
下在 Azure 应用服务中设置自定义域,然后添加通配符 SSL 绑定。