.NET, ASP.NET, AWS, C#, CSharp, Programming, REST, WebAPI

AWS Cognito – Identity Pool usage in .NET Core

Some time ago I had to implement authorization and authentication for REST (.NET Core 2.1, the latest stable nuget package version is 2.1.3) web service using JWT tokens (bearer) using AWS Cognito Identity Pools. It was pretty hard to find the updated tutorials or materials (even on AWS most of the data is about old .NET). For AWS integration I used AWS SDK in the latest version (3.3.x)

Following nuget packages are required:

  • AWSSDK.Core
  • AWSSDK.CognitoIdentityProvider
  • AWSSDK.Extensions.CognitoAuthentication

I will not describe how to setup AWS Cognito – it is pretty well documented. Much harder is correct setup of .NET Core application.

Lets enumerate what variables we will need to login:

  • region name – region where Identity Pool was set up
  • pool id – unique pool name
  • clientId – unique Cognito client ID
  • secretKey – key generated on Identity Pool creation
  • issuer – https://cognito-idp.{region name}.amazonaws.com/{pool id}
  • useHttps – indicates if it uses HTTP (false) or HTTPS (true)
  • expo – value required to calculate RSA key
  • key – key required to calculate RSA key

Getting expo and key values is not obvious – these values are hidden in https://cognito-idp.{region name}.amazonaws.com/{pool id}/.well-known/jwks.json in fields e for expo and n for key (this one is very long value). These fields are used to calculate RSA key to validate bearer token.

Let focus first on configuration in Startup.cs.

            services.AddAuthentication(o => o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
            {
                options.Audience = Configuration["cognito:clientId"];
                options.TokenValidationParameters = this.TokenValidationParameters();
                options.IncludeErrorDetails = true;
                options.SaveToken = true;
                options.Authority = Configuration["cognito:issuer"];
                options.RequireHttpsMetadata = bool.Parse(Configuration["cognito:useHttps"]);
            });

            services.AddAuthorization(auth =>
            {
                auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
                    .RequireAuthenticatedUser()
                    .Build());
            });

So how to generate token validation parameters? It is pretty easy and well documented on AWS forum (thread with code). Personally, I’ve implemented that in a little bit different way, but it is only detail.

So now – how to register, confirm and login user?

We need an instance of the identity provider. Also a piece of code which will generate secret hash is required. Secret hash has to be attached to each request as a kind of signature. Setting up Identity Pool for an application, it is required to choose attributes. The names are a little bit different than on the list (e.g. Phone/Phone number is phone_number). Just a side note regarding phone number – required format is +COUNTRY_NUMBERXXXXXXXXX so for Poland it would be +48123456789.

Up to configuration, confirmation code is send to the user via email or SMS. After account creation it has to be confirmed and at the end we can login. In login request, the value of AuthFlow parameter depends on what was setup in Cognito. Passing through many samples the most often choosen value was AuthFlowType.ADMIN_NO_SRP_AUTH.

        private AmazonCognitoIdentityProviderClient provider;
        private IConfiguration configuration;

        ...

        public async Task Register(string username, string password)
        {
            var signUpRequest = new SignUpRequest
            {
                ClientId = configuration["cognito:clientId"],
                SecretHash = GetSecretHash(username, configuration["cognito:clientId"], configuration["cognito:secretKey"]),
                Username = username,
                Password = password,
                UserAttributes = new System.Collections.Generic.List<AttributeType>
                {
                // attributes set in Cognito as mandatory and optional, without mandatory account will not be created
                }
            };

            await provider.SignUpAsync(signUpRequest);
        }

        public async Task Confirm(string username, string confirmationCode)
        {
            ConfirmSignUpRequest confirmRequest = new ConfirmSignUpRequest
            {
                Username = username,
                ClientId = configuration["cognito:clientId"],
                ConfirmationCode = confirmationCode,
                SecretHash = GetSecretHash(username, configuration["cognito:clientId"], configuration["cognito:secretKey"])
            };

            await provider.ConfirmSignUpAsync(confirmRequest);
        }

        public async Task<string> Login(string username, string password)
        {
            var authReq = new AdminInitiateAuthRequest
            {
                UserPoolId = configuration["cognito:poolId"],
                ClientId = configuration["cognito:clientId"],
                AuthFlow = AuthFlowType.ADMIN_NO_SRP_AUTH,
            };
            authReq.AuthParameters.Add("USERNAME", username);
            authReq.AuthParameters.Add("PASSWORD", password);
            authReq.AuthParameters.Add("SECRET_HASH", GetSecretHash(username, configuration["cognito:clientId"], configuration["cognito:secretKey"]));

            var authResp = await provider.AdminInitiateAuthAsync(authReq);
            return authResp.AuthenticationResult.IdToken;
        }

