/*
* 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.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using Amazon.Runtime;
using Amazon.SecurityToken.Model;
namespace Amazon.SecurityToken.SAML
{
///
/// Contains the parsed SAML response data following successful user
/// authentication against a federated endpoint. We only parse out the
/// data we need to support generation of temporary AWS credentials.
///
public class SAMLAssertion
{
const string AssertionNamespace = "urn:oasis:names:tc:SAML:2.0:assertion";
const string RoleXPath = "//response:Attribute[@Name='https://aws.amazon.com/SAML/Attributes/Role']";
///
/// The full SAML assertion parsed from the identity provider's
/// response.
///
public string AssertionDocument { get; private set; }
///
/// The collection of roles available to the authenticated user.
/// he parsed friendly role name is used to key the entries.
///
public IDictionary RoleSet { get; private set; }
///
/// Retrieves a set of temporary credentials for the specified role, valid for the specified timespan.
/// If the SAML authentication data yield more than one role, a valid role name must be specified.
///
/// The STS client to use when making the AssumeRoleWithSAML request.
///
/// The arns of the principal and role as returned in the SAML assertion.
///
/// The valid timespan for the credentials.
/// Temporary session credentials for the specified or default role for the user.
public SAMLImmutableCredentials GetRoleCredentials(
IAmazonSecurityTokenService stsClient, string principalAndRoleArns, TimeSpan duration)
{
string roleArn = null;
string principalArn = null;
var swappedPrincipalAndRoleArns = string.Empty;
if (!string.IsNullOrEmpty(principalAndRoleArns))
{
var roleComponents = principalAndRoleArns.Split(',');
if(roleComponents.Count() != 2)
{
throw new ArgumentException("Unknown or invalid principal and role arns format.");
}
swappedPrincipalAndRoleArns = roleComponents.Last() + "," + roleComponents.First();
}
foreach (var s in RoleSet.Values)
{
if (s.Equals(principalAndRoleArns, StringComparison.OrdinalIgnoreCase) || s.Equals(swappedPrincipalAndRoleArns, StringComparison.OrdinalIgnoreCase))
{
var roleComponents = s.Split(',');
if (IsSamlProvider(roleComponents.First()))
{
//Backwards compatible format -- arn:...:saml-provider/SAML,arn:...:role/RoleName
principalArn = roleComponents.First();
roleArn = roleComponents.Last();
}
else
{
//Documented format -- arn:...:role/RoleName,arn:...:saml-provider/SAML
roleArn = roleComponents.First();
principalArn = roleComponents.Last();
}
break;
}
}
if (string.IsNullOrEmpty(roleArn) || string.IsNullOrEmpty(principalArn))
throw new ArgumentException("Unknown or invalid role specified.");
var assumeSamlRequest = new AssumeRoleWithSAMLRequest
{
SAMLAssertion = AssertionDocument,
RoleArn = roleArn,
PrincipalArn = principalArn,
DurationSeconds = (int)duration.TotalSeconds
};
#if NETSTANDARD
//In the NetStandard SDK flavor the sync operations are internal only.
var response = ((AmazonSecurityTokenServiceClient)stsClient).AssumeRoleWithSAML(assumeSamlRequest);
#else
var response = stsClient.AssumeRoleWithSAML(assumeSamlRequest);
#endif
return new SAMLImmutableCredentials(response.Credentials.GetCredentials(),
response.Credentials.Expiration.ToUniversalTime(),
response.Subject);
}
///
/// Constructs a new SAML assertion wrapper based on a successful authentication
/// response and extracts the role data contained in the assertion.
///
///
internal SAMLAssertion(string assertion)
{
AssertionDocument = assertion;
RoleSet = ExtractRoleData();
}
///
/// Parses the role data out of the assertion using xpath queries. We additionally
/// parse the role ARNs to extract friendly role names that can be used in UI
/// prompts in tooling.
///
/// Dictionary of friendly role names to role arn mappings.
private IDictionary ExtractRoleData()
{
var doc = new XmlDocument();
//var sw = new StringWriter(CultureInfo.InvariantCulture);
var decoded = Convert.FromBase64String(AssertionDocument);
var deflated = Encoding.UTF8.GetString(decoded);
doc.LoadXml(deflated);
//using (var tw = new XmlTextWriter(sw) { Formatting = Formatting.Indented })
//{
// doc.WriteTo(tw);
//}
var nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("response", AssertionNamespace);
var roleAttributeNodes = doc.DocumentElement.SelectNodes(RoleXPath, nsmgr);
var discoveredRoles = new Dictionary(StringComparer.OrdinalIgnoreCase);
if (roleAttributeNodes != null && roleAttributeNodes.Count > 0)
{
var roleNodes = roleAttributeNodes[0].ChildNodes;
// we use this in case we encounter a provider that does allow duplicate
// role definitions (unlikely)
var seenRoles = new HashSet(StringComparer.Ordinal);
foreach (XmlNode roleNode in roleNodes)
{
if (!string.IsNullOrEmpty(roleNode.InnerText))
{
var chunks = roleNode.InnerText.Split(new[] { ',' }, 3);
var samlRole = chunks[0] + ',' + chunks[1];
if (!seenRoles.Contains(samlRole))
{
var roleName = string.Empty;
if (IsSamlProvider(chunks[1]))
{
//Documented format -- arn:...:role/RoleName,arn:...:saml-provider/SAML
roleName = ExtractRoleName(chunks[0]);
}
else
{
//Backwards compatible format -- arn:...:saml-provider/SAML,arn:...:role/RoleName
roleName = ExtractRoleName(chunks[1]);
}
discoveredRoles.Add(roleName, samlRole);
seenRoles.Add(samlRole);
}
}
}
}
return discoveredRoles;
}
private static bool IsSamlProvider(string chunk)
{
return chunk.IndexOf(":saml-provider", StringComparison.OrdinalIgnoreCase) != -1;
}
private static string ExtractRoleName(string chunk)
{
// It is possible to configure the same role name across different accounts
// so we must take account number into consideration to get the friendly name
// to avoid duplicate keys
//Example chunk format: arn:aws:iam::account-number:role/role-name1
var roleNameStart = chunk.LastIndexOf("::", StringComparison.Ordinal);
string roleName;
if (roleNameStart >= 0)
roleName = chunk.Substring(roleNameStart + 2);
else
roleName = chunk;
return roleName;
}
}
}