Artigo
· Nov. 19, 2022 20min de leitura

Implementação do Open Authorization Framework do InterSystems IRIS (OAuth 2.0) - parte 2

Criado por Daniel Kutac, Engenheiro de vendas, InterSystems

Aviso: se você ficar confuso com os URLs usados: a série original usou telas de uma máquina chamada dk-gs2016. As novas capturas de tela foram tiradas em uma máquina diferente. Você pode tratar o URL WIN-U9J96QBJSAG como se fosse o dk-gs2016 com segurança.

Parte 2. Servidor de autorização, servidor OpenID Connect

Na parte anterior desta série curta, aprendemos sobre o caso de uso simples – atuando como um cliente OAUTH[1]. Agora, é hora de levar nossa experiência a um nível completamente novo. Vamos construir um ambiente muito mais complexo, onde o InterSystems IRIS vai desempenhar todas as funções OAUTH.

Já sabemos como fazer um cliente, então vamos nos concentrar no servidor de autorização e, ainda mais, no provedor OpenID Connect[2].

Como na parte anterior, precisamos preparar o ambiente. Desta vez, será mais complicado, pois há mais partes móveis.

Antes de entrarmos nos detalhes do nosso exemplo, precisamos falar algumas palavras sobre o OpenID Connect.

Como você deve lembrar, na parte anterior, recebemos a solicitação – para ter a autorização do Google – para nos autenticarmos primeiro com o Google. A autenticação não faz parte do framework OAUTH. Na verdade, há vários frameworks de autenticação, independentes do OAUTH. Um deles é chamado OpenID. Originalmente uma iniciativa independente, ele agora aproveita a infraestrutura fornecida pelo framework OAUTH, ou seja, as estruturas de comunicação e dados. Assim, nasceu o OpenID Connect. Na verdade, muitas pessoas o chamam de OAUTH em esteroides. De fato, com o OpenID Connect, você pode não só autorizar, mas também autenticar usando interfaces bem conhecidas do framework OAUTH.

Demonstração complexa do OpenID Connect

Aproveitaremos grande parte do código do cliente da parte 1. Isso nos poupa muito trabalho, para que possamos nos concentrar na configuração do ambiente.

Pré-requisitos

Desta vez, precisamos adicionar uma infraestrutura PKI ao servidor web já existente com SSL habilitado. Precisamos de criptografia exigida pelo OpenID Connect. Se você quer autenticar alguém, precisa ter certeza absoluta de que ninguém mais pode se passar pelo agente (cliente, servidor de autenticação...) que envia os dados confidenciais pela rede. É aqui que entra a criptografia baseada em X.509.

Observação: a partir do Cache 2017.1, não é mais necessário usar certificados X.509 para gerar JWT/JWKS (JSON Web Key Set). Devido à compatibilidade com versões anteriores e simplicidade, usamos essa opção.

PKI

A rigor, não precisamos usar a infraestrutura Caché PKI, mas é mais conveniente do que usar ferramentas como openssl diretamente para gerar todos os certificados.

Não entraremos em detalhes sobre a geração de certificados aqui, pois você pode encontrá-los na documentação do InterSystems IRIS. Como resultado da geração de certificados, criaremos 3 pares de chaves públicas/privadas e certificados associados.

Vamos chamá-los de

·        root_ca (root_ca.cer) para nossa autoridade de certificação emissora

·        auth (auth.cer e auth.key) para o servidor de autorização e OpenID

·        client (client.cer e client.key) para o servidor de aplicação do cliente

 

Credenciais X.509

Precisamos definir credenciais X.509 em servidores individuais para que eles possam assinar e validar JSON Web Tokens (JWT) trocados durante nossa demonstração

Configuração do servidor de autenticação e autorização

Sem entrar em detalhes sobre como definir credenciais X.509, apenas mostramos uma captura de tela das credenciais da instância AUTHSERVER.

Como a imagem indica, o AUTHSERVER possui a própria chave privada e o certificado, enquanto só possui o certificado com a chave pública de CLIENT

