FusionAuth / fusionauth-quickstart-java-springboot-api

Demo for using FusionAuth with a SpringBoot backend service
0 stars 0 forks source link

Quickstart doesn't provide an easy path to integration with FusionAuth react-sdk #1

Open benjaminstammen opened 1 year ago

benjaminstammen commented 1 year ago

Using Spring Boot and ReactJs together is pretty common, so I think a lot of people coming to this repository might be looking to integrate the two.

The React SDK provided by FusionAuth sets HttpOnly cookies that are automatically submitted per-request, which may work well for other server libraries, but unless I'm mistaken, parsing a JWT from a cookie in Spring is a fairly manual process and grates against the "happy path" of using an Authorization header.

It would be great if there were sample code available for this integration, or for it to be called out in the tutorial / readme (happy to contribute). Because I'm new at Javascript and decent with Kotlin, I've opted to just make to with the HttpOnly cookie while using the React SDK as is (my attempt below).

package com.benjaminstammen.bfi.security

import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.source.JWKSourceBuilder
import com.nimbusds.jose.proc.JWSKeySelector
import com.nimbusds.jose.proc.JWSVerificationKeySelector
import com.nimbusds.jose.proc.SecurityContext
import com.nimbusds.jose.proc.SimpleSecurityContext
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import com.nimbusds.jwt.proc.DefaultJWTProcessor
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.OncePerRequestFilter
import java.net.URI
import java.util.*

class JwtAuthenticationFilter : OncePerRequestFilter() {

    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
        val jwt = getJwtFromCookie(request)
        if (jwt != null && validateJwt(jwt)) {
            val authentication = getAuthentication(jwt)
            SecurityContextHolder.getContext().authentication = authentication
        }
        filterChain.doFilter(request, response)
    }

    private fun getJwtFromCookie(request: HttpServletRequest): String? {
        val cookies = request.cookies ?: return null
        val jwtCookie = cookies.firstOrNull { it.name == "app.at" }
        return jwtCookie?.value
    }

    private fun validateJwt(jwt: String): Boolean {
        // Parse the JWT
        val signedJwt = SignedJWT.parse(jwt)

        // Set up the JWK source
        val jwkSetUrl = URI("http://localhost:9011/.well-known/jwks").toURL()
        val keySource = JWKSourceBuilder
            .create<SecurityContext>(jwkSetUrl)
            .build();
        val expectedJwsAlgorithm = JWSAlgorithm.RS256;
        val keySelector: JWSKeySelector<SecurityContext> = JWSVerificationKeySelector(expectedJwsAlgorithm, keySource)

        // Create a JWT processor for the access tokens
        val jwtProcessor: DefaultJWTProcessor<SecurityContext> = DefaultJWTProcessor()
        jwtProcessor.jwsKeySelector = keySelector

        // Set the required claims that the token must contain - adjust this to match your requirements
        val securityContext = SimpleSecurityContext()
        jwtProcessor.process(signedJwt, securityContext)

        // Obtain the JWT claims...
        val claimsSet: JWTClaimsSet = signedJwt.jwtClaimsSet
        if (claimsSet.issuer != "...") return false
        if (!claimsSet.audience.contains("...")) return false
        if (claimsSet.expirationTime.before(Date())) return false

        return true // return true if valid, false otherwise
    }

    private fun getAuthentication(jwt: String): UsernamePasswordAuthenticationToken {
        // Parse the JWT to extract authorities and other credentials
        // Replace "user" and "emptyList()" with actual values after parsing the JWT
        return UsernamePasswordAuthenticationToken("user", null, emptyList())
    }
}
mooreds commented 11 months ago

Thanks for your feedback. I just updated the spring boot API to read from either the cookie or the authorization header.

You can view the java code here: https://github.com/FusionAuth/fusionauth-quickstart-java-springboot-api/blob/main/complete-application/src/main/java/io/fusionauth/quickstart/springapi/SecurityConfiguration.java

I created a custom BearerTokenResolver. I did it inline for ease of implementation, but you could definitely pull it out to another class for reuse.

mooreds commented 11 months ago

Let me know if that addresses your concerns, @benjaminstammen .