Switching unauthenticated user to authenticated user - amazon-web-services

I'm trying to set my app up to allow unauthenticated users to access an AppSync API, as mentioned in https://docs.aws.amazon.com/cognito/latest/developerguide/switching-identities.html . Ideally they would be able to start using the app, then sign in and keep all their data.
I've got:
A user pool. This is set up for Google auth/regular cognito auth
An identity pool
This is linked to the user pool via a Cognito identity provider.
The authenticated/unauthenticated roles have a policy attached to them that gives them access to the GraphQL API
An AppSync API set up with AWS_IAM auth
I create the app sync client like this:
val credentialsProvider = CognitoCachingCredentialsProvider(
context,
"us-east-2:abc...etc",
Regions.US_EAST_2)
appSyncClient = AWSAppSyncClient.builder()
.context(applicationContext)
.awsConfiguration(awsConfiguration)
.credentialsProvider(credentialsProvider)
.build()
This works fine and the identity pool creates an identity for me, and I can interact with the API. Well, it creates two anonymous identity IDs, but it works. The real trouble comes when I log in:
val hostedUIOptions: HostedUIOptions = HostedUIOptions.builder()
.scopes("openid", "email", "aws.cognito.signin.user.admin")
.build()
val signInUIOptions: SignInUIOptions = SignInUIOptions.builder()
.hostedUIOptions(hostedUIOptions)
.build()
runOnUiThread {
mobileClient.showSignIn(
mainActivity,
signInUIOptions,
object : Callback<UserStateDetails?> {
override fun onResult(result: UserStateDetails?) {
Log.i("AwsAuthSignIn", "onResult: " + result?.userState)
}
override fun onError(e: Exception?) {
Log.i("AwsAuthSignIn", "onResult: " + result?.userState)
}
}
)
}
After that I see that it's created a new identity associated with the sign in, rather than use the old one. I thought it was supposed to seamlessly transfer over the old identity ID to be connected with the authenticated user.
I've also tried calling registerIdentityChangedListener to see if it fires on logging in, but it does not. It only fires when first getting the unauth identity IDs.
Also when I log into the same account from two different device it creates two different identity IDs for the same user in the user pool. Since I'm using identityId to track RDB record ownership, this means that the same user sees different items after logging in.
So is identityId the right thing to put in the database? Is it expected to be different for different devices? I'm trying to find something else to use but am coming up dry.
This is what's available in the "identity" section of the context for use with VTL resolvers:
"identity": {
"accountId": "________",
"cognitoIdentityAuthProvider": "\"cognito-idp.us-east-2.amazonaws.com/us-east-2_______\",\"cognito-idp.us-east-2.amazonaws.com/us-east-2_______:CognitoSignIn:____________\"",
"cognitoIdentityAuthType": "authenticated",
"cognitoIdentityId": "us-east-2:___",
"cognitoIdentityPoolId": "us-east-2:___",
"sourceIp": [
"_____"
],
"userArn": "arn:aws:sts::_________:assumed-role/amplify-focaltodokotlin-prod-222302-authRole/CognitoIdentityCredentials",
"username": "__________:CognitoIdentityCredentials"
}
"username" is the only other one that makes sense, but when I call AWSMobileClient.username on my side, it comes up with a different format: "Google_". So I wouldn't be able to match it up in client-side logic.
Is this possible at all or do I need to abandon the idea of unauthenticated use and go with User Pools directly?