Configuração do servidor cliente

Da mesma forma, as credenciais definidas na instância CLIENT

Aqui o CLIENTE possui a chave privada e o certificado, mas somente o certificado com chave pública de AUTHSERVER.

Configuração do servidor de recursos

Não precisamos definir credenciais X509 na instância RESSERVER em nossa configuração de exemplo.

Configuração do OAUTH

Como a configuração descrita na parte 1 desta série, precisamos configurar nossos servidores para OAUTH. Vamos começar com a instância AUTHSERVER, pois é o componente central na configuração geral do OAUTH.

AUTHSERVER

No Portal de Gerenciamento de Sistemas, acesse System Administration (Administração do Sistema) > Security (Segurança) > OAuth 2.0 > Server Configuration (Configuração do Servidor).

Clique no link do menu e preencha os itens do formulário:

·        nome do host

·        porta (opcional)

·        prefixo (opcional) – esses três campos compõem o Issuer endpoint (endpoint do emissor)

·        especifique as condições para retornar o token de atualização

·        verifique os tipos de concessão compatíveis, para nossa demonstração basta verificar todos os quatro tipos. No entanto, apenas o código de autorização é usado.

·        opcionalmente, confira o público-alvo necessário – isso adiciona a propriedade aud no código de autorização e solicitações implícitas

·        opcionalmente, confira a sessão de usuário de suporte - isso significa que o cookie httpOnly é usado pelo servidor de autorização para manter o usuário atual deste navegador conectado.  A segunda solicitação e as solicitações subsequentes do token de acesso não pedirão o nome de usuário e a senha.

·        especifique os intervalos de endpoint

·        defina os escopos compatíveis com esse servidor

·        aceite o padrão ou insira valores de opções de personalização – observação: altere o valor da classe de token Generate de %OAuth2.Server.Generate para %OAuth2.Server.JWT para que um JWT seja usado como token de acesso em vez de um token opaco.

·        forneça o nome da configuração SSL registrada para estabelecer o SSL sobre o HTTP conforme exigido pelo OAuth 2.0

·        Preencha as configurações do JSON Web Token (JWT) 

Veja esta captura de tela da configuração de exemplo

Após definir a configuração do servidor, precisamos fornecer a configuração do cliente do servidor. Na página com o formulário de configuração do servidor, clique no botão Client Configurations e pressione Create New Configuration for your CLIENT and RESSERVER instances (Criar nova configuração para as instâncias CLIENT e RESSERVER).

Esta imagem mostra a configuração do CLIENT.

Deixe a guia JWT Token vazia — com valores padrão. Como você pode ver, nós preenchemos os campos com dados sem sentido, diferente de um caso de aplicação real.

Da mesma forma, a configuração do RESSERVER

Como você pode ver, há apenas informações muito básicas necessárias para o servidor de recursos, ou seja, você precisa definir o tipo de cliente para o servidor de recursos. Com CLIENT, você precisa fornecer mais informações, o tipo de cliente (confidencial, pois nosso cliente é executado como um web app capaz de manter o cliente em segredo no servidor, e não enviar para o agente cliente).

CLIENT

No SMP, acesse System Administration (Administração do Sistema) > Security (Segurança) > OAuth 2.0 > Client Configurations (Configurações do cliente).

Clique no botão Create Server Configuration (Criar configuração de servidor), preencha o formulário e salve.

Confira se o endpoint do emissor corresponde ao valor que definimos anteriormente na instância AUTHSERVER! Você também precisa modificar os endpoints do servidor de autorização de acordo com a configuração do seu servidor web. No nosso caso, apenas incorporamos 'authserver' em cada campo de entrada.

Agora, clique no link Client Configurations (Configurações do cliente) ao lado do Issuer Endpoint (Endpoint emissor) recém-criado e clique no botão Create Client Configuration (Criar configuração de cliente).

