├── maven ├── maven-wrapper.jar └── maven-wrapper.properties ├── cas-mfa-overlay ├── etc │ ├── authn-methods.conf │ ├── jetty │ │ ├── web.xml │ │ ├── jetty-https.xml │ │ └── jetty-ssl.xml │ ├── cas.properties │ └── log4j2.xml ├── maven │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src │ └── main │ │ └── webapp │ │ └── WEB-INF │ │ ├── spring-configuration │ │ └── propertyFileConfigurer.xml │ │ ├── classes │ │ └── services │ │ │ └── HTTPSandIMAPS-10000001.json │ │ └── deployerConfigContext.xml └── pom.xml ├── cas-mfa-duo ├── maven │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── duosecurity │ │ │ ├── DuoWebException.java │ │ │ ├── Util.java │ │ │ └── DuoWeb.java │ │ └── groovy │ │ └── net │ │ └── unicon │ │ └── cas │ │ └── mfa │ │ └── authentication │ │ └── duo │ │ ├── DuoAuthenticationService.groovy │ │ ├── DuoMultiFactorWebflowConfigurer.java │ │ ├── DuoCredentials.groovy │ │ └── DuoAuthenticationHandler.groovy └── pom.xml ├── cas-mfa-java ├── maven │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src │ ├── main │ │ ├── java │ │ │ └── net │ │ │ │ └── unicon │ │ │ │ └── cas │ │ │ │ └── mfa │ │ │ │ ├── authentication │ │ │ │ ├── CompositeAuthentication.java │ │ │ │ ├── AuthenticationMethodConfigurationProvider.java │ │ │ │ ├── AuthenticationMethodTranslator.java │ │ │ │ ├── DefaultAuthenticationMethodConfigurationProvider.java │ │ │ │ ├── RegisteredServiceMfaRoleProcessor.java │ │ │ │ ├── principal │ │ │ │ │ └── UnknownPrincipalMatchException.java │ │ │ │ ├── MultiFactorAuthenticationRequestResolver.java │ │ │ │ ├── RequestedAuthenticationMethodRankingStrategy.java │ │ │ │ ├── DefaultAuthenticationSupport.java │ │ │ │ ├── StubAuthenticationMethodTranslator.java │ │ │ │ ├── CasMultiFactorApplicationContextAware.java │ │ │ │ ├── AuthenticationSupport.java │ │ │ │ ├── RegexAuthenticationMethodTranslator.java │ │ │ │ ├── RememberAuthenticationMethodMetaDataPopulator.java │ │ │ │ ├── AuthenticationMethod.java │ │ │ │ ├── DefaultCompositeAuthentication.java │ │ │ │ ├── JsonBackedAuthenticationMethodConfigurationProvider.java │ │ │ │ ├── MultiFactorAuthenticationRequestContext.java │ │ │ │ ├── OrderedMultiFactorMethodRankingStrategy.java │ │ │ │ └── MultiFactorAuthenticationTransactionContext.java │ │ │ │ ├── web │ │ │ │ ├── flow │ │ │ │ │ ├── view │ │ │ │ │ │ ├── MultiFactorLoginViewPrincipalGreeter.java │ │ │ │ │ │ └── MultifactorLoginViewPrincipalAttributeGreeter.java │ │ │ │ │ ├── event │ │ │ │ │ │ ├── ErroringMultiFactorAuthenticationSpringWebflowEventBuilder.java │ │ │ │ │ │ ├── MultiFactorAuthenticationSpringWebflowEventBuilder.java │ │ │ │ │ │ └── ServiceAuthenticationMethodMultiFactorAuthenticationSpringWebflowEventBuilder.java │ │ │ │ │ ├── ConfigurableSpringWebflowExceptionHandler.java │ │ │ │ │ ├── NoAuthenticationContextAvailable.java │ │ │ │ │ ├── RemoveHostnameInContextAction.java │ │ │ │ │ ├── SendTicketGrantingTicketAction.java │ │ │ │ │ └── TerminatingMultiFactorAuthenticationViaFormAction.java │ │ │ │ ├── support │ │ │ │ │ ├── AuthenticationMethodVerifier.java │ │ │ │ │ ├── UnrecognizedAuthenticationMethodException.java │ │ │ │ │ ├── MultiFactorWebApplicationServiceFactory.java │ │ │ │ │ ├── DefaultMultiFactorWebApplicationServiceFactory.java │ │ │ │ │ ├── MultiFactorAuthenticationSupportingWebApplicationService.java │ │ │ │ │ ├── RequestParameterMultiFactorAuthenticationArgumentExtractor.java │ │ │ │ │ ├── DefaultAuthenticationMethodVerifier.java │ │ │ │ │ └── MultiFactorAuthenticationRequestsCollectingArgumentExtractor.java │ │ │ │ └── view │ │ │ │ │ └── Cas30ResponseView.java │ │ │ │ ├── ticket │ │ │ │ ├── UnacceptableMultiFactorAuthenticationMethodException.java │ │ │ │ ├── UnrecognizedMultiFactorAuthenticationMethodException.java │ │ │ │ └── MultiFactorAuthenticationBaseTicketValidationException.java │ │ │ │ └── util │ │ │ │ └── MultiFactorUtils.java │ │ └── resources │ │ │ └── META-INF │ │ │ └── spring │ │ │ └── mfa-context.xml │ └── test │ │ ├── resources │ │ └── log4j.xml │ │ ├── java │ │ └── net │ │ │ └── unicon │ │ │ └── cas │ │ │ └── mfa │ │ │ ├── web │ │ │ └── support │ │ │ │ └── DefaultMultiFactorAuthenticationSupportingWebApplicationServiceTests.java │ │ │ ├── util │ │ │ └── MultiFactorUtilsTests.java │ │ │ ├── authentication │ │ │ ├── DefaultCompositeAuthenticationTests.java │ │ │ ├── RegexAuthenticationMethodTranslatorTests.java │ │ │ └── principal │ │ │ │ └── MultiFactorCredentialsTests.java │ │ │ └── MultiFactorAuthenticationProtocolValidationSpecificationTests.java │ │ └── groovy │ │ └── net │ │ └── unicon │ │ └── cas │ │ └── mfa │ │ └── authentication │ │ ├── OrderedMfaMethodRankingStrategyTests.groovy │ │ ├── MultiFactorAuthenticationTransactionContextTests.groovy │ │ └── principal │ │ └── PrincipalAttributeMultiFactorAuthenticationRequestResolverTests.groovy ├── README.md └── pom.xml ├── cas-mfa-web ├── maven │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── src │ ├── main │ │ └── webapp │ │ │ └── WEB-INF │ │ │ ├── spring-configuration │ │ │ ├── common-context.xml │ │ │ ├── mfaArgumentExtractorsConfiguration.xml │ │ │ └── uniqueIdGenerators.xml │ │ │ └── view │ │ │ └── jsp │ │ │ └── default │ │ │ └── ui │ │ │ ├── casUnknownPrincipalErrorView.jsp │ │ │ └── casMfaUnrecognizedAuthnMethodErrorView.jsp │ └── test │ │ └── java │ │ └── net │ │ └── unicon │ │ └── cas │ │ └── mfa │ │ └── web │ │ └── flow │ │ └── view │ │ └── MultifactorLoginViewPrincipalAttributeGreeterTests.java └── pom.xml ├── cas-mfa-duo-web ├── maven │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── pom.xml └── src │ └── main │ └── webapp │ ├── WEB-INF │ ├── view │ │ └── jsp │ │ │ └── default │ │ │ └── ui │ │ │ └── casDuoLoginView.jsp │ ├── cas-servlet-mfa-duo-two-factor.xml │ └── webflow │ │ └── mfa-duo-two-factor │ │ └── mfa-duo-two-factor-webflow.xml │ └── js │ └── duo │ └── Duo-Web-v2.min.js ├── .gitignore ├── travis ├── init-travis-build.sh ├── deploy-to-sonatype.sh └── settings.xml ├── .travis.yml ├── NOTICE.txt └── checkstyle-suppressions.xml /maven/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unicon/cas-mfa/HEAD/maven/maven-wrapper.jar -------------------------------------------------------------------------------- /cas-mfa-overlay/etc/authn-methods.conf: -------------------------------------------------------------------------------- 1 | [ { 2 | "rank" : 1, 3 | "name" : "duo-two-factor" 4 | } ] 5 | -------------------------------------------------------------------------------- /cas-mfa-duo/maven/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unicon/cas-mfa/HEAD/cas-mfa-duo/maven/maven-wrapper.jar -------------------------------------------------------------------------------- /cas-mfa-java/maven/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unicon/cas-mfa/HEAD/cas-mfa-java/maven/maven-wrapper.jar -------------------------------------------------------------------------------- /cas-mfa-web/maven/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unicon/cas-mfa/HEAD/cas-mfa-web/maven/maven-wrapper.jar -------------------------------------------------------------------------------- /cas-mfa-duo-web/maven/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unicon/cas-mfa/HEAD/cas-mfa-duo-web/maven/maven-wrapper.jar -------------------------------------------------------------------------------- /cas-mfa-overlay/maven/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unicon/cas-mfa/HEAD/cas-mfa-overlay/maven/maven-wrapper.jar -------------------------------------------------------------------------------- /maven/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Maven download properties 2 | #Tue Dec 01 19:58:21 MST 2015 3 | distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip 4 | -------------------------------------------------------------------------------- /cas-mfa-duo/maven/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Maven download properties 2 | #Tue Dec 01 19:58:21 MST 2015 3 | distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip 4 | -------------------------------------------------------------------------------- /cas-mfa-java/maven/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Maven download properties 2 | #Tue Dec 01 19:58:21 MST 2015 3 | distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip 4 | -------------------------------------------------------------------------------- /cas-mfa-web/maven/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Maven download properties 2 | #Tue Dec 01 19:58:21 MST 2015 3 | distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip 4 | -------------------------------------------------------------------------------- /cas-mfa-duo-web/maven/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Maven download properties 2 | #Tue Dec 01 19:58:21 MST 2015 3 | distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip 4 | -------------------------------------------------------------------------------- /cas-mfa-overlay/maven/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Maven download properties 2 | #Tue Dec 01 19:58:21 MST 2015 3 | distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | !/.project 3 | .project 4 | .settings 5 | target 6 | test-output/ 7 | bin/ 8 | *.ipr 9 | *.iml 10 | *.iws 11 | .idea/ 12 | .DS_Store 13 | .idea 14 | overlays/ 15 | pom.xml.versionsBackup 16 | *.log 17 | 18 | **/.checkstyle 19 | **/rebel.xml 20 | -------------------------------------------------------------------------------- /cas-mfa-overlay/etc/jetty/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /cas-mfa-web/README.md: -------------------------------------------------------------------------------- 1 | # Readme for cas-mfa-web 2 | 3 | ## What is this 4 | 5 | This is the multifactor extensions for CAS sub-module for Web components. Things that are not Java, that do not build into a .jar. 6 | 7 | For example: JSPs, deployment descriptors, customized and new Spring Web Flow XML and other Spring web framework configuration, etc. 8 | 9 | These components depend on the Java in the .jar built by the parallel `cas-mfa-java` module. 10 | 11 | ## How do I build this 12 | 13 | *Good question*. To be defined and documented. 14 | -------------------------------------------------------------------------------- /cas-mfa-duo/src/main/java/com/duosecurity/DuoWebException.java: -------------------------------------------------------------------------------- 1 | package com.duosecurity; 2 | 3 | /** 4 | * Duo security integration code copied from: https://github.com/duosecurity/duo_java . 5 | * @author Duo Security 6 | */ 7 | public class DuoWebException extends Exception { 8 | 9 | private static final long serialVersionUID = 451949380095167112L; 10 | 11 | /** 12 | * Instantiates a new Duo web exception. 13 | * 14 | * @param message the message 15 | */ 16 | public DuoWebException(final String message) { 17 | super(message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /travis/init-travis-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e "Building branch: ${TRAVIS_BRANCH}" 4 | echo -e "Build directory: ${TRAVIS_BUILD_DIR}" 5 | echo -e "Build id: ${TRAVIS_BUILD_ID}" 6 | echo -e "Builder number: ${TRAVIS_BUILD_NUMBER}" 7 | echo -e "Job id: ${TRAVIS_JOB_ID}" 8 | echo -e "Job number: ${TRAVIS_JOB_NUMBER}" 9 | echo -e "Repo slug: ${TRAVIS_REPO_SLUG}" 10 | echo -e "OS name: ${TRAVIS_OS_NAME}" 11 | 12 | if [ "$TRAVIS_SECURE_ENV_VARS" == "false" ] 13 | then 14 | echo -e "Secure environment variables are NOT available...\n" 15 | else 16 | echo -e "Secure environment variables are available...\n" 17 | fi 18 | -------------------------------------------------------------------------------- /travis/deploy-to-sonatype.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e "Evaluating change before pushing to Sonatype..." 4 | 5 | # Only invoke the deployment to Sonatype when it's not a PR and only for master 6 | if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then 7 | echo -e "Starting to deploy to Sonatype..." 8 | mvn deploy --settings ./travis/settings.xml 9 | echo -e "Successfully deployed SNAPSHOT artifacts to Sonatype under Travis job ${TRAVIS_JOB_NUMBER}" 10 | else 11 | echo -e "Skipped Sonatype deployment. This is either a pull request or a change on a different branch other than master" 12 | fi 13 | echo -e "Fnished deploying to Sonatype." 14 | -------------------------------------------------------------------------------- /travis/settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | sonatype-nexus-snapshots 13 | ${SONATYPE_USER} 14 | ${SONATYPE_PWD} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/CompositeAuthentication.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import java.util.Set; 4 | 5 | import org.jasig.cas.authentication.Authentication; 6 | 7 | /** 8 | * A composite authentication that specifically is able to collect 9 | * authentication methods fulfilled in the chain. 10 | * @author Misagh Moayyed 11 | */ 12 | public interface CompositeAuthentication extends Authentication { 13 | 14 | /** 15 | * Retrieves the collection of authentication methods available in the list 16 | * of authentication attributes. 17 | * @return collection of authentication methods 18 | */ 19 | Set getSatisfiedAuthenticationMethods(); 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | env: 4 | global: 5 | - secure: HGkZ6LlYQDmC3ppP8UAOY6TmfddLZ5bt6UTdrh2SG3FDgBpA7L6tGyq4JyKco5OKO7N6oFcrbmYIAY2thEDxCrtcOGCvLhkxYv6uxqKt32Yeknosk5F4/JNHCu/bEFIoyIyz/RzW5QvoF27kTb7BZ3fdjPbU39k9V1rYIU6/Oqw= 6 | - secure: FuhW99CpzFj/iPR9dgLhhkw6UFJltWb2HDLLXGMmLb/OKMAQjenV+yAIao5v5KaD6LTZqFe4Ugi+XIzepCSBFayJFsdLDNYb8Bp/bINaq9t3kmsSXE0e3LUGs8wO6rgHgmVv6WpscTjSt7uvZsiuKwYzaSnivcXTEfMxF72Q8mw= 7 | branches: 8 | only: 9 | - master 10 | before_install: 11 | - chmod -R 777 ./travis/init-travis-build.sh 12 | - ./travis/init-travis-build.sh 13 | install: 14 | - mvn -T 10 install -Dmaven.javadoc.skip=true -B -V 15 | script: 16 | - mvn clean package -q 17 | after_success: 18 | - chmod -R 777 ./travis/deploy-to-sonatype.sh 19 | - ./travis/deploy-to-sonatype.sh 20 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/view/MultiFactorLoginViewPrincipalGreeter.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow.view; 2 | 3 | import org.jasig.cas.authentication.principal.Principal; 4 | import org.springframework.binding.message.MessageContext; 5 | 6 | /** 7 | * Defines an abstraction by which principals can be greeted in the view. 8 | * @author Misagh Moayyed 9 | */ 10 | public interface MultiFactorLoginViewPrincipalGreeter { 11 | /** 12 | * Return the identifier that is to used to greet the credentials 13 | * in the view. 14 | * @param p the principal we are trying to welcome to the view. 15 | * @param messageContext context to resolve the message code associated with the greeting 16 | * @return the greetee 17 | */ 18 | String getPersonToGreet(Principal p, MessageContext messageContext); 19 | } 20 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/AuthenticationMethodConfigurationProvider.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | /** 4 | * Defines where authentication methods come from, which are 5 | * supported and how they are loaded into the application context. 6 | * @author Misagh Moayyed 7 | */ 8 | public interface AuthenticationMethodConfigurationProvider { 9 | /** 10 | * Contains authentication method. 11 | * 12 | * @param name the name 13 | * @return true if the method is found 14 | */ 15 | boolean containsAuthenticationMethod(String name); 16 | 17 | /** 18 | * Gets authentication method. 19 | * 20 | * @param name the name 21 | * @return the authentication method, or null if none is found. 22 | */ 23 | AuthenticationMethod getAuthenticationMethod(String name); 24 | } 25 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/resources/META-INF/spring/mfa-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /cas-mfa-overlay/src/main/webapp/WEB-INF/spring-configuration/propertyFileConfigurer.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /cas-mfa-duo/src/main/java/com/duosecurity/Util.java: -------------------------------------------------------------------------------- 1 | package com.duosecurity; 2 | 3 | import java.security.InvalidKeyException; 4 | import java.security.NoSuchAlgorithmException; 5 | 6 | import javax.crypto.Mac; 7 | import javax.crypto.spec.SecretKeySpec; 8 | 9 | public class Util { 10 | 11 | private Util() {} 12 | 13 | public static String hmacSign(final String skey, final String data) 14 | throws NoSuchAlgorithmException, InvalidKeyException { 15 | final SecretKeySpec key = new SecretKeySpec(skey.getBytes(), "HmacSHA1"); 16 | final Mac mac = Mac.getInstance("HmacSHA1"); 17 | mac.init(key); 18 | final byte[] raw = mac.doFinal(data.getBytes()); 19 | return bytesToHex(raw); 20 | } 21 | 22 | public static String bytesToHex(final byte[] b) { 23 | String result = ""; 24 | for (int i = 0; i < b.length; i++) { 25 | result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1); 26 | } 27 | return result; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/support/AuthenticationMethodVerifier.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | import org.jasig.cas.authentication.principal.WebApplicationService; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | 7 | /** 8 | * Strategy interface for verifying requested mfa authentication methods. 9 | * 10 | * @author Dmitriy Kopylenko 11 | * @author Unicon inc. 12 | */ 13 | public interface AuthenticationMethodVerifier { 14 | 15 | /** 16 | * Verify requested mfa authentication method. 17 | * 18 | * @param authenticationMethod requested authentication method 19 | * @param targetService targetService 20 | * @param request Http request 21 | * @return true if the authn method is supported and verified 22 | */ 23 | boolean verifyAuthenticationMethod(String authenticationMethod, WebApplicationService targetService, HttpServletRequest request); 24 | } 25 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/event/ErroringMultiFactorAuthenticationSpringWebflowEventBuilder.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow.event; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.webflow.execution.Event; 6 | import org.springframework.webflow.execution.RequestContext; 7 | 8 | /** 9 | * Builds the error event on MFA ops. 10 | * @author Misagh Moayyed 11 | */ 12 | public class ErroringMultiFactorAuthenticationSpringWebflowEventBuilder 13 | implements MultiFactorAuthenticationSpringWebflowEventBuilder { 14 | 15 | private final Logger logger = LoggerFactory.getLogger(getClass()); 16 | 17 | /** 18 | * The Constant MFA_ERROR_EVENT_ID. 19 | */ 20 | public static final String MFA_ERROR_EVENT_ID = "error"; 21 | 22 | @Override 23 | public Event buildEvent(final RequestContext context) { 24 | logger.debug("Building event id {}", MFA_ERROR_EVENT_ID); 25 | return new Event(this, MFA_ERROR_EVENT_ID); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/ConfigurableSpringWebflowExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow; 2 | 3 | import org.springframework.webflow.engine.support.TransitionExecutingFlowExecutionExceptionHandler; 4 | 5 | /** 6 | * A extension of {@link TransitionExecutingFlowExecutionExceptionHandler} that exposes configuration 7 | * for convenience directly via a constructor, such that handlers can be configured via explicit spring beans. 8 | * @author Misagh Moayyed 9 | */ 10 | public final class ConfigurableSpringWebflowExceptionHandler extends TransitionExecutingFlowExecutionExceptionHandler { 11 | 12 | /** 13 | * Initialize the handler with the exception class to handle, and the state to which the flow must move. 14 | * @param exceptionClass exception class to handle 15 | * @param state state to which the flow moves. 16 | */ 17 | public ConfigurableSpringWebflowExceptionHandler(final Class exceptionClass, final String state) { 18 | super(); 19 | add(exceptionClass, state); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cas-mfa-web/src/main/webapp/WEB-INF/spring-configuration/common-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/ticket/UnacceptableMultiFactorAuthenticationMethodException.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.ticket; 2 | 3 | /** 4 | * Multifactor authentication exception that is thrown 5 | * when the request authentication method cannot be accepted/provided 6 | * by this CAS server. 7 | * @author Misagh Moayyed 8 | * @see net.unicon.cas.mfa.AbstractMultiFactorAuthenticationProtocolValidationSpecification 9 | */ 10 | public class UnacceptableMultiFactorAuthenticationMethodException extends MultiFactorAuthenticationBaseTicketValidationException { 11 | 12 | private static final long serialVersionUID = -8544747236126342213L; 13 | 14 | /** 15 | * Constructor to spin up the exception instance. 16 | * @param code error code 17 | * @param msg error message 18 | * @param authnMethod authentication method associated with the error 19 | */ 20 | public UnacceptableMultiFactorAuthenticationMethodException(final String code, final String msg, final String authnMethod) { 21 | super(code, msg, authnMethod); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/ticket/UnrecognizedMultiFactorAuthenticationMethodException.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.ticket; 2 | 3 | /** 4 | * Multifactor authentication exception that is thrown 5 | * when the requested authentication method cannot be accepted 6 | * or isn't support by this CAS server. 7 | * @author Misagh Moayyed 8 | * @see net.unicon.cas.mfa.AbstractMultiFactorAuthenticationProtocolValidationSpecification 9 | */ 10 | public class UnrecognizedMultiFactorAuthenticationMethodException extends MultiFactorAuthenticationBaseTicketValidationException { 11 | 12 | private static final long serialVersionUID = -8544747236126342213L; 13 | 14 | /** 15 | * Constructor to spin up the exception instance. 16 | * @param code error code 17 | * @param msg error message 18 | * @param authnMethod authentication method associated with the error 19 | */ 20 | public UnrecognizedMultiFactorAuthenticationMethodException(final String code, final String msg, final String authnMethod) { 21 | super(code, msg, authnMethod); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/event/MultiFactorAuthenticationSpringWebflowEventBuilder.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow.event; 2 | 3 | import org.springframework.webflow.execution.Event; 4 | import org.springframework.webflow.execution.RequestContext; 5 | 6 | /** 7 | * Describes the necessary mechanics for building multifactor event ids 8 | * for spring webflow from one transition to another. 9 | * @author Misagh Moayyed 10 | */ 11 | public interface MultiFactorAuthenticationSpringWebflowEventBuilder { 12 | 13 | /** 14 | * The Constant MFA_EVENT_ID_PREFIX. 15 | */ 16 | String MFA_EVENT_ID_PREFIX = "mfa-"; 17 | 18 | /** 19 | * Builds the MFA event required for the next transition to occur. 20 | * 21 | * @param context the context 22 | * @return the event 23 | * @throws java.lang.IllegalStateException if no matching transitions exists 24 | * for this particular event. Implementations may want to check the request context 25 | * for the validity of the configured event before passing it on. 26 | */ 27 | Event buildEvent(RequestContext context); 28 | } 29 | -------------------------------------------------------------------------------- /cas-mfa-duo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cas-mfa 5 | net.unicon 6 | 2.0.0-RC4-SNAPSHOT 7 | 8 | 4.0.0 9 | cas-mfa-duo 10 | CAS MFA Duo 11 | 12 | Module containing Duo security support code as well as CAS' authentication subsystem code 13 | built on top of Duo. 14 | 15 | jar 16 | 17 | 18 | 19 | org.jasig.cas 20 | cas-server-core 21 | 22 | 23 | 24 | org.codehaus.groovy 25 | groovy 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/AuthenticationMethodTranslator.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import org.jasig.cas.authentication.principal.WebApplicationService; 4 | 5 | /** 6 | * Defines operations that are to translate a received/retrieved authentication method 7 | * into one that CAS can recognize. This is useful in cases where principal attributes 8 | * have defined arbitrary names for the MFA trigger, or that service providers are unable 9 | * to change submitted parameter names in the request to trigger MFA. 10 | * @author Misagh Moayyed 11 | */ 12 | public interface AuthenticationMethodTranslator { 13 | /** 14 | * Translate an authentication method to one that CAS can recognize. 15 | * Implementations may choose to decide what should happen if no mapping 16 | * is found between the source and target authentication methods. 17 | * @param targetService the target service 18 | * @param receivedAuthenticationMethod the received authentication method 19 | * @return the translated method name 20 | */ 21 | String translate(WebApplicationService targetService, String receivedAuthenticationMethod); 22 | } 23 | -------------------------------------------------------------------------------- /cas-mfa-overlay/src/main/webapp/WEB-INF/classes/services/HTTPSandIMAPS-10000001.json: -------------------------------------------------------------------------------- 1 | { 2 | "@class" : "org.jasig.cas.services.RegexRegisteredService", 3 | "serviceId" : "^(https|imaps)://.*", 4 | "name" : "HTTPS and IMAPS", 5 | "id" : 10000001, 6 | "description" : "This service definition authorized all application urls that support HTTPS and IMAPS protocols.", 7 | "proxyPolicy" : { 8 | "@class" : "org.jasig.cas.services.RefuseRegisteredServiceProxyPolicy" 9 | }, 10 | "evaluationOrder" : 0, 11 | "usernameAttributeProvider" : { 12 | "@class" : "org.jasig.cas.services.DefaultRegisteredServiceUsernameProvider" 13 | }, 14 | "logoutType" : "BACK_CHANNEL", 15 | "attributeReleasePolicy" : { 16 | "@class" : "org.jasig.cas.services.ReturnAllowedAttributeReleasePolicy", 17 | "principalAttributesRepository" : { 18 | "@class" : "org.jasig.cas.authentication.principal.DefaultPrincipalAttributesRepository" 19 | }, 20 | "authorizedToReleaseCredentialPassword" : false, 21 | "authorizedToReleaseProxyGrantingTicket" : false 22 | }, 23 | "accessStrategy" : { 24 | "@class" : "org.jasig.cas.services.DefaultRegisteredServiceAccessStrategy", 25 | "enabled" : true, 26 | "ssoEnabled" : true 27 | } 28 | } -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/NoAuthenticationContextAvailable.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow; 2 | 3 | /** 4 | * In the event that a left-over TGT exists from previous session, from which 5 | * an authentication context cannot be established, again, because the TGT is considered 6 | * invalid, this exception will be thrown. 7 | *

8 | * In particular, the flow is to handle this exception and navigate to the 9 | * appropriate state, without requiring any additional action from the user 10 | * such as closing the browser to clearing the cache thereby killing the TGT. 11 | * The flow is responsible for handling this annoyance. 12 | * 13 | *

Essentially, the responsibility of this exception is solely to 14 | * communicate a broken and existing TGT, or in other words, the inability 15 | * to construct the authentication object from either the flow context 16 | * or an existing TGT. Beyond that task, it will not do 17 | * or provide any interesting functionality. 18 | * @author Misagh Moayyed 19 | * @see GenerateMultiFactorCredentialsAction 20 | */ 21 | public class NoAuthenticationContextAvailable extends RuntimeException { 22 | private static final long serialVersionUID = -1693098929280964735L; 23 | } 24 | -------------------------------------------------------------------------------- /cas-mfa-web/src/main/webapp/WEB-INF/view/jsp/default/ui/casUnknownPrincipalErrorView.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | 3 | Licensed to Jasig under one or more contributor license 4 | agreements. See the NOTICE file distributed with this work 5 | for additional information regarding copyright ownership. 6 | Jasig licenses this file to you under the Apache License, 7 | Version 2.0 (the "License"); you may not use this file 8 | except in compliance with the License. You may obtain a 9 | copy of the License at the following location: 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, 14 | software distributed under the License is distributed on an 15 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | KIND, either express or implied. See the License for the 17 | specific language governing permissions and limitations 18 | under the License. 19 | 20 | --%> 21 | 22 |

23 |

24 |

25 |
26 | -------------------------------------------------------------------------------- /cas-mfa-java/README.md: -------------------------------------------------------------------------------- 1 | # Readme for cas-mfa-java sub-module 2 | 3 | ## What is this 4 | 5 | This module is for the Java source code of the multifactor authentication extensions for CAS. 6 | It builds to a .jar. 7 | 8 | This module is intended to include all the Java you need to add to a CAS implementation to take advantage of the extended multifactor authentication features in this project. 9 | 10 | Of course, it's kind of useless all by itself, since you also need Web application components, which live in the parallel `cas-mfa-web` directory. Those Web components depend on these Java components. 11 | 12 | This is kind of complicated and may not be the final answer here. 13 | 14 | ## How do I build it? 15 | 16 | In this directory, run 17 | 18 | mvn package 19 | 20 | This will yield a `target` directory containing, among other artifacts, a `cas-mfa-java-{VERSION}.jar`, where {VERSION} is, as of this writing, "0.0.1-SNAPSHOT". As in, `cas-mfa-java-0.0.1-SNAPSHOT.jar`. 21 | 22 | You'd then include that .jar in an application, e.g. by declaring it as a Maven dependency in a `pom.xml`. 23 | 24 | The `cas-mfa-web` project does this, and the top level (up one directory) `pom.xml` automates first building this .jar and then making use of it in the other (i.e., .war) artifacts it builds. 25 | 26 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014, The Evergreen State College, University of Utah, Internet2 and/or the author or authors 2 | 3 | This project includes software developed by and/or licensed to Apereo, 4 | as per the copyright notice and license terms documented in the Jasig / Apereo CAS 5 | product distribution and source code themselves. 6 | http://www.apereo.org/ 7 | 8 | Licensed under the Apache License, Version 2.0 (the 9 | "License"); you may not use this software except in compliance 10 | with the License. You may obtain a copy of the License at: 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, 15 | software distributed under the License is distributed on 16 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | KIND, either express or implied. See the License for the 18 | specific language governing permissions and limitations 19 | under the License. 20 | 21 | This project includes or includes software based on: 22 | Jasig CAS Core under Apache 2 23 | Jasig CAS Web Application under Apache 2 24 | Jasig Central Authentication Service under Apache 2 25 | 26 | This project was partially supported by the National Strategy for Trusted Identities 27 | in Cyberspace (NSTIC) National Program Office and the National 28 | Institute of Standards and Technology (NIST). 29 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/DefaultAuthenticationMethodConfigurationProvider.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @author Misagh Moayyed 7 | */ 8 | public class DefaultAuthenticationMethodConfigurationProvider implements AuthenticationMethodConfigurationProvider { 9 | private final Map authenticationMethodsMap; 10 | 11 | /** 12 | * Instantiates a new Default authentication method configuration provider. 13 | * 14 | * @param authenticationMethodsMap the authentication methods map 15 | */ 16 | public DefaultAuthenticationMethodConfigurationProvider(final Map authenticationMethodsMap) { 17 | this.authenticationMethodsMap = authenticationMethodsMap; 18 | } 19 | 20 | @Override 21 | public boolean containsAuthenticationMethod(final String name) { 22 | return getAuthenticationMethod(name) != null; 23 | } 24 | 25 | @Override 26 | public AuthenticationMethod getAuthenticationMethod(final String name) { 27 | if (this.authenticationMethodsMap.containsKey(name)) { 28 | final Integer rank = this.authenticationMethodsMap.get(name); 29 | return new AuthenticationMethod(name, rank); 30 | } 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/support/UnrecognizedAuthenticationMethodException.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | /** 4 | * Thrown if an incoming authentication request specified an authentication 5 | * method that is not supported and/or recognized by the MFA configuration. 6 | * @author Misagh Moayyed 7 | * @see net.unicon.cas.mfa.web.support.AbstractMultiFactorAuthenticationArgumentExtractor 8 | */ 9 | public class UnrecognizedAuthenticationMethodException extends RuntimeException { 10 | 11 | private static final long serialVersionUID = -4141126343252978132L; 12 | 13 | private final String authnMethod; 14 | private final String service; 15 | 16 | /** 17 | * Spin up the exception instance with the requested authentication method. 18 | * @param authnMethod the unsupported authentication method in the request 19 | * @param service the service we are trying to log into 20 | */ 21 | public UnrecognizedAuthenticationMethodException(final String authnMethod, final String service) { 22 | this.authnMethod = authnMethod; 23 | this.service = service; 24 | } 25 | 26 | public final String getAuthenticationMethod() { 27 | return this.authnMethod; 28 | } 29 | 30 | public final String getService() { 31 | return this.service; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cas-mfa-web/src/main/webapp/WEB-INF/view/jsp/default/ui/casMfaUnrecognizedAuthnMethodErrorView.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | 3 | Licensed to Jasig under one or more contributor license 4 | agreements. See the NOTICE file distributed with this work 5 | for additional information regarding copyright ownership. 6 | Jasig licenses this file to you under the Apache License, 7 | Version 2.0 (the "License"); you may not use this file 8 | except in compliance with the License. You may obtain a 9 | copy of the License at the following location: 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, 14 | software distributed under the License is distributed on an 15 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | KIND, either express or implied. See the License for the 17 | specific language governing permissions and limitations 18 | under the License. 19 | 20 | --%> 21 | 22 |
23 |

24 |