I'll have a shot of answering this, I'll stick to rather how Cognito works than what to do with a specific SDK.
Quick recap, in Cognito, when a user authenticates, they will get 3 tokens, access, id & refresh. With an Identity Pool, they can exchange one of these (I forget which one) to get short-term credentials to assume a role. STS is what is used under the hood for this, and that's what you see in the userArn there. You don't want to look at that guy for an ID, it's an STS construct your client needs to assume an IAM role.
I'll go back to the tokens, lets look at the id_token, my favourite:
{
"at_hash": "l.......",
"sub": ".....",
"cognito:groups": [
"ap-southeast-......_Google",
"kibana"
],
"email_verified": false,
"cognito:preferred_role": "arn:aws:iam::...:role/...",
"iss": "https://cognito-idp.ap-southeast-2.amazonaws.com/ap-southeast-...",
"phone_number_verified": false,
"custom:yourapp:foo": "Bar",
"cognito:username": "Google_......",
"nonce": ".........",
"cognito:roles": [
"arn:aws:iam::.....",
"arn:aws:iam::....."
],
"aud": ".....",
"identities": [
{
"userId": "....",
"providerName": "Google",
"providerType": "Google",
"issuer": null,
"primary": "true",
"dateCreated": "1583907536164"
}
],
"token_use": "id",
"auth_time": 1596366937,
"exp": 1596370537,
"iat": 1596366937,
"email": "test#test.com"
}
I have a Google one here too, I removed a bit of stuff to hide my account etc, but anyway the id you want to use is cognito:username which will in-fact be in the form of Google_. This is internal and typically you would not show this to users. So instead in Cognito you can use the another claim preferred_username, which can also be used as an alias to sign-in as mentioned here but not for external identity providers.
You can use create custom claims to help show information on the UI, which will be prefixed with custom:, I have one here: custom:yourapp:foo. But there might be one existing for you already such as email which is available from Google. When you created your external identity provider you would have configured what claims you wanted to map from Google, email would have been there, so in your app you can read the email claim, but you should use the cognito:username in your App's backend, but keep in mind that if a user deletes and recreates their account I don't know that you get the same ID again. You may rather want users to be able to define a preferred_username on signup, which you could display in the UI, but don't use that to save data against, use the cognito:username claim.
And now for Start using the app, then sign in and keep all their data. Typically this would be implemented by saving all the data in local storage on the device, not the backend. The reason being is that if a user has not authenticated, (excluding creating a session upon opening the app), then there is no way to verify that foo#gmail.com is actually foo#gmail.com when they were hitting your API as what you saw with the unauthenticated role. I could hit your API and say that I am foo#gmail even though I was bar#gmail.
The neatest way would be to store the data locally on the device, so it wouldn't matter if the user was authenticated or not. If for some reason your app does need to store this data in the backend to function, and you cannot refactor, what you could do is use a Customized Userpool Workflow, and create a Pre-Signup Lambda, which could take a custom claim (I wouldn't use the sts userArn seems wrong but you could use that too), preferably a GUID of a shopping cart for example custom:yourapp:cartGuid. So what I mean is:
When an anonymous user visits the app for the first time, they would
be issued a GUID for a shopping cart, and save all items in that
cart.
If they choose to sign up, they can pass in a custom claim: custom:yourapp:cartGuid, and in your Lambda function you will create the user in your DB, and add the cart to their account.
Guessing another user's GUID would be near impossible, but if this is a security concern then you could create a signed token.
You probably want to clean up users's carts that don't move to the sign-up after a certain amount of time.
Just give me a comment if you have any questions or are unsure. I believe from memory that you need to use the pre-signup hook because the post-confirm doesn't have access to the claims passed in on the signup process. You may want to create the user with an unconfirmed flag in the pre-hook, and then enable them in the post-hook which I believe is more safe incase another failure would happen your pool and then you have users created in a dirty state. Best of luck I've been through Cognito battles myself and survived!

Related

AWS Cognito - Create a user via API Endpoint in Postman

