Skip to main content

Command Palette

Search for a command to run...

JWT authentication for Spring Boot simplified using GoTrue and Supabase

Updated

In a quest to have a simpler JWT Authentication flow and not have to deal with security-related related userdata in my backend I discovered Supabase Auth which is an implementation of Netlify GoTrue.

For Kotlin there is the awesome supabase gotrue-kt library.

In your User Registration and Login Services you need to create a GoTrueClient

val goTrueClient = GoTrueClient.defaultGoTrueClient(
    url = "<base-url>",
    headers = mapOf("Authorization" to "foo", "apiKey" to "bar")
)

If you are using supabase, the base URL will be: https://<your-project-id>.supabase.co/auth/v1

Then in your signup method you can just call signUpWithEmail().

val authDTO = goTrueClient()
    .signUpWithEmail(credentials["email"]!!, credentials["password"]!!)
websiteUserRepository.save(WebsiteUser(authDTO))

With the default client, this returns a GoTrueUserResponse which most importantly contains a id that you then can persist in a WebsiteUser Authentication Pojo which holds information related to the user

With the goTrue Kotlin Library, you can also specify a custom return type for example if you turned email confirmation off.

We define our DTO:

data class AuthDTO(
    val accessToken: String,
    val tokenType: String,
    val refreshToken: String,
    val user: User
)

data class User(
    val id: UUID,
    val email: String,
    val phone: String

)

and then create a Client where we pass this DTO:

return GoTrueClient.customApacheJacksonGoTrueClient<AuthDTO, GoTrueTokenResponse>(url,headers)

In the Login method, we call signInWithEmail and then return the JWT from the GoTrue Response as Cookie

val repsonse = goTrueClient().signInWithEmail(
  credentials["email"],
  credentials["password"]
)
response.addCookie(
    Cookie("JWT", resp.accessToken).also {
        it.secure = true
        it.isHttpOnly = true
        it.path = "/"
        it.maxAge = 6000
    }
)

But we need to verify that the JWT is correct when a User requests a page and that the user has the required access rights.

We do this in a JWT Filter that overrides the doFilterInternal method from the OncePerRequestFilter().

When our current SecurityContext Authentication is empty we need to extract the JWT from the Cookie and get the UserID from GoTrue to find the WebsiteUser we persisted earlier. We then set the SecurityContext with the retrieved WebsiteUser

@Component
class JwtFilter(
    val websiteUserRepository: WebsiteUserRepository
) : OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        if (SecurityContextHolder.getContext().authentication == null) {
            val auth = SecurityContextHolder.getContext()
            request.cookies?.find { it.name == "JWT" }?.let { cookie ->
                try {
                    goTrueClient.getUser(cookie.value).let {
                       SecurityContextHolder.getContext().authentication = websiteUserRepository.findByIdWithJPQL(it.id)
                    }
                } catch (e: GoTrueHttpException) {
                    if (e.data?.contains("Invalid token") == true) {
                        val oldCookie = request.cookies.find { it.name == "JWT" }
                        oldCookie?.maxAge = 0

                        response.addCookie(oldCookie)
                        response.sendRedirect("/")
                    }
                }
            }
        }

        filterChain.doFilter(request, response)
    }
}

At last, we add this filter in our WebSecurityConfiguration:

@Configuration
@EnableWebSecurity(debug = false)
class SpringSecurityConfig(
    val jwtFilter: JwtFilter
) : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.formLogin()
            .loginPage("/login")
            .and()
            .logout()
            .deleteCookies("JWT","authenticated")
            .logoutUrl("/logout")
            .logoutSuccessUrl("/")
            // Our private endpoints
            .antMatchers("/konto").hasRole("USER")
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
    }

If you want to learn more about HTMX + Spring Boot check out my series Web development without the JavaScript headache with Spring + HTMX.

My side business PhotoQuest is also built with HTMX + JTE

More from this blog

Thomas Schilling | Spring/HTMX/Claude Code

22 posts

Youngest Speaker @Spring I/O & Spring ViewComponent creator.

Passionate about building awesome software with Spring + HTMX. Pushing full-stack development with Spring forward.