Post Cover

Role-based Access Control in AWS - Assuming Roles and accessing via ASP.NET Core

AWS ASP.NET Core Posted Jan 23, 2021

We've reached the final lap in our journey to implementing Role-based Access Control in AWS. So far we have looked at how we design an Admin-Editor system who are authenticated on the front-end using a Cognito User Pool and are aggregated based on their expected Roles via Cognito Groups.

We then looked at the importance of an Identity Pool, which provides a federated authentication mechanism and allows users who are authenticated by a configured Cognito User Pool to access AWS resources by issuing Identity Credentials which are the means to authenticate users internal to AWS resources. We've looked at how we create and link an Identity Pool to our existing User Pool to issue credentials for users based on their roles.

In the final act, we shall fit all these pieces together to form the complete picture and see how we can Assume Role and access the AWS resources via AWS SDK with an example in ASP.NET Core API.

Assuming Role and Executing Functionality:

We know that the Editors can access a particular bucket and access a particular DynamoDB table for assets and data respectively, while Admins can create these Editors. While the things done by Editors such as Uploading assets to S3 bucket or Querying from a DynamoDB table are something we've seen before, let's look at something which is important for us - impersonating a user's role and retching the user's credentials for accessing these resources.

Any AWS resource such as AmazonS3Client or AmazonDynamoDBClient requires an Access Key and an Access Secret for invoking the methods. In our case, we shall create a service called AssumeRoleService which returns the credentials for us based on the Role that we created all along.

We do it in three steps:

  1. Obtain the correct Role for the current user from the Claims
  2. Request the IdentityPool for the credentials for the current user
  3. Extract the credentials and use them in the AWS SDK

In our ASP.NET Core API, lets create a ScopedService called AssumeRoleService which encapsulates the logic to get credentials from the AWS Token Service SDK for an incoming user context which is available via the API Gateway which authenticates and delegates requests to this API.

public class AssumeRoleService : IAssumeRoleService
{
    private readonly IHttpContextAccessor _accessor;
    private const string KEY_COGNITO_USERID = "cognito:username";
    private const string KEY_COGNITO_PREFERRED_ROLE = "cognito:preferred_role";
    private const string KEY_COGNITO_ROLES = "cognito:roles";

    public AssumeRoleService(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

    // Get the User ObjectId from Claims
    private string GetRoleSessionName(IEnumerable<Claim> claims)
    {
        var roleSessionNameClaims = claims.FirstOrDefault(x => 
                  x.Type == KEY_COGNITO_USERID);
        if (roleSessionNameClaims != null)
        {
            return roleSessionNameClaims.Value;
        }
        return string.Empty;
    }

    // Get Role from Claims
    private string GetRole(IEnumerable<Claim> claims)
    {
        var preferredRoleClaims = claims.FirstOrDefault(x => 
                x.Type == KEY_COGNITO_PREFERRED_ROLE);
        if (preferredRoleClaims != null)
        {
            return preferredRoleClaims.Value;
        }
        else
        {
            var roleClaims = claims.Where(x => x.Type == KEY_COGNITO_ROLES);
            if (roleClaims != null && roleClaims.Any())
            {
                return roleClaims.First().Value;
            }
        }
        return string.Empty;
    }

    public async Task<Credentials> GetAssumedRoleCredentialsAsync()
    {
        var role = GetRole(_accessor.HttpContext.User.Claims);

        var stsClient = new AmazonSecurityTokenServiceClient();
        var assumeRoleReq = new AssumeRoleRequest()
        {
            DurationSeconds = 3600,
            RoleArn = role,
            RoleSessionName = GetRoleSessionName(_accessor.HttpContext.User.Claims),
        };

        AssumeRoleResponse response = await stsClient.AssumeRoleAsync(assumeRoleReq);

        if (response == null)
        {
            return null;
        }

        // returns Credentials Object
        // which contains the AccessKey and AccessSecret
        return response.Credentials;
    }
}

To use in any service, say like to create an AmazonDynamoDBClient is simply done like below:

private async Task<DynamoDBContext> GetAmazonDynamoDBClientAsync()
{

    DynamoDBContext context = null;

    if (context == null)
    {
        // Fetch the Credentials of the current User
        // calls the AssumeRoleService.GetAssumedRoleCredentialsAsync() method
        var credentials = await _role.GetAssumedRoleCredentialsAsync();
        
        // create a DynamoDB client with the credentials fetched
        var client = new AmazonDynamoDBClient(credentials, new AmazonDynamoDBConfig
        {
            RegionEndpoint = RegionEndpoint.GetBySystemName(Configuration.AwsRegion)
        });
        context = new DynamoDBContext(client);
    }

    return context;
}

Which can be then used in to access any record from the DynamoDB Table using the .NET Core AWS SDK which the user has access to - provided via the IAM role and authorized using the Credentials obtained.

or an AmazonS3Client like below:

private async Task<AmazonS3Client> GetAmazonS3ClientWithCredentialsAsync()
{
    var credentials = await _role.GetAssumedRoleCredentialsAsync();
    return new AmazonS3Client(credentials, _region);
}

using which the user can then access the .NET Core AWS SDK to upload Files to S3 bucket - for which the access is provided by the IAM role.

While these are common to both Editors and Admins, we should also look at how Admins create new Editors based on their role. While we use the same AssumeRoleService in this case as well for the Credentials, the way of creating new Users using the AWS SDK is a bit different.

Creating new Cognito User in AWS with .NET Core:

Creating a new User in Cognito involves:

