Artigo
· Maio 9, 2023 10min de leitura

Criando uma API REST com autenticação JWT no ObjectScript

Prefácio

O InterSystems IRIS a partir da versão 2022.2 inclui a capacidade de autenticar uma API REST usando JSON web tokens (JWTs). Esse recurso aprimora a segurança ao limitar quando e com que frequência as senhas são transferidas pela rede, além de definir um tempo de expiração para o acesso.

O objetivo deste artigo é servir como um tutorial de como implementar uma API REST simulada usando o InterSystems IRIS e bloquear o acesso a ela com JWTs.

OBSERVAÇÃO NÃO sou uma desenvolvedora. Não faço alegações sobre a eficiência, escalabilidade ou qualidade das amostras de código que uso neste artigo. Estes exemplos são APENAS para fins educacionais. Eles NÃO se destinam a código de produção.

Prólogo

Depois de fazer esse aviso, vamos explorar os conceitos que serão analisados aqui.

O que é REST?

REST é um acrônimo para "REpresentational State Transfer". É uma arquitetura para os programas se comunicarem com os aplicativos da Web e acessarem as funções publicadas por esses aplicativos.

O que é um JWT?

Um JSON web token (JWT) é uma maneira compacta e segura para o URL de representar informações transferidas entre duas partes que podem ser assinadas digitalmente, criptografadas ou ambos. Se você quiser saber mais sobre os JWTs e outras classes da Web JSON compatíveis com o InterSystems IRIS, leia esta postagem.

Colocando a mão na massa

De acordo com a especificação

Para consumir uma API REST, primeiro precisamos ter uma API REST. Disponibilizei uma amostra de especificação da OpenAPI 2.0 para um RPG de mesa (TTRPG). Ela será usada nos exemplos deste artigo. Há vários exemplos de como escrever a sua online, então fique à vontade para se aprofundar nisso, mas a especificação é apenas um modelo. A única coisa que faz é informar como usar a API.

Geração da API REST

O InterSystems IRIS oferece uma maneira bastante organizada de gerar stubs de código da API REST. Esta documentação oferece uma maneira completa de gerar stubs de código. Fique à vontade para usar a especificação da OpenAPI 2.0 que forneci na seção anterior.

Implementação

É aqui que vamos ir a fundo. A seção de geração criará três arquivos .cls para você:

  1. impl.cls
  2. disp.cls
  3. spec.cls

Vamos passar a maior parte do nosso tempo no impl.cls, talvez mexer no disp.cls para depurar e não encostar no spec.cls.

No impl.cls, há stubs de código para os métodos que disp.cls chamará quando receber uma solicitação da API. A especificação da OpenAPI definida nessas assinaturas. Ela informa o que você quer que seja feito, mas é você quem precisa implementar isso no final. Então, vamos fazer isso!

Criação

Uma das maneiras que usamos um banco de dados é adicionando objetos a ele. Esses objetos servem como uma base para outras funções. Sem objetos existentes, não haverá nada para ver, então vamos começar com nosso modelo de objeto: um Character (personagem)!

Um Character precisa ter nome e, como opção, especificar a classe, a raça e o nível. Veja abaixo um exemplo de implementação da classe TTRPG.Character

Class TTRPG.Character Extends %Persistent
{

Property Name As %String [ Required ];

Property Race As %String;

Property Class As %String;

Property Level As %String;

Index IndexName On Name [ IdKey ];

ClassMethod GetCharByName(name As %String) As TTRPG.Character
{
    set character = ##class(TTRPG.Character).%OpenId(name)

    Quit character
}
}

Já que queremos armazenar objetos Character no banco de dados, precisamos herdar a classe %Persistent. Queremos que seja possível procurar personagens pelo nome, em vez de atribuir uma chave de ID arbitrária. Portanto, definimos o atributo [ IdKey ] no Index para a propriedade Character.Name. Isso também garante a exclusividade do nome do personagem.

Com nosso modelo de objeto base definido, podemos analisar a implementação da API REST. O primeiro método que vamos explorar é o PostCharacter.

