Artigo
· Nov. 27, 2023 11min de leitura

Protegendo endpoints individuais de API REST

Eu estava tentando encontrar uma solução para conceder aos clientes acesso anônimo a determinados endpoints de API e também proteger outros endpoints na minha API REST. No entanto, ao definir um Web App, você só pode proteger o aplicativo inteiro, e não partes específicas.

Procurei respostas na comunidade, mas não encontrei nenhuma solução exata, exceto uma recomendação para criar dois Web Apps separados, um protegido e outro não. No entanto, na minha opinião, essa estratégia requer muito trabalho e cria um overhead de manutenção desnecessário. Prefiro desenvolver minhas APIs começando pela especificação e decidir nela quais endpoints devem permitir o acesso anônimo ou não.

Neste artigo, forneço dois exemplos: um para a Autenticação Básica e outro para o JWT, que é usado no contexto do OAuth 2.0. Se você notar alguma falha nestes exemplos, me avise e farei as correções necessárias.

Pré-requisitos

Primeiro, defina um Web App para sua API REST. Configure-o para o acesso não autenticado e especifique os privilégios necessários para o aplicativo. Especifique apenas as funções e os recursos necessários para o sucesso do uso da API.

Crie uma classe, por exemplo, REST.Utils, onde você implementará os classmethods helper que verificam as credenciais.

Class REST.Utils  
{

}

Autenticação Básica

Se você quiser um endpoint seguro usando a Autenticação Básica, use o seguinte método para verificar se o nome de usuário e a senha informados no cabeçalho da Autorização HTTP têm os privilégios corretos para acessar o endpoint de API restrito.

/// Confira se os usuários têm as permissões necessárias.
/// - auth: o cabeçalho da Autorização.
/// - resource: o recurso para verificar as permissões.
/// - permissions: as permissões verificadas.
/// 
/// Exemplo:
/// > Do ##class(REST.Utils).CheckBasicCredentials(%request.GetCgiEnv("HTTP_AUTHORIZATION", ""), "RESOURCE", "U")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckBasicCredentials(auth As %String, resource As %String, permissions As %String) As %Status
{
  /// Analise a sanidade da entrada  
  if (auth = "") {
    Return $$$ERROR($$$GeneralError, "No Authorization header provided")
  }

  /// Confira se o cabeçalho da autorização começa com Basic  
  if ($FIND(auth, "Basic") > 0) {
    /// Retire a parte "Basic" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
  }

  Set tStatus = $$$OK

  /// Decodifique o nome de usuário e a senha codificados em base64  
  Set auth = $SYSTEM.Encryption.Base64Decode(auth)
  Set username = $PIECE(auth, ":", 1)
  Set password = $PIECE(auth, ":", 2)

  /// Tente fazer login como o usuário informado no cabeçalho da Autorização  
  Set tStatus = $SYSTEM.Security.Login(username, password)

  if $$$ISERR(tStatus) {
    Return tStatus
  }

  /// Confira se o usuário tem as permissões necessárias  
  Set tStatus = $SYSTEM.Security.Check(resource, permissions)

  /// Retorne o status. Se o usuário tiver as permissões necessárias, o status será $$$OK  
  Return tStatus
}

No endpoint que você quer proteger, chame o método CheckBasicCredentials e confira o valor retornado. Um valor de retorno 0 indica a falha na verificação. Nesses casos, retornamos HTTP 401 de volta ao cliente.

O exemplo abaixo verifica se o usuário tem o recurso SYSTEM_API definido com privilégios USE. Se não tiver, retorne HTTP 401 ao cliente. Lembre-se de que o usuário da API precisa ter o privilégio %Service_Login:USE para usar o método Security.Login.

Exemplo

  Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
  Set tStatus = ##class(REST.Utils).CheckBasicCredentials(authHeader, "SYSTEM_API", "U")
  if ($$$ISERR(tStatus)) {
    Set %response.Status = 401
    Return
  }
  ... resto do código

JWT

Em vez de usar a Autenticação Básica para proteger um endpoint, prefiro usar os Tokens de Acesso JWT do OAuth 2.0, já que são mais seguros e oferecem uma maneira mais flexível de definir privilégios por escopos. O seguinte método verifica se o token de acesso JWT fornecido no cabeçalho da Autorização HTTP tem os privilégios corretos para acessar o endpoint de API restrito.