Now we have to consider how to get username after login process. It would be too obvious to get it automatically in User.Identity.Name. We have to get if from claims (like all other parameter values (whatever was choosen in cognito – phone number etc).

this.User.Claims.First(x => x.Type == "cognito:username").Value

And that is all code which you need to use now [Authorize] attribute in your code controllers and actions (of course after implementing proper actions on your controller(s) to register, confirm and login user).

The token is alive during 1 hour. Later the token has to be refreshed.

7 Comments

  1. Thank you for your post.

    I got requirement like if internal user (AD) try to access my site then it should not prompt AWS Cognito screen and directly go to dashboard but if external user (public facing site as well) access site it should prompt Cognito Login screen so that they will enter details once they signup mail has to sent to my business team where they will approve/deny user to get access for site. (For this task do I need to use Dynamo DB?)
    Hope I will get some guidance from you as I am new to AWS Cognito…Thank you in advance

    Can you please help me where exactly I need to have in the .net core app

    Register
    Confirm
    Login

    1. You have 2 options for that:

      1) create own implementation for all operations and communicate with AWS from your solution (hiding that from user’s view)
      2) using AWS provider (https://aws.amazon.com/blogs/developer/introducing-the-asp-net-core-identity-provider-preview-for-amazon-cognito/)

      Are you building an API or MVC app? If both versions (for internal and external users) will be deployed on the same server(s) or are they deployed separetely with sharing the same database?

      You need just a database (not necessarily SQL or NoSQL). The easiest would be the same as you’re using currently.

  2. Hi Janek,
    Could you please tell us how you’re implement the GetSecretHas function in C#?
    Thank you

    1. I know this is way late but I was searching for the same thing. Wanted to share.

      ///
      /// Computes the secret hash for the user pool using the corresponding userID, clientID,
      /// and client secret
      ///
      /// The current userID
      /// The clientID for the client being used
      /// The client secret of the corresponding clientID
      /// Returns the secret hash for the user pool using the corresponding
      /// userID, clientID, and client secret
      private static string GetUserPoolSecretHash(string userID, string clientID, string clientSecret)
      {
      string message = userID + clientID;
      byte[] keyBytes = Encoding.UTF8.GetBytes(clientSecret);
      byte[] messageBytes = Encoding.UTF8.GetBytes(message);

      HMACSHA256 hmacSha256 = new HMACSHA256(keyBytes);
      byte[] hashMessage = hmacSha256.ComputeHash(messageBytes);

      return Convert.ToBase64String(hashMessage);
      }

      I found it here:
      https://github.com/aws/aws-sdk-net-extensions-cognito/blob/master/src/Amazon.Extensions.CognitoAuthentication/Util/CognitoAuthHelper.cs

  3. Hi Janek. Thanks for the tutorial. I followed your code example with slight adaptation, which worked well for me. However, despite the fact that “authResp” object contains Id and Access tokens, the collection of claims — “this.User.Claims” is empty. The User.Identity.IsAuthenticated is false. Can you share the source code?

    1. Hi Gen,

      Do you use the same .NET Core version which I used in the post? Did you send the token as Authorization header?

Leave a Reply

Your email address will not be published. Required fields are marked *