I’m have an issue with IdentityServer Front Channel Logout when deploying to Azure App Service. I have three applications (Idp and two SP’s) that I have configured to use Front Channel Logout as follows:
IdP Client Configurations:
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 Logout Actions:
/// <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();
}
In testing on localhost running on different ports the FrontChannelLogout action is called in both SP’s and both are signed out of IdP. When code is deployed to Azure App Services, only the SP that initiated signout is signed out. The second SP still remains signed in to IdP.
I was thinking this was something to do with Content Security Policy but still get same result after configuring CSP as follows:
IdP Content Security Policy:
csp += "frame-ancestors 'self' https://cdcavell.name https://as-ui-cdcavell.azurewebsites.net; ";
csp += "frame-src 'self'; ";
SP Content Security Policy:
csp += "frame-ancestors 'self'; ";
csp += "frame-src 'self' https://dis5-cdcavell.azurewebsites.net https://www.google.com; ";
Wanting to know if anyone has experienced this or if it might be something to to with Azue App Service configuration?
Full source at: https://github.com/cdcavell/cdcavell.name
Website at: https://cdcavell.name
Update:
I have tried several things such as adding the fix for samesite = none
issue
// 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;
});
as well as disabling the Arr Session Affinity cookie
<httpProtocol>
<customHeaders>
<add name="Arr-Disable-Session-Affinity" value="true"/>
</customHeaders>
</httpProtocol>
neither of these worked so I’m abandoning FrontChanelLogout and will try and implement BackChanelLogout utilizing Redis Cache as outlined by damienbod’s article
2
Answers
Issue resolved:
It was due to mixed domains (cdcavell.name and azurewebsites.net). One app service was under cdcavell.name and the other two were under azurewebsites.net.
Setup custom domains in Azure App Service and then added a wild card SSL binding.
When you call signout, you should not return your own result or view back.
Instead the action method should look like this:
This is because SignOutAsync creates its own response and when you return a response you overwrite this internal response.