  1. Creating a new User with username and password, which puts the user in an UNCONFIRMED state
  2. Confirming the user email which then activates the user

We're not interested in these steps, so we'll create user and force confirm the user so that any new Editor created can directly login to the editor's Cognito page without having to wait for any other steps. We'll just ensure that the Editor changes the password on the first login.

And let's not forget that we need to add this new Editor to the Editors group so that when logged in, the Editor would inherit all the privileges of the Editor (like accessing S3 or DynamoDB) from the start.

public async Task<string> CreateUserAsync(CognitoUserModel model)
{
    // Fetch the Credentials of the current User
    var credentials = await _role.GetAssumedRoleCredentialsAsync();

    // create the CognitoClient with the credentials
    var cognitoClient = new AmazonCognitoIdentityProviderClient(credentials, RegionEndpoint.GetBySystemName(Configuration.AwsRegion));

    // createUserRequest with Admin previliges
    var adminSignupRequest = new AdminCreateUserRequest
    {
        TemporaryPassword = GeneratePassword(),
        Username = model.EmailAddress,
        UserPoolId = _poolId
    };
    adminSignupRequest.UserAttributes.Add(new AttributeType
    {
        Name = "email",
        Value = model.EmailAddress
    });

    // createUser with admin previliges
    // the Cognito UserPool permissions we gave
    // for the Admin Role comes handy here
    AdminCreateUserResponse userResponse = await cognitoClient.AdminCreateUserAsync(adminSignupRequest);

    // Add User to the Editors Group
    var addToGrpRequest = new AdminAddUserToGroupRequest
    {
        GroupName = Configuration.UserGroupName,
        Username = userResponse.User.Username,
        UserPoolId = _poolId
    };
    AdminAddUserToGroupResponse grpResponse = await cognitoClient.AdminAddUserToGroupAsync(addToGrpRequest);
    string userId = userResponse.User.Attributes.First(x => x.Name == "sub").Value;
    return userId;
}

To complete this setup, we deploy our API in an AWS Lambda which is connected to an API Gateway and then enable Cognito Authorizer for token validation. Once we're done with this and invoke our API for any of the above mentioned operations (such as DynamoDB table or S3 upload) the API acts according to the role present in the User context based on the token and access the resources.

One advantage of going by this process is that the executing Lambda doesn't have any other extra privileges - it just has the privileges to run the lambda function and push to Cloud Watch logs and nothing more.

This way we can create an entire AWS accessibility setup, with priority on letting the users drive the application permissions rather than giving the lambda full access over the resources.

Author-Image

Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

Hope this article was helpful. You can now show us your support. 😊

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept