aws / aws-sdk-net-extensions-cognito

An extension library to assist in the Amazon Cognito User Pools authentication process
Apache License 2.0
102 stars 50 forks source link

Issue using Refresh Token flow #75

Closed BenWoodford closed 3 years ago

BenWoodford commented 3 years ago

I'm using the snippet from this flow and can successfully retrieve an access token and refresh token from the AuthenticationResult value, but upon saving the refresh token and putting it back through the aforementioned snippet I get Invalid Refresh Token as a response.

Am I missing some key AWS-side config setting here or something like that? Is the client secret required for refresh keys maybe? (there isn't one set currently)

This is using the SDK 3.7.57.0 and Extensions 2.21 in Unity

ashishdhingra commented 3 years ago

Hi @BenWoodford,

Good morning.

Thanks for posting guidance question.

Please refer the below working code sample that has capability to use RefreshToken. Kindly note that this is a sample (console) application and you might want to move the secrets to a configuration file. You should have the correct username that exists in CognitoUserPool to use the RefreshToken. Kindly note that this sample uses Amazon.Extensions.CognitoAuthentication package.

using System;
using System.Threading.Tasks;
using Amazon;
using Amazon.CognitoIdentityProvider;
using Amazon.Extensions.CognitoAuthentication;
using Amazon.Runtime;

namespace CognitoStartWithRefreshTokenAuthAsync
{
    class Program
    {
        private static string userPoolId = "<<userpool_id>>";
        private static string clientId = "<<client_id>>";
        private static string clientSecret = "<<client_secret>>";
        private static RegionEndpoint regionEndpoint = RegionEndpoint.USEast2; // Change the region appropriately. Credentials are loaded from default profile.

        static void Main(string[] args)
        {
            Console.WriteLine("User Name: ");
            string userName = Console.ReadLine();
            while (string.IsNullOrWhiteSpace(userName))
            {
                Console.WriteLine("Please enter a valid User Name.");
                userName = Console.ReadLine();
            }

            Console.WriteLine("Do you have a Refresh Token (Y/N): ");
            char hasRefreshTokenResponse = Convert.ToChar(Console.Read());
            bool hasRefreshToken = (char.ToLower(hasRefreshTokenResponse) == 'y');
            Console.WriteLine();

            Console.WriteLine("Password: ");
            string password = Console.ReadLine();
            while (string.IsNullOrWhiteSpace(password) && !hasRefreshToken)
            {
                Console.WriteLine("Please enter a valid Password.");
                password = Console.ReadLine();
            }

            Console.WriteLine("Existing Refresh Token: ");
            string refreshToken = Console.ReadLine();
            while (string.IsNullOrWhiteSpace(refreshToken) && hasRefreshToken)
            {
                Console.WriteLine("Please enter a valid Refresh Token.");
                refreshToken = Console.ReadLine();
            }

            AuthFlowResponse authFlowResponse = (!string.IsNullOrWhiteSpace(refreshToken) ? GetCredsFromRefreshAsync(userName, refreshToken).GetAwaiter().GetResult() : GetCredentials(userName, password).GetAwaiter().GetResult());
        }

        public static async Task<AuthFlowResponse> GetCredentials(string userName, string password)
        {
            var provider = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), FallbackRegionFactory.GetRegionEndpoint());
            var userPool = new CognitoUserPool(userPoolId, clientId, provider, clientSecret);
            var user = new CognitoUser(userName, clientId, userPool, provider, clientSecret);

            AuthFlowResponse authResponse = await user.StartWithSrpAuthAsync(new InitiateSrpAuthRequest()
            {
                Password = password
            }).ConfigureAwait(false);

            while (authResponse.AuthenticationResult == null)
            {
                if (authResponse.ChallengeName == ChallengeNameType.NEW_PASSWORD_REQUIRED)
                {
                    Console.WriteLine("Enter your desired new password: ");
                    string newPassword = Console.ReadLine();

                    authResponse = await user.RespondToNewPasswordRequiredAsync(new RespondToNewPasswordRequiredRequest()
                    {
                        SessionID = authResponse.SessionID,
                        NewPassword = newPassword
                    });
                }
                else if (authResponse.ChallengeName == ChallengeNameType.SMS_MFA)
                {
                    Console.WriteLine("Enter the MFA Code sent to your device: ");
                    string mfaCode = Console.ReadLine();

                    authResponse = await user.RespondToSmsMfaAuthAsync(new RespondToSmsMfaRequest()
                    {
                        SessionID = authResponse.SessionID,
                        MfaCode = mfaCode

                    }).ConfigureAwait(false);
                }
                else
                {
                    Console.WriteLine("Unrecognized authentication challenge.");
                    return null;
                }
            }

            return authResponse;
        }

        public static async Task<AuthFlowResponse> GetCredsFromRefreshAsync(string userName, string refreshToken)
        {
            AmazonCognitoIdentityProviderClient provider = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), FallbackRegionFactory.GetRegionEndpoint());
            CognitoUserPool userPool = new CognitoUserPool(userPoolId, clientId, provider, clientSecret);

            CognitoUser user = new CognitoUser(userName, clientId, userPool, provider, clientSecret);

            user.SessionTokens = new CognitoUserSession(null, null, refreshToken, DateTime.Now, DateTime.Now.AddHours(1));

            InitiateRefreshTokenAuthRequest refreshRequest = new InitiateRefreshTokenAuthRequest()
            {
                AuthFlowType = AuthFlowType.REFRESH_TOKEN_AUTH
            };

            return await user.StartWithRefreshTokenAuthAsync(refreshRequest).ConfigureAwait(false);
        }
    }
}

