Versões comparadas

Chave

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

Objetivo


Este documento tem como objetivo explicar de forma técnica aos desenvolvedores TOTVS o funcionamento do mecanismo de autenticação e autorização para os serviços HTTP expostos pelo Host.

Aviso
titleAviso Legal

Este documento pode conter informações confidenciais e/ou privilegiadas. Se você não for o colaborador da TOTVS ou uma pessoa autorizada a receber este documento, não deve usar, copiar ou divulgar as informações nele contidas ou tomar qualquer ação baseada nessas informações.

Aviso
titleDisclaimer

The information contained in this document may be privileged and confidential and protected from disclosure. If the reader of this document is not the employee TOTVS, or an employee agent responsible for delivering this document to the intended recipient, you are hereby notified that any dissemination, distribution or copying of this communication is strictly prohibited.

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 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 a requisição 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;
    }
	//...
}

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.

Atributos importantes

A Lib do RM fornece alguns atributos que são necessários para a criação e exposição de serviços HTTP no Host. São eles:

  • RMSWseService: Expõe um RMSServer como um serviço SOAP.
  • RMSRestService: Expõe um RMSServer como um serviço REST.
  • RMSRestRawService: Expõe um RMSServer como um serviço REST que recebe e responde em Raw. (Ignora o DataContractSerializer)

Todos estes atributos possuem a propriedade RequiredAuthentication, cujo valor padrão é verdadeiro. Caso o desenvolvedor necessite criar um serviço que não utiliza o mecanismo de autenticação padrão, pode mudar este valor para falso. Essa propriedade atua em conjunto com outro atributo que permite a definição de operações anônimas.

Quando o serviço possui o atributo e a propriedade RequiredAuthentication tiver o valor falso, o atributo de operações anônimas é inserido automaticamente em todas as operações. 

O atributo para definir operações anônimas é o AllowAnnonymous e pode ser aplicado somente a métodos do serviço:

Bloco de código
languagec#
titleExemplo Operação Anônima
linenumberstrue
collapsetrue
[RMSRestRawService("rest/", RequireAuthentication = true)]
public class RMSAnnonymousRestServer : RMSServer, IRMSAnnonymousRestServer
{
    [AllowAnonymous]
    public Stream Annonymous()
    {
      return new MemoryStream(Encoding.UTF8.GetBytes("Annonymous"));
    }
}

demonstrar como alterar o logo padrão do Portal FrameHTML.

Alterando arquivos na estrutura do Portal


Por padrão, o portal é exibido com a logo da TOTVS®, mas em alguns casos é necessário alterar a imagem ali presente.

Image Added

Para alterar as imagens, siga os passos abaixo:

  1. Acesse o caminho: C:\Totvs\CorporeRM\FrameHTML\RM\totvs-html-framework\assets\img;
  2. Altere o arquivo totvs.png para a imagem desejada (lembre se de manter esse mesmo nome)

 

Conforme visualizado abaixo, a logo foi alterada.

Image Added

Outros arquivos podem ser alterados seguindo o mesmo procedimento. Por exemplo, o favicon.ico

 

Informações
iconfalse
Informações
iconfalse

Produto: Framework

Informações
iconfalse

Versão: 12.1.1918

Informações
iconfalse

Processo: Estrutura de segurança de serviços HTTP Alteração de identidade visual

Informações
iconfalse
titleÍndice

Índice
exclude.*ndice:
stylenone

Informações
iconfalse
Informações
iconfalse

Status: Em desenvolvimento Finalizado

Informações
iconfalse

Data:20  

Informações
iconfalse

Autores:

Diogo Damiani Ferreira