Versões comparadas

Chave

  • Esta linha foi adicionada.
  • Esta linha foi removida.
  • A formatação mudou.

Objetivo


Este documento tem como objetivo auxiliar na configuração do Host para permitir autenticação via Token (JWT).

Segurança via WCF

Os de segurança dos serviços HTTP expostos pelo Host são amparados pelo WCF, componente do .NET Framework responsável pela criação e exposição de serviços. O WCF fornece diversos pontos de extensão permitindo a personalização de muitas funcionalidades, dentre elas Autenticação/Autorização.

No WCF, o principal ponto de extensão para este fim é o ServiceAuthorizationManager (SAM). Este componente é invocado cedo o bastante no fluxo da requisição, tem acesso aos detalhes do protocolo HTTP e consegue atribuir o Principal, componente de autenticação do .NET Framework, na thread corrente.

O trabalho feito pelo SAM é simples:

  1. Checa o cabeçalho da requisição HTTP em busca do parâmetro de autorização (Authorization Header);
  2. Checa se um token está presente;
  3. Se sim, valida o token usando um manipulador de token (SecurityTokenHandler - STH), cria o Principal, faz as transformações necessárias à aplicação e atribui à thread;
  4. Se não, atribui um Principal anônimo à thread. Por padrão, principals anônimos tem acesso negado, logo o request termina em um erro 401.

Para utilizar um SAM personalizado, precisa-se de um ServiceHost (SH) personalizado. O SH é o componente responsável por expor os serviços via WCF.

Segurança do Host

O Host permite configurar dois tipos de autenticação para os serviços HTTP: Basic Authentication e Token Based Authentication via JWT. O padrão é permitir os dois tipos.

Permite também que os serviços sejam expostos complemente ou parcialmente anônimos.

Principais Componentes da Solução

Nesta seção, serão apresentados alguns componentes da solução de Autenticação/Autorização com o intuito de demonstrar como o Host expõe e configura a segurança. O código fonte apresentado pode ser encontrado em: $/RM/<Versão>/Lib/RM.Lib.Server.

Primeiramente, como dito anteriormente, é necessário um SH personalizado. Nele é configurado o SAM personalizado, que realizará a autenticação.

Bloco de código
languagec#
titleWebTokenWebServiceHost
linenumberstrue
collapsetrue
public class WebTokenWebServiceHost : WebServiceHost
{
    protected WebTokenWebServiceHostConfiguration _configuration;
    protected WebSecurityTokenHandlerCollectionManager _manager;
    public WebTokenWebServiceHost(Type serviceType, WebSecurityTokenHandlerCollectionManager tokenManager, params Uri[] baseAddresses)
        : this(serviceType, tokenManager, new WebTokenWebServiceHostConfiguration(), baseAddresses)
    { }
    public WebTokenWebServiceHost(Type serviceType, WebSecurityTokenHandlerCollectionManager tokenManager, WebTokenWebServiceHostConfiguration configuration, params Uri[] baseAddresses)
        : base(serviceType, baseAddresses)
    {
      _configuration = configuration;
      _manager = tokenManager;
 
      // Configura o SAM personalizado
      Authorization.ServiceAuthorizationManager = new WebTokenServiceAuthorizationManager(tokenManager, configuration);
      Authorization.PrincipalPermissionMode = PrincipalPermissionMode.Custom;
     
      // ...
    }
    
    //...
  }
}

O SH repassa ao SAM um outro componente, WebSecurityTokenHandlerCollectionManager (STHM), resposável por gerenciar os STHs configurados, responsáveis por fazer a validação do token e criação do Principal. A configuração é feita na inicialização do Host.

Bloco de código
languagec#
titleRMSWCFBrokerServer
linenumberstrue
collapsetrue
//...
private static WebTokenWebServiceHost CreateWebTokenServiceHost(Type serviceType, Uri baseAddress)
{
      var manager = SetupSecurityTokenHandler();
      var configuration = SetupServiceHostConfiguration();
      var restHost = new WebTokenWebServiceHost(serviceType,
          manager,
          configuration,
          baseAddress);
      
      return restHost;
}
    
