/*
* 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);
}
}
}