Skip to content
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #822: The CPF resource processor now supports system expressions and macros in CPF merge files
- #578 Added functionality to record and display IPM history of install, uninstall, load, and update
- #961: Adding creation of a lock file for a module by using the `-create-lockfile` flag on install.

- #973: Enables CORS and JWT configuration for WebApplications in module xml
### Changed
- #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies
- #885: Always synchronously load dependencies and let each module do multi-threading as needed
Expand Down
39 changes: 39 additions & 0 deletions src/cls/IPM/Main.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,7 @@ ClassMethod GenerateModuleXML(ByRef pCommandInfo) As %Status [ Internal ]

do ##class(%Library.Prompt).GetString("Enter module keywords:", .tKeywords)

#dim tTemplate As %IPM.Storage.ModuleTemplate
set tTemplate = ##class(%IPM.Storage.ModuleTemplate).NewTemplate(tPath, tName, tVersion, tDescription, tKeywords)
return:'$isobject(tTemplate)

Expand Down Expand Up @@ -1361,6 +1362,44 @@ ClassMethod GenerateModuleXML(ByRef pCommandInfo) As %Status [ Internal ]
}
do ##class(%Library.Prompt).GetString(" Enter a comma separated list of web applications or * for all:", .tWebAppList)

#if $$$CacheVersionMajor>=2025
//cors
write !,"Cross-Origin Settings:"
do ##class(%Library.Prompt).GetYesNo("Configure Access-Control-Allow-Credentials:",.enableCors)
if enableCors {
do ##class(%Library.Prompt).GetString(" Enter a comma separated list of Allowed Headers:",.allowedHeaders,,32767,"comma seperated values e.g: Access-Control-Allow-Origin,Access-Control-Allow-Headers")
while (1) {
do ##class(%Library.Prompt).GetString(" Enter a comma separated list of Allowed Origins:",.allowedOrigins,,32767,"comma seperated values e.g: http://www.example.com")
if allowedOrigins="" quit
set sc = tTemplate.ValidateCorsOrigin(allowedOrigins)
if $$$ISERR(sc) {
do ..DisplayError(sc)
set allowedOrigins = ""
continue
}
quit
}
set corsConfig("enableCors") = enableCors
set corsConfig("allowedHeaders") = allowedHeaders
set corsConfig("allowedOrigins") = allowedOrigins
do tTemplate.SetCORSProps(tWebAppList,.corsConfig)
}
#endif

#if $$$CacheVersionMajor>=2024
//jwt
write !,"JWT Authentication"
do ##class(%Library.Prompt).GetYesNo("Configure JWT Authentication: ",.enableJWT)
if enableJWT {
do ##class(%Library.Prompt).GetNumber(" Enter JWT Access Token Timeout:",.JWTAccessTokenTO,1)
do ##class(%Library.Prompt).GetNumber(" Enter JWT Refresh Token Timeout:",.JWTRefreshTokenTO,1)
set jwtconfig("enableJWT") = enableJWT
set jwtconfig("JWTAccessTokenTO") = JWTAccessTokenTO
set jwtconfig("JWTRefreshTokenTO") = JWTRefreshTokenTO
do tTemplate.SetJWTProps(tWebAppList,.jwtconfig)
}
#endif

