Article
· Jan 29 12m read

Creating custom login pages with %CSP.Login

The %CSP.Login class is the utility class provided by InterSystems IRIS to do custom login pages. If you want to control your IRIS application authentication UI, you must extend %CSP.Login and override some methods according to your needs. This article is going to detail those methods and what you can do with them. In addition to that, you will get an explanation of the delegated authentication mechanism provided by ZAUTHENTICATE.mac routine. Ultimately, you will be able to create customized authentication logic, including the ability to validate existing users in other non-IRIS data repositories and different authentication rules.

Sample application for this article

To illustrate and to learn about %CSP.Login and ZAUTHENTICATE.mac, install the sample application by taking the following steps:

1. If you want to install with ZPM:

zpm:USER>install custom-login


2. If you want to install with Docker

Clone/git pull the repo into any local directory:

git clone https://github.com/yurimarx/custom-login.git

Open the terminal in that directory and run:

docker-compose up -d --build


How to develop a Login Page from %CSP.Login

To develop a Login page, follow the next steps:

1. Create a class extending %CSP.Login:

Class dc.Sample.CustomLogin Extends %CSP.Login
{
}

2. Override the class method OnLoginPage and write ObjectScript and HTML code (between &html<>). It is necessary to render your custom Login UI (you can copy OnLoginPage implementation from %CSP.Login and change it according to your requirements):

Include (%sqlui, %sySystem, %products)

