diff --git a/docs/assets/azure-ad-token.png b/docs/assets/azure-ad-token.png
new file mode 100644
index 0000000..6420059
Binary files /dev/null and b/docs/assets/azure-ad-token.png differ
diff --git a/docs/assets/catalog-api-managed-identity.png b/docs/assets/catalog-api-managed-identity.png
new file mode 100644
index 0000000..4ac6146
Binary files /dev/null and b/docs/assets/catalog-api-managed-identity.png differ
diff --git a/docs/assets/redis-add-new-user.png b/docs/assets/redis-add-new-user.png
new file mode 100644
index 0000000..748575f
Binary files /dev/null and b/docs/assets/redis-add-new-user.png differ
diff --git a/docs/assets/redis-assign-role-to-user.png b/docs/assets/redis-assign-role-to-user.png
new file mode 100644
index 0000000..4590e7e
Binary files /dev/null and b/docs/assets/redis-assign-role-to-user.png differ
diff --git a/docs/assets/redis-enable-aad.png b/docs/assets/redis-enable-aad.png
new file mode 100644
index 0000000..4c809f3
Binary files /dev/null and b/docs/assets/redis-enable-aad.png differ
diff --git a/docs/assets/redis-validate-user-creation.png b/docs/assets/redis-validate-user-creation.png
new file mode 100644
index 0000000..a6b50ea
Binary files /dev/null and b/docs/assets/redis-validate-user-creation.png differ
diff --git a/docs/workshop.md b/docs/workshop.md
index 7ea921e..fa217ed 100644
--- a/docs/workshop.md
+++ b/docs/workshop.md
@@ -1339,6 +1339,158 @@ As a side note, we really encourage you to take the time to dig in the toolbox o
---
+# Lab 5 : AAD + RBAC
+
+In this Lab you will focus on securing connections to Azure Cache for Redis by replacing secrets (e.g. connection strings) with [AAD-integration and RBAC][redis-aad-auth].
+
+
+
+Here are the steps for securing your application with AAD and RBAC:
+- You enable the AAD-integration in your Azure Cache for Redis resource.
+- You assign a role to your application's identity (e.g. System assigned identity) to allow it to access data in Redis.
+- Your application requests a token from Azure AD. This can be done automatically using a Redis client library (if it supports AAD-integration) otherwise you need to use an authentication library like [Microsoft Authentication Library][msal] to get the token.
+- The application then uses that token as a password to establish a connection to Azure Cache for Redis.
+- The application uses the connection to communicate with Redis.
+- Before the expiry of the AAD token, your application needs to refresh the token (e.g. via [MSAL][msal]) to avoid losing access to the Azure Cache for Redis instance.
+
+The goal of this lab is to update `catalog-api` to use AAD and RBAC instead of a Connection String.
+`catalog-api` is already using [Microsoft.Azure.StackExchangeRedis][microsoft-azure-stackexchangeredis] which extends the Redis client [StackExchange.Redis][stackexchange-redis] to support AAD-integration by handling AAD token fetching and refreshing.
+
+## Enabling AAD-integration
+
+
+
+> Enable `AAD access authorization` on your Azure Cache for Redis resource from the `Advanced settings` menu
+
+
+
+
+
+> This operation may take few minutes to take effect in a real-life scenario but in this lab it should be fairly quick
+
+
+
+
+
+Toggle solution
+
+Head to the `Advanced settings` menu and then check the checkbox of `AAD access authorization`:
+
+
+
+
+
+## Assigning a role
+
+
+
+> Assign the `Data Contributor` role to the system-assigned identity of your `catalog-api`
+
+
+
+
+
+> [Configure RBAC with Data Access Policy][configure-rbac-with-data-access-policy]
+
+
+
+
+
+Toggle solution
+
+Head to the `Data Access Configuration` menu, then click on the `Add` button, and choose `New Redis User`:
+
+
+
+Select the `Data Contributor` policy, click on the `Redis Users` tab and select `Managed Identity`, and then click on the `Select member` button.
+
+
+
+Select `App Service` in the type of Managed Identity, then select your App Service Web App in which `catalog-api` is deployed, and validate your selection by clicking the `Select` button.
+
+Finally click on `Review + assign` to crate the new Redis user:
+
+
+
+
+
+
+## Updating catalog-api to use the AAD-integration
+
+So far, you were using a Connection String to connect to the Azure Cache for Redis resource.
+
+Connections are currently established using the following code:
+
+```csharp
+var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(_connectionString!, AzureCacheForRedis.ConfigureForAzure);
+```
+
+To use system-assigned identities, the code above needs to be replaced with:
+
+```csharp
+var configurationOptions = await ConfigurationOptions.Parse($"{_hostname}:{_port}").ConfigureForAzureWithSystemAssignedManagedIdentityAsync(_managedIdentityPrincipalId!);
+var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions);
+```
+
+
+
+> - Update the code of the `GetDatabaseAsync` method in `src/catalog-api/RedisService.cs` to use system-assigned identity of the App Service Web App
+> - Set the value of the `AZURE_MANAGED_IDENTITY_PRINCIPAL_ID` app setting to the system-assigned identity of the App Service Web App (`Object (principal) ID`)
+
+
+
+
+
+> The variables `_hostname`, `_port`, and `_managedIdentityPrincipalId` were already defined in `src/catalog-api/RedisService.cs`
+
+
+
+
+
+Toggle solution
+
+Open the file `src/catalog-api/RedisService.cs` and locate the `GetDatabaseAsync` method.
+
+Replace the `connectionMultiplexer` variable initialization logic in the first code block above (which uses a connection string) with the contents of the second code block (which uses a managed identity principal ID).
+
+Next, you need to re-deploy your code to App Service like what you did in Lab 1 using the Visual Studio Code Azure extension and the `Deploy to Web App...` option.
+
+Afterwards, go to the `Identity` menu on your App Service resource, and copy the value of `Object (principal) ID`.
+
+
+
+Finally, go to the `Configuration` menu of your App Service resource and set the app setting `AZURE_MANAGED_IDENTITY_PRINCIPAL_ID` to the value of `Object (principal) ID`.
+
+Validate the change by clicking `Ok`, then `Save` and you should be all set now.
+
+
+
+## Testing the new setup
+
+
+
+> Use the Web App to view some products and ensure `catalog-api` can still use your Azure Cache for Redis instance
+
+
+
+
+
+Toggle solution
+
+Open the Static Web App and make sure you can still see a list of products:
+
+
+
+
+
+[redis-aad-auth]: https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-azure-active-directory-for-authentication
+[microsoft-azure-stackexchangeredis]: https://github.com/Azure/Microsoft.Azure.StackExchangeRedis
+[stackexchange-redis]: https://github.com/StackExchange/StackExchange.Redis
+[configure-rbac-with-data-access-policy]: https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-configure-role-based-access-control
+[msal]: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview
+
+---
+
# Closing the workshop
The **Product Hands on Lab : Azure Cache for Redis in Azure World** comes to an end : We hope you liked practicing with Azure solutions and that this lab will help you kick start your journey to caching in Azure.
diff --git a/solutions/catalog-api/RedisService.cs b/solutions/catalog-api/RedisService.cs
new file mode 100644
index 0000000..34f9cc9
--- /dev/null
+++ b/solutions/catalog-api/RedisService.cs
@@ -0,0 +1,106 @@
+using Microsoft.Azure.StackExchangeRedis;
+using StackExchange.Redis;
+
+public interface IRedisService {
+ Task Get(string key);
+ Task Set(string key, string value);
+ Task AddToStream(string streamName, Dictionary data);
+}
+
+public class RedisService : IRedisService
+{
+ private IDatabase? _database = null;
+ private readonly string? _connectionString;
+ private readonly string? _managedIdentityPrincipalId;
+ private readonly string? _hostname;
+ private readonly string? _port;
+
+ private readonly int _defaultTTLInSeconds = 60;
+ private readonly TimeSpan _ttl; // Time To Live
+
+ public RedisService(IConfiguration configuration)
+ {
+ _ttl = TTL(configuration["AZURE_REDIS_TTL_IN_SECONDS"]);
+ _connectionString = configuration["AZURE_REDIS_CONNECTION_STRING"];
+ _managedIdentityPrincipalId = configuration["AZURE_MANAGED_IDENTITY_PRINCIPAL_ID"];
+ _port = configuration["AZURE_REDIS_PORT"];
+ _hostname = configuration["AZURE_REDIS_HOSTNAME"];
+ }
+
+ private async Task GetDatabaseAsync() {
+ if (_database != null) {
+ return _database;
+ }
+
+ Console.WriteLine("Initializing Redis database connection");
+
+ // var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(_connectionString!, AzureCacheForRedis.ConfigureForAzure);
+ var configurationOptions = await ConfigurationOptions.Parse($"{_hostname}:{_port}").ConfigureForAzureWithSystemAssignedManagedIdentityAsync(_managedIdentityPrincipalId!);
+ var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configurationOptions);
+
+ _database = connectionMultiplexer.GetDatabase();
+
+ Console.WriteLine("Redis database connection established");
+
+ return _database;
+ }
+
+ private TimeSpan TTL(string? ttlInSecondsAsString)
+ {
+ int ttlInSeconds;
+
+ try
+ {
+ ttlInSeconds = string.IsNullOrEmpty(ttlInSecondsAsString) ? _defaultTTLInSeconds : int.Parse(ttlInSecondsAsString);
+ }
+ catch
+ {
+ ttlInSeconds = _defaultTTLInSeconds;
+ }
+
+ if (ttlInSeconds <= 0) {
+ ttlInSeconds = _defaultTTLInSeconds;
+ }
+
+ return TimeSpan.FromSeconds(ttlInSeconds);
+ }
+
+ public async Task Get(string key)
+ {
+ try
+ {
+ var database = await GetDatabaseAsync();
+ var value = await database.StringGetAsync(key);
+ var stringValue = value.ToString();
+
+ if (stringValue == string.Empty) {
+ return null;
+ }
+
+ return stringValue;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public async Task Set(string key, string value)
+ {
+ var database = await GetDatabaseAsync();
+ await database.StringSetAsync(key, value, _ttl);
+ }
+
+ public async Task AddToStream(string streamName, Dictionary data)
+ {
+ List entries = new();
+
+ foreach(KeyValuePair keyValuePair in data)
+ {
+ entries.Add(new(keyValuePair.Key, keyValuePair.Value));
+ }
+
+ var database = await GetDatabaseAsync();
+ await database.StreamAddAsync(streamName, entries.ToArray());
+ }
+}
\ No newline at end of file
diff --git a/src/catalog-api/RedisService.cs b/src/catalog-api/RedisService.cs
index 1e0f36c..5affafb 100644
--- a/src/catalog-api/RedisService.cs
+++ b/src/catalog-api/RedisService.cs
@@ -1,3 +1,4 @@
+using Microsoft.Azure.StackExchangeRedis;
using StackExchange.Redis;
public interface IRedisService {
@@ -8,16 +9,38 @@ public interface IRedisService {
public class RedisService : IRedisService
{
- // TODO: provide a sample with AAD authentication
- private readonly IDatabase _database;
+ private IDatabase? _database = null;
+ private readonly string? _connectionString;
+ private readonly string? _managedIdentityPrincipalId;
+ private readonly string? _hostname;
+ private readonly string? _port;
+
private readonly int _defaultTTLInSeconds = 60;
private readonly TimeSpan _ttl; // Time To Live
public RedisService(IConfiguration configuration)
{
- var connectionMultiplexer = ConnectionMultiplexer.Connect(configuration["AZURE_REDIS_CONNECTION_STRING"], AzureCacheForRedis.ConfigureForAzure);
- _database = connectionMultiplexer.GetDatabase();
_ttl = TTL(configuration["AZURE_REDIS_TTL_IN_SECONDS"]);
+ _connectionString = configuration["AZURE_REDIS_CONNECTION_STRING"];
+ _managedIdentityPrincipalId = configuration["AZURE_MANAGED_IDENTITY_PRINCIPAL_ID"];
+ _port = configuration["AZURE_REDIS_PORT"];
+ _hostname = configuration["AZURE_REDIS_HOSTNAME"];
+ }
+
+ private async Task GetDatabaseAsync() {
+ if (_database != null) {
+ return _database;
+ }
+
+ Console.WriteLine("Initializing Redis database connection");
+
+ var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(_connectionString!, AzureCacheForRedis.ConfigureForAzure);
+
+ _database = connectionMultiplexer.GetDatabase();
+
+ Console.WriteLine("Redis database connection established");
+
+ return _database;
}
private TimeSpan TTL(string? ttlInSecondsAsString)
@@ -26,7 +49,7 @@ private TimeSpan TTL(string? ttlInSecondsAsString)
try
{
- ttlInSeconds = String.IsNullOrEmpty(ttlInSecondsAsString) ? _defaultTTLInSeconds : Int32.Parse(ttlInSecondsAsString);
+ ttlInSeconds = string.IsNullOrEmpty(ttlInSecondsAsString) ? _defaultTTLInSeconds : int.Parse(ttlInSecondsAsString);
}
catch
{
@@ -44,7 +67,8 @@ private TimeSpan TTL(string? ttlInSecondsAsString)
{
try
{
- var value = await _database.StringGetAsync(key);
+ var database = await GetDatabaseAsync();
+ var value = await database.StringGetAsync(key);
var stringValue = value.ToString();
if (stringValue == string.Empty) {
@@ -61,7 +85,8 @@ private TimeSpan TTL(string? ttlInSecondsAsString)
public async Task Set(string key, string value)
{
- await _database.StringSetAsync(key, value, _ttl);
+ var database = await GetDatabaseAsync();
+ await database.StringSetAsync(key, value, _ttl);
}
public async Task AddToStream(string streamName, Dictionary data)
@@ -73,6 +98,7 @@ public async Task AddToStream(string streamName, Dictionary data
entries.Add(new(keyValuePair.Key, keyValuePair.Value));
}
- await _database.StreamAddAsync(streamName, entries.ToArray());
+ var database = await GetDatabaseAsync();
+ await database.StreamAddAsync(streamName, entries.ToArray());
}
}
\ No newline at end of file
diff --git a/src/catalog-api/appsettings.json.template b/src/catalog-api/appsettings.json.template
index 132cd3b..2dc4d1a 100644
--- a/src/catalog-api/appsettings.json.template
+++ b/src/catalog-api/appsettings.json.template
@@ -9,6 +9,9 @@
"AZURE_COSMOS_CONNECTION_STRING": "YOUR_COSMOS_CONNECTION_STRING",
"AZURE_COSMOS_DATABASE": "catalogdb",
"AZURE_REDIS_CONNECTION_STRING":"YOUR_REDIS_CONNECTION_STRING",
+ "AZURE_MANAGED_IDENTITY_PRINCIPAL_ID": "MANAGED_IDENTITY_PRINCIPAL_ID_FOR_AAD_INTEGRATION",
+ "AZURE_REDIS_PORT": "OPTIONAL_REDIS_PORT_FOR_ADD_INTEGRATION",
+ "AZURE_REDIS_HOSTNAME": "REDIS_HOSTNAME_FOR_ADD_INTEGRATION",
"PRODUCT_LIST_CACHE_DISABLE":"0",
"SIMULATED_DB_LATENCY_IN_SECONDS": "",
"PRODUCT_VIEWS_STREAM_NAME": "productViews"
diff --git a/terraform/app.tf b/terraform/app.tf
index 57831a7..a4f92c9 100644
--- a/terraform/app.tf
+++ b/terraform/app.tf
@@ -15,6 +15,9 @@ resource "azurerm_linux_web_app" "this" {
AZURE_COSMOS_CONNECTION_STRING = azurerm_cosmosdb_account.this.connection_strings[0]
AZURE_COSMOS_DATABASE = "catalogdb"
AZURE_REDIS_CONNECTION_STRING = azurerm_redis_cache.this.primary_connection_string
+ AZURE_REDIS_HOSTNAME = azurerm_redis_cache.this.hostname
+ AZURE_REDIS_PORT = azurerm_redis_cache.this.ssl_port
+ AZURE_MANAGED_IDENTITY_PRINCIPAL_ID = ""
PRODUCT_LIST_CACHE_DISABLE = "0"
SIMULATED_DB_LATENCY_IN_SECONDS = "2"
APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.this.instrumentation_key