Azure Active Directory 与 WebForms 的集成在登录时获得无限循环
Azure Active Directory Integration with WebForms Getting Infinite Loop at Login
我已阅读并遵循 this article 使用我们的 AAD(Azure Active Directory)设置我的网站以获取 SSO(单点登录)。我已经让它在一个全新的网站上工作 localhost
以及当我将它发布到 Azure 时。
以下是工作版本应用程序注册的设置:
品牌:
首页URL:https://<worksgood>.azurewebsites.net
身份验证:
重定向 URI:
https://localhost:44390/
https://<worksgood>.azurewebsites.net/.auth/login/aad/callback
隐式授予:
ID 令牌:已检查
支持的账户类型
仅限此组织目录中的帐户(我的公司 - 单租户)
将应用程序视为 public 客户端
没有
而当我运行这里的申请是callback
请求。
如您所见,Response Header | Location
看起来不错(对我来说)
以下是我尝试将相同逻辑集成到的网站的应用程序注册设置:
品牌:
首页URL:https://<notsogood>.azurewebsites.net
身份验证:
重定向 URI:
https://localhost:54449/
https://<notsogood>.azurewebsites.net/.auth/login/aad/callback
隐式授予:
ID 令牌:已检查
支持的账户类型
仅限此组织目录中的帐户(我的公司 - 单租户)
将应用程序视为 public 客户端
没有
而当我运行这里的申请是callback
请求。
当我 运行 它时,我确实看到了 AD 登录屏幕,我可以在其中输入我的 AD 用户和凭据。但是,它没有成功登录。
如您所见,响应中的 Location
发生了变化。我知道这个非工作版本在 web.config
中有 authentication
和 authorization
部分,如果我将 loginUrl
属性从 /login
更改为 /loginklg
它会将 location
更改为 /loginklg?ReturnUrl=%2f.auth%2flogin%2faad%2fcallback
但如果我删除该部分,该网站将无法运行。
您还应该注意到有一个循环,它尝试让我登录,然后由于某种原因不能登录,然后重试。
最初,无法正常工作的站点具有以下用于身份验证的启动代码:
public void ConfigureAuth(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/login"),
Provider = new CookieAuthenticationProvider {
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)
)
}
});
}
我把它放进去,也把它取出来了,这对我的结果没有影响。
唯一真正的区别是工作版本是 MVC 并且调用了 SignIn
方法。
public void SignIn() {
if (!Request.IsAuthenticated) {
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
并且对于不工作的版本,它是一个 WebForm/Page 并且 Page_Load
方法被调用:
请注意
此应用程序不是由我或我的公司创建的,因此我试图将它与一些单独的 类 和配置设置集成在一起,并尽可能减少代码更改。 _avantiaSSOEnabled
只是读取我添加的 web.config
中的 appSettings
。 _openIdEnabled
已经存在。
_openIdEnabled = false
_avantiaSSOEnabled = true
即使我启用 _openIdEnabled
,Location
仍然很糟糕。
protected void Page_Load(object sender, EventArgs e) {
if (_avantiaSSOEnabled) {
if (!Request.IsAuthenticated) {
Request.GetOwinContext().Authentication.Challenge(
new Microsoft.Owin.Security.AuthenticationProperties { RedirectUri = "/klg" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
if (_openIdEnabled)
openIdBackgroundSignIn.OnOpenIdSSOLoggedIn += OnOpenIdSSOLoggedIn;
if (!IsPostBack) {
if (SystemHub.Maintenance.IsActive)
HandleInfoPopup(MaintenenceException.Text, true);
else if (Request["error"] != null)
HandleError(Request["error"].ToString());
else if (Request["auto"] == "true")
HandleInfoPopup(AutoLogout.Text, true);
else if (_openIdEnabled) {
openIdBackgroundSignIn.ClearData();
if (Request["oidc_error"] != null) //This is usually when auto-login fails, so we pass it to client side which will handle it
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ERROR, Request["oidc_error"].ToString());
else if (Request["oidc_login"] == "true")
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_LOGIN_SUCCESS, true);
else if (User.Identity.IsAuthenticated)
Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
else if (Request["lo"] == null) //lo is set when coming from logout, so don't try to autologin
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ATTEMPT_LOGIN_AUTO, true);
}
else if (User.Identity.IsAuthenticated) {
Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
}
}
}
我所做的唯一代码更改(不在上面链接的文章中)是为了修复它,阅读了许多其他文章,默认 cookie 管理器存在一个已知问题。这是结果:
app.UseCookieAuthentication(new CookieAuthenticationOptions {
CookieManager = new SystemWebChunkingCookieManager() // Originally SystemWebCookieManager
});
我知道我很接近。显然,某些东西正在拦截请求并对其进行调整。我只是不确定去哪里看。我从一开始就用 C# 编写代码,但我不太习惯它的 security/SSO 方面,所以任何帮助都将不胜感激。如果您需要我添加更多信息,我可以,让我知道什么。
更新 - 2020 年 7 月 31 日
在关注 this article 之后,我能够修复位置 /login?ReturnUrl...
,如下所示。
正如您在下图中看到的,从 AAD 登录日志中,我已成功登录。因此,代码似乎无法记住或存储登录后的令牌,然后尝试再次并且在失败之前必须有一定的尝试阈值或时间。
奇怪的是,当它停止循环时,我收到以下消息,其中包含我尝试登录的帐户的电子邮件,并显示“已登录”
通过删除以下行解决了循环问题:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
循环发生是因为身份验证类型(在上一行中设置)将 return Cookies
。但是,AAD 的响应将类型设置为 ApplicationCookie
。
ConfigAuth 中的完整代码现在是:
public void ConfigAuth(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions {
CookieManager = new SystemWebChunkingCookieManager(),
Provider = new CookieAuthenticationProvider {
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: AuthenticationHelper.OpenIdEnabled
? TimeSpan.FromSeconds(30)
: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
ClientId = AvantiaSSOHelper.ClientId,
Authority = AvantiaSSOHelper.Authority,
PostLogoutRedirectUri = AvantiaSSOHelper.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = (context) => {
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
},
AuthorizationCodeReceived = (context) => {
Debug.WriteLine($"Authorization code received: {context.Code}");
return Task.FromResult(0);
},
MessageReceived = (context) => {
Debug.WriteLine($"Message received: {context.Response.StatusCode}");
return Task.FromResult(0);
},
SecurityTokenReceived = (context) => {
Debug.WriteLine($"Security token received: {context.ProtocolMessage.IdToken}");
string test = context.ProtocolMessage.AccessToken;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) => {
Debug.WriteLine($"Security token validated: {context.Response.StatusCode}");
var nameClaim = context.AuthenticationTicket.Identity.Claims
.Where(x => x.Type == AvantiaSSOHelper.ClaimTypeWithEmail)
.FirstOrDefault();
if (nameClaim != null)
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));
return Task.FromResult(0);
},
TokenResponseReceived = (context) => {
string test = context.ProtocolMessage.AccessToken;
return Task.FromResult(0);
}
}
}
);
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
进行了一次 (non-looping) 调用,然后系统尝试以已验证模式继续。但是,我还需要做一步。最后一步是通过在身份验证票证中添加适当的响应声明来更改 SecurityTokenValidated
事件。我们的系统使用 Micrososft Identity
,因此基于电子邮件地址。因此,我需要从提取的电子邮件声明值中向身份验证票证添加类型为 ClaimTypes.Name
的声明,如下所示:
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));
AvantiaSSOHelper.ClaimTypeWithEmail
只是我从 Web.config
文件中读出的一个值,以防其他实现有不同的声明,我需要 extsract。
我已阅读并遵循 this article 使用我们的 AAD(Azure Active Directory)设置我的网站以获取 SSO(单点登录)。我已经让它在一个全新的网站上工作 localhost
以及当我将它发布到 Azure 时。
以下是工作版本应用程序注册的设置:
品牌:
首页URL:https://<worksgood>.azurewebsites.net
身份验证:
重定向 URI:
https://localhost:44390/
https://<worksgood>.azurewebsites.net/.auth/login/aad/callback
隐式授予:
ID 令牌:已检查
支持的账户类型
仅限此组织目录中的帐户(我的公司 - 单租户)
将应用程序视为 public 客户端
没有
而当我运行这里的申请是callback
请求。
如您所见,Response Header | Location
看起来不错(对我来说)
以下是我尝试将相同逻辑集成到的网站的应用程序注册设置:
品牌:
首页URL:https://<notsogood>.azurewebsites.net
身份验证:
重定向 URI:
https://localhost:54449/
https://<notsogood>.azurewebsites.net/.auth/login/aad/callback
隐式授予:
ID 令牌:已检查
支持的账户类型
仅限此组织目录中的帐户(我的公司 - 单租户)
将应用程序视为 public 客户端
没有
而当我运行这里的申请是callback
请求。
当我 运行 它时,我确实看到了 AD 登录屏幕,我可以在其中输入我的 AD 用户和凭据。但是,它没有成功登录。
如您所见,响应中的 Location
发生了变化。我知道这个非工作版本在 web.config
中有 authentication
和 authorization
部分,如果我将 loginUrl
属性从 /login
更改为 /loginklg
它会将 location
更改为 /loginklg?ReturnUrl=%2f.auth%2flogin%2faad%2fcallback
但如果我删除该部分,该网站将无法运行。
您还应该注意到有一个循环,它尝试让我登录,然后由于某种原因不能登录,然后重试。
最初,无法正常工作的站点具有以下用于身份验证的启动代码:
public void ConfigureAuth(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/login"),
Provider = new CookieAuthenticationProvider {
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)
)
}
});
}
我把它放进去,也把它取出来了,这对我的结果没有影响。
唯一真正的区别是工作版本是 MVC 并且调用了 SignIn
方法。
public void SignIn() {
if (!Request.IsAuthenticated) {
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
并且对于不工作的版本,它是一个 WebForm/Page 并且 Page_Load
方法被调用:
请注意
此应用程序不是由我或我的公司创建的,因此我试图将它与一些单独的 类 和配置设置集成在一起,并尽可能减少代码更改。 _avantiaSSOEnabled
只是读取我添加的 web.config
中的 appSettings
。 _openIdEnabled
已经存在。
_openIdEnabled = false
_avantiaSSOEnabled = true
即使我启用 _openIdEnabled
,Location
仍然很糟糕。
protected void Page_Load(object sender, EventArgs e) {
if (_avantiaSSOEnabled) {
if (!Request.IsAuthenticated) {
Request.GetOwinContext().Authentication.Challenge(
new Microsoft.Owin.Security.AuthenticationProperties { RedirectUri = "/klg" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
if (_openIdEnabled)
openIdBackgroundSignIn.OnOpenIdSSOLoggedIn += OnOpenIdSSOLoggedIn;
if (!IsPostBack) {
if (SystemHub.Maintenance.IsActive)
HandleInfoPopup(MaintenenceException.Text, true);
else if (Request["error"] != null)
HandleError(Request["error"].ToString());
else if (Request["auto"] == "true")
HandleInfoPopup(AutoLogout.Text, true);
else if (_openIdEnabled) {
openIdBackgroundSignIn.ClearData();
if (Request["oidc_error"] != null) //This is usually when auto-login fails, so we pass it to client side which will handle it
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ERROR, Request["oidc_error"].ToString());
else if (Request["oidc_login"] == "true")
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_LOGIN_SUCCESS, true);
else if (User.Identity.IsAuthenticated)
Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
else if (Request["lo"] == null) //lo is set when coming from logout, so don't try to autologin
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ATTEMPT_LOGIN_AUTO, true);
}
else if (User.Identity.IsAuthenticated) {
Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
}
}
}
我所做的唯一代码更改(不在上面链接的文章中)是为了修复它,阅读了许多其他文章,默认 cookie 管理器存在一个已知问题。这是结果:
app.UseCookieAuthentication(new CookieAuthenticationOptions {
CookieManager = new SystemWebChunkingCookieManager() // Originally SystemWebCookieManager
});
我知道我很接近。显然,某些东西正在拦截请求并对其进行调整。我只是不确定去哪里看。我从一开始就用 C# 编写代码,但我不太习惯它的 security/SSO 方面,所以任何帮助都将不胜感激。如果您需要我添加更多信息,我可以,让我知道什么。
更新 - 2020 年 7 月 31 日
在关注 this article 之后,我能够修复位置 /login?ReturnUrl...
,如下所示。
正如您在下图中看到的,从 AAD 登录日志中,我已成功登录。因此,代码似乎无法记住或存储登录后的令牌,然后尝试再次并且在失败之前必须有一定的尝试阈值或时间。
奇怪的是,当它停止循环时,我收到以下消息,其中包含我尝试登录的帐户的电子邮件,并显示“已登录”
通过删除以下行解决了循环问题:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
循环发生是因为身份验证类型(在上一行中设置)将 return Cookies
。但是,AAD 的响应将类型设置为 ApplicationCookie
。
ConfigAuth 中的完整代码现在是:
public void ConfigAuth(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions {
CookieManager = new SystemWebChunkingCookieManager(),
Provider = new CookieAuthenticationProvider {
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: AuthenticationHelper.OpenIdEnabled
? TimeSpan.FromSeconds(30)
: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
ClientId = AvantiaSSOHelper.ClientId,
Authority = AvantiaSSOHelper.Authority,
PostLogoutRedirectUri = AvantiaSSOHelper.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = (context) => {
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
},
AuthorizationCodeReceived = (context) => {
Debug.WriteLine($"Authorization code received: {context.Code}");
return Task.FromResult(0);
},
MessageReceived = (context) => {
Debug.WriteLine($"Message received: {context.Response.StatusCode}");
return Task.FromResult(0);
},
SecurityTokenReceived = (context) => {
Debug.WriteLine($"Security token received: {context.ProtocolMessage.IdToken}");
string test = context.ProtocolMessage.AccessToken;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) => {
Debug.WriteLine($"Security token validated: {context.Response.StatusCode}");
var nameClaim = context.AuthenticationTicket.Identity.Claims
.Where(x => x.Type == AvantiaSSOHelper.ClaimTypeWithEmail)
.FirstOrDefault();
if (nameClaim != null)
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));
return Task.FromResult(0);
},
TokenResponseReceived = (context) => {
string test = context.ProtocolMessage.AccessToken;
return Task.FromResult(0);
}
}
}
);
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
进行了一次 (non-looping) 调用,然后系统尝试以已验证模式继续。但是,我还需要做一步。最后一步是通过在身份验证票证中添加适当的响应声明来更改 SecurityTokenValidated
事件。我们的系统使用 Micrososft Identity
,因此基于电子邮件地址。因此,我需要从提取的电子邮件声明值中向身份验证票证添加类型为 ClaimTypes.Name
的声明,如下所示:
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));
AvantiaSSOHelper.ClaimTypeWithEmail
只是我从 Web.config
文件中读出的一个值,以防其他实现有不同的声明,我需要 extsract。