← Back to list

Securing Your Spring boot with JWE: A Guide to Implementing JWT Encryption for User Authentication…

In the previous year, I was working on the back-end of a project and wanted to implement a user management and authentication and…

parsa_gh · 2023-07-02 17:50 · 12 claps · 5.3 min read
#spring-security-6 #jwe #jwt-authentication #kotlin #spring-boot
Open on Medium ↗
Wiki topics: BIZ · Business Strategy 📱 · Mobile Development 🔒 · Cybersecurity

Securing Your Spring boot with JWE: A Guide to Implementing JWT Encryption for User Authentication and Authorization

In the previous year, I was working on the back-end of a project and wanted to implement a user management and authentication and authorization system, also known as SSO, using Spring Security.

The app had Spring Boot version 2.7.8, with Kotlin as the programming language. The system I was working on was a microservice architecture and distributed through containerization.

If you are not familiar with Spring Security authorization and resource server, I suggest you do some research on them before reading this article.

After a few days of researching, I came across a lot of topics and ways to do the job. like using OAuth2 with third party SSO services.

All of them somehow fulfilled my need for an SSO, but none of them were appropriate for the project’s business.

Anyway, most of them were about Spring Security user authentication with a simple username and password. All the examples i found on the internet copied this topic. Some of them used user session authentication, while others used simple JWT without any encryption.

Some open-source third-party tools, like Keycloak, were available, but I found that using them was not appropriate for my use cases.

Also OAuth2 with spring authorization server was one of the options.

So, I didn’t find any useful solution and decided to read the entire Spring Security packages to find out how they work under the hood.

The scenario was also like this:

user can be signed in using mobile number and verification code or two step verification and so on.

After conducting my research and making sure that there is no easy way or built-in classes in Spring Security to achieve my goal, I decided to utilize some of the features of Spring Security OAuth2 and Spring Resource Server to do the job.

The one thing you have to notice is that I didn’t want to use the full OAuth2 protocols, which involves redirecting the user to an authorization server to enter some user password and then redirecting them back, due to business rules.

So, after some planning, I found these steps that had to be implemented:

  1. Generating an encrypted JWT (JWE) with asymmetric keys as soon as the user logs into the system.
  2. The tokens are refresh and access token. Only the access token, with a short expiration time, contains the user info.
  3. Validating the user token, extracting its info and roles that are included in the body of the JWT, and accessing them anywhere in the app contexts.
  4. Every resource server can decrypt JWEs and authorize user access to its resources.
  5. The resource servers had to be completely stateless.
  6. Every service in my microservice environment is a Spring Security Resource Server, including the SSO service, which is responsible for logging in the user and generating JWE.

First, let’s write the core code to get Spring Security up and running in the SSO service.

import mu.KotlinLogging
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.builders.WebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler
import org.springframework.security.web.SecurityFilterChain
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource

