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:
- Checa o cabeçalho da requisição HTTP em busca do parâmetro de autorização (Authorization Header);
- Checa se um token está presente;
- 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;
- 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 |
---|
language | c# |
---|
title | WebTokenWebServiceHost |
---|
linenumbers | true |
---|
collapse | true |
---|
|
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 |
---|
language | c# |
---|
title | RMSWCFBrokerServer |
---|
linenumbers | true |
---|
collapse | true |
---|
|
//...
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 |
---|
language | c# |
---|
title | WebTokenServiceAuthorizationManager |
---|
linenumbers | true |
---|
collapse | true |
---|
|
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 |
---|
language | c# |
---|
title | WebUserNameSecurityTokenHandler |
---|
linenumbers | true |
---|
collapse | true |
---|
|
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 |
---|
language | c# |
---|
title | WebJwtSecurityTokenHandler |
---|
linenumbers | true |
---|
collapse | true |
---|
|
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.
Chave | Tipo | Obrigatoriedade | Valor |
---|
HOSTAUTHENTICATION | Valor Específico | Opcional | |
JWTALLOWEDSCOPES | Texto | Opcional | 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 |
JWTAUDIENCE | Texto | Opcional | Destinatário do token, representa a aplicação que irá usá-lo. Ex.: api1 |
JWTCERTIFICATETHUMBPRINT | Texto | Obrigatório | Identifica o certificado utilizado para validar a assinatura do JWT Ex.: D1AE26F90417AC68D13DC0C734F950541F0CCB36 |
JWTISSUERNAME | Texto | Opcional | O 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 |
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 |
---|
| <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)
Este parâmetro é opcional, e irá determinar quais destinatários/aplicações irão utilizar o token.
Informações |
---|
Bloco de código |
---|
| <add key="JWTAUDIENCE" value="api1" |
|
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 |
---|
| <add key="JWTCERTIFICATETHUMBPRINT" value="D1AE26F90417AC68D13DC0C734F950541F0CCB36" |
|
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 |
---|
| <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 |
---|
|
Bloco de código |
---|
GET [HOST]:[PORT]/api/.well-known/security/jwks |
|
Exemplos
Chamando endpoint via Postman
- Endpoint protegido sem autenticação:
Image Added
- Endpoint protegido com HTTP Basic:
Image Added
- 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.