private static WebTokenWebServiceHostConfiguration SetupServiceHostConfiguration()
{
      var configuration = new WebTokenWebServiceHostConfiguration
      {
        RequireSsl = false,
        EnableRequestAuthorization = false,
        AllowAnonymousAccess = true,
        
        ClaimsAuthenticationManager = new ClaimsTransformer()
      };
      return configuration;
}
private static WebSecurityTokenHandlerCollectionManager SetupSecurityTokenHandler()
{
      var manager = new WebSecurityTokenHandlerCollectionManager();
      
      // Requests anônimos
      manager.AddDefaultHandler();
      
      // Basic Authentication
      manager.AddBasicAuthenticationHandler((username, password) => RMSUsersManager.ValidateCredentials(username, password));
      
      // Bearer with JWT
      var jwtValidationOptions = new JwtValidationOptions()
      {
          IssuerName = Settings.Default.GetValue<string>(RMSCONFIGOPTIONSENUM.JWTISSUERNAME),
          Audience = Settings.Default.GetValue<string>(RMSCONFIGOPTIONSENUM.JWTAUDIENCE),
          SigningCertificateOptions = new JwtSigningCertificateOptions()
          {
              SigningCertificateName = Settings.Default.GetValue<string>(RMSCONFIGOPTIONSENUM.JWTCERTIFICATETHUMBPRINT),
              NameType = NameType.Thumbprint,
          },
          AllowedScopes = Settings.Default.GetValue<string>(RMSCONFIGOPTIONSENUM.JWTALLOWEDSCOPES)?.Split(',') ?? new string[] { }
      };
      manager.AddJsonWebTokenHandler(jwtValidationOptions);

      return manager;
}
//...

O SAM extrai do cabeçalho HTTP o esquema e o token repassando ao STHM para que ele invoque o STH específico para a validação. Por exemplo:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkQxQUUy...

Esquema: Bearer
Token: eyJhbGciOiJSUzI1NiIsImtpZCI6IkQxQUUy...

Authorization: Basic bYVzsHJlTnUvdHZz

Esquema: Basic
Token: bYVzsHJlTnUvdHZz

Bloco de código
languagec#
titleWebTokenServiceAuthorizationManager
linenumberstrue
collapsetrue
class WebTokenServiceAuthorizationManager : ServiceAuthorizationManager
{
    WebSecurityTokenHandlerCollectionManager _manager;
    WebTokenWebServiceHostConfiguration _configuration;
    public WebTokenServiceAuthorizationManager(WebSecurityTokenHandlerCollectionManager manager, WebTokenWebServiceHostConfiguration configuration)
    {
      _manager = manager;
      _configuration = configuration;
    }
    protected override bool CheckAccessCore(OperationContext operationContext)
    {
      var properties = operationContext.ServiceSecurityContext.AuthorizationContext.Properties;
      var to = operationContext.IncomingMessageHeaders.To.AbsoluteUri;
      ClaimsPrincipal principal;
      if (TryGetPrincipal(out principal))
      {
        // set the IClaimsPrincipal
        if (_configuration.ClaimsAuthenticationManager != null)
        {
          principal = _configuration.ClaimsAuthenticationManager.Authenticate(to, principal);
        }
        properties["Principal"] = principal;
      }
      else
      {
        if (_configuration.AllowAnonymousAccess)
        {
          // set anonymous principal
          principal = new ClaimsPrincipal(); //ClaimsPrincipal.AnonymousPrincipal;
          properties["Principal"] = principal;
        }
        else
        {
          return false;
        }
      }
      if (!_configuration.EnableRequestAuthorization)
      {
        return true;
      }
      return CallClaimsAuthorization(principal, operationContext);
    }
    private bool TryGetPrincipal(out ClaimsPrincipal principal)
    {
      principal = null;
      // check headers - authorization and x-authorization
      var headers = WebOperationContext.Current.IncomingRequest.Headers;
      if (headers != null)
      {
        var authZheader = headers[HttpRequestHeader.Authorization] ?? headers["X-Authorization"];
        if (!string.IsNullOrEmpty(authZheader))
        {
          int sep = authZheader.IndexOf(' ');
          if (sep != -1)
          {
            var scheme = authZheader.Substring(0, sep);
            var token = authZheader.Substring(sep + 1);
            try
            {
              principal = _manager.ValidateWebToken(scheme, token);
            }
            catch
            {
              throw new WebFaultException(HttpStatusCode.Unauthorized);
            }
            return (principal != null);
          }
          else
          {
            throw new SecurityTokenValidationException("Malformed authorization header");
          }
        }
        else
        {
          try
          {
            principal = _manager.ValidateWebToken("*", string.Empty);
            return (principal != null);
          }
          catch
          {
            throw new WebFaultException(HttpStatusCode.Unauthorized);
          }
        }
      }
      return false;
    }
    bool CallClaimsAuthorization(ClaimsPrincipal principal, OperationContext operationContext)
    {
      string action = string.Empty;
      var property = operationContext.IncomingMessageProperties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
      if (property != null)
      {
        action = property.Method;
      }
      Uri to = operationContext.IncomingMessageHeaders.To;
      if (to == null || string.IsNullOrEmpty(action))
      {
        return false;
      }
      var context = new AuthorizationContext(principal, to.AbsoluteUri, action);
      if (_configuration.ClaimsAuthorizationManager != null)
      {
        return _configuration.ClaimsAuthorizationManager.CheckAccess(context);
      }
      return false;
    }
    // ...
}

