.NET

17 jul, 2018

Autorização em APIs ASP.Net Core

Publicidade

Em um artigo anterior eu falei sobre a criação de uma API de Autenticação utilizando ASP.Net Core. Hoje eu quero falar um pouco sobre como podemos fazer a autorização de nossos usuários dentro das APIs.

No artigo citado acima eu fiz uma breve introdução sobre os conceitos de autenticação e autorização, então se você não viu, corre lá e depois volte aqui.

Os recursos de autorização descritos neste artigo foram implementados a titulo de demonstração nesse projeto que está no meu GitHub.

Autorização baseada em Roles

Quando uma identidade de usuário é criada, ela pode ter Roles (papéis) associadas ao usuário. Por exemplo, um usuário pode ter o “papel” de Administrador, enquanto outro pode ter o “papel” de Usuário, sendo que cada um possui permissões de acesso diferentes dentro do sistema.

Usamos as Roles para autorizar o acesso do usuário à determinados recursos dentro da aplicação, sendo essa a forma mais comum de autorização.

Para autorizar o acesso dos usuários em nossas APIs, seja usando Roles ou Policies, conforme veremos mais adiante, devemos fazer uso de um atributo chamado AuthorizeAttribute. Para isso, basta adicionar esse atributo na Controller ou Action que queremos proteger.

[Route("api/[controller]"), Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
	[HttpGet, Route("accounts")]
	public async Task<IActionResult> ListUsers()
	{
		// ... conteúdo omitido
	}
}

No exemplo acima, o acesso à essa Controller será permitido apenas aos usuários que possuem a role Administrator. Usuários que não possuem essa role receberão um erro (403 Forbidden) ao tentar acessar esse recurso.

Abaixo, temos o exemplo do payload de um token JWT com uma role User.

Ao chamar a API no Postman passando esse token JWT no header Authorization, o acesso é negado, pois o usuário não possui a role Administrator que é requerida para acessar essa Controller.

Já neste exemplo, temos o payload de um usuário com role de administração. No caso, Administrator.

Ao chamar a mesma API com esse token JWT, o acesso é permitido e uma listagem dos usuários cadastrados é exibida.

Veja que o atributo Authorize deve ser colocado em cada classe que queremos autorizar o acesso, mesmo que não informemos nenhuma Role específica. Nesse caso, quando nenhuma Role é informada, apenas uma identidade de usuário válida já basta para autorizar o acesso.

Temos que concordar que adicionar esse atributo em cada Controller ou Action que queremos proteger pode se tornar algo trabalhoso e chato, dependendo do tamanho da aplicação. Mas existe uma forma de aplicar a autorização de forma global para toda a API. Para isso devemos fazer uso de Filtros do ASP.Net MVC, mais precisamente do filtro chamado AuthorizeFilter.

public void ConfigureServices(IServiceCollection services)
{
	// ... restante do método ocultado
	
	services.AddMvc(config =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.Build();

		config.Filters.Add(new AuthorizeFilter(policy));
	});
}

Com isso, toda a API irá requerer ao menos um token válido. Nas Controllers ou Actions que você desejar acesso anônimo, use o atributo AllowAnonymous.

[HttpPost, AllowAnonymous, Route("register")]
public async Task<IActionResult> CreateUser([FromBody] CreateUser command)
{
	// ... conteúdo ocultado
}

[HttpPost, AllowAnonymous, Route("login")]
public async Task<IActionResult> Authenticate([FromBody] AuthenticateUser command)
{
	// ... conteúdo ocultado
}

Autorização baseada em Policies

Outra forma de autorizar o acesso à determinados recursos é fazendo uso de Policies.

Uma Policy pode ser composta por um ou mais requirements que uma identidade de usuário deve satisfazer.

As Policies são mais flexíveis quando comparadas às Roles, e com elas podemos elaborar melhor o acesso aos recursos.

Você precisa criar um Requirement que deverá ser satisfeito durante a autorização do usuário, usando a interface IAuthorizationRequirement. No meu exemplo criei uma classe que representa o requirement necessário para permitir a exclusão de um usuário na aplicação.

public class DeleteUserRequirement : IAuthorizationRequirement
{
	public string RequiredPermission { get; }

	public DeleteUserRequirement(string requiredPermission)
	{
		RequiredPermission = requiredPermission;
	}
}

Após criar o requirement, precisamos criar um handler que será responsável por validar se ele foi satisfeito. Para isso fazemos uso da classe AuthorizationHandler informando em sua definição o requirement que ela irá tratar.

