I was attempting to find a solution to grant clients anonymous access to certain API endpoints while securing others within my REST API. However, when defining a Web Application, you can only secure the entire application and not specific parts of it.
I scoured the community for answers but didn't find any exact solutions, except one recommendation to create two separate web applications, one secured and the other unsecured. However, in my opinion, this approach involves too much work and creates unnecessary maintenance overhead. I prefer to develop my APIs spec-first and decide within the specification which endpoints should allow anonymous access and which should not.
In this article, I provide two examples: one for Basic Auth and the other for JWT, which is used in OAuth 2.0 context. If you notice any flaws in these examples, please let me know, and I will make the necessary fixes accordingly.
Prerequisites
First, define a Web Application for your REST API. Configure it for unauthenticated access and specify the required privileges for the application. Specify only the roles and resources necessary for the successful use of the API.
Create a class, for example REST.Utils
where you will implement the helper classmethods that verify the credentials.
Class REST.Utils
{
}
Basic Auth
If you want to secure a endpoint using Basic Auth, use the following method to check if the username/password provided in the HTTP Authorization header has the correct privileges to access the restricted API endpoint.
/// Check if the user has the required permissions.
/// - auth: The Authorization header.
/// - resource: The resource to check permissions for.
/// - permissions: The permissions to check.
///
/// Example:
/// > Do ##class(REST.Utils).CheckBasicCredentials(%request.GetCgiEnv("HTTP_AUTHORIZATION", ""), "RESOURCE", "U")
///
/// Return: %Status. The status of the check.
ClassMethod CheckBasicCredentials(auth As %String, resource As %String, permissions As %String) As %Status
{
/// Sanity check the input
if (auth = "") {
Return $$$ERROR($$$GeneralError, "No Authorization header provided")
}
/// Check if the auth header starts with Basic
if ($FIND(auth, "Basic") > 0) {
/// Strip the "Basic" part from the Authorization header and remove trailing and leading spaces.
set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
}
Set tStatus = $$$OK
/// Decode the base64 encoded username and password
Set auth = $SYSTEM.Encryption.Base64Decode(auth)
Set username = $PIECE(auth, ":", 1)
Set password = $PIECE(auth, ":", 2)
/// Attempt to log in as the user provided in the Authorization header
Set tStatus = $SYSTEM.Security.Login(username, password)
if $$$ISERR(tStatus) {
Return tStatus
}
/// Check if the user has the required permissions
Set tStatus = $SYSTEM.Security.CheckUserPermission($USERNAME, resource, permissions)
/// Return the status. If the user has the required permissions, the status will be $$$OK
Return tStatus
}
In the endpoint you want to secure, call the CheckBasicCredentials
-method and check the return value. A return value of 0
indicates a failed check. In these cases, we return an HTTP 401
back to the client.
The example below checks that the user has SYSTEM_API
resource defined with USE
privileges. If it does not, return HTTP 401
to the client. Remember that the API user has to have %Service_Login:USE
privilege to be able to use the Security.Login
method.
Example
Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
Set tStatus = ##class(REST.Utils).CheckBasicCredentials(authHeader, "SYSTEM_API", "USE")
if ($$$ISERR(tStatus)) {
Set %response.Status = 401
Return
}
... rest of the code
JWT
Instead of using Basic Auth to secure an endpoint, I prefer to use OAuth 2.0 JWT Access Tokens, as they are more secure and provides a more flexible way to define privileges via scopes. The following method checks if the JWT access token provided in the HTTP Authorization header has the correct privileges to access the restricted API endpoint.
/// Check if the supplied JWT is valid.
/// - auth: The Authorization header.
/// - scopes: The scopes that this JWT token should have.
/// - oauthClient: The OAuth client that is used to validate the JWT token. (optional)
/// - jwks: The JWKS used for token signature validation (optional)
///
/// Example:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
///
/// Return: %Status. The status of the check.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
Set tStatus = $$$OK
/// Sanity check the input
if (token = "") {
Return $$$ERROR($$$GeneralError, "No token provided")
}
/// Check if the auth header starts with Bearer. Cleanup the token if yes.
if ($FIND(token, "Bearer") > 0) {
/// Strip the "Bearer" part from the Authorization header and remove trailing and leading spaces.
set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
}
/// Build a list from the string of scopes
Set scopes = $LISTFROMSTRING(scopes, ",")
Set scopeList = ##class(%ListOfDataTypes).%New()
Do scopeList.InsertList(scopes)
/// Strip whitespaces from each scope
For i=1:1:scopeList.Count() {
Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
}
/// Decode the token
Try {
Do ..JWTToObject(token, .payload, .header)
} Catch ex {
Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
}
/// Get the epoch time of now
Set now = $ZDATETIME($h,-2)
/// Check if the token has expired
if (payload.exp < now) {
Return $$$ERROR($$$GeneralError, "Token has expired")
}
Set scopesFound = 0
/// Check if the token has the required scopes
for i=1:1:scopeList.Count() {
Set scope = scopeList.GetAt(i)
Set scopeIter = payload.scope.%GetIterator()
While scopeIter.%GetNext(.key, .jwtScope) {
if (scope = jwtScope) {
Set scopesFound = scopesFound + 1
}
}
}
if (scopesFound < scopeList.Count()) {
Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
}
/// If the token is valid scope-wise and it hasn't expired, check if the signature is valid
if (oauthClient '= "") {
/// If we have specified a OAuth client, use that to validate the token signature
Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
if ($$$ISERR(tStatus) || result '= 1) {
Return $$$ERROR($$$GeneralError, "Token failed signature validation")
}
} elseif (jwks '= "") {
/// If we have specified a JWKS, use that to validate the token signature
Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
if ($$$ISERR(tStatus)) {
Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
}
}
Return tStatus
}
/// Decode a JWT token.
/// - token: The JWT token to decode.
/// - payload: The payload of the JWT token. (Output)
/// - header: The header of the JWT token. (Output)
///
/// Example:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
///
/// Return: %Status. The status of the check.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")
/// Decode and parse Header
Set header = $SYSTEM.Encryption.Base64Decode(header)
Set header = {}.%FromJSON(header)
/// Decode and parse Payload
Set payload = $SYSTEM.Encryption.Base64Decode(payload)
Set payload = {}.%FromJSON(payload)
Return $$$OK
}
Again, in the endpoint you want to secure, call the CheckJWTCredentials
-method and check the return value. A return value of 0
indicates a failed check. In these cases, we return an HTTP 401
back to the client.
The example below checks if the token has the scopes scope1
and scope2
defined. If it lacks the required scopes, has expired, or fails signature validation, it returns an HTTP 401
status code to the client.
Example
Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
Set tStatus = ##class(REST.Utils).CheckJWTCredentials(authHeader, "scope1,scope2")
if ($$$ISERR(tStatus)) {
Set %response.Status = 401
Return
}
... rest of the code
Conclusion
Here is the full code for the REST.Utils
class. If you have any suggestions on how to improve the code, please let me know. I will update the article accordingly.
One obvious improvement would be to check the JWT signature to make sure it is valid. To be able to do that, you need to have the public key of the issuer.
Class REST.Utils
{
/// Check if the user has the required permissions.
/// - auth: The Authorization header contents.
/// - resource: The resource to check permissions for.
/// - permissions: The permissions to check.
///
/// Example:
/// > Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckBasicCredentials(authHeader, "RESOURCE", "U"))
///
/// Return: %Status. The status of the check.
ClassMethod CheckBasicCredentials(authHeader As %String, resource As %String, permissions As %String) As %Status
{
Set auth = authHeader
/// Sanity check the input
if (auth = "") {
Return $$$ERROR($$$GeneralError, "No Authorization header provided")
}
/// Check if the auth header starts with Basic
if ($FIND(auth, "Basic") > 0) {
// Strip the "Basic" part from the Authorization header and remove trailing and leading spaces.
set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
}
Set tStatus = $$$OK
Try {
/// Decode the base64 encoded username and password
Set auth = $SYSTEM.Encryption.Base64Decode(auth)
Set username = $PIECE(auth,":",1)
Set password = $PIECE(auth,":",2)
} Catch {
Return $$$ERROR($$$GeneralError, "Not a valid Basic Authorization header")
}
/// Attempt to login as the user provided in the Authorization header
Set tStatus = $SYSTEM.Security.Login(username,password)
if $$$ISERR(tStatus) {
Return tStatus
}
/// Check if the user has the required permissions
Set tStatus = $SYSTEM.Security.CheckUserPermission($USERNAME, resource, permissions)
/// Return the status. If the user has the required permissions, the status will be $$$OK
Return tStatus
}
/// Check if the supplied JWT is valid.
/// - auth: The Authorization header.
/// - scopes: The scopes that this JWT token should have.
/// - oauthClient: The OAuth client that is used to validate the JWT token. (optional)
/// - jwks: The JWKS used for token signature validation (optional)
///
/// Example:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
///
/// Return: %Status. The status of the check.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
Set tStatus = $$$OK
/// Sanity check the input
if (token = "") {
Return $$$ERROR($$$GeneralError, "No token provided")
}
/// Check if the auth header starts with Bearer. Cleanup the token if yes.
if ($FIND(token, "Bearer") > 0) {
/// Strip the "Bearer" part from the Authorization header and remove trailing and leading spaces.
set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
}
/// Build a list from the string of scopes
Set scopes = $LISTFROMSTRING(scopes, ",")
Set scopeList = ##class(%ListOfDataTypes).%New()
Do scopeList.InsertList(scopes)
/// Strip whitespaces from each scope
For i=1:1:scopeList.Count() {
Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
}
/// Decode the token
Try {
Do ..JWTToObject(token, .payload, .header)
} Catch ex {
Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
}
/// Get the epoch time of now
Set now = $ZDATETIME($h,-2)
/// Check if the token has expired
if (payload.exp < now) {
Return $$$ERROR($$$GeneralError, "Token has expired")
}
Set scopesFound = 0
/// Check if the token has the required scopes
for i=1:1:scopeList.Count() {
Set scope = scopeList.GetAt(i)
Set scopeIter = payload.scope.%GetIterator()
While scopeIter.%GetNext(.key, .jwtScope) {
if (scope = jwtScope) {
Set scopesFound = scopesFound + 1
}
}
}
if (scopesFound < scopeList.Count()) {
Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
}
/// If the token is valid scope-wise and it hasn't expired, check if the signature is valid
if (oauthClient '= "") {
/// If we have specified a OAuth client, use that to validate the token signature
Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
if ($$$ISERR(tStatus) || result '= 1) {
Return $$$ERROR($$$GeneralError, "Token failed signature validation")
}
} elseif (jwks '= "") {
/// If we have specified a JWKS, use that to validate the token signature
Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
if ($$$ISERR(tStatus)) {
Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
}
}
Return tStatus
}
/// Decode a JWT token.
/// - token: The JWT token to decode.
/// - payload: The payload of the JWT token. (Output)
/// - header: The header of the JWT token. (Output)
///
/// Example:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
///
/// Return: %Status. The status of the check.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")
/// Decode and parse Header
Set header = $SYSTEM.Encryption.Base64Decode(header)
Set header = {}.%FromJSON(header)
/// Decode and parse Payload
Set payload = $SYSTEM.Encryption.Base64Decode(payload)
Set payload = {}.%FromJSON(payload)
Return $$$OK
}
}
Hello @Kari Vatjus-Anttila
This was a nice implementation for securing the individual REST calls. However, Will loose the benefits of built JWT Authentication for web application(from 2022 version). Maintaining two separate web applications for secure and unsecured seems useful.
Thanks for sharing your thoughts. When you have lots of REST APIs to deal with, it's a real hassle to split them into secure and non-secure apps because it makes maintenance complicated. Plus, the way JWT authentication works in the web app seems pretty basic and doesn't handle scopes properly, as per the docs.
In cases where you're using an external OAuth 2.0 token provider, not IRIS, and you want an easy way to check those tokens in your API, this solution still makes sense.
However, it's important to note that JWT signatures should be validated if they are signed. You can achieve this in a couple of ways: by using an OAuth2 Client defined in IRIS, which contains data about the token server and by using the
%SYS.OAuth2.Validation
class'sValidateJWT
method to validate the token. Alternatively, you can utilize a predefinedJWKS_URI
parameter inside the REST implementation class to facilitate the validation process.I'll take a look at the JWT validation using these two methods and update the post accordingly.
Thanks,
Kari
There are two thoughts I have on this example:
Your utility methods are fine. However I would implement them using Delegated Authentication. This feature of IRIS allows you to provide your own code to do authentication. The difference is that it executes as part of the normal authentication process. The connection has not yet gained access to the environment and will not gain access if authentication is not passed. Any failure or attempted breach of the code causes an "Access Denied" message to be returned and the connection terminated. It is also possible to use this in combination with other authentication methods so this only needs to be used on REST services where it is needed. This would also remove the need to add any special permissions like %All to the Web Application definition. Here is the documentation for this.
Delegated Authentication
@Rich Taylor
You've raised some excellent points, and it's certainly something I need to consider for the future. Thank you for bringing up Delegated Authentication. I'll make the necessary modifications to the article to emphasize that Web Applications should not be granted %All permissions but rather only the permissions that are essential for the API usage.
I was actually pondering this question myself for an article I'm working on. I ended up in a very different place than you did, though. I created an Abstract class that extends %CSP.REST and overrides the XData schema, the DispatchMap method, and the DispatchRequest method. If I extend this class - which I've called REST.Resourceful - I can include a resource in the URL map as a resource:permission pair. For example:
Will only allow you to access the /securetest endpoint if the user has Use permission on the resource "MyResource". If you don't, you get a 401 error. If I leave out the Resource attribute on that node, it doesn't check for any additional resources.
Source code is below. If you search the text for my initials, DLH, you'll see comments where I've made changes in the method, plus I added the attribute "Resource" to the XData schema.
Class REST.Resourceful Extends %CSP.REST [ Abstract ]
{ XData Schema [ Internal ]
{
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" > <xs:element name="Routes">
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="Route">
<xs:complexType>
<xs:attribute name="Url" type="string" use="required"/>
<xs:attribute name="Method" type="string" use="required"/>
<xs:attribute name="Call" type="call" use="required"/>
<xs:attribute name="Cors" type="xs:boolean" use="optional" default="false"/>
<xs:attribute name="Resource" type="string" use="optional" default=" "/>
</xs:complexType>
</xs:element>
<xs:element name="Map">
<xs:complexType>
<xs:attribute name="Prefix" type="string" use="required"/>
<xs:attribute name="Forward" type="forward" use="required"/>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element> <xs:simpleType name="call">
<xs:restriction base="xs:string">
<xs:pattern value="([%]?[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*:)?[%]?[a-zA-Z][a-zA-Z0-9]*"/>
</xs:restriction>
</xs:simpleType> <xs:simpleType name="forward">
<xs:restriction base="xs:string">
<xs:pattern value="([%]?[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*)"/>
</xs:restriction>
</xs:simpleType> <xs:simpleType name="string">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
</xs:restriction>
</xs:simpleType> </xs:schema>
} /// This generator creates the DispatchMap Method used to dispatch the Url and Method to the associated target method
ClassMethod DispatchMap(pIndex As %Integer) As %String [ CodeMode = generator ]
{
#dim tSC As %Status = $$$OK
#dim e As %Exception.AbstractException
#dim tStream As %Stream.TmpCharacter
#dim tHandler As %XML.ImportHandler
#dim tCompiledClass As %Dictionary.CompiledClass
#dim tArgCount,tIndex,tI,tCounter As %Integer
#dim tArgs,tChild,tClassName,tCall,tCors,tForward,tError,tMap,tMethod,tPattern,tPiece,tPrefix,tType,tUrl,tResource As %String
Try {
Set tClassName=%classname
#; Don't run on base class
If tClassName="%CSP.REST" Quit
#; Find named XDATA block
If ##class(%Dictionary.CompiledXData).%ExistsId(tClassName_"||UrlMap") {
Set tCompiledClass=##class(%Dictionary.CompiledClass).%OpenId(tClassName,,.tSC)
If '$IsObject(tCompiledClass)||$$$ISERR(tSC) Quit
Set tIndex = tCompiledClass.XDatas.FindObjectId(tClassName_"||UrlMap")
If tIndex="" Set tSC=$$$ERROR($$$XDataBlockMissing,tClassName,"UrlMap") Quit
#; Get XDATA as stream
Set tStream = tCompiledClass.XDatas.GetAt(tIndex).Data
Do tStream.Rewind()
#; Create an XML import handler ( use the internal handler )
Set tHandler=##class(%XML.ImportHandler).%New("CacheTemp",$$$IntHandler)
#; Create the Entity Resolver
Set tResolver=##class(%XML.SAX.XDataEntityResolver).%New(tClassName)
#; Parse the XML data in the specfied stream
Set tSC=##Class(%XML.SAX.Parser).ParseStream(tStream,tHandler,tResolver,,,"Schema")
If $$$ISERR(tSC) Quit
#; Copy tree because handler will delete its copy when it goes out of scope
Merge tMap=@tHandler.DOMName@(tHandler.Tree)
If $Data(tMap("error"))||$Data(tMap("warning")) {
Set tSC=$$$ERROR($$$InvalidDispatchMap)
For tType="error","warning" {
Set tIndex = "" For {
Set tIndex=$Order(tMap(tType,tIndex),1,tError) If tIndex="" Quit
Set tSC=$$$ADDSC(tSC,$$$ERROR($$$GeneralError,tError))
}
}
Quit
}
#; Walk the xml and generate the routing map
Set tChild="",tCounter=0 For {
Set tChild=$Order(tMap(1,"c",tChild)) If tChild="" Quit
If tMap(tChild)="Route" {
#; Need to substitute capture groups for arguments
#; Added setting of tResource based on URL map - DLH
Set tPattern="",tArgCount=0,tUrl=tMap(tChild,"a","Url"),tCors=tMap(tChild,"a","Cors"),tResource=tMap(tChild,"a","Resource")
#; Substitute variable placeholders for capture group
For tI=1:1:$Length(tUrl,"/") {
Set tPiece=$Piece(tUrl,"/",tI)
If $Extract(tPiece)=":" {
Set $Piece(tPattern,"/",tI)="([^"_$Char(0)_"]+)"
} else {
Set $Piece(tPattern,"/",tI)=tPiece }
}
Set tPattern=$Translate(tPattern,$Char(0),"/") Set tCounter=$Increment(tCounter),tMethod=tMap(tChild,"a","Method"),tCall=$Get(tMap(tChild,"a","Call"))
#; Added getting resource from the URL Map here. - DLH
$$$GENERATE(" If pIndex="_tCounter_" Quit $ListBuild(""R"","""_tPattern_""","""_tMethod_""","""_tCall_""","""_tCors_""","""_tResource_""")")
} else {
Set tCounter=$Increment(tCounter),tPrefix=tMap(tChild,"a","Prefix"),tForward=$Get(tMap(tChild,"a","Forward")) #; Need to substitute capture groups for arguments
Set tPattern=""
For tI=2:1:$Length(tPrefix,"/") {
Set tPiece=$Piece(tPrefix,"/",tI)
If $Extract(tPiece)=":" {
Set tPattern=tPattern_"/[^/]+"
} else {
Set tPattern=tPattern_"/"_tPiece
}
}
Set tPattern = "("_ tPattern _ ")/.*"
$$$GENERATE(" If pIndex="_tCounter_" Quit $ListBuild(""M"","""_tPattern_""","""_tForward_""")")
}
}
$$$GENERATE(" Quit """"")
} else {
#; The specified class must have an XDATA Block named UrlMap
Set tSC=$$$ERROR($$$XDataBlockMissing,tClassName,"UrlMap")
}
} Catch (e) {
Set tSC=e.AsStatus()
}
Quit tSC
} /// Dispatch a REST request according to URL and Method
ClassMethod DispatchRequest(pUrl As %String, pMethod As %String, pForwarded As %Boolean = 0) As %Status
{
#dim tSC As %Status = $$$OK
#dim e As %Exception.AbstractException
#dim tMatcher As %Regex.Matcher
#dim tArgs,tClass,tMatchUrl,tMapEntry,tRegEx,tCall,tForward,tAccess,tSupportedVerbs,tTarget,tType As %String
#dim tI,tIndex As %Integer
#dim tResourceMatched,tContinue As %Boolean
#dim tMethodMatched As %Boolean
Try {
Set (tResourceMatched,tMethodMatched)=0
#; Initializing tSecurityResourceMatched - DLH
Set tSecurityResourceMatched = 1
#; Extract the match url from the application name
If (0=pForwarded) {
Set tMatchUrl="/"_$Extract(pUrl,$Length(%request.Application)+1,*)
} else {
Set tMatchUrl=pUrl
}
#; Uppercase the method
Set pMethod=$ZCVT(pMethod,"U")
#; Pre-Dispatch
Set tContinue=1,tSC=..OnPreDispatch(tMatchUrl,pMethod,.tContinue)
If $$$ISERR(tSC) Quit
#; It's the users responsibility to return the response in OnPreDispatch() if Continue = 0
If tContinue=0 Quit
#; Walk the dispatch map in collation order of defintion
For tIndex=1:1 {
#; Get the next map entry
Set tMapEntry=..DispatchMap(tIndex) If tMapEntry="" Quit
#; Pick out the RegEx
Set tRegEx=$List(tMapEntry,2)
#; Create a matcher
Set tMatcher=##class(%Regex.Matcher).%New(tRegEx)
#; Test each regular expression in turn, extracting the arguments,
#; dispatching to the named method
If tMatcher.Match(tMatchUrl) {
#; We have matched the resource
Set tResourceMatched=1
#; Logic to check the resource from the URL map
set tResource = $List(tMapEntry,6)
If tResource '= " "{
If $SYSTEM.Security.Check($P(tResource,":",1),$P(tResource,":",2))=0{
Set tSecurityResourceMatched=0
}
}
#; Added an if so the method only gets dispatched if we have the resource permission
If tSecurityResourceMatched = 1{
Set tType=$List(tMapEntry,1)
#; If we are a simple route
If tType="R" {
#; Support OPTIONS VERB (cannot be overriden)
If pMethod="OPTIONS" {
Set tMethodMatched=1
Set tSC=..OnHandleOptionsRequest(tMatchUrl)
If $$$ISERR(tSC) Quit
#; Dispatch CORS
Set tSC=..ProcessCorsRequest(pUrl,$list(tMapEntry,5))
If $$$ISERR(tSC) Quit
Quit
}
#; comparison is case-insensitive now
If pMethod'=$ZCVT($List(tMapEntry,3),"U") Continue
Set tTarget=$List(tMapEntry,4)
#; We have matched a method
Set tMethodMatched=1
#; Dispatch CORS
Set tSC=..ProcessCorsRequest(pUrl,$list(tMapEntry,5))
If $$$ISERR(tSC) Quit
#; Got a match, marshall the arguments can call directly
If tMatcher.GroupCount {
For tI=1:1:tMatcher.GroupCount Set tArgs(tI)=tMatcher.Group(tI)
Set tArgs=tI
} else {
Set tArgs=0
}
#; Check for optional ClassName prefix
Set tClass=$classname()
If tTarget[":" Set tClass=$Piece(tTarget,":"),tTarget=$Piece(tTarget,":",2)
#; Dispatch
Set tSC=$zobjclassmethod(tClass,tTarget,tArgs...)
} else {
#; We are a map, massage the URL and forward the request
Set tMatchUrl=$piece(tMatchUrl,tMatcher.Group(1),"2",*),tForward=$ListGet(tMapEntry,3)
Set (tResourceMatched,tMethodMatched)=1
#; Dispatch with modified URL
Set tSC=$zobjclassmethod(tForward,"DispatchRequest",tMatchUrl,pMethod,1)
}
If $$$ISERR(tSC) Quit
#; Don't want multiple matches
Quit
}
}
}
#; Didn't have permission for the resource required by this enpoint; return 401 - DLH
If tSecurityResourceMatched = 0 Set tSC=..ReportHttpStatusCode(..#HTTP401UNAUTHORIZED) Quit
#; Didn't have a match for the resource, report not found
If tResourceMatched=0 Set tSC=..ReportHttpStatusCode(..#HTTP404NOTFOUND) Quit
#; Had a match for resource but method not matched
If tMethodMatched=0 {
Set tSC=..SupportedVerbs(tMatchUrl,.tSupportedVerbs)
If $$$ISERR(tSC) Quit
Set tSC=..Http405(tSupportedVerbs) Quit
}
} Catch (e) {
Set tSC=e.AsStatus()
}
Quit tSC
} }
An interesting approach, but it involves custom tinkering with internal classes. Ultimately, it comes down to personal preference, but I tend to favor building on top of verified and tested implementations, reserving the extension of InterSystems base classes as a last resort.
In my approach, there's no need to modify any class implementations. Instead, you can simply use a utility method for validation. As previously discussed, these methods should be invoked as part of the normal authentication process, not within the API implementation, as Rich Taylor mentioned.
If I understand your solution correctly, the client sends a request, and the logic checks whether the application has the correct privileges. Does it also verify whether the provided credentials are accurate? In my method, I verify Basic Auth credentials via
$SYSTEM.Security.Login()
and then use$SYSTEM.Security.Check()
to confirm whether the user in question possesses the required privileges.I'll need to explore Delegated Authentication in IRIS and adjust the article accordingly.
I didn't check the credentials because this all takes place after the user has already been through the authentication process. If they aren't valid, they wouldn't get this far anyway. My approach is dealing with just the authorization, not the authentication. I still use $SYSTEM.Security.Check to see if the process has the right permissions.
As you said, it comes down to personal preference. One of my preferences is not messing with the authentication processes if I don't have to. That way I don't have to account for all of the different authentication options, and they could all still be used.