AWS IAM Database Authentication with EF Core

AWS IAM Database Authentication with EF Core

AWS Aurora (both MySQL and Postgres) and MariaDB allow IAM authentication. With this authentication method, you don't need to use a password when you connect to a database. Instead, you use an authentication token. In this post, we are going to explore how to use IAM database authentication with EF Core and Aurora Postgres.

Before starting, you need to fulfill a few pre-requisites:

The first step is to create an IAM policy using this template:

{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Action": [
             "rds-db:connect"
         ],
         "Resource": [
             "arn:aws:rds-db:[region]:[account-id]:dbuser:[db-cluster-resourceId]/[db-user-name]"
         ]
      }
   ]
}
  • [region] is the AWS Region for the database cluster. An example of a region is us-east-2.
  • [account-id] is the AWS account number for the database cluster. An example of an account number is 1234567890.
  • [db-cluster-resourceId] is the identifier for the database cluster. An example of this identifier is cluster-ABCDEFGHIJKL01234.
  • [db-user-name] is the name of the database account to associate with IAM authentication.

Once we have that, we need to attach it to our IAM user. The next step is to create a database (we are using the postgres user to run the following scripts):

CREATE DATABASE tasksmanager
WITH 
ENCODING = 'UTF8';

And then a table inside this new database:

CREATE TABLE tasks(
  id UUID PRIMARY KEY,
  name VARCHAR(255)
);

And finally, the database user:

CREATE USER db_iam_user; 
GRANT rds_iam TO db_iam_user;
GRANT postgres TO db_iam_user;

Let's do some code now, we are going to create a .NET ASP Core Web API application. Add the following NuGet packages to that application:

In our appsetting.json, we are going to add a ConnectionString property:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionString": "host=[aurora-database-endpoint];Database=tasksmanager;Username=db_iam_user;SSL Mode=Require;TrustServerCertificate=true"
}

Add the a file with the following content:

    public class Task
    {
        public Guid Id { get; set; }
        public string Name { get; set; }

        private Task()
        {

        }

        public Task(string name)
        {
            Id = Guid.NewGuid();
            Name = name;
        }
    }

With that, we can create our DbContext class:

    public class ApplicationDbContext : DbContext
    {
        public DbSet<Task> Tasks { get; set; }

        public ApplicationDbContext(DbContextOptions options) : base(options)
        {

        }
    }

Now, add a ServiceCollectionExtensions.cs file. Here is where all the magic happens:

    public static class ServiceCollectionExtensions
    {
        public static IServiceCollection AddPostgreSQL(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                var connectionString = configuration["ConnectionString"];
                options.UseNpgsql(connectionString, npgsqlOption => { npgsqlOption.UseAwsIamAuthentication(); });
                options.UseLowerCaseNamingConvention();
            });
            return services;
        }

        public static NpgsqlDbContextOptionsBuilder UseAwsIamAuthentication(this NpgsqlDbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.ProvidePasswordCallback(RequestAwsIamAuthToken);
            return optionsBuilder;
        }

        static string RequestAwsIamAuthToken(string host, int port, string database, string username)
        {
            return RDSAuthTokenGenerator.GenerateAuthToken(host, port, username);
        }
    }

And then, add the following line to your Program.cs file:

builder.Services.AddPostgreSQL(builder.Configuration);

Add a couple of models for our request and response:

    public class RegisterTaskCommand
    {
        public string Name { get; set; }
    }

    public class ListTasksResult
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }

And finally, our TaskController.cs file:

    [ApiController]
    [Route("[controller]")]
    public class TasksController : ControllerBase
    {
        private readonly ApplicationDbContext _context;
        public TasksController(ApplicationDbContext context)
        {
            _context = context;
        }

        [HttpGet()]
        public Task<ListTasksResult[]> ListTasks()
        {
            return _context.Tasks.Select(t => new ListTasksResult() { Id = t.Id, Name = t.Name }).ToArrayAsync();
        }

        [HttpPost()]
        public async Task<Guid> RegisterTask(RegisterTaskCommand command)
        {
            var task = new Task(command.Name);
            _context.Tasks.Add(task);
            await _context.SaveChangesAsync();
            return task.Id;
        }
    }

That's it, run your application and test your passwordless connection against your database. Here, you can find all the code and database scripts.