検索

Announcement
· Jun 7

Videos for InterSystems Developers, May 2025 Recap

Hello and welcome to the May 2025 Developer Community YouTube Recap.
InterSystems Global Summit
"Code to Care" videos
How Project BEST Transforms FDA Adverse Event Reporting with FHIR
By Don Woodlock, Head of Global Healthcare Solutions, InterSystems
Streamlining Electronic Prior Authorizations with FHIR
By Don Woodlock, Head of Global Healthcare Solutions, InterSystems
More from InterSystems developers
Prompt the frontend UI for InterSystems IRIS with Lovable
By Evgeny Shvarov, Senior Manager of Developer and Startup Programs, InterSystems
SMART on FHIR: Introduction & FHIR Server Setup
By Tani Frankel, Sales Engineer Manager, InterSystems
SMART on FHIR: OAuthServer
By Tani Frankel, Sales Engineer Manager, InterSystems
SMART on FHIR: FHIR Server - OAuth Config
By Tani Frankel, Sales Engineer Manager, InterSystems
Discussion (0)1
Log in or sign up to continue
Article
· Jun 7 6m read

Persistencia de sesión Oauth con token OpenID en cookie

¿Conoces a Google? Seguro que si 😄 a menudo hacemos login en webs con nuestra cuenta de Gmail por la comodidad de simplemente hacer click! sin tener que escribir email ni contraseña, esto es posible porque nuestro navegador guarda un token de acceso que nos identifica y, en este caso Google, comparte un acceso para poder consultar información de nosotros como el correo electrónico.

🔐 Existen unas pautas o proceso para hacer esta identificación de forma segura, lo que se conoce como Oauth.

En este artículo no voy a explicar cómo funciona Oauth, te mostraré cómo hacer para persistir la sesión en el Oauth de IRIS sin tener que introducir usuario y contraseña cada vez que entras y de paso cómo saltar la pantalla de aceptación de permisos.

Aquí tienes una imagen marcando el flujo que vamos a crear.

¡¡Vamos al lío!!

Como primer paso puedes montar todo un sistema de Oauth en IRIS con el proyecto https://github.com/intersystems-ib/workshop-iris-oauth2, en el archivo Readme tienes los pasos para hacerlo funcionar con Docker.

Abre desde VS code el área de trabajo del proyecto.

 

 


Crea una clase que extienda de %OAuth2.Server.Authenticate, en nuestro caso la hemos llamado cysnet.oauth.server.Authenticate.

 

 

Configura Oauth para usar la clase personalizada.


Aquí la joya de la corona, crea dos métodos en la clase personalizada.

ClassMethod LoginFromCookie(authorizationCode As %String) As %Status [ Internal, ServerOnly = 1 ]
{
    #dim sc As %Status = $$$OK
    Set currentNS = $NAMESPACE
    Try {
        // Get cookie with jwt access token
        Set cookieToken = %request.GetCookie("access_token")
        If cookieToken '= "" {
            ZNspace "%SYS"
            // Get valid access token from cookie
            Set accessToken = ##class(OAuth2.Server.AccessToken).OpenByToken(cookieToken,.sc)
            If $$$ISOK(sc) && $ISOBJECT(accessToken) {
                // Get current access token
                Set currentToken = ##class(OAuth2.Server.AccessToken).OpenByCode(authorizationCode,.sc)
                If $$$ISOK(sc) && $ISOBJECT(currentToken) {
                    // Get oauth client
                    Set client = ##class(OAuth2.Server.Client).Open(currentToken.ClientId,.sc)
                    If $$$ISOK(sc) && $ISOBJECT(client) {
                        // Skip login page
                        Set currentToken.Username = accessToken.Username
                        #dim propertiesNew As %OAuth2.Server.Properties = currentToken.Properties
                        Set key=""
                        For {
                            Set value=accessToken.Properties.ResponseProperties.GetNext(.key)
                            If key="" Quit
                            Do ..SetTokenProperty(.propertiesNew,key,value)
                        }
                        Do ##class(OAuth2.Server.Auth).AddClaimValues(currentToken, currentToken.ClientId, accessToken.Username)
                        Set currentToken.Properties = propertiesNew
                        Set currentToken.Stage = "permission"
                        Do currentToken.Save()
                        // Skip permissions page
                        Set url = %request.URL_"?AuthorizationCode="_authorizationCode_"&Accept=Aceptar"
                        Set %response.Redirect = url
                    } Else {
                        Set sc = $$$ERROR($$$GeneralError, "Error getting oauth client")
                    }
                } Else {
                    Set sc = $$$ERROR($$$GeneralError, "Error getting current token")
                }
            } Else {
                Set sc = $$$ERROR($$$GeneralError, "Error getting cookie token")
                // Clear cookie access_token
                Set %response.Headers("Set-Cookie") = "access_token=; Path=/; Max-Age=0"
            }
            ZNspace currentNS
        } Else {
            Set sc = $$$ERROR($$$GeneralError, "Error cookie access_token missing")
        }
        
    } Catch ex {
        ZNspace currentNS
        If $$$ISOK(sc) {
            Set sc = ex.AsStatus()
        }
    }
    
    Quit sc
}