NOTE: In case you app client has client secret as well, you would need to pass them to CognitoUserPool and CognitoUser constructors.

This sample takes guidance from Amazon CognitoAuthentication Extension Library Examples. Hope this helps.

Thanks, Ashish

BenWoodford commented 3 years ago

So just to confirm, there's no server-side settings I should be using, no requirement to use a client secret, etc? I just need a refresh token and a username.

ashishdhingra commented 3 years ago

So just to confirm, there's no server-side settings I should be using, no requirement to use a client secret, etc? I just need a refresh token and a username.

@BenWoodford You are not required to configure client secret for an app client. If you configured client secret for app client, then you need to would need to pass them to CognitoUserPool and CognitoUser constructors. Please try using the above sample code for testing and let me know if it works for you.

github-actions[bot] commented 3 years ago

This issue has not recieved a response in 2 weeks. If you want to keep this issue open, please just leave a comment below and auto-close will be canceled.

BenWoodford commented 3 years ago

Keeping open until I have a chance to properly test

(it also hasn’t been two weeks…?)

BenWoodford commented 3 years ago

Unfortunately I still get an Invalid Refresh Token error with that code.

To confirm, I'm getting the token from response.AuthenticationResult.RefreshToken, is this correct?

ashishdhingra commented 3 years ago

@BenWoodford In case you app client has client secret as well, you would need to pass them to CognitoUserPool and CognitoUser constructors.

BenWoodford commented 3 years ago

@BenWoodford In case you app client has client secret as well, you would need to pass them to CognitoUserPool and CognitoUser constructors.

There’s no client secret (presumably username/password login wouldn’t work in that case anyway?)

ashishdhingra commented 3 years ago

@BenWoodford In case you app client has client secret as well, you would need to pass them to CognitoUserPool and CognitoUser constructors.

There’s no client secret (presumably username/password login wouldn’t work in that case anyway?)

@BenWoodford Thanks for your persistence. It appears that you are affected by the issue mentioned in #77 in Amazon.Extensions.CognitoAuthentication 2.2.1. Please use the below workaround for using refresh token for now (notice the use of DateTime.Now.AddHours(1).ToUniversalTime() for expiration time):