Ótimo! Agora, temos CLIENT e AUTHSERVER configurados. Isso pode ser suficiente para muitos casos de uso, pois o servidor de recursos pode ser apenas um namespace de AUTHSERVER, já protegido. Porém, vamos considerar que queremos cobrir um caso de uso em que um médico externo está tentando recuperar dados do nosso sistema clínico interno. Portanto, para permitir que esse médico recupere os dados, queremos armazenar as informações da conta dele DENTRO do nosso servidor de recursos para auditorias e fins forenses. Nesse caso, precisamos continuar e definir as configurações em RESSERVER.

RESSERVER

No SMP, acesse System Administration (Administração do Sistema) > Security (Segurança) > OAuth 2.0 > Client Configurations (Configurações do cliente).

Clique no botão Create Server Configuration (Criar configuração de servidor), preencha o formulário e salve.

Usamos a função de descoberta, um novo recurso implementado no Cache 2017.1

Como você pode ver, essa configuração está usando os mesmos dados que a configuração correspondente na instância CLIENT.

Agora, clique no link Client Configurations (Configurações do cliente) ao lado do Issuer Endpoint (Endpoint emissor) recém-criado e clique no botão Create Client Configuration (Criar configuração de cliente).

A criação do WT a partir das credenciais X.509 não é recomendada, mas nós as usamos para a compatibilidade.

Isso! Foi um processo tedioso, mas necessário. Agora, podemos avançar e começar a programar!

Aplicativo cliente

Para manter as coisas o mais simples possível, reciclaremos grande parte do código do nosso exemplo do Google que descrevemos na parte 1.

O aplicativo cliente tem apenas duas páginas de CSP, sendo executado no aplicativo /csp/myclient, sem segurança aplicada – ele é apenas executado como usuário não autenticado.

Página 1

Class Web.OAUTH2.Cache1N Extends %CSP.Page
{

Parameter OAUTH2CLIENTREDIRECTURI = "https://dk-gs2016/client/csp/myclient/Web.OAUTH2.Cache2N.cls";

Parameter OAUTH2APPNAME = "demo client";

ClassMethod OnPage() As %Status
{
  &html<<html>

<body>
  <h1>Authenticating and Authorizing against Cache&acute; OAuth2 provider</h1>
  <p>This page demo shows how to call Cache&acute; API functions using OAuth2 authorization.
  <p>We are going to call Cache&acute; authentication and authorization server to grant our application access to data stored at another
  Cache&acute; server.
 >

  // Get the url for authorization endpoint with appropriate redirect and scopes.
  // The returned url is used in the button below.

  // DK: use 'dankut' account to authenticate!
  set scope="openid profile scope1 scope2"
  set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
    ..#OAUTH2APPNAME,
    scope,
    ..#OAUTH2CLIENTREDIRECTURI,
    .properties,
    .isAuthorized,
    .sc)
  if $$$ISERR(sc) {
    write "GetAuthorizationCodeEndpoint Error="
    write ..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!
  } 

  &html<
  <div class="portalLogoBox"><a class="portalLogo" href="#(url)#">Authorize for <b>ISC</b></a></div>
  </body></html>>
  Quit $$$OK
}

ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
  #dim %response as %CSP.Response
  set scope="openid profile scope1 scope2"
  if ##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,scope,.accessToken,.idtoken,.responseProperties,.error) {
    set %response.ServerSideRedirect="Web.OAUTH2.Cache2N.cls"
  }
  quit 1
}

}

Página 2