Como visão geral, esta parte consome uma solicitação HTTP POST para o endpoint /characters com nossas propriedades de personagem definidas no corpo. Ela deve pegar os argumentos fornecidos e criar um objeto TTRPG.Character a partir deles, salvá-lo no banco de dados e informar se teve êxito ou não.

ClassMethod PostCharacter(name As %String, class As %String, race As %String, level As %String) As %DynamicObject
{
    set results = {} // cria o retorno %DynamicObject

    //cria o objeto character
    set char = ##class(TTRPG.Character).%New()

    set char.Name = name
    set char.Class = class
    set char.Race = race
    set char.Level = level
    set st = char.%Save()

    if st {
        set charInfo = {}
        set charInfo.Name = char.Name
        set charInfo.Class = char.Class
        set charInfo.Race = char.Race
        set charInfo.Level = char.Level
        set results.Character = charInfo
        Set results.Status = "success"
    }
    else {
        Set results.Status = "error"
        Set results.Message = "Unable to create the character"
    }
    Quit results
}

Agora que podemos criar personagens, como buscamos aquele que acabamos de criar? De acordo com a especificação da OpenAPI, o endpoint /characters/{charName} permite a busca de um personagem pelo nome. Buscamos a instância do personagem, se ela existir. Se não existir, retornamos um erro para informar ao usuário que não existe um personagem com o nome fornecido. Isso é implementado no método GetCharacterByName.

ClassMethod GetCharacterByName(charName As %String) As %DynamicObject
{
   // Cria um novo objeto dinâmico para armazenar os resultados
        Set results = {}

        set char = ##class(TTRPG.Character).GetCharByName(charName)

        if char {
           set charInfo = {}
            set charInfo.Name = char.Name
            set charInfo.Class = char.Class
            set charInfo.Race = char.Race
            set charInfo.Level = char.Level
            set results.Character = charInfo
            Set results.Status = "success"
        }
        // Se nenhum character foi encontrado, define uma mensagem de erro no objeto dos resultados
        else {
            Set results.Status = "error"
            Set results.Message = "No characters found"
        }

        // Retorna o objeto dos resultados
        Quit results
}

Mas isso é só o seu personagem. E todos os personagens que as outras pessoas criaram? Podemos ver esses personagens usando o método GetCharacterList. Ele consome uma solicitação HTTP GET para o endpoint /characters para compilar uma lista de todos os personagens no banco de dados e retorna essa lista.

ClassMethod GetCharacterList() As %DynamicObject
{
    // Cria um novo objeto dinâmico para armazenar os resultados
        Set results = {}
        set query = "SELECT Name, Class, Race, ""Level"" FROM TTRPG.""Character"""
        set tStatement = ##class(%SQL.Statement).%New()
        set qstatus = tStatement.%Prepare(query)
        if qstatus '= 1 { Do ##class(TTRPG.impl).%WriteResponse("Error: " _ $SYSTEM.Status.DisplayError(qstatus)) }
        set rset = tStatement.%Execute()
        Set characterList = []
        while rset.%Next(){
            Set characterInfo = {}
            Set characterInfo.Name = rset.Name
            set characterInfo.Race = rset.Race
            Set characterInfo.Class = rset.Class
            Set characterInfo.Level = rset.Level 

            Do characterList.%Push(characterInfo)

        }
        if (rset.%SQLCODE < 0) {write "%Next failed:", !, "SQLCODE ", rset.%SQLCODE, ": ", rset.%Message quit}

        set totalCount = rset.%ROWCOUNT

            // Define as propriedades status, totalCount e characterList no objeto dos resultados
            Set results.Status = "success"
            Set results.TotalCount = totalCount
            Set results.CharacterList = characterList


        // Retorna o objeto dos resultados
        Quit results
}

E essa é nossa API! A especificação atual não oferece uma maneira de atualizar ou excluir personagens do banco de dados, e isso fica como um exercício para o leitor!

Configuração do IRIS

