Skip to content

Commit

Permalink
Implement CAS auth
Browse files Browse the repository at this point in the history
  • Loading branch information
apardyl committed Mar 9, 2019
1 parent 4509c80 commit 553dfa2
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 14 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
124 changes: 124 additions & 0 deletions src/main/kotlin/pl/edu/uj/ii/ksi/mordor/configuration/CasConfig.kt
Original file line number Diff line number Diff line change
@@ -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<CasAssertionAuthenticationToken> {
private fun getStrOrNull(map: Map<String, Any?>, 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
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
7 changes: 6 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 553dfa2

Please sign in to comment.