Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 3f5ba5f7fdf6f168d09d171227a04a75b6d9a2ef 1 parent fb01b1f
Rafael Luque authored
Showing with 4,898 additions and 0 deletions.
  1. +297 −0 SpringSecurityOtpGrailsPlugin.groovy
  2. +6 −0 application.properties
  3. +39 −0 grails-app/conf/BuildConfig.groovy
  4. +24 −0 grails-app/conf/Config.groovy
  5. +43 −0 grails-app/conf/DataSource.groovy
  6. +71 −0 grails-app/conf/DefaultOtpSecurityConfig.groovy
  7. +13 −0 grails-app/conf/UrlMappings.groovy
  8. +11 −0 grails-app/views/error.gsp
  9. BIN  lib/groovy-OTP.jar
  10. +28 −0 plugin.xml
  11. +10 −0 scripts/_Install.groovy
  12. +5 −0 scripts/_Uninstall.groovy
  13. +10 −0 scripts/_Upgrade.groovy
  14. +85 −0 src/groovy/es/osoco/grails/plugins/otp/OneTimePasswordService.groovy
  15. +328 −0 src/groovy/es/osoco/grails/plugins/otp/access/AbstractMultipleVoterFilterInvocationDefinition.groovy
  16. +333 −0 src/groovy/es/osoco/grails/plugins/otp/access/AnnotationMultipleVoterFilterInvocationDefinition.groovy
  17. +87 −0 src/groovy/es/osoco/grails/plugins/otp/access/InterceptUrlMapMultipleVoterFilterInvocatioinDefinition.groovy
  18. +94 −0 src/groovy/es/osoco/grails/plugins/otp/access/OneTimePasswordVoter.groovy
  19. +98 −0 src/groovy/es/osoco/grails/plugins/otp/access/RequestmapMultipleVoterFilterInvocationDefinition.groovy
  20. +179 −0 src/groovy/es/osoco/grails/plugins/otp/access/TwoFactorDecisionManager.groovy
  21. +61 −0 src/groovy/es/osoco/grails/plugins/otp/authentication/NopAuthenticationSuccessHandler.groovy
  22. +177 −0 src/groovy/es/osoco/grails/plugins/otp/authentication/OneTimePasswordAuthenticationProvider.groovy
  23. +116 −0 src/groovy/es/osoco/grails/plugins/otp/authentication/OneTimePasswordAuthenticationToken.groovy
  24. +65 −0 src/groovy/es/osoco/grails/plugins/otp/authentication/TwoFactorAuthenticationException.groovy
  25. +66 −0 src/groovy/es/osoco/grails/plugins/otp/authentication/TwoFactorInsufficientAuthenticationException.groovy
  26. +75 −0 src/groovy/es/osoco/grails/plugins/otp/userdetails/GrailsOtpUser.groovy
  27. +74 −0 src/groovy/es/osoco/grails/plugins/otp/userdetails/OneTimePasswordUserDetailsService.groovy
  28. +64 −0 src/groovy/es/osoco/grails/plugins/otp/web/FirstFactorRequestHolderAuthenticationFilter.groovy
  29. +177 −0 src/groovy/es/osoco/grails/plugins/otp/web/OneTimePasswordAuthenticationFilter.groovy
  30. +111 −0 src/groovy/es/osoco/grails/plugins/otp/web/TwoFactorExceptionTranslationFilter.groovy
  31. +33 −0 web-app/WEB-INF/applicationContext.xml
  32. +14 −0 web-app/WEB-INF/sitemesh.xml
  33. +572 −0 web-app/WEB-INF/tld/c.tld
  34. +671 −0 web-app/WEB-INF/tld/fmt.tld
  35. +550 −0 web-app/WEB-INF/tld/grails.tld
  36. +311 −0 web-app/WEB-INF/tld/spring.tld