public class DeleteUserRequirementHandler : AuthorizationHandler<DeleteUserRequirement>
{
	private const string AdministratorRoleName = "Administrator";

	private AuthorizationHandlerContext _context;

	protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DeleteUserRequirement requirement)
	{
		_context = context;

		var isAdministrator = IsAdministrator();
		var canDeleteUser = HasRequirements(requirement);

		if (isAdministrator && canDeleteUser)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}

	private bool IsAdministrator() => 
		GetClaim(ClaimTypes.Role, AdministratorRoleName);

	private bool HasRequirements(DeleteUserRequirement requirement) =>
		GetClaim("permissions", requirement.RequiredPermission);

	private bool GetClaim(string type, string value) => _context.User.HasClaim(type, value);

}

Neste exemplo hipotético, essa classe irá verificar se o usuário possui a role Administrator, e também se ele possui a permissão para exclusão de um usuário. Nesse caso ele deverá possuir a claim Permissions com o valor CanDeleteUser, que será passado através da propriedade RequiredPermission do requirement, conforme veremos mais adiante.

Para que tudo isso funcione, você deve configurar a Policy utilizando o método AddAuthorization da interface IServiceCollection.

public void ConfigureServices(IServiceCollection services)
{
	// ... restante do códito omitido 
	
	services.AddAuthorization(options =>
	{
		options.AddPolicy("DeleteUserPolicy", policy =>
			policy.Requirements.Add(new DeleteUserRequirement("CanDeleteUser")));
	});

	services.AddSingleton<IAuthorizationHandler, DeleteUserRequirementHandler>();
}

Você deve informar um nome para a policy – aqui ela se chama DeleteUserPolicy – e então adicionar o requirement que quer validar. Em meu exemplo temos o DeleteUserRequirement. Também deve ser informado o valor esperado para esse requirement, que é CanDeleteUser; nsse caso o usuário precisa ter esse valor na claim Permissions, caso contrário o requirement não será satisfeito.

Além disso, também é necessário injetar o handler DeleteUserRequirementHandler no containder de DI.

Com tudo isso pronto, basta utilizar novamente o atributo Authorize, informando o nome da Policy que queremos utilizar. Neste caso, DeleteUserPolicy.

[Route("api/[controller]"), Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
	// ... restante do código omitido

	[HttpDelete, Route("accounts/{accountId}"), Authorize(Policy = "DeleteUserPolicy")]
	public async Task<IActionResult> DeleteAccount(Guid accountId)
	{
		var command = new RemoveAccount(accountId);
		var response = await _mediator.Send(command);

		if (response.HasMessages)
		{
			return BadRequest(response.Messages);
		}
		return Ok();
	}
}

Somente usuários que possuem a role Administrator, e que possuem a permissão adequada, neste caso CanDeleteUser, conseguirão acessar a action DeleteAccount.

Abaixo temos o exemplo de um payload JWT em que o usuário possui a role Administrator, e a permissão CanDeleteUser.

Authorization Service

Além do atributo Authorize que vimos anteriormente, podemos usar a interface IAuthorizationService manualmente. Ela é utilizada internamente pelos filtros de autorização do ASP.Net Core.

Você pode injetar essa interface no construtor da classe onde quer autorizar o acesso e fazer uso do método AuthorizeAsync. Após ser chamado, esse método irá disparar o handler associado ao requirement informado, da mesma forma que o atributo Authorize faria.

[Route("api/[controller]")]
public class ProfileController : Controller
{
	private readonly IAuthorizationService _authorizationService;
	
	public CustomersController(IAuthorizationService authorizationService)
	{
		_authorizationService = authorizationService;
	}
	
	[HttpPut, Route("email")]
	public async Task<IActionResult> ChangeUserEmail([FromBody] ChangeUserEmail command)
	{
		var authorizationResult = await _authorizationService
			.AuthorizeAsync(HttpContext.User, command.EletronicSignature, new EletronicSignatureRequirement());

		if (!authorizationResult.Succeeded)
		{
			return BadRequest("Assinatura eletrônica inválida");
		}
		
		// ... restante do código omitido
	}
}

Conclusão

Roles e Policies representam a forma mais comum e segura de autorizar o acesso à determinado recurso em nossas aplicações, e fazendo o correto uso deles podemos customizar o acesso à nossas APIs de forma simples.

Espero que tenham gostado e se tiverem dúvidas, críticas ou sugestões, entrem em contato.

Abraços!

Referências