我正在尝试实现自定义OWIN中间件身份验证,该身份验证重定向到CAS服务器(中央身份验证服务)。一位同事构建了中间件,它似乎在大多数情况下工作,但是一旦中间件成功验证用户,我们都不知道如何存储cookie并重定向到AccountController上的ExternalCallbackLogin,以便允许用户访问受保护的内容。
程序流程应遵循位于Jasig CAS Webflow Diagram
的Web流程图我按照预期被重定向到我们的内部CAS服务器,当我登录时,我能够检索服务器提供的XML以生成声明,但是 从这里我不知道如何创建应用程序cookie并访问中间件之外的声明。
我们没有在此应用程序中实现ASP.Net Identity,我们遵循此tutorial将CAS设置为唯一的登录选项。我们无意允许其他外部登录。
非常感谢任何帮助。如果我需要提供更多信息,我将很高兴。
以下是中间件的所有代码:
CasOptions.cs
using Microsoft.Owin.Security;
using System;
namespace owin.cas.client {
public class CasOptions : AuthenticationOptions {
private string _casVersion;
private string _callbackPath;
public CasOptions() : base(Constants.AuthenticationType) {
this.AuthenticationMode = AuthenticationMode.Passive;
this.AuthenticationType = Constants.AuthenticationType; // Default is owin.cas.client
this.callbackPath = "/casHandler";
this.casVersion = "3";
this.Caption = Constants.AuthenticationType;
}
/// <summary>
/// The local URI path that will handle callbacks from the remote CAS server. The default is "/casHandler".
/// </summary>
/// <value>The callback path.</value>
public string callbackPath
{
get{ return this._callbackPath; }
set{
if (value.StartsWith("/", StringComparison.InvariantCulture)){
this._callbackPath = value;
}
else
{
this._callbackPath = "/" + value;
}
}
}
/// <summary>
/// This must be the base URL for your application as it is registered with the remote CAS server, minus the
/// callback path. For example, if your service is registered as "https://example.com/casHandler" with the
/// remote CAS server then you would set this property to "https://example.com".
/// </summary>
/// <value>The application URL.</value>
public string applicationURL { get; set; }
/// <summary>
/// This must be set to the base URL for the remote CAS server. For example, if the remote CAS server's login
/// URL is "https://cas.example.com/login" you would set this value to "https://cas.example.com".
/// </summary>
/// <value>The cas base URL.</value>
public string casBaseUrl {
get { return this._casVersion; }
set {
this._casVersion = value.TrimEnd('/');
}
}
public string Caption
{
get { return Description.Caption; }
set { Description.Caption = value; }
}
/// <summary>
/// Set to the CAS protocol version the remote CAS server supports. The default is "3". Acceptable values
/// are "1", "2", or "3".
/// </summary>
/// <value>The cas version.</value>
public string casVersion { get; set; }
// Used to store the Url that requires authentication. Typically marked by an Authorize tag.
public string externalRedirectUrl { get; set; }
}
}
CasMiddleware.cs
using Microsoft.Owin;
using Microsoft.Owin.Security.Infrastructure;
using Owin;
using System.Net.Http;
using System.Configuration;
namespace owin.cas.client {
public class CasMiddleware : AuthenticationMiddleware<CasOptions> {
private readonly HttpClient httpClient;
private readonly ICasCommunicator casCommunicator;
public CasMiddleware(OwinMiddleware next, IAppBuilder app, CasOptions options) : base(next, options) {
if (string.IsNullOrEmpty(options.casBaseUrl)) {
throw new SettingsPropertyNotFoundException("Missing required casBaseUrl option.");
}
if (string.IsNullOrEmpty(options.applicationURL)) {
throw new SettingsPropertyNotFoundException("Missing required serviceUrl option.");
}
this.httpClient = new HttpClient();
switch (options.casVersion) {
case "1":
this.casCommunicator = new Cas10(this.httpClient, options);
break;
case "3":
this.casCommunicator = new Cas30(this.httpClient, options);
break;
}
}
protected override AuthenticationHandler<CasOptions> CreateHandler() {
return new CasHandler(this.casCommunicator);
}
}
}
CasHandler.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using System.Security.Claims;
namespace owin.cas.client {
public class CasHandler : AuthenticationHandler<CasOptions> {
private readonly ICasCommunicator casCommunicator;
public CasHandler(ICasCommunicator casCommunicator) {
this.casCommunicator = casCommunicator;
}
public override async Task<bool> InvokeAsync() {
// Handle the callback from the remote CAS server
if (this.Request.Path.ToString().Equals(this.Options.callbackPath)) {
return await this.InvokeCallbackAsync();
}
// Let the next middleware do its thing instead.
return false;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() {
IReadableStringCollection query = Request.Query;
IList<string> tickets = query.GetValues("ticket");
string ticket = (tickets.Count == 1) ? tickets[0] : null;
if (string.IsNullOrEmpty(ticket)) {
return new AuthenticationTicket(null, new AuthenticationProperties());
}
CasIdentity casIdentity = await this.casCommunicator.validateTicket(ticket);
return new AuthenticationTicket(casIdentity, casIdentity.authenticationProperties);
}
protected override Task ApplyResponseChallengeAsync() {
if (Response.StatusCode != 401) {
return Task.FromResult<object>(null);
}
AuthenticationResponseChallenge challenge = this.Helper.LookupChallenge(this.Options.AuthenticationType, this.Options.AuthenticationMode);
if (challenge != null) {
string authUrl = this.Options.casBaseUrl + "/login?service=" + Uri.EscapeUriString(this.Options.applicationURL + this.Options.callbackPath);
this.Options.externalRedirectUrl = challenge.Properties.RedirectUri;
this.Response.StatusCode = 302;
this.Response.Headers.Set("Location", authUrl);
}
return Task.FromResult<object>(null);
}
// Basically the same thing as InvokereplyPathAsync() found in most
// middleware
protected async Task<bool> InvokeCallbackAsync() {
AuthenticationTicket authenticationTicket = await this.AuthenticateAsync();
if (authenticationTicket == null) {
this.Response.StatusCode = 500;
this.Response.Write("Invalid authentication ticket.");
return true;
}
// this.Context.Authentication.SignIn(authenticationTicket.Identity);
this.Context.Authentication.SignIn(authenticationTicket.Properties, authenticationTicket.Identity);
if(this.Options.externalRedirectUrl != null) {
Response.Redirect(this.Options.externalRedirectUrl);
}
return true;
}
}
}
Constants.cs
using System;
namespace owin.cas.client {
internal static class Constants {
internal const string AuthenticationType = "owin.cas.client";
internal const string V1_VALIDATE = "/validate";
internal const string V2_VALIDATE = "/serviceValidate";
internal const string V3_VALIDATE = "/p3/serviceValidate";
}
}
ICasCommunicator.cs
using System;
using System.Threading.Tasks;
namespace owin.cas.client {
public interface ICasCommunicator {
Task<CasIdentity> validateTicket(string ticket);
}
}
CasIdentity.cs
using System.Collections.Generic;
using System.Security.Claims;
using Microsoft.Owin.Security;
namespace owin.cas.client {
public class CasIdentity : ClaimsIdentity {
public CasIdentity() : base() { }
public CasIdentity(IList<Claim> claims) : base(claims) { }
public CasIdentity(IList<Claim> claims, string authType) : base(claims, authType) { }
public AuthenticationProperties authenticationProperties { get; set; }
}
}
CasExtensions.cs
using Owin;
using Microsoft.Owin.Extensions;
namespace owin.cas.client {
public static class CasExtensions {
public static IAppBuilder UseCasAuthentication(this IAppBuilder app, CasOptions options) {
app.Use(typeof(CasMiddleware), app, options);
app.UseStageMarker(PipelineStage.Authenticate);
return app;
}
}
}
Cas30.cs(这与Jasig CAS的当前协议版本相对应)
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.Owin.Security;
using System.Xml;
using XML;
using System.Collections.Generic;
using System.Diagnostics;
namespace owin.cas.client {
public class Cas30 : ICasCommunicator {
private readonly HttpClient httpClient;
private readonly CasOptions options;
public Cas30(HttpClient httpClient, CasOptions options) {
this.httpClient = httpClient;
this.options = options;
}
public async Task<CasIdentity> validateTicket(string ticket) {
CasIdentity result = new CasIdentity();
HttpResponseMessage response = await this.httpClient.GetAsync(
this.options.casBaseUrl + Constants.V3_VALIDATE +
"?service=" + Uri.EscapeUriString(this.options.applicationURL + this.options.callbackPath) +
"&ticket=" + Uri.EscapeUriString(ticket)
);
string httpResult = await response.Content.ReadAsStringAsync();
XmlDocument xml = XML.Documents.FromString(httpResult);
//Begin modification
XmlNamespaceManager nsmgr = new XmlNamespaceManager(xml.NameTable);
nsmgr.AddNamespace("cas", "http://www.yale.edu/tp/cas");
if (xml.GetElementsByTagName("cas:authenticationFailure").Count > 0) {
result = new CasIdentity();
result.authenticationProperties = new AuthenticationProperties();
} else {
IList<Claim> claims = new List<Claim>();
string username = xml.SelectSingleNode("//cas:user", nsmgr).InnerText;
claims.Add(new Claim(ClaimTypes.Name, username));
claims.Add(new Claim(ClaimTypes.NameIdentifier, username));
XmlNodeList xmlAttributes = xml.GetElementsByTagName("cas:attributes");
AuthenticationProperties authProperties = new AuthenticationProperties();
if (xmlAttributes.Count > 0){
foreach (XmlElement attr in xmlAttributes) {
if (attr.HasChildNodes) {
for (int i = 0; i < attr.ChildNodes.Count; i++) {
switch (attr.ChildNodes[i].Name)
{
case "cas:authenticationDate":
authProperties.Dictionary.Add(attr.ChildNodes[i].Name, DateTime.Parse(attr.ChildNodes[i].InnerText).ToString());
break;
case "cas:longTermAuthenticationRequestTokenUsed":
case "cas:isFromNewLogin":
authProperties.Dictionary.Add(attr.ChildNodes[i].Name, Boolean.Parse(attr.ChildNodes[i].InnerText).ToString());
break;
case "cas:memberOf":
claims.Add(new Claim(ClaimTypes.Role, attr.ChildNodes[i].InnerText));
break;
default:
authProperties.Dictionary.Add(attr.ChildNodes[i].Name, attr.ChildNodes[i].InnerText);
break;
}
}
}
}
result = new CasIdentity(claims, this.options.AuthenticationType);
}
result.authenticationProperties = authProperties;
}
return result;
}
}
}
Startup.Auth.cs
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Google;
using owin.cas.client;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
[assembly: OwinStartup(typeof(TestCASapp.Startup))] // This specifies the startup class.
namespace TestCASapp
{
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
var cookieOptions = new CookieAuthenticationOptions
{
LoginPath = new PathString("/Account/Login"),
};
app.UseCookieAuthentication(cookieOptions);
app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType = "owin.cas.client");
CasOptions casOptions = new CasOptions();
casOptions.applicationURL = "http://www.yourdomain.com/TestCASapp"; // The application URL registered with the CAS server minus the callback path, in this case /casHandler
casOptions.casBaseUrl = "https://devcas.int.*****.com"; // The base url of the remote CAS server you are targeting for login.
casOptions.callbackPath = "/casHandler"; // Callback path picked up by the middleware to begin the authentication ticket process
casOptions.AuthenticationMode = AuthenticationMode.Passive;
app.UseCasAuthentication(casOptions);
}
}
}
的AccountController
using Microsoft.Owin.Security;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace TestCASapp.Controllers
{
public class AccountController : Controller
{
public ActionResult Login(string returnUrl)
{
// Request a redirect to the external login provider
return new ChallengeResult("owin.cas.client",
Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
//return new ChallengeResult("Google",
// Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}
public ActionResult ExternalLoginCallback(string returnUrl)
{
return new RedirectResult(returnUrl);
}
// Implementation copied from a standard MVC Project, with some stuff
// that relates to linking a new external login to an existing identity
// account removed.
private class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUri)
{
LoginProvider = provider;
RedirectUri = redirectUri;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties() { RedirectUri = RedirectUri };
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
}
}
}
}
Startup.cs
using Microsoft.Owin;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
[assembly: OwinStartupAttribute(typeof(TestCASapp.Startup))]
namespace TestCASapp
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
答案 0 :(得分:2)
我弄清楚是什么导致中间件不存储cookie信息。在文章"Using Owin External Login without Identity"中,我在下面的句子中找到了解决方案,&#34;如果AuthenticationType与社交登录中间件创建的身份中的一个匹配,则cookie中间件将仅发出cookie。&#34;
当我发布问题时,我将cookie中间件身份验证类型设置为其默认属性,即&#34; ApplicationCookie&#34;如果我没有弄错的话。但是,我需要将身份验证类型设置为&#34; owin.cas.client&#34;为了使其与外部登录中间件创建的身份相匹配。一旦我相应地设置了它,我的应用程序就开始按预期设置cookie。
我遇到的另一个问题是中间件没有重定向到帐户控制器上的ExternalLoginCallback。这是因为我没有保存在CasMiddleware中调用ChallangeResult类时创建的redirectUrl。我将RedirectUrl添加到CasOptions类,然后,一旦身份验证完成,我只需将用户重定向回需要身份验证的页面。
我更新了原来的问题,以反映我的变化,希望将来可以证明这对其他人有益。