带CAS的OWIN中间件

时间:2016-08-17 14:09:52

标签: c# asp.net owin cas

我正在尝试实现自定义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);
        }
    }
}

1 个答案:

答案 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类,然后,一旦身份验证完成,我只需将用户重定向回需要身份验证的页面。

我更新了原来的问题,以反映我的变化,希望将来可以证明这对其他人有益。