From b53ee853a7c472f63755f67af893986754c83ca1 Mon Sep 17 00:00:00 2001 From: Hong Tian Date: Fri, 3 Dec 2021 18:47:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0cas30=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DotNetCasClient.sln | 9 +- DotNetCasClient/CasAuthentication.cs | 2 + .../Configuration/CasClientConfiguration.cs | 1 + .../Cas30/AuthenticationSuccessAttributes.cs | 48 ++++++ .../Cas30ServiceTicketValidator.cs | 162 ++++++++++++++++++ 5 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 DotNetCasClient/Validation/Schema/Cas30/AuthenticationSuccessAttributes.cs create mode 100644 DotNetCasClient/Validation/TicketValidator/Cas30ServiceTicketValidator.cs diff --git a/DotNetCasClient.sln b/DotNetCasClient.sln index b1af275..41b56d0 100644 --- a/DotNetCasClient.sln +++ b/DotNetCasClient.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26430.15 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetCasClient", "DotNetCasClient\DotNetCasClient.csproj", "{883A296E-C898-4D1F-9ED9-DE7569DEFB3D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCasClient", "DotNetCasClient\DotNetCasClient.csproj", "{883A296E-C898-4D1F-9ED9-DE7569DEFB3D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -19,4 +19,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {92BDC157-981A-4EE2-B2B9-4CD3CCC41CE4} + EndGlobalSection EndGlobal diff --git a/DotNetCasClient/CasAuthentication.cs b/DotNetCasClient/CasAuthentication.cs index af77160..966fc61 100644 --- a/DotNetCasClient/CasAuthentication.cs +++ b/DotNetCasClient/CasAuthentication.cs @@ -253,6 +253,8 @@ public static void Initialize() ticketValidator = new Cas10TicketValidator(); else if (String.Compare(CasClientConfiguration.CAS20_TICKET_VALIDATOR_NAME, ticketValidatorName) == 0) ticketValidator = new Cas20ServiceTicketValidator(); + else if (String.Compare(CasClientConfiguration.CAS30_TICKET_VALIDATOR_NAME, ticketValidatorName) == 0) + ticketValidator = new Cas30ServiceTicketValidator(); else if (String.Compare(CasClientConfiguration.SAML11_TICKET_VALIDATOR_NAME, ticketValidatorName) == 0) ticketValidator = new Saml11TicketValidator(); else diff --git a/DotNetCasClient/Configuration/CasClientConfiguration.cs b/DotNetCasClient/Configuration/CasClientConfiguration.cs index b1c98c1..d18fe11 100644 --- a/DotNetCasClient/Configuration/CasClientConfiguration.cs +++ b/DotNetCasClient/Configuration/CasClientConfiguration.cs @@ -75,6 +75,7 @@ public class CasClientConfiguration : ConfigurationSection // Names for the supported ticket validators public const string CAS10_TICKET_VALIDATOR_NAME = "Cas10"; public const string CAS20_TICKET_VALIDATOR_NAME = "Cas20"; + public const string CAS30_TICKET_VALIDATOR_NAME = "Cas30"; public const string SAML11_TICKET_VALIDATOR_NAME = "Saml11"; // Names for the supported Service Ticket state provider diff --git a/DotNetCasClient/Validation/Schema/Cas30/AuthenticationSuccessAttributes.cs b/DotNetCasClient/Validation/Schema/Cas30/AuthenticationSuccessAttributes.cs new file mode 100644 index 0000000..c3d1e86 --- /dev/null +++ b/DotNetCasClient/Validation/Schema/Cas30/AuthenticationSuccessAttributes.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; +using System.Xml; + +namespace DotNetCasClient.Validation.Schema.Cas30 +{ + public class AuthenticationSuccessAttributes + { + internal AuthenticationSuccessAttributes() { } + + public static AuthenticationSuccessAttributes ParseResponse(string casResponse) + { + XmlDocument document = new XmlDocument(); + document.Load(new StringReader(casResponse)); + + XmlNamespaceManager nsmgr = new XmlNamespaceManager(document.NameTable); + nsmgr.AddNamespace("cas", "http://www.yale.edu/tp/cas"); + + var entity = new AuthenticationSuccessAttributes() + { + Attributes = new Dictionary>() + }; + var attributes = entity.Attributes; + + if (document.DocumentElement != null) + { + XmlNode xmlNode = document.DocumentElement.SelectSingleNode("cas:authenticationSuccess/cas:attributes", nsmgr); + + if (xmlNode != null) + { + foreach (XmlNode node in xmlNode.ChildNodes) + { + string key = node.LocalName, value = node.InnerText; + if (!attributes.ContainsKey(key)) + { + attributes.Add(key, new List()); + } + attributes[key].Add(value); + } + } + } + + return entity; + } + + public IDictionary> Attributes; + } +} diff --git a/DotNetCasClient/Validation/TicketValidator/Cas30ServiceTicketValidator.cs b/DotNetCasClient/Validation/TicketValidator/Cas30ServiceTicketValidator.cs new file mode 100644 index 0000000..a88f994 --- /dev/null +++ b/DotNetCasClient/Validation/TicketValidator/Cas30ServiceTicketValidator.cs @@ -0,0 +1,162 @@ +/* + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System; +using System.Web; +using DotNetCasClient.Security; +using DotNetCasClient.Utils; +using DotNetCasClient.Validation.Schema.Cas20; +using DotNetCasClient.Validation.Schema.Cas30; + +namespace DotNetCasClient.Validation.TicketValidator +{ + /// + /// CAS 3.0 Ticket Validator + /// + /// + /// This is the .Net port of org.jasig.cas.client.validation.Cas30ServiceTicketValidator. + /// + /// Tian Hong + class Cas30ServiceTicketValidator : AbstractCasProtocolTicketValidator + { + #region Properties + /// + /// The endpoint of the validation URL. Should be relative (i.e. not start with a "/"). + /// i.e. validate or serviceValidate. + /// + public override string UrlSuffix + { + get + { + if (CasAuthentication.ProxyTicketManager != null) + { + return "p3/proxyValidate"; + } + else + { + return "p3/serviceValidate"; + } + } + } + #endregion + + #region Methods + /// + /// Performs Cas20ServiceTicketValidator initialization. + /// + public override void Initialize() + { + if (CasAuthentication.ProxyTicketManager != null) + { + CustomParameters.Add("pgtUrl", HttpUtility.UrlEncode(UrlUtil.ConstructProxyCallbackUrl())); + } + } + + /// + /// Parses the response from the server into a CAS Assertion and includes this in + /// a CASPrincipal. + /// + /// Parsing of a <cas:attributes> element is not supported. The official + /// CAS 3.0 protocol does include this feature. If attributes are needed, + /// SAML must be used. + /// + /// + /// the response from the server, in any format. + /// The ticket used to generate the validation response + /// + /// a Principal backed by a CAS Assertion, if one could be created from the response. + /// + /// + /// Thrown if creation of the Assertion fails. + /// + protected override ICasPrincipal ParseResponseFromServer(string response, string ticket) + { + if (String.IsNullOrEmpty(response)) + { + throw new TicketValidationException("CAS Server response was empty."); + } + + ServiceResponse serviceResponse; + try + { + serviceResponse = ServiceResponse.ParseResponse(response); + } + catch (InvalidOperationException) + { + throw new TicketValidationException("CAS Server response does not conform to CAS 3.0 schema"); + } + + if (serviceResponse.IsAuthenticationSuccess) + { + AuthenticationSuccess authSuccessResponse = (AuthenticationSuccess)serviceResponse.Item; + + if (String.IsNullOrEmpty(authSuccessResponse.User)) + { + throw new TicketValidationException(string.Format("CAS Server response parse failure: missing 'cas:user' element.")); + } + + string proxyGrantingTicketIou = authSuccessResponse.ProxyGrantingTicket; + + if (CasAuthentication.ProxyTicketManager != null && !string.IsNullOrEmpty(proxyGrantingTicketIou)) + { + string proxyGrantingTicket = CasAuthentication.ProxyTicketManager.GetProxyGrantingTicket(proxyGrantingTicketIou); + if ( proxyGrantingTicket != null ) + CasAuthentication.ProxyTicketManager.InsertProxyGrantingTicketMapping( proxyGrantingTicketIou, proxyGrantingTicket ); + } + + var attributes = AuthenticationSuccessAttributes.ParseResponse(response); + + if (authSuccessResponse.Proxies != null && authSuccessResponse.Proxies.Length > 0) + { + return new CasPrincipal(new Assertion(authSuccessResponse.User, attributes.Attributes), proxyGrantingTicketIou, authSuccessResponse.Proxies); + } + else + { + return new CasPrincipal(new Assertion(authSuccessResponse.User, attributes.Attributes), proxyGrantingTicketIou); + } + } + + if (serviceResponse.IsAuthenticationFailure) + { + try + { + AuthenticationFailure authFailureResponse = (AuthenticationFailure) serviceResponse.Item; + throw new TicketValidationException(authFailureResponse.Message, authFailureResponse.Code); + } + catch + { + throw new TicketValidationException("CAS ticket could not be validated."); + } + } + + if (serviceResponse.IsProxySuccess) + { + throw new TicketValidationException("Unexpected service validate response: ProxySuccess"); + } + + if (serviceResponse.IsProxyFailure) + { + throw new TicketValidationException("Unexpected service validate response: ProxyFailure"); + } + + throw new TicketValidationException("Failed to validate CAS ticket."); + } + #endregion + } +} \ No newline at end of file