Does anybody know if I can make a request to create or a sign up a user in AWS Cognito user pool?
For example, something like below is to display the login screen.
But is there a POST request or endpoint I can call to create a user?
I tried looking through their documentation but no look finding anything concrete.
Keep in mind, if it possible I would like to populate a value for a custom attribute I created.
This is the main reason why I am looking for an endpoint because I cannot seem to find a way to populate the value for a custom attribute via the AWS interface.
So technically I do not need an endpoint if it is possible to populate a custom attribute per user in AWS.
GET https://mydomain.auth.us-east-1.amazoncognito.com/login?
response_type=code&
client_id=ad398u21ijw3s9w3939&
redirect_uri=https://YOUR_APP/redirect_uri&
state=STATE&
scope=openid+profile+aws.cognito.signin.user.admin
It looks like what you need is https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html or https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SignUp.html. As far as I'm aware, there is no way to prepopulate the attribute on the Cognito hosted UI. You did not specify what programming language you are using, but at the bottom of the page there are links to documentation with examples for different SDKs. The difference between these two approaches is discussed here: https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html. So in this case, AdminCreateUser corresponds to option 3 and SignUp to option 1. The difference is mainly in whether or not the user will receive an invite. Also, for AdminCreateUser Cognito will generate a temporary password and require user to enter a new password the first time they log in.
Body
{
"ClientId": "test",
"Password": "Qwerty123",
"UserAttributes": [
{
"Name": "email",
"Value": "test#test.com"
}
],
"Username": "test#test.com"
}
Headers
URL
POST https://cognito-idp.eu-west-1.amazonaws.com/ HTTP/1.1

"No current user": Isn't it even possible to make unauth calls to AWS AppSync through Amplify with authentication type AMAZON_COGNITO_USER_POOLS?

I have an AWS AppSync schema with the default authorization mode set to Amazon Cognito User Pool. I make calls to this AppSync endpoint from a web app using AWS Amplify GraphQL Client and, coherently, its configuration points Cognito User Pools as authentication type, too:
aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS'
It works as expected when the user is authenticated; however (although the involving Cognito Identity Pool has proper Auth and Unath roles set already), when the website runs some Amplify fetch command like for a unauthenticated(guest) user:
const item = await API.graphql(graphqlOperation(getItem, { id: 'my-id' }))
Ends up with throwing an error:
"No current user"
Well, I expected it to perform if I allow unauthenticated users, but it simply fails. Seeking for a way out, I found some discussions like:
a GitHub issue comment here,
another Github issue,
or an SO question here.
And, all of the above suggest revisiting the Amplify configs so that the AppSync authentication type is converted from AMAZON_COGNITO_USER_POOLS to AWS_IAM or API_KEY. However, for some detailed reason 1:
I want to stick with AMAZON_COGNITO_USER_POOLS authentication type,
And still be able to fetch some AppSync resources as a guest user unless they are restricted with
#aws_auth decorators or such.
Is it possible in any way?
1 I have more granular controls depending on the user's group (admin, normal etc.) with decorators such as #aws_auth(cognito_groups: ["default-user-group"]) on the AppSync schema. So, I need Cognito User Pools for that usage.
So, I just went through a similar issue and managed to get it sorted. I hope this might help you sort this out. The SO question you mentioned in your question is almost the right way to do it. However, there are one "little" tiny details that are not documented and took me a while to find out.
Apart from having to enable both authenticated and unauthenticated access by running amplify update auth (you can see how in the SO linked above) there are other tweaks you need to do.
First, in your model you need to adjust your rules to be something like:
#auth(
rules: [
# allow owners ability to update and delete their these messages (user pools)
{ allow: owner },
# allow all authenticated users to access this data
{ allow: private, provider: userPools },
# allow all guest users (not authenticated) to access this data
{ allow: public, provider: iam }
]
)
Once you set up this model to allow user pools to access all the data they "own", you can also let any "guest" user to access the data too.
In the frontend, let's get your code as an example, you need to use a bit of a different approach:
Instead of
const item = await API.graphql(graphqlOperation(getItem, { id: 'my-id' }))
try something like
// Check if the user is logged in or not
let isLoggedIn = await isLoggedIn();
const item = (await API.graphql({
query: getItem,
variables: { id: 'my-id' },
authMode: isLoggedIn ? GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS : GRAPHQL_AUTH_MODE.AWS_IAM,
}));
by the way the "isLoggedIn" function look like this
async function isLoggedIn() {
// Another way to get if its a guest or not
//return await Auth.Credentials.getCredSource() === "guest"
try {
await Auth.currentAuthenticatedUser();
return true;
} catch {
return false;
}
}
So... this line is what it does the trick, which is not really well documented.
authMode: isLoggedIn ? GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS : GRAPHQL_AUTH_MODE.AWS_IAM
you need to path the different methods according to the state of the user (logged in or guest), not just the AWS_IAM one.
This will only get you as far as READING the data, in order to make sure guests can do Create/Update/Delete, and separate the data ownership from each other and logged users, that's a completely different story that you will need to start digging up on resolvers to get it sorted. But the good news is, there is a way :)

