/** * Copyright 2011-2014 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. * You may obtain a copy of the License at: * * http://aws.amazon.com/apache2.0 * * 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. */ package com.amazonaws.auth; import com.amazonaws.AmazonServiceException; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.ClientConfiguration; import com.amazonaws.SDKGlobalConfiguration; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; import com.amazonaws.services.cognitoidentity.AmazonCognitoIdentity; import com.amazonaws.services.cognitoidentity.AmazonCognitoIdentityClient; import com.amazonaws.services.cognitoidentity.model.GetCredentialsForIdentityRequest; import com.amazonaws.services.cognitoidentity.model.GetCredentialsForIdentityResult; import com.amazonaws.services.cognitoidentity.model.ResourceNotFoundException; import com.amazonaws.services.securitytoken.AWSSecurityTokenService; import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient; import com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest; import com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityResult; import com.amazonaws.services.securitytoken.model.Credentials; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * AWSCredentialsProvider implementation that uses the Amazon Cognito Identity * service and AWS Security Token Service to create temporary, short-lived * sessions to use for authentication */ public class CognitoCredentialsProvider implements AWSCredentialsProvider { /** Used in the enhanced get credentials flow */ private AmazonCognitoIdentity cib; /** Handle the identity-specific interactions */ private final AWSCognitoIdentityProvider identityProvider; /** Default duration for started sessions */ public static final int DEFAULT_DURATION_SECONDS = 3600; /** Default threshold for refreshing session credentials */ public static final int DEFAULT_THRESHOLD_SECONDS = 500; /** The current session credentials */ protected AWSSessionCredentials sessionCredentials; /** The expiration time for the current session credentials */ protected Date sessionCredentialsExpiration; /** The current Token */ protected String token; /** * The client for starting STS sessions, used in the basic (non-enhanced * flow) */ protected AWSSecurityTokenService securityTokenService; protected int sessionDuration; protected int refreshThreshold; protected String unauthRoleArn; protected String authRoleArn; protected String customRoleArn; protected boolean useEnhancedFlow; /** * Constructs a new {@link CognitoCredentialsProvider}, which will use the * specified Amazon Cognito identity pool to make a request, using the basic * authentication flow, to the AWS Security Token Service (STS) to request * short-lived session credentials, which will then be returned by this * class's {@link #getCredentials()} method. * * @param accountId The AWS accountId for the account with Amazon Cognito * @param identityPoolId The Amazon Cogntio identity pool to use * @param unauthRoleArn The ARN of the IAM Role that will be assumed when * unauthenticated * @param authRoleArn The ARN of the IAM Role that will be assumed when * authenticated * @param region The region to use when contacting Cognito Identity */ public CognitoCredentialsProvider(String accountId, String identityPoolId, String unauthRoleArn, String authRoleArn, Regions region) { this(accountId, identityPoolId, unauthRoleArn, authRoleArn, region, new ClientConfiguration()); } /** * Constructs a new {@link CognitoCredentialsProvider}, which will use the * specified Amazon Cognito identity pool to make a request, using the basic * authentication flow, to the AWS Security Token Service (STS) to request * short-lived session credentials, which will then be returned by this * class's {@link #getCredentials()} method. * <p> * This version of the constructor allows you to specify a client * configuration for the Amazon Cognito and STS clients. * </p> * * @param accountId The AWS accountId for the account with Amazon Cognito * @param identityPoolId The Amazon Cognito identity pool to use * @param unauthRoleArn The ARN of the IAM Role that will be assumed when * unauthenticated * @param authRoleArn The ARN of the IAM Role that will be assumed when * authenticated * @param region The region to use when contacting Cognito Identity * @param clientConfiguration Configuration to apply to service clients * created */ public CognitoCredentialsProvider(String accountId, String identityPoolId, String unauthRoleArn, String authRoleArn, Regions region, ClientConfiguration clientConfiguration) { this(accountId, identityPoolId, unauthRoleArn, authRoleArn, new AmazonCognitoIdentityClient(new AnonymousAWSCredentials(), clientConfiguration), (unauthRoleArn == null && authRoleArn == null) ? null : new AWSSecurityTokenServiceClient(new AnonymousAWSCredentials(), clientConfiguration)); this.cib.setRegion(Region.getRegion(region)); } /** * Constructs a new {@link CognitoCredentialsProvider}, which will use the * specified Amazon Cognito identity pool to make a request to Cognito, * using the enhanced flow, to get short lived session credentials, which * will then be returned by this class's {@link #getCredentials()} method. * * @param identityPoolId The Amazon Cognito identity pool to use * @param region The region to use when contacting Cognito Identity */ public CognitoCredentialsProvider(String identityPoolId, Regions region) { this(null, identityPoolId, null, null, region, new ClientConfiguration()); } /** * Constructs a new {@link CognitoCredentialsProvider}, which will use the * specified Amazon Cognito identity pool to make a request to Cognito, * using the enhanced flow, to get short lived session credentials, which * will then be returned by this class's {@link #getCredentials()} method. * <p> * This version of the constructor allows you to specify a client * configuration for the Amazon Cognito client. * </p> * * @param identityPoolId The Amazon Cognito identity pool to use * @param region The region to use when contacting Cognito Identity * @param clientConfiguration Configuration to apply to service clients * created */ public CognitoCredentialsProvider(String identityPoolId, Regions region, ClientConfiguration clientConfiguration) { this(null, identityPoolId, null, null, region, clientConfiguration); } /** * Constructs a new {@link CognitoCredentialsProvider}, which will use the * specified Amazon Cognito identity pool to make a request to the AWS * Security Token Service (STS) to get short-lived session credentials, * which will then be returned by this class's {@link #getCredentials()} * method. * <p> * This version of the constructor allows you to specify the Amazon Cognito * and STS client to use. * </p> * <p> * Set the roles and stsClient to null to use the enhanced authentication * flow, not contacting STS. Otherwise the basic flow will be used. * </p> * * @param accountId The AWS accountId for the account with Amazon Cognito * @param identityPoolId The Amazon Cogntio identity pool to use * @param unauthRoleArn The ARN of the IAM Role that will be assumed when * unauthenticated * @param authRoleArn The ARN of the IAM Role that will be assumed when * authenticated * @param cibClient Preconfigured CognitoIdentity client to make requests * with * @param stsClient Preconfigured STS client to make requests with */ public CognitoCredentialsProvider(String accountId, String identityPoolId, String unauthRoleArn, String authRoleArn, AmazonCognitoIdentityClient cibClient, AWSSecurityTokenService stsClient) { // No need to specify parameters for Region and ClientConfig because we // don't create the clients this.cib = cibClient; this.securityTokenService = stsClient; this.unauthRoleArn = unauthRoleArn; this.authRoleArn = authRoleArn; this.sessionDuration = DEFAULT_DURATION_SECONDS; this.refreshThreshold = DEFAULT_THRESHOLD_SECONDS; this.useEnhancedFlow = (unauthRoleArn == null && authRoleArn == null); if (this.useEnhancedFlow) { this.identityProvider = new AWSEnhancedCognitoIdentityProvider(accountId, identityPoolId, cibClient); } else { this.identityProvider = new AWSBasicCognitoIdentityProvider(accountId, identityPoolId, cibClient); } } /** * Constructs a new CognitoCredentialsProvider, which will set up a link to * the provider passed in using the basic authentication flow to get get * short-lived credentials from STS, which can be retrieved from * {@link #getCredentials()} * <p> * This version of the constructor allows you to specify your own Identity * Provider class. * </p> * * @param provider a reference to the provider in question, including what's * needed to interact with it to later connect with STS * @param unauthArn the unauthArn, for use with the STS call * @param authArn the authArn, for use with the STS call */ public CognitoCredentialsProvider(AWSCognitoIdentityProvider provider, String unauthArn, String authArn) { this(provider, unauthArn, authArn, new AWSSecurityTokenServiceClient( new AnonymousAWSCredentials(), new ClientConfiguration())); } /** * Constructs a new CognitoCredentialsProvider, which will set up a link to * the provider passed in to use the basic authentication flow to get * short-lived credentials from STS, which can be retrieved from * {@link #getCredentials()} * <p> * This version of the constructor allows you to specify your own Identity * Provider class, and the STS client to use. * </p> * * @param provider a reference to the provider in question, including what's * needed to interact with it to later connect with STS * @param unauthArn the unauthArn, for use with the STS call * @param authArn the authArn, for use with the STS call * @param stsClient the sts endpoint to get session credentials from */ public CognitoCredentialsProvider(AWSCognitoIdentityProvider provider, String unauthArn, String authArn, AWSSecurityTokenService stsClient) { this.identityProvider = provider; this.unauthRoleArn = unauthArn; this.authRoleArn = authArn; this.securityTokenService = stsClient; this.sessionDuration = DEFAULT_DURATION_SECONDS; this.refreshThreshold = DEFAULT_THRESHOLD_SECONDS; this.useEnhancedFlow = false; } /** * Constructs a new CognitoCredentialsProvider, which will set up a link to * the provider passed in using the enhanced authentication flow to get * short-lived credentials from Amazon Cognito, which can be retrieved from * {@link #getCredentials()} * <p> * This version of the constructor allows you to specify your own Identity * Provider class. * </p> * * @param provider a reference to the provider in question, including what's * needed to interact with it to later connect with Amazon * Cognito * @param region The region to use when contacting Cognito */ public CognitoCredentialsProvider(AWSCognitoIdentityProvider provider, Regions region) { this(provider, region, new ClientConfiguration()); } /** * Constructs a new CognitoCredentialsProvider, which will set up a link to * the provider passed in using the enhanced authentication flow to get * short-lived credentials from Amazon Cognito, which can be retrieved from * {@link #getCredentials()} * <p> * This version of the constructor allows you to specify your own Identity * Provider class and the configuration for the Amazon Cognito client. * </p> * * @param provider a reference to the provider in question, including what's * needed to interact with it to later connect with Amazon * Cognito * @param clientConfiguration Configuration to apply to service clients * created * @param region The region to use when contacting Cognito Identity */ public CognitoCredentialsProvider(AWSCognitoIdentityProvider provider, Regions region, ClientConfiguration clientConfiguration) { this(provider, new AmazonCognitoIdentityClient( new AnonymousAWSCredentials(), clientConfiguration)); this.cib.setRegion(Region.getRegion(region)); } /** * Constructs a new CognitoCredentialsProvider, which will set up a link to * the provider passed in using the enhanced authentication flow to get * short-lived credentials from Amazon Cognito, which can be retrieved from * {@link #getCredentials()} * <p> * This version of the constructor allows you to specify your own Identity * Provider class and the Amazon Cognito client. * </p> * * @param provider a reference to the provider in question, including what's * needed to interact with it to later connect with Amazon * Cognito * @param cibClient Preconfigured CognitoIdentity client to make requests * with * @param region The region to use when contacting Cognito Identity */ public CognitoCredentialsProvider(AWSCognitoIdentityProvider provider, AmazonCognitoIdentityClient cib) { this.cib = cib; this.identityProvider = provider; this.unauthRoleArn = null; this.authRoleArn = null; this.securityTokenService = null; this.sessionDuration = DEFAULT_DURATION_SECONDS; this.refreshThreshold = DEFAULT_THRESHOLD_SECONDS; this.useEnhancedFlow = true; } public String getIdentityId() { return identityProvider.getIdentityId(); } public String getToken() { return identityProvider.getToken(); } public AWSIdentityProvider getIdentityProvider() { return identityProvider; } public void setSessionCredentialsExpiration(Date expiration) { sessionCredentialsExpiration = expiration; } public Date getSessionCredentitalsExpiration() { return sessionCredentialsExpiration; } public String getIdentityPoolId() { return identityProvider.getIdentityPoolId(); } /** * If the current session has expired/credentials are invalid, a new session * is started, establishing the credentials. In either case, those * credentials are returned */ @Override public AWSSessionCredentials getCredentials() { if (needsNewSession()) { startSession(); } return sessionCredentials; } /** * Set the duration of the session credentials created by this client in * seconds. Values must be supported by AssumeRoleWithWebIdentityRequest. * * @see com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest * @param sessionDuration The new duration for session credentials created * by this provider */ public void setSessionDuration(int sessionDuration) { this.sessionDuration = sessionDuration; } /** * Set the duration of the session credentials created by this client in * seconds. Values must be supported by AssumeRoleWithWebIdentityRequest. * Returns reference to object so methods can be chained together. * * @see com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest * @param sessionDuration The new duration for session credentials created * by this provider * @return A reference to this updated object so that method calls can be * chained together. */ public CognitoCredentialsProvider withSessionDuration(int sessionDuration) { this.setSessionDuration(sessionDuration); return this; } /** * Get the duration of the session credentials created by this client in * seconds. Values must be supported by AssumeRoleWithWebIdentityRequest. * * @see com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest * @return The duration for session credentials created by this provider */ public int getSessionDuration() { return this.sessionDuration; } /** * Set the refresh threshold for the session credentials created by this * client in seconds. This value will be used internally to determine if new * credentials should be fetched from STS. * * @see com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest * @param refreshThreshold The new refresh threshold for session credentials * created by this provider */ public void setRefreshThreshold(int refreshThreshold) { this.refreshThreshold = refreshThreshold; } /** * Set the refresh threshold for the session credentials created by this * client in seconds. This value will be used internally to determine if new * credentials should be fetched from STS. Returns a reference to the object * so methods can be chained. * * @see com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest * @param refreshThreshold The new refresh threshold for session credentials * created by this provider * @return A reference to this updated object so that method calls can be * chained together. */ public CognitoCredentialsProvider withRefreshThreshold(int refreshThreshold) { this.setRefreshThreshold(refreshThreshold); return this; } /** * Get the refresh threshold for the session credentials created by this * client in seconds. This value will be used internally to determine if new * credentials should be fetched from STS. * * @see com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest * @return The refresh threshold for session credentials created by this * provider */ public int getRefreshThreshold() { return this.refreshThreshold; } protected void setIdentityId(String identityId) { identityProvider.identityChanged(identityId); } /** * Set the logins map used to authenticated with Amazon Cognito. Note: You * should manually call refresh on on the credentials provider after adding * logins to the provider as your Identity Id may have changed. * * @param logins The new logins map (providerName, providerToken) to use to * communicate with Amazon Cognito */ public void setLogins(Map<String, String> logins) { identityProvider.setLogins(logins); clearCredentials(); } /** * Get the custom role arn associated with the credentials provider. * * @return Custom role arn. */ public String getCustomRoleArn() { return customRoleArn; } /** * Set the custom role arn that will be used to get credentials with Amazon * Cognito. This parameter needs to be set when idp provides roles in the * token (eg: SAML Assertion) and there are multiple roles. Roles set by the * method will be assumed when it matches with the roles received in the * token from IdP. * * @param customRoleArn The role arn to be used to get the credentials. */ public void setCustomRoleArn(String customRoleArn) { this.customRoleArn = customRoleArn; } /** * Set the logins map used to authenticated with Amazon Cognito. Returns a * reference to the object so methods can be chained. Note: You should * manually call refresh on on the credentials provider after adding logins * to the provider as your Identity Id may have changed. * * @see com.amazonaws.services.securitytoken.model.AssumeRoleWithWebIdentityRequest * @param logins The new logins map (providerName, providerToken) to use to * communicate with Amazon Cognito * @return A reference to this updated object so that method calls can be * chained together. */ public AWSCredentialsProvider withLogins(Map<String, String> logins) { this.setLogins(logins); return this; } /** * Get the logins map used to authenticated with Amazon Cognito * * @return The logins map (providerName, providerToken) to use to * communicate with Amazon Cognito */ public Map<String, String> getLogins() { return identityProvider.getLogins(); } @Override public void refresh() { startSession(); } /** * Clear all in-memory and saved state for the credentials provider. Will * destroy any saved Amazon Cognito Identity Id and associated AWS * credentials. */ public void clear() { clearCredentials(); setIdentityId(null); identityProvider.setLogins(new HashMap<String, String>()); } /** * Clear credentials. This will destroy all the saved AWS credentials but * not the identity Id. */ public void clearCredentials() { sessionCredentials = null; sessionCredentialsExpiration = null; } /** * Starts a new session by getting short lived session credentials. */ protected void startSession() { // make sure we have an identityId. In the case of cognito identity, // the try catch will handle a deleted or corrupted id. // Developer authenticated won't throw amazon exceptions, // and for 2hop, it will be handled below, as the getId call // won't fail since it is set. try { token = identityProvider.refresh(); } catch (ResourceNotFoundException rnfe) { // If the identity id or identity pool is non-existant, this is // thrown token = retryRefresh(); } catch (AmazonServiceException ase) { // If it's a corrupt id, then a validation exception is thrown if (ase.getErrorCode().equals("ValidationException")) { token = retryRefresh(); } else { throw ase; } } if (useEnhancedFlow) { populateCredentialsWithCognito(token); } else { populateCredentialsWithSts(token); } } /** * To be used to call the provider back end to get identifiers. * Specifically, this is the helper that handles the case for when a refresh * call ran into the corrupt identity id case, either a deleted id or a * malformed id. If that happens, this is called, a new id and token are * fetched, and the process is resumed. * * @return the new token gotten by the service */ private String retryRefresh() { // Ensure we get a new id and token setIdentityId(null); token = identityProvider.refresh(); return token; } /** * To be used to help the calling of the 2hop flow in event of the identity * id being either missing or deleted. Once that is caught as having * happened, this call is made, which will clear the old id, get a new * one/token, and get the flow going back to where it was with a new request * * @return the result of the new request */ private GetCredentialsForIdentityResult retryGetCredentialsForIdentity() { token = retryRefresh(); Map<String, String> logins; if (token != null && !token.isEmpty()) { logins = new HashMap<String, String>(); logins.put("cognito-identity.amazonaws.com", token); } else { logins = getLogins(); } GetCredentialsForIdentityRequest request = new GetCredentialsForIdentityRequest() .withIdentityId(getIdentityId()) .withLogins(logins).withCustomRoleArn(customRoleArn); return cib.getCredentialsForIdentity(request); } /** * Gets the session credentials from Amazon Cognito. */ private void populateCredentialsWithCognito(String token) { // For Cognito-authenticated identities token will always be null, but // for developer-authenticated identities, refresh() may return a token // that the the developer backend has received from Cognito and we have // to send back in our request. Map<String, String> logins; if (token != null && !token.isEmpty()) { logins = new HashMap<String, String>(); logins.put("cognito-identity.amazonaws.com", token); } else { logins = getLogins(); } GetCredentialsForIdentityRequest request = new GetCredentialsForIdentityRequest() .withIdentityId(getIdentityId()) .withLogins(logins).withCustomRoleArn(customRoleArn); GetCredentialsForIdentityResult result = null; try { result = cib.getCredentialsForIdentity(request); } catch (ResourceNotFoundException rnfe) { // If the identity id or identity pool is non-existant, this is // thrown result = retryGetCredentialsForIdentity(); } catch (AmazonServiceException ase) { // If it's a corrupt id, then a validation exception is thrown if (ase.getErrorCode().equals("ValidationException")) { result = retryGetCredentialsForIdentity(); } else { throw ase; } } com.amazonaws.services.cognitoidentity.model.Credentials credentials = result .getCredentials(); sessionCredentials = new BasicSessionCredentials(credentials.getAccessKeyId(), credentials.getSecretKey(), credentials.getSessionToken()); sessionCredentialsExpiration = credentials.getExpiration(); if (!result.getIdentityId().equals(getIdentityId())) { setIdentityId(result.getIdentityId()); } } /** * Gets the session credentials by requesting an OpenId Connect token from * Amazon Cognito and then trading it with AWS Secure Token Service for the * short lived session credentials. */ private void populateCredentialsWithSts(String token) { boolean isAuthenticated = identityProvider.isAuthenticated(); String roleArn = (isAuthenticated) ? authRoleArn : unauthRoleArn; AssumeRoleWithWebIdentityRequest sessionTokenRequest = new AssumeRoleWithWebIdentityRequest() .withWebIdentityToken(token) .withRoleArn(roleArn) .withRoleSessionName("ProviderSession") .withDurationSeconds(sessionDuration); appendUserAgent(sessionTokenRequest, getUserAgent()); AssumeRoleWithWebIdentityResult sessionTokenResult = securityTokenService .assumeRoleWithWebIdentity(sessionTokenRequest); Credentials stsCredentials = sessionTokenResult.getCredentials(); sessionCredentials = new BasicSessionCredentials( stsCredentials.getAccessKeyId(), stsCredentials.getSecretAccessKey(), stsCredentials.getSessionToken()); sessionCredentialsExpiration = stsCredentials.getExpiration(); } /** * Returns true if a new STS session needs to be started. A new STS session * is needed when no session has been started yet, or if the last session is * within the configured refresh threshold. * * @return True if a new STS session needs to be started. */ protected boolean needsNewSession() { if (sessionCredentials == null) { return true; } long currentTime = System.currentTimeMillis() - SDKGlobalConfiguration.getGlobalTimeOffset() * 1000; long timeRemaining = sessionCredentialsExpiration.getTime() - currentTime; return timeRemaining < (refreshThreshold * 1000); } /** * Append user agent string to the request. The final string is what is set * in the ClientCofniguration concatenated with the given userAgent string. * * @param request the request object to be appended * @param userAgent additional user agent string to append */ private void appendUserAgent(AmazonWebServiceRequest request, String userAgent) { request.getRequestClientOptions().appendUserAgent(userAgent); } /** * Gets the user agent string to append to all requests made by this * provider. Default is an empty string. */ protected String getUserAgent() { return ""; } /** * Adds a new identity changed listener to process some event when the * identity has changed. * * @param listener the listener to be triggered on id change */ public void registerIdentityChangedListener(IdentityChangedListener listener) { identityProvider.registerIdentityChangedListener(listener); } /** * Removes an identity changed listener from being triggered when the * identity has changed. * * @param listener the listener to be removed */ public void unregisterIdentityChangedListener(IdentityChangedListener listener) { identityProvider.unregisterIdentityChangedListener(listener); } }