/*
* Copyright 2011-2019 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.CognitoIdentity.Model;
using Amazon.Runtime;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Amazon.CognitoIdentity
{
///
/// Temporary, short-lived session credentials that are automatically retrieved from
/// Amazon Cognito Identity Service and AWS Security Token Service.
/// Depending on configured Logins, credentials may be authenticated or unauthenticated.
///
public partial class CognitoAWSCredentials : RefreshingAWSCredentials
{
#region Private members
private static object refreshIdLock = new object();
private string identityId;
private static int DefaultDurationSeconds = (int)TimeSpan.FromHours(1).TotalSeconds;
#if !UNITY
private IAmazonCognitoIdentity cib;
#else
private AmazonCognitoIdentityClient cib;
#endif
#if !UNITY
private IAmazonSecurityTokenService sts;
#else
private AmazonSecurityTokenServiceClient sts;
#endif
private bool IsIdentitySet
{
get
{
if (string.IsNullOrEmpty(identityId))
{
identityId = GetCachedIdentityId();
}
return !string.IsNullOrEmpty(identityId);
}
}
// Updates IdentityId to new value and fires IdentityChangedEvent
private void UpdateIdentity(string newIdentityId)
{
// No-op if new IdentityId is same as old
if (string.Equals(identityId, newIdentityId, StringComparison.Ordinal))
return;
//save the new identity id and destroy the credentials associated with the old id.
CacheIdentityId(newIdentityId);
ClearCredentials();
// Swap in new identity
string oldIdentityId = identityId;
identityId = newIdentityId;
// Fire the event
var handler = mIdentityChangedEvent;
if (handler != null)
{
var args = new IdentityChangedArgs(oldIdentityId, newIdentityId);
handler(this, args);
}
}
#endregion
#region protected methods and enum
///
/// Returns clone of the Logins dictionary
///
/// The Clone of Logins dictionary.
protected Dictionary CloneLogins
{
get
{
Dictionary ret = new Dictionary(Logins.Count,
Logins.Comparer);
foreach (KeyValuePair entry in Logins)
{
ret.Add(entry.Key, entry.Value);
}
return ret;
}
}
///
/// Gives a namespaced key for supporting multiple identity pool id's
///
///
///
protected string GetNamespacedKey(string key)
{
return key + ":" + IdentityPoolId;
}
[Flags]
private enum RefreshIdentityOptions
{
///
/// Dont refresh identity.
///
None = 0,
///
/// Refresh if Id not set or If Identity Provider is BYOI
///
Refresh = 1
}
#endregion
#region Public properties, methods, classes, and events
///
/// Information about an identity change in the CognitoAWSCredentials.
///
public class IdentityChangedArgs : EventArgs
{
///
/// Gets the OldIdentityId property.
///
public string OldIdentityId { get; private set; }
///
/// Gets the NewIdentityId property.
///
public string NewIdentityId { get; private set; }
internal IdentityChangedArgs(string oldIdentityId, string newIdentityId)
{
OldIdentityId = oldIdentityId;
NewIdentityId = newIdentityId;
}
}
///
/// Information about the state of the identity
///
public class IdentityState
{
///
/// Gets the Identity Id
///
public string IdentityId { get; private set; }
///
/// Gets the Login Provider
///
public string LoginProvider { get; private set; }
///
/// Gets the Login Token
///
public string LoginToken { get; private set; }
///
/// Indicates if the identity Id is from cache
///
public bool FromCache { get; private set; }
///
/// Creates an instance of the Identity State using identity id , token, provider, fromCache flag
///
///
///
///
///
public IdentityState(string identityId, string provider, string token, bool fromCache)
{
IdentityId = identityId;
LoginProvider = provider;
LoginToken = token;
FromCache = fromCache;
}
///
/// Creates an instance using the identity id and from cache flag
///
///
///
public IdentityState(string identityId, bool fromCache)
{
IdentityId = identityId;
FromCache = fromCache;
}
///
/// returns true is the Login provider and login token values are present
///
public bool LoginSpecified
{
get
{
return (!string.IsNullOrEmpty(LoginProvider) && string.IsNullOrEmpty(LoginToken));
}
}
}
///
/// The AWS accountId for the account with Amazon Cognito
///
public string AccountId { get; private set; }
///
/// The Amazon Cogntio identity pool to use
///
public string IdentityPoolId { get; private set; }
///
/// The ARN of the IAM Role that will be assumed when unauthenticated
///
public string UnAuthRoleArn { get; private set; }
///
/// The ARN of the IAM Role that will be assumed when authenticated
///
public string AuthRoleArn { get; private set; }
///
/// Logins map used to authenticated with Amazon Cognito.
/// Note: After modifying this field, you must manually call Clear on this
/// instance of the CognitoAWSCredentials, as your Identity Id may have changed.
///
private Dictionary Logins { get; set; }
///
/// Identity State which is returned by refresh identity.
///
private IdentityState _identityState;
///
/// Clears current credentials state. This will reset the IdentityId.
/// Use instead if you just want to trigger a credentials refresh.
///
public void Clear()
{
identityId = null;
ClearCredentials();
ClearIdentityCache();
Logins.Clear();
}
///
/// The list of current providers that are used for authenticated credentials.
///
public string[] CurrentLoginProviders
{
get { return this.Logins.Keys.ToArray(); }
}
///
/// Returns if the providerName is present in the Logins Collection.
///
/// The provider name for the login (i.e. graph.facebook.com)
/// true if the provider name is present in the logins collection, else false
public bool ContainsProvider(string providerName)
{
return Logins.ContainsKey(providerName);
}
///
/// Removes a provider from the collection of logins.
///
/// The provider name for the login (i.e. graph.facebook.com)
public void RemoveLogin(string providerName)
{
this.Logins.Remove(providerName);
this.ClearCredentials();
}
///
/// Adds a login to be used for authenticated requests.
///
/// The provider name for the login (i.e. graph.facebook.com)
/// The token provided by the identity provider.
public void AddLogin(string providerName, string token)
{
Logins[providerName] = token;
this.ClearCredentials();
}
///
/// Returns count of Login Providers.
///
/// The count of the login provider.
public int LoginsCount
{
get
{
return Logins.Count;
}
}
///
/// Gets the Identity Id corresponding to the credentials retrieved from Cognito.
/// Note: this setting may change during execution. To be notified of its
/// new value, attach a listener to IdentityChangedEvent
///
public string GetIdentityId()
{
return GetIdentityId(RefreshIdentityOptions.None);
}
private string GetIdentityId(RefreshIdentityOptions options)
{
// Locking so that concurrent calls do not make separate network calls,
// and instead wait for the first caller to cache the Identity ID.
lock (refreshIdLock)
{
if (!IsIdentitySet || options == RefreshIdentityOptions.Refresh)
{
_identityState = RefreshIdentity();
if (!string.IsNullOrEmpty(_identityState.LoginProvider))
{
Logins[_identityState.LoginProvider] = _identityState.LoginToken;
}
UpdateIdentity(_identityState.IdentityId);
}
}
return identityId;
}
///
/// Provides a way to override fetching the identity in case of developer authenticated identities.
/// The default behaviour will be using Cognito to retrieve the identity id.
///
/// returns a
protected virtual IdentityState RefreshIdentity()
{
bool isCached = true;
if (!IsIdentitySet)
{
var getIdRequest = new GetIdRequest
{
AccountId = AccountId,
IdentityPoolId = IdentityPoolId,
Logins = Logins
};
#if BCL || UNITY
var response = cib.GetId(getIdRequest);
#else
var response = Amazon.Runtime.Internal.Util.AsyncHelpers.RunSync(() => cib.GetIdAsync(getIdRequest));
#endif
isCached = false;
UpdateIdentity(response.IdentityId);
}
return new IdentityState(identityId, isCached);
}
#if AWS_ASYNC_API
///
/// Gets the Identity Id corresponding to the credentials retrieved from Cognito.
/// Note: this setting may change during execution. To be notified of its
/// new value, attach a listener to IdentityChangedEvent
///
public async System.Threading.Tasks.Task GetIdentityIdAsync()
{
return await GetIdentityIdAsync(RefreshIdentityOptions.None).ConfigureAwait(false);
}
private async System.Threading.Tasks.Task GetIdentityIdAsync(RefreshIdentityOptions options)
{
if (!IsIdentitySet || options == RefreshIdentityOptions.Refresh)
{
_identityState = await RefreshIdentityAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(_identityState.LoginProvider))
{
Logins[_identityState.LoginProvider] = _identityState.LoginToken;
}
UpdateIdentity(_identityState.IdentityId);
}
return identityId;
}
///
/// Provides a way to override fetching the identity in case of developer authenticated identities.
/// The default behaviour will be using Cognito to retrieve the identity id.
///
/// returns a
public virtual async System.Threading.Tasks.Task RefreshIdentityAsync()
{
bool isCached = true;
if (!IsIdentitySet)
{
var getIdRequest = new GetIdRequest
{
AccountId = AccountId,
IdentityPoolId = IdentityPoolId,
Logins = Logins
};
var response = await cib.GetIdAsync(getIdRequest).ConfigureAwait(false);
isCached = false;
UpdateIdentity(response.IdentityId);
}
return new IdentityState(identityId, isCached);
}
#endif
///
/// Checks the exception from a call that used an identity id and determines if the
/// failure was caused by a cached identity id. If it was determined then the cache
/// is cleared and true is return.
///
///
///
private bool ShouldRetry(AmazonCognitoIdentityException e)
{
if ((_identityState.LoginSpecified) &&
((e is NotAuthorizedException && e.Message.StartsWith("Access to Identity", StringComparison.OrdinalIgnoreCase)) ||
e is ResourceNotFoundException)
)
{
this.identityId = null;
this.ClearIdentityCache();
return true;
}
return false;
}
private EventHandler mIdentityChangedEvent;
///
/// Event for identity change notifications.
/// This event will fire whenever the Identity Id changes.
///
public event EventHandler IdentityChangedEvent
{
add
{
lock (this)
{
mIdentityChangedEvent += value;
}
}
remove
{
lock (this)
{
mIdentityChangedEvent -= value;
}
}
}
#endregion
#region Constructors
///
/// Constructs a new CognitoAWSCredentials instance, which will use the
/// specified Amazon Cognito identity pool to get short lived session credentials.
///
/// The Amazon Cogntio identity pool to use
/// Region to use when accessing Amazon Cognito and AWS Security Token Service.
public CognitoAWSCredentials(string identityPoolId, RegionEndpoint region)
: this(
accountId: null, identityPoolId: identityPoolId,
unAuthRoleArn: null, authRoleArn: null,
region: region)
{ }
///
/// Constructs a new CognitoAWSCredentials instance, which will use the
/// specified Amazon Cognito identity pool to make a requests to the
/// AWS Security Token Service (STS) to request short lived session credentials.
///
/// The AWS accountId for the account with Amazon Cognito
/// The Amazon Cogntio identity pool to use
/// The ARN of the IAM Role that will be assumed when unauthenticated
/// The ARN of the IAM Role that will be assumed when authenticated
/// Region to use when accessing Amazon Cognito and AWS Security Token Service.
public CognitoAWSCredentials(
string accountId, string identityPoolId,
string unAuthRoleArn, string authRoleArn,
RegionEndpoint region)
: this(
accountId, identityPoolId,
unAuthRoleArn, authRoleArn,
new AmazonCognitoIdentityClient(new AnonymousAWSCredentials(), region),
new AmazonSecurityTokenServiceClient(new AnonymousAWSCredentials(), region))
{ }
///
/// Constructs a new CognitoAWSCredentials instance, which will use the
/// specified Amazon Cognito identity pool to make a requests to the
/// AWS Security Token Service (STS) to request short lived session credentials.
///
/// The AWS accountId for the account with Amazon Cognito
/// The Amazon Cogntio identity pool to use
/// The ARN of the IAM Role that will be assumed when unauthenticated
/// The ARN of the IAM Role that will be assumed when authenticated
/// Preconfigured Cognito Identity client to make requests with
/// >Preconfigured STS client to make requests with
public CognitoAWSCredentials(
string accountId, string identityPoolId,
string unAuthRoleArn, string authRoleArn,
IAmazonCognitoIdentity cibClient, IAmazonSecurityTokenService stsClient)
{
if (string.IsNullOrEmpty(identityPoolId)) throw new ArgumentNullException("identityPoolId");
if (cibClient == null) throw new ArgumentNullException("cibClient");
if ((unAuthRoleArn != null || authRoleArn != null) && stsClient == null) throw new ArgumentNullException("stsClient");
AccountId = accountId;
IdentityPoolId = identityPoolId;
UnAuthRoleArn = unAuthRoleArn;
AuthRoleArn = authRoleArn;
Logins = new Dictionary(StringComparer.Ordinal);
#if !UNITY
cib = cibClient;
sts = stsClient;
#else
cib = (AmazonCognitoIdentityClient)cibClient;
sts = (AmazonSecurityTokenServiceClient)stsClient;
#endif
//check cache for identity id
string cachedIdentity = GetCachedIdentityId();
if (!string.IsNullOrEmpty(cachedIdentity))
{
UpdateIdentity(cachedIdentity);
//update the credentials from cache
currentState = GetCachedCredentials();
}
}
#endregion
#region Overrides
#if AWS_ASYNC_API
///
/// Retrieves credentials from Cognito Identity and optionally STS
///
///
protected override async System.Threading.Tasks.Task GenerateNewCredentialsAsync()
{
CredentialsRefreshState credentialsState;
// Pick role to use, depending on Logins
string roleArn = UnAuthRoleArn;
if (Logins.Count > 0)
roleArn = AuthRoleArn;
bool roleSpecified = !string.IsNullOrEmpty(roleArn);
// Get credentials from determined role or from identity pool
if (roleSpecified)
credentialsState = await GetCredentialsForRoleAsync(roleArn).ConfigureAwait(false);
else
credentialsState = await GetPoolCredentialsAsync().ConfigureAwait(false);
CacheCredentials(credentialsState);
return credentialsState;
}
private async System.Threading.Tasks.Task GetCredentialsForRoleAsync(string roleArn)
{
CredentialsRefreshState credentialsState;
// Retrieve Open Id Token
// (Reuses existing IdentityId or creates a new one)
var identity = await GetIdentityIdAsync(RefreshIdentityOptions.Refresh).ConfigureAwait(false);
var getTokenRequest = new GetOpenIdTokenRequest { IdentityId = identity };
// If logins are set, pass them to the GetOpenId call
if (Logins.Count > 0)
getTokenRequest.Logins = Logins;
bool retry = false;
GetOpenIdTokenResponse getTokenResult = null;
try
{
getTokenResult = await cib.GetOpenIdTokenAsync(getTokenRequest).ConfigureAwait(false);
}
catch (AmazonCognitoIdentityException e)
{
if (ShouldRetry(e))
retry = true;
else
throw;
}
if (retry)
{
return await GetCredentialsForRoleAsync(roleArn).ConfigureAwait(false);
}
string token = getTokenResult.Token;
// IdentityId may have changed, save the new value
UpdateIdentity(getTokenResult.IdentityId);
// Assume role with Open Id Token
var assumeRequest = new AssumeRoleWithWebIdentityRequest
{
WebIdentityToken = token,
RoleArn = roleArn,
RoleSessionName = "NetProviderSession",
DurationSeconds = DefaultDurationSeconds
};
var credentials = (await sts.AssumeRoleWithWebIdentityAsync(assumeRequest).ConfigureAwait(false)).Credentials;
// Return new refresh state (credentials and expiration)
credentialsState = new CredentialsRefreshState(credentials.GetCredentials(), credentials.Expiration);
return credentialsState;
}
// Retrieves credentials for the roles defined on the identity pool
private async System.Threading.Tasks.Task GetPoolCredentialsAsync()
{
CredentialsRefreshState credentialsState;
var identity = await GetIdentityIdAsync(RefreshIdentityOptions.Refresh).ConfigureAwait(false);
var getCredentialsRequest = new GetCredentialsForIdentityRequest { IdentityId = identity };
if (Logins.Count > 0)
getCredentialsRequest.Logins = Logins;
if (_identityState != null && !string.IsNullOrEmpty(_identityState.LoginToken))
{
getCredentialsRequest.Logins = new Dictionary();
getCredentialsRequest.Logins.Add("cognito-identity.amazonaws.com", _identityState.LoginToken);
}
bool retry = false;
GetCredentialsForIdentityResponse response = null;
try
{
response = (await cib.GetCredentialsForIdentityAsync(getCredentialsRequest).ConfigureAwait(false));
// IdentityId may have changed, save the new value
UpdateIdentity(response.IdentityId);
}
catch (AmazonCognitoIdentityException e)
{
if (ShouldRetry(e))
retry = true;
else
throw;
}
if (retry)
{
return await GetPoolCredentialsAsync().ConfigureAwait(false);
}
var credentials = response.Credentials;
credentialsState = new CredentialsRefreshState(credentials.GetCredentials(), credentials.Expiration);
return credentialsState;
}
#endif
///
/// Retrieves credentials from Cognito Identity and optionally STS
///
///
protected override CredentialsRefreshState GenerateNewCredentials()
{
CredentialsRefreshState credentialsState;
// Pick role to use, depending on Logins
string roleArn = UnAuthRoleArn;
if (Logins.Count > 0)
roleArn = AuthRoleArn;
bool roleSpecified = !string.IsNullOrEmpty(roleArn);
// Get credentials from determined role or from identity pool
if (roleSpecified)
credentialsState = GetCredentialsForRole(roleArn);
else
credentialsState = GetPoolCredentials();
CacheCredentials(credentialsState);
// Return new refresh state (credentials and expiration)
return credentialsState;
}
// Retrieves credentials for the roles defined on the identity pool
private CredentialsRefreshState GetPoolCredentials()
{
CredentialsRefreshState credentialsState;
var identity = this.GetIdentityId(RefreshIdentityOptions.Refresh);
var getCredentialsRequest = new GetCredentialsForIdentityRequest { IdentityId = identity };
if (Logins.Count > 0)
getCredentialsRequest.Logins = Logins;
//incase its BYOI provider override the logins dictionary with the new instance and set the values for cognito-identity provider
if (_identityState != null && !string.IsNullOrEmpty(_identityState.LoginToken))
{
getCredentialsRequest.Logins = new Dictionary();
getCredentialsRequest.Logins["cognito-identity.amazonaws.com"] = _identityState.LoginToken;
}
bool retry = false;
GetCredentialsForIdentityResponse response = null;
try
{
response = GetCredentialsForIdentity(getCredentialsRequest);
}
catch (AmazonCognitoIdentityException e)
{
if (ShouldRetry(e))
retry = true;
else
throw;
}
if (retry)
{
return GetPoolCredentials();
}
// IdentityId may have changed, save the new value
UpdateIdentity(response.IdentityId);
var credentials = response.Credentials;
credentialsState = new CredentialsRefreshState(credentials.GetCredentials(), credentials.Expiration);
return credentialsState;
}
// Retrieves credentials for the specific role, by making a call to STS
private CredentialsRefreshState GetCredentialsForRole(string roleArn)
{
CredentialsRefreshState credentialsState;
// Retrieve Open Id Token
// (Reuses existing IdentityId or creates a new one)
var identity = this.GetIdentityId(RefreshIdentityOptions.Refresh);
var getTokenRequest = new GetOpenIdTokenRequest { IdentityId = identity };
// If logins are set, pass them to the GetOpenId call
if (Logins.Count > 0)
getTokenRequest.Logins = Logins;
bool retry = false;
GetOpenIdTokenResponse getTokenResult = null;
try
{
getTokenResult = GetOpenId(getTokenRequest);
}
catch (AmazonCognitoIdentityException e)
{
if (ShouldRetry(e))
retry = true;
else
throw;
}
if (retry)
{
return GetCredentialsForRole(roleArn);
}
string token = getTokenResult.Token;
// IdentityId may have changed, save the new value
UpdateIdentity(getTokenResult.IdentityId);
// Assume role with Open Id Token
var assumeRequest = new AssumeRoleWithWebIdentityRequest
{
WebIdentityToken = token,
RoleArn = roleArn,
RoleSessionName = "NetProviderSession",
DurationSeconds = DefaultDurationSeconds
};
var credentials = GetStsCredentials(assumeRequest);
credentialsState = new CredentialsRefreshState(credentials.GetCredentials(), credentials.Expiration);
return credentialsState;
}
#endregion
}
}