C# Storing API Keys in Redis Cache

Print Friendly, PDF & Email

C# Storing API Keys in Redis Cache

In today’s interconnected digital landscape, managing API keys securely is a critical challenge for many organizations. This article explores an innovative approach that combines the power of Redis, a high-performance in-memory data store, with the robust security of Windows authentication to create a secure, efficient, and scalable API key management solution.

The Challenge of API Key Management

API keys are the lifeblood of modern software integrations, but they also represent a significant security risk if not managed properly. Traditional approaches often involve hardcoding keys into application code or configuration files, which can lead to security vulnerabilities and make key rotation a nightmare.

Our Solution: Redis + Windows Authentication

Our approach leverages two powerful technologies:

  1. Redis: An in-memory data structure store, used as a database, cache, and message broker.
  2. Windows Authentication: A secure, integrated authentication method built into Windows operating systems.
    • This could also be an Existing Authentication, such as re-using the Authentication in 3DExperience to get users privileges.

By combining these technologies, we create a system that is both secure and easy to manage.

How It Works

  1. User Authentication:
    • Users log into the application using their Windows credentials, or App Credentials.
    • This ensures that only authorized users can access the application.
  2. Application Authentication:
    • Once a user is authenticated, the application requests an API key from Redis.
    • This adds an extra layer of security, as only authenticated applications can access the keys.
  3. Secure Storage:
    • API keys are stored in Redis in an encrypted format.
    • Keys are only accessible within the controlled environment.
  4. Key Retrieval and Usage:
    • The application retrieves the encrypted key from Redis and decrypts it.
    • The decrypted key is then used to make API calls.

Benefits of This Approach

  1. Enhanced Security: Multiple layers of authentication and encryption protect the API keys.
  2. Centralized Management: All API keys are stored in one secure location, making management and rotation easier.
  3. Scalability: Redis is highly scalable, allowing this solution to grow with your organization.
  4. Auditability: It’s easy to log and monitor key usage, enhancing security and compliance.
  5. Simplified User Experience: Users don’t need to manage additional credentials beyond their Windows login.

Implementation Considerations

While this approach offers significant benefits, there are some important considerations for implementation:

  1. Encryption: Use strong, industry-standard encryption for storing keys in Redis.
  2. Key Rotation: Implement a robust strategy for regular key rotation.
  3. Monitoring: Set up comprehensive monitoring and alerting for any suspicious access patterns.
  4. Disaster Recovery: Ensure you have a solid backup and recovery plan for your Redis data.

Conclusion

By leveraging Redis for secure API key storage and Windows authentication for user verification, organizations can create a robust, scalable, and user-friendly API key management system. This approach addresses many of the traditional challenges associated with API key management, providing a balance of security, usability, and scalability.

As with any security solution, it’s crucial to regularly review and update your system to address new threats and challenges. With careful implementation and ongoing management, this Redis-based approach can significantly enhance your organization’s API security posture.

using System;
using System.Security.Cryptography;
using System.Text;
using StackExchange.Redis;

public class RedisApiKeyHelper
{
    private string EncryptString(string plainText)
    {
        using (Aes aes = Aes.Create())
        {
            aes.Key = Encoding.UTF8.GetBytes(_encryptionKey);
            aes.IV = new byte[16]; // Use a fixed IV for simplicity. In production, use a random IV for each encryption and prepend it to the ciphertext.

            using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
            using (var ms = new System.IO.MemoryStream())
            {
                using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                using (var sw = new System.IO.StreamWriter(cs))
                {
                    sw.Write(plainText);
                }
                return Convert.ToBase64String(ms.ToArray());
            }
        }
    }

    private string DecryptString(string cipherText)
    {
        using (Aes aes = Aes.Create())
        {
            aes.Key = Encoding.UTF8.GetBytes(_encryptionKey);
            aes.IV = new byte[16]; // Use the same fixed IV as in EncryptString

            using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
            using (var ms = new System.IO.MemoryStream(Convert.FromBase64String(cipherText)))
            using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
            using (var sr = new System.IO.StreamReader(cs))
            {
                return sr.ReadToEnd();
            }
        }
    }
}

Now that we can Encrypt and Decrypt the API Key, we need to be able to store an Encrypted API key to Redis.

using System;
using System.Security.Cryptography;
using System.Text;
using StackExchange.Redis;

public class RedisApiKeyHelper
{
    private readonly IConnectionMultiplexer _redis;
    private readonly string _encryptionKey;

    public RedisApiKeyHelper(string redisConnectionString, string encryptionKey)
    {
        _redis = ConnectionMultiplexer.Connect(redisConnectionString);
        _encryptionKey = encryptionKey;
    }