Class dc.Sample.CustomLogin Extends %CSP.Login
{

ClassMethod OnLoginPage() As %Status
{
    // text strings
    Set ConfigName = $PIECE($ZUTIL(86),"*",2)
    // get key, lookup in localization global
    Set tLang = $$$SessionLanguage
    Set tTitle = $$FormatText^%occMessages($$$GetSysMessage(tLang,..#DOMAIN,"logintitle","Login %1"),ConfigName)
    Set tPrompt = $$$GetSysMessage(tLang,..#DOMAIN,"loginenter","Please login")
    Set tUserName = $$$GetSysMessage(tLang,..#DOMAIN,"loginusername","User Name")
    Set tPassword = $$$GetSysMessage(tLang,..#DOMAIN,"loginpassword","Password")
    Set tLogin = $$$GetSysMessage(tLang,..#DOMAIN,"login","LOGIN")
    Set OtherAutheEnabled = $$OtherAuthEnabled^%SYS.cspServer(%request)
    &html<<html>>
    Do ..DrawHEAD(tTitle)

    &html<
        <body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" onload="pageLoad();">
        <div id="content">>
    Do ..DrawTitle(tTitle)

    &html<
    <div style="background-color:#FBFBFB;">
    <table border="0" cellpadding="10" align="center" class="LayoutTable">
    <tr>
    <td align="center">
    <table border="0" width="100%" cellpadding="5" cellspacing="0">>
    &html<<tr><td style="height:90px;"><br/></td></tr>>
    &html<<tr><td><center>>
  If OtherAutheEnabled = 1 {
    // Show standard login form
    &html<
   
    <form name="Login" method="post" action="#($ZCONVERT($GET(%request.Data("Error:FullURL",1)),"O","HTML"))#" autocomplete="off">>
    Write ..InsertHiddenFields($ZCONVERT($GET(%request.Data("Error:URL",1)),"O","HTML"))

    &html<
    <h1>Custom Login</h1>
    <table class="login" border="0" cellspacing="10" cellpadding="10" >
    <tr valign="bottom">
    <td nowrap class="loginCaption">#(tUserName)#</td>
    <td><input type="text" size="30" name="IRISUsername" autocomplete="off" value="#($ZCONVERT($GET(%request.Data("IRISUsername",1)),"O","HTML"))#"/>
    </td>
    </tr>
    <tr valign="bottom">
    <td nowrap class="loginCaption">#(tPassword)#</td>
    <td><input type="password" size="30" name="IRISPassword" autocomplete="off"/>
    </td>
    </tr>
    <tr><td>&nbsp;</td>
     <td style="text-align:right"><input type="submit" name="IRISLogin" class="button" value="#(tLogin)#" style="width:120px;"></td>
    </tr>
    </table>
    </form></center></div>>
  }  // End OtherAutheEnabled = 1 block
  Else {
      // This is accessed when IRIS is installed with minimum security and user clicked Logout.
      Set msg1 = $$$GetSysMessage(tLang,..#DOMAIN,"loginclickhere","Please click here to log in.")
      Set tLink = ..Link("/csp/sys/UtilHome.csp")
    &html<
    <a href="#(tLink)#" class="loginlink">#(msg1)#</a>
    </center>
    </td>
    </tr>
    </table>
    >
  }

    // test for error
    Set tMsg = $GET(%request.Data("Error:ErrorCode",1))
    If ((tMsg'="")&&($SYSTEM.Status.GetErrorCodes(tMsg)'[$$$ERRORCODE($$$RequireAuthentication))) {
        &html<<tr><td><center>>
        Do ShowError^%apiCSP(tMsg)
        &html<</center></td></tr>>
    }

    &html<</td></tr><tr><td style="height:180px;"><br/></td></tr></table></div></div></body></html>>
    Quit $$$OK
}

}

3. Pay attention to some points related to the above-mentioned implementation:

a. To write HTML code use &html<HTML TAGS HERE>
b. Utilize $$OtherAuthEnabled^%SYS.cspServer to test if preliminary authentication is required.
c. Use Do ..DrawHEAD(tTitle) to write the HTML HEAD of the HTML page.
d. Utilize Do ..DrawTitle(tTitle) to write the title section of the HTML page.
e. Use Write ..InsertHiddenFields to write any hidden HTML fields required by your authentication logic on the backend.
f. tTitle is a variable for writing the HTML Title of the HTML page.
g. I composed the HTML code <h1>Custom Login</h1> to show you a custom HTML code for the UI login page.

4. You can override the DrawHEAD if you wish to insert JavaScript and CSS code needed to run on your page.
5. Set the Web Gateway user (CSPSystem) with permission to consult the database for the location of the custom login page:

a. Assign the desired database resource to the appropriate role, and then give that role to the CSPSystem user.
b. In this sample, the custom login is needed for the csp/sys application, so setting the %DB_IRISSYS role to the user CSPSystem is crucial because csp/sys employs the %SYS database.
c. If the login page were created for an application installed in the USER namespace, then the assigned role would be %DB_USER.
d. In my case, I configured the appropriate permission in the script that runs along with my iris.script file on this sample application:

Do ##class(Security.Users).AddRoles("CSPSystem", "%DB_IRISSYS")


6. Set your login class to the application required to use this new login page:

a. In my example, I picked a class from the passwordless project (https://openexchange.intersystems.com/package/passwordless) called SetupUtil. It applies the binding when calling the Apply class method. I also customized the method code to point to my login class:

Class dc.Sample.SetupUtil
{

Parameter GN = "^%ZAPM.AppsDelegate";
/// write ##class(dc.login.SetupUtil).Apply("/csp/sys")
ClassMethod Apply(app = "/csp/sys", user = "", pass = "", mode = "0")
{
    Quit:app="" $$$OK
    New $NAMESPACE
    If $NAMESPACE'="%SYS" {
        Write !,""
        If $$EXIST^%R("ZAUTHENTICATE.mac","%SYS") {
            Set msg="Routine ZAUTHENTICATE.mac in %SYS already installed"
            Write !,msg
            If mode=1 Quit $$$OK
            If mode=2 Quit $$$ERROR($$$GeneralError,msg)
        }
        Set tempFile = ##class(%File).TempFilename("xml")
        Set list("ZAUTHENTICATE.MAC")=""
        Set list("dc.Sample.CustomLogin.CLS")=""
        Set list("dc.Sample.SetupUtil.CLS")=""
        Set st=$SYSTEM.OBJ.Export(.list, tempFile)
        Set $NAMESPACE="%SYS"
        Set st = $SYSTEM.OBJ.Load(tempFile, "c")
        If 'st {
            Set msg=$SYSTEM.Status.GetErrorText(st) Write !,msg
            Quit $$$ERROR($$$GeneralError,msg)
        }
    }
    Set $NAMESPACE="%SYS"
    Set:app="" app= $SYSTEM.Util.GetEnviron("ISC_app")
    Set:app="" app="/csp/sys"
    // Edit Security Authentication/Web Session Options
    Set ss=##class(Security.System).%OpenId("SYSTEM")
    If '$ZBOOLEAN(ss.AutheEnabled, $$$AutheDelegated, 1) {
        Set ss.AutheEnabled = ss.AutheEnabled + $$$AutheDelegated
    }
    Set st=ss.%Save()
    If 'st Quit st
   
    If app="*" {
        Set result=##CLASS(%ResultSet).%New("%DynamicQuery:SQL")
        Set tSC=result.Prepare("select Name FROM Security.Applications")
        Set:tSC tSC=result.Execute()
        If '$$$ISOK(tSC) {
            Set text="Application setup error :"_$SYSTEM.Status.GetErrorText(tSC)  
            Write !,text
            Quit $$$ERROR(text)
        }
        Else {
            While result.Next() {
                Set CSP=result.Data("Name")
                Set csp=$ZCONVERT(CSP,"L")
                Set st=..AddDelegate(csp,user,pass)
            }
        Write !,"OK"
        }  
    }
    ElseIf app["," {
        Set a=""
        For i=1:1:$LENGTH(app,",") { Set a=$PIECE(app,",",i)
            Continue:a=""
            Set st=..AddDelegate(a,$PIECE(user,",",i),$PIECE(pass,",",i))
        }
    }
    Else {
        Set st=..AddDelegate(app,user,pass)
    }
    Quit $$$OK
}

ClassMethod AddDelegate(app, user = "", pass = "")
{
    Set st=##class(Security.Applications).Get(app,.par)
    If st {
        If par("AutheEnabled")=$$$AutheUnauthenticated Quit $$$OK
        // Remove unauthenticated and add delegated
        If $ZBOOLEAN(par("AutheEnabled"), $$$AutheUnauthenticated, 1) {
            Set par("AutheEnabled") = par("AutheEnabled") - $$$AutheUnauthenticated
        }
        If '$ZBOOLEAN(par("AutheEnabled"), $$$AutheDelegated, 1) {
            Set par("AutheEnabled") = par("AutheEnabled") + $$$AutheDelegated
        }
        Set par("LoginPage") = "dc.Sample.CustomLogin.cls"
        Set st=##class(Security.Applications).Modify(app,.par)
        If st {
            Set acc=""
            If user'="" Set acc=$LISTBUILD(user,pass)
            If $EXTRACT(app,*)'="/" Set app=app_"/"
            Set @..#GN@(app)=acc
        }
    }
    Quit st
}

}


b. I utilized the IPM (or ZPM) invoked functionality to execute the Apply method (on the file module.xml) right after I started IRIS on my Docker instance:

<Invokes>
        <Invoke Class="dc.Sample.SetupUtil" Method="Apply">
          <Arg>/csp/sys</Arg>
          <Arg>SuperUser</Arg>
          <Arg>SYS</Arg>
        </Invoke>
</Invokes>


c. If you want to do it manually (set login page for an application), follow the next instructions:

i. Go to System Administration > Security > Applications > Web Applications.

ii. Select the csp/sys application and set the Login Page field with your Login class (in my case, it is dc.Sample.CustomLogin).

7. Ready! Now, you have a simple custom login page for the Management Portal application!
8. For more details, check out the documentation page below:

https://docs.intersystems.com/iris20233/csp/docbook/Doc.View.cls?KEY=ASCWL.

How to develop a custom (delegated) authentication code

Quite often, in addition to the custom login page, it is necessary to implement custom logic to authenticate the user. If this is your case, you must provide the custom code in the AUTHENTICATE.mac file. In InterSystems IRIS, this authentication model is known as Delegated Authentication. It means that InterSystems IRIS entrusts you with writing and applying queries and rules required to validate and accept the user as a real user. The custom logic here will provide personal information and access permissions (authorization) that the user must have.
To set delegated authentication, proceed with the following instructions:
1. Go to System Administration > Security > System Security > Authentication/Web Session Options:

2. Check the option Allow Delegated authentication:


3. Create a mac folder in the src folder.
4. Create a file ZAUTHENTICATE.mac in the mac folder.
5. Write the next logic:

ROUTINE ZAUTHENTICATE
ZAUTHENTICATE(ServiceName,Namespace,Username,Password,Credentials,Properties) PUBLIC {

#include %occErrors
#include %sySecurity
 Set $ZTRAP="Error"
 Quit $SYSTEM.Status.OK()

Error //Handle any COS errors here
  //Reset error trap to avoid infinite loop
  Set $ZTRAP=""
  //Return the generalized COS error message #5002
  Quit $SYSTEM.Status.Error(5002 /*$$$CacheError*/,$ZERROR)
}

GetCredentials(ServiceName,Namespace,Username,Password,Credentials) Public {

    // For console sessions, authenticate as _SYSTEM.
    If ServiceName="%Service_Console" {  
        Quit $SYSTEM.Status.Error($$$GetCredentialsFailed)
    }

    // For bindings connections, use regular prompting.
    If ServiceName="%Service_Bindings" {
        Quit $SYSTEM.Status.Error($$$GetCredentialsFailed)
    }

    Set validUser = 0
    If ServiceName="%Service_WebGateway" {
        set Username=$get(%request.Data("IRISUsername",1))
        set Password=$get(%request.Data("IRISPassword",1))

        if Username '= "" {
            Set validUser = ##class(%SYSTEM.Security).Login(Username, Password)
       
            if validUser = 0 {
                set str=##class(%Stream.FileCharacter).%New()
                do str.LinkToFile("/usr/irissys/lib/csv")
           
                while 'str.AtEnd {
                    set $listbuild(UserFullName,UserNamespace,
                                UserPassword,UserUsername,UserPhone) = $listfromstring(str.ReadLine())
                    if (UserUsername = Username) && (UserPassword = Password)  {
                        If %request.Application="/csp/sys/" {
                            Set validUser = 1
                            Set status = ##class(Security.Users).Create(
                                UserUsername,"%All",UserPassword,
                                UserFullName,UserNamespace,"","",0,1,"",1,0,"",1,1,1)
                            if $$$ISOK(status) {
                                set validUser = 1
                            } else {
                                set validUser = 0
                            }
                                                   }
                    }
                }
            }
        } Else {
            set validUser = 2
        }
    }
   
    if validUser = 1 {
        Quit $SYSTEM.Status.OK()
    } elseif validUser = 2 {
        Quit $SYSTEM.Status.Error($$$GeneralError,"Provide your username and password")
    } else {
        Quit $SYSTEM.Status.Error($$$AccessDenied)
    }

}


a. GetCredentials is the point for writing the custom authentication logic into the ZAUTHENTICATE. This method has the following arguments (extracted from https://docs.intersystems.com/iris20233/csp/docbook/DocBook.UI.Page.cls?...):


i. ServiceName — a string representing the name of the service utilized to connect the user to InterSystems IRIS (for instance, %Service_Console or %Service_WebGateway).
ii. Namespace — a string defining the namespace on the InterSystems IRIS server to which a connection is being established. It is designed to be used with such %Service_Bindings services as Studio or ODBC.
iii. Username — a string describing the name of the account entered by the user that needs to be validated by the routine’s code.
iv. Password — A string depicting the password entered by the user that is to be validated.
v. Credentials — hey are not implemented in this version of InterSystems IRIS and are passed by reference.
vi. Properties — being passed by reference, they represent an array of returned values that define such characteristics of the account named by Username as email.

b. We used $ISOBJECT($GET(%request)) to test if it is a web authentication.
c. $get(%request.Data("IRISUsername",1)) and $get(%request.Data("IRISPassword",1)) get the username and password from the login page.
d. ##class(%SYSTEM.Security).Login(Username, Password) authenticates the existing user. When a user is a new one, it returns validUser = 0.
e. set str=##class(%Stream.FileCharacter).%New() and do str.LinkToFile("/usr/irissys/lib/csv") instructions load the list of superusers mentioned in the file superusers.csv. These users do not exist in IRIS and are created during the first-time login.
f. ##class(Security.Users).Create(...) creates the user provided by the login page if this user exists in superusers.csv file. When the user is already formed, it is set to validUser = 1.
g. If validUser equals 1, the authentication returns success using Quit $SYSTEM.Status.OK(). Otherwise, it returns Quit $SYSTEM.Status.Error(...)
h. The content of the file with superusers is illustrated below:

Learning more about custom login

If you wish to know more about custom login, check the resources below:

  1. https://docs.intersystems.com/iris20233/csp/docbook/Doc.View.cls?KEY=ASCWL
  2. https://docs.intersystems.com/iris20233/csp/docbook/DocBook.UI.Page.cls?KEY=GAUTHN_delegated
  3. https://openexchange.intersystems.com/package/passwordless
  4. https://openexchange.intersystems.com/package/Google-IRIS-Login
Discussion (2)2
Log in or sign up to continue