/// Verifique se o JWT fornecido é válido.
/// - auth: o cabeçalho da Autorização.
/// - scopes: os escopos que esse token JWT deve ter.
/// - oauthClient: o cliente OAuth que é usado para validar o token JWT. (opcional)
/// - jwks: o JWKS usado para a validação da assinatura do token (opcional)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
  Set tStatus = $$$OK

  /// Analise a sanidade da entrada  
  if (token = "") {
    Return $$$ERROR($$$GeneralError, "No token provided")
  }

  /// Confira se o cabeçalho da autorização começa com Bearer. Se sim, faça a limpeza do token.  
  if ($FIND(token, "Bearer") > 0) {
    /// Retire a parte "Bearer" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
  }

  /// Crie uma lista da string de escopos  
  Set scopes = $LISTFROMSTRING(scopes, ",")

  Set scopeList = ##class(%ListOfDataTypes).%New()
  Do scopeList.InsertList(scopes)

  /// Remova os espaços em branco de cada escopo  
  For i=1:1:scopeList.Count() {
    Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
  }

  /// Decodifique o token  
  Try {
    Do ..JWTToObject(token, .payload, .header)
  } Catch ex {
    Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
  }

  /// Obtenha o epoch time atual
  Set now = $ZDATETIME($h,-2)

  /// Confira se o token expirou  
  if (payload.exp < now) {
    Return $$$ERROR($$$GeneralError, "Token has expired")
  }

  Set scopesFound = 0

  /// Confira se o token tem os escopos necessários
  for i=1:1:scopeList.Count() {
    Set scope = scopeList.GetAt(i)
    Set scopeIter = payload.scope.%GetIterator()
    While scopeIter.%GetNext(.key, .jwtScope) {
      if (scope = jwtScope) {
        Set scopesFound = scopesFound + 1
      }
    }
  }

  if (scopesFound < scopeList.Count()) {
    Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
  }

  /// Se o token é válido em todo o escopo e não expirou, confira se a assinatura é valida
  if (oauthClient '= "") {
    /// Se especificamos um cliente OAuth, use isso para validar a assinatura do token
    Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
    if ($$$ISERR(tStatus) || result '= 1) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation")
    }
  } elseif (jwks '= "") {
    /// Se especificamos um JWKS, use isso para validar a assinatura do token
    Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
    if ($$$ISERR(tStatus)) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
    }
  }

  Return tStatus
}

/// Decodifique um token JWT.
/// - token: o token JWT a decodificar.
/// - payload: o payload do token JWT. (Saída)
/// - header: o cabeçalho do token JWT. (Saída)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
  Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")

  /// Decodifique e processe o Cabeçalho  
  Set header = $SYSTEM.Encryption.Base64Decode(header)
  Set header = {}.%FromJSON(header)

  /// Decodifique e processe o Payload  
  Set payload = $SYSTEM.Encryption.Base64Decode(payload)
  Set payload = {}.%FromJSON(payload)

  Return $$$OK
}

Novamente, no endpoint que você quer proteger, chame o método CheckJWTCredentials e confira o valor retornado. Um valor de retorno 0 indica a falha na verificação. Nesses casos, retornamos HTTP 401 de volta ao cliente.

O exemplo abaixo verifica se o token tem os escopos scope1 e scope2 definidos. Se não tiver os escopos necessários, houver expirado ou falhar na validação da assinatura, ele retornará um código de status HTTP 401 ao cliente.

Exemplo

  Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
  Set tStatus = ##class(REST.Utils).CheckJWTCredentials(authHeader, "scope1,scope2")
  if ($$$ISERR(tStatus)) {
    Set %response.Status = 401
    Return
  }
  ... resto do código

Conclusão

Aqui está o código completo para a classe REST.Utils. Se você tiver quaisquer sugestões sobre como melhorar o código, me informe. Atualizarei o artigo devidamente.

Uma melhoria óbvia seria verificar a assinatura JWT para garantir que é válida. Para fazer isso, você precisa ter a chave pública do emissor.

