Desenvolvimento - WIF

Autorização com WIF

Dentro do desenvolvimento de software, o termo autorização descreve o processo de verificar se um determinado usuário possui ou não o acesso à um determinado recurso. Mas é importante dizer que a autorização acontece depois da autenticação, pois primeiramente é necessário saber quem o usuário é, para depois saber que direitos ele tem no sistema.

por Israel Aéce



Dentro do desenvolvimento de software, o termo autorização descreve o processo de verificar se um determinado usuário possui ou não o acesso à um determinado recurso. Mas é importante dizer que a autorização acontece depois da autenticação, pois primeiramente é necessário saber quem o usuário é, para depois saber que direitos ele tem no sistema.

Recentemente eu comentei em alguns artigos sobre a utilização de um sistema de identidade para autenticar os usuários. Uma das principais finalidades é a terceirização do processo de autenticação, delegando isso à uma outra aplicação/serviço, e assim, todas as aplicações que confiarem naquele autenticador (STS), poderão fazer uso do mesmo para autenticar seus respectivos usuários, ficando essas aplicações totalmente isentas em como isso deve acontecer.

Como comentando nos artigos acima, o WIF (Windows Identity Foundation) abstrai grande parte da complexidade para fazer isso tudo funcionar, mas a autorização ainda continua sendo uma responsabilidade da própria aplicação. A finalidade deste artigo é apresentar algumas alternativas para efetuar a autorização nas aplicações, utilizando as claims emitidas por um determinado autenticador para refinar o acesso.

Grande parte das aplicações fazem uso do modelo baseado em papéis (roles), onde concedem ou negam acesso à algum recurso da aplicação se o usuário está ou não contido em um grupo específico. Tecnicamente falando, sempre recorremos a utilização de alguma implementação da interface IPrincipal, que fornece um método chamado IsInRole, que dado o papel retorna um valor boleano indicando se o usuário corrente está ou não contido nele.

Como já comentei anteriormente, a autorização baseada em claims é muito mais flexível do que isso, ou seja, não estamos condicionados a somente trabalhar com papéis (simples strings), mas também avaliar se o usuário poderá ou não acessar um determinado recurso a partir da sua idade, ou somente se o seu cartão de crédito estiver válido, e assim por diante.

Uma das grandes preocupações que a Microsoft teve, foi em manter compatibilidade com o modelo de objetos que utilizamos desde a primeira versão do .NET Framework, mas conseguindo já tirar proveito deste novo modelo. E foi justamente pensando nisso, que ela criou as interfaces IClaimsIdentity e IClaimsPrincipal. Elas já foram comentadas com mais detalhes neste artigo, e somente para recapitular, uma das principais propriedades é a Claims, exposta pela interface IClaimsIdentity, que fornece o conjunto de claims que foram emitidas pelo autenticador para o usuário atual.

A propriedade RoleClaimType também foi criada com propósito de compatibilidade. Como todas as informações emitidas por umSTS chegam para uma relying party através de claims, é necessário saber qual ou quais as claims representam os papéis (IPrincipal.IsInRole). Com isso, podemos continuar utilizando a autorização baseada em papéis, mas nos bastidores, são claims que estão sendo utilizadas. Com isso, por mais que começamos a utilizar as técnicas de autenticação através do WIF, a autorização pode continuar da forma que está, sem a necessidade de sofrer qualquer alteração.

Como sabemos, entre as claims que existem predefinidas nas especificações, ainda podemos criar claims customizadas. Isso quer dizer que um STS pode emitir uma claim sem vincular ela ao tipo role, que por padrão, a aplicação utiliza quando chamamos o método IsInRole. Mas felizmente isso pode ser configurado de forma declarativa, onde cada aplicação determina qual a claim que ela vai quer utilizar/avaliar quando o método IsInRole for chamado. Para efetuar essa customização, podemos recorrer ao sub-elemento roleClaimType do elemento samlSecurityTokenRequirement, assim como é mostrado abaixo:

<samlSecurityTokenRequirement>
<roleClaimType value= "http://MinhaAplicacao/departamento" />
</samlSecurityTokenRequirement>

