anastosios / mi
Azure Managed Identity authentication for Laravel Azure Blob Storage with automatic token caching and refresh
Installs: 69
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/anastosios/mi
Requires
- php: ^7.4|^8.0
- guzzlehttp/guzzle: ^7.0
- illuminate/console: ^8.0|^10.0|^11.0
- illuminate/database: ^8.0|^10.0|^11.0
- illuminate/filesystem: ^8.0|^10.0|^11.0
- illuminate/redis: ^8.0|^10.0|^11.0
- illuminate/support: ^8.0|^10.0|^11.0
- league/flysystem-azure-blob-storage: ^1.0|^3.0
- microsoft/azure-storage-blob: ^1.0|^1.5
This package is auto-updated.
Last update: 2026-02-04 15:48:44 UTC
README
This package enables Azure Managed Identity (MI) authentication for:
- Azure Blob Storage (Laravel
Storagedisk:azure) - Azure Cache for Redis (Laravel Redis client:
azure-mi) - Azure Database for PostgreSQL (Laravel DB connector:
pgsql)
It fetches short-lived access tokens from the Azure Instance Metadata Service (IMDS), caches them (recommended: file cache), then injects them into each client connection so the application can authenticate without long-lived secrets.
Table of Contents
- High-Level Overview
- Token Service
- Configuration
- Azure Blob Storage
- Azure Redis
- Azure PostgreSQL (pgsql)
- Logging
- Security Notes
- Troubleshooting
High-Level Overview
All services follow the same MI pattern:
- Determine if MI is enabled for that service/connection.
- Fetch a token from IMDS:
- Endpoint:
http://169.254.169.254/metadata/identity/oauth2/token - Header:
Metadata: true - Params:
api-version=...resource=...- optional
client_id=...(User Assigned Managed Identity)
- Endpoint:
- Cache the token in Laravel cache (recommended:
file). - Inject the token into the relevant client:
- Storage:
Authorization: Bearer <token> - Redis:
password=<token>(andusername=<aad-user>) - PostgreSQL:
password=<token>
- Storage:
- Connect normally through Laravel APIs.
Token Service
Class
MI\AzureManagedIdentity\Services\AzureManagedIdentityTokenService
Responsibilities
- Read resource configuration from:
config('azure-managed-identity.resources.<key>') - Fetch token from IMDS with the correct:
resourceapi-version- optional
client_id
- Cache tokens by:
- resource key (e.g.
storage,redis,db) - client id (or
system)
- resource key (e.g.
- Apply a buffer before expiry to reduce refresh failures.
Token caching key pattern
azure_managed_identity_token:<resource_key>:<client_id|system>
Why file cache is recommended
- Avoids any circular dependency (for example: if Redis needs MI, you must not depend on Redis to cache the MI token).
- Keeps startup reliable.
- Works well in containers as long as the filesystem is writable.
Configuration
Config File
config/azure-managed-identity.php
return [ 'cache_store' => env('AZURE_MI_CACHE_STORE', 'file'), 'cache_buffer' => env('AZURE_MI_CACHE_BUFFER', 300), 'timeout' => env('AZURE_MI_TIMEOUT', 10), 'resources' => [ 'storage' => [ 'resource' => env('AZURE_MI_STORAGE_RESOURCE', 'https://storage.azure.com/'), 'api_version' => env('AZURE_MI_STORAGE_API_VERSION', '2018-02-01'), 'cache_store' => env('AZURE_MI_STORAGE_CACHE_STORE', null), ], 'redis' => [ 'resource' => env('AZURE_MI_REDIS_RESOURCE', 'https://redis.azure.com'), 'api_version' => env('AZURE_MI_REDIS_API_VERSION', '2019-08-01'), 'cache_store' => env('AZURE_MI_REDIS_CACHE_STORE', null), ], 'db' => [ 'resource' => env('AZURE_MI_DB_RESOURCE', 'https://ossrdbms-aad.database.windows.net'), 'api_version' => env('AZURE_MI_DB_API_VERSION', '2019-08-01'), 'cache_store' => env('AZURE_MI_DB_CACHE_STORE', null), ], ], ];
.env Variables
Minimum recommended variables (based on the provided classes):
# Common AZURE_USE_MANAGED_IDENTITY=true AZURE_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Token caching (recommended) AZURE_MI_CACHE_STORE=file AZURE_MI_CACHE_BUFFER=300 AZURE_MI_TIMEOUT=10 # Storage (Blob) AZURE_MI_STORAGE_RESOURCE=https://storage.azure.com/ AZURE_MI_STORAGE_API_VERSION=2018-02-01 # Redis AZURE_MI_REDIS_RESOURCE=https://redis.azure.com AZURE_MI_REDIS_API_VERSION=2019-08-01 # PostgreSQL AZURE_MI_DB_RESOURCE=https://ossrdbms-aad.database.windows.net AZURE_MI_DB_API_VERSION=2019-08-01 DB_USE_MANAGED_IDENTITY=true DB_SSLMODE=require
Azure Blob Storage
How Blob Works
Classes involved
- Service Provider:
MI\AzureManagedIdentity\Providers\AzureServiceProvider
- Disk adapter:
MI\AzureManagedIdentity\Filesystem\AzureAdapter
- Blob client wrapper:
MI\AzureManagedIdentity\Services\ManagedIdentityBlobRestProxy
- Token fetcher:
MI\AzureManagedIdentity\Services\AzureManagedIdentityTokenService
What happens when you call Storage::disk('azure')?
AzureServiceProvider::boot()registers a custom driver:Storage::extend('azure', ...)
- When the disk is used:
- If
use_managed_identity=truein the disk config:- Fetch token using
TokenService->getAccessToken($clientId, 'storage')
- Fetch token using
- If
- The blob client is created:
ManagedIdentityBlobRestProxy::createWithManagedIdentity(...)
- A custom Guzzle middleware injects:
Authorization: Bearer <token>x-ms-version: 2021-08-06
- Flysystem adapter is created and returned as a Laravel disk.
- Your app uses it like normal:
- list files
- put files
- get files
- delete files
- etc.
Why a custom HTTP client is injected
The microsoft/azure-storage-blob SDK is primarily designed for Shared Key / SAS.
Your implementation injects a Bearer-token aware HTTP client so requests are authenticated using MI.
Blob Mermaid Flow Chart
flowchart TD
A["Storage::disk('azure')"] --> B["Storage driver 'azure' registered"]
B --> C{"use_managed_identity?"}
C -- "No" --> D["Create Blob client using account key"]
C -- "Yes" --> E["TokenService.getAccessToken(clientId, 'storage')"]
E --> F{"Token cached?"}
F -- "Yes" --> G["Use cached token"]
F -- "No" --> H["Call IMDS token endpoint"]
H --> I["Receive access_token + expires_in"]
I --> J["Cache token in file store"]
J --> G
G --> K["Create Blob client"]
K --> L["Inject Authorization header (Bearer token)"]
L --> M["Flysystem Azure adapter"]
M --> N["Storage operations succeed"]
Loading
Blob Test Command
Command: azure:test
php artisan azure:test --disk=azure
What it does:
- If MI is enabled, it fetches a token and performs a direct container list request.
- Then it tries a normal Laravel Storage call (
files()) to confirm end-to-end integration.
Azure Redis
How Redis Works
Classes involved
- Redis connector:
MI\AzureManagedIdentity\Redis\AzureManagedIdentityPhpRedisConnector
- Token fetcher:
MI\AzureManagedIdentity\Services\AzureManagedIdentityTokenService
- Service Provider registration:
MI\AzureManagedIdentity\Providers\AzureServiceProvider
What happens when you call Redis::connection('default')?
- You set:
REDIS_CLIENT=azure-mi
AzureServiceProvider::boot()registers:Redis::extend('azure-mi', ...)
- Laravel uses
AzureManagedIdentityPhpRedisConnectorwhen connecting. - Inside
connect():- If
use_managed_identity=truefor that Redis connection:- Validate
usernameexists (required for Azure Redis AAD auth) - Fetch token via:
TokenService->getAccessToken($clientId, 'redis')
- Set:
config['password'] = <token>config['username'] = <aad-username>
- Validate
- If
- Parent connector connects normally.
- Redis commands work normally (
PING,SET,GET, etc.).
Required Redis config fields
When MI is enabled, the Redis connection config must provide:
username(the AAD principal/object id used by Redis AAD auth)scheme=tls(recommended)port=6380(common for TLS)
Redis Mermaid Flow Chart
flowchart TD
A["Redis::connection(name)"] --> B["REDIS_CLIENT=azure-mi"]
B --> C["AzureManagedIdentityPhpRedisConnector.connect"]
C --> D{"use_managed_identity?"}
D -- "No" --> E["Use normal password flow"]
D -- "Yes" --> F["Require username present"]
F --> G["TokenService.getAccessToken(clientId, 'redis')"]
G --> H{"Token cached?"}
H -- "Yes" --> I["Use cached token"]
H -- "No" --> J["Call IMDS token endpoint"]
J --> K["Receive access_token + expires_in"]
K --> L["Cache token in file store"]
L --> I
I --> M["Set config.username and config.password=token"]
M --> N["Connect via PhpRedis (TLS)"]
N --> O["Redis ready for commands"]
Loading
Redis Test Command
Command: azure:redis-test
php artisan azure:redis-test --connection=default
What it does:
- Connects using Laravel Redis Manager
- Executes:
PINGSETthenGET
Azure PostgreSQL (pgsql)
How DB Works
Classes involved
- Postgres connector:
MI\AzureManagedIdentity\Database\AzureManagedIdentityPostgresConnector
- Token fetcher:
MI\AzureManagedIdentity\Services\AzureManagedIdentityTokenService
- Service Provider binding:
MI\AzureManagedIdentity\Providers\AzureServiceProvider
What happens when Laravel connects to pgsql?
- In
AzureServiceProvider::register(), the package binds:db.connector.pgsql→AzureManagedIdentityPostgresConnector
- When Laravel creates a pgsql connection, it uses this connector.
- Inside
connect(array $config):- Determine if MI is enabled:
use_managed_identityin the pgsql config
- If enabled:
- Fetch token via:
TokenService->getAccessToken($clientId, 'db')
- Set:
config['password'] = <token>
- Ensure SSL:
- If
sslmodeis empty, set it torequire
- If
- Fetch token via:
- Determine if MI is enabled:
- The connector calls
parent::connect($config)which creates the real PDO connection. - From the application point of view, DB queries work normally.
What the DB token represents
For Azure Database for PostgreSQL with Entra/AAD auth, the access token acts like a short-lived password for the configured DB user. Your DevOps verification command mirrors that behavior:
- fetch token from IMDS for resource:
https://ossrdbms-aad.database.windows.net
- use it as
PGPASSWORD - connect using TLS (
sslmode=require)
DB Mermaid Flow Chart
flowchart TD
A["Laravel DB connection pgsql"] --> B["db.connector.pgsql overridden"]
B --> C["AzureManagedIdentityPostgresConnector.connect"]
C --> D{"use_managed_identity?"}
D -- "No" --> E["Use DB_PASSWORD from env"]
D -- "Yes" --> F["TokenService.getAccessToken(clientId, 'db')"]
F --> G{"Token cached?"}
G -- "Yes" --> H["Use cached token"]
G -- "No" --> I["Call IMDS token endpoint"]
I --> J["Receive access_token + expires_in"]
J --> K["Cache token in file store"]
K --> H
H --> L["Set config.password=token"]
L --> M{"sslmode empty?"}
M -- "Yes" --> N["Set sslmode=require"]
M -- "No" --> O["Keep existing sslmode"]
N --> P["Create PDO connection"]
O --> P
P --> Q["DB ready for queries"]
Loading
Notes About Read/Write Hosts
Your database.php supports read replicas:
'read' => ['host' => [env('DB_HOST_READ'), env('DB_HOST_READ_1')]], 'write' => ['host' => [env('DB_HOST_WRITE')]],
Laravel may connect to:
- write host for write operations
- read host for read operations (depending on query patterns and runtime)
Managed Identity still works because:
- The connector injects the token as the password for every pgsql connection attempt.
- SSL mode should be
requirefor Azure PostgreSQL.
If you experience authentication issues only on read replicas, that usually means the replica side is not configured to accept the same Entra/AAD principal or security rules differ.
Logging
The package logs:
- service start (storage/redis/db)
- cached token usage vs IMDS call
- token caching TTL (without printing the token)
- connection success
Recommended:
- Keep
LOG_LEVEL=debugin non-prod environments while validating. - Never log the raw token; only log token length if needed.
Security Notes
- Use
AZURE_MI_CACHE_STORE=fileto avoid circular dependencies (Redis itself might require MI). - Tokens are short-lived and automatically refreshed before expiry by the cache buffer strategy.
- Use TLS for:
- Azure Redis (commonly
scheme=tlsandport=6380) - Azure PostgreSQL (
sslmode=require)
- Azure Redis (commonly
- Ensure the filesystem used by file cache is writable by the application user and not world-readable.
Troubleshooting
Storage fails (403/401)
- Ensure the managed identity has correct RBAC on the Storage Account:
- typically
Storage Blob Data Contributor(or as required)
- typically
- Validate
AZURE_MI_STORAGE_RESOURCEishttps://storage.azure.com/
Redis fails
Common causes:
- Missing
usernamein redis connection config - Not using TLS or using wrong port
- Wrong resource:
- must be
https://redis.azure.com
- must be
- AAD not enabled/configured for the Redis instance
DB fails (password authentication failed)
Common causes:
DB_USERNAMEdoes not match the Entra/AAD configured DB user/principal- wrong DB token resource:
- must be
https://ossrdbms-aad.database.windows.net
- must be
- SSL not required:
- set
DB_SSLMODE=require
- set
- read replicas not configured the same as primary