/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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.Net; using System.Runtime.InteropServices; using System.Security; using System.IO; using System.Text.RegularExpressions; namespace Amazon.SecurityToken.SAML { /// /// Implementation of IAuthenticationController, allowing authentication calls against /// an AD FS endpoint. /// internal class AdfsAuthenticationController : IAuthenticationController { /// /// Authenticates the user with the specified AD FS endpoint and /// yields the SAML response data for subsequent parsing. /// /// /// The https endpoint of the federated identity provider. /// /// /// Credentials for the call. If null, the user's default network credentials /// will be used in a temporary impersonation context. /// /// /// The authentication type to be used with the endpoint. Valid values are 'NTLM', /// 'Digest', 'Kerberos' and 'Negotiate'. /// /// Null or configured proxy settings for the HTTPS call. /// The response data from a successful authentication request. public string Authenticate(Uri identityProvider, ICredentials credentials, string authenticationType, #if NETSTANDARD IWebProxy proxySettings) #else WebProxy proxySettings) #endif { try { return QueryProvider(identityProvider, proxySettings, credentials, authenticationType); } catch (Exception e) { throw new AdfsAuthenticationControllerException(e.ToString(), e); } } private static string QueryProvider(Uri identityProvider, IWebProxy proxySettings, ICredentials credentials, string authenticationType) { var uri = identityProvider; var cookieContainer = new CookieContainer(); int redirectionsCount = 0; string responseData = null; var connectionGroup = Guid.NewGuid().ToString(); //This is to avoid having multiple users sharing the same connection //if they authenticate against the same endpoint. while (responseData == null) { HttpWebResponse response = null; try { HttpWebRequest request = null; WebException webRequestException = null; try { request = (HttpWebRequest)WebRequest.Create(uri); request.CookieContainer = cookieContainer; request.ConnectionGroupName = connectionGroup; request.KeepAlive = true; //KeepAlive = false doesn't work on .NET Core 2.1+ request.UserAgent = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)"; request.AllowAutoRedirect = false; // Handling redirection manually to avoid 401 errors if (proxySettings != null) { request.Proxy = proxySettings; } if (credentials != null) { request.Credentials = credentials?.GetCredential(uri, authenticationType); } else { request.UseDefaultCredentials = true; } response = (HttpWebResponse)request.GetResponse(); } catch (WebException e) { webRequestException = e; response = (HttpWebResponse)e.Response; } Uri redirectedUri = null; if (response != null) { const int minRedirectStatusCode = 300; const int maxRedirectStatusCode = 399; int statusCode = (int)response.StatusCode; if (statusCode >= minRedirectStatusCode && statusCode <= maxRedirectStatusCode && redirectionsCount++ < request.MaximumAutomaticRedirections) { var location = response.Headers[HttpResponseHeader.Location]; if (location != null) { redirectedUri = new Uri(uri, location); } } } if (redirectedUri != null) { uri = redirectedUri; } else if (webRequestException != null) { throw webRequestException; } else { using (var reader = new StreamReader(response.GetResponseStream())) { responseData = reader.ReadToEnd(); } } } finally { response?.Close(); #if !BCL35 response?.Dispose(); #endif } } return responseData; } } /// /// Custom exception thrown when authentication failure is detected against /// a configured AD FS endpoint. /// #if !NETSTANDARD [Serializable] #endif public class AdfsAuthenticationControllerException : Exception { /// /// Initializes a new exception instance. /// /// public AdfsAuthenticationControllerException(string message) : base(message) { } /// /// Initializes a new exception instance. /// /// /// public AdfsAuthenticationControllerException(string message, Exception innerException) : base(message, innerException) { } /// /// Initializes a new exception instance. /// /// public AdfsAuthenticationControllerException(Exception innerException) : base(innerException.Message, innerException) { } #if !NETSTANDARD /// /// Constructs a new instance of the AdfsAuthenticationControllerException class with serialized data. /// /// The that holds the serialized object data about the exception being thrown. /// The that contains contextual information about the source or destination. /// The parameter is null. /// The class name is null or is zero (0). protected AdfsAuthenticationControllerException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } #endif } /// /// Implementation of IAuthenticationResponseParser, allowing parsing of the responses for /// successful authentication calls against AD FS endpoints. /// internal class AdfsAuthenticationResponseParser : IAuthenticationResponseParser { /// /// Parses the authentication response (html) and extracts the SAML response (xml) /// for further parsing. /// /// /// The HTML response data from the successful authentication call. /// /// /// Assertion instance containing the data needed to support credential generation. /// public SAMLAssertion Parse(string authenticationResponse) { var samlAssertion = string.Empty; var reg = new Regex("SAMLResponse\\W+value\\=\\\"([^\\\"]+)\\\""); var matches = reg.Matches(authenticationResponse); foreach (Match m in matches) { var last = m.Groups[1].Value; samlAssertion = last; } return new SAMLAssertion(samlAssertion); } } }