Custom attribute not passed into ID_TOKEN created by AWS Cognito

I am not able to get custom attribute in ID_TOKEN returned from AWS Cognito after successful user login.
Steps I tried :
1.Created user pool
2.Created app client and checked the custom attribute(customattrib1,customattrib2)
User Pool screen :
Check custom attribute in app client config
3.Created user using admin-create-user api
Below image shows the value for user attributes:
4.Signed in user using aws-cognito-auth.js in client app.The ID token returned do not contain the custom attribute.
ID_TOKEN
{
"at_hash": "PKfjYDaiEty5mUOyJZlPQA",
"sub": "639d5016-2bd3-4c6f-b82d-21ae38071b09",
"email_verified": true,
"iss": "https://cognito-idp.ap-south-1.amazonaws.com/ap-south-1_XXXXXXX",
"phone_number_verified": true,
"cognito:username": "testuser",
"aud": "XYXYXYXYX",
"token_use": "id",
"auth_time": 1549349674,
"phone_number": "##########",
"exp": 1549353274,
"iat": 1549349674,
"email": "testuser#somedomain.com"
}
I have already checked links below, which had some info regarding this issue, but nothing helped so far.
Adding Cognito custom attributes post pool creation?
Cognito User Pool custom attributes do not show up in the ID token if user pool is configured with a SAML identity provider
Cognito User Pool custom attributes do not show up in the ID token if user pool is configured with a SAML identity provider
https://www.reddit.com/r/aws/comments/a07dwg/cognito_add_custom_attribute_to_jwt_token/
Please help me figure out if I am missing something..
In your Cognito user pool go to General Settings -> App Clients, then for each app client click on Show Details, then Set attribute read and write permissions. Check the checkbox next to your attribute name under Readable Attributes.
In your Cognito user pool go to App client settings -> Allowed OAuth Scopes and enable profile scope.
I had the same trouble and your question came up when I was searching for a solution.
My custom attributes started to appear in ID token when I enabled profile scope in 'App client settings'. (available at: AWS console-> 'User pools'-> click your pool -> 'App client settings' -> 'Allowed OAuth Scopes')
(BTW: I was misled by this sentence from the documentation: "The openid scope returns all user attributes in the ID token that are readable by the client". In my case openid scope was not enough.)
For me the problem was that I was getting my token (after authenticating with the Amplify js library) from:
Auth.currentSession().then(u => u.getAccessToken().getJwtToken())
Instead of:
Auth.currentSession().then(u => u.getIdToken().getJwtToken());
After changing it worked fine! Hope it helps!
For anyone coming here that is using the Amplify SDK like the OP - Copy and Paste calls out an important point that your client ALSO needs to explicitly ask for the scope.
In my case we I am using Angular. After adding "profile" to the User Pool "Allowed OAuth Scopes" - you also need to specify it in your client configuration:

Are there universal Cognito Ids for AWS regardless of sign in?

