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 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;
    }
	//...
}

de segurança dos serviços HTTP (API's) expostos pelo RM Host. Por padrão, o Host já possui padrões de segurança já pré-definidos, porém, caso seja de interesse do cliente, é possível personalizar as informações de segurança, conforme explícito abaixo.

Parâmetros


Para personalizar as informações de segurança de API's é necessário realizar alterações nos arquivos de configuração dos hosts correspondentes (RM.Host.*.config). Na tabela abaixo estão contidos os parâmetros de segurança que são possíveis de serem alterados e mais abaixo, a explicação de cada um destes parâmetros.

ChaveTipoObrigatoriedadeValor
HOSTAUTHENTICATIONValor EspecíficoOpcional
  • All (Default)
  • Basic
  • JWT
JWTALLOWEDSCOPESTextoOpcional

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

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

JWTAUDIENCETextoOpcionalDestinatário do token, representa a aplicação que irá usá-lo.
Ex.: api1
JWTCERTIFICATETHUMBPRINTTextoObrigatórioIdentifica o certificado utilizado para validar a assinatura do JWT
Ex.: D1AE26F90417AC68D13DC0C734F950541F0CCB36
JWTISSUERNAMETextoOpcionalO nome da API/fornecedor do token (Caso não seja fornecido, a assinatura será feita como "totvs-rm-host").
Ex.: https://sts.totvs.com.br


  • HOSTAUTHENTICATION

O RM possibilita a autenticação de diversas formas pelas API's, conforme a documentação: Autorização / Autenticação em API's. Esta tag de configuração permite que o host aceite apenas um tipo de autenticação ao invés de todos suportados, que é o valor padrão da configuração. Caso seja escolhido um método específico, os outros naturalmente rejeitarão qualquer tentativa de autenticação, retornando o HTTP Error 401 - Unauthorized.

Informações
Bloco de código
languagexml
<add key="HOSTAUTHENTICATION" value="JWT"

Na configuração acima, o host só aceitará chamadas que utilizem de bearer token em suas tentativas de autenticação.

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

  • JWTAUDIENCE

Este parâmetro é opcional, e irá determinar quais destinatários/aplicações irão utilizar o token.

Informações
Bloco de código
languagexml
<add key="JWTAUDIENCE" value="api1"


  • JWTCERTIFICATETHUMBPRINT

Caso o objetivo seja utilizar um certificado digital personalizado para registro de tokens JWT, garantindo assim ainda mais segurança para sua aplicação (Vide Autorização / Autenticação em API's), este parâmetro é obrigatório, afinal ele receberá a assinatura (Thumbprint) do certificado personalizado salvo no servidor. Caso este parâmetro não seja fornecido, o RM irá registrar os tokens utilizando um certificado padrão embutido e assinado pelo próprio RM

Informações
Bloco de código
languagexml
<add key="JWTCERTIFICATETHUMBPRINT" value="D1AE26F90417AC68D13DC0C734F950541F0CCB36"


  • JWTISSUERNAME

Este parâmetro recebe o fornecedor do certificado digital personalizado, caso este seja fornecido (Veja JWTCERTIFICATETHUMBPRINT). Este fornecedor será enviado no JWT e será utilizado por aplicações exteriores que façam uso da autenticação com o RM, como o TReports. Caso não seja fornecido, o fornecedor será enviado como "totvs-rm-host" automaticamente.

Informações
Bloco de código
languagexml
<add key="JWTISSUERNAME" value="https://sts.totvs.com.br"
Dica
Por padrão, o RM certifica e valida um token padrão do sistema, porém, para um nível maior de segurança, é recomendada a implementação de um certificado confiável próprio para validação. Instruções mais detalhadas para implementação podem ser encontradas em Autorização / Autenticação em API's
Informações

Abaixo o exemplo de uma configuração de certificado digital para JWT válida:Image Added


O certificado para validação de tokens JWT deve estar na pasta Pessoal da máquina local:

Image Added

Validação de JWT


Para confirmar que o token não foi adulterado durante a comunicação entre aplicações e o RM, é necessário validá-lo e para isso, é necessário possuir a chave pública responsável pela assinatura do JWT. Como a assinatura de tokens de segurança do RM funciona de forma assimétrica (Qualquer aplicação é capaz de validar o token recebido pelo RM, através de chaves públicas, porém apenas o RM é capaz de assinar um token de segurança, através de chaves privadas), a API de JWKS fornece a lista de JWK's possíveis para validação do token:

Informações
iconfalse
Bloco de código
GET [HOST]:[PORT]/api/.well-known/security/jwks
Informações

Para mais informações acerca de Json Web Tokens (JWT) e toda a arquitetura, acesse: Json Web Tokens - jwt.io, RFC7519 - JSON Web Token (JWT), RFC7515 - JSON Web Signature (JWS), RFC7516 - JSON Web Encryption (JWE), RFC7517 - JSON Web Key (JWK)RFC7518 - JSON Web Algorithms (JWA)

Exemplos


Chamando endpoint via Postman

  1. Endpoint protegido sem autenticação:
    Image Added

  2. Endpoint protegido com HTTP Basic:
    Image Added

  3. Endpoint protegido com Bearer Token (JWT):

    Image Added
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.19XXXX

Informações
iconfalse

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

Informações
iconfalse
titleÍndice

Índice
exclude.*ndice:
stylenone

Informações
iconfalse
Informações
iconfalse

Status: Em desenvolvimento

Informações
iconfalse

Data:20092017 

Informações
iconfalse

Autores:

Diogo Damiani Ferreira

Samuel Rener Santos Silva_