ClassMethod SetTokenProperty(Output properties As %OAuth2.Server.Properties, Name, Value, Type = "string") [ Internal, ServerOnly = 1 ]
{
    // Add claims and more
    Set tClaim = ##class(%OAuth2.Server.Claim).%New()
    Do properties.ResponseProperties.SetAt(Value,Name)
    Do properties.IntrospectionClaims.SetAt(tClaim,Name)
    Do properties.UserinfoClaims.SetAt(tClaim,Name)
    Do properties.JWTClaims.SetAt(tClaim,Name)
    Do properties.IDTokenClaims.SetAt(tClaim,Name)
    Do properties.SetClaimValue(Name,Value,Type)
    Quit $$$OK
}

 

Sobrescribe el método DisplayLogin

ClassMethod DisplayLogin(authorizationCode As %String, scope As %ArrayOfDataTypes, properties As %OAuth2.Server.Properties, loginCount As %Integer = 1) As %Status
{
	Set sc = ..LoginFromCookie(authorizationCode)
	If $$$ISOK(sc) {
		Quit sc
	} Else {
		$$$LOGERROR($system.Status.GetErrorText(sc))
	}

	Quit ..DisplayLogin(authorizationCode, scope, properties, loginCount)
}

 

Veamos paso por paso que hace el método LoginFromCookie, supongamos que ya hemos hecho login con anterioridad y tenemos el token JWT de OpenID en una cookie llamada access_token.

  1. Obtiene la cookie access_token.
    Set cookieToken = %request.GetCookie("access_token")
  2. Cambia al namespace %SYS para usar los métodos de las librerías de Oauth.
    ZNspace "%SYS"
  3. Obtiene el token con la cookie, este método elimina los tokens expirados, nos vale para saber que el token es válido y no ha sido modificado.
    Set accessToken = ##class(OAuth2.Server.AccessToken).OpenByToken(cookieToken,.sc)
  4. Obtiene el token recién creado para el cliente de Oauth que solicita el login de usuario.
    Set currentToken = ##class(OAuth2.Server.AccessToken).OpenByCode(authorizationCode,.sc)
  5. Obtiene el cliente de Oauth.
    Set client = ##class(OAuth2.Server.Client).Open(currentToken.ClientId,.sc)
  6. Replica el usuario y las propiedades en el token nuevo.
    Set currentToken.Username = accessToken.Username
    #dim propertiesNew As %OAuth2.Server.Properties = currentToken.Properties
    Set key=""
    For {
    	Set value=accessToken.Properties.ResponseProperties.GetNext(.key)
    	If key="" Quit
    	Do ..SetTokenProperty(.propertiesNew,key,value)
    }
    Do ##class(OAuth2.Server.Auth).AddClaimValues(currentToken, currentToken.ClientId, accessToken.Username)
    Set currentToken.Properties = propertiesNew
  7. Salta el login y guarda los cambios del token.
    Set currentToken.Stage = "permission"
    Do currentToken.Save()
      En este punto podríamos llamar al método DisplayPermissions si queremos mostrar la pantalla de aceptación de permisos.  
  8. Saltar pantalla de permisos y enviar el código de autorización al callback del cliente de oauth
    Set url = %request.URL_"?AuthorizationCode="_authorizationCode_"&Accept=Aceptar"
    Set %response.Redirect = url

 

Otros apuntes

Nos ha pasado en otros entornos con versiones anteriores a la actual de IRIS que la redirección no funciona, una posible solución a esto es devolver un javascript que lo haga:

&html<<script type="text/javascript">
    window.location.href="#(url)#";
</script>>

 

Respecto a la pantalla de permisos lo ideal sería almacenar en una persistencia: Cliente de OAuth, Usuario y Scopes aceptados, y no mostrar la pantalla si los permisos fueron aceptados con anterioridad.

