Issue with groovy trigger and mfa-duo

92 views
Skip to first unread message

Otto Myyrä

unread,
Oct 13, 2020, 5:02:48 AM10/13/20
to CAS Community
Hello.

We are having trouble actually triggering the mfa-authentication with a groovy trigger script despite the trigger script running (and logging what it's doing) seemingly just fine.

We authenticate from ldap and also support spnego authentication and then trigger mfa with a groovy trigger if spnego isn't in use. The groovy script runs and does what it's supposed to do and then returns what it's (apparently) supposed to return but the mfa process does not trigger after that regardless.

If we activate mfa globally based on a principal attribute instead of a groovy trigger, then the mfa works as it should. If we try to do it with the groovy script it won't activate. Would any of you have any idea what we're doing wrong?

Here's the mfa configuration in cas.properties:
##
#DUO MFA provider
cas.authn.mfa.duo[0].duoSecretKey=[redacted]
cas.authn.mfa.duo[0].rank=1
cas.authn.mfa.duo[0].duoApplicationKey=[redacted]
cas.authn.mfa.duo[0].duoIntegrationKey=[redacted]
cas.authn.mfa.duo[0].duoApiHost=[redacted]
cas.authn.mfa.duo[0].trustedDeviceEnabled=false
cas.authn.mfa.duo[0].id=mfa-duo
cas.authn.mfa.duo[0].registrationUrl=https://[redacted]
cas.authn.mfa.duo[0].name=Login (CAS)
cas.authn.mfa.duo[0].order=1

cas.authn.mfa.groovyScript=file:/etc/cas/mfaGroovyTrigger.groovy
cas.authn.mfa.provider-selection-enabled=true

#cas.authn.mfa.globalPrincipalAttributeNameTriggers=LGUserType,Company,CostCenter
#cas.authn.mfa.globalPrincipalAttributeValueRegex=23K65.*
#cas.authn.mfa.globalPrincipalAttributeValueRegex=donotmatch

(the commented out lines are the tests with the principal attribute, those work)


This is the groovy trigger script:

import java.util.*

class MFACustomTrigger {
        def String run(final Object... args) {
                def service = args[0]
                def registeredService = args[1]
                def authentication = args[2]
                def httpRequest = args[3]
                def logger = args[4]

                logger.info("Evaluating authentication attributes [{}]", authentication.attributes)
                logger.info("Evaluating principal attributes [{}]", authentication.principal.attributes)

                def isSpnego = authentication.attributes['credentialType']
                def cc = authentication.principal.attributes['costCenter']

                if (isSpnego.contains('SpnegoCredential')) {
                        logger.info("Spnego active, bypassing MFA [{}]", isSpnego)
                        return null
                } else {
                        cc.each {
                                if (it.matches('23K65.+')) {
                                        logger.info("CostCenter TIHA [{}]", cc)
                                        logger.info("Activating MFA for this authentication session")
                                        return "mfa-duo"
                                } else {
                                        logger.info("CostCenter something else [{}]", cc)
                                        return null
                                }
                        }
                }
        }
}

Good ideas, suggestions, general advice and pointers to best practices are more than welcome.
Thank you in advance.
BR,
Otto Myyrä

Otto Myyrä

unread,
Oct 13, 2020, 5:05:48 AM10/13/20
to CAS Community
Sorry, realized I forgot to include version information. We are running CAS 6.1.7.1.

Samuel Lyons

unread,
Oct 15, 2020, 5:46:20 PM10/15/20
to CAS Community, Otto Myyrä
Here's one that we managed to get working, you can try similar settings to see if they help


cas.authn.mfa.duo[0].id=mfa-duo
cas.authn.mfa.globalPrincipalAttributePredicate=file:/etc/cas/attributeCollection/DetermineMFA.groovy
cas.authn.mfa.duo[0].rank=0
cas.authn.mfa.duo[0].duoApplicationKey=${key_duo_app}
cas.authn.mfa.duo[0].duoIntegrationKey=${key_duo_integration}
cas.authn.mfa.duo[0].duoApiHost=${duo_api_host}
cas.authn.mfa.duo[0].duoSecretKey=${key_duo}

Otto Myyrä

unread,
Oct 19, 2020, 2:55:53 AM10/19/20
to CAS Community, checkif...@gmail.com, Otto Myyrä
Hi.

Thank you for your suggestion. Unfortunately it seems we won't be able to use it since we are checking for SPnego in the groovy script and globalPrincipalAttributePredicate doesn't seem to convey authentication context to the groovy script, therefore making it impossible to base the mfa triggering logic on the existing/not existing SPnego auth.

Running cas with debug level logging tells us this after the groovy-trigger returns "mfa-duo" for cas:
2020-10-16 14:47:49,276 DEBUG [org.apereo.cas.web.flow.resolver.impl.DefaultCasDelegatingWebflowEventResolver] - <Transition definition cannot be found for event mfa-duo>
org.apereo.cas.authentication.AuthenticationException: Transition definition cannot be found for event mfa-duo
        at org.apereo.cas.authentication.MultifactorAuthenticationUtils.lambda$validateEventIdForMatchingTransitionInContext$1(MultifactorAuthenticationUtils.java:74) ~[cas-server-core-authentication-mfa-api-6.1.7.1.jar!/:6.1.7.1]

Are we just missing something obvious?

BR,
Otto Myyrä

Otto Myyrä

unread,
Oct 19, 2020, 5:31:19 AM10/19/20
to CAS Community, checkif...@gmail.com
Ok, found the problem. It started working after removing all the different return something lines within the if clauses in the groovy script.

Replaced all returns within the code with a temporary variable and returned that variable at the end of the script. Works now.

BR,
Otto Myyrä

Yan Zhou

unread,
Dec 4, 2025, 4:01:32 PM (3 days ago) Dec 4
to CAS Community, Otto Myyrä, checkif...@gmail.com
Hello, 

this is interesting, it is happening to me with CAS 7.3.1 overlay,  is this something specific to Groovy?  Only one return statement allowed in script?

thx!

Petr Bodnár

unread,
Dec 6, 2025, 10:13:34 AM (2 days ago) Dec 6
to CAS Community, Yan Zhou, Otto Myyrä, checkif...@gmail.com
Hello Yan,

no, this is not something specific to Groovy. You can also have as return statements as needed.

The problem here was in wrongly expecting that "return ..." inside "cc.each" exits the whole "run" method (even if it did, the script's functionality would then depend on whether there could be multiple items in "cc" and on their order). One of the solutions is to use a simple "for" loop instead, or a method like "any": if (cc.any { it.matches('23K65.+') }) { ...

Best regards
Petr
Reply all
Reply to author
Forward
0 new messages