View
297 SpringSecurityOtpGrailsPlugin.groovy
@@ -0,0 +1,297 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import es.osoco.grails.plugins.otp.OneTimePasswordService
+
+import es.osoco.grails.plugins.otp.access.AnnotationMultipleVoterFilterInvocationDefinition
+import es.osoco.grails.plugins.otp.access.InterceptUrlMapMultipleVoterFilterInvocationDefinition
+import es.osoco.grails.plugins.otp.access.OneTimePasswordVoter
+import es.osoco.grails.plugins.otp.access.RequestmapMultipleVoterFilterInvocationDefinition
+import es.osoco.grails.plugins.otp.access.TwoFactorDecisionManager
+
+import es.osoco.grails.plugins.otp.authentication.NopAuthenticationSuccessHandler
+import es.osoco.grails.plugins.otp.authentication.OneTimePasswordAuthenticationProvider
+
+import es.osoco.grails.plugins.otp.userdetails.OneTimePasswordUserDetailsService
+
+import es.osoco.grails.plugins.otp.web.FirstFactorRequestHolderAuthenticationFilter
+import es.osoco.grails.plugins.otp.web.OneTimePasswordAuthenticationFilter
+import es.osoco.grails.plugins.otp.web.TwoFactorExceptionTranslationFilter
+
+import org.codehaus.groovy.grails.plugins.springsecurity.AjaxAwareAuthenticationEntryPoint
+import org.codehaus.groovy.grails.plugins.springsecurity.AjaxAwareAuthenticationSuccessHandler
+import org.codehaus.groovy.grails.plugins.springsecurity.RequestHolderAuthenticationFilter
+import org.codehaus.groovy.grails.plugins.springsecurity.SecurityFilterPosition
+import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtils
+
+import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
+import org.springframework.security.web.util.AntUrlPathMatcher
+import org.springframework.security.web.util.RegexUrlPathMatcher
+
+
+class SpringSecurityOtpGrailsPlugin {
+
+ def version = "0.1"
+ def grailsVersion = "1.3.7 > *"
+
+ def dependsOn = [springSecurityCore: '1.2.7.2 > *']
+
+ def title = "Spring Security OTP Plugin"
+ def authorEmail = "info@osoco.es"
+ def organization = [name: "OSOCO", url: "http://osoco.es/"]
+ def developers = [
+ [ name: "Rafael Luque", email: "rafael.luque@osoco.es" ],
+ [ name: "Arturo Garcia", email: "arturo.garcia@osoco.es" ] ]
+ def description = '''\
+Adds support for one-time password to Spring Security.
+'''
+ def license = "APACHE"
+ def documentation = "http://grails.org/plugin/spring-security-otp"
+ def scm = [ url: "https://github.com/osoco/grails-spring-security-otp" ]
+ def issueManagement = [ system: "GitHub", url: "https://github.com/osoco/grails-spring-security-otp/issues" ]
+
+ def pluginExcludes = [
+ "grails-app/views/error.gsp"
+ ]
+
+
+ def doWithSpring = {
+
+ def conf = SpringSecurityUtils.securityConfig
+ if (!conf || !conf.active) {
+ return
+ }
+
+ SpringSecurityUtils.loadSecondaryConfig 'DefaultOtpSecurityConfig'
+
+ conf = SpringSecurityUtils.securityConfig
+ if (!conf.otp.active) {
+ return
+ }
+
+ println 'Configuring Spring Security OTP...'
+
+ /** otpAuthenticationFilter */
+ otpAuthenticationFilter(OneTimePasswordAuthenticationFilter) {
+ authenticationManager = ref('authenticationManager')
+ sessionAuthenticationStrategy = ref('sessionAuthenticationStrategy')
+ authenticationSuccessHandler = ref('authenticationSuccessHandler')
+ authenticationFailureHandler = ref('authenticationFailureHandler')
+ rememberMeServices = ref('rememberMeServices')
+ authenticationDetailsSource = ref('authenticationDetailsSource')
+ filterProcessesUrl = conf.otp.apf.filterProcessesUrl // '/j_spring_security_otp'
+ usernameParameter = conf.otp.apf.usernameParameter // 'j_username'
+ passwordParameter = conf.otp.apf.otpParameter // 'j_otp'
+ continueChainBeforeSuccessfulAuthentication = conf.otp.apf.continueChainBeforeSuccessfulAuthentication
+ allowSessionCreation = conf.otp.apf.allowSessionCreation
+ postOnly = conf.otp.apf.postOnly
+ }
+
+ /** otpAuthenticationProvider */
+ otpAuthenticationProvider(OneTimePasswordAuthenticationProvider) {
+ userDetailsService = ref('otpUserDetailsService')
+ oneTimePasswordService = ref('oneTimePasswordService')
+ passwordEncoder = null
+ userCache = ref('userCache')
+ saltSource = null
+ preAuthenticationChecks = ref('preAuthenticationChecks')
+ postAuthenticationChecks = ref('postAuthenticationChecks')
+ hideUserNotFoundExceptions = conf.dao.hideUserNotFoundExceptions // true
+ }
+
+ /** oneTimePasswordService */
+ oneTimePasswordService(OneTimePasswordService) {
+ otpDigits = conf.otp.totp.digits
+ otpAlgorithm = conf.otp.totp.algorithm
+ preStepsWindow = conf.otp.totp.preStepsValidWindow
+ postStepsWindow = conf.otp.totp.postStepsValidWindow
+ }
+
+ /** userDetailsService */
+ otpUserDetailsService(OneTimePasswordUserDetailsService) {
+ grailsApplication = ref('grailsApplication')
+ }
+
+ /** otpAuthenticationEntryPoint */
+ otpAuthenticationEntryPoint(AjaxAwareAuthenticationEntryPoint) {
+ loginFormUrl = conf.otp.auth.loginFormUrl // '/login/authOTP'
+ forceHttps = conf.otp.auth.forceHttps // false
+ ajaxLoginFormUrl = conf.otp.auth.ajaxLoginFormUrl // '/login/authOTPAjax'
+ useForward = conf.otp.auth.useForward // false
+ portMapper = ref('portMapper')
+ portResolver = ref('portResolver')
+ }
+
+ if (conf.otp.useTwoFactorsCombinedLoginForm) {
+
+ /** twoFactorsAuthenticationEntryPoint */
+ twoFactorsAuthenticationEntryPoint(AjaxAwareAuthenticationEntryPoint) {
+ loginFormUrl = conf.otp.auth.combinedLoginFormUrl // '/login/authTwoFactors'
+ forceHttps = conf.otp.auth.forceHttps // false
+ ajaxLoginFormUrl = conf.otp.auth.combinedAjaxLoginFormUrl // '/login/authTwoFactorsAjax'
+ useForward = conf.otp.auth.useForward // false
+ portMapper = ref('portMapper')
+ portResolver = ref('portResolver')
+ }
+
+ /** twoFactorExceptionTranslationFilter */
+ twoFactorExceptionTranslationFilter(TwoFactorExceptionTranslationFilter) {
+ useTwoFactorsCombinedLoginForm = conf.otp.useTwoFactorsCombinedLoginForm
+ authenticationEntryPoint = ref('authenticationEntryPoint')
+ secondFactorAuthenticationEntryPoint = ref('otpAuthenticationEntryPoint')
+ twoFactorsAuthenticationEntryPoint = ref('twoFactorsAuthenticationEntryPoint')
+ accessDeniedHandler = ref('accessDeniedHandler')
+ authenticationTrustResolver = ref('authenticationTrustResolver')
+ requestCache = ref('requestCache')
+ }
+
+ /** nonRedirectAuthenticationSuccessHandler */
+ nopAuthenticationSuccessHandler(NopAuthenticationSuccessHandler)
+
+ /** firstFactorAuthenticationFilter */
+ firstFactorAuthenticationFilter(FirstFactorRequestHolderAuthenticationFilter) {
+ authenticationManager = ref('authenticationManager')
+ sessionAuthenticationStrategy = ref('sessionAuthenticationStrategy')
+ authenticationSuccessHandler = ref('nopAuthenticationSuccessHandler')
+ authenticationFailureHandler = ref('authenticationFailureHandler')
+ rememberMeServices = ref('rememberMeServices')
+ authenticationDetailsSource = ref('authenticationDetailsSource')
+ filterProcessesUrl = conf.otp.apf.twoFactorsFilterProcessesUrl // '/j_spring_security_twofactors'
+ usernameParameter = conf.otp.apf.usernameParameter // 'j_username'
+ passwordParameter = conf.otp.apf.passwordParameter // 'j_password'
+ continueChainBeforeSuccessfulAuthentication = true
+ allowSessionCreation = conf.otp.apf.allowSessionCreation
+ postOnly = conf.otp.apf.postOnly
+ }
+
+ /** secondFactorAuthenticationFilter */
+ secondFactorAuthenticationFilter(OneTimePasswordAuthenticationFilter) {
+ authenticationManager = ref('authenticationManager')
+ sessionAuthenticationStrategy = ref('sessionAuthenticationStrategy')
+ authenticationSuccessHandler = ref('authenticationSuccessHandler')
+ authenticationFailureHandler = ref('authenticationFailureHandler')
+ rememberMeServices = ref('rememberMeServices')
+ authenticationDetailsSource = ref('authenticationDetailsSource')
+ filterProcessesUrl = conf.otp.apf.twoFactorsFilterProcessesUrl // '/j_spring_security_twofactors'
+ usernameParameter = conf.otp.apf.usernameParameter // 'j_username'
+ passwordParameter = conf.otp.apf.otpParameter // 'j_otp'
+ continueChainBeforeSuccessfulAuthentication = conf.otp.apf.continueChainBeforeSuccessfulAuthentication
+ allowSessionCreation = conf.otp.apf.allowSessionCreation
+ postOnly = conf.otp.apf.postOnly
+ }
+
+ SpringSecurityUtils.registerFilter 'firstFactorAuthenticationFilter', SecurityFilterPosition.FORM_LOGIN_FILTER.order + 1
+ SpringSecurityUtils.registerFilter 'secondFactorAuthenticationFilter', SecurityFilterPosition.FORM_LOGIN_FILTER.order + 2
+
+ } else {
+
+ /** twoFactorExceptionTranslationFilter */
+ twoFactorExceptionTranslationFilter(TwoFactorExceptionTranslationFilter) {
+ useTwoFactorsCombinedLoginForm = conf.otp.useTwoFactorsCombinedLoginForm
+ authenticationEntryPoint = ref('authenticationEntryPoint')
+ secondFactorAuthenticationEntryPoint = ref('otpAuthenticationEntryPoint')
+ accessDeniedHandler = ref('accessDeniedHandler')
+ authenticationTrustResolver = ref('authenticationTrustResolver')
+ requestCache = ref('requestCache')
+ }
+
+ }
+
+ /** otpVoter **/
+ otpVoter(OneTimePasswordVoter)
+ SpringSecurityUtils.registerVoter 'otpVoter'
+
+ /** twoFactorAccessDecisionManager */
+ twoFactorDecisionManager(TwoFactorDecisionManager) {
+ firstFactorDecisionManager = ref('accessDecisionManager')
+ twoFactorDecisionVoter = ref('otpVoter')
+ }
+
+ /** filterInvocationInterceptor */
+ filterInvocationInterceptor(FilterSecurityInterceptor) {
+ authenticationManager = ref('authenticationManager')
+ accessDecisionManager = ref('twoFactorDecisionManager')
+ securityMetadataSource = ref('objectDefinitionSource')
+ runAsManager = ref('runAsManager')
+ }
+
+ def createRefList = { names -> names.collect { name -> ref(name) } }
+ def decisionVoters = createRefList(SpringSecurityUtils.getVoterNames())
+ String securityConfigType = SpringSecurityUtils.securityConfigType
+
+ if (securityConfigType == 'Annotation') {
+ objectDefinitionSource(AnnotationMultipleVoterFilterInvocationDefinition) {
+ application = ref('grailsApplication')
+ voters = decisionVoters
+ expressionHandler = ref('webExpressionHandler')
+ boolean lowercase = conf.controllerAnnotations.lowercase // true
+ if ('ant'.equals(conf.controllerAnnotations.matcher)) {
+ urlMatcher = new AntUrlPathMatcher(lowercase)
+ }
+ else {
+ urlMatcher = new RegexUrlPathMatcher(lowercase)
+ }
+ if (conf.rejectIfNoRule instanceof Boolean) {
+ rejectIfNoRule = conf.rejectIfNoRule
+ }
+ }
+ }
+ else if (securityConfigType == 'Requestmap') {
+ objectDefinitionSource(RequestmapMultipleVoterFilterInvocationDefinition) {
+ voters = decisionVoters
+ expressionHandler = ref('webExpressionHandler')
+ urlMatcher = new AntUrlPathMatcher(true)
+ if (conf.rejectIfNoRule instanceof Boolean) {
+ rejectIfNoRule = conf.rejectIfNoRule
+ }
+ }
+ }
+ else if (securityConfigType == 'InterceptUrlMap') {
+ objectDefinitionSource(InterceptUrlMultipleVoterMapFilterInvocationDefinition) {
+ voters = decisionVoters
+ expressionHandler = ref('webExpressionHandler')
+ urlMatcher = new AntUrlPathMatcher(true)
+ if (conf.rejectIfNoRule instanceof Boolean) {
+ rejectIfNoRule = conf.rejectIfNoRule
+ }
+ }
+ }
+
+ SpringSecurityUtils.registerProvider 'otpAuthenticationProvider'
+ SpringSecurityUtils.registerFilter 'otpAuthenticationFilter', SecurityFilterPosition.FORM_LOGIN_FILTER.order + 3
+ SpringSecurityUtils.registerFilter 'twoFactorExceptionTranslationFilter', SecurityFilterPosition.EXCEPTION_TRANSLATION_FILTER.order + 1
+
+ println '...finished configuring Spring Security OTP'
+
+ }
+
+}
View
6 application.properties
@@ -0,0 +1,6 @@
+#Grails Metadata file
+#Fri Sep 28 14:30:12 CEST 2012
+app.grails.version=2.0.4
+app.name=spring-security-otp
+plugins.spring-security-core=1.2.7.3
+plugins.svn=1.0.1
View
39 grails-app/conf/BuildConfig.groovy
@@ -0,0 +1,39 @@
+grails.project.class.dir = "target/classes"
+grails.project.test.class.dir = "target/test-classes"
+grails.project.test.reports.dir = "target/test-reports"
+grails.project.target.level = 1.6
+//grails.project.war.file = "target/${appName}-${appVersion}.war"
+
+grails.project.dependency.resolution = {
+ // inherit Grails' default dependencies
+ inherits("global") {
+ // uncomment to disable ehcache
+ // excludes 'ehcache'
+ }
+ log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
+ repositories {
+ grailsCentral()
+ // uncomment the below to enable remote dependency resolution
+ // from public Maven repositories
+ //mavenCentral()
+ //mavenLocal()
+ //mavenRepo "http://snapshots.repository.codehaus.org"
+ //mavenRepo "http://repository.codehaus.org"
+ //mavenRepo "http://download.java.net/maven/2/"
+ //mavenRepo "http://repository.jboss.com/maven2/"
+ }
+ dependencies {
+ // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
+
+ // runtime 'mysql:mysql-connector-java:5.1.5'
+
+ compile 'commons-codec:commons-codec:1.7'
+ }
+
+ plugins {
+ build(":tomcat:$grailsVersion",
+ ":release:1.0.0") {
+ export = false
+ }
+ }
+}
View
24 grails-app/conf/Config.groovy
@@ -0,0 +1,24 @@
+// configuration for plugin testing - will not be included in the plugin zip
+
+log4j = {
+ // Example of changing the log pattern for the default console
+ // appender:
+ //
+ //appenders {
+ // console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n')
+ //}
+
+ error 'org.codehaus.groovy.grails.web.servlet', // controllers
+ 'org.codehaus.groovy.grails.web.pages', // GSP
+ 'org.codehaus.groovy.grails.web.sitemesh', // layouts
+ 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
+ 'org.codehaus.groovy.grails.web.mapping', // URL mapping
+ 'org.codehaus.groovy.grails.commons', // core / classloading
+ 'org.codehaus.groovy.grails.plugins', // plugins
+ 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
+ 'org.springframework',
+ 'org.hibernate',
+ 'net.sf.ehcache.hibernate'
+
+ warn 'org.mortbay.log'
+}
View
43 grails-app/conf/DataSource.groovy
@@ -0,0 +1,43 @@
+dataSource {
+ pooled = true
+ driverClassName = "org.h2.Driver"
+ username = "sa"
+ password = ""
+}
+hibernate {
+ cache.use_second_level_cache = true
+ cache.use_query_cache = false
+ cache.region.factory_class = 'net.sf.ehcache.hibernate.EhCacheRegionFactory'
+}
+// environment specific settings
+environments {
+ development {
+ dataSource {
+ dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+ url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
+ }
+ }
+ test {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
+ }
+ }
+ production {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:h2:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
+ pooled = true
+ properties {
+ maxActive = -1
+ minEvictableIdleTimeMillis=1800000
+ timeBetweenEvictionRunsMillis=1800000
+ numTestsPerEvictionRun=3
+ testOnBorrow=true
+ testWhileIdle=true
+ testOnReturn=true
+ validationQuery="SELECT 1"
+ }
+ }
+ }
+}
View
71 grails-app/conf/DefaultOtpSecurityConfig.groovy
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import es.osoco.grails.plugins.otp.web.OneTimePasswordAuthenticationFilter as OTPAF
+
+security {
+
+ otp {
+ active = true
+
+ // Use a combined form for the two-factors or different forms
+ useTwoFactorsCombinedLoginForm = false
+
+ totp.digits = 6
+ totp.algorithm = 'HmacSHA1'
+ totp.preStepsValidWindow = 1
+ totp.postStepsValidWindow = 1
+
+ auth.loginFormUrl = '/login/authOTP'
+ auth.ajaxLoginFormUrl = '/login/authOTPAjax'
+
+ auth.combinedLoginFormUrl = '/login/authTwoFactors'
+ auth.combinedAjaxLoginFormUrl = '/login/authTwoFactorsAjax'
+
+ auth.forceHttps = false
+ auth.useForward = false
+
+ /** authentication processing filters */
+ apf.filterProcessesUrl = '/j_spring_security_otp'
+ apf.twoFactorsFilterProcessesUrl = '/j_spring_security_twofactors'
+ apf.usernameParameter = OTPAF.SPRING_SECURITY_OTP_FORM_USERNAME_KEY // 'j_username'
+ apf.passwordParameter = OTPAF.SPRING_SECURITY_OTP_FORM_PASSWORD_KEY // 'j_password'
+ apf.otpParameter = OTPAF.SPRING_SECURITY_OTP_FORM_OTP_KEY // 'j_otp'
+ apf.continueChainBeforeSuccessfulAuthentication = false
+ apf.allowSessionCreation = true
+ apf.postOnly = true
+
+ /** user class specific properties */
+ userLookup.secretKeyPropertyName = 'secretKey'
+ }
+}
View
13 grails-app/conf/UrlMappings.groovy
@@ -0,0 +1,13 @@
+class UrlMappings {
+
+ static mappings = {
+ "/$controller/$action?/$id?"{
+ constraints {
+ // apply constraints here
+ }
+ }
+
+ "/"(view:"/index")
+ "500"(view:'/error')
+ }
+}
View
11 grails-app/views/error.gsp
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Grails Runtime Exception</title>
+ <meta name="layout" content="main">
+ <link rel="stylesheet" href="${resource(dir: 'css', file: 'errors.css')}" type="text/css">
+ </head>
+ <body>
+ <g:renderException exception="${exception}" />
+ </body>
+</html>
View
BIN  lib/groovy-OTP.jar
Binary file not shown
View
28 plugin.xml
@@ -0,0 +1,28 @@
+<plugin name='spring-security-otp' version='0.1' grailsVersion='1.3.7 &gt; *'>
+ <authorEmail>info@osoco.es</authorEmail>
+ <title>Spring Security OTP Plugin</title>
+ <description>Adds support for one-time password to Spring Security.
+</description>
+ <documentation>http://grails.org/plugin/spring-security-otp</documentation>
+ <type>SpringSecurityOtpGrailsPlugin</type>
+ <resources>
+ <resource>DefaultOtpSecurityConfig</resource>
+ </resources>
+ <repositories>
+ <repository name='grailsCentral' url='http://plugins.grails.org' />
+ <repository name='http://repo.grails.org/grails/plugins' url='http://repo.grails.org/grails/plugins/' />
+ <repository name='http://repo.grails.org/grails/core' url='http://repo.grails.org/grails/core/' />
+ <repository name='grailsCore' url='http://svn.codehaus.org/grails/trunk/grails-plugins' />
+ <repository name='mavenCentral' url='http://repo1.maven.org/maven2/' />
+ </repositories>
+ <dependencies>
+ <compile>
+ <dependency group='commons-codec' name='commons-codec' version='1.7' />
+ </compile>
+ </dependencies>
+ <plugins />
+ <runtimePluginRequirements>
+ <plugin name='springSecurityCore' version='1.2.7.2 &gt; *' />
+ </runtimePluginRequirements>
+ <behavior />
+</plugin>
View
10 scripts/_Install.groovy
@@ -0,0 +1,10 @@
+//
+// This script is executed by Grails after plugin was installed to project.
+// This script is a Gant script so you can use all special variables provided
+// by Gant (such as 'baseDir' which points on project base dir). You can
+// use 'ant' to access a global instance of AntBuilder
+//
+// For example you can create directory under project tree:
+//
+// ant.mkdir(dir:"${basedir}/grails-app/jobs")
+//
View
5 scripts/_Uninstall.groovy
@@ -0,0 +1,5 @@
+//
+// This script is executed by Grails when the plugin is uninstalled from project.
+// Use this script if you intend to do any additional clean-up on uninstall, but
+// beware of messing up SVN directories!
+//
View
10 scripts/_Upgrade.groovy
@@ -0,0 +1,10 @@
+//
+// This script is executed by Grails during application upgrade ('grails upgrade'
+// command). This script is a Gant script so you can use all special variables
+// provided by Gant (such as 'baseDir' which points on project base dir). You can
+// use 'ant' to access a global instance of AntBuilder
+//
+// For example you can create directory under project tree:
+//
+// ant.mkdir(dir:"${basedir}/grails-app/jobs")
+//
View
85 src/groovy/es/osoco/grails/plugins/otp/OneTimePasswordService.groovy
@@ -0,0 +1,85 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp
+
+import es.osoco.oath.totp.TOTP
+
+import org.apache.commons.codec.binary.Base32
+import org.apache.commons.logging.Log
+import org.apache.commons.logging.LogFactory
+
+/**
+ * This class checks the validity of one-time passwords delegating to the groovy-OTP library.
+ *
+ * @author <a href="mailto:rafael.luque@osoco.es">Rafael Luque</a>
+ */
+class OneTimePasswordService {
+
+ int otpDigits
+ String otpAlgorithm
+ int preStepsWindow
+ int postStepsWindow
+
+ protected final Log logger = LogFactory.getLog(getClass())
+
+ public boolean isPasswordValid(String presentedPassword, String secretKey) {
+ def decodedSecretKey = new Base32().decode(secretKey)
+ String hexDecodedSecretKey = decodedSecretKey.encodeHex().toString()
+ int currentTimeSteps = stepsForUnixTime(new Date().time)
+ def validWindow = (currentTimeSteps - preStepsWindow)..(currentTimeSteps + postStepsWindow)
+ logger.debug "validating OTP in the window: $validWindow"
+
+ validWindow.any { steps ->
+ presentedPassword == TOTP.generateTOTP(
+ hexDecodedSecretKey,
+ toStepsTimeHex(steps),
+ "$otpDigits".toString(),
+ otpAlgorithm)
+ }
+ }
+
+ private int stepsForUnixTime(unixTimeInMillis) {
+ int steps = unixTimeInMillis / 1000 / 30
+ steps
+ }
+
+ private String toStepsTimeHex(int stepTime) {
+ String steps = Long.toHexString(stepTime).toUpperCase()
+ while (steps.length() < 16) {
+ steps = "0" + steps;
+ }
+ steps
+ }
+
+
+}
View
328 src/groovy/es/osoco/grails/plugins/otp/access/AbstractMultipleVoterFilterInvocationDefinition.groovy
@@ -0,0 +1,328 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package es.osoco.grails.plugins.otp.access
+
+import grails.util.GrailsUtil
+
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.InitializingBean
+import org.springframework.expression.Expression
+import org.codehaus.groovy.grails.plugins.springsecurity.WebExpressionConfigAttribute
+import org.springframework.security.access.AccessDecisionVoter
+import org.springframework.security.access.ConfigAttribute
+import org.springframework.security.access.SecurityConfig
+import org.springframework.security.web.FilterInvocation
+import org.springframework.security.web.access.expression.WebSecurityExpressionHandler
+import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource
+import org.springframework.security.web.util.AntUrlPathMatcher
+import org.springframework.security.web.util.UrlMatcher
+import org.springframework.util.Assert
+import org.springframework.util.StringUtils
+
+/**
+ * Based on Grails Spring Security Core's <code>org.codehaus.groovy.grails.plugins.springsecurity.access.AbstractFilterInvocationDefinition</code>.
+ *
+ * @author <a href='mailto:rafael.luque@osoco.es'>Rafael Luque</a>
+ */
+public abstract class AbstractMultipleVoterFilterInvocationDefinition
+ implements FilterInvocationSecurityMetadataSource, InitializingBean {
+
+ private UrlMatcher _urlMatcher
+ private boolean _rejectIfNoRule
+ private boolean _stripQueryStringFromUrls = true
+ def voters
+ private WebSecurityExpressionHandler _expressionHandler
+
+ private final Map<Object, Collection<ConfigAttribute>> _compiled = new LinkedHashMap<Object, Collection<ConfigAttribute>>()
+
+ protected final Logger _log = LoggerFactory.getLogger(getClass())
+
+ protected static final Collection<ConfigAttribute> DENY = Collections.emptyList()
+
+ /**
+ * Allows subclasses to be externally reset.
+ * @throws Exception
+ */
+ public void reset() throws Exception {
+ // override if necessary
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.springframework.security.access.SecurityMetadataSource#getAttributes(java.lang.Object)
+ */
+ public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
+ Assert.isTrue(object != null && supports(object.getClass()), "Object must be a FilterInvocation")
+
+ FilterInvocation filterInvocation = (FilterInvocation)object
+
+ String url = determineUrl(filterInvocation)
+
+ Collection<ConfigAttribute> configAttributes
+ try {
+ configAttributes = findConfigAttributes(url)
+ }
+ catch (Exception e) {
+ // TODO fix this
+ throw new RuntimeException(e)
+ }
+
+ if (configAttributes == null && _rejectIfNoRule) {
+ return DENY
+ }
+
+ return configAttributes
+ }
+
+ protected abstract String determineUrl(FilterInvocation filterInvocation)
+
+ protected boolean stopAtFirstMatch() {
+ return false
+ }
+
+ private Collection<ConfigAttribute> findConfigAttributes(final String url) throws Exception {
+
+ initialize()
+
+ Collection<ConfigAttribute> configAttributes = null
+ Object configAttributePattern = null
+
+ boolean stopAtFirstMatch = stopAtFirstMatch()
+ for (Map.Entry<Object, Collection<ConfigAttribute>> entry : _compiled.entrySet()) {
+ Object pattern = entry.getKey()
+ if (_urlMatcher.pathMatchesUrl(pattern, url)) {
+ // TODO this assumes Ant matching, not valid for regex
+ if (configAttributes == null || _urlMatcher.pathMatchesUrl(configAttributePattern, (String)pattern)) {
+ configAttributes = entry.getValue()
+ configAttributePattern = pattern
+ if (_log.isTraceEnabled()) {
+ _log.trace("new candidate for '" + url + "': '" + pattern
+ + "':" + configAttributes)
+ }
+ if (stopAtFirstMatch) {
+ break
+ }
+ }
+ }
+ }
+
+ if (_log.isTraceEnabled()) {
+ if (configAttributes == null) {
+ _log.trace("no config for '" + url + "'")
+ }
+ else {
+ _log.trace("config for '" + url + "' is '" + configAttributePattern + "':" + configAttributes)
+ }
+ }
+
+ return configAttributes
+ }
+
+ protected void initialize() throws Exception {
+ // override if necessary
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.springframework.security.access.SecurityMetadataSource#supports(java.lang.Class)
+ */
+ public boolean supports(Class<?> clazz) {
+ return FilterInvocation.class.isAssignableFrom(clazz)
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.springframework.security.access.SecurityMetadataSource#getAllConfigAttributes()
+ */
+ public Collection<ConfigAttribute> getAllConfigAttributes() {
+ try {
+ initialize()
+ }
+ catch (Exception e) {
+ GrailsUtil.deepSanitize(e)
+ _log.error(e.getMessage(), e)
+ }
+
+ Collection<ConfigAttribute> all = new HashSet<ConfigAttribute>()
+ for (Collection<ConfigAttribute> configs : _compiled.values()) {
+ all.addAll(configs)
+ }
+ return Collections.unmodifiableCollection(all)
+ }
+
+ /**
+ * Dependency injection for the url matcher.
+ * @param urlMatcher the matcher
+ */
+ public void setUrlMatcher(final UrlMatcher urlMatcher) {
+ _urlMatcher = urlMatcher
+ _stripQueryStringFromUrls = _urlMatcher instanceof AntUrlPathMatcher
+ }
+
+ /**
+ * Dependency injection for whether to reject if there's no matching rule.
+ * @param reject if true, reject access unless there's a pattern for the specified resource
+ */
+ public void setRejectIfNoRule(final boolean reject) {
+ _rejectIfNoRule = reject
+ }
+
+ protected String lowercaseAndStripQuerystring(final String url) {
+
+ String fixed = url
+
+ if (getUrlMatcher().requiresLowerCaseUrl()) {
+ fixed = fixed.toLowerCase()
+ }
+
+ if (_stripQueryStringFromUrls) {
+ int firstQuestionMarkIndex = fixed.indexOf("?")
+ if (firstQuestionMarkIndex != -1) {
+ fixed = fixed.substring(0, firstQuestionMarkIndex)
+ }
+ }
+
+ return fixed
+ }
+
+ protected UrlMatcher getUrlMatcher() {
+ return _urlMatcher
+ }
+
+ /**
+ * For debugging.
+ * @return an unmodifiable map of {@link AnnotationFilterInvocationDefinition}ConfigAttributeDefinition
+ * keyed by compiled patterns
+ */
+ public Map<Object, Collection<ConfigAttribute>> getConfigAttributeMap() {
+ return Collections.unmodifiableMap(_compiled)
+ }
+
+ // fixes extra spaces, trailing commas, etc.
+ protected List<String> split(final String value) {
+ if (!value.startsWith("ROLE_") && !value.startsWith("IS_")) {
+ // an expression
+ return Collections.singletonList(value)
+ }
+
+ String[] parts = StringUtils.commaDelimitedListToStringArray(value)
+ List<String> cleaned = new ArrayList<String>()
+ for (String part : parts) {
+ part = part.trim()
+ if (part.length() > 0) {
+ cleaned.add(part)
+ }
+ }
+ return cleaned
+ }
+
+ protected void compileAndStoreMapping(final String pattern, final List<String> tokens) {
+
+ Object key = getUrlMatcher().compile(pattern)
+
+ Collection<ConfigAttribute> configAttributes = buildConfigAttributes(tokens)
+
+ Collection<ConfigAttribute> replaced = storeMapping(key,
+ Collections.unmodifiableCollection(configAttributes))
+ if (replaced != null) {
+ _log.warn("replaced rule for '" + key + "' with roles " + replaced +
+ " with roles " + configAttributes)
+ }
+ }
+
+ protected Collection<ConfigAttribute> buildConfigAttributes(final Collection<String> tokens) {
+ Collection<ConfigAttribute> configAttributes = new HashSet<ConfigAttribute>()
+ for (String token : tokens) {
+ ConfigAttribute config = new SecurityConfig(token)
+ if (supports(config)) {
+ configAttributes.add(config)
+ }
+ else {
+ Expression expression = _expressionHandler.getExpressionParser().parseExpression(token)
+ configAttributes.add(new WebExpressionConfigAttribute(expression))
+ }
+ }
+ return configAttributes
+ }
+
+ protected boolean supports(final ConfigAttribute config) {
+ voters.any { supportsAttribute(config, it) } || config.getAttribute().startsWith("RUN_AS")
+ }
+
+ private boolean supportsAttribute(final ConfigAttribute config, final AccessDecisionVoter voter) {
+ voter?.supports(config)
+ }
+
+ protected Collection<ConfigAttribute> storeMapping(final Object key,
+ final Collection<ConfigAttribute> configAttributes) {
+ return _compiled.put(key, configAttributes)
+ }
+
+ protected void resetConfigs() {
+ _compiled.clear()
+ }
+
+ /**
+ * For admin/debugging - find all config attributes that apply to the specified URL.
+ * @param url the URL
+ * @return matching attributes
+ */
+ public Collection<ConfigAttribute> findMatchingAttributes(final String url) {
+ for (Map.Entry<Object, Collection<ConfigAttribute>> entry : _compiled.entrySet()) {
+ if (_urlMatcher.pathMatchesUrl(entry.getKey(), url)) {
+ return entry.getValue()
+ }
+ }
+ return Collections.emptyList()
+ }
+
+ /**
+ * Dependency injection for the expression handler.
+ * @param handler the handler
+ */
+ public void setExpressionHandler(final WebSecurityExpressionHandler handler) {
+ _expressionHandler = handler
+ }
+ protected WebSecurityExpressionHandler getExpressionHandler() {
+ return _expressionHandler
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
+ */
+ public void afterPropertiesSet() {
+ Assert.notNull(_urlMatcher, "url matcher is required")
+ }
+}
View
333 src/groovy/es/osoco/grails/plugins/otp/access/AnnotationMultipleVoterFilterInvocationDefinition.groovy
@@ -0,0 +1,333 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.access
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang.WordUtils;
+import org.codehaus.groovy.grails.commons.ControllerArtefactHandler;
+import org.codehaus.groovy.grails.commons.GrailsApplication;
+import org.codehaus.groovy.grails.commons.GrailsClass;
+import org.codehaus.groovy.grails.commons.GrailsControllerClass;
+import org.codehaus.groovy.grails.web.context.ServletContextHolder;
+import org.codehaus.groovy.grails.web.mapping.UrlMappingInfo;
+import org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder;
+import org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap;
+import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest;
+import org.codehaus.groovy.grails.web.util.WebUtils;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.web.FilterInvocation;
+import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Based on Grails Spring Security Core's <code>org.codehaus.groovy.grails.plugins.springsecurity.AnnotationFilterInvocationDefinition</code>.
+ *
+ * @author <a href='mailto:rafael.luque@osoco.es'>Rafael Luque</a>
+ */
+public class AnnotationMultipleVoterFilterInvocationDefinition extends AbstractMultipleVoterFilterInvocationDefinition {
+
+ private static final List<String> ANNOTATION_CLASS_NAMES = Arrays.asList(
+ grails.plugins.springsecurity.Secured.class.getName(),
+ org.springframework.security.access.annotation.Secured.class.getName());
+
+ private UrlMappingsHolder _urlMappingsHolder;
+ private GrailsApplication _application;
+
+ @Override
+ protected String determineUrl(final FilterInvocation filterInvocation) {
+ HttpServletRequest request = filterInvocation.getHttpRequest();
+ HttpServletResponse response = filterInvocation.getHttpResponse();
+
+ GrailsWebRequest existingRequest = WebUtils.retrieveGrailsWebRequest();
+
+ String requestUrl = request.getRequestURI().substring(request.getContextPath().length());
+
+ String url = null;
+ try {
+ GrailsWebRequest grailsRequest = new GrailsWebRequest(request, response,
+ ServletContextHolder.getServletContext());
+ WebUtils.storeGrailsWebRequest(grailsRequest);
+
+ Map<String, Object> savedParams = copyParams(grailsRequest);
+
+ for (UrlMappingInfo mapping : _urlMappingsHolder.matchAll(requestUrl)) {
+ configureMapping(mapping, grailsRequest, savedParams);
+
+ url = findGrailsUrl(mapping);
+ if (url != null) {
+ break;
+ }
+ }
+ }
+ finally {
+ if (existingRequest == null) {
+ WebUtils.clearGrailsWebRequest();
+ }
+ else {
+ WebUtils.storeGrailsWebRequest(existingRequest);
+ }
+ }
+
+ if (!StringUtils.hasLength(url)) {
+ // probably css/js/image
+ url = requestUrl;
+ }
+
+ return lowercaseAndStripQuerystring(url);
+ }
+
+ protected String findGrailsUrl(final UrlMappingInfo mapping) {
+
+ String uri = mapping.getURI();
+ if (StringUtils.hasLength(uri)) {
+ return uri;
+ }
+
+ String actionName = mapping.getActionName();
+ if (!StringUtils.hasLength(actionName)) {
+ actionName = "";
+ }
+
+ String controllerName = mapping.getControllerName();
+
+ if (isController(controllerName, actionName)) {
+ if (!StringUtils.hasLength(actionName) || "null".equals(actionName)) {
+ actionName = "index";
+ }
+ return ("/" + controllerName + "/" + actionName).trim();
+ }
+
+ return null;
+ }
+
+ private boolean isController(final String controllerName, final String actionName) {
+ return _application.getArtefactForFeature(ControllerArtefactHandler.TYPE,
+ "/" + controllerName + "/" + actionName) != null;
+ }
+
+ private void configureMapping(final UrlMappingInfo mapping, final GrailsWebRequest grailsRequest,
+ final Map<String, Object> savedParams) {
+
+ // reset params since mapping.configure() sets values
+ GrailsParameterMap params = grailsRequest.getParams();
+ params.clear();
+ params.putAll(savedParams);
+
+ mapping.configure(grailsRequest);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> copyParams(final GrailsWebRequest grailsRequest) {
+ return new HashMap<String, Object>(grailsRequest.getParams());
+ }
+
+ /**
+ * Called by the plugin to set controller role info.<br/>
+ *
+ * Reinitialize by calling <code>ctx.objectDefinitionSource.initialize(
+ * ctx.authenticateService.securityConfig.security.annotationStaticRules,
+ * ctx.grailsUrlMappingsHolder,
+ * grailsApplication.controllerClasses)</code>
+ *
+ * @param staticRules keys are URL patterns, values are role or token names for that pattern
+ * @param urlMappingsHolder mapping holder
+ * @param controllerClasses all controllers
+ */
+ public void initialize(final Map<String, Collection<String>> staticRules,
+ final UrlMappingsHolder urlMappingsHolder, final GrailsClass[] controllerClasses) {
+
+ Map<String, Map<String, Set<String>>> actionRoleMap = new HashMap<String, Map<String,Set<String>>>();
+ Map<String, Set<String>> classRoleMap = new HashMap<String, Set<String>>();
+
+ Assert.notNull(staticRules, "staticRules map is required");
+ Assert.notNull(urlMappingsHolder, "urlMappingsHolder is required");
+
+ resetConfigs();
+
+ _urlMappingsHolder = urlMappingsHolder;
+
+ for (GrailsClass controllerClass : controllerClasses) {
+ findControllerAnnotations((GrailsControllerClass)controllerClass, actionRoleMap, classRoleMap);
+ }
+
+ compileActionMap(actionRoleMap);
+ compileClassMap(classRoleMap);
+ compileStaticRules(staticRules);
+
+ if (_log.isTraceEnabled()) {
+ _log.trace("configs: " + getConfigAttributeMap());
+ }
+ }
+
+ private void compileActionMap(final Map<String, Map<String, Set<String>>> map) {
+ for (Map.Entry<String, Map<String, Set<String>>> controllerEntry : map.entrySet()) {
+ String controllerName = controllerEntry.getKey();
+ Map<String, Set<String>> actionRoles = controllerEntry.getValue();
+ for (Map.Entry<String, Set<String>> actionEntry : actionRoles.entrySet()) {
+ String actionName = actionEntry.getKey();
+ Set<String> tokens = actionEntry.getValue();
+ storeMapping(controllerName, actionName, tokens, false);
+ if (actionName.endsWith("Flow")) {
+ // WebFlow actions end in Flow but are accessed without the suffix, so guard both
+ storeMapping(controllerName, actionName.substring(0, actionName.length() - 4), tokens, false);
+ }
+ }
+ }
+ }
+
+ private void compileClassMap(final Map<String, Set<String>> classRoleMap) {
+ for (Map.Entry<String, Set<String>> entry : classRoleMap.entrySet()) {
+ String controllerName = entry.getKey();
+ Set<String> tokens = entry.getValue();
+ storeMapping(controllerName, null, tokens, false);
+ }
+ }
+
+ private void compileStaticRules(final Map<String, Collection<String>> staticRules) {
+ for (Map.Entry<String, Collection<String>> entry : staticRules.entrySet()) {
+ String pattern = entry.getKey();
+ Collection<String> tokens = entry.getValue();
+ storeMapping(pattern, null, tokens, true);
+ }
+ }
+
+ private void storeMapping(final String controllerNameOrPattern, final String actionName,
+ final Collection<String> tokens, final boolean isPattern) {
+
+ String fullPattern;
+ if (isPattern) {
+ fullPattern = controllerNameOrPattern;
+ }
+ else {
+ StringBuilder sb = new StringBuilder();
+ sb.append('/').append(controllerNameOrPattern);
+ if (actionName != null) {
+ sb.append('/').append(actionName);
+ }
+ sb.append("/**");
+ fullPattern = sb.toString();
+ }
+
+ Collection<ConfigAttribute> configAttributes = buildConfigAttributes(tokens);
+
+ Object key = getUrlMatcher().compile(fullPattern);
+ Collection<ConfigAttribute> replaced = storeMapping(key, configAttributes);
+ if (replaced != null) {
+ _log.warn("replaced rule for '" + key + "' with tokens " + replaced
+ + " with tokens " + configAttributes);
+ }
+ }
+
+ private void findControllerAnnotations(final GrailsControllerClass controllerClass,
+ final Map<String, Map<String, Set<String>>> actionRoleMap,
+ final Map<String, Set<String>> classRoleMap) {
+
+ Class<?> clazz = controllerClass.getClazz();
+ String controllerName = WordUtils.uncapitalize(controllerClass.getName());
+
+ Annotation annotation = findAnnotation(clazz.getAnnotations());
+ if (annotation != null) {
+ classRoleMap.put(controllerName, asSet(getValue(annotation)));
+ }
+
+ Map<String, Set<String>> annotatedClosureNames = findActionRoles(clazz);
+ if (annotatedClosureNames != null) {
+ actionRoleMap.put(controllerName, annotatedClosureNames);
+ }
+ }
+
+ private Map<String, Set<String>> findActionRoles(final Class<?> clazz) {
+ // since action closures are defined as "def foo = ..." they're
+ // fields, but they end up as private
+ Map<String, Set<String>> actionRoles = new HashMap<String, Set<String>>();
+ for (Field field : clazz.getDeclaredFields()) {
+ Annotation annotation = findAnnotation(field.getAnnotations());
+ if (annotation != null) {
+ actionRoles.put(field.getName(), asSet(getValue(annotation)));
+ }
+ }
+ for (Method method : clazz.getDeclaredMethods()) {
+ Annotation annotation = findAnnotation(method.getAnnotations());
+ if (annotation != null) {
+ actionRoles.put(method.getName(), asSet(getValue(annotation)));
+ }
+ }
+ return actionRoles;
+ }
+
+ private Annotation findAnnotation(Annotation[] annotations) {
+ for (Annotation annotation : annotations) {
+ if (ANNOTATION_CLASS_NAMES.contains(annotation.annotationType().getName())) {
+ return annotation;
+ }
+ }
+ return null;
+ }
+
+ private String[] getValue(Annotation annotation) {
+ if (annotation instanceof grails.plugins.springsecurity.Secured) {
+ return ((grails.plugins.springsecurity.Secured)annotation).value();
+ }
+ return ((org.springframework.security.access.annotation.Secured)annotation).value();
+ }
+
+ private Set<String> asSet(final String[] strings) {
+ Set<String> set = new HashSet<String>();
+ for (String string : strings) {
+ set.add(string);
+ }
+ return set;
+ }
+
+ /**
+ * Dependency injection for the application.
+ * @param application the application
+ */
+ public void setApplication(GrailsApplication application) {
+ _application = application;
+ }
+}
View
87 ...groovy/es/osoco/grails/plugins/otp/access/InterceptUrlMapMultipleVoterFilterInvocatioinDefinition.groovy
@@ -0,0 +1,87 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.access
+
+import javax.servlet.http.HttpServletRequest
+
+import org.springframework.security.web.FilterInvocation
+
+/**
+ * Class based on Grails Spring Security Core's <code>org.codehaus.groovy.grails.plugins.springsecurity.IntercepUrlFilterInvocationDefinition</code>.
+ *
+ * @author <a href='mailto:rafael.luque@osoco.es'>Rafael Luque</a>
+ */
+public class InterceptUrlMapMultipleVoterFilterInvocationDefinition extends AbstractMultipleVoterFilterInvocationDefinition {
+
+ private boolean _initialized;
+
+ @Override
+ protected String determineUrl(final FilterInvocation filterInvocation) {
+ HttpServletRequest request = filterInvocation.getHttpRequest();
+ String requestUrl = request.getRequestURI().substring(request.getContextPath().length());
+ return lowercaseAndStripQuerystring(requestUrl);
+ }
+
+ @Override
+ protected void initialize() {
+ if (_initialized) {
+ return;
+ }
+
+ reset();
+ }
+
+ @Override
+ protected boolean stopAtFirstMatch() {
+ return true;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void reset() {
+ Object map = ReflectionUtils.getConfigProperty("interceptUrlMap");
+ if (!(map instanceof Map)) {
+ _log.warn("interceptUrlMap config property isn't a Map");
+ return;
+ }
+
+ resetConfigs();
+
+ Map<String, List<String>> data = ReflectionUtils.splitMap((Map<String, Object>)map);
+ for (Map.Entry<String, List<String>> entry : data.entrySet()) {
+ compileAndStoreMapping(entry.getKey(), entry.getValue());
+ }
+
+ _initialized = true;
+ }
+}
View
94 src/groovy/es/osoco/grails/plugins/otp/access/OneTimePasswordVoter.groovy
@@ -0,0 +1,94 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.access
+
+import es.osoco.grails.plugins.otp.authentication.OneTimePasswordAuthenticationToken
+import es.osoco.grails.plugins.otp.userdetails.GrailsOtpUser
+
+import org.springframework.security.access.AccessDecisionVoter
+
+import org.springframework.security.access.ConfigAttribute
+import org.springframework.security.access.vote.AuthenticatedVoter
+import org.springframework.security.authentication.AuthenticationTrustResolver
+import org.springframework.security.authentication.AuthenticationTrustResolverImpl
+import org.springframework.security.core.Authentication
+
+/**
+ * An {@link AccessDecisionVoter} implementation that votes if a {@link ConfigAttribute#getAttribute()}
+ * of <code>IS_AUTHENTICATED_OTP</code> value is present.
+ *
+ * <p>
+ * The current <code>Authentication</code> will be inspected to determine if the principal has a particular
+ * level of authentication.
+ *
+ * <p>
+ * All comparisons are case sensitive.
+ *
+ * @author Rafael Luque
+ */
+class OneTimePasswordVoter implements AccessDecisionVoter {
+
+ static final String IS_AUTHENTICATED_OTP = "IS_AUTHENTICATED_OTP"
+
+ public boolean supports(ConfigAttribute attribute) {
+ attribute?.attribute == OneTimePasswordVoter.IS_AUTHENTICATED_OTP
+ }
+
+ /**
+ * This implementation supports any type of class, because it does not query the presented secure object.
+ *
+ * @param clazz the secure object
+ *
+ * @return always <code>true</code>
+ */
+ public boolean supports(Class<?> clazz) {
+ true
+ }
+
+ public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
+ int result = ACCESS_ABSTAIN
+
+ for (ConfigAttribute attribute : attributes) {
+ if (supports(attribute)) {
+ result = isOtpAuthenticated(authentication) ? ACCESS_GRANTED : ACCESS_DENIED
+ }
+ }
+
+ result
+ }
+
+ private boolean isOtpAuthenticated(Authentication authentication) {
+ authentication.principal instanceof GrailsOtpUser
+ }
+
+}
View
98 src/groovy/es/osoco/grails/plugins/otp/access/RequestmapMultipleVoterFilterInvocationDefinition.groovy
@@ -0,0 +1,98 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.access
+
+import javax.servlet.http.HttpServletRequest
+
+import org.springframework.security.web.FilterInvocation
+
+/**
+ * Class based on Grails Spring Security Core's <code><code>org.codehaus.groovy.grails.plugins.springsecurity.RequestmapFilterInvocationDefinition</code>.
+ *
+ * @author <a href='mailto:rafael.luque@osoco.es'>Rafael Luque</a>
+ */
+public class RequestmapMultipleVoterFilterInvocationDefinition extends AbstractMultipleVoterFilterInvocationDefinition {
+
+ private boolean _initialized;
+
+ @Override
+ protected String determineUrl(final FilterInvocation filterInvocation) {
+ HttpServletRequest request = filterInvocation.getHttpRequest();
+ String requestUrl = request.getRequestURI().substring(request.getContextPath().length());
+ return lowercaseAndStripQuerystring(requestUrl);
+ }
+
+ @Override
+ protected void initialize() {
+ if (_initialized) {
+ return;
+ }
+
+ try {
+ reset();
+ _initialized = true;
+ }
+ catch (RuntimeException e) {
+ _log.warn("Exception initializing; this is ok if it's at startup and due " +
+ "to GORM not being initialized yet since the first web request will " +
+ "re-initialize. Error message is: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Call at startup or when <code>Requestmap</code> instances have been added, removed, or changed.
+ */
+ @Override
+ public synchronized void reset() {
+ Map<String, String> data = loadRequestmaps();
+ resetConfigs();
+
+ for (Map.Entry<String, String> entry : data.entrySet()) {
+ compileAndStoreMapping(entry.getKey(), split(entry.getValue()));
+ }
+
+ if (_log.isTraceEnabled()) _log.trace("configs: " + getConfigAttributeMap());
+ }
+
+ protected Map<String, String> loadRequestmaps() {
+ Map<String, String> data = new HashMap<String, String>();
+
+ for (Object requestmap : ReflectionUtils.loadAllRequestmaps()) {
+ String urlPattern = ReflectionUtils.getRequestmapUrl(requestmap);
+ String configAttribute = ReflectionUtils.getRequestmapConfigAttribute(requestmap);
+ data.put(urlPattern, configAttribute);
+ }
+
+ return data;
+ }
+}
View
179 src/groovy/es/osoco/grails/plugins/otp/access/TwoFactorDecisionManager.groovy
@@ -0,0 +1,179 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.access
+
+import es.osoco.grails.plugins.otp.authentication.TwoFactorAuthenticationException
+import es.osoco.grails.plugins.otp.authentication.TwoFactorInsufficientAuthenticationException
+
+import org.springframework.beans.factory.InitializingBean
+import org.springframework.context.MessageSource
+import org.springframework.context.MessageSourceAware
+import org.springframework.context.support.MessageSourceAccessor
+import org.springframework.security.core.SpringSecurityMessageSource
+
+import org.codehaus.groovy.grails.plugins.springsecurity.AuthenticatedVetoableDecisionManager
+
+import org.springframework.security.access.AccessDecisionVoter
+import org.springframework.security.access.AccessDecisionManager
+import org.springframework.security.access.AccessDeniedException
+import org.springframework.security.access.ConfigAttribute
+import org.springframework.security.access.vote.AbstractAccessDecisionManager
+import org.springframework.security.access.vote.AuthenticatedVoter
+import org.springframework.security.authentication.InsufficientAuthenticationException
+import org.springframework.security.core.Authentication
+
+import org.springframework.util.Assert
+
+/**
+ * Uses the affirmative-based logic for roles, i.e. any in the list will grant access, but allows
+ * an authenticated voter to 'veto' access. This allows specification of roles and
+ * <code>IS_AUTHENTICATED_FULLY</code> on one line in SecurityConfig.groovy.
+ *
+ * @author <a href='mailto:rafael.luque@osoco.es'>Rafael Luque</a>
+ */
+public class TwoFactorDecisionManager implements AccessDecisionManager, InitializingBean, MessageSourceAware {
+
+ protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor()
+
+ private AccessDecisionManager firstFactorDecisionManager
+
+ private AccessDecisionVoter twoFactorDecisionVoter;
+
+ public void afterPropertiesSet() throws Exception {
+ Assert.notNull(messages, "A message source must be set")
+ Assert.notNull(firstFactorDecisionManager, "A first-factor decision manager is required")
+ Assert.notNull(twoFactorDecisionVoter, "A two-factor voter is required")
+ }
+
+ public void setMessageSource(MessageSource messageSource) {
+ messages = new MessageSourceAccessor(messageSource);
+ }
+
+ public AccessDecisionManager getFirstFactorDecisionManager() {
+ firstFactorDecisionManager
+ }
+
+ public void setFirstFactorDecisionManager(AccessDecisionManager anAccessDecisionManager) {
+ firstFactorDecisionManager = anAccessDecisionManager
+ }
+
+ public AccessDecisionVoter getTwoFactorDecisionVoter() {
+ twoFactorDecisionVoter
+ }
+
+ public void setTwoFactorDecisionVoter(AccessDecisionVoter anAccessDecisionVoter) {
+ twoFactorDecisionVoter = anAccessDecisionVoter
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.springframework.security.access.AccessDecisionManager#decide(org.springframework.security.core.Authentication, java.lang.Object, java.util.Collection)
+ *
+ * The method can throw the following exceptions to be managed by the {@link ExceptionTranslationFilter}:
+ * <ul>
+ * <li>AccessDeniedException</li>
+ * <li>AccessDeniedWithTwoFactorException</li>
+ * <li>TwoFactorInsufficientAuthenticationException</li>
+ * </ul>
+ */
+ public void decide(final Authentication authentication, final Object object, final Collection<ConfigAttribute> configAttributes)
+ throws AccessDeniedException, InsufficientAuthenticationException {
+
+ try {
+ firstFactorDecisionManager.decide(authentication, object, configAttributes)
+ checkTwoFactorVoter(authentication, object, configAttributes)
+ } catch (AccessDeniedException ae) {
+ try {
+ checkTwoFactorVoter(authentication, object, configAttributes)
+ throw ae
+ } catch (InsufficientAuthenticationException iae) {
+ throw new TwoFactorAuthenticationException(messages.getMessage(
+ "TwoFactorDecisionManager.twoFactorAuthenticationException",
+ "Access is denied and a two-factor authentication will be required"))
+ }
+ }
+
+ }
+
+ public boolean supports(ConfigAttribute attribute) {
+ firstFactorDecisionManager.supports(attribute) || twoFactorDecisionVoter.supports(attribute)
+ }
+
+ /**
+ * Iterates through all <code>AccessDecisionVoter</code>s and ensures each can support the presented class.
+ * <p/>
+ * If one or more voters cannot support the presented class, <code>false</code> is returned.
+ * </p>
+ *
+ * @param clazz the type of secured object being presented
+ * @return true if this type is supported
+ */
+ public boolean supports(Class<?> clazz) {
+ firstFactorDecisionManager.supports(clazz) || twoFactorDecisionVoter.supports(clazz)
+ }
+
+ /**
+ * Allow the {@link TwoFactorVoter} to veto. If the voter denies,
+ * throw an {@link InsufficientAuthenticationException} exception;
+ * if it grants, returns <code>true</code>;
+ * otherwise returns <code>false</code> if it abstains.
+ */
+ private boolean checkTwoFactorVoter(final Authentication authentication, final Object object,
+ final Collection<ConfigAttribute> configAttributes) {
+
+ boolean grant = false
+ AccessDecisionVoter voter = getTwoFactorDecisionVoter()
+ if (voter) {
+ int result = voter.vote(authentication, object, configAttributes)
+ switch (result) {
+ case AccessDecisionVoter.ACCESS_GRANTED:
+ grant = true
+ break
+ case AccessDecisionVoter.ACCESS_DENIED:
+ twoFactorDeny()
+ break
+ default: // abstain
+ break
+ }
+ }
+
+ return grant
+ }
+
+ private void twoFactorDeny() {
+ throw new TwoFactorInsufficientAuthenticationException(messages.getMessage(
+ "TwoFactorDecisionManager.insufficientAuthentication",
+ "Access is denied because of two-factor authentication pending"))
+ }
+
+}
View
61 src/groovy/es/osoco/grails/plugins/otp/authentication/NopAuthenticationSuccessHandler.groovy
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.authentication
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.Authentication;
+
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler
+
+/**
+ * Strategy used to handle a successful user authentication.
+ *
+ * @author <a href='mailto:rafael.luque@osoco.es'>Rafael Luque</a>
+ */
+public class NopAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+ /**
+ * Called when a user has been successfully authenticated.
+ *
+ * @param request the request which caused the successful authentication
+ * @param response the response
+ * @param authentication the <tt>Authentication</tt> object which was created during the authentication process.
+ */
+ void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
+ Authentication authentication) throws IOException, ServletException {
+ }
+
+}
View
177 src/groovy/es/osoco/grails/plugins/otp/authentication/OneTimePasswordAuthenticationProvider.groovy
@@ -0,0 +1,177 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.authentication
+
+import org.springframework.security.core.Authentication
+import org.springframework.security.authentication.AuthenticationProvider
+import org.springframework.security.authentication.AuthenticationServiceException
+import org.springframework.security.authentication.BadCredentialsException
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider
+import org.springframework.security.authentication.encoding.PasswordEncoder
+import org.springframework.security.authentication.encoding.PlaintextPasswordEncoder
+import org.springframework.security.core.AuthenticationException
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.dao.DataAccessException
+import org.springframework.util.Assert
+
+/**
+ * A subclass of {@link DaoAuthenticationProvider} that supports the {@link OneTimePasswordAuthenticationToken}
+ * kind of token and checks the OTP validity delegating to the {@link OneTimePasswordService} as an additional
+ * check.
+ *
+ * @author <a href="mailto:rafael.luque@osoco.es">Rafael Luque</a>
+ */
+class OneTimePasswordAuthenticationProvider extends DaoAuthenticationProvider {
+
+ def oneTimePasswordService
+
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+
+ Assert.isInstanceOf(OneTimePasswordAuthenticationToken.class, authentication,
+ messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
+ "Only OneTimePasswordAuthenticationToken is supported"))
+
+ // Determine username
+ String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName()
+
+ boolean cacheWasUsed = true
+ UserDetails user = this.userCache.getUserFromCache(username)
+
+ if (user == null) {
+ cacheWasUsed = false
+
+ try {
+ user = retrieveUser(username, (OneTimePasswordAuthenticationToken) authentication)
+ } catch (UsernameNotFoundException notFound) {
+
+ if (hideUserNotFoundExceptions) {
+ throw new BadCredentialsException(messages.getMessage(
+ "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"))
+ } else {
+ throw notFound
+ }
+ }
+
+ Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract")
+ }
+
+ try {
+ preAuthenticationChecks.check(user)
+ additionalAuthenticationChecks(user, (OneTimePasswordAuthenticationToken) authentication)
+ } catch (AuthenticationException exception) {
+ if (cacheWasUsed) {
+ // There was a problem, so try again after checking
+ // we're using latest data (i.e. not from the cache)
+ cacheWasUsed = false
+ user = retrieveUser(username, (OneTimePasswordAuthenticationToken) authentication)
+ preAuthenticationChecks.check(user)
+ additionalAuthenticationChecks(user, (OneTimePasswordAuthenticationToken) authentication)
+ } else {
+ throw exception
+ }
+ }
+
+ postAuthenticationChecks.check(user)
+
+ if (!cacheWasUsed) {
+ this.userCache.putUserInCache(user)
+ }
+
+ Object principalToReturn = user
+
+ if (forcePrincipalAsString) {
+ principalToReturn = user.getUsername()
+ }
+
+ return createSuccessAuthentication(principalToReturn, authentication, user)
+ }
+
+ protected final UserDetails retrieveUser(String username, OneTimePasswordAuthenticationToken authentication)
+ throws AuthenticationException {
+ UserDetails loadedUser
+
+ try {
+ loadedUser = this.getUserDetailsService().loadUserByUsername(username)
+ }
+ catch (DataAccessException repositoryProblem) {
+ throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem)
+ }
+
+ if (loadedUser == null) {
+ throw new AuthenticationServiceException(
+ "UserDetailsService returned null, which is an interface contract violation")
+ }
+ return loadedUser
+ }
+
+ protected void additionalAuthenticationChecks(UserDetails userDetails,
+ OneTimePasswordAuthenticationToken authentication) throws AuthenticationException {
+
+ Object salt = null
+
+ logger.debug("OneTimePasswordAuthenticationProvider for authentication ${authentication.class.name}")
+
+ if (this.saltSource != null) {
+ salt = this.saltSource.getSalt(userDetails)
+ }
+
+ if (authentication.getCredentials() == null) {
+ logger.debug("Authentication failed: no credentials provided")
+
+ throw new BadCredentialsException(messages.getMessage(
+ "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"),
+ includeDetailsObject ? userDetails : null)
+ }
+
+ String presentedPassword = authentication.getCredentials().toString()
+
+ if (!oneTimePasswordService.isPasswordValid(presentedPassword, userDetails.secretKey)) {
+ logger.debug("Authentication failed: one time password is not valid")
+
+ throw new BadCredentialsException(messages.getMessage(
+ "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"),
+ includeDetailsObject ? userDetails : null)
+ }
+ }
+
+ public boolean supports(Class<? extends Object> authentication) {
+ boolean supported = OneTimePasswordAuthenticationToken.class.isAssignableFrom(authentication)
+ logger.debug "Supports ${authentication.class.name} ? $supported"
+ supported
+ }
+
+
+}
View
116 src/groovy/es/osoco/grails/plugins/otp/authentication/OneTimePasswordAuthenticationToken.groovy
@@ -0,0 +1,116 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.authentication
+
+import org.apache.commons.logging.Log
+import org.apache.commons.logging.LogFactory
+
+import org.springframework.security.authentication.AbstractAuthenticationToken
+import org.springframework.security.core.GrantedAuthority
+
+/**
+ * An {@link org.springframework.security.core.Authentication} implementation that is designed for simple presentation
+ * of a username and one-time password.
+ *
+ * This class is based on {@link UsernamePasswordAuthenticationToken}'s source.
+ *
+ * @author <a href="mailto:rafael.luque@osoco.es">Rafael Luque</a>
+ */
+class OneTimePasswordAuthenticationToken extends AbstractAuthenticationToken {
+
+ private final Object principal
+ private Object credentials
+
+ protected final Log logger = LogFactory.getLog(getClass())
+
+ /**
+ * This constructor can be safely used by any code that wishes to create a
+ * <code>UsernamePasswordAuthenticationToken</code>, as the {@link
+ * #isAuthenticated()} will return <code>false</code>.
+ *
+ */
+ public OneTimePasswordAuthenticationToken(Object principal, Object credentials) {
+ super(null)
+ this.principal = principal
+ this.credentials = credentials
+ setAuthenticated(false)
+ }
+
+ /**
+ * @deprecated use the list of authorities version
+ */
+ public OneTimePasswordAuthenticationToken(Object principal, Object credentials, GrantedAuthority[] authorities) {
+ this(principal, credentials, Arrays.asList(authorities))
+ }
+
+ /**
+ * This constructor should only be used by <code>AuthenticationManager</code> or <code>AuthenticationProvider</code>
+ * implementations that are satisfied with producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
+ * authentication token.
+ *
+ * @param principal
+ * @param credentials
+ * @param authorities
+ */
+ public OneTimePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
+ super(authorities)
+ this.principal = principal
+ this.credentials = credentials
+ super.setAuthenticated(true) // must use super, as we override
+ }
+
+
+ public Object getCredentials() {
+ return this.credentials
+ }
+
+ public Object getPrincipal() {
+ return this.principal
+ }
+
+ public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+ if (isAuthenticated) {
+ throw new IllegalArgumentException(
+ "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead")
+ }
+
+ super.setAuthenticated(false)
+ }
+
+ @Override
+ public void eraseCredentials() {
+ super.eraseCredentials()
+ credentials = null
+ }
+
+}
View
65 src/groovy/es/osoco/grails/plugins/otp/authentication/TwoFactorAuthenticationException.groovy
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.authentication
+
+import org.springframework.security.core.AuthenticationException
+
+/**
+ * Thrown if an {@link org.springframework.security.core.Authentication Authentication} object does not hold
+ * a required authority and the secured object requires also one-time password authentication.
+ *
+ * @author <a href="mailto:rafael.luque@osoco.es">Rafael Luque</a>
+ */
+class TwoFactorAuthenticationException extends AuthenticationException {
+
+ /**
+ * Constructs an <code>TwoFactorAuthenticationException</code> with the specified
+ * message.
+ *
+ * @param msg the detail message
+ */
+ public TwoFactorAuthenticationException(String msg) {
+ super(msg)
+ }
+
+ /**
+ * Constructs an <code>TwoFactorAuthenticationException</code> with the specified
+ * message and root cause.
+ *
+ * @param msg the detail message
+ * @param t root cause
+ */
+ public TwoFactorAuthenticationException(String msg, Throwable t) {
+ super(msg, t)
+ }
+}
View
66 src/groovy/es/osoco/grails/plugins/otp/authentication/TwoFactorInsufficientAuthenticationException.groovy
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.authentication
+
+import org.springframework.security.authentication.InsufficientAuthenticationException
+
+/**
+ * Thrown if an authentication request is rejected because the credentials are not sufficiently trusted
+ * because of the required one-time password is missing.
+ *
+ * @author <a href="mailto:rafael.luque@osoco.es">Rafael Luque</a>
+ */
+class TwoFactorInsufficientAuthenticationException extends InsufficientAuthenticationException {
+
+ /**
+ * Constructs an <code>InsufficientAuthenticationException</code> with the
+ * specified message.
+ *
+ * @param msg the detail message
+ */
+ public TwoFactorInsufficientAuthenticationException(String msg) {
+ super(msg)
+ }
+
+ /**
+ * Constructs an <code>InsufficientAuthenticationException</code> with the
+ * specified message and root cause.
+ *
+ * @param msg the detail message
+ * @param t root cause
+ */
+ public TwoFactorInsufficientAuthenticationException(String msg, Throwable t) {
+ super(msg, t)
+ }
+
+}
View
75 src/groovy/es/osoco/grails/plugins/otp/userdetails/GrailsOtpUser.groovy
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * ____ _________ _________
+ * / __ \/ ___/ __ \/ ___/ __ \
+ * / /_/ (__ ) /_/ / /__/ /_/ /
+ * \____/____/\____/\___/\____/
+ *
+ * ~ La empresa de los programadores profesionales ~
+ *
+ * | http://osoco.es
+ * |
+ * | Edificio Moma Lofts
+ * | Planta 3, Loft 18
+ * | Ctra. Mostoles-Villaviciosa, Km 0,2
+ * | Mostoles, Madrid 28935 Spain
+ *
+ * ====================================================================
+ *
+ * Copyright 2012 OSOCO. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package es.osoco.grails.plugins.otp.userdetails
+
+import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser
+
+import org.springframework.security.core.GrantedAuthority
+
+/**
+ * Extends the default GrailsUser class to contain the secretKey attribute required to calculate
+ * the one-time passwords.
+ *
+ * @author <a href='mailto:rafael.luque@osoco.es'>Rafael Luque</a>
+ */
+class GrailsOtpUser extends GrailsUser {
+
+ String secretKey
+