/* * 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.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; private IAmazonCognitoIdentity cib; private IAmazonSecurityTokenService sts; 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 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); cib = cibClient; sts = stsClient; //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 } }