O método CheckAccessCore, obtém o principal e o atribui ao contexto de segurança da requisição; caso não consiga, o erro 401 é tratado e retornado ao chamador. 

Bloco de código
languagec#
titleWebUserNameSecurityTokenHandler
linenumberstrue
collapsetrue
public class WebUserNameSecurityTokenHandler : UserNameSecurityTokenHandler, IWebSecurityTokenHandler
{
    public ClaimsPrincipal ValidateWebToken(string token)
    {
      var decoded = DecodeBasicAuthenticationHeader(token);
      var securityToken = new UserNameSecurityToken(decoded.Item1, decoded.Item2);
      return new ClaimsPrincipal(ValidateToken(securityToken));
    }
    protected virtual Tuple<string, string> DecodeBasicAuthenticationHeader(string basicAuthToken)
    {
      Encoding encoding = Encoding.GetEncoding("iso-8859-1");
      string userPass = encoding.GetString(Convert.FromBase64String(basicAuthToken));
      int separator = userPass.IndexOf(':');
      var credential = new Tuple<string, string>(
          userPass.Substring(0, separator),
          userPass.Substring(separator + 1));
      return credential;
    }
    /// <summary>
    /// Callback type for validating the credential
    /// </summary>
    /// <param name="username">The username.</param>
    /// <param name="password">The password.</param>
    /// <returns>True when the credential could be validated succesfully. Otherwise false.</returns>
    public delegate bool ValidateUserNameCredentialDelegate(string username, string password);
    /// <summary>
    /// Gets or sets the credential validation callback
    /// </summary>
    /// <value>
    /// The credential validation callback.
    /// </value>
    public ValidateUserNameCredentialDelegate ValidateUserNameCredential { get; set; }
    /// <summary>
    /// Initializes a new instance of the <see cref="WebUserNameSecurityTokenHandler"/> class.
    /// </summary>
    public WebUserNameSecurityTokenHandler()
    { }
    /// <summary>
    /// Initializes a new instance of the <see cref="WebUserNameSecurityTokenHandler"/> class.
    /// </summary>
    /// <param name="validateUserNameCredential">The credential validation callback.</param>
    public WebUserNameSecurityTokenHandler(ValidateUserNameCredentialDelegate validateUserNameCredential)
    {
      if (validateUserNameCredential == null)
      {
        throw new ArgumentNullException("ValidateUserNameCredential");
      }
      ValidateUserNameCredential = validateUserNameCredential;
    }
    /// <summary>
    /// Validates the user name credential core.
    /// </summary>
    /// <param name="userName">Name of the user.</param>
    /// <param name="password">The password.</param>
    /// <returns></returns>
    protected virtual bool ValidateUserNameCredentialCore(string userName, string password)
    {
      if (ValidateUserNameCredential == null)
      {
        throw new InvalidOperationException("ValidateUserNameCredentialDelegate not set");
      }
      return ValidateUserNameCredential(userName, password);
    }
    /// <summary>
    /// Validates the username and password.
    /// </summary>
    /// <param name="token">The token.</param>
    /// <returns>A ClaimsIdentityCollection representing the identity in the token</returns>
    public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SecurityToken token)
    {
      if (token == null)
      {
        throw new ArgumentNullException("token");
      }
      if (base.Configuration == null)
      {
        throw new InvalidOperationException("No Configuration set");
      }
      UserNameSecurityToken unToken = token as UserNameSecurityToken;
      if (unToken == null)
      {
        throw new ArgumentException("SecurityToken is no UserNameSecurityToken");
      }
      if (!ValidateUserNameCredentialCore(unToken.UserName, unToken.Password))
      {
        throw new SecurityTokenValidationException(unToken.UserName);
      }
      var claims = new List<Claim>
            {
                new Claim(JwtClaimTypes.Subject, unToken.UserName),
                new Claim(JwtClaimTypes.Name, unToken.UserName),
                new Claim(JwtClaimTypes.AuthenticationMethod, OidcConstants.AuthenticationMethods.Password),
                AuthenticationInstantClaim.Now
            };
      var identity = new ClaimsIdentity(claims, AuthenticationTypes.Basic);
      if (Configuration.SaveBootstrapContext)
      {
        if (RetainPassword)
        {
          identity.BootstrapContext = new BootstrapContext(unToken, this);
        }
        else
        {
          identity.BootstrapContext = new BootstrapContext(new UserNameSecurityToken(unToken.UserName, null), this);
        }
      }
      return new ReadOnlyCollection<ClaimsIdentity>(new[] { identity });
    }
    /// <summary>
    /// Gets a value indicating whether this instance can validate a token.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this instance can validate a token; otherwise, <c>false</c>.
    /// </value>
    public override bool CanValidateToken
    {
      get
      {
        return true;
      }
    }
}
Bloco de código
languagec#
titleWebJwtSecurityTokenHandler
linenumberstrue
collapsetrue
public class WebJwtSecurityTokenHandler : JwtSecurityTokenHandler, IWebSecurityTokenHandler
{
    string _issuer = null;
    string _audience = null;
    X509Certificate2 _signingCert = null;
    private readonly ICollection<string> _allowedScopes;
    private WebJwtSecurityTokenHandler()
        : base()
    {
      JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
    }
    public WebJwtSecurityTokenHandler(JwtValidationOptions options)
       : this()
    {
      // Certificate
      var signOptions = options.SigningCertificateOptions;
      var certificate = signOptions.SigningCertificate;
      if (certificate == null)
        certificate = FindCertificate(signOptions.SigningCertificateName, signOptions.StoreLocation, signOptions.NameType);
      if (certificate == null) throw new ArgumentNullException(nameof(certificate));
      if (!certificate.HasPrivateKey)
      {
        throw new InvalidOperationException("X509 certificate does not have a private key.");
      }
      // Scopes
      var allowedScopes = new string[] { };
      if (options.ValidateScope && options.AllowedScopes.Any())
      {
        allowedScopes = options.AllowedScopes.ToArray();
      }
      
      _issuer = options.IssuerName;
      _signingCert = certificate;
      _audience = options.Audience;
      _allowedScopes = allowedScopes;
    }
    public ClaimsPrincipal ValidateWebToken(string token)
    {
      ClaimsPrincipal principal = null;
      if (token != null)
      {
        // seems to be a JWT
        if (token.Contains('.'))
        {
          var parameters = new TokenValidationParameters
          {
            IssuerSigningKey = new X509SecurityKey(_signingCert),
            ValidIssuer = _issuer,
            ValidateIssuer = true,
            ValidAudience = _audience,
            ValidateAudience = true,
            RequireSignedTokens = true,
            RequireExpirationTime = true
          };
          if (string.IsNullOrWhiteSpace(_audience))
            parameters.ValidateAudience = false;
          try
          {
            SecurityToken validatedToken;
            principal = ValidateToken(token, parameters, out validatedToken);
          }
          catch (Exception ex)
          {
            throw new SecurityTokenValidationException("Error validating token", ex);
          }
          if (_allowedScopes.Any())
          {
            bool found = false;
            foreach (var scope in _allowedScopes)
            {
              if (principal.HasClaim("scope", scope))
              {
                found = true;
                break;
              }
            }
            if (found == false)
            {
              throw new SecurityTokenValidationException("Insufficient Scope");
            }
          }
        }
      }
      return principal;
    }
	//...
}