Este es mi primer post publicado, espero haberlo hecho de manera clara y que sea de utilidad para llevar el Oauth de IRIS al siguiente nivel.

Si has llegado hasta aquí agradecerte de corazón el tiempo de leer este post y si tienes alguna duda puedes escribirla en un comentario.

No olvides darle like 👍

Un saludo, Miguelio, Cysnet.

Discussion (0)1
Log in or sign up to continue
Question
· Jun 6

Web Gateway/Apache configuration to make SOAP Client Requests?

I am having issues trying to send SOAP requests to a Cloud Based AWS Application that lives outside of our network. 

It is using a Basic Authentication, Key, Certificate Authority and Whitelist for Security. 

If I attempt the connection using wget from the command line I am able to connect,

:>wget --bind-address=10.95.129.245 --server-response https://xxxxxxxxxx/xxxxxxx/services/Mirth
--2025-06-06 15:54:51--  https://xxxxxxx/xxxxxxxx/services/Mirth
wget: /ensemble/.netrc:16: unknown token xxxxxxx
wget: /ensemble/.netrc:16: unknown token xxxxxxxx
Resolving xxxxxxx.com (xxxxxxx)... 34.233.89.102, 54.165.234.62
Connecting to hcis-staging.cbord.com (xxxxxxxx)|xxxxxxx|:443... connected.
HTTP request sent, awaiting response...
  HTTP/1.1 200 OK
  Cache-control: no-cache="set-cookie"
  Content-Type: text/html; charset=utf-8
  Date: Fri, 06 Jun 2025 19:54:51 GMT
  Server: nginx
  Set-Cookie: AWSELB=D507B3110AAE4C39FED576EDFCA8C486670B04F18A5E3BBF0AE38321B36B528F14A78096B862E1C523ADEB028C4EB54BB3C1A6750FC29A6832764251160DDA704F73127995;PATH=/
  Set-Cookie: AWSELBCORS=D507B3110AAE4C39FED576EDFCA8C486670B04F18A5E3BBF0AE38321B36B528F14A78096B862E1C523ADEB028C4EB54BB3C1A6750FC29A6832764251160DDA704F73127995;PATH=/;SECURE;SAMESITE=None
  Content-Length: 754
  Connection: keep-alive
Length: 754 [text/html]
Saving to: 'Mirth'
 

 

 

however when I attempt from a Business Operation I am getting....

06/06/2025 15:51:49.8039637 *********************
Output from Web client with SOAP action = http://xxxxxxxx/msg/SendMessage
<?xml version="1.0" encoding="UTF-8" ?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:s='http://www.w3.org/2001/XMLSchema'>
  <SOAP-ENV:Body><SendMessage xmlns="http://xxxxxxxxx/msg/"><key xsi:type="s:string">xxxxxxxxxxxxxxx</key><encodedMessage xsi:type="s:string">xxxx@EnsLib.HL7.Message</encodedMessage></SendMessage></SOAP-ENV:Body>
</SOAP-ENV:Envelope>

06/06/2025 15:51:54.8081368 *********************
Input to Web client with SOAP action = http://xxxxxxx/msg/SendMessage

ERROR #6059: Unable to open TCP/IP socket to server xxxxxxx:443
string**** SOAP client return error. method=SendMessage, action=http://xxxxxxxx/msg/SendMessage
     ERROR #6059: Unable to open TCP/IP socket to server xxxxxxxxxx:443

Within the Business Operation class, I was able to define the Local Interface it should use.

Class osuwmc.Nutrition.OSU.CBORD.Operation.CBORDHL7Port Extends Ens.BusinessOperation [ ProcedureBlock ]
{

Parameter ADAPTER = "EnsLib.SOAP.OutboundAdapter";

Property LocalInterface As %String(MAXLEN = 255);

Parameter SETTINGS = "LocalInterface:Connection:selector?context={Ens.ContextSearch/TCPLocalInterfaces}";

Method OnInit() As %Status
{
 IF '$IsObject(..Adapter.%Client.HttpRequest) {
	set ..Adapter.%Client.HttpRequest = ##class(%Net.HttpRequest).%New()
 }
 set ..Adapter.%Client.HttpRequest.LocalInterface = $ZStrip($P(..LocalInterface,"("),"*W")

	quit $$$OK
}