Como comentado acima, podemos continuar trabalhando com a segurança baseada em papéis. E para isso, utilizamos o método IsInRole exposto pela interface IPrincipal ou através da classe PrincipalPermission, ou ainda,do atributo PrincipalPermissionAttribute, que nos permite trabalhar de forma imperativa ou declarativa, respectivamente. Todas deverão recorrer as claims do tipo role (http://schemas.microsoft.com/ws/2008/06/identity/claims/role) ou alguma outra, conforme configuramos acima. Mas o WIF também fornece um novo par de classes para trabalhar explicitamente com claims, a saber: ClaimsPrincipalPermission e ClaimsPrincipalPermissionAttribute. Ambas também servem para utilizar de forma imperativa ou declarativa, respectivamente, mas como já podemos perceber, vai além de somente utilizar claims como roles. Podemos aqui utilizar outras claims que foram emitidas pelo STS, e tornar o processo de autorização muito mais inteligente.

Como comentado, o uso pode ser de forma imperativa ou declarativa. Utilizar um modo ou outro tem suas vantagens e desvantagens. De qualquer forma, a utilização é extremamente fácil, pois é semelhante ao modelo que já estamos acostumados a trabalhar dentro da plataforma .NET. Para exemplificar, o exemplo abaixo mostra a utilização de ambas as formas:

[ Modelo Imperativo ]

public string AlgumaOperacao()
{
new ClaimsPrincipalPermission("AlgumaOperacao", "Executar").Demand();

//Processamento do Método
}

[ Modelo Declarativo ]

[ClaimsPrincipalPermission(SecurityAction.Demand, Resource = "AlgumaOperacao", Action = "Executar")]
public string AlgumaOperacao()
{
//Processamento do Método
}

Apesar de aparentemente funcionar como o modelo baseado em papéis, o WIF nos induz a não fixar o valor das claims pelo nosso código, que é algo que mais cedo ou mais tarde sempre acaba mudando, e justamente por isso que temos as propriedades Resource e Action, mas não devemos nos preocupar com o que significado delas agora, pois o seu uso está condicionado ao ambiente em que são utilizadas, e que serão detalhados mais abaixo. Um detalhe importante também é que as duas classes que representam a permissão não são responsáveis por efetivamente validar se o usuário poderá ou não acessar aquele recurso; isso é delegado à uma outra classe, chamada ClaimsAuthorizationManager, qual veremos a seguir.

O processo de autorização é altamente customizável, o que nos permite interceptar o momento em que o runtime avalia se o usuário possui ou não permissão, e com isso alterar o comportamento padrão, nos baseando em outras claims que foram emitidas para tomar a decisão se o usuário deverá ou não acessar o recurso.

Independentemente de qual ambiente estamos falando (ativo ou passivo), as classes que lidam com o processo de autorização são as mesmas. A diferença consiste nas informações que são colocadas dentro dessas classes, pois elas são contextualizadas, condicionados ao ambiente. A principal classe que irá gerenciar isso é a ClaimsAuthorizationManager, que fornece um local central para analisarmos a requisição e determinar se o usuário poderá ou não acessar o recurso. Essa classe possui um único método chamado CheckAccess, que é disparado antes de acessar o recurso em si, e dado o contexto da requisição, retorna um valor boleano indicando se o usuário terá acesso ao mesmo, e que por padrão sempre permite o acesso.

Como dito acima, o método CheckAccess recebe o contexto da requisição como parâmetro, fornecendo todas as informações necessárias para a tomada de decisão. Esse parâmetro é do tipo AuthorizationContext, e possui três principais propriedades: Principal, Resource e Action. A primeira propriedade retorna uma instância da classe ClaimsPrincipal, que representa o usuário atual. Já a propriedade Resource determina o recurso que você está tentando acessar, e isso varia de acordo com o ambiente. Finalmente, a propriedade Action determina a operação que está sendo aplicada ao recurso, e assim como a propriedade Resource, também varia de acordo com o ambiente.

Quando implementamos a classe ClaimsAuthorizationManager para o ambiente passivo, a propriedade Resource trará o caminho e nome da página ASPX que estamos tentando acessar e a Action deve retornar se está sendo acessada via GET ou via POST. Já no ambiente ativo, a propriedade Resource deverá representar o endereço do serviço que está sendo acessado, enquanto a propriedade Action deverá refletir a SOAPAction da operação. A propriedade Principal tem o mesmo significado para ambos ambientes, ou seja, representar o usuário que está tentando acessar o recurso. Como há algumas outras diferenças entre os dois ambientes, vamos analisar a sua implementação individualmente a partir de agora.

Autorização no Ambiente Passivo - ASP.NET

A implementação da classe ClaimsAuthorizationManager para o ambiente passivo, consistirá em avaliar se a página (ASPX) que o usuário está acessado poderá ou não ser acessado, de acordo com uma regra determinada. Cada página que for acessada por aquele usuário, o runtime do WIF em conjunto com o ASP.NET invocará o método CheckAccess, que é onde terá toda a lógica de autorização.

Como exemplo, vamos somente permitir o acesso à uma determinada página se o usuário for maior que 21 anos de idade. Como o STS emite uma claim chamada dataDeNascimento, podemos efetuar o cálculo em cima dela, para avaliar se o usuário poderá ou não acessar a página. Abaixo temos a implementação:

public class AutorizadorDePaginas : ClaimsAuthorizationManager
{
public override bool CheckAccess(AuthorizationContext context)
{
if (context.Resource.First().Value == "http://localhost:2721/PaginaRestrita.aspx")
{
var claimDeDataDeNascimento =
(
from cin context.Principal.Identities[0].Claims
where c.ClaimType == "http://MinhaAplicacao/dataDeNascimento"
select c
).FirstOrDefault();

if (claimDeDataDeNascimento != null)
{
DateTime dataDeNascimento = Convert.ToDateTime(claimDeDataDeNascimento.Value);
return (DateTime.Now.Subtract(dataDeNascimento).Days / 365) >= 21;
}

return false;
}

return true;
}
}

Em primeiro lugar verificamos se a página que está sendo acessada deve ser verificada quanto a permissão. Como sabemos que devemos filtrar o acesso a partir da idade, então vamos em busca de uma claim que represente a data de nascimento do usuário, e se encontrada, verificamos se ele já possui mais que 21 anos. Se ele tiver, então ele pode acessar, do contrário, terá o acessado negado.

Mas essa implementação por si só não funciona. Temos que saber como acoplá-la ao pipeline de processamento do ASP.NET. Na verdade, o responsável por invocar o método CheckAccess será o módulo ClaimsAuthorizationModule, que por padrão, não está configurado. Esse módulo se vincula ao evento AuthorizeRequest do objeto HttpApplication, e quando é disparado, irá extrair o autorizador configurado na seção do WIF e, consequentemente, irá passar a instância da classe AuthorizationContext, onde o Resource representará o caminho até a página ASPX e a Action deve representar o verbo HTTP (GET ou POST). Abaixo temos a configuração no arquivo Web.config que correspondem ao módulo e ao autorizador:

<system.web>
<!-- Outras Configurações -->
<httpModules>
<!-- Outros Módulos -->
<add name="ClaimsAuthorizationModule"
type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, ..."/>
</httpModules>
</system.web>
<microsoft.identityModel>
<service>
<!-- Outras Configurações -->
<claimsAuthorizationManager
type="Servico.AutorizadorDePaginas, Servico, Version=1.0.0.0, ..."/>
</service>
</microsoft.identityModel>

Autorização no Ambiente Ativo - WCF

Já no WCF vamos trabalhar de forma parecida ao que vimos acima, ao que diz respeito a forma de implementar a classe responsável pela autorização do usuário. As ligeiras diferenças estão nas informações que são colocadas na classe AuthorizationContext e como ela é acoplada à execução.

Neste ambiente,a propriedadeResource representará o serviço que está sendo acessado, mais precisamente, o endereço do mesmo. Já a propriedade Action determinará a SOAPAction que o usuário quer invocar. A SOAPAction é um header que é incluído na requisição HTTP, e determina intenção da requisição SOAP. Esse header é representado por uma URI que não aponta necessariamente para um local válido na internet, e que indicará qual o método a ser invocado.

A ideia aqui é justamente analisar a SOAPAction, verificando se trata-se de uma operação que exige a validação da permissão dousuário. Caso seja, então vamos verificar se ele possui a idade suficiente para acessar a operação em questão. Abaixo temos a implementação, bem próxima a qual fizemos no ambiente passivo:

public class AutorizadorDeOperacoes : ClaimsAuthorizationManager
{
public override bool CheckAccess(AuthorizationContext context)
{
if (context.Action.First().Value == "http://tempuri.org/IConsultasFinanceiras/RecuperarLimiteDeCredito")
{
var claimDeDataDeNascimento =
(
from c in context.Principal.Identities[0].Claims
where c.ClaimType == "http://MinhaAplicacao/dataDeNascimento"
select c
).FirstOrDefault();

if (claimDeDataDeNascimento != null)
{
DateTime dataDeNascimento = Convert.ToDateTime(claimDeDataDeNascimento.Value);
return (DateTime.Now.Subtract(dataDeNascimento).Days / 365) >= 21;
}

return false;
}

return true;
}
}

A principal diferença aqui é como o WIF acopla isso à execução no WCF, que deve executá-la sempre quando uma nova requisição chegar ao serviço. Para que o serviço faça uso do WIF, é necessário submetermos a instância da classe ServiceHost para o método estático ConfigureServiceHost da classe FederatedServiceCredentials, assim como já foi mostrado neste artigo. Assim como no ASP.NET, o que o WIF faz aqui é utilizar um ponto de estensibilidade do WCF para adicionar a implementação do ClaimsAuthorizationManager, e para isso, oWIF já possui implementado uma classe que deriva de ServiceAuthorizationManager, que é a IdentityModelServiceAuthorizationManager, e em seu método CheckAccessCore captura o autorizador implementado/configurado pelo WIF e invoca o seu método CheckAccess, abastecendo a instância da classe AuthorizationContext com informações que estão dentro do contexto da operação (OperationContext).

Sendo assim, tudo o que precisamos aqui é configurar o autorizador customizado, para que o runtime do WIF possa capturá-lo e, consequentemente, entregar ao WCF para efetuar a validação. O código abaixo ilustra essa configuração, que é realizada da mesma forma que no ambiente passivo:

<microsoft.identityModel>
<service>
<!-- Outras Configurações -->
<claimsAuthorizationManager
type="Servico.AutorizadorDeOperacoes, Servico, Version=1.0.0.0, ..."/>
</service>
</microsoft.identityModel>

Claims Explícitas

Para ratificar o que disse acima, em ambos cenários não temos as claims espalhadas pela aplicação. Como isso pode mudar, o ideal é manter centralizado, para facilitar a alteração caso ela seja necessária.

O mesmo vale para as classes que falamos acima: ClaimsPrincipalPermission e ClaimsPrincipalPermissionAttribute. Quando as utilizamos, nós vimos que tudo o que precisamos colocar ali é o Resource e Action, que determinam informações sobre o acesso e não sobre a claim que deve ser utilizada/avaliada.

Essas classes não são responsáveis por fazerem a verificação. Intermamente elas recorrem a implementação da classe ClaimsAuthorizationManager, que deve estar configurada da forma que vimos acima. Caso não tenhamos um autorizador explicitamente definido, o WIF utilizará uma implementação padrão, que sempre concederá o acesso. O mais interessante aqui é que as informações que colocamos nestas classes também são enviadas para a classe que herda de ClaimsAuthorizationManager, pois elas invocam o método CheckAccess, abstecendo o AuthorizationContext com as respectivos informações.

Nativamente o WIF não traz suporte para vinculação de claims à um determinado recurso, mas podemos criar algo customizado para isso. A ideia é ter algum repositório para catalogar as políticas de acesso à determinados recursos, por exemplo, para acessar uma determinada página ASPX ou uma operação de um serviço, precisamos verificar se a quantidade de anos da claim dataDeNascimento é maior que 21. Esse repositório pode ser um arquivo Xml, uma base de dados ou até mesmo o próprio arquivo de configuração (*.config). Criando algo customizado, nos permite tornar a implementação do ClaimsAuthorizationManager mais simples, dinâmica e ainda mantendo a centralização.

Conclusão: Vimos neste artigo o processo de autorização de uma aplicação que opta por fazer o uso do WIF para terceirizar a autenticação do usuário. Apesar de autorização ainda ser responsabilidade da relying party, o WIF se preocupou em fornecer classes para tornar esse processo mais suave, e não menos importante, mantendo compatibilidade com o legado, que são aplicações que se baseiam em papéis para refinar o acesso aos seus respectivos recursos.
Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.