Class Web.OAUTH2.Cache2N Extends %CSP.Page
{

Parameter OAUTH2APPNAME = "demo client";

Parameter OAUTH2ROOT = "https://dk-gs2016/resserver";

Parameter SSLCONFIG = "SSL4CLIENT";

ClassMethod OnPage() As %Status
{
    &html<<html>




<body>>
    
    // Check if we have an access token from oauth2 server
    set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,"scope1 scope2",.accessToken,.idtoken,.responseProperties,.error)
    
    // Continue with further checks if an access token exists.
    // Below are all possible tests and may not be needed in all cases.
    // The JSON object which is returned for each test is just displayed.
    if isAuthorized {
        write "<h3>Authorized!</h3>",!
        
        
        // Validate and get the details from the access token, if it is a JWT.
        set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(..#OAUTH2APPNAME,accessToken,"scope1 scope2",,.jsonObject,.securityParameters,.sc)
        if $$$ISOK(sc) {
            if valid {
                write "Valid JWT"_"<br>",!    
            } else {
                write "Invalid JWT"_"<br>",!    
            }
            write "Access token="
            do jsonObject.%ToJSON()
            write "<br>",!
        } else {
            write "JWT Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<br>",!

        // Call the introspection endpoint and display result -- see RFC 7662.
        set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection(..#OAUTH2APPNAME,accessToken,.jsonObject)
        if $$$ISOK(sc) {
            write "Introspection="
            do jsonObject.%ToJSON()
            write "<br>",!
        } else {
            write "Introspection Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<br>",!
        
        if idtoken'="" {
            // Validate and display the IDToken -- see OpenID Connect Core specification.
            set valid=##class(%SYS.OAuth2.Validation).ValidateIDToken(
                ..#OAUTH2APPNAME,
                idtoken,
                accessToken,,,
                .jsonObject,
                .securityParameters,
                .sc)
            if $$$ISOK(sc) {
                if valid {
                    write "Valid IDToken"_"<br>",!    
                } else {
                    write "Invalid IDToken"_"<br>",!    
                }
                write "IDToken="
                do jsonObject.%ToJSON()
                write "<br>",!
            } else {
                write "IDToken Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
            }
        } else {
            write "No IDToken returned"_"<br>",!
        }
        write "<br>",!
    
        // not needed for the application logic, but provides information about user that we can pass to Delegated authentication
    
        // Call the userinfo endpoint and display the result -- see OpenID Connect Core specification.
        set sc=##class(%SYS.OAuth2.AccessToken).GetUserinfo(
            ..#OAUTH2APPNAME,
            accessToken,,
            .jsonObject)
        if $$$ISOK(sc) {
            write "Userinfo="
            do jsonObject.%ToJSON()
            write "<br>",!
        } else {
            write "Userinfo Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<p>",!

        /***************************************************
        *                                                  *
        *   Call the resource server and display result.   *
        *                                                  *
        ***************************************************/
                
        // option 1 - resource server - by definition - trusts data coming from authorization server,
        //     so it serves data to whoever is asking
        //  as long as access token passed to resource server is valid
        
        // option 2 - alternatively, you can use delegated authentication (OpenID Connect) 
        //  and call into another CSP application (with delegated authentication protection)
        //  - that's what we do here in this demo
        
        
        write "<4>Call resource server (delegated auth)","</h4>",!
        set httpRequest=##class(%Net.HttpRequest).%New()
        // AddAccessToken adds the current access token to the request.
        set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
            httpRequest,,
            ..#SSLCONFIG,
            ..#OAUTH2APPNAME)
        if $$$ISOK(sc) {
            set sc=httpRequest.Get(..#OAUTH2ROOT_"/csp/portfolio/oauth2test.demoResource.cls")
        }
        if $$$ISOK(sc) {
            set body=httpRequest.HttpResponse.Data
            if $isobject(body) {
                do body.Rewind()
                set body=body.Read()
            }
            write body,"<br>",!
        }
        if $$$ISERR(sc) {
            write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<br>",!
    
        write "<h4>Call resource server - no auth, just token validity check","</h4>",!
        set httpRequest=##class(%Net.HttpRequest).%New()
        // AddAccessToken adds the current access token to the request.
        set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
            httpRequest,,
            ..#SSLCONFIG,
            ..#OAUTH2APPNAME)
        if $$$ISOK(sc) {
            set sc=httpRequest.Get(..#OAUTH2ROOT_"/csp/portfolio2/oauth2test.demoResource.cls")
        }
        if $$$ISOK(sc) {
            set body=httpRequest.HttpResponse.Data
            if $isobject(body) {
                do body.Rewind()
                set body=body.Read()
            }
            write body,"<br>",!
        }
        if $$$ISERR(sc) {
            write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<br>",!
    } else {
        write "Not Authorized!<p>",!
        write "<a href='Web.OAUTH2.Cache1N.cls'>Authorize me</a>"
    }    
    &html<</body></html>>
    Quit $$$OK
}

}

As seguintes capturas de tela retratam o processamento:

Página de login do servidor de autenticação do OpenID Connect/autorização na instância AUTHSERVER

Página de consentimento do usuário em AUTHSERVER

E, por fim, a página resultante

Como você pode ver, lendo o código, realmente quase não há diferença em relação ao código do cliente que mostramos na parte 1. Há algo novo na página 2. São algumas informações de depuração e a verificação da validade do JWT. Depois de validar o JWT retornado, podemos realizar a introspeção dos dados do AUTHSERVER sobre a identidade do usuário. Simplesmente apresentamos essas informações nos resultados da página, mas podemos fazer mais com elas. Como no caso de uso de um médico externo mencionado acima, podemos usar as informações de identidade e transmiti-las ao servidor de recursos para fins de autenticação, se necessário. Ou apenas passar essa informação como um parâmetro para a chamada da API ao servidor de recursos.

Os próximos parágrafos descrevem como usamos as informações de identidade do usuário em mais detalhes.

Aplicativo de recurso

O servidor de recursos pode ser o mesmo servidor que o servidor de autorização/autenticação e, muitas vezes, esse é o caso. No entanto, na nossa demonstração, criamos para os dois servidores instâncias do InterSystems IRIS separadas.

Então, temos dois casos possíveis, como trabalhar com o contexto de segurança no servidor de recursos.

Alternativa 1 — sem autenticação

Esse é o caso simples. O servidor de autorização/autenticação são apenas a mesma instância do Caché. Nesse caso, podemos simplesmente transmitir o token de acesso a um aplicativo CSP, que é criado especialmente para um único propósito — enviar dados a aplicativos clientes que usam o OAUTH para autorizar a solicitação de dados.

A configuração do aplicativo CSP de recurso (chamamos de /csp/portfolio2) pode parecer com a captura de tela abaixo.

Colocamos o mínimo de segurança na definição do aplicativo, permitindo que apenas a página CSP específica seja executada.

Como opção, o servidor de recursos pode fornecer uma API REST em vez de páginas da Web clássicas. Em situações reais, o refinamento do contexto de segurança depende do usuário.

Um exemplo de código-fonte:

Class oauth2test.demoResource Extends %CSP.Page
{

ClassMethod OnPage() As %Status
{
    set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)
    if $$$ISOK(sc) {
        set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection("RESSERVER resource",accessToken,.jsonObject)
        if $$$ISOK(sc) {        
            // optionally validate against fields in jsonObject

            w "<p><h3>Hello from Cach&eacute; server: <i>/csp/portfolio2</i> application!</h3>"
            w "<p>running code as <b>$username = "_$username_"</b> with following <b>$roles = "_$roles_"</b> at node <b>"_$p($zu(86),"*",2)_"</b>."
        }
    } else {
        w "<h3>NOT AUTHORIZED!</h3>"    
        w "<pre>"
        w
        i $d(%objlasterror) d $system.OBJ.DisplayError()
        w "</pre>"
    }
    Quit $$$OK
}

}

Alternativa 2 — autenticação delegada

Esse é outro caso extrema, queremos usar a identidade do usuário no servidor de recursos o máximo possível, como se o usuário estivesse trabalhando com o mesmo contexto de segurança que os usuários internos do servidor de recursos.

Uma das opções possíveis é usar a autenticação delegada.

Para essa definição funcionar, precisamos concluir mais algumas etapas para configurar o servidor de recursos.

·        Ativar a autenticação delegada

·        Fornecer a rotina ZAUTHENTICATE

·        Configurar o Web application (no nosso caso, chamamos em /csp/portfolio)

A implementação da rotina ZAUTHENTICATE é bastante simples e direta, já que confiamos no AUTHSERVER que forneceu a identidade do usuário e o escopo (perfil de segurança) dele, então basta aceitar o nome de usuário e transmitir com o escopo ao banco de dados de usuários do servidor de recursos (com a tradução necessária entre o escopo do OAUTH e as funções do InterSystems IRIS). É isso. O resto é realizado perfeitamente pelo InterSystems IRIS.

Veja o exemplo de uma rotina ZAUTHENTICATE

#include %occErrors
#include %occInclude

ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC
{
    set tRes=$SYSTEM.Status.OK()
    try {        
        set Properties("FullName")="OAuth account "_Username
        //set Properties("Roles")=Credentials("scope")
        set Properties("Username")=Username
        //set Properties("Password")=Password
        // temporary hack as currently we can't pass Credentials array from GetCredentials() method
        set Properties("Password")="xxx"    // we don't really care about oauth2 account password
        set Properties("Roles")=Password
    } catch (ex) {
        set tRes=$SYSTEM.Status.Error($$$AccessDenied)
    }
    quit tRes
}

GetCredentials(ServiceName,Namespace,Username,Password,Credentials) Public 
{
    s ts=$zts
    set tRes=$SYSTEM.Status.Error($$$AccessDenied)        

     try {
         If ServiceName="%Service_CSP" {
            set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)
            if $$$ISOK(sc) {
                set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection("RESSERVER resource",accessToken,.jsonObject)
                if $$$ISOK(sc) {
                    // todo: watch out for potential collision between standard account and delegated (openid) one!
                    set Username=jsonObject.username
                    set Credentials("scope")=$p(jsonObject.scope,"openid profile ",2)
                    set Credentials("namespace")=Namespace
                    // temporary hack
                    //set Password="xxx"
                    set Password=$tr(Credentials("scope")," ",",")
                    set tRes=$SYSTEM.Status.OK()
                } else {
                    set tRes=$SYSTEM.Status.Error($$$GetCredentialsFailed) 
                }
            }    
        } else {
            set tRes=$SYSTEM.Status.Error($$$AccessDenied)        
        }
     } catch (ex) {
         set tRes=$SYSTEM.Status.Error($$$GetCredentialsFailed)
    }
    Quit tRes
}

A própria página CSP pode ser bastante simples:

Class oauth2test.demoResource Extends %CSP.Page
{

ClassMethod OnPage() As %Status
{
    // access token authentication is performed by means of Delegated authentication!
    // no need to do it, again, here

    // This is a dummy resource server which just gets the access token from the request and
    // uses the introspection endpoint to ensure that the access token is valid.
    // Normally the response would not be security related, but would contain some interesting
    // data based on the request parameters.
    w "<p><h3>Hello from Cach&eacute; server: <i>/csp/portfolio</i> application!</h3>"
    w "<p>running code as <b>$username = "_$username_"</b> with following <b>$roles = "_$roles_"</b> at node <b>"_$p($zu(86),"*",2)_"</b>."
    Quit $$$OK
}

}

Por fim, a configuração do Web application para /csp/portfolio

Se você estiver muito paranoico, pode definir Classes permitidas como fizemos na primeira variante. Ou, novamente, use a API REST. No entanto, isso está muito além do escopo do nosso tema.

Na próxima vez, vamos explicar classes individuais, apresentadas pelo framework OAUTH do InterSystems IRIS. Descreveremos as APIs e quando/onde chamá-las.

 

[1] Quando mencionamos o OAUTH, queremos dizer o OAuth 2.0 conforme especificado no RFC 6749 - https://tools.ietf.org/html/rfc6749. Usamos a abreviação OAUTH apenas por simplicidade.

[2] O OpenID Connect é mantido pela OpenID Foundation – http://openid.net/connect

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