I am creating a backend application with spring boot and jwt for authentication.
My problem is that I can't get the Authorities to work the way I intend them to. I can login my user and get back a jwt. But when requesting a path on the server that is only allowed for a certain authority I get back a 200 even though I am not sending an authorization header.
Here is my code:
SecurityConfig.kt
@EnableWebSecurity
class SecurityConfig(
private val userDetailsService: UserDetailsService,
private val jwtAuthenticationFilter: JwtAuthenticationFilter)
: WebSecurityConfigurerAdapter() {
override fun configure(httpSecurity: HttpSecurity) {
httpSecurity.csrf().disable()
.authorizeRequests()
.antMatchers( "/${Constants.API_PATH}/${Constants.USER_PATH}/**") // translates to /api/v1/users/**
.hasAuthority("USER")
.antMatchers("/${Constants.API_PATH}/${Constants.EMAIL_VERIFICATION_PATH}/**")
.permitAll()
.antMatchers("/${Constants.API_PATH}/${Constants.LOGIN_PATH}")
.permitAll()
.anyRequest()
.authenticated()
httpSecurity.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
}
override fun configure(auth: AuthenticationManagerBuilder) {
auth.userDetailsService(userDetailsService)
.passwordEncoder(encoder())
}
@Bean
fun encoder(): PasswordEncoder {
return BCryptPasswordEncoder() // salts the password
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
}
LoginService.kt (don't know if this code is relevant, but maybe smth is wrong here)
@Service
class LoginService(private val authenticationManager: AuthenticationManager,
private val jwtProvider: JwtProvider,
private val userService: UserService) {
fun login(loginRequest: LoginRequest): LoginResponse {
// authenticate internally call UserDetailsService
val authenticate = authenticationManager.authenticate(
UsernamePasswordAuthenticationToken(loginRequest.email, loginRequest.password))
SecurityContextHolder.getContext().authentication = authenticate
//TODO implement logic for employer
val jwtToken = jwtProvider.generateToken(authenticate)
val jobseeker = jobseekerService.getJobseeker(loginRequest.email)
return LoginResponse(jwtToken, jobseeker)
}
UserDetailsServiceImpl.kt
@Service
class UserDetailsServiceImpl(private val userRepository: UserRepository) : UserDetailsService {
/* we don't use usernames, so we pass the email address here*/
override fun loadUserByUsername(username: String?): UserDetails {
val user = userRepository.findByEmail(username) ?: throw CustomException("jobseeker not found")
return org.springframework.security.core.userdetails.User(user.email, user.getHashedSecret(),
jobseeker.getActivated(), true, true, true,
getAuthorities("USER"))
}
private fun getAuthorities(authority: String) = singletonList(SimpleGrantedAuthority(authority))
}
JwtProvider.kt (I know that "secret" is not a good secret, it's just here for testing purposes)
@Service
class JwtProvider {
val secret: String = "secret"
// TODO: IMPORTANT -> the keystore is selfsigned and needs to be changed as soon as we get
// to the first production version
private lateinit var keyStore: KeyStore
@PostConstruct
fun init() {
try {
keyStore = KeyStore.getInstance("JKS")
// very helpful for resources: https://stackoverflow.com/questions/4301329/java-class-getresource-returns-null
val resourceAsStream: InputStream? = javaClass.getResourceAsStream("/key.jks")
keyStore.load(resourceAsStream, secret.toCharArray())
} catch (ex: Exception) {
throw CustomException("problem while loading keystore")
}
}
fun generateToken(authentication: Authentication): String {
val pricipal: User = authentication.principal as User
return Jwts.builder()
.setSubject(pricipal.username)
.signWith(getPrivateKey())
.compact()
}
private fun getPrivateKey(): PrivateKey {
return try {
keyStore.getKey("key", secret.toCharArray()) as PrivateKey
} catch (ex: Exception) {
logger.error(ex.message)
throw CustomException("problem while retreiving the private key for jwt signing")
}
}
private fun getPublicKey(): PublicKey {
return try {
keyStore.getCertificate("key").publicKey
} catch (ex: KeyStoreException) {
throw CustomException("problem while retreiving public key")
}
}
fun validateToken(jwt: String): Boolean {
// using parseClaimsJws() because the jwt is signed
Jwts.parserBuilder().setSigningKey(getPublicKey()).build().parseClaimsJws(jwt)
return true
}
fun getEmailFromJwt(jwt: String): String {
return Jwts.parserBuilder().setSigningKey(getPublicKey()).build().parseClaimsJws(jwt).body.subject
}
}
JwtAuthenticationFilter.kt
@Component
class JwtAuthenticationFilter(
private val jwtProvider: JwtProvider,
private val userDetailsService: UserDetailsService
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val jwt: String = getJwtFromRequest(request)
if (StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)) {
val email: String = jwtProvider.getEmailFromJwt(jwt)
val userDetails: UserDetails = userDetailsService.loadUserByUsername(email)
val authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
authentication.details = (WebAuthenticationDetailsSource().buildDetails(request))
SecurityContextHolder.getContext().authentication = authentication
}
filterChain.doFilter(request, response)
}
fun getJwtFromRequest(request: HttpServletRequest): String {
val bearerToken: String = request.getHeader("Authorization") ?: ""
// hasText() is needed because there are api without auth and this string could be null
if(StringUtils.hasText(bearerToken))
return bearerToken.substringAfter(" ")
return bearerToken
}
}
So when I now try GET http://localhost:8080/api/v1/users
or GET http://localhost:8080/api/v1/users/<uuid>
without the jwt as a bearer token I get back a 200 instead of a 403.
I have been trying stuff for hours now and am completey stuck.
I am also fairly new to kotlin and spring boot and would be grateful for any tips on writing the code I provided in a more elegant way.