2016-08-17 8 views
3

Я пытаюсь внедрить обычную аутентификацию промежуточного программного обеспечения OWIN, которая перенаправляется на сервер CAS (служба централизованной аутентификации). Коллега построил промежуточное ПО, которое, по-видимому, работает по большей части, но никто из нас не знает, как сохранить cookie и перенаправить на ExternalCallbackLogin на AccountController, когда промежуточное программное обеспечение успешно провело проверку подлинности пользователя, чтобы позволить пользователю посетите защищенный контент.OWIN Middleware with CAS

Поток программы должны следовать схеме Web потока, расположенную по адресу: Jasig CAS Webflow Diagram

Я перенаправляюсь на наш внутренний сервер CAS, как и ожидался, и когда я подписываю в, я могу извлечь XML, предоставленный сервер для создания претензий, но отсюда я не знаю, как я могу создать файл cookie приложения и получить доступ к претензиям за пределами промежуточного программного обеспечения.

Мы не реализовали идентификатор ASP.Net в этом приложении, мы следовали этому 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; 
     } 
     } 
    } 

Константы. 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); 
     } 
    } 
} 

ответ

2

Я понял, что вызывает к промежуточному не хранить куки Информация. В статье "Using Owin External Login without Identity" я нашел решение в следующем предложении: «Средство промежуточного программного обеспечения cookie будет выдавать cookie только в том случае, если AuthenticationType совпадает с идентификатором, созданным промежуточным программным обеспечением социального входа."

Когда я разместил вопрос, у меня был тип аутентификации промежуточного программного обеспечения cookie, установленный по умолчанию, который был« ApplicationCookie », если я не ошибаюсь. Однако мне нужен был тип аутентификации, который должен быть установлен на« owin. cas.client ", чтобы он соответствовал идентификатору, созданному внешним промежуточным программным обеспечением для входа. После того, как я установил это, мое приложение начало настраивать файл cookie, как ожидалось.

Другая проблема, с которой я столкнулся, был промежуточным программным обеспечением, не перенаправляемым на ExternalLoginCallback на контроллере учетной записи.Это было связано с тем, что я не сохранял redirectUrl, созданный при вызове класса ChallangeResult, в CasMiddleware. Я добавил RedirectUrl в класс CasOptions, а затем, после завершения проверки подлинности, я просто повторно направил пользователя на страницу, требующую аутентификации.

Я обновил свой первоначальный вопрос, чтобы отразить мои изменения в надежде, что это может оказаться полезным для других в будущем.

+0

Это где-то на github? Я смотрю на внедрение родного промежуточного программного обеспечения ASP.NET Core для CAS. – adamhathcock

+0

Никогда не используйте это: https://github.com/akunzai/GSS.Authentication.CAS – adamhathcock