    public void StoreApiKey(string keyName, string apiKey)
    {
        var encryptedKey = EncryptString(apiKey);
        var db = _redis.GetDatabase();
        db.StringSet(keyName, encryptedKey);
    }
}

Finally, Retrieval of the API Key and its Decryption.

using System;
using System.Security.Cryptography;
using System.Text;
using StackExchange.Redis;

public class RedisApiKeyHelper
{
    private readonly IConnectionMultiplexer _redis;
    private readonly string _encryptionKey;

    public RedisApiKeyHelper(string redisConnectionString, string encryptionKey)
    {
        _redis = ConnectionMultiplexer.Connect(redisConnectionString);
        _encryptionKey = encryptionKey;
    }

    public string RetrieveApiKey(string keyName)
    {
        var db = _redis.GetDatabase();
        var encryptedKey = db.StringGet(keyName);
        if (encryptedKey.IsNull)
        {
            throw new KeyNotFoundException($"API key '{keyName}' not found in Redis.");
        }
        return DecryptString(encryptedKey);
    }
}

Example

Below is a simple example of how to encrypt, store and retrieve the Redis API key.

var helper = new RedisApiKeyHelper("your_redis_connection_string", "your_32_byte_encryption_key");

// Storing an API key
helper.StoreApiKey("myApiKey", "abc123xyz789");

// Retrieving an API key
string apiKey = helper.RetrieveApiKey("myApiKey");

Redis Connection Helper

Below is a Connection Helper for Redis

using System;
using StackExchange.Redis;
using Microsoft.Extensions.Logging;

public class RedisConnectionHelper
{
    private static Lazy<ConnectionMultiplexer> lazyConnection;
    private static ILogger logger;

    public static void Initialize(string connectionString, ILogger loggerInstance)
    {
        logger = loggerInstance;
        lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
        {
            return ConnectionMultiplexer.Connect(connectionString);
        });
    }

    public static ConnectionMultiplexer Connection
    {
        get
        {
            if (lazyConnection == null)
            {
                throw new InvalidOperationException("RedisConnectionHelper is not initialized. Call Initialize method first.");
            }
            return lazyConnection.Value;
        }
    }

    public static IDatabase GetDatabase(int db = -1)
    {
        return Connection.GetDatabase(db);
    }

    public static void FlushDatabase(int db = -1)
    {
        var server = GetServer();
        server.FlushDatabase(db);
    }

    public static IServer GetServer()
    {
        var endpoints = Connection.GetEndPoints();
        return Connection.GetServer(endpoints[0]);
    }

    public static void CloseConnection()
    {
        if (lazyConnection != null && lazyConnection.IsValueCreated)
        {
            Connection.Close();
        }
    }

    public static bool TryExecute(Action redisAction)
    {
        try
        {
            redisAction();
            return true;
        }
        catch (RedisConnectionException ex)
        {
            logger?.LogError(ex, "Redis connection error");
            return false;
        }
        catch (RedisTimeoutException ex)
        {
            logger?.LogError(ex, "Redis timeout error");
            return false;
        }
        catch (Exception ex)
        {
            logger?.LogError(ex, "Unexpected Redis error");
            return false;
        }
    }
}

Using the connection helper.

// Initialize the helper at application startup:

RedisConnectionHelper.Initialize("your-redis-server.com:6379,password=your-secure-password,ssl=true,abortConnect=false", loggerInstance);

// Use the helper to perform Redis operations:

var db = RedisConnectionHelper.GetDatabase();
bool success = RedisConnectionHelper.TryExecute(() =>
{
    db.StringSet("myKey", "myValue");
    var value = db.StringGet("myKey");
    Console.WriteLine(value);
});

// Close the connection when your application shuts down:

RedisConnectionHelper.CloseConnection();

App.Config

We can store the connection string in the App.Config file, as shown below.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="RedisConnection" connectionString="your-redis-server.com:6379,password=your-secure-password,ssl=true,abortConnect=false" />
  </connectionStrings>
  <appSettings>
    <add key="RedisDatabase" value="0" />
  </appSettings>
</configuration>

we can also create a Config Helper to retrieve the connection string.

using System.Configuration;

public static class ConfigHelper
{
    public static string GetRedisConnectionString()
    {
        return ConfigurationManager.ConnectionStrings["RedisConnection"].ConnectionString;
    }

    public static int GetRedisDatabase()
    {
        string databaseString = ConfigurationManager.AppSettings["RedisDatabase"];
        if (int.TryParse(databaseString, out int database))
        {
            return database;
        }
        return 0; // Default to database 0 if parsing fails
    }
}

We can then retrieve the connection string easily.

string redisConnectionString = ConfigHelper.GetRedisConnectionString();