Switching unauthenticated user to authenticated user

Viewed 497

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:

  1. A user pool. This is set up for Google auth/regular cognito auth

  2. 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

  3. 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?

1 Answers

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