Your application now supports two authentication modes:
- AdminScheme (Custom API Key) - Simple auth for local development
- Azure AD (JWT Bearer) - Enterprise authentication for production
The app automatically selects Azure AD if TenantId and ClientId are configured, otherwise falls back to AdminScheme.
You've already completed these steps:
- ✓ Created app registration
- ✓ Exposed API with
access_as_adminscope - ✓ Created
Adminapp role - ✓ Assigned users to Admin role
- ✓ Configured redirect URIs
From the Azure Portal app registration overview page:
- Application (client) ID: e.g.,
12345678-1234-1234-1234-123456789abc - Directory (tenant) ID: e.g.,
87654321-4321-4321-4321-cba987654321 - Audience: Same as your Client ID (or use
api://{ClientId})
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "YOUR_TENANT_ID_HERE",
"ClientId": "YOUR_CLIENT_ID_HERE",
"Audience": "YOUR_CLIENT_ID_HERE"
}
}# Windows PowerShell
$env:AzureAd__TenantId = "YOUR_TENANT_ID"
$env:AzureAd__ClientId = "YOUR_CLIENT_ID"
$env:AzureAd__Audience = "YOUR_CLIENT_ID"
# Linux/macOS
export AzureAd__TenantId="YOUR_TENANT_ID"
export AzureAd__ClientId="YOUR_CLIENT_ID"
export AzureAd__Audience="YOUR_CLIENT_ID"-
Get Access Token from Azure AD:
- Open a browser to:
https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/authorize?client_id={ClientId}&response_type=token&redirect_uri=https://localhost:5001&scope=api://{ClientId}/access_as_admin&response_mode=fragment - Sign in with a user that has the Admin role
- Copy the access token from the redirect URL (after
#access_token=)
- Open a browser to:
-
Test Protected Endpoint:
GET https://localhost:5001/api/admin/puzzles Authorization: Bearer YOUR_ACCESS_TOKEN_HERE
# Get access token
$tenantId = "YOUR_TENANT_ID"
$clientId = "YOUR_CLIENT_ID"
$scope = "api://$clientId/access_as_admin"
# This will open browser for interactive login
$token = # (Use browser method above, or configure client secret for app-only flow)
# Test API
$headers = @{
"Authorization" = "Bearer $token"
}
Invoke-RestMethod -Uri "https://localhost:5001/api/admin/puzzles" -Headers $headersIn Development environment, Azure AD is not required (falls back to AdminScheme with bypass):
# Should work without any authentication
Invoke-RestMethod -Uri "https://localhost:5001/api/admin/puzzles"If you want your admin pages (admin.html, delete.html, users.html) to work with Azure AD in production:
Add to your HTML files:
<script src="https://alcdn.msauth.net/browser/2.38.0/js/msal-browser.min.js"></script>const msalConfig = {
auth: {
clientId: "YOUR_CLIENT_ID",
authority: "https://login.microsoftonline.com/YOUR_TENANT_ID",
redirectUri: window.location.origin
}
};
const loginRequest = {
scopes: ["api://YOUR_CLIENT_ID/access_as_admin"]
};
const msalInstance = new msal.PublicClientApplication(msalConfig);
async function getAccessToken() {
try {
const account = msalInstance.getAllAccounts()[0];
if (!account) {
await msalInstance.loginPopup(loginRequest);
}
const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: msalInstance.getAllAccounts()[0]
});
return response.accessToken;
} catch (error) {
console.error("Auth error:", error);
const response = await msalInstance.loginPopup(loginRequest);
return response.accessToken;
}
}
// Use in your API calls
async function fetchProtectedData() {
const token = await getAccessToken();
const response = await fetch('/api/admin/puzzles', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}- Azure AD config empty → Uses AdminScheme
BypassInDevelopment: true→ No authentication required- All admin endpoints accessible without credentials
- Azure AD config set → Uses Azure AD (JWT Bearer)
- Validates JWT tokens from Microsoft Identity Platform
- Requires users to have Admin role in Azure AD
- Tokens contain user claims and role information
- Azure AD config empty → Uses AdminScheme
BypassInDevelopment: false→ X-Admin-Key header required- Simple API key authentication
-
Check token validity:
- Decode JWT at https://jwt.ms
- Verify
aud(audience) matches your ClientId - Verify
rolesarray contains "Admin" - Check
exp(expiration) hasn't passed
-
Check app configuration:
- TenantId and ClientId are correct
- User is assigned Admin role in Azure Portal
- Token scope matches
api://{ClientId}/access_as_admin
-
Check logs:
dotnet run --environment ProductionLook for authentication-related errors
- Restart the application after changing
appsettings.Production.json - Verify environment is set to Production:
$env:ASPNETCORE_ENVIRONMENT = "Production" - Check configuration loaded: Add logging to Program.cs
In Azure Portal:
- Go to App Roles → Verify "Admin" role exists
- Go to Enterprise Applications → Find your app → Users and groups
- Assign user to "Admin" role
-
Never commit secrets to source control:
- Keep
TenantIdandClientIdin environment variables or Azure Key Vault - Use User Secrets for local development:
dotnet user-secrets set "AzureAd:ClientId" "your-value"
- Keep
-
Use HTTPS only in production:
- Azure AD requires HTTPS for redirect URIs
- Configure proper SSL certificates
-
Restrict CORS in production:
- Update
Program.csto allow only specific origins - Don't use
AllowAnyOrigin()in production
- Update
-
Enable logging and monitoring:
- Configure Application Insights
- Monitor failed authentication attempts
- Set up alerts for suspicious activity
- Fill in Azure AD configuration in
appsettings.Production.jsonwith values from Azure Portal - Test locally with
ASPNETCORE_ENVIRONMENT=Productionand Azure AD token - Update client JavaScript to use MSAL.js for token acquisition
- Deploy to production environment (Azure App Service, Docker, etc.)
- Monitor authentication logs and user access patterns