do tTemplate.AddWebApps(tWebAppList,.tCSPapps) // tCSP - list of CSP (not REST apps)
for i=1:1:$listlength(tCSPapps) {
set tCSPPath = ""
Expand Down
81 changes: 78 additions & 3 deletions src/cls/IPM/Storage/ModuleTemplate.cls
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Method AddWebApps(
{
set tAppList = ""
set pApps = $zstrip(pApps,"<>W")
if ( pApps = "*" ) {
if (pApps = "*") {
do ..GetCSPApplications(.tAppList)
} else {
set tAppList = $listfromstring(pApps,",")
Expand Down Expand Up @@ -284,6 +284,24 @@ Method SetSourcePathForCSPApp(
set ..TemplateResources(pCSPApp,"Path") = pPath
}

Method DefineReminWebAppProps(ByRef props)
{
//add cors origin details
set props("CorsAllowlist") = ""
set props("CorsCredentialsAllowed") = 0
// add cors headers
set props("CorsHeadersList") = ""
set props("DeepSeeEnabled") = 0
set props("JWTAuthEnabled") = 0
set props("JWTRefreshTokenTimeout") = 900
set props("JWTAccessTokenTimeout") = 60
set props("WSGIAppLocation") = ""
set props("WSGIAppName") = ""
set props("WSGICallable") = ""
set props("WSGIDebug") = ""
set props("WSGIType") = ""
}

ClassMethod GetGlobalsList(Output globals As %List) As %Status
{
set globals=""
Expand Down Expand Up @@ -352,6 +370,58 @@ Method SetAuthorProps(
return $$$OK
}

Method SetCORSProps(
apps As %String = "",
ByRef cors)
{
set appList = ""
set apps = $zstrip(apps,"<>W")
if (apps = "*") {
$$$ThrowOnError(..GetCSPApplications(.appList))
} else {
set appList = $listfromstring(apps,",")
}
set ptr = 0
while $listnext(appList, ptr, webpApp) {
set ..TemplateResources(webpApp,"CorsCredentialsAllowed") = cors("enableCors")
set ..TemplateResources(webpApp,"CorsHeadersList") = cors("allowedHeaders")
set ..TemplateResources(webpApp,"CorsAllowlist") = cors("allowedOrigins")
}
}

Method SetJWTProps(
apps As %String = "",
ByRef jwt)
{
set appList = ""
set apps = $zstrip(apps,"<>W")
if (apps = "*") {
$$$ThrowOnError(..GetCSPApplications(.appList))
} else {
set appList = $listfromstring(apps,",")
}
set ptr = 0
while $listnext(appList, ptr, webpApp) {
set ..TemplateResources(webpApp,"JWTAccessTokenTimeout") = jwt("JWTAccessTokenTO")
set ..TemplateResources(webpApp,"JWTAuthEnabled") = jwt("allowedHeaders")
set ..TemplateResources(webpApp,"JWTRefreshTokenTimeout") = jwt("JWTRefreshTokenTO")
}
}

Method ValidateCorsOrigin(pAllowedOrigins As %String = "") As %Status
{
new $namespace
set $namespace = "%SYS"
set originsList = $listfromstring(pAllowedOrigins,",")
set ptr = 0
set sc = $$$OK
while $listnext(originsList, ptr, origin){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: missing space between ) and {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed the space issue

set sc = ##class(Security.Applications).ValidateCorsOrigin(origin)
if $$$ISERR(sc) quit
}
return sc
}

ClassMethod NewTemplate(
pPath,
pName,
Expand Down Expand Up @@ -525,6 +595,11 @@ Method SetTemplateProps() As %Status
set ..TemplateResources("rest","UnauthenticatedEnabled") = 0
set ..TemplateResources("rest","Recurse") = 1

//CORS
set ..TemplateResources("rest","CorsAllowlist") = "http://www.example.com"
set ..TemplateResources("rest","CorsCredentialsAllowed") = 1
set ..TemplateResources("rest","CorsHeadersList") = "Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Access-Control-Max-Age, Access-Control-Expose-Headers, Origin, Access-Control-Request-Method, Access-Control-Request-Headers"

// WEB APP
set ..TemplateResources("web") = "/web"
set ..TemplateResources("web","Url") = "/web"
Expand Down Expand Up @@ -600,9 +675,9 @@ XData XSLT
</Export>
</xsl:template>
<xsl:template match="Resource[@Url]">
<CSPApplication>
<WebApplication>
<xsl:apply-templates select="@*[local-name() != 'Name']" />
</CSPApplication>
</WebApplication>
</xsl:template>
<xsl:template match="node()">
<xsl:copy>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/// This class validates that CORS headers and allowed origins are configured correctly,
/// and that JWT authentication is properly set up in the <b>&lt;WebApplication&gt;</b> configuration section.
Class Test.PM.Integration.ResourceProcessor.WebApplication Extends Test.PM.Integration.Base
{

Parameter CommonPathPrefix As STRING = "cors-rest-apps";

Parameter WebAppName As STRING = "/testcors";

/// Test.PM.Integration.Uninstall
Method TestCORSEnabledWebAppViaModule()
{
#define NormalizeDirectory(%path) ##class(%File).NormalizeDirectory(%path)
#define UTRoot ^UnitTestRoot

set testRoot = $$$NormalizeDirectory($get($$$UTRoot))
set moduleDir = $$$NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/"_..#CommonPathPrefix_"/")
set moduleFile = ##class(%File).NormalizeFilename("module.xml",moduleDir)
if ##class(%File).DirectoryExists(moduleFile) {
do $$$AssertStatusOK(1,"module.xml File exist on "_moduleDir)
}
set status = ##class(%IPM.Main).Shell("load "_moduleDir)
do $$$AssertStatusOK(status,"Loaded "_..#CommonPathPrefix_" module successfully from "_moduleDir)
do ..VerifyCORSConfiguration()
do ..VerifyJWTConfiguration()

set status = ##class(%IPM.Main).Shell("uninstall "_..#CommonPathPrefix)
do $$$AssertStatusOK(status,"uninstalled "_..#CommonPathPrefix_" module successfully.")
}

Method VerifyCORSConfiguration()
{
new $namespace
set $namespace = "%SYS"
set status = ##class(Security.Applications).Get(..#WebAppName, .props)
do $$$AssertStatusOK(status,"Web applciation "_..#WebAppName_" created scuccessfully")
if $data(props("CorsAllowlist"),corsAllowlist) {
do $$$AssertStatusOK(1,"CorsAllowlist values are defined")
do $$$LogMessage(corsAllowlist)
}
if $data(props("CorsCredentialsAllowed"),corsAllow) {
do $$$AssertStatusOK(1,"CorsCredentialsAllowed values are defined")
do $$$LogMessage(corsAllow)
}
if $data(props("CorsHeadersList"),corsHeadersList) {
do $$$AssertStatusOK(1,"CorsHeadersList values are defined")
do $$$LogMessage(corsHeadersList)
}
}

Method VerifyJWTConfiguration()
{
new $namespace
set $namespace = "%SYS"
set status = ##class(Security.Applications).Get(..#WebAppName, .props)
do $$$AssertStatusOK(status,"Web applciation "_..#WebAppName_" created scuccessfully")
do $$$LogMessage("Validating JWT configuration")
if $data(props("JWTAccessTokenTimeout"),JWTAccessTokenTimeout) {
do $$$AssertStatusOK(1,"JWTAccessTokenTimeout value is defined")
do $$$LogMessage(JWTAccessTokenTimeout)
}
if $data(props("JWTAuthEnabled"),JWTAuthEnabled) {
do $$$AssertStatusOK(1,"JWTAuthEnabled value is defined")
do $$$LogMessage(JWTAuthEnabled)
}
if $data(props("JWTRefreshTokenTimeout"),JWTRefreshTokenTimeout) {
do $$$AssertStatusOK(1,"JWTRefreshTokenTimeout value is defined")
do $$$LogMessage(JWTRefreshTokenTimeout)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="cors-rest-apps.ZPM">
<Module>
<Name>cors-rest-apps</Name>
<Version>1.0.0</Version>
<Description>cors enabling testing on webapplication</Description>
<Keywords>cors</Keywords>
<Packaging>module</Packaging>
<Resource Name="CorsTest.PKG"/>
<WebApplication
CookiePath="/testcors"
CorsAllowlist="https://www.example.com,https://pm.intersystems.com"
CorsCredentialsAllowed="1"
JWTAccessTokenTimeout="60"
JWTAuthEnabled="1"
JWTRefreshTokenTimeout="900"
CorsHeadersList="Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Access-Control-Max-Age, Access-Control-Expose-Headers, Access-Control-Request-Method, Access-Control-Request-Headers"
PasswordAuthEnabled="1"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For WebApplication, these properties for auth are all encapsulated as part of AutheEnabled and not separate properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@isc-kiyer
Could you please provide an example of how the authentication settings are represented when they are encapsulated under the AuthEnabled attribute in the element?
A sample XML snippet would be very helpful.
Thank you!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably this refers to how AutheEnabled is a bit string with these properties, e.g. AutheEnabled=32 means PasswordAuthEnabled=1
(see https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&CLASSNAME=Security.Applications#PROPERTY_AutheEnabled)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @isc-kiyer , @isc-dchui
Regarding the encapsulation of authentication properties within AutheEnabled. However, based on the class definition for Security.Applications in IRIS 2025.1, I believe these properties must remain discrete attributes within the <WebApplication> tag for the following reasons:

  1. Bitmask vs. Discrete Properties: The AutheEnabled property is a bitmask specifically for legacy authentication mechanisms such as Kerberos (4), Password (32), and Unauthenticated (64). In contrast, JWT is implemented via standalone properties: JWTAuthEnabled, JWTRefreshTokenTimeout, and JWTAccessTokenTimeout. These are not bits within the AutheEnabled integer and cannot be mathematically encapsulated into it.
  2. CORS Configuration: Similarly, CORS settings (such as CorsHeadersList and CorsAllowlist) are distinct string and boolean properties in the Security.Applications class. They do not have a representation within the authentication bitmask.
  3. Automated Handling by IPM: By including these as individual attributes in the <WebApplication> tag, the %IPM.ResourceProcessor.WebApplication class can automatically parse, map, and persist these values during the installation process. This ensures that the module.xml remains the declarative source of truth for the application's security posture.
  4. Consistency: Treating these as separate attributes aligns with how other non-bitmask properties (like DispatchClass or CookiePath) are handled within the IPM schema.
<WebApplication
    Url="/testcors"
    CookiePath="/testcors"
    AutheEnabled="32"
    JWTAuthEnabled="1"
    JWTAccessTokenTimeout="60"
    JWTRefreshTokenTimeout="900"
    CorsCredentialsAllowed="1"
    CorsAllowlist="https://www.example.com"
    CorsHeadersList="Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers"
    PasswordAuthEnabled="1"
    UnauthenticatedEnabled="0"
    Recurse="1"
    UseCookies="2" />

Could you check and suggest.
Thank you!

Recurse="1"
UnauthenticatedEnabled="0"
Url="/testcors"
UseCookies="2"/>
<LifecycleClass>%IPM.Lifecycle.Module</LifecycleClass>
<SourcesRoot>src</SourcesRoot>
</Module>
</Document>
</Export>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Class CorsTest.Rest.Cors Extends %RegisteredObject
{

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
<Route Url="/" Method="GET" Call="GetInfo" Cors="true"/>
</Routes>
}

ClassMethod GetInfo() As %Status
{
write "Hello, World!"
quit $$$OK
}

}