I've been wrapping my head around AWS Cognito, and can't seem to find a clear answer on this one.
Here is our situation: I have a react-native mobile application (using aws-amplify and it uses three different identity providers:
AWS User Pools
Facebook
Google
I know that user pools are a method to manage, create, and sync users across platforms. Within those user pools, each user has GUID assigned to them called sub, short for subject.
I also know that if I use a third party, say Facebook, to sign in, the identity pool will get the third party user, or the user pool, temp access to the AWS services (say dynamo db).
We have another legacy database of previous info, and we want to have a matching process to assign the user to. So, if user1 logs in, they correspond to our old website with user_one in the legacy db, and we want to link up their data and save it for reference later.
My question is, in the identity pool that all 3 methods access, is there a universal GUID or ID that I can associate with each user regardless of sign in type, and record in a DB? And how do I get it using react-native?
Here is what I have tried:
Auth.signIn(username, password)
.then(user => {
// etc
User returns the user_poolId, clientId, and no identity pool info.
With a federated (FB, Google) sign in:
Auth.federatedSignIn('facebook', {
token: data.accessToken.toString(),
expires_at: data.expirationTime,
}, result)
.then(data => {
let user = {
id: data._identityId,
};
This gives me the _identityId. I'm hoping to get a value similar to this for the user pool signin.

Save AWS Cognito Users in DynamoDB

I recently started experimenting with AWS AppSync but I had some questions around AWS Cognito.
I would like for users to be able to authenticate with Facebook but I need their profile picture, name and email as data for my public user profiles in my app. So far, I noticed Cognito integrates with Facebook Auth but it does not allow access to the user information and this info does not get saved in a DynamoDB table.
My question is, how can I create a new User in DynamoDB when Cognito receives a new sign in, or return an existing user/id when the user already exists in the db.
I was trying to achieve the same a few weeks ago.
After reading the docs for hours, I realised that Cognito may not help us in regards to the data that comes back from FB or how to save it.
I ended up doing the following:
(1) Using FB-SDK, pulled in the user data.
(2) Invoked a Lambda function that saved this data (like FB_id,etc) to DynamoDB.
(3) If user logged in again, their FB_id (or email) was used to check against DynamoDB entries to retrieve their data.
If Cognito is able to help us and I missed it somehow, I would love to know.
Happy Coding!
You could use custom attributes and federating user from Facebook in your user pool to achieve this. Here are the steps at high level to do this.
You will first have to define custom attributes for the profile information you want to save in each user profile.
Define attribute mapping to link the custom attributes to Facebook attributes you want to save.
Build you application using Cognito hosted pages and federation to allow your users to log in using Facebook.
After this, on each new user log in in your app a new user is created in your user pool with all the attributes that were defined in attribute mapping and values which Cognito gets in the Facebook token. Your app will get these attribute values in the IDToken issued after authentication and you app can use these.
Additionally, if you want to store these attribute values outside of Cognito user pools profile, like your own DynamoDB table, you can configure a PreSignUp trigger in the pool which will be invoked on all new user creations. You can export the user attributes from this trigger to any database of your choice.
Hope this helps.
AWS AppSync allows you to access information in the GraphQL resolver which you can choose to store in a DynamoDB table. In your case for data coming from a Facebook profile you could pass this as arguments to a GraphQL mutation or in a header to AppSync which you can then access in the resolver via $ctx.request.headers.NAME where NAME is your header name. Then you could simply choose which attributes you want to write to DynamoDB for that user as part of the mutation. More information is in the reference guide here: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html
Since you also asked that you'd like to do a check first to see if the user is already in the DDB first you could just do an existence check first:
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"userId": $util.dynamodb.toDynamoDBJson($ctx.identity.username),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(userId)"
},
}
This checks against the username from Cognito User Pools. If you were using the Cognito Federated Identities feature it would be ctx.identity.cognitoIdentityId. If the record is already there the response that comes back will tell you which means the user is already present. You could also transform the returned message in the response mapping template by looking at $ctx.result with a conditional statement and either building the JSON response by scratch or using one of the $util.error() methods in the guide above.
Finally as you mentioned that you'll have public profile data, you might want to mark this on certain records for control. In AWS AppSync you can filter GraphQL responses on authorization metadata such as this. You would just have an attribute (aka column) on the DynamoDB record marked 'public' or 'private. Then your response template would look like so:
#if($context.result.public == 'yes')
$utils.toJson($context.result)
#else
$utils.unauthorized()
#end
You can see more examples of this here: https://docs.aws.amazon.com/appsync/latest/devguide/security-authorization-use-cases.html#public-and-private-records