public static async Task<AuthFlowResponse> GetCredsFromRefreshAsync(string userName, string refreshToken)
{
    AmazonCognitoIdentityProviderClient provider = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), FallbackRegionFactory.GetRegionEndpoint());
    CognitoUserPool userPool = new CognitoUserPool(userPoolId, clientId, provider, clientSecret);

    CognitoUser user = new CognitoUser(userName, clientId, userPool, provider, clientSecret);

    user.SessionTokens = new CognitoUserSession(null, null, refreshToken, DateTime.Now, DateTime.Now.AddHours(1).ToUniversalTime());

    InitiateRefreshTokenAuthRequest refreshRequest = new InitiateRefreshTokenAuthRequest()
    {
        AuthFlowType = AuthFlowType.REFRESH_TOKEN_AUTH
    };

    return await user.StartWithRefreshTokenAuthAsync(refreshRequest).ConfigureAwait(false);
}

There is another related issue https://github.com/aws/aws-sdk-net-extensions-cognito/issues/76 which states that refresh token auth flow should not check for expiration time of access token.

Once the fix for #77 and #76 is implemented, I will update here accordingly. This is not longer a guidance issue and I will change the label to bug.

Thanks, Ashish

BenWoodford commented 3 years ago

@ashishdhingra thanks, I tried that out but still no avail unfortunately. Same "Invalid Refresh Token" error

To confirm, Cognito's Client settings on the AWS Console shows "(no client secret)" - so presumably this means there is definitely not one set?

To double-check, you don't need a client secret to use a refresh token do you?

ashishdhingra commented 3 years ago

@ashishdhingra thanks, I tried that out but still no avail unfortunately. Same "Invalid Refresh Token" error

To confirm, Cognito's Client settings on the AWS Console shows "(no client secret)" - so presumably this means there is definitely not one set?

To double-check, you don't need a client secret to use a refresh token do you?

@BenWoodford No, you do not need client secret. To troubleshoot, could you please share screenshot of user pool and app client settings (with sensitive information masked). It works perfectly fine at my end.

BenWoodford commented 3 years ago

Will do tomorrow morning, thanks!

Is there an email address I can send that to instead though? Just to avoid any information mishaps as it’s a client’s AWS account.

ashishdhingra commented 3 years ago

Will do tomorrow morning, thanks!

Is there an email address I can send that to instead though? Just to avoid any information mishaps as it’s a client’s AWS account.

@BenWoodford You can upload the screenshots here. However, please make sure that you mask out sensitive information.

BenWoodford commented 3 years ago

image

image

image

image

BenWoodford commented 3 years ago

I've narrowed this down: if I disable device tracking I can use the refresh token, so the endpoint error isn't terribly helpful at all as the token itself is fine.

However there's no documentation on how I ensure the device key is provided for the refresh flow, I've tried creating a device like the below using a remembered device key but it still returns an invalid refresh token:

        user.Device = new CognitoDevice(new Amazon.CognitoIdentityProvider.Model.DeviceType()
        {
            DeviceKey = deviceKey
        }, user);