Method SendMessage(pRequest As osuwmc.Nutrition.OSU.CBORD.Request.SendMessageRequest, Output pResponse As osuwmc.Nutrition.OSU.CBORD.Response.SendMessageResponse) As %Library.Status
{
 Set ..Adapter.WebServiceClientClass = "CBORDHL7WSService.CBORDHL7Por.....

Do I have to do something special with the Web Gateway and Apache Web Service to allow the connection out?

3 Comments
Discussion (3)4
Log in or sign up to continue
Question
· Jun 6

Shared code execution speed

Let's suppose two different routines use one and the same chunk of code. From the object-oriented POV, a good decision is to have this chunk of code in a separate class and have both routines call it. However, whenever you call code outside of the routine as opposed to calling code in the same routine, some execution speed is lost. For reports churning through millions of transactions this lost speed might be noticeable. Any advice how to optimize specifically speed?
P.S. Whenever someone is talking about the best choice for whatever, I am always tempted to ask: "What are we optimizing?". Optimizing speed here.
P.P.S. I did notice that some classes speed is very different comparing with old style utilities, while doing largely the same, like exporting.

14 Comments
Discussion (14)3
Log in or sign up to continue
Article
· Jun 6 2m read

Converting Oracle Hierarchical Queries to InterSystems IRIS: Generating Date Ranges

If you're migrating from Oracle to InterSystems IRIS—like many of my customers—you may run into Oracle-specific SQL patterns that need translation.

Take this example:

SELECT (TO_DATE('2023-05-12','YYYY-MM-DD') - LEVEL + 1) AS gap_date
FROM dual
CONNECT BY LEVEL <= (TO_DATE('2023-05-12','YYYY-MM-DD') - TO_DATE('2023-05-02','YYYY-MM-DD') + 1);

In Oracle:

  • LEVEL is a pseudo-column used in hierarchical queries (CONNECT BY). It starts at 1 and increments by 1.
  • CONNECT BY LEVEL <= (...) determines how many rows to generate.
  • The difference between the two dates plus one gives 11, so the query produces 11 rows, counting backwards from May 12, 2023 to May 2, 2023.

Breakdown of the result:

LEVEL = 1  → 2023-05-12
LEVEL = 2  → 2023-05-11
...
LEVEL = 11 → 2023-05-02

Now the question is: How do you achieve this in InterSystems IRIS, which doesn’t support CONNECT BY?

One solution is to implement a SQL-style query using ObjectScript that mimics this behavior. Below is a sample CREATE QUERY definition that accepts a STARTDATE and a number of DAYS, and returns the descending list of dates.


✅ InterSystems IRIS: Implementing a Date Gap Query

CREATE QUERY GET_GAP_DATE(IN STARTDATE DATE, IN DAYS INT)
  RESULTS (GAP_DATE DATE)
  PROCEDURE
  LANGUAGE OBJECTSCRIPT

  Execute(INOUT QHandle BINARY(255), IN STARTDATE DATE, IN DAYS INT)
  {
    SET QHandle("start") = STARTDATE
    SET QHandle("days")  = DAYS
    SET QHandle("level") = 1
    RETURN $$$OK
  }

  Fetch(INOUT QHandle BINARY(255), INOUT Row %List, INOUT AtEnd INT)
  {
    IF (QHandle("level") > QHandle("days")) {
      SET Row = ""
      SET AtEnd = 1
    } ELSE {
      SET Row = $ListBuild(QHandle("start") - QHandle("level") + 1)
      SET QHandle("level") = QHandle("level") + 1
    }
    RETURN $$$OK
  }

  Close(INOUT QHandle BINARY(255))
  {
    KILL QHandle
    QUIT $$$OK
  }

You can run the above CREATE QUERY in IRIS System Management Portal, or through a tool like DBeaver or a Python/Jupyter notebook using JDBC/ODBC.


🧪 Example Usage:

To generate the same result as the Oracle query above, use:

SELECT * FROM GET_GAP_DATE(
  TO_DATE('2023-05-12', 'YYYY-MM-DD'),
  TO_DATE('2023-05-12', 'YYYY-MM-DD') - TO_DATE('2023-05-02', 'YYYY-MM-DD') + 1
);

This will output:

GAP_DATE
----------
2023-05-12
2023-05-11
...
2023-05-02
(11 rows)

🔁 Advanced Usage: Join with Other Tables

You can also use this query as a subquery or in joins:

SELECT * 
FROM GET_GAP_DATE(TO_DATE('2023-05-12', 'YYYY-MM-DD'), 11) 
CROSS JOIN dual;

This allows you to integrate date ranges into larger SQL workflows.


Hope this helps anyone tackling Oracle-to-IRIS migration scenarios! If you’ve built alternative solutions or have improvements, I’d love to hear your thoughts.

1 Comment
Discussion (1)2
Log in or sign up to continue