/*
* 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 Amazon.Runtime.CredentialManagement;
using Amazon.Runtime.Internal;
using Amazon.Runtime.CredentialManagement.Internal;
using Amazon.Runtime.Internal.Util;
using Amazon.Runtime.SharedInterfaces;
using Amazon.Util;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
namespace Amazon.Runtime
{
///
/// Temporary credentials that are created following successful authentication with
/// a federated endpoint supporting SAML.
///
///
/// In order to use the FederatedAWSCredentials class the AWSSDK.SecurityToken assembly
/// must be available to your application at runtime.
///
public class FederatedAWSCredentials : RefreshingAWSCredentials
{
private const int MaxAuthenticationRetries = 3;
private static readonly RegionEndpoint DefaultSTSClientRegion = RegionEndpoint.USEast1;
private static readonly TimeSpan MaximumCredentialTimespan = TimeSpan.FromHours(1);
private static readonly TimeSpan DefaultPreemptExpiryTime = TimeSpan.FromMinutes(5);
private readonly SAMLRoleSessionManager sessionManager = new SAMLRoleSessionManager();
///
/// Constructs an instance of FederatedAWSCredentials. After construction call GetCredentials
/// to authenticate the user/process and obtain temporary AWS credentials.
///
/// The SAML endpoint used for authentication.
/// The role ARN used for authentication.
public FederatedAWSCredentials(SAMLEndpoint samlEndpoint, string roleArn)
: this(samlEndpoint, roleArn, new FederatedAWSCredentialsOptions())
{
}
///
/// Constructs an instance of FederatedAWSCredentials. After construction call GetCredentials
/// to authenticate the user/process and obtain temporary AWS credentials.
///
/// The SAML endpoint used for authentication.
/// The role ARN used for authentication.
/// The options used for authentication.
/// See for details about available options.
public FederatedAWSCredentials(SAMLEndpoint samlEndpoint, string roleArn,
FederatedAWSCredentialsOptions options)
{
if (string.IsNullOrEmpty(roleArn))
{
throw new ArgumentException("RoleArn must not be null or empty.");
}
Options = options ?? throw new ArgumentNullException("options");
SAMLEndpoint = samlEndpoint ?? throw new ArgumentNullException("samlEndpoint");
RoleArn = roleArn;
PreemptExpiryTime = DefaultPreemptExpiryTime;
}
///
/// The SAML Endpoint used for authentication.
///
public SAMLEndpoint SAMLEndpoint { get; private set; }
///
/// The role ARN used for authentication.
///
public string RoleArn { get; private set; }
///
/// The options used for authentication.
/// See for details about available options.
///
public FederatedAWSCredentialsOptions Options { get; private set; }
///
/// Refresh credentials after expiry. If the role profile is configured with user identity
/// information and a callback has been registered to obtain the user credential, the callback
/// will be invoked ahead of authentication. For role profiles configured with user identity
/// but no callback registration, the SDK will fall back to attempting to use the default
/// user identity of the current process.
///
///
protected override CredentialsRefreshState GenerateNewCredentials()
{
Validate();
// If the profile indicates the user has already authenticated and received
// credentials which are still valid, adopt them instead of requiring a fresh
// authentication.
SAMLImmutableCredentials currentSession;
if (TryGetRoleSession(out currentSession))
{
// Since cached role session credentials can be obtained, stored credentials exist
// and they were not expired. However, since the stored credentials are actual
// expiration time and not preempt expiration time we must preempt the expiration
// to check that they will not expire before this call completes.
var cachedState = new CredentialsRefreshState(currentSession, currentSession.Expires);
// Use the cached credentials as long as they are not within the preempt expiry window
// or have since actually expired since the prior call to TryGetRoleSession.
//Verify the actual expiration is not within the preempt expiration time.
if (!cachedState.IsExpiredWithin(PreemptExpiryTime))
{
// The credentials have plenty of time left before they expire so they can be used. After
// return the expiration time will be preempted for future checks using ShouldUpdate.
return cachedState;
}
}
CredentialsRefreshState newState = null;
var attempts = 0;
do
{
try
{
NetworkCredential userCredential = null;
if (Options.UserIdentity != null)
{
if (Options.CredentialRequestCallback != null)
{
var callbackArgs = new CredentialRequestCallbackArgs
{
ProfileName = Options.ProfileName,
UserIdentity = Options.UserIdentity,
CustomState = Options.CustomCallbackState,
PreviousAuthenticationFailed = attempts > 0
};
userCredential = Options.CredentialRequestCallback(callbackArgs);
if (userCredential == null) // user declined to authenticate
{
throw new FederatedAuthenticationCancelledException(
"User cancelled credential request.");
}
}
else
{
var logger = Logger.GetLogger(typeof(FederatedAWSCredentials));
logger.InfoFormat(
"FederatedAWSCredentials configured for a specific user but no credential request callback registered; falling back to default identity.");
}
}
newState = Authenticate(userCredential);
}
catch (FederatedAuthenticationFailureException)
{
if (attempts < MaxAuthenticationRetries)
attempts++;
else
throw;
}
} while (newState == null && attempts < MaxAuthenticationRetries);
return newState;
}
private CredentialsRefreshState Authenticate(ICredentials userCredential)
{
CredentialsRefreshState state;
var region = Options.STSRegion;
if (region == null)
{
region = FallbackRegionFactory.GetRegionEndpoint();
}
if (region == null)
{
region = DefaultSTSClientRegion;
}
ICoreAmazonSTS coreSTSClient = null;
try
{
var stsConfig = ServiceClientHelpers.CreateServiceConfig(
ServiceClientHelpers.STS_ASSEMBLY_NAME, ServiceClientHelpers.STS_SERVICE_CONFIG_NAME);
stsConfig.RegionEndpoint = region;
if (Options.ProxySettings != null)
{
stsConfig.SetWebProxy(Options.ProxySettings);
}
coreSTSClient = ServiceClientHelpers.CreateServiceFromAssembly(
ServiceClientHelpers.STS_ASSEMBLY_NAME, ServiceClientHelpers.STS_SERVICE_CLASS_NAME,
new AnonymousAWSCredentials(), stsConfig);
}
catch (Exception e)
{
var msg = string.Format(CultureInfo.CurrentCulture,
"Assembly {0} could not be found or loaded. This assembly must be available at runtime to use this profile class.",
ServiceClientHelpers.STS_ASSEMBLY_NAME);
throw new InvalidOperationException(msg, e);
}
var samlCoreSTSClient
#if NETSTANDARD
= coreSTSClient as ICoreAmazonSTS_SAML;
if (coreSTSClient == null)
{
throw new NotImplementedException("The currently loaded version of AWSSDK.SecurityToken doesn't support SAML authentication.");
}
#else
= coreSTSClient;
#endif
try
{
var credentials = samlCoreSTSClient.CredentialsFromSAMLAuthentication(
SAMLEndpoint.EndpointUri.ToString(),
SAMLEndpoint.AuthenticationType.ToString(), RoleArn, MaximumCredentialTimespan, userCredential);
RegisterRoleSession(credentials);
state = new CredentialsRefreshState(credentials, credentials.Expires);
}
catch (Exception e)
{
var wrappedException =
new AmazonClientException("Credential generation from SAML authentication failed.", e);
var logger = Logger.GetLogger(typeof(FederatedAWSCredentials));
logger.Error(wrappedException, wrappedException.Message);
throw wrappedException;
}
return state;
}
private string GetRoleSessionName()
{
if (string.IsNullOrEmpty(Options.ProfileName))
{
return SAMLEndpoint.Name + "," + RoleArn + "," + Options.UserIdentity;
}
else
{
return Options.ProfileName;
}
}
private bool TryGetRoleSession(out SAMLImmutableCredentials sessionCredentials)
{
if (SAMLRoleSessionManager.IsAvailable)
{
return sessionManager.TryGetRoleSession(GetRoleSessionName(), out sessionCredentials);
}
else
{
sessionCredentials = null;
return false;
}
}
private void RegisterRoleSession(SAMLImmutableCredentials sessionCredentials)
{
if (SAMLRoleSessionManager.IsAvailable)
{
sessionManager.RegisterRoleSession(GetRoleSessionName(), sessionCredentials);
}
}
///
/// Clears currently-stored credentials, forcing the next GetCredentials call to generate new credentials.
///
public override void ClearCredentials()
{
// For Federated AWS credentials, this method does not force the generation of new credentials. Even though
// it clears the credential state to null, any cached credentials will immediately be picked up and used.
// Changing this behavior is a possible breaking change. Once we decide to make this change, the proper course
// of action would likely be to ensure the cache is cleared before setting the credential state to null.
base.ClearCredentials();
}
}
}