Class REST.Utils
{

/// Confira se os usuários têm as permissões necessárias.
/// - auth: o conteúdo do cabeçalho da Autorização.
/// - resource: o recurso para verificar as permissões.
/// - permissions: as permissões verificadas.
/// 
/// Exemplo:
/// > Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckBasicCredentials(authHeader, "RESOURCE", "U"))
/// 
/// Retorne: %Status. O status da verificação.  
ClassMethod CheckBasicCredentials(authHeader As %String, resource As %String, permissions As %String) As %Status
{
  Set auth = authHeader

  /// Analise a sanidade da entrada  
  if (auth = "") {
    Return $$$ERROR($$$GeneralError, "No Authorization header provided")
  }

  /// Confira se o cabeçalho da autorização começa com Basic  
  if ($FIND(auth, "Basic") > 0) {
    // Retire a parte "Basic" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
  }

  Set tStatus = $$$OK

  Try {
  /// Decodifique o nome de usuário e a senha codificados em base64  
  Set auth = $SYSTEM.Encryption.Base64Decode(auth)
  Set username = $PIECE(auth,":",1)
  Set password = $PIECE(auth,":",2)
  } Catch {
    Return $$$ERROR($$$GeneralError, "Not a valid Basic Authorization header")
  }

  /// Tente fazer login como o usuário informado no cabeçalho da Autorização  
  Set tStatus = $SYSTEM.Security.Login(username,password)

  if $$$ISERR(tStatus) {
    Return tStatus
  }

  /// Confira se o usuário tem as permissões necessárias  
  Set tStatus = $SYSTEM.Security.Check(resource, permissions)

  /// Retorne o status. Se o usuário tiver as permissões necessárias, o status será $$$OK  
  Return tStatus
}

/// Verifique se o JWT fornecido é válido.
/// - auth: o cabeçalho da Autorização.
/// - scopes: os escopos que esse token JWT deve ter.
/// - oauthClient: o cliente OAuth que é usado para validar o token JWT. (opcional)
/// - jwks: o JWKS usado para a validação da assinatura do token (opcional)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
  Set tStatus = $$$OK

  /// Analise a sanidade da entrada  
  if (token = "") {
    Return $$$ERROR($$$GeneralError, "No token provided")
  }

  /// Confira se o cabeçalho da autorização começa com Bearer. Se sim, faça a limpeza do token.  
  if ($FIND(token, "Bearer") > 0) {
    /// Retire a parte "Bearer" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
  }

  /// Crie uma lista da string de escopos  
  Set scopes = $LISTFROMSTRING(scopes, ",")

  Set scopeList = ##class(%ListOfDataTypes).%New()
  Do scopeList.InsertList(scopes)

  /// Remova os espaços em branco de cada escopo  
  For i=1:1:scopeList.Count() {
    Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
  }

  /// Decodifique o token  
  Try {
    Do ..JWTToObject(token, .payload, .header)
  } Catch ex {
    Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
  }

  /// Obtenha o epoch time atual
  Set now = $ZDATETIME($h,-2)

  /// Confira se o token expirou  
  if (payload.exp < now) {
    Return $$$ERROR($$$GeneralError, "Token has expired")
  }

  Set scopesFound = 0

  /// Confira se o token tem os escopos necessários
  for i=1:1:scopeList.Count() {
    Set scope = scopeList.GetAt(i)
    Set scopeIter = payload.scope.%GetIterator()
    While scopeIter.%GetNext(.key, .jwtScope) {
      if (scope = jwtScope) {
        Set scopesFound = scopesFound + 1
      }
    }
  }

  if (scopesFound < scopeList.Count()) {
    Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
  }

  /// Se o token é válido em todo o escopo e não expirou, confira se a assinatura é valida
  if (oauthClient '= "") {
    /// Se especificamos um cliente OAuth, use isso para validar a assinatura do token
    Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
    if ($$$ISERR(tStatus) || result '= 1) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation")
    }
  } elseif (jwks '= "") {
    /// Se especificamos um JWKS, use isso para validar a assinatura do token
    Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
    if ($$$ISERR(tStatus)) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
    }
  }

  Return tStatus
}


/// Decodifique um token JWT.
/// - token: o token JWT a decodificar.
/// - payload: o payload do token JWT. (Saída)
/// - header: o cabeçalho do token JWT. (Saída)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
  Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")

  /// Decodifique e processe o Cabeçalho  
  Set header = $SYSTEM.Encryption.Base64Decode(header)
  Set header = {}.%FromJSON(header)

  /// Decodifique e processe o Payload  
  Set payload = $SYSTEM.Encryption.Base64Decode(payload)
  Set payload = {}.%FromJSON(payload)

  Return $$$OK
}
}
Discussão (0)1
Entre ou crie uma conta para continuar