@Configuration
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
class SecurityConfig constructor(
    private val keyUtils: KeyUtils,
    private val refreshJwtConverter: RefreshJwtConverter,
    private val accessJwtToUserConverter: AccessJwtToUserConverter,
) {
    private val logger = KotlinLogging.logger {}

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .headers {
                it.xssProtection { xss ->
                    xss.xssProtectionEnabled(true)
                }
            }
            .csrf().disable()
            .cors().and()
            .httpBasic().disable()
            .oauth2ResourceServer {
                it.jwt { jwt -> jwt.jwtAuthenticationConverter(accessJwtToUserConverter) }
                it.authenticationEntryPoint(AuthExceptionHandler())
            }
            .sessionManagement {
                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
            .exceptionHandling {
                it.accessDeniedHandler(BearerTokenAccessDeniedHandler())
            }
            .authorizeHttpRequests {
                it.antMatchers("/login/**").permitAll()
                it.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                    .anyRequest()
                    .authenticated()
            }
        return http.build()
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.allowCredentials = true
        configuration.allowedOriginPatterns = listOf("YOUR_DOMAIN_NAMES OR IP ADDRESSES")
        configuration.allowedMethods = listOf("OPTIONS", "HEAD", "GET", "POST", "PUT", "DELETE", "PATCH")
        configuration.allowedHeaders = listOf(
            "Authorization", "Origin", "Accept", "Content-Type", "Content-Encoding",
            "Content-Disposition", "withCredentials", "X-Requested-With"
        )
        configuration.exposedHeaders = listOf("Authorization")
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", configuration)
        return source
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

    @Bean
    @Primary
    fun jwtAccessTokenDecoder(): JwtDecoder {
        return JwtDecryption(keyUtils.getAccessTokenPrivateKey(), keyUtils.getAccessTokenPublicKey())
    }

    @Bean
    @Primary
    fun jwtAccessTokenAuthProvider(): JwtAuthenticationProvider {
        return JwtAuthenticationProvider(jwtAccessTokenDecoder())
    }

    @Bean
    @Qualifier("jwtRefreshTokenDecoder")
    fun jwtRefreshTokenDecoder(): JwtDecoder {
        return JwtDecryption(keyUtils.getRefreshTokenPrivateKey(), keyUtils.getRefreshTokenPublicKey())
    }

    @Bean
    @Qualifier("jwtRefreshTokenAuthProvider")
    fun jwtRefreshTokenAuthProvider(): JwtAuthenticationProvider {
        val authProvider = JwtAuthenticationProvider(jwtRefreshTokenDecoder())
        authProvider.setJwtAuthenticationConverter(refreshJwtConverter)
        return authProvider
    }

As you can see, the whole process of configuring Spring Security to validate any request to the server is very simple and clean.

So, I use the OAuth2ResourceServer lambda to tell Spring to use the OAuth2 resource server for providing user authentication and converting JWT user info into the Spring Authentication context.

At the end of the class, there are some beans for decoding access and refresh token.

Spring Security will use these beans for decoding tokens in its filter chain.

Additionally, the exception handling will use my custom class for handling token invalidation and expiration exceptions to pass custom error messages to the client.

JwtTokenDecryption class :

import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.crypto.RSADecrypter
import com.nimbusds.jose.crypto.RSASSAVerifier
import org.slf4j.LoggerFactory
import org.springframework.security.oauth2.core.OAuth2Error
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult
import org.springframework.security.oauth2.jwt.*
import java.security.PrivateKey
import java.security.interfaces.RSAPublicKey

class JwtDecryption constructor(
    private val privateKey: PrivateKey,
    private val publicKey: RSAPublicKey,
) : JwtDecoder {
    private val jwtValidator = JwtValidators.createDefault()
    private val logger = LoggerFactory.getLogger(this.javaClass)
    override fun decode(token: String): Jwt {
        try {
            val decryptedJwt = this.decryptAndVerifyJWT(token)
            return this.validateJwt(decryptedJwt)
        } catch (ex: Exception) {
            logger.error("Failed to decode token cause : ${ex.message}")
            throw BadJwtException(ex.message, ex)
        }
    }

    private fun decryptAndVerifyJWT(token: String): Jwt {
        val jweObject = JWEObject.parse(token)
        if (jweObject.state.equals(JWEObject.State.ENCRYPTED)) {
            jweObject.decrypt(RSADecrypter(privateKey));
        }
        val signedJWT = jweObject.payload.toSignedJWT()
        signedJWT.verify(RSASSAVerifier(publicKey))
        return Jwt(
            token,
            signedJWT.jwtClaimsSet.issueTime.toInstant(),
            signedJWT.jwtClaimsSet.expirationTime.toInstant(),
            signedJWT.header.toJSONObject(),
            signedJWT.jwtClaimsSet.toJSONObject()
        )
    }

    private fun validateJwt(jwt: Jwt): Jwt {
        val result: OAuth2TokenValidatorResult = this.jwtValidator.validate(jwt)
        return if (result.hasErrors()) {
            val errors = result.errors
            val validationErrorString: String = this.getJwtValidationExceptionMessage(errors)
            throw JwtValidationException(validationErrorString, errors)
        } else {
            jwt
        }
    }

    private fun getJwtValidationExceptionMessage(errors: Collection<OAuth2Error>): String {
        val var2: Iterator<*> = errors.iterator()
        var oAuth2Error: OAuth2Error
        do {
            if (!var2.hasNext()) {
                return "Unable to validate Jwt"
            }
            oAuth2Error = var2.next() as OAuth2Error
        } while (oAuth2Error.description.isEmpty())
        return oAuth2Error.description
    }
}

The class was inherited from the JwtDecoder interface of Spring Security OAuth2 packages, and Spring Security will use this decoder for decoding JWTs.

In the primary constructor of this class, we took public and private keys to decrypt JWEs and verify the actual JWT token.

AccessJwtToUserConverter class

@Component
class AccessJwtToUserConverter : Converter<Jwt, UsernamePasswordAuthenticationToken> {
    override fun convert(jwt: Jwt): UsernamePasswordAuthenticationToken? {
        val user = jwt.getClaim<String>("userInfo")
        val roles = jwt.getClaim<String>("userRoles").split(",")
        return UsernamePasswordAuthenticationToken(user, jwt, roles.map { SimpleGrantedAuthority(it) })
    }
}

In this class, we implemented the Converter interface to transform JWT claims into a UsernamePasswordAuthenticationToken object so that Spring can put it into the security context.

We also have to create a converter for refresh token :

@Component
class RefreshJwtConverter : Converter<Jwt, UsernamePasswordAuthenticationToken> {
    override fun convert(jwt: Jwt): UsernamePasswordAuthenticationToken? {
        return UsernamePasswordAuthenticationToken(UUID.fromString(jwt.subject), jwt)
    }
}

In this token, we don’t have user information, and we only have the userId that exists in the JWT subject.

The exception handling class :

class AuthExceptionHandler : AuthenticationEntryPoint {
    override fun commence(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authException: AuthenticationException,
    ) {
        response.contentType = MediaType.APPLICATION_JSON_VALUE
        response.characterEncoding = "UTF-8"
        response.status = HttpStatus.UNAUTHORIZED.value()
        val out = response.writer
        val responseJson = when (authException.message!!.contains("expired")) {
            true -> CustomException.EXPIRED_TOKEN
            false -> CustomException.INVALID_TOKEN
        }
        out.print(responseJson.toResponse().toJson())
        out.close()
        out.flush()
    }
}

Every exception during the validation of the user token will be proxied into this class.

That’s it. With these few classes, we have implemented user authentication with Spring Security 6 and OAuth2 resource server.

As you can see, this is a very simple and clean way of implementing user authentication without using any custom filter chain implementation, and all of it has been done with Spring Security.

In this article, we discussed user authentication with Spring Security and JWE token validation.

You can learn how to generate JWE tokens in this great article.

[embed]The Hard Parts of JWT Security Nobody Talks About In spite of the popularity of JWTs, their security properties are often misunderstood. To ensure the security of the…www.pingidentity.com

Thank you for your time. I hope the topic was useful to you. If you have any feedback or questions, you can find me on LinkedIn here


메타데이터
post_id
173fcc4fd970
slug
securing-your-spring-boot-with-jwe-a-guide-to-implementing-jwt-encryption-for-user-authentication-173fcc4fd970
url
https://medium.com/@parsag67/securing-your-spring-boot-with-jwe-a-guide-to-implementing-jwt-encryption-for-user-authentication-173fcc4fd970
canonical_url
https://medium.com/@parsag67/securing-your-spring-boot-with-jwe-a-guide-to-implementing-jwt-encryption-for-user-authentication-173fcc4fd970
author_url
https://medium.com/@parsag67
status
ok
fetched_at
2026-06-29 01:02:39