AWS Amplify Authentication module has some methods for actions like sign in, sign up, forgot password etc. Even one can let the user to update his/her info through like:
import { Auth } from 'aws-amplify'
// Auth API Sign-in sample
Auth.signIn(username, password)
.then(user => console.log(user))
.catch(err => console.log(err))
// Auth API Change info sample
let result = await Auth.updateUserAttributes(user, {
'email': 'me#anotherdomain.com',
'family_name': 'Lastname'
})
However, I could not see anyway to disable (beware, not to delete) an account.
So, a user can sign up to a web application, but cannot deactivate it using AWS Amplify? If not, are there any other ways for disabling an AWS Cognito User Pool user via Javascript code?
I went through the AWS Documentation for the Cognito User Pools API and found some methods that let a function with admin privileges disable(not delete) a Cognito User Pool account!
Here is the link to the documentation on the AWS website:
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminDisableUser.html
There is also a method to reenable the user:
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminEnableUser.html
The javascript implementation for this can be found here:
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html#adminDisableUser-property
The code would be something like this using the AWS SDK for JS:
var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});
var params = {
UserPoolId: 'STRING_VALUE', /* required */
Username: 'STRING_VALUE' /* required */
};
cognitoidentityserviceprovider.adminDisableUser(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
The goal is to implement a social provider auth flow as described in User Pools App Integration and Federation.
One important thing that I want to satisfy, is to merge user pool accounts that have the same email address.
I am accomplishing that by calling adminLinkProviderForUser within the PreSignUp_ExternalProvider cognito lambda trigger.
So with this, everything works. The new social provided user is being registered and linked with the already existing Cognito (user+pass) user.
However, the authentication flow, from user's perspective doesn't complete. It fails at the last step where the callback uri (defined in cognito user pool) is being called:
error: invalid_request
error_description: Already found an entry for username Facebook_10155611263152353
But then, if the user retries the social auth flow, everything works, and would get session tokens that represent the original Cognito User Pool user (the one that already had that email).
Note that I'm testing the auth flow on an empty User Pool, zero user accounts.
For all the poor souls fighting with this issue still in 2020 the same way I did:
I have eventually fixed the issue by catching the "Already found an entry for username" in my client application and repeating the entire auth flow once more.
Luckily the error only gets fired on the initial external provider signup but not in the subsequent signins of the same user (cause it happens during signup trigger, duh).
I'm taking a wild guess, but here is what I think is happening:
In my case, the facebook provider was getting succesfully linked with
the pre-existing cognito email/password user. new Facebook userpool
entry linking to the email/password user was succesfully created.
Still, it seems
like cognito tried to register the fully isolated Facebook_id user
during the internal signup process (even though a link user entry with the same username was already created in the previous step). Since the "link user" with the
username Facebook_id was already existing, cognito threw an
"Already found an entry for username Facebook_id error" internal error.
This error has been repeatedly voiced over to the AWS developers since 2017 and there are even some responses of them working on it, but in 2020, it's still not fixed.
Yes, this is how it is currently setup. If you try to link users using PreSignUp trigger, the first time won't work. A better way to handle this(I think) would be to provide an option in your UI to link external accounts on sign-in. In the pre-signup trigger, search for a user with the same unique attribute (say email) and see if the sign up is from external provider. Then show a message such as email already exists. Login in & use this menu/option to link. Haven't tested this though.
To elaborate on #agent420's answer, this is what I am currently using (Typescript example).
When a social identity attempts to sign up and the email address already exists I catch this using the PreSignUp trigger and then return an error message to the user. Inside the app, on the user's profile page, there is an option to link an identity provider which calls the adminLinkProviderForUser API.
import {
Context,
CognitoUserPoolTriggerEvent,
CognitoUserPoolTriggerHandler,
} from 'aws-lambda';
import * as aws from 'aws-sdk';
import { noTryAsync } from 'no-try';
export const handle: CognitoUserPoolTriggerHandler = async (
event: CognitoUserPoolTriggerEvent,
context: Context,
callback: (err, event: CognitoUserPoolTriggerEvent) => void,
): Promise<any> => {
context.callbackWaitsForEmptyEventLoop = false;
const { email } = event.request.userAttributes;
// pre sign up with external provider
if (event.triggerSource === 'PreSignUp_ExternalProvider') {
// check if a user with the email address already exists
const sp = new aws.CognitoIdentityServiceProvider();
const { error } = await noTryAsync(() =>
sp
.adminGetUser({
UserPoolId: 'your-user-pool-id',
Username: email,
})
.promise(),
);
if (error && !(error instanceof aws.AWSError)) {
throw error;
} else if (error instanceof aws.AWSError && error.code !== 'UserNotFoundException') {
throw error;
}
}
callback(null, event);
};
I finally got this thing working in a non-weird way where users have to authorize twice or other things.
Process explained:
User tries to authenticate using an identity provider, for the first time => PreSignUp lambda kicks in and check if user exists via email
1a. If the user exists, it will throw an error, eg. CONFIRM_IDENTITY_LINK_token that I'm capturing on the client.
token is a base64 string with the username and identity id ("username:facebook_123456")
1b. If the username does not exist, I create a new user with a temporary password and throw an error FORCE_CHANGE_PASSWORD_token. Same token but I add the temporary password to this time.
In the client I have one callback route '/authorize' => this is the one you set up as a callback URL in Cognito, and 2 extra routes: '/confirm-password' and '/configure-password'.
In the /authorize route I'm capturing the errors and getting the attached tokens and redirect to the extra routes: 1a => /configure-password?token=token and 1b => /confirm-password?token=token
For "/confirm-password" I ask the user to confirm its current password in order to authorize linking with the provider, then use the token to log him in with the identity id as clientMetadata, eg "{"LINK_PROVIDER": "facebbok_12345678"}"
On login, I have a PostAuthentication lambda which checks for the "LINK_PROVIDER" in the clientMetadata, and links it to the user.
For "/configure-password" I parse the token and do a "shallow" login with the credentials from the token and identity id as client metadata (same as above) then prompt the user to configure a new password for his account.
I know it might seem a little bit restrictive but I find it better than to authorize twice.
Also, this does not create extra users for identities in the user pool.
Code examples:
PreSignUp lambda
export async function handler(event: PreSignUpTriggerEvent) {
try {
const { userPoolId, triggerSource, request, userName } = event
if (triggerSource === 'PreSignUp_ExternalProvider') {
// Check if user exists in cognito
let currentUser = await getUserByEmail(userPoolId, request.userAttributes.email)
if (currentUser) {
// User exists, thow error with identity id
const identity = Buffer.from(`${currentUser}:${userName}`).toString('base64')
throw new Error(`CONFIRM_USER_IDENTITY_${identity}`)
}
// Create new Cognito user with temp password
const tempPassword = generatePassword()
currentUser = await createNewUser(userPoolId, request.userAttributes, tempPassword)
// Throw error with token
const state = Buffer.from(`${currentUser}:${tempPassword}:${userName}`).toString('base64')
throw new Error(`FORCE_CHANGE_PASSWORD_${state}`)
}
return event
} catch (error) {
throw new Error(error)
}
}
PostAuthentication lambda
export async function handler(event: PostAuthenticationTriggerEvent) {
try {
const { userPoolId, request, userName } = event
if (request.clientMetadata?.LINK_IDENTITY) {
const identity = request.clientMetadata['LINK_IDENTITY']
// Link identity to user
await linkIdentityProvider(userPoolId, userName, identity)
}
return event
} catch (error) {
console.error(error)
throw new Error('Internal server error')
}
}
We faced the same issue and tried various hacks to get around it. As we started to use SignInWithApple, we couldn't handle it with the 'double turnaround' because Apple always wants the user to enter their email and password, not like Google, where the second time, everything works automatically. So the solution we ended up building was to store the Cognito/IdP ID (Google_1234, SignInWithApple_XXXX.XXX.XXX) in our database but still create a native Cognito user that isn't linked via Cognito.
The native user is created to make unlinking easier because first, we get rid of the data (IdP user-id) we store in our database and then the Cognito IdP user. The user can then proceed using the Native Cognito user. Then we have a middleware component in place that allows us to have JWT in the external IdP or Cognito native format and translates so we can use both versions. As long as the user uses an IdP/SSO, we reset the Native users' password to a very long random value and prevent resetting it, so they must use the IdP.
So whatever you are trying to do, prevent using the admin-link-provider-for-user command!
The same code in JavaScript getUser has been called instead of listUsers. It is also assumed that all users have their email id as their username.
const aws = require('aws-sdk');
exports.handler = async (event, context, callback) => {
console.log("event" + JSON.stringify(event));
const cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});
const emailId = event.request.userAttributes.email
const userName = event.userName
const userPoolId = event.userPoolId
var params = {
UserPoolId: userPoolId,
Username: userName
};
var createUserParams = {
UserPoolId: userPoolId,
Username: emailId,
UserAttributes: [
{
Name: "email",
Value: emailId
},
],
TemporaryPassword: "xxxxxxxxx"
};
var googleUserNameSplitArr = userName.split("_");
var adminLinkUserParams = {
DestinationUser: {
ProviderAttributeName: 'UserName',
ProviderAttributeValue: emailId,
ProviderName: "Cognito"
},
SourceUser: {
ProviderAttributeName: "Cognito_Subject",
ProviderAttributeValue: googleUserNameSplitArr[1],
ProviderName: 'Google'
},
UserPoolId: userPoolId
};
var addUserToGroupParams = {
GroupName: "Student",
UserPoolId: userPoolId,
Username: emailId
};
if (userName.startsWith("Google_")) {
await cognitoidentityserviceprovider.adminGetUser(params, function (err, data) {
if (err) {
console.log("No user present")
console.log(err, err.stack);
cognitoidentityserviceprovider.adminCreateUser(createUserParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("User Created ")
cognitoidentityserviceprovider.adminAddUserToGroup(addUserToGroupParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("added user to group");
console.log(data);
}
});
cognitoidentityserviceprovider.adminLinkProviderForUser(adminLinkUserParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("user linked");
console.log(data);
}
});
console.log(data);
}
});
} else {
console.log("user already present")
cognitoidentityserviceprovider.adminLinkProviderForUser(adminLinkUserParams, function (err, data) {
if (err) console.log(err, err.stack); // an error occurred
else {
console.log("userlinked since user already existed");
console.log(data);
}
});
console.log(data);
}
});
}
console.log("after the function custom");
callback(null, event);
};
This is a well know error. I handle it by retrying the request after this error and it will work. The error is because there is not way in the SDK to let it know to the pool that you already link the Federation Credentials to an user and it try to create a new user with those credentials
I wanted to have the feature of having a user seamlessly being able to login with one social provider (ex: Facebook) and then another one (Google).
I struggled with the retry process, especially with Google Login. At the signup process, if a user have several accounts, he will need to process twice the account selection.
What I ended up doing is just using Cognito for the client side code and token generation and have a lambda in the pre signup process mapping userIds with their email in a custom DB (Postgres or DynamoDB).
Then when a user query my API, based on their userId (whether it's a FacebookId or a cognito email userId, I am querying the DB to find the linked email and I am able to authenticate any users and their data like this.
Did this bug all of a sudden stop happening on 2/21/23? We didn't change anything but now this is no longer happening to users on their first time signing up. We also noticed that the UI for how Cognito is showing linked users is different - there is just 1 cognito account you're able to see in Cognito instead of multiple. You can still see the federated linked accounts in the identities property though
I would like to ask about 'AWS get Federation Token'.
What I want to do is that
someone authorized by using getFederationToken
could access ONLY HIS/HER NAMED AWS S3 folder like [bob] or [alice], etc.
Here is what I've done.
Make Temporary Credentials by using getFederationToken for aws:userid
AWS.config.update({
accessKeyId: [Removed],
secreteAccessKey: [Removed],
region: [Removed]
});
var params = {
Name : 'bob',
Policy : "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Effect\": \"Allow\", \"Action\": [\"s3:ListBucket\"],\"Resource\": [\"arn:aws:s3:::mybucket\"]}, {\"Effect\": \"Allow\", \"Action\": [\"s3:PutObject\", \"s3:GetObject\"],\"Resource\": [\"arn:aws:s3:::mybucket/${aws:userid}/*\"]}]}",
DurationSeconds: "129600"
}
var sts = new AWS.STS({apiVersion: '2011-06-15'});
sts.getFederationToken(params, function(err, data) {
if(err)
console.log(err, err.stack);
else
console.log(data);
});
But I don't know what will be called from ${aws:userid}. AWS User Guide say that [account:caller-specified-name] will be called when the principal is Federated user... However, I checked whether it's [AWS account ID (12-digit number):bob]. It's not..
If you have something to tell me, plz let me know.
Thank you.
I've found a solution even not perfect.
[account:caller-specified-name] is working correctly as [123456789012:bob] only with s3 policy, not JS code parameter.
I was looking for an answer to the same question. With some trial and error, I found the solution:
You can use
"arn:aws:s3:::mybucket/${aws:userid}/*\"
but then the folder name must be mybucket/[accountId]:bob
However, depending on where this code is executed, you might want to NOT use your account id as part of an URL or folder name.
You are passing the entire policy anyway, you could simply change the code above to accept the Name:
const tempAccountName = 'bob'; // passed as parameter I presume?
AWS.config.update({
accessKeyId: [Removed],
secreteAccessKey: [Removed],
region: [Removed]
});
var params = {
Name : tempAccountName,
Policy : `{\"Version\": \"2012-10-17\",\"Statement\": [{\"Effect\": \"Allow\", \"Action\": [\"s3:ListBucket\"],\"Resource\": [\"arn:aws:s3:::mybucket\"]}, {\"Effect\": \"Allow\", \"Action\": [\"s3:PutObject\", \"s3:GetObject\"],\"Resource\": [\"arn:aws:s3:::mybucket/${tempAccountName}/*\"]}]}`,
DurationSeconds: "129600"
}
var sts = new AWS.STS({apiVersion: '2011-06-15'});
sts.getFederationToken(params, function(err, data) {
if(err)
console.log(err, err.stack);
else
console.log(data);
});
I'm getting Invalid access key error using credentials redeemed from an amazon open id token from cognito
Here's what I'm doing
Get developer authenticated open id token
cognito.getOpenIdTokenForDeveloperIdentity(params, function (err, data) {
openIdToken = data.credentials
});
Redeem open id token for security credentials, I set the params to the congnito Auth role and set an arbitrary role session name. I use the token from step 1. There is no place where I set the identity id from step 1.
it('should be able to exchange temporary open id token for auth credentials', function (done) {
var sts = new AWS.STS();
var params = {
RoleArn: roleArn,
RoleSessionName: 'photo-upload-session',
WebIdentityToken: openIdToken.Token
};
sts.assumeRoleWithWebIdentity(params, function(err, data) {
should.not.exist(err);
should.exist(data.Credentials.AccessKeyId);
should.exist(data.Credentials.SecretAccessKey);
should.exist(data.Credentials.SessionToken);
credentials = data.Credentials;
done();
});
});
I update the current credentials
AWS.config.update({accessKeyId : credentials.AccessKeyId, secretAccessKey:credentials.SecretAccessKey});
I upload a file to s3 and get the [InvalidAccessKeyId: The AWS Access Key Id you provided does not exist in our records.] error
*edit using Bob Kinney's advice I tried two methods - setting the sessionToken (which worked) and using the Congito credentials which gave a TypeError not a buffer error. The CognitoIdentityCredentials example is below.
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId:config.get('aws_identity_pool_id'),
Logins: {
'cognito-identity.amazonaws.com': openIdToken.Token
}
});
var body = fs.createReadStream(__dirname + '/test_photo.jpg');
var s3obj = new AWS.S3({params: {Bucket: 'test-uploads', Key: 'test'}});
s3obj.upload({Body: body}).
on('httpUploadProgress', function(evt) { console.log(evt); }).
send(function(err, data) {
should.not.exist(err);
done();
});
** update
So moving back to the java client error, we are using the openid token (which was tested to be working correctly with the sts.assumeRoleWithWebIdentity) and passing that token into an extension of AWSAbstractCognitoIdentityProvider (code taken from this link http://docs.aws.amazon.com/cognito/devguide/identity/developer-authenticated-identities/) - then using that identity to upload to s3 getting the error
CustomAwsIdentityProvider provider = CustomAwsIdentityProvider.newInstance(this, BuildConfig.AWS_COGNITO_POOL_ID, Regions.US_EAST_1);
CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(this, provider, Regions.US_EAST_1);
TransferManager tm = new TransferManager(credentialsProvider);
tm.upload("my-upload", uuid.toString(), file);
Sorry for the issues. It appears you are using the JavaScript SDK. When using this flow you can use the standard AWS.CognitoIdentityCredentials object as mentioned in the developer guide using the key of cognito-identity.amazonaws.com and the value as the OpenId Connect token returned from the getOpenIdTokenForDeveloperIdentity call.
The reason for the error you are seeing is that you are not including the sessionToken from the STS result. Using the AWS.CognitoIdentityCredentials object should resolve this for you.
Update 2015-07-21: There is a small issue in the SDK that will unfortunately prevent AWS.CognitoIdentityCredentials from working as I described it. We are working on mitigating this issue.
Update 2015-07-24: You should be able to use the following to use the AWS.CognitoIdentityCredentials with your developer authenticated identiity:
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'MY_IDENTITY_POOL',
IdentityId: data.IdentityId,
Logins: {
'cognito-identity.amazonaws.com': data.Token
}
});
Where data is the response from GetOpenIdTokenForDeveloperIdentity.
When attempting to send a message to sqs I get a missing config credentials warning. If switch to just displaying my accesskey and password I can send the message to sqs just fine. I've included the code I'm using and the errors I get from the browser below.
Code below:
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'xxxxxx',
});
var params = {
MessageBody: 'some random message',
QueueUrl: 'xxxxxx'
};
AWS.config.credentials.get(function(err) {
if (err) console.log(err);
else console.log(AWS.config.credentials);
});
var sqs = new AWS.SQS();
sqs.sendMessage(params, function (err, data) {
if (!err) {
console.log('Message sent.');
} else {
console.log(err);
}
});
Errors from console.log:
Error: Missing region in config
at null. (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:5:10470)
at r.SequentialExecutor.r.util.inherit.callListeners (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:27635)
at r.SequentialExecutor.r.util.inherit.emit (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:27431)
at n.Request.o.emitEvent (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:15837)
at e (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:12148)
at r.runTo (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:7:23197)
at n.Request.o.runTo (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:13657)
at n.Request.o.send (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:13550)
at t.Service.i.makeUnauthenticatedRequest (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:30572)
at t.util.update.getId (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:7:2224)
index.html:57 Error: Missing credentials in config
at null. (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:5:10470)
at r.SequentialExecutor.r.util.inherit.callListeners (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:27635)
at r.SequentialExecutor.r.util.inherit.emit (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:27431)
at n.Request.o.emitEvent (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:15837)
at e (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:12148)
at r.runTo (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:7:23197)
at n.Request.o.runTo (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:13657)
at n.Request.o.send (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:13550)
at t.Service.i.makeUnauthenticatedRequest (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:6:30572)
at t.util.update.getId (https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js:7:2224)
Take note that I've tried adding region various different ways.
I don't see anything in your posted code that sets the region.
[Following copied from the Configuring the SDK in Node.js documentation]
The AWS SDK for Node.js doesn't select the region by default. You can choose a region similarly to setting credentials by either loading from disk or using AWS.config.update():
AWS.config.update({region: 'us-west-1'});