Azure AD App Roles - RBAC para aplicaciones empresariales
Cuando necesitas algo más sofisticado que “está autenticado o no”
El problema
Aplicación con diferentes niveles de acceso:
- Viewers: Solo lectura
- Contributors: Lectura + escritura
- Administrators: Todo
Azure AD Authentication nos daba identidad, pero no autorización granular.
Solución: App Roles de Azure AD.
App Roles vs Groups
| App Roles | Azure AD Groups |
|---|---|
| Definidos en app manifest | Creados en Azure AD |
| Específicos de la app | Compartidos entre apps |
| Incluidos en token automáticamente | Requieren configuración adicional |
| Máximo 200 por app | Sin límite |
Para casos simples con <10 roles: App Roles son más fáciles.
Configuración en Azure Portal
1. Definir App Roles
Azure AD → App registrations → Tu app → App roles → Create app role
Display name: Administrator
Value: admin
Description: Full administrative access
Allowed member types: Users/Groups
[Repeat para otros roles]
Roles definidos:
viewer- Solo lecturacontributor- Lectura y escrituraadmin- Acceso completo
2. Via App Manifest (más rápido)
// App manifest
{
"appRoles": [
{
"allowedMemberTypes": ["User"],
"description": "Viewers can only read data",
"displayName": "Viewer",
"id": "e1a4c3e2-4f5e-4b3a-9c7d-8e2f1a3b5c7d",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "viewer"
},
{
"allowedMemberTypes": ["User"],
"description": "Contributors can read and write data",
"displayName": "Contributor",
"id": "a2b4d6e8-1c3e-4f5a-9b7d-2e4f6a8c1e3b",
"isEnabled": true,
"value": "contributor"
},
{
"allowedMemberTypes": ["User"],
"description": "Administrators have full access",
"displayName": "Administrator",
"id": "b3c5e7f9-2d4f-5a6c-8b9d-3f5a7c9e2f4b",
"isEnabled": true,
"value": "admin"
}
]
}
Importante: El id debe ser un GUID único. Generarlo con:
[guid]::NewGuid()
3. Asignar usuarios a roles
Azure AD → Enterprise applications → Tu app → Users and groups
→ Add user/group
Select user: juan@empresa.com
Select role: Administrator
O vía PowerShell:
Connect-AzureAD
$app = Get-AzureADServicePrincipal -Filter "displayName eq 'TuApp'"
$user = Get-AzureADUser -ObjectId "juan@empresa.com"
# Obtener AppRole ID
$adminRole = $app.AppRoles | Where-Object {$_.Value -eq "admin"}
# Asignar
New-AzureADUserAppRoleAssignment `
-ObjectId $user.ObjectId `
-PrincipalId $user.ObjectId `
-ResourceId $app.ObjectId `
-Id $adminRole.Id
Configuración ASP.NET Core
1. Recibir roles en token
// Startup.cs
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
options.Instance = "https://login.microsoftonline.com/";
options.TenantId = Configuration["AzureAd:TenantId"];
options.ClientId = Configuration["AzureAd:ClientId"];
options.ClientSecret = Configuration["AzureAd:ClientSecret"];
// Importante: mapear roles claim
options.TokenValidationParameters.RoleClaimType = "roles";
});
2. Configurar autorización por roles
services.AddAuthorization(options =>
{
options.AddPolicy("ViewerPolicy", policy =>
policy.RequireRole("viewer", "contributor", "admin"));
options.AddPolicy("ContributorPolicy", policy =>
policy.RequireRole("contributor", "admin"));
options.AddPolicy("AdminPolicy", policy =>
policy.RequireRole("admin"));
});
3. Proteger endpoints
// Controllers/DataController.cs
[Authorize(Policy = "ViewerPolicy")]
[HttpGet]
public IActionResult GetData()
{
var data = _service.GetData();
return Ok(data);
}
[Authorize(Policy = "ContributorPolicy")]
[HttpPost]
public IActionResult CreateData([FromBody] DataModel data)
{
_service.CreateData(data);
return Created("", data);
}
[Authorize(Policy = "AdminPolicy")]
[HttpDelete("{id}")]
public IActionResult DeleteData(int id)
{
_service.DeleteData(id);
return NoContent();
}
O con atributo directo:
[Authorize(Roles = "admin,contributor")]
public IActionResult SomeAction() { }
Verificar roles en código
public class DataService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public void ProcessData()
{
var user = _httpContextAccessor.HttpContext.User;
if (user.IsInRole("admin"))
{
// Lógica para admins
ProcessFullData();
}
else if (user.IsInRole("contributor"))
{
// Lógica para contributors
ProcessLimitedData();
}
else
{
// Viewer
ProcessReadOnlyData();
}
}
// O más limpio con pattern matching
public void ProcessDataClean()
{
var roles = _httpContextAccessor.HttpContext.User
.FindAll("roles")
.Select(c => c.Value)
.ToList();
if (roles.Contains("admin"))
{
ProcessFullData();
}
// ...
}
}
Roles en React SPA
1. Obtener roles del token
// src/authConfig.ts
import { Configuration } from '@azure/msal-browser';
export const msalConfig: Configuration = {
auth: {
clientId: 'tu-client-id',
authority: 'https://login.microsoftonline.com/tu-tenant-id',
redirectUri: 'http://localhost:3000',
},
};
export const loginRequest = {
scopes: ['User.Read'],
};
// src/services/AuthService.ts
import { PublicClientApplication } from '@azure/msal-browser';
import { msalConfig } from '../authConfig';
const msalInstance = new PublicClientApplication(msalConfig);
export const getUserRoles = (): string[] => {
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
return [];
}
const account = accounts[0];
const roles = account.idTokenClaims?.roles || [];
return roles as string[];
};
export const hasRole = (role: string): boolean => {
const roles = getUserRoles();
return roles.includes(role);
};
export const hasAnyRole = (requiredRoles: string[]): boolean => {
const userRoles = getUserRoles();
return requiredRoles.some(role => userRoles.includes(role));
};
2. Conditional rendering por rol
// src/components/ProtectedButton.tsx
import React from 'react';
import { hasRole } from '../services/AuthService';
interface Props {
requiredRole: string;
onClick: () => void;
}
export const ProtectedButton: React.FC<Props> = ({ requiredRole, onClick, children }) => {
if (!hasRole(requiredRole)) {
return null;
}
return <button onClick={onClick}>{children}</button>;
};
// Uso
<ProtectedButton requiredRole="admin" onClick={handleDelete}>
Delete
</ProtectedButton>
3. Route protection
// src/components/ProtectedRoute.tsx
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { hasAnyRole } from '../services/AuthService';
interface Props {
component: React.ComponentType<any>;
requiredRoles: string[];
path: string;
}
export const ProtectedRoute: React.FC<Props> = ({ component: Component, requiredRoles, ...rest }) => {
return (
<Route
{...rest}
render={props =>
hasAnyRole(requiredRoles) ? (
<Component {...props} />
) : (
<Redirect to="/unauthorized" />
)
}
/>
);
};
// Uso
<ProtectedRoute
path="/admin"
component={AdminPanel}
requiredRoles={['admin']}
/>
Roles jerárquicos
Para implementar herencia (admin tiene permisos de contributor):
// Services/RoleService.cs
public class RoleService
{
private static readonly Dictionary<string, HashSet<string>> RoleHierarchy = new()
{
["viewer"] = new HashSet<string> { "viewer" },
["contributor"] = new HashSet<string> { "viewer", "contributor" },
["admin"] = new HashSet<string> { "viewer", "contributor", "admin" }
};
public static bool HasPermission(ClaimsPrincipal user, string requiredRole)
{
var userRoles = user.FindAll("roles").Select(c => c.Value);
foreach (var userRole in userRoles)
{
if (RoleHierarchy.TryGetValue(userRole, out var permissions))
{
if (permissions.Contains(requiredRole))
{
return true;
}
}
}
return false;
}
}
// Authorization handler custom
public class RoleRequirement : IAuthorizationRequirement
{
public string Role { get; }
public RoleRequirement(string role)
{
Role = role;
}
}
public class HierarchicalRoleHandler : AuthorizationHandler<RoleRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
RoleRequirement requirement)
{
if (RoleService.HasPermission(context.User, requirement.Role))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Registrar
services.AddSingleton<IAuthorizationHandler, HierarchicalRoleHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("RequireViewer", policy =>
policy.Requirements.Add(new RoleRequirement("viewer")));
options.AddPolicy("RequireContributor", policy =>
policy.Requirements.Add(new RoleRequirement("contributor")));
});
Auditoría de cambios de roles
// Middleware para logging
public class RoleAuditMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RoleAuditMiddleware> _logger;
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated == true)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var roles = string.Join(", ", context.User.FindAll("roles").Select(c => c.Value));
var endpoint = context.Request.Path;
_logger.LogInformation(
"User {UserId} with roles [{Roles}] accessed {Endpoint}",
userId,
roles,
endpoint
);
}
await _next(context);
}
}
// Registrar
app.UseMiddleware<RoleAuditMiddleware>();
Problemas comunes
1. Roles claim no aparece en token
Causa: Usuario no tiene rol asignado en Enterprise App.
Debug:
var claims = User.Claims.Select(c => new { c.Type, c.Value });
// Verificar si "roles" está presente
2. Role claim type incorrecto
Solución:
options.TokenValidationParameters.RoleClaimType = "roles"; // No "role"
3. Cambios de roles no reflejan inmediatamente
Causa: Token cacheado.
Solución: Forzar logout/login o esperar expiración del token.
Mejores prácticas
- Mantén roles simples: <5 roles es ideal
- Documenta permisos: Qué puede hacer cada rol
- Usa policies en lugar de roles directos: Más flexible
- Audita cambios: Log quién modifica roles
- Testea permisos: Unit tests para cada rol
Resultados
Con App Roles implementados:
- Autorización centralizada en Azure AD
- 0 hardcoding de permisos
- Cambios de roles sin redeploy
- Auditoría completa en Azure AD logs
¿Usas App Roles o prefieres Azure AD Groups? ¿Por qué?
Por Vicente José Moreno Escobar el
25 de
noviembre
de
2020
Puedes disfrutar de otros artículos como éste en el archivo del sitio.