.

Parâmetros


Os seguintes parâmetros podem ser utilizados no RM.Host.*.config a fim de configurar a segurança:

Chave

Tipo

Valor
HOSTAUTHENTICATION

Valor Específico

Basic

Jwt

BasicAndJwt

Parâmetros Específicos da Autenticação via Token (OAuth2/JWT)

ChaveTipoValor
JWTALLOWEDSCOPESTexto

OBS.: Este parâmetro ainda não é relevante para a linha RM. Reservado para melhorias futuras.

Indentificadores dos recursos expostos via serviço e que devem estar presentes no Jwt. Ex.: api1.write, api1.read

JWTAUDIENCETextoIdentifica o recurso protegido pelo Jwt. Ex.: api1
JWTCERTIFICATETHUMBPRINTTextoIdentifica o certificado utilizado para validar a assinatura do Jwt
JWTISSUERNAMETextoIdentifica o fornecedor do Jwt

 

 Antes de atribuir o componente de autenticação à thread corrente, é feita uma transformação do ClaimsPrincipal obtido em CorporePrincipal, componente de autenticação personalizado da linha RM. Este contém claims específicos para o correto funcionamento da linha. Esta transformação é feita pelo componente ClaimsTransfomer, que não será apresentado neste documento por questões de segurança.

Informações
iconfalse
Informações
iconfalse

Produto: Framework

Informações
iconfalse

Versão: 12.1.19

Informações
iconfalse

Processo: Estrutura de segurança de serviços HTTP

Informações
iconfalse
titleÍndice

Índice
exclude.*ndice:

Informações
iconfalse
Informações
iconfalse

Status: Em desenvolvimento

Informações
iconfalse

Data:  

Informações
iconfalse

Autores:

Diogo Damiani Ferreira