As a side question, what is the appropriate way to handle refresh tokens and the user not being connected to the internet (in the case of a mobile app that isn't currently online)? Should I just assume that the refresh token is okay and let them use the app offline, or should I be tracking when I acquired the token? Given it doesn't seem to refresh when authenticating with a refresh token (or does using it extend the expiration?) I can't really go "Oh the expiration is up, I'll just log them out" as that would always be 30 days after the last user/pass auth

ashishdhingra commented 3 years ago

Hi @BenWoodford,

The refresh toke auth flow fix was implemented in Amazon.Extensions.CognitoAuthentication 2.2.2. Please verify if your scenario is working now in the latest version.

Thanks, Ashish

BenWoodford commented 3 years ago

Will do if I get a chance, though I’ve since switched Hosted UI and talking to the OAuth2 refresh token endpoint directly which seems to be working without any problems thankfully.

However while I’m here: how does one refresh a refresh token…? As otherwise after 30 days it’s just going to log the user out, but you don’t get new refresh tokens when doing refresh login

ashishdhingra commented 3 years ago

@BenWoodford If the refresh token is expired, your app user must re-authenticate by signing in again to your user pool. You may find more details on Using the Refresh Token.

There are multiple open issues related to device tracking https://github.com/aws/aws-sdk-net-extensions-cognito/issues/73, https://github.com/aws/aws-sdk-net-extensions-cognito/issues/28 and https://github.com/aws/aws-sdk-net-extensions-cognito/issues/10. Since the refresh token auth flow works for you with device tracking disabled and you have a workaround, I would close this issue as duplicate of these other issues. I would see how I can get these issues prioritized by development team.

github-actions[bot] commented 3 years ago

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see. If you need more assistance, please either tag a team member or open a new issue that references this one. If you wish to keep having a conversation with other community members under this issue feel free to do so.

Welchen commented 2 years ago

@ashishdhingra I'm running into this same issue and as I see that the original poster never confirmed that the issue was fixed. When device tracking is turned on, the user of a refresh token is failing. I don't see any documentation about what to do in this scenario if it does even work.

LoopIssuer commented 2 years ago

Hi, I had a similar issue as mentioned above, but I get error: NotAuthorizedException: SecretHash does not match for the client: xxxxxxxxxxxxxxxxxxx I tried: -using secret directly -using GetSecretHash with userName, userEmail, USerID, User Sub Id Always the same issue.

I'm trying:

 public async void GetCredsFromRefreshAsync_(string refreshToken, string accessToken, string idToken, string userName, string userId)
{
  var secretHash = GetSecretHash(userId + _cognitoCredentials.AppClientId);
            CognitoUserPool userPool = new CognitoUserPool(_cognitoCredentials.UserPoolId, _cognitoCredentials.AppClientId, _provider, _cognitoCredentials.Secret);
            CognitoUser user = new CognitoUser(userName, _cognitoCredentials.AppClientId, userPool, _provider, _cognitoCredentials.Secret);
            user.SessionTokens = new CognitoUserSession( null, null, refreshToken, DateTime.Now, DateTime.Now.AddHours(1));
            InitiateRefreshTokenAuthRequest refreshRequest = new InitiateRefreshTokenAuthRequest()
            {               
                AuthFlowType = AuthFlowType.REFRESH_TOKEN_AUTH
            };

            AuthFlowResponse authResponse = await user.StartWithRefreshTokenAuthAsync(refreshRequest).ConfigureAwait(false);`
}

        private string GetSecretHash(string value)
        {
            var key = _cognitoCredentials.Secret;
            using (var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
            {
                var hash = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(value));
                return Convert.ToBase64String(hash);
            }
        }

Also tried with the same result:

       public async void GetCredsFromRefreshAsync(string refreshToken, string accessToken, string idToken, string userName, string userId)
        {
            var secretHash = GetSecretHash(userId + _cognitoCredentials.AppClientId);
            CognitoUserPool userPool = new CognitoUserPool(_cognitoCredentials.UserPoolId, _cognitoCredentials.AppClientId, _provider);
            CognitoUser user = new CognitoUser(userName, _cognitoCredentials.AppClientId, userPool, _provider);

            user.SessionTokens = new CognitoUserSession(idToken, accessToken, refreshToken, DateTime.Now, DateTime.Now.AddHours(1));

            var refreshReq = new InitiateAuthRequest();
            refreshReq.ClientId = _cognitoCredentials.AppClientId;

            refreshReq.AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH;
            refreshReq.AuthParameters.Add("SECRET_HASH", secretHash);
            refreshReq.AuthParameters.Add("REFRESH_TOKEN", refreshToken);

            var clientResp = await _provider.InitiateAuthAsync(refreshReq).ConfigureAwait(false);

            InitiateRefreshTokenAuthRequest refreshRequest = new InitiateRefreshTokenAuthRequest()
            {
                AuthFlowType = AuthFlowType.REFRESH_TOKEN_AUTH
            };
        }

Please help.