Agora que implementamos nossa API REST, como fazemos a comunicação com o IRIS? No Portal de Gerenciamento, se você acessar a página System Administration > Security > Applications > Web Applications, poderá criar um novo aplicativo da Web. O nome do aplicativo é o endpoint que você usará ao fazer solicitações. Por exemplo, se o nome for /api/TTRPG/, as solicitações da API vão para http://{IRISServer}:{host}/api/TTRPG/{endpoint}. Para uma instalação do IRIS padrão local com segurança normal, é assim: http://localhost:52773/api/TTRPG/{endpoint}. Adicione uma boa descrição, defina o namespace desejado e clique no botão de opção para REST. Para ativar a autenticação JWT, selecione a caixa "Use JWT Authentication". O JWT Access Token Timeout determina a frequência com que o usuário precisará receber um novo JWT. Se você planeja testar a API por um longo período, recomendo definir esse valor como uma hora (3600 segundos) e o JWT Refresh Token Timeout (o período de renovação antes que o token expire para sempre) como 900 segundos.

configuração do web app

Agora que o aplicativo foi configurado, precisamos configurar o próprio IRIS para permitir a autenticação de JWT. É possível configurar essa opção em System Administration > Security > System Security > Authentication/Web Session Options. Na parte inferior, está o campo do emissor de JWT e o algoritmo de assinatura que será usado para assinar e validar os JWTs. O campo do emissor aparecerá na seção de informações do JWT e a finalidade é informar quem forneceu o token a você. Você pode defini-lo como "InterSystems".

configuração da autenticação de JWT

Hora de testar

Está tudo configurado e implementado, então vamos testar! Carregue sua ferramenta favorita para criar solicitações de API (vou usar uma extensão do Firefox chamada RESTer nos exemplos) e vamos começar a construir solicitações da API REST.

Primeiro, vamos tentar listar os personagens existentes.

lista sem token

Recebemos um erro 401 Unauthorized. Isso ocorreu porque não fizemos login. Você talvez esteja pensando: Elliott, não implementamos funcionalidade de login nessa API REST. Não tem problema, porque o InterSystems IRIS cuida disso para nós quando usamos a autenticação de JWT. Ele oferece quatro endpoints que podemos usar para gerenciar nossa sessão. São eles: /login, /logout /revoke e /refresh. Eles podem ser personalizados no disp.cls conforme o exemplo abaixo:

Parameter TokenLoginEndpoint = "mylogin";
Parameter TokenLogoutEndpoint = "mylogout";
Parameter TokenRevokeEndpoint = "myrevoke";
Parameter TokenRefreshEndpoint = "myrefresh";

Vamos acessar o endpoint /login agora.

fazendo login

O corpo dessa solicitação não é exibido por medidas de segurança, mas ele segue esta estrutura JSON:

{"user":"{YOURUSER}", "password":"{YOURPASSWORD}"}

Em troca da senha, recebemos um JWT! Esse é o valor de "access_token". Vamos copiar isso e usar nas nossas solicitações futuras para não precisar sempre transmitir a senha.

Agora que temos um JWT para autenticação, vamos tentar criar um personagem!

Formatamos nossa solicitação conforme abaixo:

criação de personagem

Usando o bearer token como cabeçalho no formato de " Authorization: Bearer {JWTValue}". Em uma solicitação curl, você pode escrever isso com -H "Authorization: Bearer {JWTValue}"

Vamos criar outro personagem por diversão, usando os valores que você quiser.

Agora, vamos tentar listar todos os personagens que existem no banco de dados.

listando os personagens

Obtemos os dois personagens que criamos! E se só quisermos acessar um? Implementamos isso com o endpoint /characters/{charName}. Podemos formatar essa solicitação desta forma:

buscando personagem específico

Essa é a nossa API REST em ação, pessoal! Quando concluir sua sessão, é possível sair no endpoint /logout usando seu JWT. Isso revogará e bloqueará o JWT para que não seja possível usá-lo novamente.

Conclusão

O InterSystems IRIS a partir da versão 2022.2 inclui a capacidade de autenticar uma API REST usando JSON web tokens (JWTs). Esse recurso aprimora a segurança ao limitar o uso da senha e definir uma data de expiração para o acesso à API.

Espero que este manual sobre como gerar uma API REST e protegê-la com JWTs pelo IRIS tenha sido útil. Me avise se foi! Agradeço qualquer feedback.

Discussão (0)1
Entre ou crie uma conta para continuar