26 |
27 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/RegisteredServiceMfaRoleProcessor.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import org.jasig.cas.authentication.Authentication; 4 | import org.jasig.cas.authentication.principal.WebApplicationService; 5 | 6 | import javax.validation.constraints.NotNull; 7 | import java.util.List; 8 | 9 | /** 10 | * Defines a mechanism that allows the service's attributes to be compared with the users attributes. 11 | * 12 | * @author John Gasper 13 | * @author Unicon, inc. 14 | */ 15 | public interface RegisteredServiceMfaRoleProcessor { 16 | 17 | /** 18 | * mfa_attribute_name. 19 | */ 20 | String MFA_ATTRIBUTE_NAME = "mfa_attribute_name"; 21 | 22 | /** 23 | * mfa_attribute_name. 24 | */ 25 | String MFA_ATTRIBUTE_PATTERN = "mfa_attribute_pattern"; 26 | 27 | /** 28 | * Resolves the authn_method for a given service if it supports mfa_role and the user has the appropriate attribute. 29 | * @param authentication the user authentication object 30 | * @param targetService the target service being tested 31 | * @return a list (usually one) mfa authn request context. 32 | */ 33 | List resolve(@NotNull Authentication authentication, 34 | @NotNull WebApplicationService targetService); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/ticket/MultiFactorAuthenticationBaseTicketValidationException.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.ticket; 2 | 3 | /** 4 | * Base multifactor authentication exception class in the hierarchy. 5 | * Defines the authentication code, the error message and the requested 6 | * authentication method. 7 | * @author Misagh Moayyed 8 | * @see UnacceptableMultiFactorAuthenticationMethodException 9 | * @see UnrecognizedMultiFactorAuthenticationMethodException 10 | */ 11 | public abstract class MultiFactorAuthenticationBaseTicketValidationException extends RuntimeException { 12 | 13 | private static final long serialVersionUID = 7880539766094343828L; 14 | 15 | private final String authenticationMethod; 16 | private final String code; 17 | 18 | /** 19 | * Initialize the exception object. 20 | * @param c the error code 21 | * @param msg the error message describing this exception 22 | * @param authnMethod the authentication method requested 23 | */ 24 | public MultiFactorAuthenticationBaseTicketValidationException(final String c, final String msg, final String authnMethod) { 25 | super(msg); 26 | this.code = c; 27 | this.authenticationMethod = authnMethod; 28 | } 29 | 30 | public final String getAuthenticationMethod() { 31 | return this.authenticationMethod; 32 | } 33 | 34 | public final String getCode() { 35 | return this.code; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/java/net/unicon/cas/mfa/web/support/DefaultMultiFactorAuthenticationSupportingWebApplicationServiceTests.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.jasig.cas.authentication.principal.Response; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.junit.runners.JUnit4; 9 | 10 | @RunWith(JUnit4.class) 11 | public class DefaultMultiFactorAuthenticationSupportingWebApplicationServiceTests { 12 | 13 | /** 14 | * Test that an instance of {@link DefaultMultiFactorAuthenticationSupportingWebApplicationService} 15 | * properly implements getAuthenticationMethod() and ability to get a Response to direct the user to redirect to 16 | * the service with a ticket. 17 | */ 18 | @Test 19 | public void testServiceness() { 20 | final DefaultMultiFactorAuthenticationSupportingWebApplicationService svc = 21 | new DefaultMultiFactorAuthenticationSupportingWebApplicationService("https://www.github.com", 22 | "https://www.github.com", null, Response.ResponseType.REDIRECT, 23 | "test_authn_method"); 24 | assertEquals(svc.getAuthenticationMethod(), "test_authn_method"); 25 | final Response res = svc.getResponse("testTicketId"); 26 | assertNotNull(res); 27 | assertEquals(res.getResponseType(), Response.ResponseType.REDIRECT); 28 | assertEquals(res.getUrl(), "https://www.github.com?ticket=testTicketId"); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/principal/UnknownPrincipalMatchException.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication.principal; 2 | 3 | 4 | import org.jasig.cas.authentication.Authentication; 5 | 6 | /** 7 | * An exception to indicate that a mismatch has been found between authenticated principals. 8 | * Credentials that are resolved into principals throughout the authentication flow are required 9 | * to be recognized by the same identifier {@link org.jasig.cas.authentication.principal.Principal#getId()}. 10 | *

For instance, if credentials are resolved into Principal 'A' as part of the first 11 | * leg of the multifactor authentication, and the second leg then resolves the credentials to into a Principal 12 | * that is identified by 'B', this exception will be thrown. 13 | * @author Misagh Moayyed 14 | * @see MultiFactorCredentials 15 | */ 16 | public final class UnknownPrincipalMatchException extends RuntimeException { 17 | private static final long serialVersionUID = -6572930326804074536L; 18 | 19 | private final Authentication authentication; 20 | 21 | /** 22 | * Initialize the exception with the authentication given. 23 | * @param authentication the authentication context associated with this error. 24 | */ 25 | public UnknownPrincipalMatchException(final Authentication authentication) { 26 | this.authentication = authentication; 27 | } 28 | 29 | public Authentication getAuthentication() { 30 | return authentication; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /cas-mfa-duo/src/main/groovy/net/unicon/cas/mfa/authentication/duo/DuoAuthenticationService.groovy: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication.duo 2 | 3 | import com.duosecurity.DuoWeb 4 | import groovy.util.logging.Slf4j 5 | 6 | /** 7 | * An abstraction that encapsulates interaction with Duo 2fa authentication service via its public API 8 | *

9 | * Derived from the fine work of @author Eric Pierce 10 | * and @author Michael Kennedy 11 | */ 12 | @Slf4j 13 | final class DuoAuthenticationService { 14 | private final String duoIntegrationKey 15 | private final String duoSecretKey 16 | private final String duoApplicationKey 17 | private final String duoApiHost 18 | 19 | DuoAuthenticationService(duoIntegrationKey, duoSecretKey, duoApplicationKey, duoApiHost) { 20 | this.duoIntegrationKey = duoIntegrationKey 21 | this.duoSecretKey = duoSecretKey 22 | this.duoApplicationKey = duoApplicationKey 23 | this.duoApiHost = duoApiHost 24 | } 25 | 26 | def getDuoApiHost() { 27 | this.duoApiHost 28 | } 29 | 30 | def generateSignedRequestToken(final String username) { 31 | DuoWeb.signRequest(this.duoIntegrationKey, this.duoSecretKey, this.duoApplicationKey, username) 32 | } 33 | 34 | def authenticate(final String signedRequestToken) { 35 | log.debug("Calling DuoWeb.verifyResponse with signed request token '{}'", signedRequestToken) 36 | DuoWeb.verifyResponse(this.duoIntegrationKey, this.duoSecretKey, this.duoApplicationKey, signedRequestToken) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/java/net/unicon/cas/mfa/util/MultiFactorUtilsTests.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.util; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.util.Arrays; 6 | import java.util.HashMap; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.junit.runners.JUnit4; 13 | 14 | @RunWith(JUnit4.class) 15 | public class MultiFactorUtilsTests { 16 | 17 | @Test 18 | public void testConversionOfSingleValueIntoCollection() { 19 | final Set set = MultiFactorUtils.convertValueToCollection("thisIsJustAValue"); 20 | assertTrue(set.size() == 1); 21 | } 22 | 23 | @Test(expected = ClassCastException.class) 24 | public void testConversionOfNonObjectArrayIntoCollection() { 25 | final int[] array = {1, 2, 3}; 26 | MultiFactorUtils.convertValueToCollection(array); 27 | } 28 | 29 | @Test 30 | public void testConversionOfArrayIntoCollection() { 31 | final Object[] array = {1, 2, 3}; 32 | final Set set = MultiFactorUtils.convertValueToCollection(array); 33 | assertEquals(set.size(), 3); 34 | } 35 | 36 | @Test 37 | public void testConversionOfSetIntoCollection() { 38 | final Set set = MultiFactorUtils.convertValueToCollection(new HashSet(Arrays.asList("1", "2", "2"))); 39 | assertEquals(set.size(), 2); 40 | } 41 | 42 | @Test(expected = UnsupportedOperationException.class) 43 | public void testConversionOfMapValuesIntoCollection() { 44 | MultiFactorUtils.convertValueToCollection(new HashMap()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cas-mfa-duo-web/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cas-mfa 5 | net.unicon 6 | 2.0.0-RC4-SNAPSHOT 7 | 8 | 4.0.0 9 | cas-mfa-duo-web 10 | CAS MFA Duo Web Support 11 | 12 | This is the multifactor extensions for CAS sub-module for Duo Web components. Things that are not Java, 13 | that do not build into a .jar. For example: JSPs, deployment descriptors, customized and new 14 | Spring Web Flow XML and other Spring web framework configuration, etc. 15 | 16 | war 17 | 18 | 19 | 20 | 21 | org.apache.maven.plugins 22 | maven-war-plugin 23 | 24 | false 25 | 26 | 27 | 28 | cas-mfa-duo-web 29 | 30 | 31 | 32 | 33 | net.unicon 34 | cas-mfa-java 35 | ${project.version} 36 | 37 | 38 | net.unicon 39 | cas-mfa-duo 40 | ${project.version} 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /cas-mfa-duo-web/src/main/webapp/WEB-INF/view/jsp/default/ui/casDuoLoginView.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

6 |

9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 25 |
26 | 27 |

28 | 29 | 30 | 31 |

32 |
33 | 34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/support/MultiFactorWebApplicationServiceFactory.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | import static net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService.AuthenticationMethodSource; 4 | import org.jasig.cas.authentication.principal.Response.ResponseType; 5 | 6 | /** 7 | * Factory abstraction for creating instances of 8 | * {@link net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService}. 9 | * 10 | * @author Dmitriy Kopylenko 11 | * @author Unicon inc. 12 | */ 13 | public interface MultiFactorWebApplicationServiceFactory { 14 | 15 | 16 | /** 17 | * Create an instance of {@link net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService}. 18 | * 19 | * @param id service id 20 | * @param originalUrl originalUrl 21 | * @param artifactId artifactId 22 | * @param authnMethod authentication method 23 | * @param responseType the request response type 24 | * @param authenticationMethodSource authentication method source 25 | * 26 | * @return an instance of {@link net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService} 27 | */ 28 | MultiFactorAuthenticationSupportingWebApplicationService create(String id, 29 | String originalUrl, 30 | String artifactId, 31 | ResponseType responseType, 32 | String authnMethod, 33 | AuthenticationMethodSource authenticationMethodSource); 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/MultiFactorAuthenticationRequestResolver.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import org.jasig.cas.authentication.Authentication; 4 | import org.jasig.cas.authentication.principal.Response.ResponseType; 5 | import org.jasig.cas.authentication.principal.WebApplicationService; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * A strategy interface for resolving requests for multifactor authentication from existing primary authentication data. 11 | * 12 | * Example implementations might use primary authenticated principal's attribute or some other piece of contextual data 13 | * available in passed in Authentication object instance. Implementations may choose to return multiple contexts. 14 | * 15 | * @author Dmitriy Kopylenko 16 | * @author Unicon inc. 17 | */ 18 | public interface MultiFactorAuthenticationRequestResolver { 19 | 20 | 21 | /** 22 | * Default attribute name for retrieving requested mfa authentication method. 23 | */ 24 | String DEFAULT_MFA_METHOD_ATTRIBUTE_NAME = "authn_method"; 25 | 26 | 27 | /** 28 | * Resolve potential {@link net.unicon.cas.mfa.authentication.MultiFactorAuthenticationRequestContext} from passed in primary 29 | * authentication instance, for a passed in target service. 30 | * 31 | * @param authentication primary authentication instance 32 | * @param targetService target service 33 | * @param responseType response type required by the service 34 | * 35 | * @return list instance of MultiFactorAuthenticationRequestContext or null if no mfa request can be resolved 36 | */ 37 | List resolve(Authentication authentication, WebApplicationService targetService, 38 | ResponseType responseType); 39 | } 40 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/support/DefaultMultiFactorWebApplicationServiceFactory.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | import org.jasig.cas.authentication.principal.Response.ResponseType; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.util.Assert; 7 | 8 | import static net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService.AuthenticationMethodSource; 9 | 10 | /** 11 | * Default implementation of MfaWebApplicationServiceFactory. 12 | * 13 | * @author Dmitriy Kopylenko 14 | * @author Unicon inc. 15 | */ 16 | public final class DefaultMultiFactorWebApplicationServiceFactory implements MultiFactorWebApplicationServiceFactory { 17 | 18 | private final Logger logger = LoggerFactory.getLogger(getClass()); 19 | 20 | @Override 21 | public MultiFactorAuthenticationSupportingWebApplicationService create(final String id, 22 | final String originalUrl, 23 | final String artifactId, 24 | final ResponseType responseType, 25 | final String authenticationMethod, 26 | final AuthenticationMethodSource authenticationMethodSource) { 27 | 28 | Assert.notNull(authenticationMethod, "authnMethod cannot be null."); 29 | Assert.notNull(authenticationMethodSource, "authenticationMethodSource cannot be null."); 30 | 31 | return new DefaultMultiFactorAuthenticationSupportingWebApplicationService( 32 | id, originalUrl, artifactId, responseType, 33 | authenticationMethod, authenticationMethodSource); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/support/MultiFactorAuthenticationSupportingWebApplicationService.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | import org.jasig.cas.authentication.principal.WebApplicationService; 4 | 5 | /** 6 | * The MultiFactorAuthenticationService is an extension of the generic CAS {@link WebApplicationService} 7 | * that defines the authentication method accepted by the service. 8 | * @author Misagh Moayyed 9 | */ 10 | public interface MultiFactorAuthenticationSupportingWebApplicationService extends WebApplicationService { 11 | /** Parameter name that defined the method of authentication. **/ 12 | String CONST_PARAM_AUTHN_METHOD = "authn_method"; 13 | 14 | /** 15 | * Parameter name that defines the HTTP method used to send the 16 | * authentication response back to a service. 17 | */ 18 | String CONST_PARAM_METHOD = "method"; 19 | 20 | /** 21 | * Parameter name that defines the service ticket to send back 22 | * to the service. 23 | */ 24 | String CONST_PARAM_TICKET = "ticket"; 25 | 26 | /** 27 | * Define the authentication method accepted and supported by this MFA service. 28 | * @return the supported method 29 | */ 30 | String getAuthenticationMethod(); 31 | 32 | /** 33 | * An authentication method source for this MFA service. 34 | * @return the source for the supported authentication method 35 | */ 36 | AuthenticationMethodSource getAuthenticationMethodSource(); 37 | 38 | 39 | /** 40 | * Enum type representing the type of authentication method source. 41 | */ 42 | enum AuthenticationMethodSource { 43 | /** Sourced from registered service attribute. */ 44 | REGISTERED_SERVICE_DEFINITION, 45 | 46 | /** Sourced from HTTP request param. */ 47 | REQUEST_PARAM, 48 | 49 | /** Sourced from principal attribute. */ 50 | PRINCIPAL_ATTRIBUTE 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/RequestedAuthenticationMethodRankingStrategy.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 4 | 5 | import java.util.Set; 6 | 7 | /** 8 | * Strategy interface for ranking requested authentication methods. 9 | * 10 | * @author Dmitriy Kopylenko 11 | * @author Unicon inc. 12 | */ 13 | public interface RequestedAuthenticationMethodRankingStrategy { 14 | 15 | /** 16 | * Calculates the highest ranking possible authentication method from the list of the requested ones. 17 | * 18 | * @param mfaTransaction mfa transaction encapsulating possible requested authentication methods 19 | * 20 | * @return mfa service representing the highest possible ranking authentication method or null 21 | * if implementations are unable to perform such calculation 22 | */ 23 | MultiFactorAuthenticationSupportingWebApplicationService computeHighestRankingAuthenticationMethod( 24 | MultiFactorAuthenticationTransactionContext mfaTransaction); 25 | 26 | /** 27 | * Determine if provided list of previously achieved authentication methods contains any one method stronger than 28 | * currently requested one. The algorithm and configuration of the ranking strength of the authentication methods 29 | * should be provided by implementations. 30 | * 31 | * @param previouslyAchievedAuthenticationMethods set of previously achieved authentication methods 32 | * @param requestedAuthenticationMethod requestedAuthenticationMethod 33 | * 34 | * @return true if list contains any methods stronger than requested one, and false otherwise 35 | */ 36 | boolean anyPreviouslyAchievedAuthenticationMethodsStrongerThanRequestedOne(Set previouslyAchievedAuthenticationMethods, 37 | String requestedAuthenticationMethod); 38 | } 39 | -------------------------------------------------------------------------------- /cas-mfa-overlay/etc/jetty/jetty-https.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | http/1.1 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /cas-mfa-overlay/etc/cas.properties: -------------------------------------------------------------------------------- 1 | server.name=https://mmoayyed.unicon.net:8443 2 | server.prefix=${server.name}/cas 3 | 4 | cas.securityContext.status.access=hasIpAddress('127.0.0.1') 5 | 6 | cas.securityContext.statistics.access=hasIpAddress('127.0.0.1') 7 | 8 | cas.themeResolver.defaultThemeName=cas-theme-default 9 | 10 | host.name=cas01.example.org 11 | 12 | st.timeToKillInSeconds=180 13 | 14 | log4j.config.location=file:/etc/cas/log4j2.xml 15 | 16 | slo.callbacks.disabled=true 17 | 18 | # screen.mfa.greeting.userAttribute=firstName 19 | 20 | ## 21 | # The principal attribute name to determine MFA authn flow 22 | # 23 | # mfa.method.userAttribute=authn_method 24 | 25 | ## 26 | # Destroy any previous SSO sessions prior to 27 | # initiating form-based authentication? 28 | # This is specially required if primary authn 29 | # needs be cached via something like clearPass. 30 | # mfa.destroy.prev.sso=false 31 | 32 | ## 33 | # Configuration file to host supported authn methods 34 | # 35 | # mfa.authn.methods.config.location=file:/etc/cas/authn-methods.conf 36 | 37 | ## 38 | # Default authentication method to use for relying parties and 39 | # services, if the service definition is unable to define the attribute 40 | # (if the default service registry is used for instance), 41 | # or if one should be forced globally on all 42 | # 43 | # mfa.default.authn.method= 44 | 45 | ## 46 | # The name of the MFA authn method attribute to include 47 | # in the CAS validation response. By default, this 48 | # is set to the value below and generally may be the same 49 | # as the name of the user attribute retrieved that decides 50 | # on MFA. 51 | # 52 | # mfa.method.response.attribute=authn_method 53 | 54 | #################################### 55 | # Duo security 2fa authentication provider 56 | # https://www.duosecurity.com/docs/duoweb#1.-generate-an-akey 57 | #################################### 58 | 59 | duo.api.host=api-d2e616a0.duosecurity.com 60 | duo.integration.key=DICLHRWL1KQK5EUAQP43 61 | duo.secret.key=kcroINPRyGUXNYhBGXG5i4RwSZDQC2f37ANCBOZN 62 | duo.application.key=u3IHBaREMB7Cb9S4QMISAgHycpj8lPBkDGfWt55I 63 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/RemoveHostnameInContextAction.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow; 2 | 3 | import net.unicon.cas.mfa.web.flow.util.MultiFactorRequestContextUtils; 4 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 5 | import org.springframework.webflow.action.AbstractAction; 6 | import org.springframework.webflow.execution.Event; 7 | import org.springframework.webflow.execution.RequestContext; 8 | 9 | /** 10 | * Check the context to see if the service available is the {@link #hostname}. 11 | * If it is, remove the service so a service ticket wouldn't be created. This 12 | * is only used in cases where a service is not specified upon logging into 13 | * CAS, and in order for principal attributes to evaluate the authentication 14 | * and step through mfa, a service that is the hostname, that is CAS itself 15 | * is specified in order to allow for mfa based on principal attributes. 16 | * Since this "dummy" service is only used for that particular sequence, 17 | * it needs to be removed from the context so further actions in the flow 18 | * don't consider it a valid service deserving of an ST. 19 | * @author Misagh Moayyed 20 | */ 21 | public final class RemoveHostnameInContextAction extends AbstractAction { 22 | private final String hostname; 23 | 24 | /** 25 | * Instantiates a new Check hostname in context action. 26 | * 27 | * @param hostname the hostname 28 | */ 29 | public RemoveHostnameInContextAction(final String hostname) { 30 | this.hostname = hostname; 31 | } 32 | 33 | @Override 34 | protected Event doExecute(final RequestContext context) throws Exception { 35 | final MultiFactorAuthenticationSupportingWebApplicationService svc = 36 | MultiFactorRequestContextUtils.getMultifactorWebApplicationService(context); 37 | if (svc != null && svc.getId().equals(this.hostname)) { 38 | MultiFactorRequestContextUtils.setMultifactorWebApplicationService(context, null); 39 | } 40 | 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cas-mfa-duo/src/main/groovy/net/unicon/cas/mfa/authentication/duo/DuoMultiFactorWebflowConfigurer.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication.duo; 2 | 3 | import org.jasig.cas.authentication.Credential; 4 | import org.jasig.cas.authentication.principal.PersonDirectoryPrincipalResolver; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.InitializingBean; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.context.WebApplicationContext; 11 | 12 | import javax.annotation.PostConstruct; 13 | import java.util.List; 14 | 15 | /** 16 | * Initialize the application context with the needed webflow mfa configuration 17 | * as much as possible to simplify adding mfa into an existing overlay. 18 | * 19 | * @author Misagh Moayyed 20 | */ 21 | @Component 22 | public final class DuoMultiFactorWebflowConfigurer implements InitializingBean { 23 | private static final Logger LOGGER = LoggerFactory.getLogger(DuoMultiFactorWebflowConfigurer.class); 24 | 25 | @Autowired 26 | private WebApplicationContext context; 27 | 28 | 29 | @PostConstruct 30 | @Override 31 | public void afterPropertiesSet() throws Exception { 32 | try { 33 | final List resolvers = this.context.getBean("mfaCredentialsToPrincipalResolvers", List.class); 34 | resolvers.add(0, new DuoCredentialsToPrincipalResolver()); 35 | } catch (final Exception e) { 36 | LOGGER.error(e.getMessage(), e); 37 | } 38 | } 39 | 40 | private static class DuoCredentialsToPrincipalResolver extends PersonDirectoryPrincipalResolver { 41 | @Override 42 | protected String extractPrincipalId(final Credential credential) { 43 | final DuoCredentials duoCredentials = (DuoCredentials) credential; 44 | return duoCredentials.getUsername(); 45 | } 46 | 47 | @Override 48 | public boolean supports(final Credential credentials) { 49 | return credentials != null && credentials instanceof DuoCredentials; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cas-mfa-duo/src/main/groovy/net/unicon/cas/mfa/authentication/duo/DuoCredentials.groovy: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication.duo 2 | 3 | import org.apache.commons.lang3.StringUtils 4 | import org.apache.commons.lang3.builder.EqualsBuilder 5 | import org.apache.commons.lang3.builder.HashCodeBuilder 6 | import org.apache.commons.lang3.builder.ToStringBuilder 7 | import org.jasig.cas.authentication.Credential 8 | 9 | class DuoCredentials implements Credential, Serializable { 10 | 11 | private String username; 12 | private String signedDuoResponse; 13 | 14 | @Override 15 | public String toString() { 16 | return new ToStringBuilder(this) 17 | .append("username", this.username) 18 | .toString(); 19 | } 20 | 21 | @Override 22 | public boolean equals(final Object obj) { 23 | if (!(obj instanceof DuoCredentials)) { 24 | return false; 25 | } 26 | if (obj == this) { 27 | return true; 28 | } 29 | final DuoCredentials other = (DuoCredentials) obj; 30 | final EqualsBuilder builder = new EqualsBuilder(); 31 | builder.append(this.username, other.username); 32 | return builder.isEquals(); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | final HashCodeBuilder builder = new HashCodeBuilder(97, 31); 38 | builder.append(this.username); 39 | return builder.toHashCode(); 40 | } 41 | 42 | @Override 43 | public String getId() { 44 | return this.username; 45 | } 46 | 47 | public String getSignedDuoResponse() { 48 | return signedDuoResponse; 49 | } 50 | 51 | public String getUsername() { 52 | return username; 53 | } 54 | 55 | 56 | public void setUsername(final String username) { 57 | this.username = username; 58 | } 59 | 60 | public void setSignedDuoResponse(final String signedDuoResponse) { 61 | this.signedDuoResponse = signedDuoResponse; 62 | } 63 | 64 | public boolean isValid() { 65 | return StringUtils.isNotBlank(this.username) && StringUtils.isNotBlank(this.signedDuoResponse); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/DefaultAuthenticationSupport.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import org.jasig.cas.authentication.Authentication; 4 | import org.jasig.cas.authentication.principal.Principal; 5 | import org.jasig.cas.ticket.TicketGrantingTicket; 6 | import org.jasig.cas.ticket.registry.TicketRegistry; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * Default implementation of AuthenticationSupport. 12 | *

13 | * Uses CAS' TicketRegistry to retrieve TGT and its associated objects by provided tgt String token 14 | * 15 | * @author Dmitriy Kopylenko 16 | * @author Unicon, inc. 17 | */ 18 | public class DefaultAuthenticationSupport implements AuthenticationSupport { 19 | 20 | /** 21 | * The Ticket registry. 22 | */ 23 | private TicketRegistry ticketRegistry; 24 | 25 | /** 26 | * Instantiates a new Default authentication support. 27 | * 28 | * @param ticketRegistry the ticket registry 29 | */ 30 | public DefaultAuthenticationSupport(final TicketRegistry ticketRegistry) { 31 | this.ticketRegistry = ticketRegistry; 32 | } 33 | 34 | @Override 35 | /** {@inheritDoc} */ 36 | public Authentication getAuthenticationFrom(final String ticketGrantingTicketId) throws RuntimeException { 37 | final TicketGrantingTicket tgt = this.ticketRegistry.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class); 38 | return tgt == null ? null : tgt.getAuthentication(); 39 | } 40 | 41 | @Override 42 | /** {@inheritDoc} */ 43 | public Principal getAuthenticatedPrincipalFrom(final String ticketGrantingTicketId) throws RuntimeException { 44 | final Authentication auth = getAuthenticationFrom(ticketGrantingTicketId); 45 | return auth == null ? null : auth.getPrincipal(); 46 | } 47 | 48 | @Override 49 | /** {@inheritDoc} */ 50 | public Map getPrincipalAttributesFrom(final String ticketGrantingTicketId) throws RuntimeException { 51 | final Principal principal = getAuthenticatedPrincipalFrom(ticketGrantingTicketId); 52 | return principal == null ? null : principal.getAttributes(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/StubAuthenticationMethodTranslator.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import net.unicon.cas.mfa.web.support.UnrecognizedAuthenticationMethodException; 4 | import org.jasig.cas.authentication.principal.WebApplicationService; 5 | 6 | import java.util.Collections; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | /** 11 | * A stub translator that receives its legend as a map. The key for the map 12 | * should be the set of received authentication methods, and the value is a single 13 | * string to define the new authentication method name. 14 | * 15 | * If no 16 | * @author Misagh Moayyed 17 | */ 18 | public class StubAuthenticationMethodTranslator implements AuthenticationMethodTranslator { 19 | private final Map, String> translationMap; 20 | 21 | private boolean ignoreIfNoMatchIsFound = true; 22 | 23 | /** 24 | * Instantiates a new Stub authentication method translator. 25 | */ 26 | public StubAuthenticationMethodTranslator() { 27 | this(Collections.EMPTY_MAP); 28 | } 29 | 30 | /** 31 | * Instantiates a new Sutb authentication method translator. 32 | * 33 | * @param translationMap the translation map 34 | */ 35 | public StubAuthenticationMethodTranslator(final Map, String> translationMap) { 36 | this.translationMap = translationMap; 37 | } 38 | 39 | public void setIgnoreIfNoMatchIsFound(final boolean ignoreIfNoMatchIsFound) { 40 | this.ignoreIfNoMatchIsFound = ignoreIfNoMatchIsFound; 41 | } 42 | 43 | @Override 44 | public String translate(final WebApplicationService targetService, final String receivedAuthenticationMethod) { 45 | final Set> keys = this.translationMap.keySet(); 46 | for (final Set keyset : keys) { 47 | if (keyset.contains(receivedAuthenticationMethod)) { 48 | return this.translationMap.get(keyset); 49 | } 50 | } 51 | 52 | if (this.ignoreIfNoMatchIsFound) { 53 | return receivedAuthenticationMethod; 54 | } 55 | throw new UnrecognizedAuthenticationMethodException(receivedAuthenticationMethod, targetService.getId()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/java/net/unicon/cas/mfa/authentication/DefaultCompositeAuthenticationTests.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 11 | 12 | import org.jasig.cas.authentication.principal.Principal; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.junit.runners.JUnit4; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.junit.Assert.*; 19 | 20 | @RunWith(JUnit4.class) 21 | public class DefaultCompositeAuthenticationTests { 22 | private static final String PARAM_NAME = MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD; 23 | 24 | private final CompositeAuthentication authentication; 25 | 26 | public DefaultCompositeAuthenticationTests() { 27 | 28 | final Map map = mock(Map.class); 29 | final Principal p = mock(Principal.class); 30 | when(p.getId()).thenReturn("casuser"); 31 | when(p.getAttributes()).thenReturn(map); 32 | 33 | final Map authnAttrs = mock(Map.class); 34 | this.authentication = new DefaultCompositeAuthentication(p, authnAttrs, new ArrayList(), new HashMap(), new HashMap()); 35 | } 36 | 37 | @Test 38 | public void testEmptyAuthenticationMethodAttribute() { 39 | final Map map = this.authentication.getAttributes(); 40 | when(map.containsKey(any(Object.class))).thenReturn(false); 41 | final Collection c = this.authentication.getSatisfiedAuthenticationMethods(); 42 | assertEquals(c.size(), 0); 43 | } 44 | 45 | @Test 46 | public void testSuccessfullySatisfiedValidAuthenticationMethods() { 47 | final Map map = this.authentication.getAttributes(); 48 | when(map.containsKey(any(Object.class))).thenReturn(true); 49 | 50 | final List list = Arrays.asList("first_method", "second_method"); 51 | when(map.get(PARAM_NAME)).thenReturn(list); 52 | 53 | final Collection c = this.authentication.getSatisfiedAuthenticationMethods(); 54 | assertEquals(c.size(), 2); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/event/ServiceAuthenticationMethodMultiFactorAuthenticationSpringWebflowEventBuilder.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow.event; 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 4 | import net.unicon.cas.mfa.web.support.UnrecognizedAuthenticationMethodException; 5 | import org.jasig.cas.web.support.WebUtils; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.webflow.definition.TransitionDefinition; 9 | import org.springframework.webflow.execution.Event; 10 | import org.springframework.webflow.execution.RequestContext; 11 | 12 | /** 13 | * Builds an mfa event based on the authentication method captured by the service. 14 | * @author Misagh Moayyed 15 | */ 16 | public class ServiceAuthenticationMethodMultiFactorAuthenticationSpringWebflowEventBuilder 17 | implements MultiFactorAuthenticationSpringWebflowEventBuilder { 18 | 19 | private final Logger logger = LoggerFactory.getLogger(getClass()); 20 | 21 | @Override 22 | public Event buildEvent(final RequestContext context) { 23 | final MultiFactorAuthenticationSupportingWebApplicationService service = (MultiFactorAuthenticationSupportingWebApplicationService) 24 | WebUtils.getService(context); 25 | 26 | logger.debug("Attempting to build an event based on the authentication method [{}] and service [{}]", 27 | service.getAuthenticationMethod(), service.getId()); 28 | final Event event = new Event(this, MFA_EVENT_ID_PREFIX + service.getAuthenticationMethod()); 29 | logger.debug("Resulting event id is [{}]. Locating transitions in the context for that event id...", 30 | event.getId()); 31 | 32 | final TransitionDefinition def = context.getMatchingTransition(event.getId()); 33 | if (def == null) { 34 | logger.warn("Transition definition cannot be found for event [{}]", event.getId()); 35 | throw new UnrecognizedAuthenticationMethodException(service.getAuthenticationMethod(), service.getId()); 36 | } 37 | logger.debug("Found matching transition [{}] with target [{}] for event {}. Will proceed normally..", 38 | def.getId(), def.getTargetStateId(), event.getId()); 39 | 40 | return event; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/CasMultiFactorApplicationContextAware.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import org.jasig.cas.web.support.ArgumentExtractor; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Qualifier; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.webflow.engine.builder.support.FlowBuilderServices; 11 | 12 | import javax.annotation.PostConstruct; 13 | import java.util.List; 14 | 15 | /** 16 | * Initialize the application context with the needed mfa configuration 17 | * as much as possible to simplify adding mfa into an existing overlay. 18 | * 19 | * @author Misagh Moayyed 20 | */ 21 | @Component 22 | public final class CasMultiFactorApplicationContextAware implements InitializingBean { 23 | private static final Logger LOGGER = LoggerFactory.getLogger(CasMultiFactorApplicationContextAware.class); 24 | 25 | @Autowired 26 | private FlowBuilderServices flowBuilderServices; 27 | 28 | @Autowired 29 | @Qualifier("mfaRequestsCollectingArgumentExtractor") 30 | private ArgumentExtractor mfaRequestsCollectingArgumentExtractor; 31 | 32 | @Override 33 | @PostConstruct 34 | public void afterPropertiesSet() throws Exception { 35 | try { 36 | LOGGER.debug("Configuring application context for multifactor authentication..."); 37 | addMultifactorArgumentExtractorConfiguration(); 38 | LOGGER.debug("Configured application context for multifactor authentication."); 39 | 40 | } catch (final Exception e) { 41 | LOGGER.error(e.getMessage(), e); 42 | } 43 | } 44 | 45 | /** 46 | * Add multifactor argument extractor configuration. 47 | */ 48 | private void addMultifactorArgumentExtractorConfiguration() { 49 | LOGGER.debug("Configuring application context with [{}]", 50 | mfaRequestsCollectingArgumentExtractor.getClass().getName()); 51 | 52 | final List list = this.flowBuilderServices.getApplicationContext().getBean("argumentExtractors", List.class); 53 | list.add(0, mfaRequestsCollectingArgumentExtractor); 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/AuthenticationSupport.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import org.jasig.cas.authentication.Authentication; 4 | import org.jasig.cas.authentication.principal.Principal; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * Helper strategy API to ease retrieving CAS' Authentication object and its associated components 10 | * from available CAS SSO String token called Ticket Granting Ticket (TGT). 11 | *

12 | *

Note: this API is only intended to be called by CAS server code e.g. any custom CAS server overlay extension, etc.

13 | *

14 | *

Concurrency semantics: implementations must be thread safe.

15 | * 16 | * @author Dmitriy Kopylenko 17 | * @author Unicon, inc. 18 | */ 19 | public interface AuthenticationSupport { 20 | 21 | /** 22 | * Retrieve a valid Authentication object identified by the provided TGT SSO token. 23 | * 24 | * @param ticketGrantingTicketId an SSO token identifying the requested Authentication 25 | * @return valid Authentication OR NULL if there is no valid SSO session present identified by the provided TGT id SSO token 26 | * @throws RuntimeException the runtime exception 27 | */ 28 | Authentication getAuthenticationFrom(String ticketGrantingTicketId) throws RuntimeException; 29 | 30 | /** 31 | * Retrieve a valid Principal object identified by the provided TGT SSO token. 32 | * 33 | * @param ticketGrantingTicketId an SSO token identifying the requested authenticated Principal 34 | * @return valid Principal OR NULL if there is no valid SSO session present identified by the provided TGT id SSO token 35 | * @throws RuntimeException the runtime exception 36 | */ 37 | Principal getAuthenticatedPrincipalFrom(String ticketGrantingTicketId) throws RuntimeException; 38 | 39 | /** 40 | * Retrieve a valid Principal's map of attributes identified by the provided TGT SSO token. 41 | * 42 | * @param ticketGrantingTicketId an SSO token identifying the requested authenticated Principal's attributes 43 | * @return valid Principal's attributes OR NULL if there is no valid SSO 44 | * ession present identified by the provided TGT id SSO token 45 | * @throws RuntimeException the runtime exception 46 | */ 47 | Map getPrincipalAttributesFrom(String ticketGrantingTicketId) throws RuntimeException; 48 | } 49 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/RegexAuthenticationMethodTranslator.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import net.unicon.cas.mfa.web.support.UnrecognizedAuthenticationMethodException; 4 | import org.jasig.cas.authentication.principal.WebApplicationService; 5 | 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.regex.Pattern; 9 | 10 | /** 11 | * A translator that will check a list of regex patterns and return an authentication method name. 12 | * 13 | * @author John Gasper 14 | */ 15 | public class RegexAuthenticationMethodTranslator implements AuthenticationMethodTranslator { 16 | private final Map translationMap; 17 | 18 | private String defaultMfaMethod = null; 19 | 20 | /** 21 | * Instantiates a new Regex authentication method translator. 22 | * 23 | * @param translationMap the regex/mfa method translation map (maybe an ordered map) 24 | */ 25 | public RegexAuthenticationMethodTranslator(final Map translationMap) { 26 | this(translationMap, null); 27 | } 28 | 29 | /** 30 | * Instantiates a new Regex authentication method translator. 31 | * 32 | * @param translationMap the regex/mfa method translation map (maybe an ordered map) 33 | * @param defaultMfaMethod the default MFA merhod to use if no match is found. 34 | */ 35 | public RegexAuthenticationMethodTranslator(final Map translationMap, final String defaultMfaMethod) { 36 | this.defaultMfaMethod = defaultMfaMethod; 37 | 38 | final Map optimizedMap = new LinkedHashMap<>(); 39 | 40 | for (final String pattern : translationMap.keySet()) { 41 | optimizedMap.put(Pattern.compile(pattern), translationMap.get(pattern)); 42 | } 43 | 44 | this.translationMap = optimizedMap; 45 | } 46 | 47 | @Override 48 | public String translate(final WebApplicationService targetService, final String triggerValue) { 49 | for (final Pattern pattern : translationMap.keySet()) { 50 | if (pattern.matcher(triggerValue).matches()) { 51 | return this.translationMap.get(pattern); 52 | } 53 | } 54 | 55 | if (this.defaultMfaMethod != null) { 56 | return defaultMfaMethod; 57 | } 58 | 59 | throw new UnrecognizedAuthenticationMethodException(triggerValue, targetService.getId()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/RememberAuthenticationMethodMetaDataPopulator.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 4 | import org.jasig.cas.authentication.AuthenticationBuilder; 5 | import org.jasig.cas.authentication.AuthenticationMetaDataPopulator; 6 | import org.jasig.cas.authentication.Credential; 7 | import org.jasig.cas.authentication.principal.Service; 8 | import org.jasig.cas.web.support.WebUtils; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.webflow.execution.RequestContext; 12 | import org.springframework.webflow.execution.RequestContextHolder; 13 | 14 | /** 15 | * Remember the authentication method used for the retrieved instance of 16 | * {@link MultiFactorAuthenticationSupportingWebApplicationService}. The 17 | * method will be placed into the authentication context as an attribute, 18 | * remembered by {@link MultiFactorAuthenticationSupportingWebApplicationService#CONST_PARAM_AUTHN_METHOD}. 19 | * @author Misagh Moayyed 20 | */ 21 | public class RememberAuthenticationMethodMetaDataPopulator implements AuthenticationMetaDataPopulator { 22 | 23 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 24 | 25 | @Override 26 | public void populateAttributes(final AuthenticationBuilder authenticationBuilder, final Credential credential) { 27 | final RequestContext context = RequestContextHolder.getRequestContext(); 28 | if (context != null) { 29 | final Service svc = WebUtils.getService(context); 30 | 31 | if (svc instanceof MultiFactorAuthenticationSupportingWebApplicationService) { 32 | final MultiFactorAuthenticationSupportingWebApplicationService mfaSvc = 33 | (MultiFactorAuthenticationSupportingWebApplicationService) svc; 34 | 35 | authenticationBuilder.addAttribute( 36 | MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD, 37 | mfaSvc.getAuthenticationMethod()); 38 | 39 | logger.debug("Captured authentication method [{}] into the authentication context", 40 | mfaSvc.getAuthenticationMethod()); 41 | } 42 | } 43 | } 44 | 45 | @Override 46 | public boolean supports(final Credential credential) { 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cas-mfa-web/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cas-mfa 5 | net.unicon 6 | 2.0.0-RC4-SNAPSHOT 7 | 8 | 4.0.0 9 | cas-mfa-web 10 | CAS MFA Web Support 11 | 12 | This is the multifactor extensions for CAS sub-module for Web components. Things that are not Java, 13 | that do not build into a .jar. For example: JSPs, deployment descriptors, customized and new 14 | Spring Web Flow XML and other Spring web framework configuration, etc. 15 | 16 | war 17 | 18 | 19 | 20 | 21 | org.apache.maven.plugins 22 | maven-war-plugin 23 | 24 | false 25 | 26 | 27 | 28 | cas-mfa-web 29 | 30 | 31 | 32 | 33 | net.unicon 34 | cas-mfa-java 35 | compile 36 | jar 37 | ${project.version} 38 | 39 | 40 | org.jasig.cas 41 | cas-server-webapp-support 42 | provided 43 | 44 | 45 | org.mockito 46 | mockito-core 47 | 48 | 49 | javax.servlet 50 | javax.servlet-api 51 | 52 | 53 | org.codehaus.groovy 54 | groovy 55 | 56 | 57 | org.spockframework 58 | spock-core 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /cas-mfa-overlay/etc/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/AuthenticationMethod.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import org.apache.commons.lang3.builder.CompareToBuilder; 5 | import org.apache.commons.lang3.builder.EqualsBuilder; 6 | import org.apache.commons.lang3.builder.HashCodeBuilder; 7 | import org.apache.commons.lang3.builder.ToStringBuilder; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * Defines an mfa authentication method 13 | * and its relevant properties. 14 | * 15 | * @author Misagh Moayyed 16 | */ 17 | public final class AuthenticationMethod implements Comparable, Serializable { 18 | private static final long serialVersionUID = -8960685427442975943L; 19 | 20 | @JsonProperty 21 | private final Integer rank; 22 | 23 | @JsonProperty 24 | private final String name; 25 | 26 | /** 27 | * Instantiates a new Authentication method. 28 | */ 29 | protected AuthenticationMethod() { 30 | this.rank = null; 31 | this.name = null; 32 | } 33 | 34 | /** 35 | * Instantiates a new Authentication method. 36 | * 37 | * @param rank the rank 38 | * @param name the name 39 | */ 40 | public AuthenticationMethod(final String name, final Integer rank) { 41 | this.rank = rank; 42 | this.name = name; 43 | } 44 | 45 | 46 | @Override 47 | public boolean equals(final Object obj) { 48 | if (obj == null) { 49 | return false; 50 | } 51 | if (obj == this) { 52 | return true; 53 | } 54 | if (obj.getClass() != getClass()) { 55 | return false; 56 | } 57 | final AuthenticationMethod rhs = (AuthenticationMethod) obj; 58 | return new EqualsBuilder() 59 | .append(this.name, rhs.name) 60 | .isEquals(); 61 | } 62 | 63 | 64 | @Override 65 | public int hashCode() { 66 | return new HashCodeBuilder() 67 | .append(name) 68 | .toHashCode(); 69 | } 70 | 71 | public Integer getRank() { 72 | return rank; 73 | } 74 | 75 | public String getName() { 76 | return name; 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return new ToStringBuilder(this) 82 | .append("rank", rank) 83 | .append("name", name) 84 | .toString(); 85 | } 86 | 87 | @Override 88 | public int compareTo(final Object o) { 89 | final AuthenticationMethod m = (AuthenticationMethod) o; 90 | return new CompareToBuilder().append(this.name, m.getName()).toComparison(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /cas-mfa-duo/src/main/groovy/net/unicon/cas/mfa/authentication/duo/DuoAuthenticationHandler.groovy: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication.duo 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.jasig.cas.MessageDescriptor 5 | import org.jasig.cas.authentication.Credential 6 | import org.jasig.cas.authentication.HandlerResult 7 | import org.jasig.cas.authentication.PreventedException 8 | import org.jasig.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler 9 | import org.jasig.cas.authentication.principal.Principal 10 | 11 | import javax.security.auth.login.FailedLoginException 12 | import java.security.GeneralSecurityException 13 | 14 | @Slf4j 15 | class DuoAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler { 16 | 17 | private final DuoAuthenticationService duoAuthenticationService 18 | 19 | DuoAuthenticationHandler(DuoAuthenticationService duoAuthenticationService) { 20 | this.duoAuthenticationService = duoAuthenticationService 21 | } 22 | 23 | @Override 24 | protected HandlerResult doAuthentication(final Credential credential) throws GeneralSecurityException, PreventedException { 25 | try { 26 | final DuoCredentials duoCredential = (DuoCredentials) credential; 27 | 28 | if (!duoCredential.isValid()) { 29 | throw new GeneralSecurityException("Duo credential validation failed. Ensure a username " 30 | + " and the signed Duo response is configured and passed. Credential received: " + duoCredential); 31 | } 32 | 33 | final String duoVerifyResponse = this.duoAuthenticationService.authenticate(duoCredential.getSignedDuoResponse()); 34 | logger.debug("Response from Duo verify: [{}]", duoVerifyResponse); 35 | final String primaryCredentialsUsername = duoCredential.getUsername(); 36 | 37 | final boolean isGoodAuthentication = duoVerifyResponse.equals(primaryCredentialsUsername); 38 | 39 | if (isGoodAuthentication) { 40 | logger.info("Successful Duo authentication for [{}]", primaryCredentialsUsername); 41 | 42 | final Principal principal = this.principalFactory.createPrincipal(duoVerifyResponse); 43 | return createHandlerResult(credential, principal, new ArrayList()); 44 | } 45 | throw new FailedLoginException("Duo authentication username " 46 | + primaryCredentialsUsername + " does not match Duo response: " + duoVerifyResponse); 47 | 48 | } catch (final Exception e) { 49 | logger.error(e.getMessage(), e); 50 | throw new FailedLoginException(e.getMessage()); 51 | } 52 | } 53 | 54 | @Override 55 | boolean supports(final Credential credential) { 56 | DuoCredentials.isAssignableFrom(credential.class) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/support/RequestParameterMultiFactorAuthenticationArgumentExtractor.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | import org.jasig.cas.authentication.principal.WebApplicationService; 4 | import org.jasig.cas.web.support.ArgumentExtractor; 5 | import org.springframework.util.StringUtils; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import java.util.List; 9 | 10 | import static net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService.AuthenticationMethodSource; 11 | 12 | /** 13 | * The multifactor authentication argument extractor, responsible to 14 | * instruct CAS with the constructed instance of a {@link WebApplicationService}. 15 | * 16 | * The requested authentication method discovery in this implementation is based on HTTP request parameter authn_method 17 | * 18 | * @author Misagh Moayyed 19 | * @author Dmitriy Kopylenko 20 | * @author Unicon inc. 21 | */ 22 | public final class RequestParameterMultiFactorAuthenticationArgumentExtractor extends 23 | AbstractMultiFactorAuthenticationArgumentExtractor { 24 | 25 | 26 | /** 27 | * Ctor. 28 | * 29 | * @param supportedArgumentExtractors supported protocols by argument extractors 30 | * @param mfaWebApplicationServiceFactory mfaWebApplicationServiceFactory 31 | * @param authenticationMethodVerifier authenticationMethodVerifier 32 | */ 33 | public RequestParameterMultiFactorAuthenticationArgumentExtractor(final List supportedArgumentExtractors, 34 | final MultiFactorWebApplicationServiceFactory mfaWebApplicationServiceFactory, 35 | final AuthenticationMethodVerifier authenticationMethodVerifier) { 36 | 37 | super(supportedArgumentExtractors, mfaWebApplicationServiceFactory, authenticationMethodVerifier); 38 | } 39 | 40 | @Override 41 | protected String getAuthenticationMethod(final HttpServletRequest request, final WebApplicationService targetService) { 42 | logger.debug("Attempting to extract multifactor authentication parameters from the request"); 43 | 44 | final String authenticationMethod = 45 | request.getParameter(MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD); 46 | 47 | if (!StringUtils.hasText(authenticationMethod)) { 48 | logger.debug("Request has no request parameter [{}]. Delegating to the next argument extractor in the chain...", 49 | MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD); 50 | return null; 51 | } 52 | return authenticationMethod; 53 | } 54 | 55 | @Override 56 | protected AuthenticationMethodSource getAuthenticationMethodSource() { 57 | return AuthenticationMethodSource.REQUEST_PARAM; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/java/net/unicon/cas/mfa/authentication/RegexAuthenticationMethodTranslatorTests.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import net.unicon.cas.mfa.web.support.DefaultMultiFactorAuthenticationSupportingWebApplicationService; 4 | import net.unicon.cas.mfa.web.support.UnrecognizedAuthenticationMethodException; 5 | import org.jasig.cas.authentication.principal.Response; 6 | import org.junit.Test; 7 | 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | 11 | 12 | import static org.junit.Assert.*; 13 | 14 | /** 15 | * Created by jgasper on 5/18/15. 16 | */ 17 | public class RegexAuthenticationMethodTranslatorTests { 18 | 19 | @Test 20 | public void testBasicTranslate() { 21 | final Map testMap = getLookupMap(); 22 | 23 | final RegexAuthenticationMethodTranslator regexAuthenticationMethodTranslator = new RegexAuthenticationMethodTranslator(testMap); 24 | assertEquals("mfa1", regexAuthenticationMethodTranslator.translate(null, "CN=Staff,OU=Groups,DC=example,DC=edu")); 25 | assertEquals("mfa2", regexAuthenticationMethodTranslator.translate(null, "CN=Students,OU=Groups,DC=example,DC=edu")); 26 | assertEquals("mfa3", regexAuthenticationMethodTranslator.translate(null, "CN=Others,OU=Groups,DC=example,DC=edu")); 27 | } 28 | 29 | @Test 30 | public void testDefaultMfa() { 31 | final Map testMap = getLookupMap(); 32 | 33 | final String result = "duo-strong"; 34 | 35 | final RegexAuthenticationMethodTranslator regexAuthenticationMethodTranslator = 36 | new RegexAuthenticationMethodTranslator(testMap, result); 37 | assertEquals(result, regexAuthenticationMethodTranslator.translate(null, "CN=sudoers,OU=AdminGroups,DC=example,DC=edu")); 38 | } 39 | 40 | @Test(expected = UnrecognizedAuthenticationMethodException.class) 41 | public void testTranslateException() { 42 | final DefaultMultiFactorAuthenticationSupportingWebApplicationService svc = 43 | new DefaultMultiFactorAuthenticationSupportingWebApplicationService("https://www.github.com", 44 | "https://www.github.com", null, Response.ResponseType.REDIRECT, "test_authn_method"); 45 | 46 | final Map testMap = getLookupMap(); 47 | 48 | final RegexAuthenticationMethodTranslator regexAuthenticationMethodTranslator = new RegexAuthenticationMethodTranslator(testMap); 49 | regexAuthenticationMethodTranslator.translate(svc, "CN=sudoers,OU=AdminGroups,DC=example,DC=edu"); 50 | } 51 | 52 | private static Map getLookupMap() { 53 | final Map testMap = new LinkedHashMap<>(); 54 | testMap.put("CN=Staff,OU=Groups,DC=example,DC=edu", "mfa1"); 55 | testMap.put("CN=Students,OU=Groups,DC=example,DC=edu", "mfa2"); 56 | testMap.put(".*,OU=Groups,DC=example,DC=edu", "mfa3"); 57 | return testMap; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/groovy/net/unicon/cas/mfa/authentication/OrderedMfaMethodRankingStrategyTests.groovy: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService 4 | import spock.lang.Subject 5 | 6 | import static net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService.AuthenticationMethodSource 7 | import spock.lang.Specification 8 | 9 | /** 10 | * 11 | * @author Dmitriy Kopylenko 12 | * @author Unicon inc. 13 | */ 14 | class OrderedMfaMethodRankingStrategyTests extends Specification { 15 | 16 | def mfaTransactionFixture = new MultiFactorAuthenticationTransactionContext("test service") 17 | .addMfaRequest(new MultiFactorAuthenticationRequestContext(Stub(MultiFactorAuthenticationSupportingWebApplicationService) { 18 | getId() >> 'test service' 19 | getAuthenticationMethodSource() >> AuthenticationMethodSource.REQUEST_PARAM 20 | 21 | }, 3)).addMfaRequest(new MultiFactorAuthenticationRequestContext(Stub(MultiFactorAuthenticationSupportingWebApplicationService) { 22 | getId() >> 'test service' 23 | getAuthenticationMethodSource() >> AuthenticationMethodSource.PRINCIPAL_ATTRIBUTE 24 | 25 | }, 1)).addMfaRequest(new MultiFactorAuthenticationRequestContext(Stub(MultiFactorAuthenticationSupportingWebApplicationService) { 26 | getId() >> 'test service' 27 | getAuthenticationMethodSource() >> AuthenticationMethodSource.REGISTERED_SERVICE_DEFINITION 28 | 29 | }, 2)) 30 | 31 | def "correct implementation of OrderedMfaMethodRankingStrategy#computeHighestRankingAuthenticationMethod"() { 32 | given: 33 | @Subject 34 | def rankingStrategyUnderTest = new OrderedMultiFactorMethodRankingStrategy(new JsonBackedAuthenticationMethodConfigurationProvider()) 35 | 36 | expect: 37 | rankingStrategyUnderTest.computeHighestRankingAuthenticationMethod(mfaTransactionFixture).authenticationMethodSource == AuthenticationMethodSource.PRINCIPAL_ATTRIBUTE 38 | } 39 | 40 | def "correct implementation of OrderedMfaMethodRankingStrategy#anyPreviouslyAchievedAuthenticationMethodsStrongerThanRequestedOne"() { 41 | given: 42 | def s1 = [new AuthenticationMethod("highest_factor",1), 43 | new AuthenticationMethod("lower_factor",2), 44 | new AuthenticationMethod("lowest_factor",3)] as Set 45 | 46 | def loader = new JsonBackedAuthenticationMethodConfigurationProvider(s1) 47 | @Subject 48 | def rankingStrategyUnderTest = new OrderedMultiFactorMethodRankingStrategy(loader) 49 | 50 | expect: 51 | rankingStrategyUnderTest.anyPreviouslyAchievedAuthenticationMethodsStrongerThanRequestedOne(['lower_factor', 'highest_factor'] as Set, 'lowest_factor') 52 | 53 | and: 54 | !rankingStrategyUnderTest.anyPreviouslyAchievedAuthenticationMethodsStrongerThanRequestedOne(['lowest_factor', 'lower_factor'] as Set, 'highest_factor') 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/view/Cas30ResponseView.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.view; 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 4 | import org.jasig.cas.CasProtocolConstants; 5 | import org.jasig.cas.authentication.principal.Service; 6 | import org.jasig.cas.services.RegisteredService; 7 | import org.springframework.web.servlet.view.AbstractUrlBasedView; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import java.util.Collections; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * This is {@link Cas30ResponseView}. 17 | * 18 | * @author Misagh Moayyed 19 | */ 20 | public class Cas30ResponseView extends org.jasig.cas.web.view.Cas30ResponseView { 21 | private String authenticationMethodResponseAttribute; 22 | 23 | /** 24 | * Instantiates a new Cas 30 response view. 25 | * 26 | * @param view the view 27 | */ 28 | protected Cas30ResponseView(final AbstractUrlBasedView view) { 29 | super(view); 30 | } 31 | 32 | @Override 33 | protected void prepareMergedOutputModel(final Map model, final HttpServletRequest request, 34 | final HttpServletResponse response) throws Exception { 35 | 36 | super.prepareMergedOutputModel(model, request, response); 37 | 38 | final Service service = super.getServiceFrom(model); 39 | final RegisteredService registeredService = this.servicesManager.findServiceBy(service); 40 | 41 | final Map attributes = new HashMap<>(getPrincipalAttributesAsMultiValuedAttributes(model)); 42 | attributes.put(CasProtocolConstants.VALIDATION_CAS_MODEL_ATTRIBUTE_NAME_AUTHENTICATION_DATE, 43 | Collections.singleton(getAuthenticationDate(model))); 44 | attributes.put(CasProtocolConstants.VALIDATION_CAS_MODEL_ATTRIBUTE_NAME_FROM_NEW_LOGIN, 45 | Collections.singleton(isAssertionBackedByNewLogin(model))); 46 | attributes.put(CasProtocolConstants.VALIDATION_REMEMBER_ME_ATTRIBUTE_NAME, 47 | Collections.singleton(isRememberMeAuthentication(model))); 48 | 49 | decideIfCredentialPasswordShouldBeReleasedAsAttribute(attributes, model, registeredService); 50 | decideIfProxyGrantingTicketShouldBeReleasedAsAttribute(attributes, model, registeredService); 51 | 52 | attributes.put(this.authenticationMethodResponseAttribute, 53 | getAuthenticationAttribute(model, MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD)); 54 | 55 | super.putIntoModel(model, 56 | CasProtocolConstants.VALIDATION_CAS_MODEL_ATTRIBUTE_NAME_ATTRIBUTES, 57 | this.casAttributeEncoder.encodeAttributes(attributes, getServiceFrom(model))); 58 | } 59 | 60 | public void setAuthenticationMethodResponseAttribute(final String authenticationMethodResponseAttribute) { 61 | this.authenticationMethodResponseAttribute = authenticationMethodResponseAttribute; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/java/net/unicon/cas/mfa/MultiFactorAuthenticationProtocolValidationSpecificationTests.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import net.unicon.cas.mfa.ticket.UnacceptableMultiFactorAuthenticationMethodException; 9 | import net.unicon.cas.mfa.ticket.UnrecognizedMultiFactorAuthenticationMethodException; 10 | 11 | import org.jasig.cas.authentication.Authentication; 12 | import org.jasig.cas.validation.Assertion; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.junit.runners.JUnit4; 16 | import org.mockito.Mock; 17 | import org.mockito.MockitoAnnotations; 18 | 19 | import static org.mockito.Mockito.*; 20 | 21 | @RunWith(JUnit4.class) 22 | public class MultiFactorAuthenticationProtocolValidationSpecificationTests { 23 | 24 | @Mock 25 | private Assertion assertion; 26 | 27 | @Mock 28 | private Authentication authentication; 29 | 30 | private final AbstractMultiFactorAuthenticationProtocolValidationSpecification spec; 31 | 32 | public MultiFactorAuthenticationProtocolValidationSpecificationTests() { 33 | MockitoAnnotations.initMocks(this); 34 | 35 | final List list = mock(List.class); 36 | when(list.size()).thenReturn(1); 37 | 38 | when(this.assertion.getChainedAuthentications()).thenReturn(list); 39 | when(list.get(anyInt())).thenReturn(this.authentication); 40 | final Map map = mock(Map.class); 41 | when(map.containsKey((any(Object.class)))).thenReturn(true); 42 | when(authentication.getAttributes()).thenReturn(map); 43 | 44 | this.spec = new AbstractMultiFactorAuthenticationProtocolValidationSpecification.WithoutProxy(); 45 | } 46 | 47 | @Test 48 | public void testDefaultSpec() { 49 | when(authentication.getAttributes().get(any(String.class))).thenReturn(null); 50 | spec.setAuthenticationMethod(null); 51 | assertTrue(this.spec.isSatisfiedBy(this.assertion)); 52 | } 53 | 54 | @Test 55 | public void testValidSpecWithAuthnMethods() { 56 | when(authentication.getAttributes().get(any(String.class))).thenReturn("strong_two_factor"); 57 | spec.setAuthenticationMethod("strong_two_factor"); 58 | assertTrue(this.spec.isSatisfiedBy(this.assertion)); 59 | } 60 | 61 | @Test(expected = UnacceptableMultiFactorAuthenticationMethodException.class) 62 | public void testUnacceptedSpec() { 63 | when(authentication.getAttributes().get(any(String.class))).thenReturn(null); 64 | spec.setAuthenticationMethod("strong_two_factor"); 65 | spec.isSatisfiedBy(this.assertion); 66 | } 67 | 68 | @Test(expected = UnrecognizedMultiFactorAuthenticationMethodException.class) 69 | public void testUnrecognizedSpec() { 70 | when(authentication.getAttributes().get(any(String.class))).thenReturn("strong_two_factor"); 71 | spec.setAuthenticationMethod("weak_two_factor"); 72 | spec.isSatisfiedBy(this.assertion); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/support/DefaultAuthenticationMethodVerifier.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | import net.unicon.cas.mfa.authentication.AuthenticationMethodConfigurationProvider; 4 | import org.jasig.cas.authentication.principal.WebApplicationService; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.servlet.http.HttpServletRequest; 9 | 10 | /** 11 | * Default implementation of {@link net.unicon.cas.mfa.web.support.AuthenticationMethodVerifier}. 12 | * 13 | * @author Dmitriy Kopylenko 14 | * @author Unicon inc. 15 | */ 16 | public final class DefaultAuthenticationMethodVerifier implements AuthenticationMethodVerifier { 17 | /** 18 | * The logger. 19 | */ 20 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 21 | 22 | /** 23 | * Supported authentication methods. 24 | */ 25 | private final AuthenticationMethodConfigurationProvider supportedAuthenticationMethodsConfig; 26 | 27 | /** 28 | * Ctor. 29 | * 30 | * @param authenticationMethodConfiguration list of supported authentication methods 31 | */ 32 | public DefaultAuthenticationMethodVerifier( 33 | final AuthenticationMethodConfigurationProvider authenticationMethodConfiguration) { 34 | this.supportedAuthenticationMethodsConfig = authenticationMethodConfiguration; 35 | } 36 | 37 | @Override 38 | public boolean verifyAuthenticationMethod(final String authenticationMethod, 39 | final WebApplicationService targetService, 40 | final HttpServletRequest request) { 41 | 42 | if (!supportedAuthenticationMethodsConfig.containsAuthenticationMethod(authenticationMethod)) { 43 | logger.debug("CAS is not configured to support [{}] authentication method value [{}]." 44 | + "The configuration of supported authentication methods is likely missing this method.", 45 | MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD, 46 | authenticationMethod); 47 | /** 48 | * Argument extractors are still going to be invoked, if the flow 49 | * decides to move the user experience to an error-view JSP. As such, 50 | * and since we are unable to touch request parameters removing the invalid 51 | * authn_method before that navigation takes place, there's a chance that an infinite 52 | * redirect loop might occur. The compromise here to is to "remember" that the exception 53 | * was handled once via a request attribute. 54 | */ 55 | if (request.getAttribute(UnrecognizedAuthenticationMethodException.class.getName()) == null) { 56 | request.setAttribute(UnrecognizedAuthenticationMethodException.class.getName(), Boolean.TRUE.toString()); 57 | throw new UnrecognizedAuthenticationMethodException(authenticationMethod, targetService.getId()); 58 | } 59 | return false; 60 | } 61 | return true; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/view/MultifactorLoginViewPrincipalAttributeGreeter.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow.view; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.jasig.cas.authentication.principal.Principal; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.binding.message.Message; 8 | import org.springframework.binding.message.MessageBuilder; 9 | import org.springframework.binding.message.MessageContext; 10 | import org.springframework.binding.message.MessageResolver; 11 | 12 | import java.util.Collection; 13 | 14 | /** 15 | * Greets the principal by a configurable principal attribute and falls back 16 | * to the principal id, if none is found. 17 | * @author Misagh Moayyed 18 | */ 19 | public final class MultifactorLoginViewPrincipalAttributeGreeter implements MultiFactorLoginViewPrincipalGreeter { 20 | 21 | private static final Logger LOGGER = LoggerFactory.getLogger(MultifactorLoginViewPrincipalAttributeGreeter.class); 22 | 23 | /** 24 | * MessageSource code. 25 | */ 26 | public static final String CODE = "screen.mfa.welcome.back"; 27 | 28 | private final String greetingAttributeName; 29 | 30 | /** 31 | * @param greetingAttrName attribute name 32 | */ 33 | public MultifactorLoginViewPrincipalAttributeGreeter(final String greetingAttrName) { 34 | this.greetingAttributeName = greetingAttrName; 35 | } 36 | 37 | @Override 38 | public String getPersonToGreet(final Principal p, final MessageContext messageContext) { 39 | 40 | String personId = p.getId(); 41 | final Object attrValue = p.getAttributes().get(this.greetingAttributeName); 42 | 43 | if (attrValue == null) { 44 | LOGGER.warn("No attribute value could be found for [{}]", this.greetingAttributeName); 45 | return p.getId(); 46 | } 47 | 48 | String greetingPersonId = attrValue.toString(); 49 | if (attrValue instanceof Collection) { 50 | final Collection col =((Collection) attrValue); 51 | if (!col.isEmpty()) { 52 | greetingPersonId = col.iterator().next().toString(); 53 | LOGGER.warn("Found multiple attribute values [{}] for [{}] to greet. Picked [{}]", 54 | attrValue, this.greetingAttributeName, 55 | greetingPersonId); 56 | } 57 | } 58 | 59 | if (!StringUtils.isBlank(greetingPersonId)) { 60 | personId = greetingPersonId; 61 | } 62 | 63 | final MessageResolver resolver = new MessageBuilder().source(CODE).info().code(CODE).arg(personId).build(); 64 | messageContext.addMessage(resolver); 65 | 66 | final Message[] messages = messageContext.getMessagesBySource(CODE); 67 | if (messages == null || messages.length == 0) { 68 | LOGGER.warn("The greeting message for principal [{}] could not be resolved by the " 69 | + "code [{}] in any of the configured message resource bundles. Falling back to principal id [{}]", 70 | p, CODE, p.getId()); 71 | return p.getId(); 72 | } 73 | return messages[0].getText(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /cas-mfa-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cas-mfa 5 | net.unicon 6 | 2.0.0-RC4-SNAPSHOT 7 | 8 | 4.0.0 9 | cas-mfa-java 10 | CAS MFA Java 11 | 12 | This module is intended to include all the Java you need to add to a CAS implementation 13 | to take advantage of the extended multifactor authentication features in this project. 14 | 15 | jar 16 | 17 | 18 | 19 | org.jasig.cas 20 | cas-server-webapp-support 21 | 22 | 23 | 24 | org.jasig.cas 25 | cas-server-core 26 | test-jar 27 | test 28 | 29 | 30 | 31 | org.jasig.cas 32 | cas-server-support-saml 33 | test 34 | 35 | 36 | 37 | org.springframework 38 | spring-core 39 | 40 | 41 | 42 | org.springframework 43 | spring-beans 44 | 45 | 46 | 47 | org.springframework 48 | spring-context-support 49 | 50 | 51 | 52 | org.springframework 53 | spring-context 54 | 55 | 56 | 57 | commons-io 58 | commons-io 59 | 60 | 61 | 62 | joda-time 63 | joda-time 64 | 65 | 66 | 67 | junit 68 | junit 69 | 70 | 71 | 72 | org.jasig.cas 73 | cas-server-core 74 | 75 | 76 | 77 | javax.servlet 78 | javax.servlet-api 79 | 80 | 81 | 82 | org.mockito 83 | mockito-core 84 | 85 | 86 | 87 | org.codehaus.groovy 88 | groovy 89 | 90 | 91 | 92 | org.spockframework 93 | spock-core 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/DefaultCompositeAuthentication.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import net.unicon.cas.mfa.util.MultiFactorUtils; 4 | import org.jasig.cas.authentication.CredentialMetaData; 5 | import org.jasig.cas.authentication.HandlerResult; 6 | import org.jasig.cas.authentication.principal.Principal; 7 | 8 | import java.util.Date; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | /** 14 | * A {@link CompositeAuthentication} implementation that houses an instance of 15 | * {@link Principal} inside, restricts the {@link #getAuthenticationDate()} to 16 | * the instance at which this authentication is created and exposes a mutable 17 | * instance of authentication attributes via {@link #getAttributes()}. 18 | * @author Misagh Moayyed 19 | */ 20 | public final class DefaultCompositeAuthentication implements CompositeAuthentication { 21 | 22 | private static final long serialVersionUID = 6594344317585898494L; 23 | 24 | private final Principal principal; 25 | private final Date authenticationDate = new Date(); 26 | private final Map authenticationAttributes; 27 | private final List credentials; 28 | private final Map successes; 29 | private final Map> failures; 30 | 31 | /** 32 | * Initialize this instance with a principal and given authentication attributes. 33 | * 34 | * @param p the principal 35 | * @param attributes attributes for this authentication 36 | * @param credentials the credentials 37 | * @param successes the successes 38 | * @param failures the failures 39 | */ 40 | public DefaultCompositeAuthentication(final Principal p, final Map attributes, 41 | final List credentials, 42 | final Map successes, 43 | final Map> failures) { 44 | this.principal = p; 45 | this.authenticationAttributes = attributes; 46 | this.credentials = credentials; 47 | this.successes = successes; 48 | this.failures = failures; 49 | } 50 | 51 | @Override 52 | public Principal getPrincipal() { 53 | return this.principal; 54 | } 55 | 56 | @Override 57 | public Date getAuthenticationDate() { 58 | return authenticationDate; 59 | } 60 | 61 | @Override 62 | public Map getAttributes() { 63 | return this.authenticationAttributes; 64 | } 65 | 66 | @Override 67 | public List getCredentials() { 68 | return this.credentials; 69 | } 70 | 71 | @Override 72 | public Map getSuccesses() { 73 | return this.successes; 74 | } 75 | 76 | @Override 77 | public Map> getFailures() { 78 | return this.failures; 79 | } 80 | 81 | @Override 82 | public Set getSatisfiedAuthenticationMethods() { 83 | return MultiFactorUtils.getSatisfiedAuthenticationMethods(this); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/JsonBackedAuthenticationMethodConfigurationProvider.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.apache.commons.io.FileUtils; 5 | import org.springframework.core.io.Resource; 6 | 7 | import java.io.IOException; 8 | import java.util.Iterator; 9 | import java.util.Set; 10 | import java.util.TreeSet; 11 | 12 | /** 13 | * Loads authentication methods and their rank from an external configuration file 14 | * that is expected to be JSON. The ranking of authentication methods is 15 | * determined by the {@link RequestedAuthenticationMethodRankingStrategy}. 16 | * 17 | *

Example configuration: 18 | *


19 |  [ {
20 |     "rank" : 1,
21 |      "name" : "duo_two_factor"
22 |      }, {
23 |      "rank" : 2,
24 |      "name" : "strong_two_factor"
25 |      }, {
26 |      "rank" : 3,
27 |      "name" : "sample_two_factor"
28 |  } ]
29 |  * 
30 | * @author Misagh Moayyed 31 | */ 32 | public final class JsonBackedAuthenticationMethodConfigurationProvider implements AuthenticationMethodConfigurationProvider { 33 | 34 | private final Set authnMethods; 35 | 36 | private final ObjectMapper objectMapper = new ObjectMapper(); 37 | 38 | /** 39 | * Instantiates a new Authentication method loader. 40 | * Loads supported authentication methods from 41 | * the specified resource. 42 | * @param configuration the configuration 43 | * @throws IOException the iO exception 44 | */ 45 | public JsonBackedAuthenticationMethodConfigurationProvider(final Resource configuration) throws IOException { 46 | this.authnMethods = new TreeSet<>(); 47 | final String json = FileUtils.readFileToString(configuration.getFile()); 48 | final Set set = this.objectMapper.readValue(json, Set.class); 49 | for (final Iterator it = set.iterator(); it.hasNext();) { 50 | final AuthenticationMethod method = this.objectMapper.convertValue(it.next(), AuthenticationMethod.class); 51 | this.authnMethods.add(method); 52 | } 53 | } 54 | 55 | /** 56 | * Instantiates a new Authentication method loader. 57 | * Populates the supported authn methods with the given set. 58 | * 59 | * @param authnMethods the authn methods 60 | */ 61 | public JsonBackedAuthenticationMethodConfigurationProvider(final Set authnMethods) { 62 | this.authnMethods = authnMethods; 63 | } 64 | 65 | /** 66 | * Instantiates a new Authentication method loader. 67 | */ 68 | public JsonBackedAuthenticationMethodConfigurationProvider() { 69 | this.authnMethods = new TreeSet<>(); 70 | } 71 | 72 | /** {@inheritDoc} **/ 73 | @Override 74 | public boolean containsAuthenticationMethod(final String name) { 75 | return getAuthenticationMethod(name) != null; 76 | } 77 | 78 | /** {@inheritDoc} **/ 79 | @Override 80 | public AuthenticationMethod getAuthenticationMethod(final String name) { 81 | for (final Iterator it = this.authnMethods.iterator(); it.hasNext();) { 82 | final AuthenticationMethod f = it.next(); 83 | if (f.getName().equals(name)) { 84 | return f; 85 | } 86 | } 87 | return null; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cas-mfa-web/src/main/webapp/WEB-INF/spring-configuration/mfaArgumentExtractorsConfiguration.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 24 | 25 | 26 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 47 | 49 | 50 | 55 | 56 | -------------------------------------------------------------------------------- /cas-mfa-web/src/test/java/net/unicon/cas/mfa/web/flow/view/MultifactorLoginViewPrincipalAttributeGreeterTests.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow.view; 2 | 3 | import org.jasig.cas.authentication.principal.DefaultPrincipalFactory; 4 | import org.jasig.cas.authentication.principal.Principal; 5 | import org.jasig.cas.authentication.principal.PrincipalFactory; 6 | import org.junit.Test; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | import org.springframework.binding.message.Message; 10 | import org.springframework.binding.message.MessageContext; 11 | import org.springframework.binding.message.Severity; 12 | 13 | import java.util.Arrays; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | import static org.junit.Assert.assertTrue; 18 | import static org.mockito.Mockito.any; 19 | import static org.mockito.Mockito.when; 20 | 21 | public class MultifactorLoginViewPrincipalAttributeGreeterTests { 22 | 23 | private PrincipalFactory principalFactory = new DefaultPrincipalFactory(); 24 | 25 | @Mock 26 | private MessageContext messageContext; 27 | 28 | @Mock 29 | private Message msg; 30 | 31 | public MultifactorLoginViewPrincipalAttributeGreeterTests() { 32 | MockitoAnnotations.initMocks(this); 33 | 34 | when(msg.getSeverity()).thenReturn(Severity.INFO); 35 | when(msg.getSource()).thenReturn(MultifactorLoginViewPrincipalAttributeGreeter.CODE); 36 | final Message[] values = new Message[1]; 37 | values[0] = msg; 38 | 39 | when(messageContext.getMessagesBySource(any(Object.class))).thenReturn(values); 40 | } 41 | 42 | @Test 43 | public void testValidPrincipalMultivaluedAttributeToGreet() { 44 | final Map map = new HashMap(); 45 | map.put("firstName", Arrays.asList("cas", "sso")); 46 | map.put("lastName", "user"); 47 | 48 | final Principal p = principalFactory.createPrincipal("userid", map); 49 | 50 | final MultifactorLoginViewPrincipalAttributeGreeter greeter = new MultifactorLoginViewPrincipalAttributeGreeter( 51 | "firstName"); 52 | 53 | configureMessageContextForPrincipal("cas"); 54 | final String value = greeter.getPersonToGreet(p, this.messageContext); 55 | assertTrue(value.contains("cas")); 56 | } 57 | 58 | @Test 59 | public void testValidPrincipalAttributeToGreet() { 60 | final Map map = new HashMap(); 61 | map.put("firstName", "cas"); 62 | map.put("lastName", "user"); 63 | 64 | final Principal p = principalFactory.createPrincipal("userid", map); 65 | 66 | final MultifactorLoginViewPrincipalAttributeGreeter greeter = new MultifactorLoginViewPrincipalAttributeGreeter( 67 | "firstName"); 68 | 69 | configureMessageContextForPrincipal("cas"); 70 | final String value = greeter.getPersonToGreet(p, this.messageContext); 71 | assertTrue(value.contains("cas")); 72 | } 73 | 74 | @Test 75 | public void testInvalidPrincipalAttributeToGreet() { 76 | final Map map = new HashMap(); 77 | final Principal p = principalFactory.createPrincipal("userid", map); 78 | 79 | final MultifactorLoginViewPrincipalAttributeGreeter greeter = new MultifactorLoginViewPrincipalAttributeGreeter( 80 | "firstName"); 81 | configureMessageContextForPrincipal(p.getId()); 82 | final String value = greeter.getPersonToGreet(p, this.messageContext); 83 | assertTrue(value.contains(p.getId())); 84 | } 85 | 86 | private void configureMessageContextForPrincipal(final String expectedId) { 87 | when(msg.getText()).thenReturn("Welcome " + expectedId); 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/MultiFactorAuthenticationRequestContext.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | import org.apache.commons.lang3.builder.ToStringStyle; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.core.Ordered; 10 | 11 | import javax.validation.constraints.Min; 12 | import javax.validation.constraints.NotNull; 13 | import java.io.Serializable; 14 | 15 | /** 16 | * Represents a single mfa request by wrapping 17 | * {@link net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService}. 18 | * Adds implementations of {@code equals} and {@code hashcode} to ensure the uniqueness of 19 | * one mfa method per service per request origination source. 20 | * Implements {@link org.springframework.core.Ordered} to assist implementations of 21 | * {@link net.unicon.cas.mfa.authentication.RequestedAuthenticationMethodRankingStrategy} do 22 | * the ranking if they choose to use this abstraction. 23 | * 24 | * @author Dmitriy Kopylenko 25 | * @author Unicon inc. 26 | */ 27 | public final class MultiFactorAuthenticationRequestContext implements Serializable, Ordered { 28 | 29 | private static final Logger LOGGER = LoggerFactory.getLogger(MultiFactorAuthenticationRequestContext.class); 30 | 31 | private static final long serialVersionUID = 3895119051289676064L; 32 | 33 | private final MultiFactorAuthenticationSupportingWebApplicationService mfaService; 34 | 35 | private final int rank; 36 | 37 | /** 38 | * Ctor. Treats zero or negative rank as undefined 39 | * 40 | * @param mfaService target mfa service 41 | * @param rank the rank value of this request 42 | */ 43 | public MultiFactorAuthenticationRequestContext(@NotNull final MultiFactorAuthenticationSupportingWebApplicationService mfaService, 44 | @Min(1) final int rank) { 45 | this.mfaService = mfaService; 46 | this.rank = rank; 47 | } 48 | 49 | public MultiFactorAuthenticationSupportingWebApplicationService getMfaService() { 50 | return this.mfaService; 51 | } 52 | 53 | @Override 54 | public int getOrder() { 55 | return this.rank; 56 | } 57 | 58 | @Override 59 | public boolean equals(final Object o) { 60 | if (this == o) { 61 | return true; 62 | } 63 | if (o == null || getClass() != o.getClass()) { 64 | return false; 65 | } 66 | 67 | final MultiFactorAuthenticationRequestContext that = (MultiFactorAuthenticationRequestContext) o; 68 | return this.getMfaService().equals(that.getMfaService()); 69 | } 70 | 71 | @Override 72 | public int hashCode() { 73 | final HashCodeBuilder builder = new HashCodeBuilder(13, 133); 74 | return builder.append(this.mfaService.getAuthenticationMethod()) 75 | .append(this.mfaService.getId()) 76 | .append(this.mfaService.getAuthenticationMethodSource()) 77 | .toHashCode(); 78 | } 79 | 80 | @Override 81 | public String toString() { 82 | final ToStringBuilder builder = new ToStringBuilder(ToStringStyle.DEFAULT_STYLE); 83 | return builder.append(this.mfaService.getId()) 84 | .append(this.mfaService.getAuthenticationMethod()) 85 | .append(this.mfaService.getAuthenticationMethodSource()) 86 | .toString(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/groovy/net/unicon/cas/mfa/authentication/MultiFactorAuthenticationTransactionContextTests.groovy: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService 4 | import org.jasig.cas.authentication.Authentication 5 | import spock.lang.Specification 6 | import spock.lang.Subject 7 | import static net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService.AuthenticationMethodSource 8 | 9 | /** 10 | * 11 | * @author Dmitriy Kopylenko 12 | * @author Unicon inc. 13 | */ 14 | class MultiFactorAuthenticationTransactionContextTests extends Specification { 15 | 16 | def mfaReqViaParam = new MultiFactorAuthenticationRequestContext(Stub(MultiFactorAuthenticationSupportingWebApplicationService) { 17 | getId() >> 'test service' 18 | getAuthenticationMethodSource() >> AuthenticationMethodSource.REQUEST_PARAM 19 | }, 3) 20 | 21 | def mfaReqViaRegSvc = new MultiFactorAuthenticationRequestContext(Stub(MultiFactorAuthenticationSupportingWebApplicationService) { 22 | getId() >> 'test service' 23 | getAuthenticationMethodSource() >> AuthenticationMethodSource.REGISTERED_SERVICE_DEFINITION 24 | }, 2) 25 | 26 | def mfaReqViaPrincipalAttr = new MultiFactorAuthenticationRequestContext(Stub(MultiFactorAuthenticationSupportingWebApplicationService) { 27 | getId() >> 'second service trying to sneak in somehow' 28 | getAuthenticationMethodSource() >> AuthenticationMethodSource.PRINCIPAL_ATTRIBUTE 29 | }, 1) 30 | 31 | 32 | def "creating an instance with a default constructor results in null primary authentication and empty mfa requests collection"() { 33 | given: 34 | @Subject 35 | def authnTxCtxUnderTest = new MultiFactorAuthenticationTransactionContext('test service') 36 | 37 | expect: 38 | !authnTxCtxUnderTest.primaryAuthentication 39 | 40 | and: 41 | authnTxCtxUnderTest.mfaRequests.size() == 0 42 | } 43 | 44 | def "fluid API works as expected"() { 45 | given: 46 | @Subject 47 | def authnTxCtxUnderTest = new MultiFactorAuthenticationTransactionContext('test service') 48 | .setPrimaryAuthentication(Mock(Authentication)).addMfaRequest(mfaReqViaParam).addMfaRequest(mfaReqViaRegSvc) 49 | 50 | expect: 51 | authnTxCtxUnderTest.primaryAuthentication 52 | 53 | and: 54 | authnTxCtxUnderTest.mfaRequests.size() == 2 55 | } 56 | 57 | def "no duplicate authentication method source are allowed"() { 58 | given: 59 | @Subject 60 | def authnTxCtxUnderTest = new MultiFactorAuthenticationTransactionContext('test service').addMfaRequest(mfaReqViaParam).addMfaRequest(mfaReqViaParam) 61 | 62 | expect: 63 | authnTxCtxUnderTest.mfaRequests.size() == 1 64 | } 65 | 66 | def "unmodifiable Set is returned for mfaRequests property"() { 67 | given: 68 | @Subject 69 | def authnTxCtxUnderTest = new MultiFactorAuthenticationTransactionContext('test service').addMfaRequest(mfaReqViaParam) 70 | 71 | when: 72 | authnTxCtxUnderTest.mfaRequests << mfaReqViaRegSvc 73 | 74 | then: 75 | thrown(UnsupportedOperationException) 76 | } 77 | 78 | def "only single target service is allowed for all mfa requests"() { 79 | when: 80 | @Subject 81 | def authnTxCtxUnderTest = new MultiFactorAuthenticationTransactionContext('test service') 82 | .addMfaRequest(mfaReqViaParam) 83 | .addMfaRequest(mfaReqViaPrincipalAttr) 84 | 85 | then: 86 | thrown(IllegalArgumentException) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cas-mfa-duo-web/src/main/webapp/WEB-INF/cas-servlet-mfa-duo-two-factor.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 31 | 32 | 33 | 36 | 37 | 39 | 40 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /cas-mfa-duo-web/src/main/webapp/js/duo/Duo-Web-v2.min.js: -------------------------------------------------------------------------------- 1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{var o=t();o._onReady(o.init);e.Duo=o}})(this,function(){var e=/^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;var t=/^ERR\|[\w\s\.\(\)]+$/;var o=/^DUO_OPEN_WINDOW\|/;var n=["duo.com","duosecurity.com","duomobile.s3-us-west-1.amazonaws.com"];var i="duo_iframe",a="",r="sig_response",s,u,f,d,m,c;function l(e,t){throw new Error("Duo Web SDK error: "+e+(t?"\n"+"See "+t+" for more information":""))}function h(e){return e.replace(/([a-z])([A-Z])/,"$1-$2").toLowerCase()}function p(e,t){if("dataset"in e){return e.dataset[t]}else{return e.getAttribute("data-"+h(t))}}function g(e,t,o,n){if("addEventListener"in window){e.addEventListener(t,n,false)}else{e.attachEvent(o,n)}}function w(e,t,o,n){if("removeEventListener"in window){e.removeEventListener(t,n,false)}else{e.detachEvent(o,n)}}function v(e){g(document,"DOMContentLoaded","onreadystatechange",e)}function _(e){w(document,"DOMContentLoaded","onreadystatechange",e)}function b(e){g(window,"message","onmessage",e)}function E(e){w(window,"message","onmessage",e)}function y(e){if(!e){return}if(e.indexOf("ERR|")===0){l(e.split("|")[1])}if(e.indexOf(":")===-1||e.split(":").length!==2){l("Duo was given a bad token. This might indicate a configuration "+"problem with one of Duo's client libraries.","https://www.duosecurity.com/docs/duoweb#first-steps")}var t=e.split(":");u=e;f=t[0];d=t[1];return{sigRequest:e,duoSig:t[0],appSig:t[1]}}function D(){m=document.getElementById(i);if(!m){throw new Error("This page does not contain an iframe for Duo to use."+'Add an element like '+"to this page. "+"See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe "+"for more information.")}q();_(D)}function O(n){return Boolean(n.origin==="https://"+s&&typeof n.data==="string"&&(n.data.match(e)||n.data.match(t)||n.data.match(o)))}function R(e){if(e){if(e.host){s=e.host}if(e.sig_request){y(e.sig_request)}if(e.post_action){a=e.post_action}if(e.post_argument){r=e.post_argument}if(e.iframe){if(e.iframe.tagName){m=e.iframe}else if(typeof e.iframe==="string"){i=e.iframe}}if(typeof e.submit_callback==="function"){c=e.submit_callback}}if(m){q()}else{m=document.getElementById(i);if(m){q()}else{v(D)}}_(R)}function A(e){if(O(e)){if(e.data.match(o)){var t=e.data.substring("DUO_OPEN_WINDOW|".length);if(L(t)){window.open(t,"_self")}}else{B(e.data);E(A)}}}function L(e){if(!e){return false}var t=document.createElement("a");t.href=e;if(t.protocol==="duotrustedendpoints:"){return true}else if(t.protocol!=="https:"){return false}for(var o=0;o 2 | 22 | 28 | 29 | Controls the generation of the unique identifiers for tickets. You most likely do not need to modify these. Though you may need to 30 | modify 31 | the SAML ticket id generator. 32 | 33 | 34 | 35 | 36 | 40 | 42 | 43 | 44 | 45 | 49 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 62 | 66 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 79 | 82 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /cas-mfa-duo-web/src/main/webapp/WEB-INF/webflow/mfa-duo-two-factor/mfa-duo-two-factor-webflow.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 26 | 27 | 28 | 30 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/groovy/net/unicon/cas/mfa/authentication/principal/PrincipalAttributeMultiFactorAuthenticationRequestResolverTests.groovy: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication.principal 2 | 3 | import groovy.util.logging.Slf4j 4 | import net.unicon.cas.mfa.authentication.AuthenticationMethod 5 | import net.unicon.cas.mfa.authentication.JsonBackedAuthenticationMethodConfigurationProvider 6 | import net.unicon.cas.mfa.web.support.DefaultMultiFactorWebApplicationServiceFactory 7 | import net.unicon.cas.mfa.web.support.MultiFactorWebApplicationServiceFactory 8 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService 9 | import org.jasig.cas.authentication.principal.Response 10 | 11 | import static net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService.AuthenticationMethodSource 12 | import org.jasig.cas.authentication.Authentication 13 | import org.jasig.cas.authentication.principal.Principal 14 | import org.jasig.cas.authentication.principal.SimpleWebApplicationServiceImpl 15 | import org.jasig.cas.authentication.principal.WebApplicationService 16 | import spock.lang.Shared 17 | import spock.lang.Specification 18 | import spock.lang.Subject 19 | import spock.lang.Unroll 20 | 21 | /** 22 | * 23 | * @author Dmitriy Kopylenko 24 | * @author Unicon inc. 25 | */ 26 | @Slf4j 27 | class PrincipalAttributeMultiFactorAuthenticationRequestResolverTests extends Specification { 28 | 29 | static WebApplicationService targetService = new SimpleWebApplicationServiceImpl('test target service') 30 | 31 | @Shared 32 | def authenticationWithValidPrincipalAttributeFor_strong_two_factor = Stub(Authentication) { 33 | getPrincipal() >> Stub(Principal) { 34 | getId() >> 'test principal' 35 | getAttributes() >> [authn_method: ['strong_two_factor', 'lower_factor']] 36 | } 37 | } 38 | 39 | @Shared 40 | def authenticationWithoutPrincipalAttributeFor_strong_two_factor = Stub(Authentication) { 41 | getPrincipal() >> Stub(Principal) { 42 | getAttributes() >> [:] 43 | } 44 | } 45 | 46 | @Shared 47 | MultiFactorWebApplicationServiceFactory mfaWebApplicationServiceFactory = new DefaultMultiFactorWebApplicationServiceFactory(); 48 | @Subject 49 | def s1 = [new AuthenticationMethod("strong_two_factor",1), 50 | new AuthenticationMethod("lower_factor",2), 51 | new AuthenticationMethod("lowest_factor",3)] as Set 52 | 53 | def loader = new JsonBackedAuthenticationMethodConfigurationProvider(s1) 54 | 55 | def mfaAuthnReqResolverUnderTest = 56 | new PrincipalAttributeMultiFactorAuthenticationRequestResolver(mfaWebApplicationServiceFactory, loader) 57 | 58 | @Unroll 59 | def "either authentication OR service OR both null arguments OR no authn_method principal attribute SHOULD result in empty list"() { 60 | expect: 61 | mfaAuthnReqResolverUnderTest.resolve(authn, svc, Response.ResponseType.REDIRECT).size() == 0 62 | 63 | where: 64 | authn | svc 65 | null | null 66 | authenticationWithValidPrincipalAttributeFor_strong_two_factor | null 67 | null | targetService 68 | authenticationWithoutPrincipalAttributeFor_strong_two_factor | targetService 69 | 70 | } 71 | 72 | def "correct MultiFactorAuthenticationRequestContext returned when valid service is passed and a principal attribute 'authn_method'"() { 73 | given: 74 | 75 | def mfaReq = mfaAuthnReqResolverUnderTest.resolve(authenticationWithValidPrincipalAttributeFor_strong_two_factor, targetService, 76 | Response.ResponseType.REDIRECT) 77 | def mfaContext = mfaReq.get(0); 78 | 79 | log.warn(mfaContext.mfaService.getId()) 80 | expect: 81 | mfaContext.mfaService.id == 'test target service' 82 | 83 | and: 84 | mfaContext.mfaService.authenticationMethod == 'strong_two_factor' 85 | 86 | and: 87 | mfaContext.mfaService.authenticationMethodSource == AuthenticationMethodSource.PRINCIPAL_ATTRIBUTE 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/OrderedMultiFactorMethodRankingStrategy.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 4 | import org.springframework.core.annotation.AnnotationAwareOrderComparator; 5 | import org.springframework.util.Assert; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | /** 13 | * Ranking strategy implementation that utilizes {@link org.springframework.core.Ordered} abstraction 14 | * of {@link net.unicon.cas.mfa.authentication.MultiFactorAuthenticationRequestContext}. 15 | * 16 | * @author Dmitriy Kopylenko 17 | * @author Unicon, inc. 18 | */ 19 | public class OrderedMultiFactorMethodRankingStrategy implements RequestedAuthenticationMethodRankingStrategy { 20 | 21 | /** 22 | * The authn method loader. 23 | */ 24 | private final AuthenticationMethodConfigurationProvider authenticationMethodConfiguration; 25 | 26 | /** 27 | * Ctor. 28 | * 29 | * @param authenticationMethodConfiguration the authentication method loader 30 | */ 31 | public OrderedMultiFactorMethodRankingStrategy(final AuthenticationMethodConfigurationProvider authenticationMethodConfiguration) { 32 | this.authenticationMethodConfiguration = authenticationMethodConfiguration; 33 | } 34 | 35 | @Override 36 | public MultiFactorAuthenticationSupportingWebApplicationService 37 | computeHighestRankingAuthenticationMethod(@NotNull final MultiFactorAuthenticationTransactionContext mfaTransaction) { 38 | final List sortedRequests = 39 | new ArrayList<>(mfaTransaction.getMfaRequests()); 40 | 41 | AnnotationAwareOrderComparator.sort(sortedRequests); 42 | return sortedRequests.get(0).getMfaService(); 43 | } 44 | 45 | /** 46 | * Sort the list of requests. 47 | * 48 | * @param sortedRequests the sorted requests 49 | * @return the list 50 | */ 51 | protected static List sortRequests( 52 | final List sortedRequests) { 53 | AnnotationAwareOrderComparator.sort(sortedRequests); 54 | return sortedRequests; 55 | } 56 | 57 | @Override 58 | public boolean anyPreviouslyAchievedAuthenticationMethodsStrongerThanRequestedOne( 59 | final Set previouslyAchievedAuthenticationMethods, final String requestedAuthenticationMethod) { 60 | 61 | Assert.notNull(previouslyAchievedAuthenticationMethods); 62 | Assert.notNull(requestedAuthenticationMethod); 63 | 64 | if (previouslyAchievedAuthenticationMethods.isEmpty()) { 65 | return false; 66 | } 67 | 68 | final Integer requestedRank = getRank(requestedAuthenticationMethod); 69 | Integer prevRank; 70 | for (final String prevMethod : previouslyAchievedAuthenticationMethods) { 71 | prevRank = getRank(prevMethod); 72 | //Lower rank value == stronger (higher order) 73 | //We also treat equal ranks as 'not stronger' 74 | if (prevRank <= requestedRank) { 75 | return true; 76 | } 77 | } 78 | return false; 79 | } 80 | 81 | /** 82 | * Retrieve rank value from the internal Map instance variable for the provided mfa method key. 83 | * 84 | * @param mfaMethod key to retrieve the rank value for 85 | * 86 | * @return rank value 87 | * 88 | * @throws IllegalStateException if the Map is mis-configured i.e. does not hold valid (mfaMethod -> rank) configuration data. 89 | * This is totally a config/deployment error as opposed to external input validation error. 90 | */ 91 | private Integer getRank(final String mfaMethod) { 92 | final Integer rank = this.authenticationMethodConfiguration.getAuthenticationMethod(mfaMethod).getRank(); 93 | if (rank == null) { 94 | throw new IllegalStateException("The [mfaRankingConfig] Map is mis-configured. It does not have a ranking value mapping for the" 95 | + " [" + mfaMethod + "] authentication method."); 96 | } 97 | return rank; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/SendTicketGrantingTicketAction.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow; 2 | 3 | import net.unicon.cas.mfa.authentication.AuthenticationSupport; 4 | import net.unicon.cas.mfa.authentication.principal.MultiFactorCredentials; 5 | import net.unicon.cas.mfa.web.flow.util.MultiFactorRequestContextUtils; 6 | import org.jasig.cas.CentralAuthenticationService; 7 | import org.jasig.cas.web.support.CookieRetrievingCookieGenerator; 8 | import org.jasig.cas.web.support.WebUtils; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.webflow.action.AbstractAction; 12 | import org.springframework.webflow.execution.Event; 13 | import org.springframework.webflow.execution.RequestContext; 14 | 15 | import javax.validation.constraints.NotNull; 16 | 17 | /** 18 | * This is {@link SendTicketGrantingTicketAction} that mimics the default component in CAS 19 | * with one key difference: it will only destroy the previous TGT issued if there is no MFA 20 | * context available. 21 | * 22 | * If a TGT is issued as part of primary authn first without without going through MFA, 23 | * that TGT will remain in the context. Subsequent requests that are MFA-aware will create 24 | * new MFA-aware TGTs with their associated authentications. But, we will not be able to kill 25 | * the previous TGT because that may have cached the credentials as part of the original primary authn 26 | * and invalidating it will cause issues for extensions such as clearPass. 27 | * 28 | * The default behavior is that of CAS which assumes to caching of principal credential. 29 | * Deployments that require that type of caching will need to disable destroying the primary authn TGT 30 | * via {@link #setDestroyPreviousSSOSession(boolean)}. 31 | * 32 | * @author Misagh Moayyed 33 | */ 34 | public final class SendTicketGrantingTicketAction extends AbstractAction { 35 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 36 | 37 | @NotNull 38 | private CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator; 39 | 40 | /** Instance of CentralAuthenticationService. */ 41 | @NotNull 42 | private CentralAuthenticationService centralAuthenticationService; 43 | 44 | private AuthenticationSupport authenticationSupport; 45 | 46 | private boolean destroyPreviousSSOSession = true; 47 | 48 | @Override 49 | protected Event doExecute(final RequestContext context) { 50 | 51 | final MultiFactorCredentials mfa = MultiFactorRequestContextUtils.getMfaCredentials(context); 52 | 53 | final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); 54 | final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId"); 55 | 56 | if (ticketGrantingTicketId == null) { 57 | return success(); 58 | } 59 | 60 | this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils 61 | .getHttpServletResponse(context), ticketGrantingTicketId); 62 | 63 | if ((mfa == null || this.destroyPreviousSSOSession) 64 | && ticketGrantingTicketValueFromCookie != null 65 | && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) { 66 | logger.debug("Destroying the previous SSO session mapped to [{}] because, this is not an MFA request," 67 | + " or configuration dictated destroying the SSO session.", ticketGrantingTicketValueFromCookie); 68 | this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie); 69 | } 70 | 71 | return success(); 72 | } 73 | 74 | public void setTicketGrantingTicketCookieGenerator(final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator) { 75 | this.ticketGrantingTicketCookieGenerator= ticketGrantingTicketCookieGenerator; 76 | } 77 | 78 | public void setCentralAuthenticationService( 79 | final CentralAuthenticationService centralAuthenticationService) { 80 | this.centralAuthenticationService = centralAuthenticationService; 81 | } 82 | 83 | public void setAuthenticationSupport(final AuthenticationSupport authenticationSupport) { 84 | this.authenticationSupport = authenticationSupport; 85 | } 86 | 87 | public void setDestroyPreviousSSOSession(final boolean destroyPreviousSSOSession) { 88 | this.destroyPreviousSSOSession = destroyPreviousSSOSession; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/flow/TerminatingMultiFactorAuthenticationViaFormAction.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.flow; 2 | 3 | import net.unicon.cas.mfa.authentication.AuthenticationSupport; 4 | import net.unicon.cas.mfa.authentication.MultiFactorAuthenticationRequestResolver; 5 | import net.unicon.cas.mfa.authentication.RequestedAuthenticationMethodRankingStrategy; 6 | import net.unicon.cas.mfa.authentication.principal.MultiFactorCredentials; 7 | import net.unicon.cas.mfa.web.flow.util.MultiFactorRequestContextUtils; 8 | import net.unicon.cas.mfa.web.support.AuthenticationMethodVerifier; 9 | import org.jasig.cas.authentication.Authentication; 10 | import org.jasig.cas.authentication.Credential; 11 | import org.jasig.cas.ticket.TicketGrantingTicket; 12 | import org.jasig.cas.web.support.WebUtils; 13 | import org.springframework.binding.message.MessageContext; 14 | import org.springframework.webflow.execution.Event; 15 | import org.springframework.webflow.execution.FlowSession; 16 | import org.springframework.webflow.execution.RequestContext; 17 | 18 | /** 19 | * This is the final webflow action in the mfa authentication sequence that 20 | * would ultimately issue the TGT and presents the "success" event. If multiple 21 | * actions are chained during the authentication sequence, this should be the last. 22 | * 23 | * @author Misagh Moayyed 24 | */ 25 | public class TerminatingMultiFactorAuthenticationViaFormAction extends AbstractMultiFactorAuthenticationViaFormAction { 26 | 27 | /** 28 | * Ctor. 29 | * 30 | * @param multiFactorAuthenticationRequestResolver multiFactorAuthenticationRequestResolver 31 | * @param authenticationSupport authenticationSupport 32 | * @param authenticationMethodVerifier authenticationMethodVerifier 33 | * @param authenticationMethodRankingStrategy authenticationMethodRankingStrategy 34 | * @param hostname the hostname 35 | */ 36 | public TerminatingMultiFactorAuthenticationViaFormAction( 37 | final MultiFactorAuthenticationRequestResolver multiFactorAuthenticationRequestResolver, 38 | final AuthenticationSupport authenticationSupport, 39 | final AuthenticationMethodVerifier authenticationMethodVerifier, 40 | final RequestedAuthenticationMethodRankingStrategy authenticationMethodRankingStrategy, 41 | final String hostname) { 42 | 43 | super(multiFactorAuthenticationRequestResolver, authenticationSupport, 44 | authenticationMethodVerifier, authenticationMethodRankingStrategy, hostname); 45 | } 46 | 47 | /* {@inheritDoc} */ 48 | @Override 49 | protected final Event multiFactorAuthenticationSuccessful(final Authentication authentication, final RequestContext context, 50 | final Credential credentials, final MessageContext messageContext, 51 | final String id) throws Exception { 52 | return createTicketGrantingTicket(authentication, context, credentials, messageContext, id); 53 | } 54 | 55 | /** 56 | * Creates the ticket granting ticket. 57 | * 58 | * @param authentication the authentication 59 | * @param context the context 60 | * @param credentials the credentials 61 | * @param messageContext the message context 62 | * @param id the id 63 | * @return the event 64 | * @throws Exception the exception 65 | */ 66 | private Event createTicketGrantingTicket(final Authentication authentication, final RequestContext context, 67 | final Credential credentials, final MessageContext messageContext, 68 | final String id) throws Exception { 69 | 70 | final MultiFactorCredentials mfa = MultiFactorRequestContextUtils.getMfaCredentials(context); 71 | 72 | mfa.addAuthenticationToChain(authentication); 73 | mfa.getChainedCredentials().put(id, credentials); 74 | 75 | MultiFactorRequestContextUtils.setMfaCredentials(context, mfa); 76 | 77 | final TicketGrantingTicket tgt = this.cas.createTicketGrantingTicket(mfa); 78 | WebUtils.putTicketGrantingTicketInScopes(context, tgt); 79 | final FlowSession session = context.getFlowExecutionContext().getActiveSession(); 80 | logger.debug("Located active webflow session {}", session.getDefinition().getId()); 81 | session.getParent().getScope().put("ticketGrantingTicketId", tgt.getId()); 82 | return getSuccessEvent(context); 83 | 84 | } 85 | 86 | 87 | /* {@inheritDoc} */ 88 | @Override 89 | protected final Event doAuthentication(final RequestContext context, final Credential credentials, 90 | final MessageContext messageContext, final String id) throws Exception { 91 | return super.getErrorEvent(context); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/util/MultiFactorUtils.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.util; 2 | 3 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.jasig.cas.authentication.Authentication; 6 | import org.jasig.cas.validation.Assertion; 7 | 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | import java.util.Collections; 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | 16 | /** 17 | * Utility methods to ease implementation of multifactor behavior. 18 | * @author Misagh Moayyed 19 | */ 20 | public final class MultiFactorUtils { 21 | /** 22 | * Private constructor. 23 | */ 24 | private MultiFactorUtils() { 25 | } 26 | 27 | /** 28 | * Generate the string the indicates the list of satisfied authentication methods. 29 | * Methods are separated by a space. 30 | * @param assertion the assertion carrying the methods. 31 | * @return the space-delimited list of authentication methods, or null if none is available 32 | */ 33 | public static String getFulfilledAuthenticationMethodsAsString(final Assertion assertion) { 34 | final Authentication authentication = getAuthenticationFromAssertion(assertion); 35 | return getFulfilledAuthenticationMethodsAsString(authentication); 36 | } 37 | 38 | /** 39 | * Generate the string the indicates the list of satisfied authentication methods. 40 | * Methods are separated by a space. 41 | * @param authentication the authentication carrying the methods. 42 | * @return the space-delimited list of authentication methods, or null if none is available 43 | */ 44 | public static String getFulfilledAuthenticationMethodsAsString(final Authentication authentication) { 45 | final Set previouslyAchievedAuthenticationMethods = getSatisfiedAuthenticationMethods(authentication); 46 | if (!previouslyAchievedAuthenticationMethods.isEmpty()) { 47 | return StringUtils.join(previouslyAchievedAuthenticationMethods, " "); 48 | } 49 | return null; 50 | } 51 | 52 | /** 53 | * Convert the object given into a {@link Collection} instead. 54 | * @param obj the object to convert into a collection 55 | * @return The collection instance containing the object provided 56 | */ 57 | @SuppressWarnings("unchecked") 58 | public static Set convertValueToCollection(final Object obj) { 59 | final Set c = new HashSet<>(); 60 | 61 | if (obj instanceof Collection) { 62 | c.addAll((Collection) obj); 63 | } else if (obj instanceof Map) { 64 | throw new UnsupportedOperationException(Map.class.getCanonicalName() + " is not supoorted"); 65 | } else if (obj.getClass().isArray()) { 66 | for (final Object object : (Object[]) obj) { 67 | c.add(object); 68 | } 69 | } else { 70 | c.add(obj); 71 | } 72 | return c; 73 | } 74 | 75 | /** 76 | * Retrieves the collection of authentication methods available in the list 77 | * of authentication attributes. The authentication attribute that refers to the set of methods satisfied is 78 | * by the name of {@link MultiFactorAuthenticationSupportingWebApplicationService#CONST_PARAM_AUTHN_METHOD}. 79 | * 80 | * @param authentication the authentication that houses the methods. 81 | * @return collection of fulfilled authentication methods 82 | */ 83 | public static Set getSatisfiedAuthenticationMethods(final Authentication authentication) { 84 | if (authentication.getAttributes().containsKey(MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD)) { 85 | final Object methods = authentication.getAttributes().get( 86 | MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD); 87 | if (methods != null) { 88 | final Set valuesAsACollection = convertValueToCollection(methods); 89 | return new HashSet<>(Arrays.asList(valuesAsACollection.toArray(new String[]{}))); 90 | } 91 | } 92 | return Collections.emptySet(); 93 | } 94 | 95 | 96 | /** 97 | * Gets authentication from assertionfinal. 98 | * 99 | * @param assertion the assertion 100 | * @return the authentication from assertionfinal 101 | */ 102 | public static Authentication getAuthenticationFromAssertion(final Assertion assertion) { 103 | final List chainedAuthentications = assertion.getChainedAuthentications(); 104 | if (!chainedAuthentications.isEmpty()) { 105 | final int index = chainedAuthentications.size() - 1; 106 | return chainedAuthentications.get(index); 107 | } 108 | return null; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /cas-mfa-duo/src/main/java/com/duosecurity/DuoWeb.java: -------------------------------------------------------------------------------- 1 | package com.duosecurity; 2 | 3 | import java.io.IOException; 4 | import java.security.InvalidKeyException; 5 | import java.security.NoSuchAlgorithmException; 6 | 7 | public final class DuoWeb { 8 | private static final String DUO_PREFIX = "TX"; 9 | private static final String APP_PREFIX = "APP"; 10 | private static final String AUTH_PREFIX = "AUTH"; 11 | 12 | private static final int DUO_EXPIRE = 300; 13 | private static final int APP_EXPIRE = 3600; 14 | 15 | private static final int IKEY_LEN = 20; 16 | private static final int SKEY_LEN = 40; 17 | private static final int AKEY_LEN = 40; 18 | 19 | public static final String ERR_USER = "ERR|The username passed to sign_request() is invalid."; 20 | public static final String ERR_IKEY = "ERR|The Duo integration key passed to sign_request() is invalid."; 21 | public static final String ERR_SKEY = "ERR|The Duo secret key passed to sign_request() is invalid."; 22 | public static final String ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least " + AKEY_LEN + " characters."; 23 | public static final String ERR_UNKNOWN = "ERR|An unknown error has occurred."; 24 | public static final String INVALID_RESPONSE = "Invalid response"; 25 | 26 | private DuoWeb() {} 27 | 28 | public static String signRequest(final String ikey, final String skey, final String akey, final String username) { 29 | final String duoSig; 30 | final String appSig; 31 | 32 | if (username.equals("")) { 33 | return ERR_USER; 34 | } 35 | if (username.indexOf('|') != -1) { 36 | return ERR_USER; 37 | } 38 | if (ikey.equals("") || ikey.length() != IKEY_LEN) { 39 | return ERR_IKEY; 40 | } 41 | if (skey.equals("") || skey.length() != SKEY_LEN) { 42 | return ERR_SKEY; 43 | } 44 | if (akey.equals("") || akey.length() < AKEY_LEN) { 45 | return ERR_AKEY; 46 | } 47 | 48 | try { 49 | duoSig = signVals(skey, username, ikey, DUO_PREFIX, DUO_EXPIRE); 50 | appSig = signVals(akey, username, ikey, APP_PREFIX, APP_EXPIRE); 51 | } catch (final Exception e) { 52 | return ERR_UNKNOWN; 53 | } 54 | 55 | return duoSig + ":" + appSig; 56 | } 57 | 58 | public static String verifyResponse(final String ikey, final String skey, final String akey, final String sigResponse) 59 | throws DuoWebException, NoSuchAlgorithmException, InvalidKeyException, IOException { 60 | String authUser; 61 | String appUser; 62 | 63 | final String[] sigs = sigResponse.split(":"); 64 | final String authSig = sigs[0]; 65 | final String appSig = sigs[1]; 66 | 67 | authUser = parseVals(skey, authSig, AUTH_PREFIX, ikey); 68 | appUser = parseVals(akey, appSig, APP_PREFIX, ikey); 69 | 70 | if (!authUser.equals(appUser)) { 71 | throw new DuoWebException("Authentication failed."); 72 | } 73 | 74 | return authUser; 75 | } 76 | 77 | private static String signVals(final String key, final String username, final String ikey, final String prefix, final int expire) 78 | throws InvalidKeyException, NoSuchAlgorithmException { 79 | final long ts = System.currentTimeMillis() / 1000; 80 | final long expireTs = ts + expire; 81 | final String exp = Long.toString(expireTs); 82 | 83 | final String val = username + "|" + ikey + "|" + exp; 84 | final String cookie = prefix + "|" + Base64.encodeBytes(val.getBytes()); 85 | final String sig = Util.hmacSign(key, cookie); 86 | 87 | return cookie + "|" + sig; 88 | } 89 | 90 | private static String parseVals(final String key, final String val, final String prefix, final String ikey) 91 | throws InvalidKeyException, NoSuchAlgorithmException, IOException, DuoWebException { 92 | final long ts = System.currentTimeMillis() / 1000; 93 | 94 | final String[] parts = val.split("\\|"); 95 | if (parts.length != 3) { 96 | throw new DuoWebException(INVALID_RESPONSE); 97 | } 98 | 99 | final String uPrefix = parts[0]; 100 | final String uB64 = parts[1]; 101 | final String uSig = parts[2]; 102 | 103 | final String sig = Util.hmacSign(key, uPrefix + "|" + uB64); 104 | if (!Util.hmacSign(key, sig).equals(Util.hmacSign(key, uSig))) { 105 | throw new DuoWebException("Invalid response"); 106 | } 107 | 108 | if (!uPrefix.equals(prefix)) { 109 | throw new DuoWebException("Invalid response"); 110 | } 111 | 112 | final byte[] decoded = Base64.decode(uB64); 113 | final String cookie = new String(decoded); 114 | 115 | final String[] cookieParts = cookie.split("\\|"); 116 | if (cookieParts.length != 3) { 117 | throw new DuoWebException("Invalid response"); 118 | } 119 | final String username = cookieParts[0]; 120 | final String uIkey = cookieParts[1]; 121 | final String expire = cookieParts[2]; 122 | 123 | if (!uIkey.equals(ikey)) { 124 | throw new DuoWebException("Invalid response"); 125 | } 126 | 127 | final long expireTs = Long.parseLong(expire); 128 | if (ts >= expireTs) { 129 | throw new DuoWebException("Transaction has expired. Please check that the system time is correct."); 130 | } 131 | 132 | return username; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/web/support/MultiFactorAuthenticationRequestsCollectingArgumentExtractor.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.web.support; 2 | 3 | import net.unicon.cas.mfa.authentication.AuthenticationMethod; 4 | import net.unicon.cas.mfa.authentication.AuthenticationMethodConfigurationProvider; 5 | import net.unicon.cas.mfa.authentication.MultiFactorAuthenticationRequestContext; 6 | import net.unicon.cas.mfa.authentication.MultiFactorAuthenticationTransactionContext; 7 | import org.jasig.cas.authentication.principal.WebApplicationService; 8 | import org.jasig.cas.web.support.ArgumentExtractor; 9 | import org.springframework.webflow.execution.RequestContextHolder; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import java.util.Set; 13 | 14 | /** 15 | * Composite argument extractor that collects and aggregates all possible mfa requests 16 | * (from different sources e.g. request param, registered service attribute), encapsulates them in 17 | * {@link net.unicon.cas.mfa.authentication.MultiFactorAuthenticationTransactionContext} and binds it to the SWF's conversation scope 18 | * under the {@code MultiFactorAuthenticationTransactionContext.class#getSimpleName} key. 19 | * 20 | * @author Dmitriy Kopylenko 21 | * @author Unicon inc. 22 | */ 23 | public final class MultiFactorAuthenticationRequestsCollectingArgumentExtractor implements ArgumentExtractor { 24 | 25 | 26 | /** 27 | * A set of delegate mfa argument extractors. 28 | */ 29 | private final Set mfaArgumentExstractors; 30 | 31 | /** 32 | * Configuration of authn methods loaded. 33 | */ 34 | private final AuthenticationMethodConfigurationProvider authenticationMethodConfiguration; 35 | 36 | /** 37 | * The Authentication method verifier. 38 | */ 39 | private final AuthenticationMethodVerifier authenticationMethodVerifier; 40 | 41 | /** 42 | * Ctor. 43 | * 44 | * @param mfaArgumentExstractors delegate argument extractors 45 | * @param authenticationMethodConfiguration the authn methods config 46 | * @param authenticationMethodVerifier the authentication method verifier 47 | */ 48 | public MultiFactorAuthenticationRequestsCollectingArgumentExtractor( 49 | final Set mfaArgumentExstractors, 50 | final AuthenticationMethodConfigurationProvider authenticationMethodConfiguration, 51 | final AuthenticationMethodVerifier authenticationMethodVerifier) { 52 | 53 | this.mfaArgumentExstractors = mfaArgumentExstractors; 54 | this.authenticationMethodConfiguration = authenticationMethodConfiguration; 55 | this.authenticationMethodVerifier = authenticationMethodVerifier; 56 | } 57 | 58 | @Override 59 | public WebApplicationService extractService(final HttpServletRequest request) { 60 | MultiFactorAuthenticationTransactionContext mfaTxCtx = null; 61 | 62 | for (final AbstractMultiFactorAuthenticationArgumentExtractor extractor : this.mfaArgumentExstractors) { 63 | final MultiFactorAuthenticationSupportingWebApplicationService service = 64 | MultiFactorAuthenticationSupportingWebApplicationService.class.cast(extractor.extractService(request)); 65 | 66 | if (service != null 67 | && this.authenticationMethodVerifier.verifyAuthenticationMethod(service.getAuthenticationMethod(), service, request)) { 68 | 69 | final AuthenticationMethod method = 70 | this.authenticationMethodConfiguration.getAuthenticationMethod(service.getAuthenticationMethod()); 71 | if (mfaTxCtx != null) { 72 | mfaTxCtx.addMfaRequest(createMfaRequest(service, method.getRank())); 73 | } else { 74 | mfaTxCtx = new MultiFactorAuthenticationTransactionContext( 75 | service.getId()).addMfaRequest(createMfaRequest(service, method.getRank())); 76 | } 77 | 78 | } 79 | } 80 | 81 | if (mfaTxCtx != null) { 82 | //This is not unit testable (well in Java anyway, but would be possible if this class was written in Groovy), 83 | // but it's the only way to reach into the SWF context from here, 84 | //and since there is no desire to use HttpServletRequest attribute to get this object out. 85 | RequestContextHolder.getRequestContext().getConversationScope() 86 | .put(MultiFactorAuthenticationTransactionContext.class.getSimpleName(), mfaTxCtx); 87 | } 88 | //Always return null as we have collected all the mfa requests 89 | return null; 90 | } 91 | 92 | /** 93 | * Helper to create mfa requests. 94 | * 95 | * @param service mfa service 96 | * @param mfaMethodRank mfa source rank value 97 | * 98 | * @return mfa request 99 | */ 100 | private static MultiFactorAuthenticationRequestContext createMfaRequest( 101 | final MultiFactorAuthenticationSupportingWebApplicationService service, final int mfaMethodRank) { 102 | return new MultiFactorAuthenticationRequestContext(service, mfaMethodRank); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /cas-mfa-java/src/test/java/net/unicon/cas/mfa/authentication/principal/MultiFactorCredentialsTests.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication.principal; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.HashSet; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | import net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService; 11 | 12 | import org.jasig.cas.authentication.Authentication; 13 | import org.jasig.cas.authentication.principal.DefaultPrincipalFactory; 14 | import org.jasig.cas.authentication.principal.Principal; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.junit.runners.JUnit4; 18 | 19 | import static org.junit.Assert.*; 20 | import static org.mockito.Mockito.*; 21 | 22 | @RunWith(JUnit4.class) 23 | public class MultiFactorCredentialsTests { 24 | 25 | @Test(expected = UnknownPrincipalMatchException.class) 26 | public void testMultifactorMismatchedPrincipals() { 27 | 28 | final Principal firstPrincipal = new DefaultPrincipalFactory().createPrincipal("casuser"); 29 | 30 | final Authentication firstAuthentication = mock(Authentication.class); 31 | when(firstAuthentication.getPrincipal()).thenReturn(firstPrincipal); 32 | 33 | final Principal secondPrincipal = new DefaultPrincipalFactory().createPrincipal("antheruser"); 34 | 35 | final Authentication secondAuthentication = mock(Authentication.class); 36 | when(secondAuthentication.getPrincipal()).thenReturn(secondPrincipal); 37 | 38 | final MultiFactorCredentials c = new MultiFactorCredentials(); 39 | c.addAuthenticationToChain(firstAuthentication); 40 | c.addAuthenticationToChain(secondAuthentication); 41 | } 42 | 43 | @Test 44 | public void testMultifactorAddMatchingCredentials() { 45 | final Principal firstPrincipal = new DefaultPrincipalFactory().createPrincipal("casuser"); 46 | 47 | final Authentication firstAuthentication = mock(Authentication.class); 48 | when(firstAuthentication.getPrincipal()).thenReturn(firstPrincipal); 49 | 50 | final Principal secondPrincipal = new DefaultPrincipalFactory().createPrincipal("casuser"); 51 | 52 | final Authentication secondAuthentication = mock(Authentication.class); 53 | when(secondAuthentication.getPrincipal()).thenReturn(secondPrincipal); 54 | 55 | final MultiFactorCredentials c = new MultiFactorCredentials(); 56 | c.addAuthenticationToChain(firstAuthentication); 57 | c.addAuthenticationToChain(secondAuthentication); 58 | assertEquals(2, c.countChainedAuthentications()); 59 | } 60 | 61 | @Test 62 | public void testCompositeAuthenticationAndPrincipalAttributes() { 63 | final Map attributes1 = new HashMap(); 64 | attributes1.put("attr1", "attr2"); 65 | attributes1.put("uid", "username"); 66 | 67 | final Principal firstPrincipal = new DefaultPrincipalFactory().createPrincipal("casuser", attributes1); 68 | 69 | final Authentication firstAuthentication = mock(Authentication.class); 70 | when(firstAuthentication.getPrincipal()).thenReturn(firstPrincipal); 71 | when(firstAuthentication.getAttributes()) 72 | .thenReturn(Collections.singletonMap(MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD, 73 | (Object) "first_method")); 74 | 75 | final Map attributes2 = new HashMap(); 76 | attributes2.put("attr1", "attr3"); 77 | attributes2.put("cn", "commonName"); 78 | 79 | final Principal secondPrincipal = new DefaultPrincipalFactory().createPrincipal("casuser", attributes2); 80 | 81 | final Authentication secondAuthentication = mock(Authentication.class); 82 | when(secondAuthentication.getPrincipal()).thenReturn(secondPrincipal); 83 | when(secondAuthentication.getAttributes()) 84 | .thenReturn(Collections.singletonMap(MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD, 85 | (Object) "second_method")); 86 | 87 | final MultiFactorCredentials c = new MultiFactorCredentials(); 88 | c.addAuthenticationToChain(firstAuthentication); 89 | c.addAuthenticationToChain(secondAuthentication); 90 | assertEquals(2, c.countChainedAuthentications()); 91 | 92 | final Authentication authn = c.getAuthentication(); 93 | assertNotNull(authn); 94 | assertTrue(authn.getPrincipal().equals(firstPrincipal)); 95 | assertTrue(authn.getPrincipal().equals(secondPrincipal)); 96 | 97 | final Principal thePrincipal = authn.getPrincipal(); 98 | assertEquals(thePrincipal.getAttributes().size(), 3); 99 | 100 | assertTrue(thePrincipal.getAttributes().containsKey("attr1")); 101 | assertTrue(thePrincipal.getAttributes().containsKey("uid")); 102 | assertTrue(thePrincipal.getAttributes().containsKey("cn")); 103 | assertEquals(thePrincipal.getAttributes().get("attr1"), "attr3"); 104 | 105 | assertEquals(authn.getAttributes().size(), 1); 106 | 107 | final Set set = new HashSet(Arrays.asList("first_method", "second_method")); 108 | assertEquals(authn.getAttributes().get(MultiFactorAuthenticationSupportingWebApplicationService.CONST_PARAM_AUTHN_METHOD), 109 | set); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /cas-mfa-overlay/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cas-mfa 5 | net.unicon 6 | 2.0.0-RC4-SNAPSHOT 7 | 8 | 4.0.0 9 | cas-mfa-overlay 10 | CAS MFA Overlay 11 | war 12 | 13 | This is intended to be an example of a local CAS implementation Maven overlay that incorporates the 14 | Java and Web components in this project to deliver a working example of a local CAS implementation 15 | making use of the additional multi-factor authentication capabilities. 16 | 17 | 18 | 19 | ${project.basedir} 20 | 21 | 22 | 26 | 27 | 28 | 29 | org.eclipse.jetty 30 | jetty-maven-plugin 31 | 9.3.6.v20151106 32 | 33 | ${cs.dir}/etc/jetty/jetty.xml,${cs.dir}/etc/jetty/jetty-ssl.xml,${cs.dir}/etc/jetty/jetty-https.xml 34 | 35 | 36 | org.eclipse.jetty.annotations.maxWait 37 | 240 38 | 39 | 40 | 41 | /cas 42 | ${basedir}/etc/jetty/web.xml 43 | 44 | 45 | true 46 | 47 | 48 | -Xdebug -Xrunjdwp:transport=dt_socket,address=5000,server=y,suspend=n 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-war-plugin 55 | 56 | cas 57 | WEB-INF/lib/stax-api-1.0-2.jar 58 | 59 | 60 | 61 | net.unicon 62 | cas-mfa-duo-web 63 | 64 | 65 | 66 | net.unicon 67 | cas-mfa-web 68 | 69 | 70 | org.jasig.cas 71 | cas-server-webapp 72 | 73 | WEB-INF/cas.properties 74 | WEB-INF/login-webflow.xml 75 | WEB-INF/classes/log4j.xml 76 | 77 | 78 | 79 | 80 | 81 | 82 | cas 83 | 84 | 85 | 86 | 87 | org.jasig.cas 88 | cas-server-webapp 89 | war 90 | 91 | 92 | 93 | net.unicon 94 | cas-mfa-web 95 | ${project.version} 96 | war 97 | 98 | 99 | 100 | org.jasig.cas 101 | cas-server-extension-clearpass 102 | ${cas.version} 103 | runtime 104 | 105 | 106 | 107 | net.unicon 108 | cas-mfa-duo-web 109 | ${project.version} 110 | war 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /cas-mfa-java/src/main/java/net/unicon/cas/mfa/authentication/MultiFactorAuthenticationTransactionContext.java: -------------------------------------------------------------------------------- 1 | package net.unicon.cas.mfa.authentication; 2 | 3 | import org.jasig.cas.authentication.Authentication; 4 | import org.springframework.util.Assert; 5 | 6 | import java.io.Serializable; 7 | import java.util.Collections; 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | import static net.unicon.cas.mfa.web.support.MultiFactorAuthenticationSupportingWebApplicationService.AuthenticationMethodSource; 12 | 13 | /** 14 | * Class holding contextual information pertaining to any currently in-progress mfa authentication transactions i.e. 15 | * mfa methods from multiple definition sources required to authenticate against any current target service 16 | * collected for just current authentication request. 17 | * Each new authentication request should represent a new mfa transaction. 18 | * 19 | * This class enforces invariants that ensures that only a single target service is able to participate 20 | * in such a single mfa authentication transaction 21 | * and only a single instance of the same authentication method source could exist at a time. 22 | * 23 | * @author Dmitriy Kopylenko 24 | * @author Unicon inc. 25 | */ 26 | public final class MultiFactorAuthenticationTransactionContext implements Serializable { 27 | 28 | 29 | private final String targetServiceId; 30 | 31 | /** 32 | * Primary authentication if has been accomplished OR null. 33 | */ 34 | private Authentication primaryAuthentication; 35 | 36 | /** 37 | * A collection of mfa requests or empty if none. 38 | */ 39 | private Set mfaRequests = new HashSet<>(); 40 | 41 | 42 | /** 43 | * Ctor. 44 | * 45 | * @param targetServiceId target service 46 | */ 47 | public MultiFactorAuthenticationTransactionContext(final String targetServiceId) { 48 | Assert.notNull(targetServiceId, "targetServiceId cannot be null"); 49 | this.targetServiceId = targetServiceId; 50 | } 51 | 52 | /** 53 | * Get. 54 | * 55 | * @return primary authentication. 56 | */ 57 | public Authentication getPrimaryAuthentication() { 58 | return primaryAuthentication; 59 | } 60 | 61 | 62 | /** 63 | * Setter that supports fluid API. 64 | * 65 | * @param primaryAuthentication primaryAuthentication 66 | * 67 | * @return this 68 | */ 69 | public MultiFactorAuthenticationTransactionContext setPrimaryAuthentication(final Authentication primaryAuthentication) { 70 | this.primaryAuthentication = primaryAuthentication; 71 | return this; 72 | } 73 | 74 | /** 75 | * Get. 76 | * 77 | * @return current mfa requests 78 | */ 79 | public Set getMfaRequests() { 80 | return Collections.unmodifiableSet(mfaRequests); 81 | } 82 | 83 | /** 84 | * Get. 85 | * 86 | * @return target service id 87 | */ 88 | public String getTargetServiceId() { 89 | return targetServiceId; 90 | } 91 | 92 | /** 93 | * Add mfa request to internal collection. Supports fluid API. 94 | * 95 | * @param mfaRequest mfaRequest 96 | * 97 | * @return this 98 | */ 99 | public MultiFactorAuthenticationTransactionContext addMfaRequest(final MultiFactorAuthenticationRequestContext mfaRequest) { 100 | if (differentThanTargetService(mfaRequest.getMfaService().getId())) { 101 | throw new IllegalArgumentException(String.format("Requested mfa target service {%s} is different from " 102 | + "the current authentication transaction target service {%s}", 103 | mfaRequest.getMfaService().getId(), this.targetServiceId)); 104 | } 105 | 106 | if (!authnMethodSourceAlreadyExists(mfaRequest.getMfaService().getAuthenticationMethodSource())) { 107 | this.mfaRequests.add(mfaRequest); 108 | } 109 | 110 | return this; 111 | } 112 | 113 | /** 114 | * Is the current tx's target service different from a given one. 115 | * 116 | * @param serviceId passed in service id 117 | * 118 | * @return true if the passed in service is different from the current tx's target service 119 | */ 120 | private boolean differentThanTargetService(final String serviceId) { 121 | return !(this.targetServiceId.equals(serviceId)); 122 | } 123 | 124 | /** 125 | * Is there an authentication method source already captured that equals to the incoming one. 126 | * 127 | * @param authenticationMethodSource incoming authenticationMethodSource 128 | * 129 | * @return true if authn method already exists and false otherwise 130 | */ 131 | private boolean authnMethodSourceAlreadyExists(final AuthenticationMethodSource authenticationMethodSource) { 132 | if (this.mfaRequests.isEmpty()) { 133 | return false; 134 | } 135 | for (final MultiFactorAuthenticationRequestContext ctx : this.mfaRequests) { 136 | if (ctx.getMfaService().getAuthenticationMethodSource() == authenticationMethodSource) { 137 | return true; 138 | } 139 | } 140 | return false; 141 | } 142 | 143 | @Override 144 | public String toString() { 145 | return "MultiFactorAuthenticationTransactionContext{" 146 | + 147 | "targetServiceId='" + targetServiceId + '\'' 148 | + 149 | ", primaryAuthentication=" + primaryAuthentication 150 | + 151 | ", mfaRequests=" + mfaRequests 152 | + 153 | '}'; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /cas-mfa-overlay/src/main/webapp/WEB-INF/deployerConfigContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 35 | 36 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 82 | 83 | 87 | 88 | 89 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | memberOf 98 | 99 | faculty 100 | staff 101 | org 102 | 103 | 104 | 105 | 106 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /cas-mfa-overlay/etc/jetty/jetty-ssl.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | --------------------------------------------------------------------------------