From 553dfa2005336d6650b720d5488521f5c72b5872 Mon Sep 17 00:00:00 2001 From: Adam Pardyl Date: Sat, 9 Mar 2019 16:40:28 +0100 Subject: [PATCH] Implement CAS auth --- build.gradle | 1 + .../ii/ksi/mordor/configuration/CasConfig.kt | 124 ++++++++++++++++++ .../mordor/configuration/WebSecurityConfig.kt | 50 +++++-- .../ksi/mordor/controllers/LoginController.kt | 22 +++- src/main/resources/application.properties | 7 +- 5 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/CasConfig.kt diff --git a/build.gradle b/build.gradle index 8080cb1..434b5d2 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ dependencies { compile('org.springframework.boot:spring-boot-starter-security') compile("org.springframework.security:spring-security-ldap") compile('org.springframework.security.kerberos:spring-security-kerberos-client:1.0.1.RELEASE') + compile('org.springframework.security:spring-security-cas') compile('org.springframework.boot:spring-boot-starter-validation') compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.boot:spring-boot-starter-thymeleaf') diff --git a/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/CasConfig.kt b/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/CasConfig.kt new file mode 100644 index 0000000..0d440b4 --- /dev/null +++ b/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/CasConfig.kt @@ -0,0 +1,124 @@ +package pl.edu.uj.ii.ksi.mordor.configuration + +import java.net.URLEncoder +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import javax.servlet.http.HttpSessionEvent +import org.jasig.cas.client.session.SingleSignOutFilter +import org.jasig.cas.client.session.SingleSignOutHttpSessionListener +import org.jasig.cas.client.validation.Cas30ServiceTicketValidator +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.event.EventListener +import org.springframework.security.cas.ServiceProperties +import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken +import org.springframework.security.cas.authentication.CasAuthenticationProvider +import org.springframework.security.cas.web.CasAuthenticationEntryPoint +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.authentication.logout.LogoutFilter +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler +import pl.edu.uj.ii.ksi.mordor.persistence.entities.Role +import pl.edu.uj.ii.ksi.mordor.services.ExternalUser +import pl.edu.uj.ii.ksi.mordor.services.ExternalUserService + +@Configuration +class CasConfig( + @Value("\${mordor.site.address:}") private val siteUrl: String, + @Value("\${mordor.cas.url:}") private val casUrl: String, + @Value("\${mordor.cas.role.attribute:}") private val casRoleAttribute: String, + @Value("\${mordor.cas.role.admin:}") private val casAdmin: String, + @Value("\${mordor.cas.role.mod:}") private val casMod: String, + private val externalUserService: ExternalUserService +) { + fun casEnabled(): Boolean { + return casUrl.isNotBlank() + } + + @Bean + fun serviceProperties(): ServiceProperties { + val appLogin = "$siteUrl/cas" + val serviceProperties = ServiceProperties() + serviceProperties.service = appLogin + serviceProperties.isAuthenticateAllArtifacts = true + return serviceProperties + } + + fun casAuthenticationEntryPoint(): AuthenticationEntryPoint { + val entryPoint = object : CasAuthenticationEntryPoint() { + override fun createServiceUrl(request: HttpServletRequest?, response: HttpServletResponse?): String { + return this.serviceProperties.service + "?redirect=" + URLEncoder.encode(request!!.requestURI, "UTF-8") + } + } + entryPoint.loginUrl = "$casUrl/login" + entryPoint.serviceProperties = serviceProperties() + return entryPoint + } + + @Bean + fun ticketValidatorCas30(): Cas30ServiceTicketValidator { + return Cas30ServiceTicketValidator(casUrl) + } + + private inner class CasUserDetailsService : AuthenticationUserDetailsService { + private fun getStrOrNull(map: Map, key: String): String? { + return map.getOrDefault(key, null) as? String + } + + override fun loadUserDetails(token: CasAssertionAuthenticationToken): UserDetails { + val attrs = token.assertion.principal.attributes + + var role = Role.USER + + if (casRoleAttribute.isNotBlank() && attrs.containsKey(casRoleAttribute)) { + val groups = attrs[casRoleAttribute] as List<*> + when { + groups.contains(casAdmin) -> role = Role.ADMIN + groups.contains(casMod) -> role = Role.MOD + } + } + + val externalUser = ExternalUser(token.name, getStrOrNull(attrs, "mail"), + getStrOrNull(attrs, "givenName"), getStrOrNull(attrs, "sn"), role) + return User(token.name, "external", true, true, true, true, + externalUserService.loginExternalAccount(externalUser).permissions) + } + } + + fun casAuthenticationProvider(): CasAuthenticationProvider { + val provider = CasAuthenticationProvider() + provider.setServiceProperties(serviceProperties()) + provider.setTicketValidator(ticketValidatorCas30()) + provider.setAuthenticationUserDetailsService(CasUserDetailsService()) + provider.setKey("MORDOR_CAS") + return provider + } + + @EventListener + fun singleSignOutHttpSessionListener(event: HttpSessionEvent): SingleSignOutHttpSessionListener { + return SingleSignOutHttpSessionListener() + } + + fun singleSignOutFilter(): SingleSignOutFilter { + val singleSignOutFilter = SingleSignOutFilter() + singleSignOutFilter.setCasServerUrlPrefix(casUrl) + singleSignOutFilter.setIgnoreInitConfiguration(true) + return singleSignOutFilter + } + + @Bean + fun securityContextLogoutHandler(): SecurityContextLogoutHandler { + return SecurityContextLogoutHandler() + } + + fun logoutFilter(): LogoutFilter { + val logoutFilter = LogoutFilter( + "$casUrl/logout", + securityContextLogoutHandler()) + logoutFilter.setFilterProcessesUrl("/logout/cas") + return logoutFilter + } +} diff --git a/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/WebSecurityConfig.kt b/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/WebSecurityConfig.kt index d95a2eb..b72aef9 100644 --- a/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/WebSecurityConfig.kt +++ b/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/WebSecurityConfig.kt @@ -1,7 +1,10 @@ package pl.edu.uj.ii.ksi.mordor.configuration import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.security.cas.ServiceProperties +import org.springframework.security.cas.web.CasAuthenticationFilter import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity @@ -11,6 +14,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.security.web.authentication.logout.LogoutFilter import pl.edu.uj.ii.ksi.mordor.persistence.repositories.RememberMePersistentTokenRepository import pl.edu.uj.ii.ksi.mordor.services.LocalUserService @@ -22,7 +26,8 @@ class WebSecurityConfig( private val userService: LocalUserService, @Value("\${mordor.secret}") private val secret: String, @Value("\${mordor.ldap.url:}") private val ldapUrl: String, - private val ldapConfig: LdapConfig + private val ldapConfig: LdapConfig, + private val casConfig: CasConfig ) : WebSecurityConfigurerAdapter() { private inner class DelegatingUserDetailService : UserDetailsService { override fun loadUserByUsername(username: String): UserDetails { @@ -39,13 +44,25 @@ class WebSecurityConfig( } override fun configure(http: HttpSecurity) { - http.authorizeRequests().antMatchers("/", "/register/**").permitAll() + http.exceptionHandling().accessDeniedPage("/403/") + + val allowUrls = mutableListOf("/", "/error**") + if (casConfig.casEnabled()) { + http.httpBasic().authenticationEntryPoint(casConfig.casAuthenticationEntryPoint()) + .and().logout().logoutUrl("/logout/").logoutSuccessUrl("/logout/cas").permitAll() + .and().addFilterBefore(casConfig.singleSignOutFilter(), CasAuthenticationFilter::class.java) + .addFilterBefore(casConfig.logoutFilter(), LogoutFilter::class.java) + allowUrls.add("/cas**") + } else { + http.formLogin().loginPage("/login/").permitAll() + .and().rememberMe().key(secret).tokenRepository(tokenRepository) + .userDetailsService(DelegatingUserDetailService()) + .and().logout().logoutUrl("/logout/").permitAll() + allowUrls.add("/register/**") + allowUrls.add("/login/**") + } + http.authorizeRequests().antMatchers(*allowUrls.toTypedArray()).permitAll() .anyRequest().authenticated() - .and().formLogin().loginPage("/login/").permitAll() - .and().logout().logoutUrl("/logout/").permitAll() - .and().rememberMe().key(secret).tokenRepository(tokenRepository) - .userDetailsService(DelegatingUserDetailService()) - .and().exceptionHandling().accessDeniedPage("/403/") } override fun configure(web: WebSecurity) { @@ -54,10 +71,23 @@ class WebSecurityConfig( } override fun configure(auth: AuthenticationManagerBuilder) { - auth.userDetailsService(userService) + if (casConfig.casEnabled()) { + auth.authenticationProvider(casConfig.casAuthenticationProvider()) + } else { + auth.userDetailsService(userService) - if (ldapUrl.isNotEmpty()) { - auth.authenticationProvider(ldapConfig.ldapAuthenticationProvider) + if (ldapUrl.isNotEmpty()) { + auth.authenticationProvider(ldapConfig.ldapAuthenticationProvider) + } } } + + @Bean + @Throws(Exception::class) + fun casAuthenticationFilter(serviceProperties: ServiceProperties): CasAuthenticationFilter { + val filter = CasAuthenticationFilter() + filter.setServiceProperties(serviceProperties) + filter.setAuthenticationManager(authenticationManager()) + return filter + } } diff --git a/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/controllers/LoginController.kt b/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/controllers/LoginController.kt index 79c7d2b..dbbdc1f 100644 --- a/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/controllers/LoginController.kt +++ b/src/main/kotlin/pl/edu/uj/ii/ksi/mordor/controllers/LoginController.kt @@ -1,12 +1,28 @@ package pl.edu.uj.ii.ksi.mordor.controllers +import java.security.Principal +import org.springframework.beans.factory.annotation.Value +import org.springframework.security.access.annotation.Secured import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.servlet.View +import org.springframework.web.servlet.view.RedirectView @Controller -class LoginController { +class LoginController(@Value("\${mordor.site.address:}") private val siteUrl: String) { @GetMapping(value = ["/login/"]) - fun loginPage(): String { - return "login" + fun loginPage(principal: Principal?): String { + return if (principal != null) { + "redirect:/file/" + } else { + "login" + } + } + + @Secured + @GetMapping(value = ["/cas"]) + fun casLogin(@RequestParam("redirect") redirect: String): View { + return RedirectView(siteUrl + redirect) } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 007cb23..3eb9164 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -24,7 +24,7 @@ mordor.preview.max_text_bytes=1048576 mordor.preview.max_image_bytes=10485760 mordor.list_hidden_files=false # LDAP settings -#mordor.ldap.url=ldaps://afs1.matinf.uj.edu.pl/dc=matinf,dc=uj.edu.pl +#mordor.ldap.url= #mordor.ldap.user_dn= #mordor.ldap.password= #mordor.ldap.use_krb_auth=true @@ -39,6 +39,11 @@ mordor.list_hidden_files=false #mordor.ldap.user.email= #mordor.ldap.role.admin= #mordor.ldap.role.mod= +# delegate auth to CAS 3.0 (disables local login) +#mordor.cas.url= +#mordor.cas.role.attribute=groups +#mordor.cas.role.admin=/admins +#mordor.cas.role.mod=/mordor # Development settings #spring.thymeleaf.cache=false #mordor.enable_dev_data_loader=true